FFmpeg Tutorial

本文轉(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)行視頻編碼的主要流程如:

  1. 首先解析笤闯、處理輸入?yún)?shù),如編碼器的參數(shù)棍厂、圖像的參數(shù)颗味、輸入輸出文件;
  2. 建立整個(gè)FFMpeg編碼器的各種組件工具牺弹,順序依次為:avcodec_register_all -> avcodec_find_encoder -> avcodec_alloc_context3 -> avcodec_open2 -> av_frame_alloc -> av_image_alloc;
  3. 編碼循環(huán):av_init_packet -> avcodec_encode_video2(兩次) -> av_packet_unref
  4. 關(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)體

這一部分主要包括三大部分:

  1. 讀取原始的YUV數(shù)據(jù)到輸入的frame爸业;
  2. 使用預(yù)先定義好的filter_graph處理輸入frame,生成輸出frame亏镰;
  3. 將輸出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);

參考資源

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浴鸿,一起剝皮案震驚了整個(gè)濱河市井氢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岳链,老刑警劉巖花竞,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掸哑,居然都是意外死亡约急,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門苗分,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厌蔽,“玉大人,你說我怎么就攤上這事摔癣∨” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵供填,是天一觀的道長拐云。 經(jīng)常有香客問我,道長近她,這世上最難降的妖魔是什么叉瘩? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮粘捎,結(jié)果婚禮上薇缅,老公的妹妹穿的比我還像新娘。我一直安慰自己攒磨,他們只是感情好泳桦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著娩缰,像睡著了一般灸撰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拼坎,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天浮毯,我揣著相機(jī)與錄音,去河邊找鬼泰鸡。 笑死债蓝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盛龄。 我是一名探鬼主播饰迹,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芳誓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了啊鸭?” 一聲冷哼從身側(cè)響起锹淌,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎莉掂,沒想到半個(gè)月后葛圃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡憎妙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年库正,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厘唾。...
    茶點(diǎn)故事閱讀 39,754評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡褥符,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抚垃,到底是詐尸還是另有隱情喷楣,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布鹤树,位于F島的核電站铣焊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏罕伯。R本人自食惡果不足惜曲伊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望追他。 院中可真熱鬧坟募,春花似錦、人聲如沸邑狸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单雾。三九已至赚哗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間硅堆,已是汗流浹背蜂奸。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硬萍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓围详,卻偏偏與公主長得像朴乖,于是被迫代替她去往敵國和親祖屏。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評論 2 354