ijkplayer 源码解析3(音频解码+播放)

ijkplayer源码解析系列文章

本章主要解析 音频解码 + 音频播放2个部分;
第一个部分是音频AVPacket的解封装 -> 音频AVFrame 的过程;
第二部分介绍PCM 数据如何播放输出;

音频播放流程图可以参考如下


音频解码播放流程图.jpg

前情提要,ijkplayer的解码线程独立于数据读取线程,每个类型的流(AVStream)都有其对应的解码线程,如下表所示;

类型 PacketQueue FrameQueue clock Decoder 解码线程
音频 audioq sampq audclk auddec audio_thread
视频 videoq pictq vidclk viddec video_thread
字幕 subtitleq sampq ---- subdec subtitle_thread

1.音频解码线程

read_thread 后,获取音频流的stramIndex ,使用 stream_component_open 函数开启相应的流 和相应的解码线程;
(视频流/字幕流)也是同样的逻辑;
解码线程种,用到的结构体 包括 Decoder、

函数调用堆栈顺序如下:

stream_component_open
  audio_thread

audio_thread()函数主要实现了以下功能

  • 1.1 获取解码后的音频并送入音频FrameQueue
  • 1.2 精准seek操作逻辑
1.1 获取解码后的视频并送入 Audio FrameQueue
 /// 循环decoder 获取 音频AVFrame
/// 当
 do {
        ffp_audio_statistic_l(ffp);
        if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
            //decoder解码成功
            //省略代码...
        }
} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);

1.2 精准seek操作逻辑
此处代码 暂不介绍,默认配置不开启该功能;

解封器 Decoder

struct Decoder 是 自定义的一个接缝状相关的结构体,用于对解码器的逻辑进行一些封装方便调用;实际内部调用的是ffmpegavcodec_receive_frame() 获取解码后的 AVFrame媒体帧;

typedef struct Decoder {
    AVPacket pkt;
    AVPacket pkt_temp; 
    PacketQueue *queue;  //数据包队列
    AVCodecContext *avctx;  解码器上下文
    int pkt_serial;  //包序列
    int finished;  //是否解码工作,==0 表示工作中,!= 0 表示空闲
    int packet_pending;    
    int bfsc_ret;
    uint8_t *bfsc_data;

    SDL_cond *empty_queue_cond;
    int64_t start_pts;
    AVRational start_pts_tb;
    int64_t next_pts;
    AVRational next_pts_tb;
    SDL_Thread *decoder_tid;
    SDL_Thread _decoder_tid;

    SDL_Profiler decode_profiler;
    Uint64 first_frame_decoded_time;
    int    first_frame_decoded;
} Decoder;
解码器Decoder相关的函数
  • decoder_init (初始化解码器)
  • decoder_destroy (销毁解码器)
  • decoder_decode_frame (解码)
  • decoder_abort (中止解码器)

decoder_decode_frame () 解码函数

decoder_decode_frame()函数是一个可以解码 音频/视频/字幕 的通用函数;
这个章节只对解码音频Frame做解析;

如果frame->pts 正常则先将其从pkt_timebase {1,fram->sampel_rate}
pkt_timebase->就是stream time_base
如果pkt_timebase- 不正常,则使用上一帧的next_ptsnext_pts_tb,
根据当前帧的ptsnb_samples预估下一帧的pts

switch (d->avctx->codec_type) {
    case AVMEDIA_TYPE_VIDEO:
        ///代码省略...
        break;
    case AVMEDIA_TYPE_AUDIO:
        ret = avcodec_receive_frame(d->avctx, frame);
        if (ret >= 0) {
            /// 获取音频 timeBase
            AVRational tb = (AVRational){1, frame->sample_rate};
            /// 计算pts 的正确值
            if (frame->pts != AV_NOPTS_VALUE)
                frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb);
            else if (d->next_pts != AV_NOPTS_VALUE)
                frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
            if (frame->pts != AV_NOPTS_VALUE) {
                d->next_pts = frame->pts + frame->nb_samples;
                d->next_pts_tb = tb;
            }
        }
        break;
    default:
        break;
}

