跨平臺播放器開發(fā) (四) 開發(fā)一個播放器需要用到哪些 FFmpeg 知識
前言
咱們前面三篇文章主要介紹了如何在各個主流平臺下配置開發(fā)環(huán)境,那么從該篇開始就真正進(jìn)入編碼了。由于該系列定義為「從 0 到 1 「寫一個跨平臺播放器,所以我打算」從淺到深」,從「基礎(chǔ)到進(jìn)階」的路線來進(jìn)行。
咱們先來看一個流程圖:

該系列文章就是將上圖拆分為具體的代碼模塊,那么該篇咱們主要講解如何利用FFmpeg API 來對一個輸入數(shù)據(jù)進(jìn)行解封裝,讀取原始音頻視頻信息,然后對音頻視頻做一些基本操作?;旧显诓シ牌髂K中用到的FFmpeg API 咱們都要對它有一個了解。
?ps: 如果對 Mac OS 、Windows 、Linux 下不知道怎么配置 QT & FFmpeg 環(huán)境的可以參考下面文章
跨平臺播放器開發(fā) (一) QT for MAC OS & FFmpeg 環(huán)境搭建
跨平臺播放器開發(fā) (二) QT for Linux & FFmpeg 環(huán)境搭建
跨平臺播放器開發(fā) (三) QT for Windows & FFmpeg 環(huán)境搭建
?
FFmpeg 基礎(chǔ)
解封裝
利用 FFmpeg api 來對輸入視頻進(jìn)行解封裝,先來看一下使用 api 的流程

看完上圖是不是對解封裝的 API 有一個大概的了解? 從一個 輸入 URL到讀取到 「壓縮數(shù)據(jù)流」 就這么幾步,很簡單的,下面我們用代碼實際演示一下:
1.注冊所有函數(shù)
av_register_all()
其實在最新的版本中該函數(shù)已經(jīng)過時了,在最低的版本中還是必須調(diào)用該函數(shù)的。

