需求
利用FFmpeg解析音視頻流,音視頻流可以來自一個(gè)標(biāo)準(zhǔn)的RTMP的URL或者是一個(gè)文件. 通過解析得到音視頻流,進(jìn)一步就可以解碼, 然后視頻渲染在屏幕上,音頻通過揚(yáng)聲器輸出.
實(shí)現(xiàn)原理
利用FFmpeg框架中l(wèi)ibavformat模塊可以通過函數(shù)av_read_frame
解析出音視頻流的音視頻數(shù)據(jù),如果直接使用FFmpeg硬解,僅需要解析到AVPacket即可傳給解碼模塊使用,如果使用VideoToolbox中的硬解, 對(duì)于視頻數(shù)據(jù),還需要獲取其NALU Header中的(vps)sps, pps以便后續(xù)使用.
閱讀前提:
- iOS中FFmpeg環(huán)境搭建
- FFmpeg基本知識(shí)
- 音視頻基礎(chǔ)
GitHub地址(附代碼) : iOS Parse
掘金地址 : iOS Parse
簡書地址 : iOS Parse
博客地址 : iOS Parse
簡易流程
使用流程
- 初始化解析類:
- (instancetype)initWithPath:(NSString *)path;
- 開始解析:
startParseWithCompletionHandler
- 獲取解析后的數(shù)據(jù): 從上一步中
startParseWithCompletionHandler
方法中的Block獲取解析后的音視頻數(shù)據(jù).
FFmpeg parse流程
- 創(chuàng)建format context:
avformat_alloc_context
- 打開文件流:
avformat_open_input
- 尋找流信息:
avformat_find_stream_info
- 獲取音視頻流的索引值:
formatContext->streams[i]->codecpar->codec_type == (isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)
- 獲取音視頻流:
m_formatContext->streams[m_audioStreamIndex]
- 解析音視頻數(shù)據(jù)幀:
av_read_frame
- 獲取extra data:
av_bitstream_filter_filter
具體步驟
1. 將FFmpeg框架導(dǎo)入項(xiàng)目中
下面的鏈接中包含搭建iOS需要的FFmpeg環(huán)境的詳細(xì)步驟,需要的可以提前閱讀.
導(dǎo)入FFmpeg框架后,首先需要將用到FFmpeg的文件改名為.mm
, 因?yàn)樯婕癈,C++混編,所以需要更改文件名
然后在頭文件中導(dǎo)入FFmpeg頭文件.
// FFmpeg Header File
#ifdef __cplusplus
extern "C" {
#endif
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/opt.h"
#ifdef __cplusplus
};
#endif
注意: FFmpeg是一個(gè)廣為流傳的框架,其結(jié)構(gòu)復(fù)雜,一般導(dǎo)入都按照如上格式,以文件夾名為根目錄進(jìn)行導(dǎo)入,具體設(shè)置,請(qǐng)參考上文鏈接.
2. 初始化
2.1. 注冊(cè)FFmpeg
-
void av_register_all(void);
初始化libavformat并注冊(cè)所有muxers,demuxers與協(xié)議辟灰。如果不調(diào)用此功能,則可以選擇一個(gè)特定想要支持的格式。
一般在程序中的main函數(shù)或是主程序啟動(dòng)的代理方法- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
中初始化FFmpeg,執(zhí)行一次即可.
av_register_all();
2.2. 利用視頻文件生成格式上下文對(duì)象
-
avformat_alloc_context()
: 初始化avformat上下文對(duì)象. -
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
函數(shù)-
fmt
: 如果非空表示強(qiáng)制指定一個(gè)輸入流的格式, 設(shè)置為空會(huì)自動(dòng)選擇.
-
-
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
:讀取媒體文件的數(shù)據(jù)包以獲取流信息
- (AVFormatContext *)createFormatContextbyFilePath:(NSString *)filePath {
if (filePath == nil) {
log4cplus_error(kModuleName, "%s: file path is NULL",__func__);
return NULL;
}
AVFormatContext *formatContext = NULL;
AVDictionary *opts = NULL;
av_dict_set(&opts, "timeout", "1000000", 0);//設(shè)置超時(shí)1秒
formatContext = avformat_alloc_context();
BOOL isSuccess = avformat_open_input(&formatContext, [filePath cStringUsingEncoding:NSUTF8StringEncoding], NULL, &opts) < 0 ? NO : YES;
av_dict_free(&opts);
if (!isSuccess) {
if (formatContext) {
avformat_free_context(formatContext);
}
return NULL;
}
if (avformat_find_stream_info(formatContext, NULL) < 0) {
avformat_close_input(&formatContext);
return NULL;
}
return formatContext;
}
2.3. 獲取Audio / Video流的索引值.
通過遍歷format context對(duì)象可以從nb_streams
數(shù)組中找到音頻或視頻流索引,以便后續(xù)使用.
注意: 后面代碼中僅需要知道音頻,視頻的索引就可以快速讀取到format context對(duì)象中對(duì)應(yīng)流的信息.
- (int)getAVStreamIndexWithFormatContext:(AVFormatContext *)formatContext isVideoStream:(BOOL)isVideoStream {
int avStreamIndex = -1;
for (int i = 0; i < formatContext->nb_streams; i++) {
if ((isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO) == formatContext->streams[i]->codecpar->codec_type) {
avStreamIndex = i;
}
}
if (avStreamIndex == -1) {
log4cplus_error(kModuleName, "%s: Not find video stream",__func__);
return NULL;
}else {
return avStreamIndex;
}
}
2.4. 是否支持音視頻流
目前視頻僅支持H264, H265編碼的格式.實(shí)際過程中,解碼得到視頻的旋轉(zhuǎn)角度可能是不同的,以及不同機(jī)型可以支持的解碼文件格式也是不同的,所以可以用這個(gè)方法手動(dòng)過濾一些不支持的情況.具體請(qǐng)下載代碼觀看,這里僅列出實(shí)戰(zhàn)中測試出支持的列表.
/*
各機(jī)型支持的最高分辨率和FPS組合:
iPhone 6S: 60fps -> 720P
30fps -> 4K
iPhone 7P: 60fps -> 1080p
30fps -> 4K
iPhone 8: 60fps -> 1080p
30fps -> 4K
iPhone 8P: 60fps -> 1080p
30fps -> 4K
iPhone X: 60fps -> 1080p
30fps -> 4K
iPhone XS: 60fps -> 1080p
30fps -> 4K
*/
音頻本例中僅支持AAC格式.其他格式可根據(jù)需求自行更改.
3. 開始解析
- 初始化AVPacket以存放解析后的數(shù)據(jù)
使用AVPacket這個(gè)結(jié)構(gòu)體來存儲(chǔ)壓縮數(shù)據(jù).對(duì)于視頻而言, 它通常包含一個(gè)壓縮幀,對(duì)音頻而言,可能包含多個(gè)壓縮幀,該結(jié)構(gòu)體類型通過av_malloc()
函數(shù)分配內(nèi)存,通過av_packet_ref()
函數(shù)拷貝,通過av_packet_unref().
函數(shù)釋放內(nèi)存.
AVPacket packet;
av_init_packet(&packet);
-
解析數(shù)據(jù)
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
: 此函數(shù)返回存儲(chǔ)在文件中的內(nèi)容,并且不驗(yàn)證解碼器的有效幀是什么丧失。它會(huì)將存儲(chǔ)在文件中的內(nèi)容分成幀姆泻,并為每次調(diào)用返回一個(gè)咒唆。它不會(huì)在有效幀之間省略無效數(shù)據(jù)甚垦,以便為解碼器提供解碼時(shí)可能的最大信息茶鹃。
int size = av_read_frame(formatContext, &packet);
if (size < 0 || packet.size < 0) {
handler(YES, YES, NULL, NULL);
log4cplus_error(kModuleName, "%s: Parse finish",__func__);
break;
}
-
獲取sps, pps等NALU Header信息
通過調(diào)用
av_bitstream_filter_filter
可以從碼流中過濾得到sps, pps等NALU Header信息.av_bitstream_filter_init: 通過給定的比特流過濾器名詞創(chuàng)建并初始化一個(gè)比特流過濾器上下文.
av_bitstream_filter_filter: 此函數(shù)通過過濾
buf
參數(shù)中的數(shù)據(jù),將過濾后的數(shù)據(jù)放在poutbuf
參數(shù)中.輸出的buffer必須被調(diào)用者釋放.此函數(shù)使用buf_size大小過濾緩沖區(qū)buf涣雕,并將過濾后的緩沖區(qū)放在poutbuf指向的緩沖區(qū)中艰亮。
attribute_deprecated int av_bitstream_filter_filter ( AVBitStreamFilterContext * bsfc,
AVCodecContext * avctx,
const char * args, // filter 配置參數(shù)
uint8_t ** poutbuf, // 過濾后的數(shù)據(jù)
int * poutbuf_size, // 過濾后的數(shù)據(jù)大小
const uint8_t * buf,// 提供給過濾器的原始數(shù)據(jù)
int buf_size, // 提供給過濾器的原始數(shù)據(jù)大小
int keyframe // 如果要過濾的buffer對(duì)應(yīng)于關(guān)鍵幀分組數(shù)據(jù),則設(shè)置為非零
)
注意: 下面使用
new_packet
是為了解決av_bitstream_filter_filter
會(huì)產(chǎn)生內(nèi)存泄漏的問題.每次使用完后將用new_packet
釋放即可.
if (packet.stream_index == videoStreamIndex) {
static char filter_name[32];
if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_H264) {
strncpy(filter_name, "h264_mp4toannexb", 32);
videoInfo.videoFormat = XDXH264EncodeFormat;
} else if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_HEVC) {
strncpy(filter_name, "hevc_mp4toannexb", 32);
videoInfo.videoFormat = XDXH265EncodeFormat;
} else {
break;
}
AVPacket new_packet = packet;
if (self->m_bitFilterContext == NULL) {
self->m_bitFilterContext = av_bitstream_filter_init(filter_name);
}
av_bitstream_filter_filter(self->m_bitFilterContext, formatContext->streams[videoStreamIndex]->codec, NULL, &new_packet.data, &new_packet.size, packet.data, packet.size, 0);
}
- 根據(jù)特定規(guī)則生成時(shí)間戳
可以根據(jù)自己的需求自定義時(shí)間戳生成規(guī)則.這里使用當(dāng)前系統(tǒng)時(shí)間戳加上數(shù)據(jù)包中的自帶的pts/dts生成了時(shí)間戳.
CMSampleTimingInfo timingInfo;
CMTime presentationTimeStamp = kCMTimeInvalid;
presentationTimeStamp = CMTimeMakeWithSeconds(current_timestamp + packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base), fps);
timingInfo.presentationTimeStamp = presentationTimeStamp;
timingInfo.decodeTimeStamp = CMTimeMakeWithSeconds(current_timestamp + av_rescale_q(packet.dts, formatContext->streams[videoStreamIndex]->time_base, input_base), fps);
- 獲取parse到的數(shù)據(jù)
本例將獲取到的數(shù)據(jù)放在自定義的結(jié)構(gòu)體中,然后通過block回調(diào)傳給方法的調(diào)用者,調(diào)用者可以在回調(diào)函數(shù)中處理parse到的視頻數(shù)據(jù).
struct XDXParseVideoDataInfo {
uint8_t *data;
int dataSize;
uint8_t *extraData;
int extraDataSize;
Float64 pts;
Float64 time_base;
int videoRotate;
int fps;
CMSampleTimingInfo timingInfo;
XDXVideoEncodeFormat videoFormat;
};
...
videoInfo.data = video_data;
videoInfo.dataSize = video_size;
videoInfo.extraDataSize = formatContext->streams[videoStreamIndex]->codec->extradata_size;
videoInfo.extraData = (uint8_t *)malloc(videoInfo.extraDataSize);
videoInfo.timingInfo = timingInfo;
videoInfo.pts = packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base);
videoInfo.fps = fps;
memcpy(videoInfo.extraData, formatContext->streams[videoStreamIndex]->codec->extradata, videoInfo.extraDataSize);
av_free(new_packet.data);
// send videoInfo
if (handler) {
handler(YES, NO, &videoInfo, NULL);
}
free(videoInfo.extraData);
free(videoInfo.data);
- 獲取parse到的音頻數(shù)據(jù)
struct XDXParseAudioDataInfo {
uint8_t *data;
int dataSize;
int channel;
int sampleRate;
Float64 pts;
};
...
if (packet.stream_index == audioStreamIndex) {
XDXParseAudioDataInfo audioInfo = {0};
audioInfo.data = (uint8_t *)malloc(packet.size);
memcpy(audioInfo.data, packet.data, packet.size);
audioInfo.dataSize = packet.size;
audioInfo.channel = formatContext->streams[audioStreamIndex]->codecpar->channels;
audioInfo.sampleRate = formatContext->streams[audioStreamIndex]->codecpar->sample_rate;
audioInfo.pts = packet.pts * av_q2d(formatContext->streams[audioStreamIndex]->time_base);
// send audio info
if (handler) {
handler(NO, NO, NULL, &audioInfo);
}
free(audioInfo.data);
}
- 釋放packet
因?yàn)槲覀円呀?jīng)將packet中的關(guān)鍵數(shù)據(jù)拷貝到自定義的結(jié)構(gòu)體中,所以使用完后需要釋放packet.
av_packet_unref(&packet);
- parse完成后釋放相關(guān)資源
- (void)freeAllResources {
if (m_formatContext) {
avformat_close_input(&m_formatContext);
m_formatContext = NULL;
}
if (m_bitFilterContext) {
av_bitstream_filter_close(m_bitFilterContext);
m_bitFilterContext = NULL;
}
}
注意: 如果使用FFmpeg硬解,則僅僅需要獲取到AVPacket數(shù)據(jù)結(jié)構(gòu)即可.不需要再將數(shù)據(jù)封裝到自定義的結(jié)構(gòu)體中
4. 外部調(diào)用
上面操作執(zhí)行完后,即可通過如下block獲取解析后的數(shù)據(jù),一般需要繼續(xù)對(duì)音視頻進(jìn)行解碼操作.后面文章會(huì)講到,請(qǐng)持續(xù)關(guān)注.
XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
[parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
if (isFinish) {
// parse finish
...
return;
}
if (isVideoFrame) {
// decode video
...
}else {
// decode audio
...
}
}];