生产者产生的AVPacket数据是从哪里来的
在之前文章介绍中讲过,ijkplayer读取未解码的音频数据包是存放在PacketQueue 中,这里对AVPacket 也是从所对应的音频队列中取出,送入解码器,从而得到解码后的 AVFrame帧

PacketQueue 第一篇文章ijkplayer重要结构体中有介绍,不太理解的可以再去回顾一下;

do {
    if (d->queue->nb_packets == 0)
        /// 当decoder 的packet 数量为0 的时候发送信号,继续塞入AVPacket
        SDL_CondSignal(d->empty_queue_cond);
    if (d->packet_pending) {
        /// 当有pending的数据包是,使用
        av_packet_move_ref(&pkt, &d->pkt);
        d->packet_pending = 0;
    } else {
        /// 获取队列头部的AVPacket
        if (packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0)
            return -1;
    }
} while (d->queue->serial != d->pkt_serial);

当获取到的AVPacket空包的时候,则清空整个Packet队列,字幕队列额外的解码逻辑需要额外处理;因为它和音频/视频的逻辑不太一样

if (pkt.data == flush_pkt.data) {
    /// 当遇到‘空包’ 清空队列
    avcodec_flush_buffers(d->avctx);
    d->finished = 0;
    d->next_pts = d->start_pts;
    d->next_pts_tb = d->start_pts_tb;
} else {
    if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
        /// 解码器为字幕时的逻辑
        int got_frame = 0;
        ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
        if (ret < 0) {
            ret = AVERROR(EAGAIN);
        } else {
            if (got_frame && !pkt.data) {
                d->packet_pending = 1;
                av_packet_move_ref(&d->pkt, &pkt);
            }
            ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
        }
    } else {
        if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
            av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
            d->packet_pending = 1;
            av_packet_move_ref(&d->pkt, &pkt);
        }
    }
    av_packet_unref(&pkt);
}

2.音频渲染

iOS平台上音频的渲染 IJKSDLAudioQueueController实现的,其内部调用AudioQueue来完成音频的播放;
有关AudioQueue的基础原理可以参考文章 Audio Queue录制 播放原理

关于音频的播放流程,可以参考一下流程图;

未命名文件 (1).jpg

packetQueue是生产者,FrameQueue是消费者,当FrameQueue中的buffer数据缓冲足够播放的时候交给AuidoQueue来播放;audiao_thread()的解析可以拆分为以下功能

  • 2.1.播放速度变化
  • 2.2.播放音量发生变化
  • 2.3 重采样
  • 2.4AudioQueue播放

并且IJKSDLAudioQueueController 提供一些播放音频相关的操作,

  • initWithAudioSpec(初始化)
  • play (播放)
  • pause (暂停)
  • flush (清空播放队列)
  • stop (停止)
  • setPlaybackRate (设置播放速度)
  • setPlaybackVolume (设置播放音量)
  • IJKSDLAudioQueueOuptutCallback (回调写入音频数据)

2.1 ~2.7 没什么特别好讲的,都是AudioQueueAPI,知道在干嘛就行了,重要的是 回调函数 IJKSDLAudioQueueOuptutCallback(void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer); 一旦开始播放后AudioQueue的回调函数就会一直工作,而播放器则需要往音频队列里塞入待播放的音频数据;

2.1 initWithAudioSpec(初始化)

拿到可播放的配置streamDescription,创建audioQueueRef实例对象的创建没什么特别的,AudioQueueBuffer的数量设置3个,是一个比较合理的设计方式;

可以参考苹果的 AudioQueue 播放流程图


AudioQueue 播放流程图

以下是AudioQueue的创建函数及其代码注释