2.注冊網(wǎng)絡(luò)模塊
//初始化網(wǎng)絡(luò)庫(可以打開?rtmp、rtsp、http?等協(xié)議的流媒體視頻)?
avformat_network_init();
3.打開輸入流并讀取頭信息
??//參數(shù)設(shè)置
????AVDictionary?*opts?=?NULL;
????//設(shè)置rtsp流以tcp協(xié)議打開
????av_dict_set(&opts,?"rtsp_transport",?"tcp",?0);
????//網(wǎng)絡(luò)延時時間
????av_dict_set(&opts,?"max_delay",?"1000",?0);
????//解封裝上下文
????AVFormatContext?*ic?=?NULL;
????int?re?=?avformat_open_input(
????????????&ic,
????????????inpath,
????????????0,??//?0表示自動選擇解封器
????????????&opts?//參數(shù)設(shè)置,比如rtsp的延時時間
????);
??//返回值?0?成功
????if?(re?!=?0)?{
????????char?buf[1024]?=?{0};
????????av_strerror(re,?buf,?sizeof(buf)?-?1);
????????cout?<<?"open?"?<<?inpath?<<?"?failed!?:"?<<?buf?<<?endl;
????????getchar();
????????return?-1;
????}
這里要注意,調(diào)用該函數(shù)那么在結(jié)尾處一定要調(diào)用 avformat_close_input()
4.讀取媒體文件數(shù)據(jù)包
//return?>=0?if?OK,?AVERROR_xxx?on?error?
re?=?avformat_find_stream_info(ic,?0);
//打印視頻流詳細(xì)信息
av_dump_format(ic,?0,?inpath,?0);
5.獲取音視頻流信息
- 通過遍歷的方式獲取
????//獲取音視頻流信息?(遍歷,函數(shù)獲?。?/span>
????for?(int?i?=?0;?i?<?ic->nb_streams;?i++)?{
????????AVStream?*as?=?ic->streams[i];
????????cout?<<?"codec_id?=?"?<<?as->codecpar->codec_id?<<?endl;
????????cout?<<?"format?=?"?<<?as->codecpar->format?<<?endl;
????????//音頻?AVMEDIA_TYPE_AUDIO
????????if?(as->codecpar->codec_type?==?AVMEDIA_TYPE_AUDIO)?{
????????????audioStream?=?i;
????????????cout?<<?i?<<?"音頻信息"?<<?endl;
????????????cout?<<?"sample_rate?=?"?<<?as->codecpar->sample_rate?<<?endl;
????????????//AVSampleFormat;
????????????cout?<<?"channels?=?"?<<?as->codecpar->channels?<<?endl;
????????????//一幀數(shù)據(jù)???單通道樣本數(shù)
????????????cout?<<?"frame_size?=?"?<<?as->codecpar->frame_size?<<?endl;
????????????//1024?*?2?*?2?=?4096??fps?=?sample_rate/frame_size
????????}
????????????//視頻?AVMEDIA_TYPE_VIDEO
????????else?if?(as->codecpar->codec_type?==?AVMEDIA_TYPE_VIDEO)?{
????????????videoStream?=?i;
????????????cout?<<?i?<<?"視頻信息"?<<?endl;
????????????cout?<<?"width="?<<?as->codecpar->width?<<?endl;
????????????cout?<<?"height="?<<?as->codecpar->height?<<?endl;
????????????//幀率?fps?分?jǐn)?shù)轉(zhuǎn)換
????????????cout?<<?"video?fps?=?"?<<?r2d(as->avg_frame_rate)?<<?endl;
????????}
????}
- 通過 API 方式獲取
//獲取視頻流
videoStream?=?av_find_best_stream(ic,?AVMEDIA_TYPE_VIDEO,?-1,?-1,?NULL,?0);
AVStream?*as?=?ic->streams[videoStream];
cout?<<?i?<<?"視頻信息"?<<?endl;
cout?<<?"width="?<<?as->codecpar->width?<<?endl;
cout?<<?"height="?<<?as->codecpar->height?<<?endl;
//幀率?fps?分?jǐn)?shù)轉(zhuǎn)換
cout?<<?"video?fps?=?"?<<?r2d(as->avg_frame_rate)?<<?endl;????
6.讀取壓縮數(shù)據(jù)包
????AVPacket?*pkt?=?av_packet_alloc();
????for?(;;)?{
????????int?re?=?av_read_frame(ic,?pkt);
????????if?(re?!=?0)?{
????????????//循環(huán)播放
????????????cout?<<?"==============================end=============================="?<<?endl;
????????????break;
????????}
????????cout?<<?"pkt->size?=?"?<<?pkt->size?<<?endl;
????????//顯示的時間
????????cout?<<?"pkt->pts?=?"?<<?pkt->pts?<<?endl;
????????//轉(zhuǎn)換為毫秒,方便做同步
????????cout?<<?"pkt->pts?ms?=?"?<<?pkt->pts?*?(r2d(ic->streams[pkt->stream_index]->time_base)?*?1000)?<<?endl;
????????//解碼時間
????????cout?<<?"pkt->dts?=?"?<<?pkt->dts?<<?endl;
????????if?(pkt->stream_index?==?videoStream)?{
????????????cout?<<?"圖像"?<<?endl;
????????}
????????if?(pkt->stream_index?==?audioStream)?{
????????????cout?<<?"音頻"?<<?endl;
????????}
????????//釋放,引用計數(shù)-1?為0釋放空間
????????av_packet_unref(pkt);
????}
調(diào)試之后的 log

解碼
調(diào)用 ffmpeg api 來對音視頻壓縮數(shù)據(jù)解碼的話,其實也很簡單,主要使用如下幾個 api ,見下圖:

