解封裝
復用器,比如MP4/FLV
解復用器,MP4/FLV
封裝格式相關函數
- avformat_alloc_context(); 負責申請一個AVFormatContext結構的內存,并進行簡單初始化
- avformat_free_context(); 釋放該結構里的所有東西以及該結構本身
- avformat_close_input();關閉解復用器。關閉后就不再需要使用avformat_free_context 進行釋放。
- avformat_open_input(); 打開輸入視頻文件
- avformat_find_stream_info(): 獲取視頻文件信息
- av_read_frame(); 讀取音視頻包
- avformat_seek_file(); 定位文件
- av_seek_frame(): 定位文件
解封裝流程
FFmpeg數據結構之間的關系
區分不同的碼流
AVMEDIA_TYPE_VIDEO
視頻流
video_index = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,-1,-1, NULL, 0)
AVMEDIA_TYPE_AUDIO
音頻流
video_index = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,-1,-1, NULL, 0)
AVPacket
里面也有一個index
的字段,這個字段存儲對應的一個流
重點
avformat_open_input
和avformat_find_stream_info
分別用于打開一個流和分析流信息。- 在初始信息不足的情況下(比如
FLV
和H264
文件),avformat_find_stream_info接口需要在內部調用read_frame_internal
接口讀取流數據(音視頻幀),然后再分析后,設置核心數據結構AVFormatContext
- 由于需要讀取數據包,avformat_find_stream_info接口會帶來很大的延遲
實現流程
添加視頻文件
-
在
build
路徑下添加相關mp4
,flv
,ts
文件
-
設置參數輸入
avformat_open_input
int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);
這個函數主要是用于打開輸入的媒體文件或流,并對相關上下文進行初始化:
-
為
AVFormatContext
結構體分配內存,然后將其地址存儲在*ps
中,后續對媒體文件的操作,如讀取數據包、查找流信息等,都要基于這個上下文。 -
- 輸入媒體文件的文件名或
URL
。可以是本地文件的路徑,例如"/path/to/your/file.mp4"
;也可以是網絡流的 URL,比如"http://example.com/stream.m3u8"
。
- 輸入媒體文件的文件名或
-
第三個參數用于指定輸入文件的格式。通常情況下設置為
NULL
,讓 FFmpeg 自動探測文件格式。不過,在某些特殊情況下,如果你明確知道文件的格式,也可以指定具體的AVInputFormat
類型。 -
最后一個參數是指向
AVDictionary
指針的指針,用于傳遞額外的選項,例如編解碼器選項、協議選項等。如果不需要傳遞額外選項,可以將其設置為NULL
。 -
輸入為mp4文件的時候,這一步可以獲取很多信息,如編解碼
ID
、媒體持續時間等
![[Pasted image 20250330142428.png|200]]
- 輸入文件為
flv
,則無法獲取較多信息,因為flv
頭部信息不足
比如這里的duration
沒有被設置,是隨機值
- 輸入為
ts
,效果和flv
類似,但信息比flv
多一點
avformat_find_stream_info
這個函數主要是用于分析輸入媒體的流信息,為上下文結構體補充信息,比如比特率等
- 在調用
avformat_open_input
打開媒體文件后,AVFormatContext
中僅包含了一些基本的文件信息,而各個流(如音頻流、視頻流、字幕流等)的詳細信息,像編碼格式、幀率、采樣率、分辨率等,往往還未被解析出來。avformat_find_stream_info
函數的主要功能就是讀取媒體文件的一定數量的數據包,對這些數據包進行分析,從而填充AVFormatContext
中各流的詳細信息。
ret = avformat_find_stream_info(ifmt_ctx, NULL);
參考下圖,執行后將bite_rate
、duration
等信息補充到了上下文中
如果一開始頭部信息不足,調用這個函數比較耗費時間,因為需要在內部讀入視頻幀進行分析
-
ts
文件
-
flv
文件
mp4
文件
可以發現,是存在延遲的,如果文件容量更大的話,延遲可能會更大
av_dump_format
這個函數將上下文信息打印出來
av_dump_format(ifmt_ctx, 0, in_filename, 0);
- 使用
avformat_open_input
后打印上下文信息
- 使用
avformat_find_stream_info
后打印信息
這里還可以將上下文結構體的內容打印出來:
// url: 調用avformat_open_input讀取到的媒體文件的路徑/名字printf("media name:%s\n", ifmt_ctx->url);// nb_streams: nb_streams媒體流數量printf("stream number:%d\n", ifmt_ctx->nb_streams);// bit_rate: 媒體文件的碼率,單位為bpsprintf("media average ratio:%lldkbps\n",(int64_t)(ifmt_ctx->bit_rate/1024));// 時間int total_seconds, hour, minute, second;// duration: 媒體文件時長,單位微妙total_seconds = (ifmt_ctx->duration) / AV_TIME_BASE; // 1000us = 1ms, 1000ms = 1秒hour = total_seconds / 3600;minute = (total_seconds % 3600) / 60;second = (total_seconds % 60);//通過上述運算,可以得到媒體文件的總時長printf("total duration: %02d:%02d:%02d\n", hour, minute, second);printf("\n");
注意這里的duration
為媒體總時長,單位為微秒,因此轉換為秒需要除以1e6
,AV_TIME_BASE
宏對應就是1e6
![[Pasted image 20250330145622.png|400]]
獲取相應流
可以通過遍歷上下文中對應的二維流數組來找到自己想要的流,比如音頻流和視頻流
AVStream
是 FFmpeg 庫中一個關鍵的結構體,主要用于描述媒體文件中的一個流(例如視頻流、音頻流、字幕流等),結構體內存儲了很多關于流的信息:
AVRational time_base
:AVRational
是一個表示分數的結構體,time_base
定義了流中時間戳的基本單位。時間戳(如 PTS
和 DTS
)是以 time_base
為單位的。例如,若 time_base
為 {1, 1000}
,則表示時間戳的單位是 1/1000 秒。在進行時間戳的轉換和計算時,需要使用這個 time_base
。AVCodecParameters
結構體包含了流的編解碼器相關的參數信息,如視頻流的分辨率、幀率、像素格式,音頻流的采樣率、聲道數、采樣格式等。這些信息對于選擇合適的解碼器以及進行解碼操作非常重要。
1. int index
此成員表示該流在 AVFormatContext
的 streams
數組中的索引。在處理多個流的媒體文件時,可以通過這個索引來區分不同的流。例如,在讀取數據包時,AVPacket
結構體中的 stream_index
就會指向這個索引,以此確定該數據包屬于哪個流。
2. AVRational time_base
AVRational
是一個表示分數的結構體,time_base
定義了流中時間戳的基本單位。時間戳(如 PTS
和 DTS
)是以 time_base
為單位的。例如,若 time_base
為 {1, 1000}
,則表示時間戳的單位是 1/1000 秒。在進行時間戳的轉換和計算時,需要使用這個 time_base
。
3. AVCodecParameters *codecpar
AVCodecParameters
結構體包含了流的編解碼器相關的參數信息,如視頻流的分辨率、幀率、像素格式,音頻流的采樣率、聲道數、采樣格式等。這些信息對于選擇合適的解碼器以及進行解碼操作非常重要。
4. int64_t duration
該成員表示流的總時長,單位是 time_base
。要將其轉換為秒,可以使用以下公式:duration_in_seconds = (double)duration * av_q2d(time_base)
。
5. int64_t start_time
表示流的起始時間,單位同樣是 time_base
。在某些情況下,流的起始時間可能不是從 0 開始的,通過這個成員可以獲取到流實際的起始時間。
6. AVRational avg_frame_rate
和 AVRational r_frame_rate
avg_frame_rate
表示流的平均幀率,是根據流中所有幀的時間間隔計算得出的平均幀率。r_frame_rate
表示流的實際幀率,通常是固定的幀率,對于一些固定幀率的視頻流,這個值會比較準確。
7. void *priv_data
這是一個指向私有數據的指針,用于存儲特定格式的額外信息。不同的格式可能會使用這個指針來存儲一些自定義的數據,一般情況下不需要直接操作這個指針。
在我們的示例中,通過遍歷上下文所有的流,每個流都有唯一對應的流索引,因此可以通過流中的編解碼參數信息,打印出相應的音視頻格式:
- 獲取當前索引的流結構體
AVStream *in_stream = ifmt_ctx->streams[i];// 音頻流、視頻流、字幕流
- 通過編解碼器的參數獲取編解碼類型,返回相應的類型宏定義
- 音頻
MEDIA_TYPE_AUDIO
if (AVMEDIA_TYPE_AUDIO == in_stream->codecpar->codec_type)
- 視頻
MEDIA_TYPE_VIDEO
else if (AVMEDIA_TYPE_VIDEO == in_stream->codecpar->codec_type)
- 如果是音頻,可以打印出相關的音頻信息,如采樣率、采樣格式(如
FLTP
、S16P
)、通道數、壓縮格式(如AAC
、MP3
)、音頻總時長等
printf("----- Audio info:\n");// index: 每個流成分在ffmpeg解復用分析后都有唯一的index作為標識printf("index:%d\n", in_stream->index);// sample_rate: 音頻編解碼器的采樣率,單位為Hzprintf("samplerate:%dHz\n", in_stream->codecpar->sample_rate);// codecpar->format: 音頻采樣格式if (AV_SAMPLE_FMT_FLTP == in_stream->codecpar->format){printf("sampleformat:AV_SAMPLE_FMT_FLTP\n");}else if (AV_SAMPLE_FMT_S16P == in_stream->codecpar->format){printf("sampleformat:AV_SAMPLE_FMT_S16P\n");}// channels: 音頻信道數目printf("channel number:%d\n", in_stream->codecpar->channels);// codec_id: 音頻壓縮編碼格式if (AV_CODEC_ID_AAC == in_stream->codecpar->codec_id){printf("audio codec:AAC\n");}else if (AV_CODEC_ID_MP3 == in_stream->codecpar->codec_id){printf("audio codec:MP3\n");}else{printf("audio codec_id:%d\n", in_stream->codecpar->codec_id);}// 音頻總時長,單位為秒。注意如果把單位放大為毫秒或者微秒,音頻總時長跟視頻總時長不一定相等的if(in_stream->duration != AV_NOPTS_VALUE){int duration_audio = (in_stream->duration) * av_q2d(in_stream->time_base);//將音頻總時長轉換為時分秒的格式打印到控制臺上printf("audio duration: %02d:%02d:%02d\n",duration_audio / 3600, (duration_audio % 3600) / 60, (duration_audio % 60));}else{printf("audio duration unknown");}printf("\n");
- 注意,在計算音頻時長的時候,
AVStream
中的duration
和上下文AVFormatContext
中的單位不一樣,這里的單位是時間基time_base
,不同的媒體文件可能時間基不同,比如可能是1/1000 s
作為一個時間基,那么我們轉換為妙就需要如下操作
s = A V S t r e a m ? > d u r a t i o n ? a v _ q 2 d ( A V S t r e a m ? > t i m e _ b a s e ) s = AVStream->duration*av\_q2d(AVStream->time\_base) s=AVStream?>duration?av_q2d(AVStream?>time_base)
這里的av_q2d
實際上就是將分數形式轉換為double
類型的小數形式,因此轉換實質上上就是:duration
* time_base
- 如果是視頻,同樣可以提取出視頻編解碼器的信息,比如視頻幀率(
FPS
)、視頻壓縮編碼格式(H264
、MPEG4
)、視頻幀的寬高(1080x720
),轉換視頻的持續時間的方式與音頻一樣,注意,time_base
的值通常不同:
視頻
- 典型值:
{1, 25}
(25
FPS
)、{1, 30}
(30
FPS
)、{1, 90000}
(精確時間基) - 含義:視頻幀的時間間隔以幀率倒數為單位。
音頻
- 典型值:
{1, 44100}
(44.1kHz
采樣率)、{1, 48000}
(48kHz
采樣率) - 含義:音頻幀的時間間隔以采樣周期為單位
printf("----- Video info:\n");printf("index:%d\n", in_stream->index);// avg_frame_rate: 視頻幀率,單位為fps,表示每秒出現多少幀printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));if (AV_CODEC_ID_MPEG4 == in_stream->codecpar->codec_id) //視頻壓縮編碼格式{printf("video codec:MPEG4\n");}else if (AV_CODEC_ID_H264 == in_stream->codecpar->codec_id) //視頻壓縮編碼格式{printf("video codec:H264\n");}else{printf("video codec_id:%d\n", in_stream->codecpar->codec_id);}// 視頻幀寬度和幀高度printf("width:%d height:%d\n", in_stream->codecpar->width,in_stream->codecpar->height);//視頻總時長,單位為秒。注意如果把單位放大為毫秒或者微秒,音頻總時長跟視頻總時長不一定相等的if(in_stream->duration != AV_NOPTS_VALUE){int duration_video = (in_stream->duration) * av_q2d(in_stream->time_base);printf("video duration: %02d:%02d:%02d\n",duration_video / 3600,(duration_video % 3600) / 60,(duration_video % 60)); //將視頻總時長轉換為時分秒的格式打印到控制臺上}else{printf("video duration unknown");}printf("\n");
獲取相應包(Packet
)
上下文中還存儲了壓縮的數據包,比如對應的H264
、AAC
壓縮包,我們可以讀取這些壓縮包
- 首先我們需要為
AVPacket
結構體分配內存
AVPacket *pkt = av_packet_alloc();
- 通過一個循環來依次讀取每一幀的數據包到
AVPacket
中,每次讀取一幀后,內部的指針都會向后移動
while (1){ret = av_read_frame(ifmt_ctx, pkt);}
- 判斷數據包內的流索引(視頻流、音頻流),進行相應操作,如打印
pts
、dts
、包的大小size
、包對應文件的偏移量pos
,以及根據不同的索引在不同AVStream
中找到對應的當前幀的持續時間,如下
- 音頻幀數據包持續時間
pkt->duration * av_q2d(ifmt_ctx->streams[audioindex]->time_base)
- 視頻幀數據包持續時間
pkt->duration * av_q2d(ifmt_ctx->streams[videoindex]->time_base)
- 解碼完當前幀數據包后,需要將這一幀數據包釋放,否則會導致內存泄漏,直接調用
av_packet_unref
減少引用計數即可,引用計數為0
會自動釋放幀數據包的buf
內存
av_packet_unref(pkt);
- 讀取所有幀數據包之后,需要釋放
AVPacket
結構體的內存
if(pkt)av_packet_free(&pkt);
釋放內存
所有操作之后,需要釋放上下文內存,并且關閉打開的文件或關閉對應網絡流的連接
調用 avformat_close_input
函數即可實現上述功能
if(ifmt_ctx)avformat_close_input(&ifmt_ctx);
整體代碼
main.c
#include <stdio.h>
#include <libavformat/avformat.h>
#include<time.h>int main(int argc, char **argv)
{//打開網絡流。這里如果只需要讀取本地媒體文件,不需要用到網絡功能,可以不用加上這一句
// avformat_network_init();const char *default_filename = "believe.mp4";char *in_filename = NULL;if(argv[1] == NULL){in_filename = default_filename;}else{in_filename = argv[1];}printf("in_filename = %s\n", in_filename);//AVFormatContext是描述一個媒體文件或媒體流的構成和基本信息的結構體AVFormatContext *ifmt_ctx = NULL; // 輸入文件的demuxint videoindex = -1; // 視頻索引int audioindex = -1; // 音頻索引// 打開文件,主要是探測協議類型,如果是網絡文件則創建網絡鏈接int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);if (ret < 0) //如果打開媒體文件失敗,打印失敗原因{char buf[1024] = { 0 };av_strerror(ret, buf, sizeof(buf) - 1);printf("open %s failed:%s\n", in_filename, buf);goto failed;}printf_s("\n==== av_dump_format in_filename:%s ===\n", in_filename);av_dump_format(ifmt_ctx, 0, in_filename, 0);printf_s("\n==== av_dump_format finish =======\n\n");clock_t started = clock();ret = avformat_find_stream_info(ifmt_ctx, NULL);clock_t ended = clock();double elapsed_time = (double)(ended - started) / CLOCKS_PER_SEC;printf("avformat_find_stream_info took %f seconds to execute.\n", elapsed_time);if (ret < 0) //如果打開媒體文件失敗,打印失敗原因{char buf[1024] = { 0 };av_strerror(ret, buf, sizeof(buf) - 1);printf("avformat_find_stream_info %s failed:%s\n", in_filename, buf);goto failed;}//打開媒體文件成功printf_s("\n==== av_dump_format in_filename:%s ===\n", in_filename);av_dump_format(ifmt_ctx, 0, in_filename, 0);printf_s("\n==== av_dump_format finish =======\n\n");// url: 調用avformat_open_input讀取到的媒體文件的路徑/名字printf("media name:%s\n", ifmt_ctx->url);// nb_streams: nb_streams媒體流數量printf("stream number:%d\n", ifmt_ctx->nb_streams);// bit_rate: 媒體文件的碼率,單位為bpsprintf("media average ratio:%lldkbps\n",(int64_t)(ifmt_ctx->bit_rate/1024));// 時間int total_seconds, hour, minute, second;// duration: 媒體文件時長,單位微妙total_seconds = (ifmt_ctx->duration) / AV_TIME_BASE; // 1000us = 1ms, 1000ms = 1秒hour = total_seconds / 3600;minute = (total_seconds % 3600) / 60;second = (total_seconds % 60);//通過上述運算,可以得到媒體文件的總時長printf("total duration: %02d:%02d:%02d\n", hour, minute, second);printf("\n");/** 老版本通過遍歷的方式讀取媒體文件視頻和音頻的信息* 新版本的FFmpeg新增加了函數av_find_best_stream,也可以取得同樣的效果*/for (uint32_t i = 0; i < ifmt_ctx->nb_streams; i++){AVStream *in_stream = ifmt_ctx->streams[i];// 音頻流、視頻流、字幕流//如果是音頻流,則打印音頻的信息if (AVMEDIA_TYPE_AUDIO == in_stream->codecpar->codec_type){printf("----- Audio info:\n");// index: 每個流成分在ffmpeg解復用分析后都有唯一的index作為標識printf("index:%d\n", in_stream->index);// sample_rate: 音頻編解碼器的采樣率,單位為Hzprintf("samplerate:%dHz\n", in_stream->codecpar->sample_rate);// codecpar->format: 音頻采樣格式if (AV_SAMPLE_FMT_FLTP == in_stream->codecpar->format){printf("sampleformat:AV_SAMPLE_FMT_FLTP\n");}else if (AV_SAMPLE_FMT_S16P == in_stream->codecpar->format){printf("sampleformat:AV_SAMPLE_FMT_S16P\n");}// channels: 音頻信道數目printf("channel number:%d\n", in_stream->codecpar->channels);// codec_id: 音頻壓縮編碼格式if (AV_CODEC_ID_AAC == in_stream->codecpar->codec_id){printf("audio codec:AAC\n");}else if (AV_CODEC_ID_MP3 == in_stream->codecpar->codec_id){printf("audio codec:MP3\n");}else{printf("audio codec_id:%d\n", in_stream->codecpar->codec_id);}// 音頻總時長,單位為秒。注意如果把單位放大為毫秒或者微秒,音頻總時長跟視頻總時長不一定相等的if(in_stream->duration != AV_NOPTS_VALUE){int duration_audio = (in_stream->duration) * av_q2d(in_stream->time_base);//將音頻總時長轉換為時分秒的格式打印到控制臺上printf("audio duration: %02d:%02d:%02d\n",duration_audio / 3600, (duration_audio % 3600) / 60, (duration_audio % 60));}else{printf("audio duration unknown");}printf("\n");audioindex = i; // 獲取音頻的索引}else if (AVMEDIA_TYPE_VIDEO == in_stream->codecpar->codec_type) //如果是視頻流,則打印視頻的信息{printf("----- Video info:\n");printf("index:%d\n", in_stream->index);// avg_frame_rate: 視頻幀率,單位為fps,表示每秒出現多少幀printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));if (AV_CODEC_ID_MPEG4 == in_stream->codecpar->codec_id) //視頻壓縮編碼格式{printf("video codec:MPEG4\n");}else if (AV_CODEC_ID_H264 == in_stream->codecpar->codec_id) //視頻壓縮編碼格式{printf("video codec:H264\n");}else{printf("video codec_id:%d\n", in_stream->codecpar->codec_id);}// 視頻幀寬度和幀高度printf("width:%d height:%d\n", in_stream->codecpar->width,in_stream->codecpar->height);//視頻總時長,單位為秒。注意如果把單位放大為毫秒或者微秒,音頻總時長跟視頻總時長不一定相等的if(in_stream->duration != AV_NOPTS_VALUE){int duration_video = (in_stream->duration) * av_q2d(in_stream->time_base);printf("video duration: %02d:%02d:%02d\n",duration_video / 3600,(duration_video % 3600) / 60,(duration_video % 60)); //將視頻總時長轉換為時分秒的格式打印到控制臺上}else{printf("video duration unknown");}printf("\n");videoindex = i;}}AVPacket *pkt = av_packet_alloc();int pkt_count = 0;int print_max_count = 10;printf("\n-----av_read_frame start\n");while (1){ret = av_read_frame(ifmt_ctx, pkt);if (ret < 0){printf("av_read_frame end\n");break;}if(pkt_count++ < print_max_count){if (pkt->stream_index == audioindex){printf("audio pts: %lld\n", pkt->pts);printf("audio dts: %lld\n", pkt->dts);printf("audio size: %d\n", pkt->size);printf("audio pos: %lld\n", pkt->pos);printf("audio duration: %lf\n\n",pkt->duration * av_q2d(ifmt_ctx->streams[audioindex]->time_base));}else if (pkt->stream_index == videoindex){printf("video pts: %lld\n", pkt->pts);printf("video dts: %lld\n", pkt->dts);printf("video size: %d\n", pkt->size);printf("video pos: %lld\n", pkt->pos);printf("video duration: %lf\n\n",pkt->duration * av_q2d(ifmt_ctx->streams[videoindex]->time_base));}else{printf("unknown stream_index:\n", pkt->stream_index);}}av_packet_unref(pkt);}if(pkt)av_packet_free(&pkt);
failed:if(ifmt_ctx)avformat_close_input(&ifmt_ctx);getchar(); //加上這一句,防止程序打印完信息馬上退出return 0;
}
更多資料:https://github.com/0voice