- (id)initWithAudioSpec:(const SDL_AudioSpec *)aSpec
{
        ///省略代码...
        /// ASBD 音频媒体格式配置
        AudioStreamBasicDescription streamDescription;
        IJKSDLGetAudioStreamBasicDescriptionFromSpec(&_spec, &streamDescription);

        SDL_CalculateAudioSpec(&_spec);

        if (_spec.size == 0) {
            NSLog(@"aout_open_audio: unexcepted audio spec size %u", _spec.size);
            return nil;
        }

        /* Set the desired format */
        AudioQueueRef audioQueueRef;
        /// 初始化AudioQueueRef 实例
        OSStatus status = AudioQueueNewOutput(&streamDescription,
                                              IJKSDLAudioQueueOuptutCallback,
                                              (__bridge void *) self,
                                              NULL,
                                              kCFRunLoopCommonModes,
                                              0,
                                              &audioQueueRef);       
       /// 省略代码....
       for (int i = 0;i < kIJKAudioQueueNumberBuffers; i++)
        {
            AudioQueueAllocateBuffer(audioQueueRef, _spec.size, &_audioQueueBufferRefArray[i]);
            _audioQueueBufferRefArray[i]->mAudioDataByteSize = _spec.size;
            memset(_audioQueueBufferRefArray[i]->mAudioData, 0, _spec.size);
            AudioQueueEnqueueBuffer(audioQueueRef, _audioQueueBufferRefArray[i], 0, NULL);
        }
}
2.2- play (播放)
OSStatus status = AudioQueueStart(_audioQueueRef, NULL);
2.3- pause (暂停)
/// 暂停AudioQueue
OSStatus status = AudioQueuePause(_audioQueueRef);
2.4- flush (清空播放队列)
if (_isPaused == YES) {
    /// 清空buffer 缓存
    for (int i = 0; i < kIJKAudioQueueNumberBuffers; i++)
    {
        if (_audioQueueBufferRefArray[i] && _audioQueueBufferRefArray[i]->mAudioData) {
            _audioQueueBufferRefArray[i]->mAudioDataByteSize = _spec.size;
            memset(_audioQueueBufferRefArray[i]->mAudioData, 0, _spec.size);
        }
    }
} else {
    /// 刷新_audioQueueRef 实例
    AudioQueueFlush(_audioQueueRef);
}
2.5- stop (停止)
///停止
AudioQueueStop(_audioQueueRef, true);
///销毁
AudioQueueDispose(_audioQueueRef, true);

2.6- setPlaybackRate (设置播放速度)

这段代码在初始化 audioQueueRef 中,设置后可以控制播放速速

UInt32 propValue = 1;
AudioQueueSetProperty(audioQueueRef, kAudioQueueProperty_EnableTimePitch, &propValue, sizeof(propValue));
propValue = 1;
AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchBypass, &propValue, sizeof(propValue));
propValue = kAudioQueueTimePitchAlgorithm_Spectral;
AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchAlgorithm, &propValue, sizeof(propValue));

外部接口,实际设置播放速率的地方,通过- (void)setPlaybackRate:(float)playbackRate { if (fabsf(playbackRate - 1.0f) <= 0.000001) { UInt32 propValue = 1; AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchBypass, &propValue, sizeof(propValue)); AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_PlayRate, 1.0f); } else { UInt32 propValue = 0; AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchBypass, &propValue, sizeof(propValue)); AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_PlayRate, playbackRate); } } 的函数指针来调用

- (void)setPlaybackRate:(float)playbackRate
{
    if (fabsf(playbackRate - 1.0f) <= 0.000001) {
        UInt32 propValue = 1;
        AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchBypass, &propValue, sizeof(propValue));
        AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_PlayRate, 1.0f);
    } else {
        UInt32 propValue = 0;
        AudioQueueSetProperty(_audioQueueRef, kAudioQueueProperty_TimePitchBypass, &propValue, sizeof(propValue));
        AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_PlayRate, playbackRate);
    }
}