我們接著在解封裝的代碼基礎(chǔ)上進(jìn)行添加,代碼如下:
????//找到視頻解碼器
????AVCodec?*vcodec?=?avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
????if?(!vcodec)?{
????????cout?<<?"can't?find?the?codec?id"?<<?ic->streams[videoStream]->codecpar->codec_id?<<?endl;
????????getchar();
????????return?-1;
????}
????//創(chuàng)建視頻解碼器上下文
????AVCodecContext?*vctx?=?avcodec_alloc_context3(vcodec);
????//配置解碼器上下文參數(shù)
????avcodec_parameters_to_context(vctx,?ic->streams[videoStream]->codecpar);
????//配置解碼線程
????vctx->thread_count?=?8;
????//打開解碼器上下文
????re?=?avcodec_open2(vctx,?0,?0);
????if?(re?!=?0)?{
????????char?buf[1024]?=?{0};
????????av_strerror(re,?buf,?sizeof(buf)?-?1);
????????cout?<<?"video?avcodec_open2?failed!"?<<?buf?<<?endl;
????????getchar();
????????return?-1;
????}
????cout?<<?"video?avcodec_open2?success!"?<<?endl;
找解碼器也可以通過如下 API 形式進(jìn)行:
AVCodec?*avcodec_find_decoder_by_name(const?char?*name);
如果想要打開音頻解碼器,代碼一樣,換下參數(shù)即可,下面進(jìn)行真正解碼:
????//malloc?AVPacket并初始化
????AVPacket?*pkt?=?av_packet_alloc();
????//接收解碼的原始數(shù)據(jù)
????AVFrame?*frame?=?av_frame_alloc();
????for?(;;)?{
????????int?re?=?av_read_frame(ic,?pkt);
????????if?(re?!=?0)?{
????????????break;
????????}
????????//解碼視頻
????????//發(fā)送?packet?到解碼線程
????????re?=?avcodec_send_packet(avcc,?pkt);
????????//釋放,引用計數(shù)-1?為0釋放空間
????????av_packet_unref(pkt);
????
????????//一次?send?可能對于多次?receive
????????for?(;;)?{
????????????re?=?avcodec_receive_frame(avcc,?frame);
????????????if?(re?!=?0)break;
????????????//釋放,引用計數(shù)-1?為0釋放空間
???????????av_frame_unref(frame);
????}
這樣就可以進(jìn)行解碼了,現(xiàn)在我們添加一些打印參數(shù),比如音頻采樣信息,視頻寬高信息:

?總時長:totalMs = 10534 ms
視頻信息:
bitrate=907_500
fps = 30.0003
codec_id = 86018
format = AV_PIX_FMT_YUV420P 1152
1080 - 1920
pict_type= AV_PICTURE_TYPE_I
音頻信息:
sample_rate = 48000
channels = 2
?
視頻像素格式轉(zhuǎn)換
視頻像素格式其實就是 YUV 轉(zhuǎn) RGB 的一個過程, FFmpeg 也提供了對應(yīng)的 API ,它是使用 CPU 運算能力來轉(zhuǎn)換,效率是比較低的。咱們播放器使用 OpenGL GPU 來轉(zhuǎn),效率比較高。雖然 FFmpeg API 轉(zhuǎn)換效率比較低,但是我們還是可以學(xué)習(xí)一下的。使用流程如下:

僅僅 2 個 API 就可以達(dá)到對 YUV 的轉(zhuǎn)換或者裁剪,代碼示例:
????????????????const?int?in_width?=?frame->width;
????????????????const?int?in_height?=?frame->height;
????????????????const?int?out_width?=?in_width?/?2;
????????????????const?int?out_height?=?in_height?/?2;????????????????
???????/**
?????????????????*?@param?context???:?縮放上下文,如果為?NULL,那么內(nèi)部會進(jìn)行創(chuàng)建,
?????????????????*????????????如果已經(jīng)存在,參數(shù)也沒有發(fā)生變化,那么就直接返回當(dāng)前,否者釋放縮放上下文,重新創(chuàng)建。
?????????????????*?@param?srcW??????:?輸入的寬
?????????????????*?@param?srcH??????:?輸入的高
?????????????????*?@param?srcFormat?:?輸入的格式
?????????????????*?@param?dstW????:?輸出的寬
?????????????????*?@param?dstH????:?輸出的高
?????????????????*?@param?dstFormat?:?輸出的格式
?????????????????*?@param?flags???:?提供了一系列的算法,快速線性,差值,矩陣,不同的算法性能也不同,
????????????????????????????快速線性算法性能相對較高??倳r長只針對尺寸的變換。
?????????????????*?@param?srcFilter?:?輸入過濾器
?????????????????*?@param?dstFilter?:?輸出過濾器
?????????????????*?@param?param???:?這個跟?flags?算法相關(guān),一般傳入?O
?????????????????*?@return??????????:?縮放的上下文
?????????????????*/
????????????????vsctx?=?sws_getCachedContext(
????????????????????????vsctx,//傳入NULL 會新創(chuàng)建
????????????????????????in_width,?in_height,?(AVPixelFormat)?frame->format,?//輸入的寬高,格式
????????????????????????out_width,?out_height,?AV_PIX_FMT_RGBA,?//輸出的寬高,格式
????????????????????????SWS_BILINEAR,?//尺寸變換的算法
????????????????????????0,?0,?0
????????????????);
????????????????????/**
?????????????????????*?@param?c?????????縮放上下文
?????????????????????*?@param?srcSlice??YUV?切換數(shù)據(jù)可以是指針,也可以是數(shù)組
?????????????????????*?@param?srcStride?對應(yīng)?YUV?一行的大小
?????????????????????*?@param?srcSliceY?這個用不到傳入?0?即可
?????????????????????*?@param?srcSliceH?YUV?的高
?????????????????????*?@param?dst???????輸出的像素格式數(shù)據(jù)
?????????????????????*?@param?dstStride?輸出的像素格式數(shù)據(jù)的大小
?????????????????????*?@return??????????返回轉(zhuǎn)換后的高
?????????????????????*/
????????????????????re?=?sws_scale(vsctx,
???????????????????????????????????frame->data,?//輸入數(shù)據(jù)
???????????????????????????????????frame->linesize,//輸入行大小
???????????????????????????????????0,
???????????????????????????????????frame->height,//輸出高度
???????????????????????????????????(uint8_t?*const?*)?(data),?//輸出數(shù)據(jù)
???????????????????????????????????lines//輸出大小
????????????????????);
上面的注釋都很詳細(xì),相信大家也能看的明白,最后我們看下調(diào)試后的log,如下:
?像素格式尺寸轉(zhuǎn)換上下文創(chuàng)建或者獲取成功!
in_width=1080
in_height=1920
out_width=540
out_height=960
sws_scale success! return height of the output slice =960 ===============? ?end? ?=================
?
重采樣
重采樣的意思就是將音頻的輸入?yún)?shù)統(tǒng)一輸出某個特定的值,這樣做的好處就是歸一化播放器的聲音輸出。那么怎么使用 FFmpeg API 來進(jìn)行重采樣呢? 先來看一張流程圖:

