iOS利用FFmpeg解碼音頻數據並播放

需求

利用FFmepg解析並解碼音頻流數據,而後將解碼後的音頻數據送給Audio Queue以實現播放.ios


實現原理

利用FFmpeg解析音頻數據流, 利用FFmpeg解碼音頻數據爲PCM格式. 利用Audio Queue Player實現音頻數據播放.git


閱讀前提


代碼地址 : Audio Decoder

掘金地址 : Audio Decoder

簡書地址 : Audio Decoder

博客地址 : Audio Decoder


整體架構

本例以一個蘋果原生相機錄製的.MOV文件爲例, 將該文件使用FFmpeg解析並解碼,將解碼後的數據放入傳輸隊列中,而後開啓audio queue player, 播放器在回調函數中輪循取出隊列中的數據實現播放.github

簡易流程

FFmpeg parse流程bash

  • 建立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]
  • 解析音視頻數據幀: av_read_frame

FFmpeg解碼流程架構

  • 從parse中的AVFormatContext獲取音頻流對象AVStream. m_formatContext->streams[m_audioStreamIndex];
  • 獲取解碼器上下文: formatContext->streams[audioStreamIndex]->codec
  • 獲取解碼器實例: avcodec_find_decoder(codecContext->codec_id)
  • 打開解碼器: avcodec_open2
  • 初始化音頻幀: AVFrame *av_frame_alloc(void);
  • 將數據發給解碼器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt)
  • 獲取解碼後的數據: int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
  • 建立轉碼器: struct SwrContext *swr_alloc(void);
  • 設置轉碼器參數: swr_alloc_set_opts
  • 初始化轉碼器上下文: int swr_init(struct SwrContext *s)
  • 開始轉碼: int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,const uint8_t **in , int in_count);
  • 獲取轉碼後的data與size.

文件結構

file

快速使用

  • 設置音頻格式ASBD
AudioStreamBasicDescription audioFormat = {
        .mSampleRate         = 48000,
        .mFormatID           = kAudioFormatLinearPCM,
        .mChannelsPerFrame   = 2,
        .mFormatFlags        = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
        .mBitsPerChannel     = 16,
        .mBytesPerPacket     = 4,
        .mBytesPerFrame      = 4,
        .mFramesPerPacket    = 1,
    };
複製代碼
  • 配置播放器
[[XDXAudioQueuePlayer getInstance] configureAudioPlayerWithAudioFormat:&audioFormat bufferSize:kXDXBufferSize];
複製代碼
  • Parse並解碼音頻文件數據
- (void)startDecode {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"MOV"];
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    XDXFFmpegAudioDecoder *decoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]];
    decoder.delegate = self;
    [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
        if (isFinish) {
            [decoder stopDecoder];
            return;
        }

        if (!isVideoFrame) {
            [decoder startDecodeAudioDataWithAVPacket:packet];
        }
    }];
}
複製代碼
  • 獲取解碼後數據並播放

爲了每次可以從新播放,這裏須要標記當前是否爲解碼的第一幀數據,以從新啓動播放器. 另外一點是使用NSTimer等待音頻數據放入隊列再開始播放,由於audio queue是驅動播放模式,因此必須等音頻數據放入傳輸隊列再開始播放.框架

#pragma mark - Decode Callback
- (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size isFirstFrame:(BOOL)isFirstFrame {
    if (isFirstFrame) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // First put 3 frame audio data to work queue then start audio queue to read it to play.
            [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) {
                
                XDXCustomQueueProcess *audioBufferQueue = [XDXAudioQueuePlayer getInstance]->_audioBufferQueue;
                int size = audioBufferQueue->GetQueueSize(audioBufferQueue->m_work_queue);
                if (size > 3) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [[XDXAudioQueuePlayer getInstance] startAudioPlayer];
                    });
                    [timer invalidate];
                }
            }];
        });
    }
    
    // Put audio data from audio file into audio data queue
    [self addBufferToWorkQueueWithAudioData:data size:size];
    
    // control rate
    usleep(16*1000);
}

