ijkplayer 音視頻同步流程分析

音視頻同步介紹

音頻和視頻是各自線程獨立播放的,須要同步行爲來保證聲畫的時間節點是一致的或者時間誤差值在必定的範圍內。通常來講是根據音頻時間來作同步,也就是將視頻同步到音頻。從ijkplayer中的代碼能夠看出來,默認是音頻除非音頻通道不存在纔會是視頻。引發音視頻不一樣步的緣由主要有兩種:一種是音頻和視頻的數據量不一致並且編碼算法不一樣所引發的解碼時間差致使的不一樣步。而且發送端沒有統一的同步時鐘;另外一種是網絡傳輸延時,網絡傳輸是受到網絡的實時傳輸帶寬、傳輸距離和網絡節點的處理速度等因素的影響,在網絡阻塞時,媒體信息不能保證以連續的「流」數據方式傳輸,特別是不能保證數據量大的視頻信息的連續傳輸,從而引發媒體流內和流間的失步。html

ijkplayer中的結構體介紹

IjkMediaPlayerjava

ijkplayer 的結構體,提供播放控制和播放的狀態的一些處理,結構體指針再初始化後會保存在java層,提供複用。基本每一個jni的方法都會獲取java 層對應對象的一個long 型變量,而後強轉成此結構體。算法

FFPlayer數組

主要與java層交互的結構體,音視頻的輸出,軟硬解碼器的設置。bash

VideoState網絡

FFPlay中的結構體。ijkplayer 直接拿過來包含在FFPlayer中。ide

Frame_Queueui

保存解碼後數據的環形數組,不一樣通道的大小不一致。編碼

Packet_Queuespa

保存從文件或者流讀取出來的解碼前數據的隊列。

Clock

一個用於音視頻同步的結構體

方法介紹

stream_open

對一些結構體進行初始化,而後建立文件讀取線程和視頻渲染線程

ffplay_video_thread

視頻解碼線程執行的方法

audio_thread

音頻解碼線程執行的方法

video_refresh_thread

視頻渲染,音視頻同步

調用流程


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/…

相關文章
相關標籤/搜索