我們還是以之前的代碼繼續(xù)寫,
「我們統(tǒng)一輸出的參數(shù)為 sample_rate=48000,sample_channel=2,sample_fml=AV_SAMPLE_FMT_S16」
????...
??//音頻重采樣
????SwrContext?*asctx?=?swr_alloc();
????//設(shè)置重采樣參數(shù)
????asctx?=?swr_alloc_set_opts(asctx?//重采樣上下文
????????????,?av_get_default_channel_layout(2)//輸出聲道格式
????????????,?AV_SAMPLE_FMT_S16?//輸出聲音樣本格式
????????????,?48000?//輸出采樣率
????????????,?av_get_default_channel_layout(actx->channels)//輸入通道數(shù)
????????????,?actx->sample_fmt????????????????????????????//輸入聲音樣本格式
????????????,?actx->sample_rate,?0,?0??//輸入音頻采樣率
????);
????//初始化采樣上下文
????re?=?swr_init(asctx);
????if?(re?!=?0)?{
????????char?buf[1024]?=?{0};
????????av_strerror(re,?buf,?sizeof(buf)?-?1);
????????cout?<<?"audio?swr_init?failed!"?<<?buf?<<?endl;
????????return?-1;
????}
??...
????//重采樣之后存入的數(shù)據(jù)
????unsigned?char?*pcm?=?NULL;
????for?(;;)?{
????????int?re?=?av_read_frame(ic,?pkt);
????????if?(re?!=?0)?{
????????????//循環(huán)播放
????????????cout?<<?"==============================end=============================="?<<?endl;
//????????????int?ms?=?3000;?//三秒位置?根據(jù)時間基數(shù)(分?jǐn)?shù))轉(zhuǎn)換
//????????????long?long?pos?=?(double)?ms?/?(double)?1000?*?r2d(ic->streams[pkt->stream_index]->time_base);
//????????????av_seek_frame(ic,?videoStream,?pos,?AVSEEK_FLAG_BACKWARD?|?AVSEEK_FLAG_FRAME);
//????????????continue;
????????????break;
????????}
????????cout?<<?"pkt->size?=?"?<<?pkt->size?<<?endl;
????????//顯示的時間
????????cout?<<?"pkt->pts?=?"?<<?pkt->pts?<<?endl;
????????//轉(zhuǎn)換為毫秒,方便做同步
????????cout?<<?"pkt->pts?ms?=?"?<<?pkt->pts?*?(r2d(ic->streams[pkt->stream_index]->time_base)?*?1000)?<<?endl;
????????//解碼時間
????????cout?<<?"pkt->dts?=?"?<<?pkt->dts?<<?endl;
????????AVCodecContext?*avcc?=?NULL;
????????if?(pkt->stream_index?==?videoStream)?{
????????????cout?<<?"圖像"?<<?endl;
????????????avcc?=?vctx;
????????}
????????if?(pkt->stream_index?==?audioStream)?{
????????????cout?<<?"音頻"?<<?endl;
????????????avcc?=?actx;
????????}
????????//解碼視頻
????????//發(fā)送?packet?到解碼線程
????????re?=?avcodec_send_packet(avcc,?pkt);
????????//釋放,引用計數(shù)-1?為0釋放空間
????????av_packet_unref(pkt);
????????if?(re?!=?0)?{
????????????char?buf[1024]?=?{0};
????????????av_strerror(re,?buf,?sizeof(buf)?-?1);
????????????cout?<<?"video?avcodec_send_packet?failed!"?<<?buf?<<?endl;
????????????continue;
????????}
????????//一次?send?可能對于多次?receive
????????for?(;;)?{
????????????re?=?avcodec_receive_frame(avcc,?frame);
????????????if?(re?!=?0)break;
?
??????????????...
????????????????
??????????????if?(avcc?==?actx)?{//音頻
????????????????uint8_t?*data[2]?=?{0};
????????????????if?(!pcm)?pcm?=?new?uint8_t[frame->nb_samples?*?16/8?*?2];
????????????????data[0]?=?{pcm};
????????????????int?len?=?swr_convert(asctx,?data,?frame->nb_samples?//輸出
????????????????????????,?(const?uint8_t?**)?frame->data,?frame->nb_samples?//輸入
????????????????);
????????????????if?(len?>=?0)?{
????????????????????cout?<<?"swr_convert?success?return?len?=?"?<<?len?<<?endl;
????????????????}?else?{
????????????????????cout?<<?"swr_convert?failed?return?len?=?"?<<?len?<<?endl;
????????????????}
????????????}
????????}
??...
????}
????if?(asctx)swr_close(asctx);
????if?(asctx)swr_free(&asctx);????
??
轉(zhuǎn)換后的log:
?swr_convert success return len = 1024
?
seek 操作
我們?nèi)绻胍付硞€時間看某段畫面的話就需要對視頻做 seek 操作,F(xiàn)Fmpeg 提供了 「av_seek_frame」 函數(shù)來對視頻的跳轉(zhuǎn),它有 4 個輸入?yún)?shù),含義如下:
/**
?*?根據(jù)時間戳和音頻或視頻的索引?seek?到關(guān)鍵幀的操作
?*
?*?@param?s?媒體格式上下文
?*?@param?stream_index?流索引,傳入?-1?為默認(rèn)
?*?@param?timestamp?需要跳轉(zhuǎn)到時間戳的位置
?*?@param?flags?seek?的模式
?*?@return?>=?0?on?success
?*/
int?av_seek_frame(AVFormatContext?*s,?int?stream_index,?int64_t?timestamp,
??????????????????int?flags);
我們著重看下最后一個 「flags」 參數(shù)
?//AVSEEK_FLAG_BACKWARD
seek 到后面的關(guān)鍵幀
//AVSEEK_FLAG_BYTE
基于以字節(jié)為單位的位置查找
//AVSEEK_FLAG_ANY
Seek 到任意一幀,注意不是關(guān)鍵幀,那么會有花屏的可能。
//AVSEEK_FLAG_FRAME
seek 到關(guān)鍵幀的位置
?
我們一般以這樣的形式來進(jìn)行 seek 操作:
int?ms?=?3000;?//三秒位置?根據(jù)時間基數(shù)(分?jǐn)?shù))轉(zhuǎn)換
long?long?pos?=?(double)?ms?/?(double)?1000?*?r2d(ic->streams[pkt->stream_index]->time_base);
av_seek_frame(ic,?videoStream,?pos,?AVSEEK_FLAG_BACKWARD?|?AVSEEK_FLAG_FRAME);
上面的含義就是定位到 3000 ms 位置后面的關(guān)鍵幀處開始播放。后面播放器 seek 功能我們會介紹如何精準(zhǔn) seek 操作
該篇文章對于 FFmpeg 的知識我們就介紹到這里,后面在開發(fā)中如果有新遇見的我會再詳細(xì)介紹一下。
總結(jié)
播放器要用到的 FFmpeg 知識 大概就這么多,可以發(fā)現(xiàn)這些 API 其實都比較簡單。此刻我相信你已經(jīng)對這些 API 有一定的印象和了解了吧。
下一篇文章將帶來 QT 如何渲染 PCM 和 YUV 的數(shù)據(jù)。
「以上代碼可以通過該地址訪問」:https://github.com/yangkun19921001/YKAVStudyPlatform
