ijkplayer
源码解析系列文章
本章主要解析 音频解码
+ 音频播放
2个部分;
第一个部分是音频AVPacket
的解封装 -> 音频AVFrame
的过程;
第二部分介绍PCM
数据如何播放输出;
音频播放流程图可以参考如下
前情提要,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
是 自定义的一个接缝状相关的结构体,用于对解码器的逻辑进行一些封装方便调用;实际内部调用的是ffmpeg
的avcodec_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_pts
和next_pts_tb
,
根据当前帧的pts
和nb_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录制 播放原理
关于音频的播放流程,可以参考一下流程图;
packetQueue
是生产者,FrameQueue
是消费者,当FrameQueue
中的buffer
数据缓冲足够播放的时候交给AuidoQueue
来播放;audiao_thread()
的解析可以拆分为以下功能
- 2.1.播放速度变化
- 2.2.播放音量发生变化
- 2.3 重采样
- 2.4
AudioQueue
播放
并且IJKSDLAudioQueueController
提供一些播放音频相关的操作,
-
initWithAudioSpec
(初始化) -
play
(播放) -
pause
(暂停) -
flush
(清空播放队列) -
stop
(停止) -
setPlaybackRate
(设置播放速度) -
setPlaybackVolume
(设置播放音量) -
IJKSDLAudioQueueOuptutCallback
(回调写入音频数据)
2.1 ~2.7 没什么特别好讲的,都是AudioQueue
的 API
,知道在干嘛就行了,重要的是 回调函数 IJKSDLAudioQueueOuptutCallback(void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
; 一旦开始播放后AudioQueue
的回调函数就会一直工作,而播放器则需要往音频队列里塞入待播放的音频数据;
2.1 initWithAudioSpec
(初始化)
拿到可播放的配置streamDescription
,创建audioQueueRef
实例对象的创建没什么特别的,AudioQueueBuffer
的数量设置3个,是一个比较合理的设计方式;
可以参考苹果的 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);
}
}
}
至此,音频解码
+ 音频播放
的逻辑已经介绍完了,后续有时间还会补充 音频重采样
的逻辑,感谢各位看官姥爷的阅读,您辛苦了 🙏;