音視訊同步介紹
音訊和視訊是各自執行緒獨立播放的,需要同步行為來保證聲畫的時間節點是一致的或者時間偏差值在一定的範圍內。一般來說是根據音訊時間來做同步,也就是將視訊同步到音訊。從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 所達成的同步。