ijkplayer 音视频同步流程分析「建议收藏」

后端 (98) 2023-03-30 21:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说ijkplayer 音视频同步流程分析「建议收藏」,希望能够帮助你!!!。

音视频同步介绍

音频和视频是各自线程独立播放的,需要同步行为来保证声画的时间节点是一致的或者时间偏差值在一定的范围内。一般来说是根据音频时间来做同步,也就是将视频同步到音频。从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

视频渲染,音视频同步

调用流程

ijkplayer 音视频同步流程分析「建议收藏」_https://bianchenghao6.com/blog_后端_第1张ijkplayer 音视频同步流程分析「建议收藏」_https://bianchenghao6.com/blog_后端_第2张

stream_open 方法中,会对 Frame_QueuePacket_QueueClock 进行初始化,如下所示。

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 所达成的同步。

引用

www.cnblogs.com/x_wukong/p/…

发表回复