ffmpeg architecture(中)
ffmpeg architecture(中)
艱苦學習FFmpeg libav
您是否不奇怪有時會發(fā)出聲音和視覺?
由于FFmpeg作為命令行工具非常有用,可以對媒體文件執(zhí)行基本任務,因此如何在程序中使用它?
FFmpeg 由幾個庫組成,這些庫可以集成到我們自己的程序中。通常,當您安裝FFmpeg時,它將自動安裝所有這些庫。我將這些庫的集合稱為FFmpeg libav。
此標題是對Zed Shaw的系列“ Learn X the Hard Way”(特別是他的書“ Learn C the Hard Way” )的致敬。
第0章-臭名昭著的你好世界
您好世界實際上不會"hello world"在終端中顯示消息👅 相反,我們將打印出有關視頻的信息,例如其格式(容器),時長,分辨率,音頻通道之類的信息,最后,我們將解碼一些幀并將其保存為圖像文件。
FFmpeg libav體系結構
但是在開始編碼之前,讓我們學習FFmpeg libav架構如何工作以及其組件如何與其他組件通信。
這是解碼視頻的過程:
首先,您需要將媒體文件加載到名為AVFormatContext(視頻容器也稱為格式)的組件中。實際上,它并未完全加載整個文件:它通常僅讀取標頭。
加載容器的最小標頭后,就可以訪問其流(將其視為基本的音頻和視頻數(shù)據(jù))。每個流都可以在名為的組件中使用AVStream。
流是連續(xù)數(shù)據(jù)流的奇特名稱。
假設我們的視頻有兩個流:用AAC CODEC編碼的音頻和用H264(AVC)CODEC編碼的視頻。從每個流中,我們可以提取稱為數(shù)據(jù)包的數(shù)據(jù)片段(切片),這些數(shù)據(jù)將加載到名為的組件中AVPacket。
該包內(nèi)的數(shù)據(jù)仍然編碼(壓縮),并以數(shù)據(jù)包進行解碼,我們需要將它們傳遞給特定的AVCodec。
在AVCodec將它們解碼成AVFrame最后,該組件為我們提供了非壓縮幀。注意,音頻和視頻流使用相同的術語/過程。
要求
由于有些人在編譯或運行 我們將Docker用作開發(fā)/ 運行器環(huán)境的示例時遇到問題,因此,我們還將使用大型的兔子視頻,因此,如果您在本地沒有該視頻,請運行命令make fetch_small_bunny_video。
第0章-代碼演練
TLDR;給我看代碼和執(zhí)行。
$ make run_hello
我們將跳過一些細節(jié),但是請放心:源代碼可在github上找到。
我們將分配內(nèi)存給AVFormatContext將保存有關格式(容器)信息的組件。
AVFormatContext * pFormatContext = avformat_alloc_context();
現(xiàn)在,我們將打開文件并讀取其標頭,并AVFormatContext使用有關該格式的最少信息填充(注意,通常不會打開編解碼器)。用于執(zhí)行此操作的函數(shù)是avformat_open_input。它需要一個AVFormatContext,一個filename和兩個可選參數(shù):(AVInputFormat如果通過NULL,則FFmpeg會猜測格式)和AVDictionary(這是解復用器的選項)。
avformat_open_input(&pFormatContext,filename,NULL,NULL);
我們可以打印格式名稱和媒體持續(xù)時間:
printf(“格式%s,持續(xù)時間%lld us ”,pFormatContext-> iformat-> long_name,pFormatContext-> duration);
要訪問streams,我們需要從媒體讀取數(shù)據(jù)。該功能可以avformat_find_stream_info做到這一點。現(xiàn)在,pFormatContext->nb_streams將保留流的數(shù)量,并且pFormatContext->streams[i]將為我們提供i流(an AVStream)。
avformat_find_stream_info(pFormatContext, NULL);
現(xiàn)在,我們將遍歷所有流。
對于(int i = 0 ; i nb_streams; i ++){ // }
對于每個流,我們將保留AVCodecParameters,它描述了該流使用的編解碼器的屬性i。
AVCodecParameters * pLocalCodecParameters = pFormatContext-> streams [i]-> codecpar;
隨著編解碼器的屬性,我們可以看一下正確的CODEC查詢功能avcodec_find_decoder,并找到注冊解碼器編解碼器ID并返回AVCodec,知道如何連接部件有限公司德和DEC ODE流。
AVCodec * pLocalCodec = avcodec_find_decoder(pLocalCodecParameters-> codec_id);
現(xiàn)在我們可以打印有關編解碼器的信息。
//特定視頻和音頻如果(pLocalCodecParameters-> codec_type == AVMEDIA_TYPE_VIDEO){ printf的( “視頻編解碼器:分辨率%d X %d ”,pLocalCodecParameters->寬度,pLocalCodecParameters->高度);} 否則 如果(pLocalCodecParameters-> codec_type == AVMEDIA_TYPE_AUDIO){ printf的(“音頻編解碼器:%d通道,采樣率%d ”,pLocalCodecParameters-> 通道,pLocalCodecParameters-> SAMPLE_RATE);}// //常規(guī)printf( “ \ t編解碼器%s ID %d bit_rate %lld ”,pLocalCodec-> long_name,pLocalCodec-> id,pCodecParameters-> bit_rate);
使用編解碼器,我們可以為分配內(nèi)存,該內(nèi)存AVCodecContext將保存我們的解碼/編碼過程的上下文,但是隨后我們需要使用CODEC參數(shù)填充此編解碼器上下文;我們這樣做avcodec_parameters_to_context。
填充編解碼器上下文后,我們需要打開編解碼器。我們調(diào)用該函數(shù)avcodec_open2,然后就可以使用它了。
AVCodecContext * pCodecContext = avcodec_alloc_context3(pCodec);avcodec_parameters_to_context(pCodecContext,pCodecParameters);avcodec_open2(pCodecContext,pCodec,NULL);
現(xiàn)在,我們打算從流中讀取數(shù)據(jù)包,并將其解碼為幀,但首先,我們需要為這兩個組件的分配內(nèi)存AVPacket和AVFrame。
AVPacket * pPacket = av_packet_alloc();AVFrame * pFrame = av_frame_alloc();
讓我們在函數(shù)av_read_frame有數(shù)據(jù)包時從流中提供數(shù)據(jù)包。
while(av_read_frame(pFormatContext,pPacket)> = 0){ // … }
讓我們使用函數(shù)通過編解碼器上下文將原始數(shù)據(jù)包(壓縮幀)發(fā)送到解碼器avcodec_send_packet。
avcodec_send_packet(pCodecContext,pPacket);
然后,我們使用function通過相同的編解碼器上下文從解碼器接收原始數(shù)據(jù)幀(未壓縮的幀)avcodec_receive_frame。
avcodec_receive_frame(pCodecContext,pFrame);
我們可以打印幀號,PTS,DTS,幀類型等。
printf( “幀%c(%d)點%d dts %d key_frame %d [coded_picture_number %d,display_picture_number %d ] ”, av_get_picture_type_char(pFrame-> pict_type), pCodecContext-> frame_number, pFrame-> pts, pFrame-> pkt_dts, pFrame-> key_frame, pFrame-> coded_picture_number, pFrame-> display_picture_number);
最后,我們可以將解碼后的幀保存為簡單的灰度圖像。該過程非常簡單,我們將使用pFrame->data索引與平面Y,Cb和Cr相關的位置,我們剛剛選擇0(Y)保存灰度圖像。
save_gray_frame(pFrame-> data [ 0 ],pFrame-> linesize [ 0 ],pFrame-> width,pFrame-> height,frame_filename); static void save_gray_frame(unsigned char * buf,int wrap,int xsize,int ysize,char * filename){ 文件 * f; 詮釋 I; f = fopen(文件名,“ w ”); //編寫pgm文件格式所需的最小標頭 //便攜式灰度圖格式-> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example fprintf(f,“ P5 \ n %d %d \ n %d \ n “,xsize,ysize,255); // 為(i = 0 ; i <ysize; i ++) 逐行編寫fwrite(buf + i * wrap, 1,xsize,f); fclose(f);}
第1章-同步音頻和視頻
成為播放器 -一個年輕的JS開發(fā)人員,編寫新的MSE視頻播放器。
在開始編寫轉碼示例代碼之前,我們先談一下定時,或者視頻播放器如何知道正確的時間播放幀。
在上一個示例中,我們保存了一些可以在此處看到的幀:
在設計視頻播放器時,我們需要以給定的速度播放每一幀,否則,由于播放的速度太快或太慢,很難令人愉快地觀看視頻。
因此,我們需要引入一些邏輯來平穩(wěn)地播放每個幀。為此,每個幀具有表示時間戳(PTS),其是在時基中分解的遞增數(shù)字,該時基是可被幀速率(fps)整除的有理數(shù)(其中分母稱為時間標度)。
當我們看一些示例時,更容易理解,讓我們模擬一些場景。
對于fps=60/1,timebase=1/60000每個PTS都會增加,timescale / fps = 1000因此每個幀的PTS實時可能是(假設從0開始):
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033
對于幾乎相同的情況,但時基等于1/60。
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050
對于fps=25/1和timebase=1/75每個PTS將增加timescale / fps = 3和PTS時間可能是:
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
…
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
…
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56
現(xiàn)在,借助,pts_time我們可以找到一種方法來呈現(xiàn)與音頻pts_time或系統(tǒng)時鐘同步的同步。FFmpeg libav通過其API提供以下信息:
fps
= AVStream->avg_frame_rate
tbr
= AVStream->r_frame_rate
tbn
= AVStream->time_base
出于好奇,我們保存的幀以DTS順序發(fā)送(幀:1、6、4、2、3、5),但以PTS順序播放(幀:1、2、3、4、5)。另外,請注意,B幀與P幀或I幀相比價格便宜。
LOG: AVStream->r_frame_rate 60/1LOG: AVStream->time_base 1/60000…LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]
第2章-重新混合
重塑是將一種格式(容器)更改為另一種格式的行為,例如,我們可以使用FFmpeg 輕松地將MPEG-4視頻更改為MPEG-TS:
ffmpeg input.mp4 -c復制output.ts
它將對mp4進行解復用,但不會對其進行解碼或編碼(-c copy),最后,會將其復用為mpegts文件。如果您不提供格式,-f則ffmpeg會嘗試根據(jù)文件擴展名猜測它。
FFmpeg或libav的一般用法遵循模式/體系結構或工作流程:
協(xié)議層 -接受input(file例如,但也可以是rtmp或HTTP輸入)
格式層 -它demuxes的內(nèi)容,主要顯示元數(shù)據(jù)及其流
編解碼器層 -decodes壓縮流數(shù)據(jù)可選
像素層 -也可以將其應用于filters原始幀(如調(diào)整大小)可選
然后它做反向路徑
編解碼器層 -它encodes(或re-encodes什至transcodes)原始幀是可選的
格式層 -它muxes(或remuxes)原始流(壓縮數(shù)據(jù))
協(xié)議層 -最終將多路復用的數(shù)據(jù)發(fā)送到output(另一個文件或網(wǎng)絡遠程服務器)
此圖受到雷小華和Slhck的作品的強烈啟發(fā)。
現(xiàn)在,讓我們使用libav編寫示例,以提供與中相同的效果ffmpeg input.mp4 -c copy output.ts。
我們將從一個輸入(input_format_context)讀取并將其更改為另一個輸出(output_format_context)。
AVFormatContext * input_format_context = NULL ;AVFormatContext * output_format_context = NULL ;
我們開始進行通常的分配內(nèi)存并打開輸入格式。對于這種特定情況,我們將打開一個輸入文件并為輸出文件分配內(nèi)存。
if((ret = avformat_open_input(&input_format_context,in_filename,NULL,NULL))< 0){ fprintf(stderr,“無法打開輸入文件’ %s ’ ”,in_filename); 轉到結尾}if((ret = avformat_find_stream_info(input_format_context,NULL))< 0){ fprintf(stderr,“無法檢索輸入流信息”); 轉到結尾} avformat_alloc_output_context2(&output_format_context,NULL,NULL,out_filename);if(!output_format_context){ fprintf(stderr,“無法創(chuàng)建輸出上下文\ n ”); ret = AVERROR_UNKNOWN; 轉到結尾}
我們將只重新混合流的視頻,音頻和字幕類型,因此我們將要使用的流保留到索引數(shù)組中。
number_of_streams = input_format_context-> nb_streams;stream_list = av_mallocz_array(stream_numbers,sizeof(* streams_list));
分配完所需的內(nèi)存后,我們將遍歷所有流,并需要使用avformat_new_stream函數(shù)為每個流在輸出格式上下文中創(chuàng)建新的輸出流。請注意,我們標記的不是視頻,音頻或字幕的所有流,因此我們可以在以后跳過它們。
對于(i = 0 ; i <input_format_context-> nb_streams; i ++){ AVStream * out_stream; AVStream * in_stream = input_format_context-> 流 [i]; AVCodecParameters * in_codecpar = in_stream-> codecpar ; 如果(in_codecpar-> codec_type!= AVMEDIA_TYPE_AUDIO && in_codecpar-> codec_type!= AVMEDIA_TYPE_VIDEO && in_codecpar-> codec_type!= AVMEDIA_TYPE_SUBTITLE){ stream_list [i] = -1 ; 繼續(xù) ; } stream_list [i] = stream_index ++; out_stream = avformat_new_stream(output_format_context,NULL); if(!out_stream){ fprintf(stderr,“無法分配輸出流\ n ”); ret = AVERROR_UNKNOWN; 轉到結尾 } ret = avcodec_parameters_copy(out_stream-> codecpar,in_codecpar); if(ret < 0){ fprintf(stderr,“復制編解碼器參數(shù)失敗\ n ”); 轉到結尾 }}
現(xiàn)在我們可以創(chuàng)建輸出文件了。
如果(!(output_format_context-> oformat-> flags和AVFMT_NOFILE)){ ret = avio_open(&output_format_context-> pb,out_filename,AVIO_FLAG_WRITE); if(ret < 0){ fprintf(stderr,“無法打開輸出文件’ %s ’ ”,out_filename); 轉到結尾 }} ret = avformat_write_header(output_format_context,NULL);if(ret < 0){ fprintf(stderr,“打開輸出文件時發(fā)生錯誤\ n ”); 轉到結尾}
之后,我們可以逐個數(shù)據(jù)包地將流從輸入復制到輸出流。我們將在它有數(shù)據(jù)包(av_read_frame)時循環(huán)播放,對于每個數(shù)據(jù)包,我們需要重新計算PTS和DTS以最終將其(av_interleaved_write_frame)寫入輸出格式上下文。
而(1){ AVStream * in_stream,* out_stream; ret = av_read_frame(input_format_context,&packet); 如果(ret < 0) 中斷 ; in_stream = input_format_context-> 流 [數(shù)據(jù)包。stream_index ]; 如果(分組。stream_index > = number_of_streams || streams_list [數(shù)據(jù)包。stream_index ] < 0){ av_packet_unref(包); 繼續(xù) ; } 包。stream_index = stream_list [數(shù)據(jù)包。stream_index ]; out_stream = output_format_context-> 流 [數(shù)據(jù)包。stream_index ]; / 復制數(shù)據(jù)包 / 數(shù)據(jù)包。pts = av_rescale_q_rnd(數(shù)據(jù)包pts,in_stream-> time_base,out_stream-> time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); 包。dts = av_rescale_q_rnd(數(shù)據(jù)包dts,in_stream-> time_base,out_stream-> time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); 包。持續(xù)時間 = av_rescale_q(數(shù)據(jù)包duration,in_stream-> time_base,out_stream-> time_base); // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903 數(shù)據(jù)包。pos = -1 ; // https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1 ret = av_interleaved_write_frame(output_format_context,&packet); if(ret < 0){ fprintf(stderr, “錯誤合并數(shù)據(jù)包\ n ”); 休息 ; } av_packet_unref(&packet);}
最后,我們需要使用av_write_trailer函數(shù)將流預告片寫入輸出媒體文件。
av_write_trailer(output_format_context);
現(xiàn)在我們準備對其進行測試,并且第一個測試將是從MP4到MPEG-TS視頻文件的格式(視頻容器)轉換。我們基本上是ffmpeg input.mp4 -c
copy output.ts使用libav 制作命令行。
使run_remuxing_ts
工作正常!!!可以通過以下方法進行檢查ffprobe:
ffprobe -i remuxed_small_bunny_1080p_60fps.ts從’remuxed_small_bunny_1080p_60fps.ts’ 輸入# 0,mpegts: 持續(xù)時間:00:00:10.03,開始:0.000000,比特率:2751 kb / s 程序1 元數(shù)據(jù): service_name :服務 01 service_provider:FFmpeg 流# 0:0 [0x100]:視頻:h264(高)([27] [0] [0] [0] / 0x001B),yuv420p(逐行),1920x1080 [SAR 1:1 DAR 16:9],60 fps,60 tbr,90k tbn,120 tbc 流# 0:1 [0x101]:音頻:ac3([129] [0] [0] [0] / 0x0081),48000 Hz,5.1(側面),fltp,320 kb /秒
總結一下我們在圖中所做的事情,我們可以回顧一下關于libav如何工作的最初想法,但表明我們跳過了編解碼器部分。
在結束本章之前,我想展示重混合過程的重要部分,您可以將選項傳遞給多路復用器。假設我們要為此提供MPEG-DASH格式,我們需要使用分段的mp4(有時稱為fmp4)代替MPEG-TS或純MPEG-4。
使用命令行,我們可以輕松地做到這一點。
ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4
由于命令行是libav版本,因此幾乎同樣容易,我們只需要在復制數(shù)據(jù)包之前在寫入輸出標頭時傳遞選項即可。
AVDictionary * opts = NULL ;av_dict_set(&opts,“ movflags ”,“ frag_keyframe + empty_moov + default_base_moof ”,0);ret = avformat_write_header(output_format_context,&opts);
現(xiàn)在,我們可以生成此分段的mp4文件:
制作run_remuxing_fragmented_mp4
但是要確保我沒有對你說謊。您可以使用令人驚嘆的site / tool gpac / mp4box.js或網(wǎng)站http://mp4parser.com/來查看差異,首先加載“常用” mp4。
如您所見,它只有一個mdat原子/盒子,這是視頻和音頻幀所在的位置。現(xiàn)在加載零碎的mp4,以查看它如何散布mdat盒子。
總結
以上是生活随笔為你收集整理的ffmpeg architecture(中)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ffmpeg architecture(
- 下一篇: ffmpeg architecture(