本文轉(zhuǎn)載自Github上面殷汶杰-yinwenjie的一個(gè)項(xiàng)目FFmpeg_Tutorial,關(guān)于FFMPEG的學(xué)習(xí)可以參考雷霄驊的CSDN博客https://blog.csdn.net/leixiaohua1020外潜,其Github主頁為:https://github.com/leixiaohua1020原环,里面有不少關(guān)于FFMPEG音視頻開發(fā)方面的資料。他有篇開篇關(guān)于[總結(jié)]FFMPEG視音頻編解碼零基礎(chǔ)學(xué)習(xí)方法处窥,地址為:https://blog.csdn.net/leixiaohua1020/article/details/15811977嘱吗。還有夏曹俊老師的FFMPEG中文網(wǎng)站https://ffmpeg.club/
FFmpeg_Tutorial
FFmpeg工具和sdk庫的使用demo
一、使用FFmpeg命令行工具和批處理腳本進(jìn)行簡單的音視頻文件編輯
1滔驾、基本介紹
對于每一個(gè)從事音視頻技術(shù)開發(fā)的工程師谒麦,想必沒有一個(gè)人對FFmpeg這個(gè)名稱感到陌生。FFmpeg是一套非常知名的音視頻處理的開源工具哆致,它包含了開發(fā)完成的工具軟件绕德、封裝好的函數(shù)庫以及源代碼供我們按需使用。FFmpeg提供了非常強(qiáng)大的功能摊阀,可以完成音視頻的編碼耻蛇、解碼踪蹬、轉(zhuǎn)碼、視頻采集臣咖、后處理(抓圖跃捣、水印、封裝/解封裝夺蛇、格式轉(zhuǎn)換等)疚漆,還有流媒體服務(wù)等諸多功能,可以說涵蓋了音視頻開發(fā)中絕大多數(shù)的領(lǐng)域刁赦。原生的FFmpeg是在Linux環(huán)境下開發(fā)的娶聘,但是通過各種方法(比如交叉編譯等)可以使它運(yùn)行在多種平臺環(huán)境上,具有比較好的可移植性截型。
FFmpeg項(xiàng)目的官方網(wǎng)址為:https://ffmpeg.org/。在它的官網(wǎng)上我們可以找到許多非常有用的內(nèi)容儒溉,如項(xiàng)目的簡介宦焦、版本更新日志、庫和源代碼的地址顿涣、使用文檔等信息波闹。官方的使用文檔是我們在開發(fā)時(shí)必不可少的信息來源,其重要性不言而喻涛碑。除了官方網(wǎng)站以外精堕,我們下載的FFmpeg的程序包中也有使用參考文檔的離線版本。
2蒲障、FFmpeg組成
構(gòu)成FFmpeg主要有三個(gè)部分歹篓,第一部分是四個(gè)作用不同的工具軟件,分別是:ffmpeg.exe揉阎,ffplay.exe庄撮,ffserver.exe和ffprobe.exe。
- ffmpeg.exe:音視頻轉(zhuǎn)碼毙籽、轉(zhuǎn)換器
- ffplay.exe:簡單的音視頻播放器
- ffserver.exe:流媒體服務(wù)器
- ffprobe.exe:簡單的多媒體碼流分析器
第二部分是可以供開發(fā)者使用的SDK洞斯,為各個(gè)不同平臺編譯完成的庫。如果說上面的四個(gè)工具軟件都是完整成品形式的玩具坑赡,那么這些庫就相當(dāng)于樂高積木一樣烙如,我們可以根據(jù)自己的需求使用這些庫開發(fā)自己的應(yīng)用程序。這些庫有:
- libavcodec:包含音視頻編碼器和解碼器
- libavutil:包含多媒體應(yīng)用常用的簡化編程的工具毅否,如隨機(jī)數(shù)生成器亚铁、數(shù)據(jù)結(jié)構(gòu)、數(shù)學(xué)函數(shù)等功能
- libavformat:包含多種多媒體容器格式的封裝螟加、解封裝工具
- libavfilter:包含多媒體處理常用的濾鏡功能
- libavdevice:用于音視頻數(shù)據(jù)采集和渲染等功能的設(shè)備相關(guān)
- libswscale:用于圖像縮放和色彩空間和像素格式轉(zhuǎn)換功能
- libswresample:用于音頻重采樣和格式轉(zhuǎn)換等功能
第三部分是整個(gè)工程的源代碼刀闷,無論是編譯出來的可執(zhí)行程序還是SDK熊泵,都是由這些源代碼編譯出來的。FFmpeg的源代碼由C語言實(shí)現(xiàn)甸昏,主要在Linux平臺上進(jìn)行開發(fā)顽分。FFmpeg不是一個(gè)孤立的工程,它還存在多個(gè)依賴的第三方工程來增強(qiáng)它自身的功能施蜜。在當(dāng)前這一系列的博文/視頻中卒蘸,我們暫時(shí)不會涉及太多源代碼相關(guān)的內(nèi)容,主要以FFmpeg的工具和SDK的調(diào)用為主翻默。到下一系列我們將專門研究如何編譯源代碼并根據(jù)源代碼來進(jìn)行二次開發(fā)缸沃。
3、FFMpeg工具的下載和使用
(1)FFmpeg工具的下載:
在官網(wǎng)上我們可以找到"Download"頁面修械,該頁上可以下載FFmpeg的工具趾牧、庫和源代碼等。在選擇"Windows Packages"下的Windows Builds后肯污,會跳轉(zhuǎn)到Windows版本的下載頁面:
在下載頁面上翘单,我們可以看到,對于32位和64位版本蹦渣,分別提供了三種不同的模式:static哄芜、shared和dev
- static: 該版本提供了靜態(tài)版本的FFmpeg工具,將依賴的庫生成在了最終的可執(zhí)行文件中柬唯;作為工具而言此版本就可以滿足我們的需求认臊;
- share: 該版本的工具包括可執(zhí)行文件和dll,程序運(yùn)行過程必須依賴于提供的dll文件锄奢;
- dev: 提供了庫的頭文件和dll的引導(dǎo)庫失晴;
(2)ffplay.exe的使用
ffplay是一個(gè)極為簡單的音視頻媒體播放器。ffplay.exe使用了ffmpeg庫和SDL庫開發(fā)成的拘央,可以用作FFmpeg API的測試工具师坎。
ffplay的使用方法,最簡單的是直接按照默認(rèn)格式播放某一個(gè)音視頻文件或流:
ffplay.exe -i ../video/IMG_0886.MOV
除此之外堪滨,ffplay還支持傳入各種參數(shù)來控制播放行為胯陋。比較常用的參數(shù)有:
- -i input_file:輸入文件名
- -x width -y height:控制播放窗口的寬高
- -t duration:控制播放的時(shí)長
- -window_title title:播放窗口的標(biāo)題,默認(rèn)為輸入文件名
- -showmode mode:設(shè)置顯示模式袱箱,0:顯示視頻;1:顯示音頻波形遏乔;2:顯示音頻頻譜
- -autoexit:設(shè)置視頻播放完成后自動(dòng)退出
其他參數(shù)可以參考官網(wǎng)的文檔:https://www.ffmpeg.org/ffplay.html或下載包里的文檔
(3)ffprobe的使用
ffprobe可以提供簡單的音視頻文件分析功能。最簡單的方法同ffplay類似:
ffprobe.exe -i ../video/IMG_0886.MOV
分析完成后发笔,ffprobe會顯示音視頻文件中包含的每個(gè)碼流的信息盟萨,包括編碼格式、像素分辨率了讨、碼率捻激、幀率等信息:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-aTJUcp0h-1600929567435)(http://cl.ly/2h2l1g1U1m1F/QQ截圖20160331231357.png)]
(4)ffmpeg的使用
ffmpeg.exe可謂是整個(gè)工程的核心所在制轰,它的主要功能是完成音視頻各種各樣的轉(zhuǎn)換操作。
視頻轉(zhuǎn)碼:ffmpeg.exe可以將視頻文件由原格式轉(zhuǎn)換為其他格式胞谭,如從avi轉(zhuǎn)為mp4等:
ffmpeg -i ../video/IMG_0886.MOV ../video/output_mpeg4_mp3.avi
這里垃杖,ffmpeg默認(rèn)將視頻編碼格式選擇為mpeg4,音頻轉(zhuǎn)碼格式為mp3丈屹。如果我們希望保留原始編碼调俘,需要增加參數(shù)-c copy,表明不做任何轉(zhuǎn)碼操作:
ffmpeg -i ../video/IMG_0886.MOV -c copy ../video/output_copy.avi
如果我們希望將視頻轉(zhuǎn)換為其他編碼格式旺垒,則需要在參數(shù)中指定目標(biāo)格式-c:v libx265或-vcodec libx265彩库。ffmpeg支持的所有編碼器格式可以通過以下命令查看:
ffmpeg.exe -encoders
實(shí)際操作:
ffmpeg -i ../video/IMG_0886.MOV -c:v mjpeg ../video/output_mjpeg.avi
視頻解封裝:ffmpeg可以將視頻中的音頻和視頻流分別提取出來。需要在命令行中添加參數(shù)-an和-vn先蒋,分別表示屏蔽音頻和視頻流:
@REM 提取視頻流
ffmpeg -i ../video/IMG_0886.MOV -c:v copy -an ../video/IMG_0886_v.MOV
@REM 提取音頻流
ffmpeg -i ../video/IMG_0886.MOV -c:a copy -vn ../video/IMG_0886_a.aac
視頻截群铡:使用ffmpeg命令并指定參數(shù)-ss和-t,分別表示截取開始時(shí)刻和截取時(shí)長
@REM 視頻截取
ffmpeg -ss 5 -t 5 -i ../video/IMG_0886.MOV -c copy ../video/IMG_0886_cut.MOV
二竞漾、調(diào)用FFmpeg SDK對YUV視頻序列進(jìn)行編碼
視頻由像素格式編碼為碼流格式是FFMpeg的一項(xiàng)基本功能眯搭。通常,視頻編碼器的輸入視頻通常為原始的圖像像素值畴蹭,輸出格式為符合某種格式規(guī)定的二進(jìn)制碼流坦仍。
1鳍烁、FFMpeg進(jìn)行視頻編碼所需要的結(jié)構(gòu):
- AVCodec:AVCodec結(jié)構(gòu)保存了一個(gè)編解碼器的實(shí)例叨襟,實(shí)現(xiàn)實(shí)際的編碼功能。通常我們在程序中定義一個(gè)指向AVCodec結(jié)構(gòu)的指針指向該實(shí)例幔荒。
- AVCodecContext:AVCodecContext表示AVCodec所代表的上下文信息糊闽,保存了AVCodec所需要的一些參數(shù)。對于實(shí)現(xiàn)編碼功能爹梁,我們可以在這個(gè)結(jié)構(gòu)中設(shè)置我們指定的編碼參數(shù)右犹。通常也是定義一個(gè)指針指向AVCodecContext。
- AVFrame:AVFrame結(jié)構(gòu)保存編碼之前的像素?cái)?shù)據(jù)姚垃,并作為編碼器的輸入數(shù)據(jù)念链。其在程序中也是一個(gè)指針的形式。
- AVPacket:AVPacket表示碼流包結(jié)構(gòu)积糯,包含編碼之后的碼流數(shù)據(jù)掂墓。該結(jié)構(gòu)可以不定義指針,以一個(gè)對象的形式定義看成。
在我們的程序中君编,我們將這些結(jié)構(gòu)整合在了一個(gè)結(jié)構(gòu)體中:
/*************************************************
Struct: CodecCtx
Description: FFMpeg編解碼器上下文
*************************************************/
typedef struct
{
AVCodec *codec; //指向編解碼器實(shí)例
AVFrame *frame; //保存解碼之后/編碼之前的像素?cái)?shù)據(jù)
AVCodecContext *c; //編解碼器上下文,保存編解碼器的一些參數(shù)設(shè)置
AVPacket pkt; //碼流包結(jié)構(gòu)川慌,包含編碼碼流數(shù)據(jù)
} CodecCtx;
2吃嘿、FFMpeg編碼的主要步驟:
(1)祠乃、輸入編碼參數(shù)
這一步我們可以設(shè)置一個(gè)專門的配置文件,并將參數(shù)按照某個(gè)事寫入這個(gè)配置文件中兑燥,再在程序中解析這個(gè)配置文件獲得編碼的參數(shù)亮瓷。如果參數(shù)不多的話,我們可以直接使用命令行將編碼參數(shù)傳入即可贪嫂。
(2)寺庄、按照要求初始化需要的FFMpeg結(jié)構(gòu)
首先,所有涉及到編解碼的的功能力崇,都必須要注冊音視頻編解碼器之后才能使用斗塘。注冊編解碼調(diào)用下面的函數(shù):
avcodec_register_all();
編解碼器注冊完成之后,根據(jù)指定的CODEC_ID查找指定的codec實(shí)例亮靴。CODEC_ID通常指定了編解碼器的格式馍盟,在這里我們使用當(dāng)前應(yīng)用最為廣泛的H.264格式為例。查找codec調(diào)用的函數(shù)為avcodec_find_encoder茧吊,其聲明格式為:
AVCodec *avcodec_find_encoder(enum AVCodecID id);
該函數(shù)的輸入?yún)?shù)為一個(gè)AVCodecID的枚舉類型贞岭,返回值為一個(gè)指向AVCodec結(jié)構(gòu)的指針,用于接收找到的編解碼器實(shí)例搓侄。如果沒有找到憋他,那么該函數(shù)會返回一個(gè)空指針熟菲。調(diào)用方法如下:
/* find the mpeg1 video encoder */
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根據(jù)CODEC_ID查找編解碼器對象實(shí)例的指針
if (!ctx.codec)
{
fprintf(stderr, "Codec not found\n");
return false;
}
AVCodec查找成功后,下一步是分配AVCodecContext實(shí)例。分配AVCodecContext實(shí)例需要我們前面查找到的AVCodec作為參數(shù)轩娶,調(diào)用的是avcodec_alloc_context3函數(shù)锄弱。其聲明方式為:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特點(diǎn)同avcodec_find_encoder類似聪舒,返回一個(gè)指向AVCodecContext實(shí)例的指針知残。如果分配失敗,會返回一個(gè)空指針云石。調(diào)用方式為:
ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext實(shí)例
if (!ctx.c)
{
fprintf(stderr, "Could not allocate video codec context\n");
return false;
}
需注意唉工,在分配成功之后,應(yīng)將編碼的參數(shù)設(shè)置賦值給AVCodecContext的成員汹忠。
現(xiàn)在淋硝,AVCodec、AVCodecContext的指針都已經(jīng)分配好宽菜,然后以這兩個(gè)對象的指針作為參數(shù)打開編碼器對象谣膳。調(diào)用的函數(shù)為avcodec_open2,聲明方式為:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
該函數(shù)的前兩個(gè)參數(shù)是我們剛剛建立的兩個(gè)對象赋焕,第三個(gè)參數(shù)為一個(gè)字典類型對象参歹,用于保存函數(shù)執(zhí)行過程總未能識別的AVCodecContext和另外一些私有設(shè)置選項(xiàng)。函數(shù)的返回值表示編碼器是否打開成功隆判,若成功返回0犬庇,失敗返回一個(gè)負(fù)數(shù)僧界。調(diào)用方式為:
if (avcodec_open2(ctx.c, ctx.codec, NULL) < 0) //根據(jù)編碼器上下文打開編碼器
{
fprintf(stderr, "Could not open codec\n");
exit(1);
}
然后,我們需要處理AVFrame對象臭挽。AVFrame表示視頻原始像素?cái)?shù)據(jù)的一個(gè)容器捂襟,處理該類型數(shù)據(jù)需要兩個(gè)步驟,其一是分配AVFrame對象欢峰,其二是分配實(shí)際的像素?cái)?shù)據(jù)的存儲空間葬荷。分配對象空間類似于new操作符一樣,只是需要調(diào)用函數(shù)av_frame_alloc纽帖。如果失敗宠漩,那么函數(shù)返回一個(gè)空指針。AVFrame對象分配成功后懊直,需要設(shè)置圖像的分辨率和像素格式等扒吁。實(shí)際調(diào)用過程如下:
ctx.frame = av_frame_alloc(); //分配AVFrame對象
if (!ctx.frame)
{
fprintf(stderr, "Could not allocate video frame\n");
return false;
}
ctx.frame->format = ctx.c->pix_fmt;
ctx.frame->width = ctx.c->width;
ctx.frame->height = ctx.c->height;
分配像素的存儲空間需要調(diào)用av_image_alloc函數(shù),其聲明方式為:
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
該函數(shù)的四個(gè)參數(shù)分別表示AVFrame結(jié)構(gòu)中的緩存指針室囊、各個(gè)顏色分量的寬度雕崩、圖像分辨率(寬、高)融撞、像素格式和內(nèi)存對其的大小盼铁。該函數(shù)會返回分配的內(nèi)存的大小,如果失敗則返回一個(gè)負(fù)值尝偎。具體調(diào)用方式如:
ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32);
if (ret < 0)
{
fprintf(stderr, "Could not allocate raw picture buffer\n");
return false;
}
(3)饶火、編碼循環(huán)體
到此為止,我們的準(zhǔn)備工作已經(jīng)大致完成冬念,下面開始執(zhí)行實(shí)際編碼的循環(huán)過程趁窃。用偽代碼大致表示編碼的流程為:
while (numCoded < maxNumToCode)
{
read_yuv_data();
encode_video_frame();
write_out_h264();
}
其中牧挣,read_yuv_data部分直接使用fread語句讀取即可急前,只需要知道的是,三個(gè)顏色分量Y/U/V的地址分別為AVframe::data[0]瀑构、AVframe::data[1]和AVframe::data[2]裆针,圖像的寬度分別為AVframe::linesize[0]、AVframe::linesize[1]和AVframe::linesize[2]寺晌。需要注意的是世吨,linesize中的值通常指的是stride而不是width,也就是說呻征,像素保存區(qū)可能是帶有一定寬度的無效邊區(qū)的耘婚,在讀取數(shù)據(jù)時(shí)需注意。
編碼前另外需要完成的操作時(shí)初始化AVPacket對象陆赋。該對象保存了編碼之后的碼流數(shù)據(jù)沐祷。對其進(jìn)行初始化的操作非常簡單嚷闭,只需要調(diào)用av_init_packet并傳入AVPacket對象的指針。隨后將AVPacket::data設(shè)為NULL赖临,AVPacket::size賦值0.
成功將原始的YUV像素值保存到了AVframe結(jié)構(gòu)中之后胞锰,便可以調(diào)用avcodec_encode_video2函數(shù)進(jìn)行實(shí)際的編碼操作。該函數(shù)可謂是整個(gè)工程的核心所在兢榨,其聲明方式為:
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);
其參數(shù)和返回值的意義:
- avctx: AVCodecContext結(jié)構(gòu)嗅榕,指定了編碼的一些參數(shù);
- avpkt: AVPacket對象的指針吵聪,用于保存輸出碼流凌那;
- frame:AVframe結(jié)構(gòu),用于傳入原始的像素?cái)?shù)據(jù)吟逝;
- got_packet_ptr:輸出參數(shù)案怯,用于標(biāo)識AVPacket中是否已經(jīng)有了完整的一幀;
- 返回值:編碼是否成功澎办。成功返回0嘲碱,失敗則返回負(fù)的錯(cuò)誤碼
通過輸出參數(shù)*got_packet_ptr,我們可以判斷是否應(yīng)有一幀完整的碼流數(shù)據(jù)包輸出局蚀,如果是麦锯,那么可以將AVpacket中的碼流數(shù)據(jù)輸出出來,其地址為AVPacket::data琅绅,大小為AVPacket::size扶欣。具體調(diào)用方式如下:
/* encode the image */
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //將AVFrame中的像素信息編碼為AVPacket中的碼流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\n");
exit(1);
}
if (got_output)
{
//獲得一個(gè)完整的編碼幀
printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
因此,一個(gè)完整的編碼循環(huán)提就可以使用下面的代碼實(shí)現(xiàn):
/* encode 1 second of video */
for (frameIdx = 0; frameIdx < io_param.nTotalFrames; frameIdx++)
{
av_init_packet(&(ctx.pkt)); //初始化AVPacket實(shí)例
ctx.pkt.data = NULL; // packet data will be allocated by the encoder
ctx.pkt.size = 0;
fflush(stdout);
Read_yuv_data(ctx, io_param, 0); //Y分量
Read_yuv_data(ctx, io_param, 1); //U分量
Read_yuv_data(ctx, io_param, 2); //V分量
ctx.frame->pts = frameIdx;
/* encode the image */
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //將AVFrame中的像素信息編碼為AVPacket中的碼流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\n");
exit(1);
}
if (got_output)
{
//獲得一個(gè)完整的編碼幀
printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
} //for (frameIdx = 0; frameIdx < io_param.nTotalFrames; frameIdx++)
(4)千扶、收尾處理
如果我們就此結(jié)束編碼器的整個(gè)運(yùn)行過程料祠,我們會發(fā)現(xiàn),編碼完成之后的碼流對比原來的數(shù)據(jù)少了一幀澎羞。這是因?yàn)槲覀兪歉鶕?jù)讀取原始像素?cái)?shù)據(jù)結(jié)束來判斷循環(huán)結(jié)束的髓绽,這樣最后一幀還保留在編碼器中尚未輸出。所以在關(guān)閉整個(gè)解碼過程之前妆绞,我們必須繼續(xù)執(zhí)行編碼的操作顺呕,直到將最后一幀輸出為止。執(zhí)行這項(xiàng)操作依然調(diào)用avcodec_encode_video2函數(shù)括饶,只是表示AVFrame的參數(shù)設(shè)為NULL即可:
/* get the delayed frames */
for (got_output = 1; got_output; frameIdx++)
{
fflush(stdout);
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), NULL, &got_output); //輸出編碼器中剩余的碼流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\n");
exit(1);
}
if (got_output)
{
printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
} //for (got_output = 1; got_output; frameIdx++)
此后株茶,我們就可以按計(jì)劃關(guān)閉編碼器的各個(gè)組件,結(jié)束整個(gè)編碼的流程图焰。編碼器組件的釋放流程可類比建立流程启盛,需要關(guān)閉AVCocec、釋放AVCodecContext、釋放AVFrame中的圖像緩存和對象本身:
avcodec_close(ctx.c);
av_free(ctx.c);
av_freep(&(ctx.frame->data[0]));
av_frame_free(&(ctx.frame));
3僵闯、總結(jié)
使用FFMpeg進(jìn)行視頻編碼的主要流程如:
- 首先解析笤闯、處理輸入?yún)?shù),如編碼器的參數(shù)棍厂、圖像的參數(shù)颗味、輸入輸出文件;
- 建立整個(gè)FFMpeg編碼器的各種組件工具牺弹,順序依次為:avcodec_register_all -> avcodec_find_encoder -> avcodec_alloc_context3 -> avcodec_open2 -> av_frame_alloc -> av_image_alloc;
- 編碼循環(huán):av_init_packet -> avcodec_encode_video2(兩次) -> av_packet_unref
- 關(guān)閉編碼器組件:avcodec_close浦马,av_free,av_freep张漂,av_frame_free
三晶默、調(diào)用FFmpeg SDK對H.264格式的視頻壓縮碼流進(jìn)行解碼
經(jīng)過了上篇調(diào)用FFMpeg SDK對視頻進(jìn)行編碼的過程之后,我們可以比較容易地理解本篇的內(nèi)容航攒,即上一篇的逆過程——將H.264格式的裸碼流解碼為像素格式的圖像信息磺陡。
1、FFMpeg視頻解碼器所包含的結(jié)構(gòu)
同F(xiàn)FMpeg編碼器類似漠畜,F(xiàn)FMpeg解碼器也需要編碼時(shí)的各種結(jié)構(gòu)币他,除此之外,解碼器還需要另一個(gè)結(jié)構(gòu)——編解碼解析器——用于從碼流中截取出一幀完整的碼流數(shù)據(jù)單元憔狞。因此我們定義一個(gè)編解碼上下文結(jié)構(gòu)為:
/*************************************************
Struct: CodecCtx
Description: FFMpeg編解碼器上下文
*************************************************/
typedef struct
{
AVCodec *pCodec; //編解碼器實(shí)例指針
AVCodecContext *pCodecContext; //編解碼器上下文蝴悉,指定了編解碼的參數(shù)
AVCodecParserContext *pCodecParserCtx; //編解碼解析器,從碼流中截取完整的一個(gè)NAL Unit數(shù)據(jù)
AVFrame *frame; //封裝圖像對象指針
AVPacket pkt; //封裝碼流對象實(shí)例
} CodecCtx;
2瘾敢、FFMpeg進(jìn)行解碼操作的主要步驟
(1). 參數(shù)傳遞和解析
同編碼器類似拍冠,解碼器也需要傳遞參數(shù)。不過相比編碼器簇抵,解碼器在運(yùn)行時(shí)所需要的大部分信息都包含在輸入碼流中庆杜,因此輸入?yún)?shù)一般只需要指定一個(gè)待解碼的視頻碼流文件即可
(2). 按照要求初始化需要的FFMpeg結(jié)構(gòu)
首先,所有涉及到編解碼的的功能碟摆,都必須要注冊音視頻編解碼器之后才能使用晃财。注冊編解碼調(diào)用下面的函數(shù):
avcodec_register_all();
編解碼器注冊完成之后,根據(jù)指定的CODEC_ID查找指定的codec實(shí)例焦履。CODEC_ID通常指定了編解碼器的格式拓劝,在這里我們使用當(dāng)前應(yīng)用最為廣泛的H.264格式為例雏逾。查找codec調(diào)用的函數(shù)為avcodec_find_encoder嘉裤,其聲明格式為:
AVCodec *avcodec_find_encoder(enum AVCodecID id);
該函數(shù)的輸入?yún)?shù)為一個(gè)AVCodecID的枚舉類型,返回值為一個(gè)指向AVCodec結(jié)構(gòu)的指針栖博,用于接收找到的編解碼器實(shí)例屑宠。如果沒有找到,那么該函數(shù)會返回一個(gè)空指針仇让。調(diào)用方法如下:
/* find the mpeg1 video encoder */
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根據(jù)CODEC_ID查找編解碼器對象實(shí)例的指針
if (!ctx.codec)
{
fprintf(stderr, "Codec not found\n");
return false;
}
AVCodec查找成功后典奉,下一步是分配AVCodecContext實(shí)例躺翻。分配AVCodecContext實(shí)例需要我們前面查找到的AVCodec作為參數(shù),調(diào)用的是avcodec_alloc_context3函數(shù)卫玖。其聲明方式為:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特點(diǎn)同avcodec_find_encoder類似公你,返回一個(gè)指向AVCodecContext實(shí)例的指針。如果分配失敗假瞬,會返回一個(gè)空指針陕靠。調(diào)用方式為:
ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext實(shí)例
if (!ctx.c)
{
fprintf(stderr, "Could not allocate video codec context\n");
return false;
}
我們應(yīng)該記得,在FFMpeg視頻編碼的實(shí)現(xiàn)中脱茉,AVCodecContext對象分配完成后剪芥,下一步實(shí)在該對象中設(shè)置編碼的參數(shù)。而在解碼器的實(shí)現(xiàn)中琴许,基本不需要額外設(shè)置參數(shù)信息税肪,因此這個(gè)對象更多地作為輸出參數(shù)接收數(shù)據(jù)。因此對象分配完成后榜田,不需要進(jìn)一步的初始化操作益兄。
解碼器與編碼器實(shí)現(xiàn)中不同的一點(diǎn)在于,解碼器的實(shí)現(xiàn)中需要額外的一個(gè)AVCodecParserContext結(jié)構(gòu)箭券,用于從碼流中截取一個(gè)完整的NAL單元偏塞。因此我們需要分配一個(gè)AVCodecParserContext類型的對象,使用函數(shù)av_parser_init邦鲫,聲明為:
AVCodecParserContext *av_parser_init(int codec_id);
調(diào)用方式為:
ctx.pCodecParserCtx = av_parser_init(AV_CODEC_ID_H264);
if (!ctx.pCodecParserCtx)
{
printf("Could not allocate video parser context\n");
return false;
}
隨后灸叼,打開AVCodec對象庆捺,然后分配AVFrame對象:
//打開AVCodec對象
if (avcodec_open2(ctx.pCodecContext, ctx.pCodec, NULL) < 0)
{
fprintf(stderr, "Could not open codec\n");
return false;
}
//分配AVFrame對象
ctx.frame = av_frame_alloc();
if (!ctx.frame)
{
fprintf(stderr, "Could not allocate video frame\n");
return false;
}
(3)古今、解碼循環(huán)體
完成必須的codec組件的建立和初始化之后,開始進(jìn)入正式的解碼循環(huán)過程滔以。解碼循環(huán)通常按照以下幾個(gè)步驟實(shí)現(xiàn):
首先按照某個(gè)指定的長度讀取一段碼流保存到緩存區(qū)中捉腥。
由于H.264中一個(gè)包的長度是不定的,我們讀取一段固定長度的碼流通常不可能剛好讀出一個(gè)包的長度你画。所以我們就需要使用AVCodecParserContext結(jié)構(gòu)對我們讀出的碼流信息進(jìn)行解析抵碟,直到取出一個(gè)完整的H.264包。對碼流解析的函數(shù)為av_parser_parse2坏匪,聲明方式如:
int av_parser_parse2(AVCodecParserContext *s,
AVCodecContext *avctx,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size,
int64_t pts, int64_t dts,
int64_t pos);
這個(gè)函數(shù)的各個(gè)參數(shù)的意義:
- AVCodecParserContext *s:初始化過的AVCodecParserContext對象拟逮,決定了碼流該以怎樣的標(biāo)準(zhǔn)進(jìn)行解析;
- *AVCodecContext avctx:預(yù)先定義好的AVCodecContext對象适滓;
- uint8_t **poutbuf:AVPacket::data的地址敦迄,保存解析完成的包數(shù)據(jù);
- int *poutbuf_size:AVPacket的實(shí)際數(shù)據(jù)長度;如果沒解析出完整的一個(gè)包罚屋,這個(gè)值為0苦囱;
- const uint8_t *buf, int buf_size:輸入?yún)?shù),緩存的地址和長度脾猛;
- int64_t pts, int64_t dts:顯示和解碼的時(shí)間戳撕彤;
- nt64_t pos :碼流中的位置;
- 返回值為解析所使用的比特位的長度猛拴;
具體的調(diào)用方式為:
len = av_parser_parse2(ctx.pCodecParserCtx, ctx.pCodecContext,
&(ctx.pkt.data), &(ctx.pkt.size),
pDataPtr, uDataSize,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);
如果參數(shù)poutbuf_size的值為0喉刘,那么應(yīng)繼續(xù)解析緩存中剩余的碼流;如果緩存中的數(shù)據(jù)全部解析后依然未能找到一個(gè)完整的包漆弄,那么繼續(xù)從輸入文件中讀取數(shù)據(jù)到緩存睦裳,繼續(xù)解析操作,直到pkt.size不為0為止撼唾。
在最終解析出一個(gè)完整的包之后廉邑,我們就可以調(diào)用解碼API進(jìn)行實(shí)際的解碼過程了。解碼過程調(diào)用的函數(shù)為avcodec_decode_video2倒谷,該函數(shù)的聲明為:
int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
const AVPacket *avpkt);
這個(gè)函數(shù)與前篇所遇到的編碼函數(shù)avcodec_encode_video2有些類似蛛蒙,只是參數(shù)的順序略有不同,解碼函數(shù)的輸入輸出參數(shù)與編碼函數(shù)相比交換了位置渤愁。該函數(shù)各個(gè)參數(shù)的意義:
- AVCodecContext *avctx:編解碼器上下文對象牵祟,在打開編解碼器時(shí)生成;
- AVFrame *picture: 保存解碼完成后的像素?cái)?shù)據(jù)抖格;我們只需要分配對象的空間诺苹,像素的空間codec會為我們分配好;
- int *got_picture_ptr: 標(biāo)識位雹拄,如果為1收奔,那么說明已經(jīng)有一幀完整的像素幀可以輸出了
- const AVPacket *avpkt: 前面解析好的碼流包;
實(shí)際調(diào)用的方法為:
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf("Decode Error.\n");
return ret;
}
if (got_picture)
{
//獲得一幀完整的圖像滓玖,寫出到輸出文件
write_out_yuv_frame(ctx, inputoutput);
printf("Succeed to decode 1 frame!\n");
}
最后坪哄,同編碼器一樣,解碼過程的最后一幀可能也存在延遲势篡。處理最后這一幀的方法也跟解碼器類似:將AVPacket::data設(shè)為NULL翩肌,AVPacket::size設(shè)為0,然后在調(diào)用avcodec_encode_video2完成最后的解碼過程:
ctx.pkt.data = NULL;
ctx.pkt.size = 0;
while(1)
{
//將編碼器中剩余的數(shù)據(jù)繼續(xù)輸出完
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf("Decode Error.\n");
return ret;
}
if (got_picture)
{
write_out_yuv_frame(ctx, inputoutput);
printf("Flush Decoder: Succeed to decode 1 frame!\n");
}
else
{
break;
}
} //while(1)
(4). 收尾工作
收尾工作主要包括關(guān)閉輸入輸出文件禁悠、關(guān)閉FFMpeg解碼器各個(gè)組件念祭。其中關(guān)閉解碼器組件需要:
avcodec_close(ctx.pCodecContext);
av_free(ctx.pCodecContext);
av_frame_free(&(ctx.frame));
3、總結(jié)
解碼器的流程與編碼器類似绷蹲,只是中間需要加入一個(gè)解析的過程棒卷。整個(gè)流程大致為:
1.讀取碼流數(shù)據(jù) -> 2.解析數(shù)據(jù)顾孽,是否尚未解析出一個(gè)包就已經(jīng)用完祝钢?是返回1比规,否繼續(xù) -> 3.解析出一個(gè)包?是則繼續(xù)拦英,否則返回上一步繼續(xù)解析 -> 4.調(diào)用avcodec_decode_video2進(jìn)行解碼 -> 5.是否解碼出一幀完整的圖像蜒什?是則繼續(xù),否則返回上一步繼續(xù)解碼 -> 6.寫出圖像數(shù)據(jù) -> 返回步驟2繼續(xù)解析疤估。
四灾常、調(diào)用FFmpeg SDK解析封裝格式的視頻為音頻流和視頻流
我們平常最常用的音視頻文件通常不是單獨(dú)的音頻信號和視頻信號,而是一個(gè)整體的文件铃拇。這個(gè)文件會在其中包含音頻流和視頻流钞瀑,并通過某種方式進(jìn)行同步播放。通常慷荔,文件的音頻和視頻通過某種標(biāo)準(zhǔn)格式進(jìn)行復(fù)用雕什,生成某種封裝格式,而封裝的標(biāo)志就是文件的擴(kuò)展名显晶,常用的有mp4/avi/flv/mkv等贷岸。
從底層考慮,我們可以使用的只有視頻解碼器磷雇、音頻解碼器偿警,或者再加上一些附加的字幕解碼等額外信息,卻不存在所謂的mp4解碼器或者avi解碼器唯笙。所以螟蒸,為了可以正確播放視頻文件,必須將封裝格式的視頻文件分離出視頻和音頻信息分別進(jìn)行解碼和播放崩掘。
事實(shí)上尿庐,無論是mp4還是avi等文件格式,都有不同的標(biāo)準(zhǔn)格式呢堰,對于不同的格式并沒有一種通用的解析方法抄瑟。因此,F(xiàn)FMpeg專門定義了一個(gè)庫來處理設(shè)計(jì)文件封裝格式的功能枉疼,即libavformat皮假。涉及文件的封裝、解封裝的問題骂维,都可以通過調(diào)用libavformat的API實(shí)現(xiàn)惹资。這里我們實(shí)現(xiàn)一個(gè)demo來處理音視頻文件的解復(fù)用與解碼的功能。
1. FFMpeg解復(fù)用-解碼器所包含的結(jié)構(gòu)
這一過程實(shí)際上包括了封裝文件的解復(fù)用和音頻/視頻解碼兩個(gè)步驟航闺,因此需要定義的結(jié)構(gòu)體大致包括用于解碼和解封裝的部分褪测。我們定義下面這樣的一個(gè)結(jié)構(gòu)體實(shí)現(xiàn)這個(gè)功能:
/*************************************************
Struct: DemuxingVideoAudioContex
Description: 保存解復(fù)用器和解碼器的上下文組件
*************************************************/
typedef struct
{
AVFormatContext *fmt_ctx;
AVCodecContext *video_dec_ctx, *audio_dec_ctx;
AVStream *video_stream, *audio_stream;
AVFrame *frame;
AVPacket pkt;
int video_stream_idx, audio_stream_idx;
int width, height;
uint8_t *video_dst_data[4];
int video_dst_linesize[4];
int video_dst_bufsize;
enum AVPixelFormat pix_fmt;
} DemuxingVideoAudioContex;
這個(gè)結(jié)構(gòu)體中的大部分?jǐn)?shù)據(jù)類型我們在前面做編碼/解碼等功能時(shí)已經(jīng)見到過猴誊,另外幾個(gè)是涉及到視頻文件的復(fù)用的,其中有:
- AVFormatContext:用于處理音視頻封裝格式的上下文信息侮措。
- AVStream:表示音頻或者視頻流的結(jié)構(gòu)懈叹。
- AVPixelFormat:枚舉類型,表示圖像像素的格式分扎,最常用的是AV_PIX_FMT_YUV420P
2澄成、FFMpeg解復(fù)用-解碼的過程
(1)、相關(guān)結(jié)構(gòu)的初始化
與使用FFMpeg進(jìn)行其他操作一樣畏吓,首先需注冊FFMpeg組件:
av_register_all();
隨后墨状,我們需要打開待處理的音視頻文件。然而在此我們不使用打開文件的fopen函數(shù)菲饼,而是使用avformat_open_input函數(shù)肾砂。該函數(shù)不但會打開輸入文件,而且可以根據(jù)輸入文件讀取相應(yīng)的格式信息宏悦。該函數(shù)的聲明如下:
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
該函數(shù)的各個(gè)參數(shù)的作用為:
- ps:根據(jù)輸入文件接收與格式相關(guān)的句柄信息镐确;可以指向NULL,那么AVFormatContext類型的實(shí)例將由該函數(shù)進(jìn)行分配肛根。
- url:視頻url或者文件路徑辫塌;
- fmt:強(qiáng)制輸入格式,可設(shè)置為NULL以自動(dòng)檢測派哲;
- options:保存文件格式無法識別的信息臼氨;
- 返回值:成功返回0,失敗則返回負(fù)的錯(cuò)誤碼芭届;
該函數(shù)的調(diào)用方式為:
if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
{
fprintf(stderr, "Could not open source file %s\n", files.src_filename);
return -1;
}
打開文件后储矩,調(diào)用avformat_find_stream_info函數(shù)獲取文件中的流信息。該函數(shù)的聲明為:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
該函數(shù)的第一個(gè)參數(shù)即前面的文件句柄褂乍,第二個(gè)參數(shù)也是用于保存無法識別的信息的AVDictionary的結(jié)構(gòu)持隧,通常可設(shè)為NULL逃片。調(diào)用方式如:
/* retrieve stream information */
if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0)
{
fprintf(stderr, "Could not find stream information\n");
return -1;
}
獲取文件中的流信息后屡拨,下一步則是獲取文件中的音頻和視頻流,并準(zhǔn)備對音頻和視頻信息進(jìn)行解碼褥实。獲取文件中的流使用av_find_best_stream函數(shù)呀狼,其聲明如:
int av_find_best_stream(AVFormatContext *ic,
enum AVMediaType type,
int wanted_stream_nb,
int related_stream,
AVCodec **decoder_ret,
int flags);
其中各個(gè)參數(shù)的意義:
- ic:視頻文件句柄;
- type:表示數(shù)據(jù)的類型损离,常用的有AVMEDIA_TYPE_VIDEO表示視頻哥艇,AVMEDIA_TYPE_AUDIO表示音頻等;
- wanted_stream_nb:我們期望獲取到的數(shù)據(jù)流的數(shù)量僻澎,設(shè)置為-1使用自動(dòng)獲让蔡ぁ十饥;
- related_stream:獲取相關(guān)的音視頻流,如果沒有則設(shè)為-1祖乳;
- decoder_ret:返回這一路數(shù)據(jù)流的解碼器逗堵;
- flags:未定義;
- 返回值:函數(shù)執(zhí)行成功返回流的數(shù)量凡资,失敗則返回負(fù)的錯(cuò)誤碼砸捏;
在函數(shù)執(zhí)行成功后谬运,便可調(diào)用avcodec_find_decoder和avcodec_open2打開解碼器準(zhǔn)備解碼音視頻流隙赁。該部分的代碼實(shí)現(xiàn)如:
static int open_codec_context(IOFileName &files, DemuxingVideoAudioContex &va_ctx, enum AVMediaType type)
{
int ret, stream_index;
AVStream *st;
AVCodecContext *dec_ctx = NULL;
AVCodec *dec = NULL;
AVDictionary *opts = NULL;
ret = av_find_best_stream(va_ctx.fmt_ctx, type, -1, -1, NULL, 0);
if (ret < 0)
{
fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), files.src_filename);
return ret;
}
else
{
stream_index = ret;
st = va_ctx.fmt_ctx->streams[stream_index];
/* find decoder for the stream */
dec_ctx = st->codec;
dec = avcodec_find_decoder(dec_ctx->codec_id);
if (!dec)
{
fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type));
return AVERROR(EINVAL);
}
/* Init the decoders, with or without reference counting */
av_dict_set(&opts, "refcounted_frames", files.refcount ? "1" : "0", 0);
if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0)
{
fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type));
return ret;
}
switch (type)
{
case AVMEDIA_TYPE_VIDEO:
va_ctx.video_stream_idx = stream_index;
va_ctx.video_stream = va_ctx.fmt_ctx->streams[stream_index];
va_ctx.video_dec_ctx = va_ctx.video_stream->codec;
break;
case AVMEDIA_TYPE_AUDIO:
va_ctx.audio_stream_idx = stream_index;
va_ctx.audio_stream = va_ctx.fmt_ctx->streams[stream_index];
va_ctx.audio_dec_ctx = va_ctx.audio_stream->codec;
break;
default:
fprintf(stderr, "Error: unsupported MediaType: %s\n", av_get_media_type_string(type));
return -1;
}
}
return 0;
}
整體初始化的函數(shù)代碼為:
int InitDemuxContext(IOFileName &files, DemuxingVideoAudioContex &va_ctx)
{
int ret = 0, width, height;
/* register all formats and codecs */
av_register_all();
/* open input file, and allocate format context */
if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
{
fprintf(stderr, "Could not open source file %s\n", files.src_filename);
return -1;
}
/* retrieve stream information */
if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0)
{
fprintf(stderr, "Could not find stream information\n");
return -1;
}
if (open_codec_context(files, va_ctx, AVMEDIA_TYPE_VIDEO) >= 0)
{
files.video_dst_file = fopen(files.video_dst_filename, "wb");
if (!files.video_dst_file)
{
fprintf(stderr, "Could not open destination file %s\n", files.video_dst_filename);
return -1;
}
/* allocate image where the decoded image will be put */
va_ctx.width = va_ctx.video_dec_ctx->width;
va_ctx.height = va_ctx.video_dec_ctx->height;
va_ctx.pix_fmt = va_ctx.video_dec_ctx->pix_fmt;
ret = av_image_alloc(va_ctx.video_dst_data, va_ctx.video_dst_linesize, va_ctx.width, va_ctx.height, va_ctx.pix_fmt, 1);
if (ret < 0)
{
fprintf(stderr, "Could not allocate raw video buffer\n");
return -1;
}
va_ctx.video_dst_bufsize = ret;
}
if (open_codec_context(files, va_ctx, AVMEDIA_TYPE_AUDIO) >= 0)
{
files.audio_dst_file = fopen(files.audio_dst_filename, "wb");
if (!files.audio_dst_file)
{
fprintf(stderr, "Could not open destination file %s\n", files.audio_dst_filename);
return -1;
}
}
if (va_ctx.video_stream)
{
printf("Demuxing video from file '%s' into '%s'\n", files.src_filename, files.video_dst_filename);
}
if (va_ctx.audio_stream)
{
printf("Demuxing audio from file '%s' into '%s'\n", files.src_filename, files.audio_dst_filename);
}
/* dump input information to stderr */
av_dump_format(va_ctx.fmt_ctx, 0, files.src_filename, 0);
if (!va_ctx.audio_stream && !va_ctx.video_stream)
{
fprintf(stderr, "Could not find audio or video stream in the input, aborting\n");
return -1;
}
return 0;
}
隨后要做的,是分配AVFrame和初始化AVPacket對象:
va_ctx.frame = av_frame_alloc(); //分配AVFrame結(jié)構(gòu)對象
if (!va_ctx.frame)
{
fprintf(stderr, "Could not allocate frame\n");
ret = AVERROR(ENOMEM);
goto end;
}
/* initialize packet, set data to NULL, let the demuxer fill it */
av_init_packet(&va_ctx.pkt); //初始化AVPacket對象
va_ctx.pkt.data = NULL;
va_ctx.pkt.size = 0;
(2)梆暖、循環(huán)解析視頻文件的包數(shù)據(jù)
解析視頻文件的循環(huán)代碼段為:
/* read frames from the file */
while (av_read_frame(va_ctx.fmt_ctx, &va_ctx.pkt) >= 0) //從輸入程序中讀取一個(gè)包的數(shù)據(jù)
{
AVPacket orig_pkt = va_ctx.pkt;
do
{
ret = Decode_packet(files, va_ctx, &got_frame, 0); //解碼這個(gè)包
if (ret < 0)
break;
va_ctx.pkt.data += ret;
va_ctx.pkt.size -= ret;
} while (va_ctx.pkt.size > 0);
av_packet_unref(&orig_pkt);
}
這部分代碼邏輯上非常簡單,首先調(diào)用av_read_frame函數(shù),從文件中讀取一個(gè)packet的數(shù)據(jù)绩蜻,并實(shí)現(xiàn)了一個(gè)Decode_packet對這個(gè)packet進(jìn)行解碼礁苗。Decode_packet函數(shù)的實(shí)現(xiàn)如下:
int Decode_packet(IOFileName &files, DemuxingVideoAudioContex &va_ctx, int *got_frame, int cached)
{
int ret = 0;
int decoded = va_ctx.pkt.size;
static int video_frame_count = 0;
static int audio_frame_count = 0;
*got_frame = 0;
if (va_ctx.pkt.stream_index == va_ctx.video_stream_idx)
{
/* decode video frame */
ret = avcodec_decode_video2(va_ctx.video_dec_ctx, va_ctx.frame, got_frame, &va_ctx.pkt);
if (ret < 0)
{
printf("Error decoding video frame (%d)\n", ret);
return ret;
}
if (*got_frame)
{
if (va_ctx.frame->width != va_ctx.width || va_ctx.frame->height != va_ctx.height ||
va_ctx.frame->format != va_ctx.pix_fmt)
{
/* To handle this change, one could call av_image_alloc again and
* decode the following frames into another rawvideo file. */
printf("Error: Width, height and pixel format have to be "
"constant in a rawvideo file, but the width, height or "
"pixel format of the input video changed:\n"
"old: width = %d, height = %d, format = %s\n"
"new: width = %d, height = %d, format = %s\n",
va_ctx.width, va_ctx.height, av_get_pix_fmt_name((AVPixelFormat)(va_ctx.pix_fmt)),
va_ctx.frame->width, va_ctx.frame->height,
av_get_pix_fmt_name((AVPixelFormat)va_ctx.frame->format));
return -1;
}
printf("video_frame%s n:%d coded_n:%d pts:%s\n", cached ? "(cached)" : "", video_frame_count++, va_ctx.frame->coded_picture_number, va_ctx.frame->pts);
/* copy decoded frame to destination buffer:
* this is required since rawvideo expects non aligned data */
av_image_copy(va_ctx.video_dst_data, va_ctx.video_dst_linesize,
(const uint8_t **)(va_ctx.frame->data), va_ctx.frame->linesize,
va_ctx.pix_fmt, va_ctx.width, va_ctx.height);
/* write to rawvideo file */
fwrite(va_ctx.video_dst_data[0], 1, va_ctx.video_dst_bufsize, files.video_dst_file);
}
}
else if (va_ctx.pkt.stream_index == va_ctx.audio_stream_idx)
{
/* decode audio frame */
ret = avcodec_decode_audio4(va_ctx.audio_dec_ctx, va_ctx.frame, got_frame, &va_ctx.pkt);
if (ret < 0)
{
printf("Error decoding audio frame (%s)\n", ret);
return ret;
}
/* Some audio decoders decode only part of the packet, and have to be
* called again with the remainder of the packet data.
* Sample: fate-suite/lossless-audio/luckynight-partial.shn
* Also, some decoders might over-read the packet. */
decoded = FFMIN(ret, va_ctx.pkt.size);
if (*got_frame)
{
size_t unpadded_linesize = va_ctx.frame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)va_ctx.frame->format);
printf("audio_frame%s n:%d nb_samples:%d pts:%s\n",
cached ? "(cached)" : "",
audio_frame_count++, va_ctx.frame->nb_samples,
va_ctx.frame->pts);
/* Write the raw audio data samples of the first plane. This works
* fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
* most audio decoders output planar audio, which uses a separate
* plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
* In other words, this code will write only the first audio channel
* in these cases.
* You should use libswresample or libavfilter to convert the frame
* to packed data. */
fwrite(va_ctx.frame->extended_data[0], 1, unpadded_linesize, files.audio_dst_file);
}
}
/* If we use frame reference counting, we own the data and need
* to de-reference it when we don't use it anymore */
if (*got_frame && files.refcount)
av_frame_unref(va_ctx.frame);
return decoded;
}
在該函數(shù)中,首先對讀取到的packet中的stream_index分別于先前獲取的音頻和視頻的stream_index進(jìn)行對比來確定是音頻還是視頻流级解。而后分別調(diào)用相應(yīng)的解碼函數(shù)進(jìn)行解碼冒黑,以視頻流為例,判斷當(dāng)前stream為視頻流后勤哗,調(diào)用avcodec_decode_video2函數(shù)將流數(shù)據(jù)解碼為像素?cái)?shù)據(jù)抡爹,并在獲取完整的一幀之后,將其寫出到輸出文件中芒划。
3冬竟、總結(jié)
相對于前文講述過的解碼H.264格式裸碼流,解封裝+解碼過程看似多了一個(gè)步驟民逼,然而在實(shí)現(xiàn)起來實(shí)際上并無過多差別泵殴。這主要是由于FFMpeg中的多個(gè)API已經(jīng)很好地實(shí)現(xiàn)了封裝文件的解析和讀取過程,如打開文件我們使用avformat_open_input代替fopen拼苍,讀取數(shù)據(jù)包使用av_read_frame代替fread笑诅,其他方面只需要多一步判斷封裝文件中數(shù)據(jù)流的類型即可,剩余部分與裸碼流的解碼并無太多差別疮鲫。
五吆你、調(diào)用FFMpeg SDK封裝音頻和視頻為視頻文件
音頻和視頻的封裝過程為解封裝的逆過程,即將獨(dú)立的音頻數(shù)據(jù)和視頻數(shù)據(jù)按照容器文件所規(guī)定的格式封裝為一個(gè)完整的視頻文件的過程棚点。對于大多數(shù)消費(fèi)者來說早处,視頻封裝的容器是大家最為熟悉的,因?yàn)樗苯芋w現(xiàn)在了我們使用的音視頻文件擴(kuò)展名上瘫析,比較常見的有mp4砌梆、avi默责、mkv、flv等等咸包。
在進(jìn)行音頻和視頻封裝時(shí)桃序,我們將實(shí)際操作一系列音頻或視頻流數(shù)據(jù)的生成和寫入。所謂流烂瘫,指的是一系列相關(guān)聯(lián)的包的集合媒熊,這些包一般同屬于一組按照時(shí)間先后順序進(jìn)行解碼/渲染等處理的數(shù)據(jù)。在一個(gè)比較典型的視頻文件中坟比,我們通常至少會包含一個(gè)視頻流和一個(gè)音頻流芦鳍。
在FFMpeg中,表示音頻流或視頻流有一個(gè)專門的結(jié)構(gòu)葛账,即"AVStream"實(shí)現(xiàn)柠衅。該結(jié)構(gòu)主要對音頻和視頻數(shù)據(jù)的處理進(jìn)行管理和控制。另外籍琳,"AVFormatContext"結(jié)構(gòu)也是必須的菲宴,因?yàn)樗丝刂戚斎牒洼敵龅男畔ⅰ?/p>
音頻和視頻數(shù)據(jù)封裝為視頻文件的主要步驟為:
1. 相關(guān)數(shù)據(jù)結(jié)構(gòu)的準(zhǔn)備
首先,根據(jù)輸出文件的格式獲取AVFormatContext結(jié)構(gòu)趋急,獲取AVFormatContext結(jié)構(gòu)使用函數(shù)avformat_alloc_output_context2實(shí)現(xiàn)喝峦。該函數(shù)的聲明為:
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);
其中:
- ctx:輸出到AVFormatContext結(jié)構(gòu)的指針,如果函數(shù)失敗則返回給該指針為NULL呜达;
- oformat:指定輸出的AVOutputFormat類型谣蠢,如果設(shè)為NULL則使用format_name和filename生成;
- format_name:輸出格式的名稱闻丑,如果設(shè)為NULL則使用filename默認(rèn)格式漩怎;
- filename:目標(biāo)文件名,如果不使用嗦嗡,可以設(shè)為NULL勋锤;
分配AVFormatContext成功后,我們需要添加希望封裝的數(shù)據(jù)流侥祭,一般是一路視頻流+一路音頻流(可能還有其他音頻流和字幕流等)叁执。添加流首先需要查找流所包含的媒體的編碼器,這需要傳入codec_id后使用avcodec_find_encoder函數(shù)實(shí)現(xiàn)矮冬,將查找到的編碼器保存在AVCodec指針中谈宛。
之后,調(diào)用avformat_new_stream函數(shù)向AVFormatContext結(jié)構(gòu)中所代表的媒體文件中添加數(shù)據(jù)流胎署。該函數(shù)的聲明如下:
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
其中各個(gè)參數(shù)的含義:
- s:AVFormatContext結(jié)構(gòu)吆录,表示要封裝生成的視頻文件;
- c:上一步根據(jù)codec_id產(chǎn)生的編碼器指針琼牧;
- 返回值:指向生成的stream對象的指針恢筝;如果失敗則返回NULL指針哀卫。
此時(shí),一個(gè)新的AVStream便已經(jīng)加入到輸出文件中撬槽,下面就可以設(shè)置stream的id和codec等參數(shù)此改。AVStream::codec是一個(gè)AVCodecContext類型的指針變量成員,設(shè)置其中的值可以對編碼進(jìn)行配置侄柔。整個(gè)添加stream的例子如:
/* Add an output stream. */
static void add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id)
{
AVCodecContext *c;
int i;
/* find the encoder */
*codec = avcodec_find_encoder(codec_id);
if (!(*codec))
{
fprintf(stderr, "Could not find encoder for '%s'\n", avcodec_get_name(codec_id));
exit(1);
}
ost->st = avformat_new_stream(oc, *codec);
if (!ost->st)
{
fprintf(stderr, "Could not allocate stream\n");
exit(1);
}
ost->st->id = oc->nb_streams - 1;
c = ost->st->codec;
switch ((*codec)->type)
{
case AVMEDIA_TYPE_AUDIO:
c->sample_fmt = (*codec)->sample_fmts ? (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
c->bit_rate = 64000;
c->sample_rate = 44100;
if ((*codec)->supported_samplerates)
{
c->sample_rate = (*codec)->supported_samplerates[0];
for (i = 0; (*codec)->supported_samplerates[i]; i++)
{
if ((*codec)->supported_samplerates[i] == 44100)
c->sample_rate = 44100;
}
}
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
c->channel_layout = AV_CH_LAYOUT_STEREO;
if ((*codec)->channel_layouts)
{
c->channel_layout = (*codec)->channel_layouts[0];
for (i = 0; (*codec)->channel_layouts[i]; i++)
{
if ((*codec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
c->channel_layout = AV_CH_LAYOUT_STEREO;
}
}
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
{
AVRational r = { 1, c->sample_rate };
ost->st->time_base = r;
}
break;
case AVMEDIA_TYPE_VIDEO:
c->codec_id = codec_id;
c->bit_rate = 400000;
/* Resolution must be a multiple of two. */
c->width = 352;
c->height = 288;
/* timebase: This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented. For fixed-fps content,
* timebase should be 1/framerate and timestamp increments should be
* identical to 1. */
{
AVRational r = { 1, STREAM_FRAME_RATE };
ost->st->time_base = r;
}
c->time_base = ost->st->time_base;
c->gop_size = 12; /* emit one intra frame every twelve frames at most */
c->pix_fmt = AV_PIX_FMT_YUV420P;
if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO)
{
/* just for testing, we also add B frames */
c->max_b_frames = 2;
}
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO)
{
/* Needed to avoid using macroblocks in which some coeffs overflow.
* This does not happen with normal video, it just happens here as
* the motion of the chroma plane does not match the luma plane. */
c->mb_decision = 2;
}
break;
default:
break;
}
/* Some formats want stream headers to be separate. */
if (oc->oformat->flags & AVFMT_GLOBALHEADER)
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
2. 打開音視頻
打開音視頻主要涉及到打開編碼音視頻數(shù)據(jù)所需要的編碼器共啃,以及分配相應(yīng)的frame對象。其中打開編碼器如之前一樣暂题,調(diào)用avcodec_open函數(shù)移剪,分配frame對象調(diào)用av_frame_alloc以及av_frame_get_buffer。分配frame對象的實(shí)現(xiàn)如下:
static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
AVFrame *picture;
int ret;
picture = av_frame_alloc();
if (!picture)
{
return NULL;
}
picture->format = pix_fmt;
picture->width = width;
picture->height = height;
/* allocate the buffers for the frame data */
ret = av_frame_get_buffer(picture, 32);
if (ret < 0)
{
fprintf(stderr, "Could not allocate frame data.\n");
exit(1);
}
return picture;
}
而上層打開音視頻部分的實(shí)現(xiàn)如:
void Open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg, IOParam &io)
{
int ret;
AVCodecContext *c = ost->st->codec;
AVDictionary *opt = NULL;
av_dict_copy(&opt, opt_arg, 0);
/* open the codec */
ret = avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
if (ret < 0)
{
fprintf(stderr, "Could not open video codec: %d\n", ret);
exit(1);
}
/* allocate and init a re-usable frame */
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
if (!ost->frame)
{
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
/* If the output format is not YUV420P, then a temporary YUV420P
* picture is needed too. It is then converted to the required
* output format. */
ost->tmp_frame = NULL;
if (c->pix_fmt != AV_PIX_FMT_YUV420P)
{
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height);
if (!ost->tmp_frame)
{
fprintf(stderr, "Could not allocate temporary picture\n");
exit(1);
}
}
//打開輸入YUV文件
fopen_s(&g_inputYUVFile, io.input_file_name, "rb+");
if (g_inputYUVFile == NULL)
{
fprintf(stderr, "Open input yuv file failed.\n");
exit(1);
}
}
3. 打開輸出文件并寫入文件頭
如果判斷需要寫出文件的話敢靡,則需要打開輸出文件挂滓。在這里苦银,我們可以不再定義輸出文件指針啸胧,并使用fopen打開,而是直接使用FFMpeg的API——avio_open來實(shí)現(xiàn)輸出文件的打開功能幔虏。該函數(shù)的聲明如下:
int avio_open(AVIOContext **s, const char *url, int flags);
該函數(shù)的輸入?yún)?shù)為:
- s:輸出參數(shù)纺念,返回一個(gè)AVIOContext;如果打開失敗則返回NULL想括;
- url:輸出的url或者文件的完整路徑陷谱;
- flags:控制文件打開方式,如讀方式瑟蜈、寫方式和讀寫方式烟逊;
實(shí)際的代碼實(shí)現(xiàn)方式如下:
/* open the output file, if needed */
if (!(fmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&oc->pb, io.output_file_name, AVIO_FLAG_WRITE);
if (ret < 0)
{
fprintf(stderr, "Could not open '%s': %d\n", io.output_file_name, ret);
return 1;
}
}
寫入文件頭操作是生成視頻文件中極為重要的一步,而實(shí)現(xiàn)過程卻非常簡單铺根,只需要通過函數(shù)avformat_write_header即可宪躯,該函數(shù)的聲明為:
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
其輸入?yún)?shù)實(shí)際上重要的只有第一個(gè),即標(biāo)記輸出文件的句柄對象指針位迂;options用于保存無法識別的設(shè)置項(xiàng)访雪,可以傳入一個(gè)空指針。其返回值表示寫文件頭成功與否掂林,成功則返回0臣缀,失敗則返回負(fù)的錯(cuò)誤碼。
實(shí)現(xiàn)方式如:
/* Write the stream header, if any. */
ret = avformat_write_header(oc, &opt);
if (ret < 0)
{
fprintf(stderr, "Error occurred when opening output file: %d\n",ret);
return 1;
}
4. 編碼和封裝循環(huán)
以視頻流為例泻帮。編解碼循環(huán)的過程實(shí)際上可以封裝在一個(gè)函數(shù)Write_video_frame中精置。該函數(shù)從邏輯上可以分為3個(gè)部分:獲取原始視頻信號、視頻編碼锣杂、寫入輸出文件脂倦。
(1) 讀取原始視頻數(shù)據(jù)
這一部分主要實(shí)現(xiàn)根據(jù)時(shí)長判斷是否需要繼續(xù)進(jìn)行處理饲常、讀取視頻到AVFrame和設(shè)置pts。其中時(shí)長判斷部分根據(jù)pts和AVCodecContext的time_base判斷狼讨。實(shí)現(xiàn)如下:
AVCodecContext *c = ost->st->codec;
/* check if we want to generate more frames */
{
AVRational r = { 1, 1 };
if (av_compare_ts(ost->next_pts, ost->st->codec->time_base, STREAM_DURATION, r) >= 0)
{
return NULL;
}
}
讀取視頻到AVFrame我們定義一個(gè)fill_yuv_image函數(shù)實(shí)現(xiàn):
static void fill_yuv_image(AVFrame *pict, int frame_index, int width, int height)
{
int x, y, i, ret;
/* when we pass a frame to the encoder, it may keep a reference to it
* internally;
* make sure we do not overwrite it here
*/
ret = av_frame_make_writable(pict);
if (ret < 0)
{
exit(1);
}
i = frame_index;
/* Y */
for (y = 0; y < height; y++)
{
ret = fread_s(&pict->data[0][y * pict->linesize[0]], pict->linesize[0], 1, width, g_inputYUVFile);
if (ret != width)
{
printf("Error: Read Y data error.\n");
exit(1);
}
}
/* U */
for (y = 0; y < height / 2; y++)
{
ret = fread_s(&pict->data[1][y * pict->linesize[1]], pict->linesize[1], 1, width / 2, g_inputYUVFile);
if (ret != width / 2)
{
printf("Error: Read U data error.\n");
exit(1);
}
}
/* V */
for (y = 0; y < height / 2; y++)
{
ret = fread_s(&pict->data[2][y * pict->linesize[2]], pict->linesize[2], 1, width / 2, g_inputYUVFile);
if (ret != width / 2)
{
printf("Error: Read V data error.\n");
exit(1);
}
}
}
然后進(jìn)行pts的設(shè)置贝淤,很簡單,就是上一個(gè)frame的pts遞增1:
ost->frame->pts = ost->next_pts++;
整個(gè)獲取視頻信號的實(shí)現(xiàn)如:
static AVFrame *get_video_frame(OutputStream *ost)
{
AVCodecContext *c = ost->st->codec;
/* check if we want to generate more frames */
{
AVRational r = { 1, 1 };
if (av_compare_ts(ost->next_pts, ost->st->codec->time_base, STREAM_DURATION, r) >= 0)
{
return NULL;
}
}
fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height);
ost->frame->pts = ost->next_pts++;
return ost->frame;
}
(2) 視頻編碼
視頻編碼的方式同之前幾次使用的方式相同政供,即調(diào)用avcodec_encode_video2播聪,實(shí)現(xiàn)方法如:
/* encode the image */
ret = avcodec_encode_video2(c, &pkt, frame, &got_packet);
if (ret < 0)
{
fprintf(stderr, "Error encoding video frame: %d\n", ret);
exit(1);
}
(3) 寫出編碼后的數(shù)據(jù)到輸出視頻文件
這部分的實(shí)現(xiàn)過程很簡單,方式如下:
/* rescale output packet timestamp values from codec to stream timebase */
av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;
/* Write the compressed frame to the media file. */
// log_packet(fmt_ctx, pkt);
return av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_rescale_ts函數(shù)的作用為不同time_base度量之間的轉(zhuǎn)換布隔,在這里起到的作用是將AVCodecContext的time_base轉(zhuǎn)換為AVStream中的time_base离陶。av_interleaved_write_frame函數(shù)的作用是寫出AVPacket到輸出文件。該函數(shù)的聲明為:
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
該函數(shù)的聲明也很簡單衅檀,第一個(gè)參數(shù)是之前打開并寫入文件頭的文件句柄招刨,第二個(gè)參數(shù)是寫入文件的packet。返回值為錯(cuò)誤碼哀军,成功返回0沉眶,失敗則返回一個(gè)負(fù)值。
Write_video_frame函數(shù)的整體實(shí)現(xiàn)如:
int Write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
int ret;
AVCodecContext *c;
AVFrame *frame;
int got_packet = 0;
AVPacket pkt = { 0 };
c = ost->st->codec;
frame = get_video_frame(ost);
av_init_packet(&pkt);
/* encode the image */
ret = avcodec_encode_video2(c, &pkt, frame, &got_packet);
if (ret < 0)
{
fprintf(stderr, "Error encoding video frame: %d\n", ret);
exit(1);
}
if (got_packet)
{
ret = write_frame(oc, &c->time_base, ost->st, &pkt);
}
else
{
ret = 0;
}
if (ret < 0)
{
fprintf(stderr, "Error while writing video frame: %d\n", ret);
exit(1);
}
return (frame || got_packet) ? 0 : 1;
}
以上是寫入一幀視頻數(shù)據(jù)的方法杉适,寫入音頻的方法于此大同小異谎倔。整個(gè)編碼封裝的循環(huán)上層實(shí)現(xiàn)如:
while (encode_video || encode_audio)
{
/* select the stream to encode */
if (encode_video && (!encode_audio || av_compare_ts(video_st.next_pts, video_st.st->codec->time_base, audio_st.next_pts, audio_st.st->codec->time_base) <= 0))
{
encode_video = !Write_video_frame(oc, &video_st);
if (encode_video)
{
printf("Write %d video frame.\n", videoFrameIdx++);
}
else
{
printf("Video ended, exit.\n");
}
}
else
{
encode_audio = !Write_audio_frame(oc, &audio_st);
if (encode_audio)
{
printf("Write %d audio frame.\n", audioFrameIdx++);
}
else
{
printf("Audio ended, exit.\n");
}
}
}
5. 寫入文件尾,并進(jìn)行收尾工作
寫入文件尾的數(shù)據(jù)同寫文件頭一樣簡單猿推,只需要調(diào)用函數(shù)av_write_trailer即可實(shí)現(xiàn):
int av_write_trailer(AVFormatContext *s);
該函數(shù)只有一個(gè)參數(shù)即視頻文件的句柄片习,當(dāng)返回值為0時(shí)表示函數(shù)執(zhí)行成功。
整個(gè)流程的收尾工作包括關(guān)閉文件中的數(shù)據(jù)流蹬叭、關(guān)閉輸出文件和釋放AVCodecContext對象藕咏。其中關(guān)閉數(shù)據(jù)流的實(shí)現(xiàn)方式如:
void Close_stream(AVFormatContext *oc, OutputStream *ost)
{
avcodec_close(ost->st->codec);
av_frame_free(&ost->frame);
av_frame_free(&ost->tmp_frame);
sws_freeContext(ost->sws_ctx);
swr_free(&ost->swr_ctx);
}
關(guān)閉輸出文件和釋放AVCodecContext對象:
if (!(fmt->flags & AVFMT_NOFILE))
/* Close the output file. */
avio_closep(&oc->pb);
/* free the stream */
avformat_free_context(oc);
至此,整個(gè)處理流程便結(jié)束了秽五。正確設(shè)置輸入的YUV文件就可以獲取封裝好的音視頻文件孽查。
六、調(diào)用FFMpeg SDK實(shí)現(xiàn)視頻文件的轉(zhuǎn)封裝
有時(shí)候我們可能會面對這樣的一種需求筝蚕,即我們不需要對視頻內(nèi)的音頻或視頻信號進(jìn)行什么實(shí)際的操作卦碾,只是希望能把文件的封裝格式進(jìn)行轉(zhuǎn)換,例如從avi轉(zhuǎn)換為mp4格式或者flv格式等起宽。實(shí)際上洲胖,轉(zhuǎn)封裝不需要對內(nèi)部的音視頻進(jìn)行解碼,只需要根據(jù)從輸入文件中獲取包含的數(shù)據(jù)流添加到輸出文件中坯沪,然后將輸入文件中的數(shù)據(jù)包按照規(guī)定格式寫入到輸出文件中去绿映。
1、解析命令行參數(shù)
如同之前的工程一樣,我們使用命令行參數(shù)傳入輸入和輸出的文件名叉弦。為此丐一,我們定義了如下的結(jié)構(gòu)體和函數(shù)來實(shí)現(xiàn)傳入輸入輸出文件的過程:
typedef struct _IOFiles
{
const char *inputName;
const char *outputName;
} IOFiles;
static bool hello(int argc, char **argv, IOFiles &io_param)
{
printf("FFMpeg Remuxing Demo.\nCommand format: %s inputfile outputfile\n", argv[0]);
if (argc != 3)
{
printf("Error: command line error, please re-check.\n");
return false;
}
io_param.inputName = argv[1];
io_param.outputName = argv[2];
return true;
}
在main函數(shù)執(zhí)行時(shí),調(diào)用hello函數(shù)解析命令行并保存到IOFiles結(jié)構(gòu)中:
int main(int argc, char **argv)
{
IOFiles io_param;
if (!hello(argc, argv, io_param))
{
return -1;
}
//......
}
2淹冰、所需要的結(jié)構(gòu)與初始化操作
為了實(shí)現(xiàn)視頻文件的轉(zhuǎn)封裝操作库车,我們需要以下的結(jié)構(gòu):
AVOutputFormat *ofmt = NULL;
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
然后所需要的初始化操作有打開輸入視頻文件、獲取其中的流信息和獲取輸出文件的句柄:
av_register_all();
//按封裝格式打開輸入視頻文件
if ((ret = avformat_open_input(&ifmt_ctx, io_param.inputName, NULL, NULL)) < 0)
{
printf("Error: Open input file failed.\n");
goto end;
}
//獲取輸入視頻文件中的流信息
if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
{
printf("Error: Failed to retrieve input stream information.\n");
goto end;
}
av_dump_format(ifmt_ctx, 0, io_param.inputName, 0);
//按照文件名獲取輸出文件的句柄
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, io_param.outputName);
if (!ofmt_ctx)
{
printf("Error: Could not create output context.\n");
goto end;
}
ofmt = ofmt_ctx->oformat;
3樱拴、 向輸出文件中添加Stream并打開輸出文件
在我們獲取到了輸入文件中的流信息后柠衍,保持輸入流中的codec不變,并以其為依據(jù)添加到輸出文件中:
for (unsigned int i = 0; i < ifmt_ctx->nb_streams ; i++)
{
AVStream *inStream = ifmt_ctx->streams[i];
AVStream *outStream = avformat_new_stream(ofmt_ctx, inStream->codec->codec);
if (!outStream)
{
printf("Error: Could not allocate output stream.\n");
goto end;
}
ret = avcodec_copy_context(outStream->codec, inStream->codec);
outStream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
{
outStream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
}
av_dump_format(ofmt_ctx, 0, io_param.outputName, 1);
這里調(diào)用了函數(shù)avcodec_copy_context函數(shù)晶乔,該函數(shù)的聲明如下:
int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
該函數(shù)的作用是將src表示的AVCodecContext中的內(nèi)容拷貝到dest中珍坊。
隨后,調(diào)用avio_open函數(shù)打開輸出文件:
av_dump_format(ofmt_ctx, 0, io_param.outputName, 1);
if (!(ofmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&ofmt_ctx->pb, io_param.outputName, AVIO_FLAG_WRITE);
if (ret < 0)
{
printf("Error: Could not open output file.\n");
goto end;
}
}
4正罢、寫入文件的音視頻數(shù)據(jù)
首先向輸出文件中寫入文件頭:
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0)
{
printf("Error: Could not write output file header.\n");
goto end;
}
寫入文件的視頻和音頻包數(shù)據(jù)阵漏,其實(shí)就是將音頻和視頻Packets從輸入文件中讀出來,正確設(shè)置pts和dts等時(shí)間量之后翻具,再寫入到輸出文件中去:
while (1)
{
AVStream *in_stream, *out_stream;
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/* copy packet */
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0)
{
fprintf(stderr, "Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
最后要做的就是寫入文件尾:
av_write_trailer(ofmt_ctx);
5履怯、 收尾工作
寫入輸出文件完成后,需要對打開的結(jié)構(gòu)進(jìn)行關(guān)閉或釋放等操作呛占。主要有關(guān)閉輸入輸出文件虑乖、釋放輸出文件的句柄等:
avformat_close_input(&ifmt_ctx);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_closep(&ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF)
{
fprintf(stderr, "Error failed to write packet to output file.\n");
return 1;
}
七、 FFMpeg實(shí)現(xiàn)視頻水印
視頻的水印通常指附加在原始視頻上的可見或者不可見的晾虑,與原始視頻無直接關(guān)聯(lián)的標(biāo)識。通常在有線電視畫面上電視臺的臺標(biāo)以及視頻網(wǎng)站上的logo就是典型的視頻水印的應(yīng)用場景仅叫。通常實(shí)現(xiàn)視頻水印可以通過FFMpeg提供的libavfilter庫實(shí)現(xiàn)帜篇。libavfilter庫實(shí)際上實(shí)現(xiàn)的是視頻的濾鏡功能,除了水印之外诫咱,還可以實(shí)現(xiàn)視頻幀的灰度化笙隙、平滑、翻轉(zhuǎn)坎缭、直方圖均衡竟痰、裁剪等操作。
我們這里實(shí)現(xiàn)的視頻水印等操作掏呼,完全在視頻像素域?qū)崿F(xiàn)坏快,即從一個(gè)yuv文件中讀取數(shù)據(jù)到AVFrame結(jié)構(gòu),對AVFrame結(jié)構(gòu)進(jìn)行處理后再輸出到另一個(gè)yuv文件憎夷。中間不涉及封裝或編碼解碼等操作莽鸿。
1. 解析命令行,獲取輸入輸出文件信息
我們通過與之前類似的方式,在命令行中獲取輸入祥得、輸出文件名兔沃,圖像寬高。首先定義如下的結(jié)構(gòu)體用于保存配置信息:
typedef struct _IOFiles
{
const char *inputFileName; //輸入文件名
const char *outputFileName; //輸出文件名
FILE *iFile; //輸入文件指針
FILE *oFile; //輸出文件指針
uint8_t filterIdx; //Filter索引
unsigned int frameWidth; //圖像寬度
unsigned int frameHeight; //圖像高度
}IOFiles;
在這個(gè)結(jié)構(gòu)體中级及,filterIdx用于表示當(dāng)前工程選擇哪一種filter乒疏,即希望實(shí)現(xiàn)哪一種功能。
在進(jìn)入main函數(shù)之后饮焦,調(diào)用hello函數(shù)來解析命令行參數(shù):
static int hello(int argc, char **argv, IOFiles &files)
{
if (argc < 4)
{
printf("usage: %s output_file input_file filter_index\n"
"Filter index:.\n"
"1. Color component\n"
"2. Blur\n"
"3. Horizonal flip\n"
"4. HUE\n"
"5. Crop\n"
"6. Box\n"
"7. Text\n"
"\n", argv[0]);
return -1;
}
files.inputFileName = argv[1];
files.outputFileName = argv[2];
files.frameWidth = atoi(argv[3]);
files.frameHeight = atoi(argv[4]);
files.filterIdx = atoi(argv[5]);
fopen_s(&files.iFile, files.inputFileName, "rb+");
if (!files.iFile)
{
printf("Error: open input file failed.\n");
return -1;
}
fopen_s(&files.oFile, files.outputFileName, "wb+");
if (!files.oFile)
{
printf("Error: open output file failed.\n");
return -1;
}
return 0;
}
該函數(shù)實(shí)現(xiàn)了輸入輸出文件的文件名獲取并打開缰雇,并讀取filter索引。
2. Video Filter初始化
在進(jìn)行初始化之前追驴,必須調(diào)用filter的init函數(shù)械哟,之后才能針對Video Filter進(jìn)行各種操作。其聲明如下:
void avfilter_register_all(void);
為了實(shí)現(xiàn)視頻水印的功能殿雪,所需要的相關(guān)結(jié)構(gòu)主要有:
AVFilterContext *buffersink_ctx;
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
其中AVFilterContext用于表示一個(gè)filter的實(shí)例上下文暇咆,AVFilterGraph表示一個(gè)video filtering的工作流。Video Filter的初始化實(shí)現(xiàn)如以下函數(shù):
//初始化video filter相關(guān)的結(jié)構(gòu)
int Init_video_filter(const char *filter_descr, int width, int height)
{
char args[512];
AVFilter *buffersrc = avfilter_get_by_name("buffer");
AVFilter *buffersink = avfilter_get_by_name("buffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
AVBufferSinkParams *buffersink_params;
filter_graph = avfilter_graph_alloc();
/* buffer video source: the decoded frames from the decoder will be inserted here. */
snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", width,height,AV_PIX_FMT_YUV420P, 1, 25,1,1);
int ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph);
if (ret < 0)
{
printf("Error: cannot create buffer source.\n");
return ret;
}
/* buffer video sink: to terminate the filter chain. */
buffersink_params = av_buffersink_params_alloc();
buffersink_params->pixel_fmts = pix_fmts;
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, buffersink_params, filter_graph);
av_free(buffersink_params);
if (ret < 0)
{
printf("Error: cannot create buffer sink\n");
return ret;
}
/* Endpoints for the filter graph. */
outputs->name = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
inputs->name = av_strdup("out");
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, NULL)) < 0)
{
printf("Error: avfilter_graph_parse_ptr failed.\n");
return ret;
}
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
{
printf("Error: avfilter_graph_config");
return ret;
}
return 0;
}
3. 初始化輸入輸出AVFrame并分配內(nèi)存
我們首先聲明AVFrame類型的對象和指向像素緩存的指針:
AVFrame *frame_in = NULL;
AVFrame *frame_out = NULL;
unsigned char *frame_buffer_in = NULL;
unsigned char *frame_buffer_out = NULL;
然后分配AVFrame對象丙曙,并分配其中的緩存區(qū):
void Init_video_frame_in_out(AVFrame **frameIn, AVFrame **frameOut, unsigned char **frame_buffer_in, unsigned char **frame_buffer_out, int frameWidth, int frameHeight)
{
*frameIn = av_frame_alloc();
*frame_buffer_in = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, frameWidth,frameHeight,1));
av_image_fill_arrays((*frameIn)->data, (*frameIn)->linesize,*frame_buffer_in, AV_PIX_FMT_YUV420P,frameWidth,frameHeight,1);
*frameOut = av_frame_alloc();
*frame_buffer_out = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, frameWidth,frameHeight,1));
av_image_fill_arrays((*frameOut)->data, (*frameOut)->linesize,*frame_buffer_out, AV_PIX_FMT_YUV420P,frameWidth,frameHeight,1);
(*frameIn)->width = frameWidth;
(*frameIn)->height = frameHeight;
(*frameIn)->format = AV_PIX_FMT_YUV420P;
}
4. Video Filtering循環(huán)體
這一部分主要包括三大部分:
- 讀取原始的YUV數(shù)據(jù)到輸入的frame爸业;
- 使用預(yù)先定義好的filter_graph處理輸入frame,生成輸出frame亏镰;
- 將輸出frame中的像素值寫入輸出yuv文件扯旷;
第一部分,讀取原始yuv的實(shí)現(xiàn)由自定義函數(shù)Read_yuv_data_to_buf實(shí)現(xiàn):
//從輸入yuv文件中讀取數(shù)據(jù)到buffer和frame結(jié)構(gòu)
bool Read_yuv_data_to_buf(unsigned char *frame_buffer_in, const IOFiles &files, AVFrame **frameIn)
{
AVFrame *pFrameIn = *frameIn;
int width = files.frameWidth, height = files.frameHeight;
int frameSize = width * height * 3 / 2;
if (fread_s(frame_buffer_in, frameSize, 1, frameSize, files.iFile) != frameSize)
{
return false;
}
pFrameIn->data[0] = frame_buffer_in;
pFrameIn->data[1] = pFrameIn->data[0] + width * height;
pFrameIn->data[2] = pFrameIn->data[1] + width * height / 4;
return true;
}
第二部分實(shí)際上分為兩部分索抓,即將輸入frame送入filter graph钧忽,以及從filter graph中取出輸出frame。實(shí)現(xiàn)方法分別為:
//將待處理的輸入frame添加進(jìn)filter graph
bool Add_frame_to_filter(AVFrame *frameIn)
{
if (av_buffersrc_add_frame(buffersrc_ctx, frameIn) < 0)
{
return false;
}
return true;
}
//從filter graph中獲取輸出frame
int Get_frame_from_filter(AVFrame **frameOut)
{
if (av_buffersink_get_frame(buffersink_ctx, *frameOut) < 0)
{
return false;
}
return true;
}
第三部分逼肯,寫出輸出frame到輸出yuv文件:
//從輸出frame中寫出像素?cái)?shù)據(jù)到輸出文件
void Write_yuv_to_outfile(const AVFrame *frame_out, IOFiles &files)
{
if(frame_out->format==AV_PIX_FMT_YUV420P)
{
for(int i=0;i<frame_out->height;i++)
{
fwrite(frame_out->data[0]+frame_out->linesize[0]*i,1,frame_out->width,files.oFile);
}
for(int i=0;i<frame_out->height/2;i++)
{
fwrite(frame_out->data[1]+frame_out->linesize[1]*i,1,frame_out->width/2,files.oFile);
}
for(int i=0;i<frame_out->height/2;i++)
{
fwrite(frame_out->data[2]+frame_out->linesize[2]*i,1,frame_out->width/2,files.oFile);
}
}
}
該部分的綜合實(shí)現(xiàn)如下:
while (Read_yuv_data_to_buf(frame_buffer_in, files, &frame_in))
{
//將輸入frame添加到filter graph
if (!Add_frame_to_filter(frame_in))
{
printf("Error while adding frame.\n");
goto end;
}
//從filter graph中獲取輸出frame
if (!Get_frame_from_filter(&frame_out))
{
printf("Error while getting frame.\n");
goto end;
}
//將輸出frame寫出到輸出文件
Write_yuv_to_outfile(frame_out, files);
printf("Process 1 frame!\n");
av_frame_unref(frame_out);
}
5耸黑、 收尾工作
整體實(shí)現(xiàn)完成后,需要進(jìn)行善后的收尾工作有釋放輸入和輸出frame篮幢、關(guān)閉輸入輸出文件大刊,以及釋放filter graph:
//關(guān)閉文件及相關(guān)結(jié)構(gòu)
fclose(files.iFile);
fclose(files.oFile);
av_frame_free(&frame_in);
av_frame_free(&frame_out);
avfilter_graph_free(&filter_graph);
八、 FFMpeg實(shí)現(xiàn)視頻縮放
視頻縮放是視頻開發(fā)中一項(xiàng)最基本的功能三椿。通過對視頻的像素?cái)?shù)據(jù)進(jìn)行采樣或插值缺菌,可以將低分辨率的視頻轉(zhuǎn)換到更高的分辨率,或者將高分辨率的視頻轉(zhuǎn)換為更低的分辨率搜锰。通過FFMpeg提供了libswscale庫伴郁,可以輕松實(shí)現(xiàn)視頻的分辨率轉(zhuǎn)換功能。除此之外纽乱,libswscale庫還可以實(shí)現(xiàn)顏色空間轉(zhuǎn)換等功能蛾绎。
通常情況下視頻縮放的主要思想是對視頻進(jìn)行解碼到像素域后,針對像素域的像素值進(jìn)行采樣或差值操作。這種方式需要在解碼端消耗一定時(shí)間租冠,但是通用性最好鹏倘,不需要對碼流格式作出任何特殊處理。在FFMpeg中l(wèi)ibswscale庫也是針對AVFrame結(jié)構(gòu)進(jìn)行縮放處理顽爹。
1. 解析命令行參數(shù)
輸入輸出的數(shù)據(jù)使用以下結(jié)構(gòu)進(jìn)行封裝:
typedef struct _IOFiles
{
char *inputName; //輸入文件名
char *outputName; //輸出文件名
char *inputFrameSize; //輸入圖像的尺寸
char *outputFrameSize; //輸出圖像的尺寸
FILE *iFile; //輸入文件指針
FILE *oFile; //輸出文件指針
} IOFiles;
輸入?yún)?shù)解析過程為:
static bool hello(int argc, char **argv, IOFiles &files)
{
printf("FFMpeg Scaling Demo.\nCommand format: %s input_file input_frame_size output_file output_frame_size\n", argv[0]);
if (argc != 5)
{
printf("Error: command line error, please re-check.\n");
return false;
}
files.inputName = argv[1];
files.inputFrameSize = argv[2];
files.outputName = argv[3];
files.outputFrameSize = argv[4];
fopen_s(&files.iFile, files.inputName, "rb+");
if (!files.iFile)
{
printf("Error: cannot open input file.\n");
return false;
}
fopen_s(&files.oFile, files.outputName, "wb+");
if (!files.oFile)
{
printf("Error: cannot open output file.\n");
return false;
}
return true;
}
在參數(shù)讀入完成后纤泵,需要從表示視頻分辨率的字符串中解析出圖像的寬和高兩個(gè)值。我們在命令行中傳入的視頻分辨率字符串的格式為“width x height”镜粤,例如"720x480"捏题。解析過程需要調(diào)用av_parse_video_size函數(shù)。聲明如下:
int av_parse_video_size(int *width_ptr, int *height_ptr, const char *str);
例如肉渴,我們傳入下面的參數(shù):
int frameWidth, frameHeight;
av_parse_video_size(&frameWidth, &frameHeight, "720x480");
函數(shù)將分別把720和480傳入frameWidth和frameHeight中公荧。
在獲取命令行參數(shù)后,調(diào)用該函數(shù)解析圖像分辨率:
int srcWidth, srcHeight, dstWidth, dstHeight;
if (av_parse_video_size(&srcWidth, &srcHeight, files.inputFrameSize))
{
printf("Error: parsing input size failed.\n");
goto end;
}
if (av_parse_video_size(&dstWidth, &dstHeight, files.outputFrameSize))
{
printf("Error: parsing output size failed.\n");
goto end;
}
這樣同规,我們就獲得了源和目標(biāo)圖像的寬和高度循狰。
2. 創(chuàng)建SwsContext結(jié)構(gòu)
進(jìn)行視頻的縮放操作離不開libswscale的一個(gè)關(guān)鍵的結(jié)構(gòu),即SwsContext券勺,該結(jié)構(gòu)提供了縮放操作的必要參數(shù)绪钥。創(chuàng)建該結(jié)構(gòu)需調(diào)用函數(shù)sws_getContext。該函數(shù)的聲明如下:
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
該函數(shù)的前兩行參數(shù)分別表示輸入和輸出圖像的寬关炼、高程腹、像素格式,參數(shù)flags表示采樣和差值使用的算法儒拂,常用的有SWS_BILINEAR表示雙線性差值等寸潦。剩余的不常用參數(shù)通常設(shè)為NULL。創(chuàng)建該結(jié)構(gòu)的代碼如:
//創(chuàng)建SwsContext結(jié)構(gòu)
enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_YUV420P;
enum AVPixelFormat dst_pix_fmt = AV_PIX_FMT_YUV420P;
struct SwsContext *sws_ctx = sws_getContext(srcWidth, srcHeight, src_pix_fmt, dstWidth, dstHeight, dst_pix_fmt, SWS_BILINEAR, NULL,NULL,NULL );
if (!sws_ctx)
{
printf("Error: parsing output size failed.\n");
goto end;
}
3. 分配像素緩存
視頻縮放實(shí)際上是在像素域?qū)崿F(xiàn)侣灶,但是實(shí)際上我們沒有必要真的建立一個(gè)個(gè)AVFrame對象甸祭,我們只需要其像素緩存空間即可,我們定義兩個(gè)指針數(shù)組和兩個(gè)保存stride的數(shù)組褥影,并為其分配內(nèi)存區(qū)域:
//分配input和output
uint8_t *src_data[4], *dst_data[4];
int src_linesize[4], dst_linesize[4];
if ((ret = av_image_alloc(src_data, src_linesize, srcWidth, srcHeight, src_pix_fmt, 32)) < 0)
{
printf("Error: allocating src image failed.\n");
goto end;
}
if ((ret = av_image_alloc(dst_data, dst_linesize, dstWidth, dstHeight, dst_pix_fmt, 1)) < 0)
{
printf("Error: allocating dst image failed.\n");
goto end;
}
4. 循環(huán)處理輸入frame
循環(huán)處理的代碼為:
//從輸出frame中寫出到輸出文件
int dst_bufsize = ret;
for (int idx = 0; idx < MAX_FRAME_NUM; idx++)
{
read_yuv_from_ifile(src_data, src_linesize, srcWidth, srcHeight, 0, files);
read_yuv_from_ifile(src_data, src_linesize, srcWidth, srcHeight, 1, files);
read_yuv_from_ifile(src_data, src_linesize, srcWidth, srcHeight, 2, files);
sws_scale(sws_ctx, (const uint8_t * const*)src_data, src_linesize, 0, srcHeight, dst_data, dst_linesize);
fwrite(dst_data[0], 1, dst_bufsize, files.oFile);
}
其核心函數(shù)為sws_scale,其聲明為:
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
該函數(shù)的各個(gè)參數(shù)比較容易理解咏雌,除了第一個(gè)是之前創(chuàng)建的SwsContext之外凡怎,其他基本上都是源和目標(biāo)圖像的緩存區(qū)和大小等。在寫完一幀后赊抖,調(diào)用fwrite將輸出的目標(biāo)圖像寫入輸出yuv文件中统倒。
5. 收尾工作
收尾工作除了釋放緩存區(qū)和關(guān)閉輸入輸出文件之外,就是需要釋放SwsContext結(jié)構(gòu)氛雪,需調(diào)用函數(shù):sws_freeContext房匆。實(shí)現(xiàn)過程為:
fclose(files.iFile);
fclose(files.oFile);
av_freep(&src_data[0]);
av_freep(&dst_data[0]);
sws_freeContext(sws_ctx);
參考資源
- 殷汶杰-yinwenjie的項(xiàng)目FFmpeg_Tutorial
- https://ffmpeg.org/
- 雷霄驊的CSDN博客:https://blog.csdn.net/leixiaohua1020
- 雷霄驊的Github主頁:https://github.com/leixiaohua1020
- [總結(jié)]FFMPEG視音頻編解碼零基礎(chǔ)學(xué)習(xí)方法,地址為:https://blog.csdn.net/leixiaohua1020/article/details/15811977。
- 夏曹俊老師的FFMPEG中文網(wǎng)站https://ffmpeg.club/