複製代碼

具體實現

1. 初始化解碼器

從Parse模塊中能夠獲取當前文件對應FFmepg的上下文對象AVFormatContext.所以音頻流解碼器信息能夠直接獲取.async

  • 獲取音頻流對象
AVStream *audioStream = m_formatContext->streams[m_audioStreamIndex];
複製代碼
  • 獲取解碼器上下文對象
- (AVCodecContext *)createAudioEncderWithFormatContext:(AVFormatContext *)formatContext stream:(AVStream *)stream audioStreamIndex:(int)audioStreamIndex {
    AVCodecContext *codecContext = formatContext->streams[audioStreamIndex]->codec;
    AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
    if (!codec) {
        log4cplus_error(kModuleName, "%s: Not find audio codec",__func__);
        return NULL;
    }

    if (avcodec_open2(codecContext, codec, NULL) < 0) {
        log4cplus_error(kModuleName, "%s: Can't open audio codec",__func__);
        return NULL;
    }

    return codecContext;
}
複製代碼
  • 初始化音頻幀

AVFrame 做爲解碼後原始的音視頻數據的容器.AVFrame一般被分配一次而後屢次重複(例如,單個AVFrame以保持從解碼器接收的幀)。在這種狀況下,av_frame_unref()將釋放框架所持有的任何引用,並在再次重用以前將其重置爲其原始的清理狀態。ide

// Get audio frame
    m_audioFrame = av_frame_alloc();
    if (!m_audioFrame) {
        log4cplus_error(kModuleName, "%s: alloc audio frame failed",__func__);
        avcodec_close(m_audioCodecContext);
    }
複製代碼

2. 將原始數據發給解碼器

調用avcodec_send_packet將壓縮數據發送給解碼器.最後利用循環接收avcodec_receive_frame解碼後的音視頻數據.函數

int result = avcodec_send_packet(audioCodecContext, &packet);
    if (result < 0) {
        log4cplus_error(kModuleName, "%s: Send audio data to decoder failed.",__func__);
    }
複製代碼

3. 接收解碼後的數據.

result = avcodec_receive_frame(audioCodecContext, audioFrame);
複製代碼

4. 將解碼後的數據轉碼爲iOS設備可播放的類型

result = avcodec_receive_frame(audioCodecContext, audioFrame);
        while (0 == result) {
            struct SwrContext *au_convert_ctx = swr_alloc();
            au_convert_ctx = swr_alloc_set_opts(au_convert_ctx,
                                                AV_CH_LAYOUT_STEREO,
                                                AV_SAMPLE_FMT_S16,
                                                48000,
                                                audioCodecContext->channel_layout,
                                                audioCodecContext->sample_fmt,
                                                audioCodecContext->sample_rate,
                                                0,
                                                NULL);
            swr_init(au_convert_ctx);
            int out_linesize;
            int out_buffer_size = av_samples_get_buffer_size(&out_linesize,
                                                             audioCodecContext->channels,
                                                             audioCodecContext->frame_size,
                                                             audioCodecContext->sample_fmt,
                                                             1);
            
            uint8_t *out_buffer = (uint8_t *)av_malloc(out_buffer_size);
            // 解碼
            swr_convert(au_convert_ctx, &out_buffer, out_linesize, (const uint8_t **)audioFrame->data , audioFrame->nb_samples);
            swr_free(&au_convert_ctx);
            au_convert_ctx = NULL;
            if ([self.delegate respondsToSelector:@selector(getDecodeAudioDataByFFmpeg:size:isFirstFrame:)]) {
                [self.delegate getDecodeAudioDataByFFmpeg:out_buffer size:out_linesize isFirstFrame:m_isFirstFrame];
                m_isFirstFrame=NO;
            }
            
            av_free(out_buffer);
        }
        
        if (result != 0) {
            log4cplus_error(kModuleName, "%s: Decode finish.",__func__);
        }
複製代碼
相關文章
相關標籤/搜索