音视频同步介绍
因为音频和视频是各自线程独立播放的, 所以需要同步行为来保证声画的时间节点是一致的或者时间偏差值在一定的范围内. 一般来说是根据音频时间来做同步, 也就是将视频同步到音频. 从 ijkplayer 中的代码可以看出来, 默认是音频除非音频通道不存在才会是视频. 引起音视频不同步的原因主要有两种: 一种是音频和视频的数据量不一致而且编码算法不同所引起的解码时间差导致的不同步. 并且发送端没有统一的同步时钟; 另一种是网络传输延时, 网络传输是受到网络的实时传输带宽, 传输距离和网络节点的处理速度等因素的影响, 在网络阻塞时, 媒体信息不能保证以连续的 "流" 数据方式传输, 特别是不能保证数据量大的视频信息的连续传输, 从而引起媒体流内和流间的失步.
ijkplayer 中的结构体介绍
IjkMediaPlayer
ijkplayer 的结构体, 提供播放控制和播放的状态的一些处理, 结构体指针再初始化后会保存在 java 层, 提供复用. 基本每个 jni 的方法都会获取 java 层对应对象的一个 long 型变量, 然后强转成此结构体.
FFPlayer
主要与 java 层交互的结构体, 音视频的输出, 软硬解码器的设置.
VideoState
FFPlay 中的结构体. ijkplayer 直接拿过来包含在 FFPlayer 中.
Frame_Queue
保存解码后数据的环形数组, 不同通道的大小不一致.
Packet_Queue
保存从文件或者流读取出来的解码前数据的队列.
Clock
一个用于音视频同步的结构体
方法介绍
stream_open
对一些结构体进行初始化, 然后创建文件读取线程和视频渲染线程
ffplay_video_thread
视频解码线程执行的方法
audio_thread
音频解码线程执行的方法
video_refresh_thread
视频渲染, 音视频同步
调用流程
在 stream_open 方法中, 会对 Frame_Queue,Packet_Queue 和 Clock 进行初始化, 如下所示.
- if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) <0)
- goto fail;
- if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) <0)
- goto fail;
- if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) <0)
- goto fail;
- if (packet_queue_init(&is->videoq) <0 ||
- packet_queue_init(&is->audioq) <0 ||
- packet_queue_init(&is->subtitleq) <0)
- goto fail;
- init_clock(&is->vidclk, &is->videoq.serial);
- init_clock(&is->audclk, &is->audioq.serial);
- init_clock(&is->extclk, &is->extclk.serial);
同时, stream_open 方法中会启动一个 read_thread 线程. 在线程中会根据读取的文件或者流的信息去判断是否存在音频流和视频流, 然后通过 stream_component_open 方法找到对应的解码器, 启动解码线程.
- if (st_index[AVMEDIA_TYPE_AUDIO]>= 0) {
- stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
- } else {
- // 如果音频流不存在, 那就没办法通过音频去同步, 所以把同步方式改为视频
- ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
- is->av_sync_type = ffp->av_sync_type;
- }
- if (st_index[AVMEDIA_TYPE_VIDEO]>= 0) {
- ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
- }
- if (st_index[AVMEDIA_TYPE_SUBTITLE]>= 0) {
- stream_component_open(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
- }
在 read_thread 中读取文件, 将读出来的 AVPacket 根据不同的通道, 压入对应的 Packet_Queue 中
- AVPacket pkt1, *pkt = &pkt1;
- for (;;) {
- ...
- ret = av_read_frame(ic, pkt);
- if (ret <0) {
- // 主要是文件有误或者 EOF 的处理.
- ...
- continue;
- }
- if (pkt->stream_index == is->audio_stream
- && pkt_in_play_range) {
- packet_queue_put(&is->audioq, pkt);
- } else if (pkt->stream_index == is->video_stream
- && pkt_in_play_range
- && !(is->video_st
- && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))) {
- packet_queue_put(&is->videoq, pkt);
- } else if (pkt->stream_index == is->subtitle_stream
- && pkt_in_play_range) {
- packet_queue_put(&is->subtitleq, pkt);
- } else {
- av_packet_unref(pkt);
- }
- }
接下来就是视频的解码, 这个是通过 stream_component_open 方法启动的视频解码线程中执行的方法, get_video_frame 获取解码的数据后, 计算出 pts 也就是当前帧的播放时间 ,pts 的计算方式是 frame->pts * av_q2d(tb) 其中 tb 是 AVRational 结构体, 是一个时间基.
- static int ffplay_video_thread(void *arg){
- ...
- double pts;
- AVFrame *frame = av_frame_alloc();
- for (;;) {
- // 获取到解码出来的 AVFrame
- ret = get_video_frame(ffp, frame);
- if (ret <0)
- goto the_end;
- if (!ret)
- continue;
- ...
- // 计算出当前帧的播放时间
- pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
- // 在此方法中压入 Frame_Queue 这个环形队列中
- ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
- }
- }
is->pictq 也就是 对应的视频的 Frame_Queue , max_size 为 3, 音频的 max_size 是 9, 不清楚是什么原因用这个大小, 可能是出于内存的考虑. AVFrame 的每次写入都要从 Frame_Queue 中获取一个 Frame, 因为数量有限, 所以这里会有一个等待通知的过程. 这就是视频解码到压入数组的过程.
- static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial) {
- ...
- Frame *vp;
- // 从 Frame_Queue 中获取一个可写的 Frame, 如果没有则 wait 等待 signal
- if (!(vp = frame_queue_peek_writable(&is->pictq)))
- return -1;
- // 将 AVFrame 中的一些值赋给 Frame
- ...
- // 修改 Frame_Queue 中的 size
- frame_queue_push(&is->pictq);
- }
接下来是音频解码, 过程和视频解码的差不多, 同样是将解码出来的 AVFrame 赋值到 Frame 中, 然后修改对应的 Frame_Queue 的 size.
- static int audio_thread(void *arg) {
- ...
- AVFrame *frame = av_frame_alloc();
- Frame *af;
- do {
- if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) <0)
- goto the_end;
- if (got_frame) {
- ...
- if (!(af = frame_queue_peek_writable(&is->sampq)))
- goto the_end;
- ...
- av_frame_move_ref(af->frame, frame);
- frame_queue_push(&is->sampq);
- }
- } while (ret>= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
- }
下面是音视频同步的处理了, 在音频播放的方法里, 每播放一帧都会得到这一帧的播放时间, 将其保存在 Video_State 这个结构体的 audio_clock 中, 而音视频同步的计算是利用到此结构体, 具体执行在 audio_decode_frame 方法中.
- static int audio_decode_frame(FFPlayer *ffp) {
- ...
- if (!(af = frame_queue_peek_readable(&is->sampq)))
- return -1;
- ...
- /* update the audio clock with the pts */
- if (!isnan(af->pts))
- is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
- else
- is->audio_clock = NAN;
- }
然后在外部的方法将得到的 audio_clock 通过一系列处理, 保存到 Clock 结构体里面, 其中 set_clock_at 的第二个参数最后得到的结果是当前帧播放的秒数.
- static void sdl_audio_callback(void *opaque, Uint8 *stream, int len) {
- audio_size = audio_decode_frame(ffp);
- 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);
- }
- }
最后, 就到了视频的渲染了, 视频渲染的线程是 video_refresh_thread, remaining_time 是视频渲染线程需要 sleep 的时间也就是同步时间, 单位是 us. 通过 video_refresh 方法计算出来.
- 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));
- }
- //REFRESH_RATE = 0.01
- remaining_time = REFRESH_RATE;
- if (is->show_mode != SHOW_MODE_NONE
- && (!is->paused || is->force_refresh))
- video_refresh(ffp, &remaining_time);
- }
- return 0;
- }
- static void video_refresh(FFPlayer *opaque, double *remaining_time){
- FFPlayer *ffp = opaque;
- VideoState *is = ffp->is;
- double time;
- Frame *sp, *sp2;
- if (!is->paused
- && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK
- && is->realtime) {
- check_external_clock_speed(is);
- }
- if (!ffp->display_disable
- && is->show_mode != SHOW_MODE_VIDEO
- && is->audio_st) {
- time = av_gettime_relative() / 1000000.0;
- if (is->force_refresh
- || is->last_vis_time + ffp->rdftspeed <time) {
- video_display2(ffp);
- is->last_vis_time = time;
- }
- *remaining_time = FFMIN(*remaining_time, is->last_vis_time + ffp->rdftspeed - time);
- }
- if (is->video_st) {
- retry:
- if (frame_queue_nb_remaining(&is->pictq) == 0) {
- // nothing to do, no picture to display in the queue
- } else {
- double last_duration, duration, delay;
- Frame *vp, *lastvp;
- /* dequeue the picture */
- lastvp = frame_queue_peek_last(&is->pictq);
- vp = frame_queue_peek(&is->pictq);
- // 跳帧处理.
- if (vp->serial != is->videoq.serial) {
- frame_queue_next(&is->pictq);
- goto retry;
- }
- if (lastvp->serial != vp->serial) {
- is->frame_timer = av_gettime_relative() / 1000000.0;
- }
- if (is->paused)
- goto display;
- /* compute nominal last_duration */
- // 计算此帧的播放时长
- last_duration = vp_duration(is, lastvp, vp);
- // 计算当前需要 delay 的时间.
- delay = compute_target_delay(ffp, last_duration, is);
- time= av_gettime_relative()/1000000.0;
- av_gettime_relative(), is->frame_timer, delay);
- if (isnan(is->frame_timer) || time <is->frame_timer) {
- is->frame_timer = time;
- }
- if (time <is->frame_timer + delay) {
- // 计算出真正需要 sleep 的时间, 然后跳到 display 渲染此帧
- *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
- goto display;
- }
- 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)) {
- // 修改 Clock , 下次同步计算处理
- 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;
- }
- }
- // 字幕处理
- ...
- frame_queue_next(&is->pictq);
- is->force_refresh = 1;
- SDL_LockMutex(ffp->is->play_mutex);
- if (is->step) {
- is->step = 0;
- if (!is->paused)
- stream_update_pause_l(ffp);
- }
- SDL_UnlockMutex(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);
- }
- }
- ...
- }
- static void video_image_display2(FFPlayer *ffp)
- {
- VideoState *is = ffp->is;
- Frame *vp;
- Frame *sp = NULL;
- vp = frame_queue_peek_last(&is->pictq);
- if (vp->bmp) {
- // 渲染字幕
- ...
- // 渲染图像
- SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
- ...
- // 消息通知到 JAVA 层
- }
- }
总结
音视频同步, 是通过视频和音频的播放过程中, 将当前的播放帧的时间保存进 Clock 结构体中, 再在视频播放的时候, 也就是 video_refresh 方法, 首先通过 vp_duration 获取到此帧的播放时长, 然后 compute_target_delay 计算出需要同步的时间, 最后就渲染此帧, 然后 sleep 所达成的同步.
引用
https://www.cnblogs.com/x_wukong/p/5876645.html
来源: https://juejin.im/post/5b09409051882538b429859e