iOS利用FFmpeg解析音視頻數(shù)據(jù)流

需求

利用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ì)步驟,需要的可以提前閱讀.

iOS編譯FFmpeg

導(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
            ...
        }
    }];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挣郭,一起剝皮案震驚了整個(gè)濱河市迄埃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兑障,老刑警劉巖侄非,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異流译,居然都是意外死亡逞怨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門福澡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叠赦,“玉大人,你說我怎么就攤上這事革砸〕悖” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵算利,是天一觀的道長册踩。 經(jīng)常有香客問我,道長效拭,這世上最難降的妖魔是什么暂吉? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮缎患,結(jié)果婚禮上借笙,老公的妹妹穿的比我還像新娘。我一直安慰自己较锡,他們只是感情好业稼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚂蕴,像睡著了一般低散。 火紅的嫁衣襯著肌膚如雪俯邓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天熔号,我揣著相機(jī)與錄音稽鞭,去河邊找鬼。 笑死引镊,一個(gè)胖子當(dāng)著我的面吹牛朦蕴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弟头,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吩抓,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了赴恨?” 一聲冷哼從身側(cè)響起疹娶,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伦连,沒想到半個(gè)月后雨饺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惑淳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年额港,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歧焦。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡移斩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出倚舀,到底是詐尸還是另有隱情蜻牢,我是刑警寧澤卡辰,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響啦租,放射性物質(zhì)發(fā)生泄漏伶选。R本人自食惡果不足惜深员,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一嗓违、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哺徊,春花似錦室琢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春巢钓,著一層夾襖步出監(jiān)牢的瞬間病苗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工症汹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硫朦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓背镇,卻偏偏與公主長得像咬展,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞒斩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354