iOS ijkplayer 音视频同步
2017-05-31 15:41
495 查看
http://www.jianshu.com/p/daf0a61cc1e0
3.3.1 音频输出
ijkplayer中Android平台使用OpenSL ES或AudioTrack输出音频,iOS平台使用AudioQueue输出音频。
audio output节点,在
回到ffplay.c中,如果发现待播放的文件中含有音频,那么在调用
output设备。
在
设置给了Audio Output, iOS平台上即为AudioQueue。
AudioQueue模块在工作过程中,通过不断的callback来获取pcm数据进行播放。
有关AudioQueue的具体内容此处不再介绍。
3.3.2 视频渲染
iOS平台上采用OpenGL渲染解码后的YUV图像,渲染线程为
从代码实现上可以看出,该线程的主要工作为:
调用
调用
display_overlay函数指针在前面初始化流程有介绍过,它在
方法中被赋值为
3.4.3 音视频同步
对于播放器来说,音视频同步是一个关键点,同时也是一个难点,同步效果的好坏,直接决定着播放器的质量。通常音视频同步的解决方案就是选择一个参考时钟,播放时读取音视频帧上的时间戳,同时参考当前时钟参考时钟上的时间来安排播放。如下图所示:
音视频同步示意图.png
如果音视频帧的播放时间大于当前参考时钟上的时间,则不急于播放该帧,直到参考时钟达到该帧的时间戳;如果音视频帧的时间戳小于当前参考时钟上的时间,则需要“尽快”播放该帧或丢弃,以便播放进度追上参考时钟。
参考时钟的选择也有多种方式:
选取视频时间戳作为参考时钟源
选取音频时间戳作为参考时钟源
选取外部时间作为参考时钟源
考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。
ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染
从上述实现可以看出,该方法中主要循环做两件事情:
休眠等待,
调用
可见同步的重点是在
lastvp是上一帧,vp是当前帧,last_duration则是根据当前帧和上一帧的pts,计算出来上一帧的显示时间,经过
在
如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间
如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。
回到
如果当前这一帧的播放时间已经过了,并且其和当前系统时间的差值超过了
否则进入正常显示当前帧的流程,调用
3.3 音视频渲染及同步
3.3.1 音频输出
ijkplayer中Android平台使用OpenSL ES或AudioTrack输出音频,iOS平台使用AudioQueue输出音频。
audio output节点,在
ffp_prepare_async_l方法中被创建:
ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
ffpipeline_open_audio_output方法实际上调用的是IJKFF_Pipeline对象的函数指针
func_open_audio_utput,该函数指针在初始化中的
ijkmp_ios_create方法中被赋值,最后指向的是
func_open_audio_output
static SDL_Aout *func_open_audio_output(IJKFF_Pipeline *pipeline, FFPlayer *ffp) { return SDL_AoutIos_CreateForAudioUnit(); }
SDL_AoutIos_CreateForAudioUnit定义如下,主要完成的是创建SDL_Aout对象
SDL_Aout *SDL_AoutIos_CreateForAudioUnit() { SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque)); if (!aout) return NULL; // SDL_Aout_Opaque *opaque = aout->opaque; aout->free_l = aout_free_l; aout->open_audio = aout_open_audio; aout->pause_audio = aout_pause_audio; aout->flush_audio = aout_flush_audio; aout->close_audio = aout_close_audio; aout->func_set_playback_rate = aout_set_playback_rate; aout->func_set_playback_volume = aout_set_playback_volume; aout->func_get_latency_seconds = auout_get_latency_seconds; aout->func_get_audio_persecond_callbacks = aout_get_persecond_callbacks; return aout; }
回到ffplay.c中,如果发现待播放的文件中含有音频,那么在调用
stream_component_open打开解码器时,该方法里面也调用
audio_open打开了audio
output设备。
static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params) { FFPlayer *ffp = opaque; VideoState *is = ffp->is; SDL_AudioSpec wanted_spec, spec; ...... wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout); wanted_spec.channels = wanted_nb_channels; wanted_spec.freq = wanted_sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.silence = 0; wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AoutGetAudioPerSecondCallBacks(ffp->aout))); wanted_spec.callback = sdl_audio_callback; wanted_spec.userdata = opaque; while (SDL_AoutOpenAudio(ffp->aout, &wanted_spec, &spec) < 0) { ..... } ...... return spec.size; }
在
audio_open中配置了音频输出的相关参数
SDL_AudioSpec,并通过
int SDL_AoutOpenAudio(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained) { if (aout && desired && aout->open_audio) return aout->open_audio(aout, desired, obtained); return -1; }
设置给了Audio Output, iOS平台上即为AudioQueue。
AudioQueue模块在工作过程中,通过不断的callback来获取pcm数据进行播放。
有关AudioQueue的具体内容此处不再介绍。
3.3.2 视频渲染
iOS平台上采用OpenGL渲染解码后的YUV图像,渲染线程为
video_refresh_thread,最后渲染图像的方法为
video_image_display2,定义如下:
static void video_image_display2(FFPlayer *ffp) { VideoState *is = ffp->is; Frame *vp; Frame *sp = NULL; vp = frame_queue_peek_last(&is->pictq); ...... SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp); ...... }
从代码实现上可以看出,该线程的主要工作为:
调用
frame_queue_peek_last从pictq中读取当前需要显示视频帧
调用
SDL_VoutDisplayYUVOverlay进行绘制
int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay) { if (vout && overlay && vout->display_overlay) return vout->display_overlay(vout, overlay); return -1; }
display_overlay函数指针在前面初始化流程有介绍过,它在
SDL_Vout *SDL_VoutIos_CreateForGLES2()
方法中被赋值为
vout_display_overlay,该方法就是调用OpengGL绘制图像。
3.4.3 音视频同步
对于播放器来说,音视频同步是一个关键点,同时也是一个难点,同步效果的好坏,直接决定着播放器的质量。通常音视频同步的解决方案就是选择一个参考时钟,播放时读取音视频帧上的时间戳,同时参考当前时钟参考时钟上的时间来安排播放。如下图所示:
音视频同步示意图.png
如果音视频帧的播放时间大于当前参考时钟上的时间,则不急于播放该帧,直到参考时钟达到该帧的时间戳;如果音视频帧的时间戳小于当前参考时钟上的时间,则需要“尽快”播放该帧或丢弃,以便播放进度追上参考时钟。
参考时钟的选择也有多种方式:
选取视频时间戳作为参考时钟源
选取音频时间戳作为参考时钟源
选取外部时间作为参考时钟源
考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。
ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染
video_refresh_thread的线程中:
static int video_refresh_thread(void *arg) { FFPlayer *ffp = arg; VideoState *is = ffp->is; double remaining_time = 0.0; while (!is->abort_request) { if (remaining_time > 0.0) av_usleep((int)(int64_t)(remaining_time * 1000000.0)); remaining_time = REFRESH_RATE; if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) video_refresh(ffp, &remaining_time); } return 0; }
从上述实现可以看出,该方法中主要循环做两件事情:
休眠等待,
remaining_time的计算在
video_refresh中
调用
video_refresh方法,刷新视频帧
可见同步的重点是在
video_refresh中,下面着重分析该方法:
lastvp = frame_queue_peek_last(&is->pictq); vp = frame_queue_peek(&is->pictq); ...... /* compute nominal last_duration */ last_duration = vp_duration(is, lastvp, vp); delay = compute_target_delay(ffp, last_duration, is);
lastvp是上一帧,vp是当前帧,last_duration则是根据当前帧和上一帧的pts,计算出来上一帧的显示时间,经过
compute_target_delay方法,计算出显示当前帧需要等待的时间。
static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is) { double sync_threshold, diff = 0; /* update delay to follow master synchronisation source */ if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) { /* if video is slave, we try to correct big delays by duplicating or deleting a frame */ diff = get_clock(&is->vidclk) - get_master_clock(is); /* skip or repeat frame. We take into account the delay to compute the threshold. I still don't know if it is the best guess */ sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)); /* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */ if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) { if (diff <= -sync_threshold) delay = FFMAX(0, delay + diff); else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) delay = delay + diff; else if (diff >= sync_threshold) delay = 2 * delay; } } ..... return delay; }
在
compute_target_delay方法中,如果发现当前主时钟源不是video,则计算当前视频时钟与主时钟的差值:
如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间
如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。
回到
video_refresh中
time= av_gettime_relative()/1000000.0; if (isnan(is->frame_timer) || time < is->frame_timer) is->frame_timer = time; if (time < is->frame_timer + delay) { *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); goto display; }
frame_timer实际上就是上一帧的播放时间,而
frame_timer + delay实际上就是当前这一帧的播放时间,如果系统时间还没有到当前这一帧的播放时间,直接跳转至display,而此时
is->force_refresh变量为0,不显示当前帧,进入
video_refresh_thread中下一次循环,并睡眠等待。
is->frame_timer += delay; if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) is->frame_timer = time; SDL_LockMutex(is->pictq.mutex); if (!isnan(vp->pts)) update_video_pts(is, vp->pts, vp->pos, vp->serial); SDL_UnlockMutex(is->pictq.mutex); if (frame_queue_nb_remaining(&is->pictq) > 1) { Frame *nextvp = frame_queue_peek_next(&is->pictq); duration = vp_duration(is, vp, nextvp); if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) { frame_queue_next(&is->pictq); goto retry; } }
如果当前这一帧的播放时间已经过了,并且其和当前系统时间的差值超过了
AV_SYNC_THRESHOLD_MAX,则将当前这一帧的播放时间改为系统时间,并在后续判断是否需要丢帧,其目的是为后面帧的播放时间重新调整frame_timer,如果缓冲区中有更多的数据,并且当前的时间已经大于当前帧的持续显示时间,则丢弃当前帧,尝试显示下一帧。
{ frame_queue_next(&is->pictq); is->force_refresh = 1; SDL_LockMutex(ffp->is->play_mutex); ...... display: /* display picture */ if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown) video_display2(ffp);
否则进入正常显示当前帧的流程,调用
video_display2开始渲染。
相关文章推荐
- iOS开发之集成ijkplayer视频直播
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- 详解iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- iOS开发之集成ijkplayer视频直播
- iOS:集成ijkplayer视频直播
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- iOS开发之ijkplayer视频直播
- iOS中集成ijkplayer视频直播框架
- iOS开发之集成ijkplayer视频直播
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- iOS中集成ijkplayer视频直播框架
- 一步步搭建视频直播系统,基于LFLiveKit+ijkplayer+rtmp(iOS端)
- iOS中集成ijkplayer视频直播框架