2.7- setPlaybackVolume (设置播放音量)
float aq_volume = playbackVolume;
if (fabsf(aq_volume - 1.0f) <= 0.000001) {
    AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_Volume, 1.f);
} else {
    AudioQueueSetParameter(_audioQueueRef, kAudioQueueParam_Volume, aq_volume);
}
2.8 IJKSDLAudioQueueOuptutCallback (回调写入音频数据) 很重要
static void IJKSDLAudioQueueOuptutCallback(void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
    @autoreleasepool {
        IJKSDLAudioQueueController* aqController = (__bridge IJKSDLAudioQueueController *) inUserData;

        if (!aqController) {
            // do nothing;
        } else if (aqController->_isPaused || aqController->_isStopped) {
            /// 当暂停/停止 填充0 数据保持静音
            memset(inBuffer->mAudioData, aqController.spec.silence, inBuffer->mAudioDataByteSize);
        } else {
            /// 向inBuffer 填充解码后的PCM数据 (调用 sdl_audio_callback 方法)
            (*aqController.spec.callback)(aqController.spec.userdata, inBuffer->mAudioData, inBuffer->mAudioDataByteSize);
        }
        /// inBuffer 送入音频播放队列
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}
2.8.1 sdl_audio_callback()填充音频数据函数

is->audio_buf 表示音频数据的内存指针;
is->audio_buf_size 可以传输的音频数据有多大,多少个字节;
is->audio_buf_index 当前已经读区到第几个字节;
理解了这三个变量,就容易看懂代码中的逻辑,其实就是不断copy audio_buf 中的字节 填充到 stream 内存中;

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    FFPlayer *ffp = opaque;
    VideoState *is = ffp->is;
    int audio_size, len1;
    if (!ffp || !is) {
        memset(stream, 0, len);
        return;
    }

    ffp->audio_callback_time = av_gettime_relative();

    if (ffp->pf_playback_rate_changed) {
        /// 播放速度发生变化的回调设置
        ffp->pf_playback_rate_changed = 0;
#if defined(__ANDROID__)
        if (!ffp->soundtouch_enable) {
            SDL_AoutSetPlaybackRate(ffp->aout, ffp->pf_playback_rate);
        }
#else
        SDL_AoutSetPlaybackRate(ffp->aout, ffp->pf_playback_rate);
#endif
    }
    if (ffp->pf_playback_volume_changed) {
        /// 音量发生变化的回调设置
        ffp->pf_playback_volume_changed = 0;
        SDL_AoutSetPlaybackVolume(ffp->aout, ffp->pf_playback_volume);
    }

    while (len > 0) {
        if (is->audio_buf_index >= is->audio_buf_size) {
           /// audio_buf 缓存已读完的操作
           /// 解码新的音频数据 
           audio_size = audio_decode_frame(ffp);
           if (audio_size < 0) {
                /// 解码失败的情况
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
                /// 解码成功、更新size   
               is->audio_buf_size = audio_size;
           }
           /// 更新可读取音频字节索引
           is->audio_buf_index = 0;
        }

        if (is->auddec.pkt_serial != is->audioq.serial) {
            /// 当seek操作或切换播放源的情况 设置为0 表静音
            is->audio_buf_index = is->audio_buf_size;
            memset(stream, 0, len);
            // stream += len;
            // len = 0;
            SDL_AoutFlushAudio(ffp->aout);
            break;
        }
        /// len1 表示获取缓存的audio 数据还剩多少
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            /// 复制音频数据到stream
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
                /// 复制音频数据到stream
                SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume);
        }
        /// 更新读取位置
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
    if (!ffp->first_audio_frame_rendered) {
        ///音频首次播放 发送FFP_MSG_AUDIO_RENDERING_START消息通知
        ffp->first_audio_frame_rendered = 1;
        ffp_notify_msg1(ffp, FFP_MSG_AUDIO_RENDERING_START);
    }

    if (is->latest_audio_seek_load_serial == is->audio_clock_serial) {
        /// 序列号不一致发送消息通知
        int latest_audio_seek_load_serial = __atomic_exchange_n(&(is->latest_audio_seek_load_serial), -1, memory_order_seq_cst);
        if (latest_audio_seek_load_serial == is->audio_clock_serial) {
            if (ffp->av_sync_type == AV_SYNC_AUDIO_MASTER) {
                ffp_notify_msg2(ffp, FFP_MSG_AUDIO_SEEK_RENDERING_START, 1);
            } else {
                ffp_notify_msg2(ffp, FFP_MSG_AUDIO_SEEK_RENDERING_START, 0);
            }
        }
    }

    if (ffp->render_wait_start && !ffp->start_on_prepared && is->pause_req) {
        while (is->pause_req && !is->abort_request) {
            SDL_Delay(20);
        }
    }
}

至此,音频解码 + 音频播放的逻辑已经介绍完了,后续有时间还会补充 音频重采样的逻辑,感谢各位看官姥爷的阅读,您辛苦了 🙏;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容