之前我們對單獨的音頻和視頻的播放進行了分析。
但是實際上播放一段影片,還需要音視頻同步播放。
主要思路是
- 在解碼獲得數據時,對frame的pts進行計算。
- 在視頻送顯的時候,或者是音頻賦值的時候,進行時間的糾正。
如果以音頻時間為主的話,就需要修正視頻的送顯時間。
如果是視頻的時間為主的,同樣需要修正音頻的播放時間。(通過減少音頻的播放幀數。)
1. 計算PTS
case AVMEDIA_TYPE_VIDEO:
//對視頻進行解碼。
ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp);
if (got_frame) {
//默認情況下 為-1
if (decoder_reorder_pts == -1) {
//視頻的時間戳pts 可以通過av_frame_get_best_effort_timestamp來計算
frame->pts = av_frame_get_best_effort_timestamp(frame);
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
//對音頻進行解碼
ret = avcodec_decode_audio4(d->avctx, frame, &got_frame, &d->pkt_temp);
if (got_frame) {
//通過sample_rate來計算time_base
AVRational tb = (AVRational){1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
//pts的通用公式 pts*av_q2d(time_base)
//av_rescale_q(a,b,c)是用來把時間戳從一個時基調整到另外一個時基時候用的函數。
//它基本的動作是計算a*b/c 。將編碼器中的時基和當前的時基做轉換。因為我們上面可能轉碼嗎?
frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb);
else if (d->next_pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
//記錄下 next_pts
if (frame->pts != AV_NOPTS_VALUE) {
d->next_pts = frame->pts + frame->nb_samples;
d->next_pts_tb = tb;
}
}
這樣就計算好了,視頻幀和音頻幀的pts。
2. 同步
以音頻時間鐘
這個時候就需要同步的就是視頻。
同樣在顯示的時候,進行處理
video_refresh方法
static void video_refresh(void *opaque, double *remaining_time)
{
//....省略
double last_duration, duration, delay;
Frame *vp, *lastvp;
//計算duration
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);
//計算兩幀的時間
last_duration = vp_duration(is, lastvp, vp);
//通過這方法來計算延遲
delay = compute_target_delay(last_duration, is);
//獲取當前的時間
time= av_gettime_relative()/1000000.0;
//還未到顯示的時間,這個時候先保持當前幀的顯示,并且計算下次循環的睡眠時間
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
//累加幀的時間。frame_timer就是下一個幀顯示的時間
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);
進入compute_target_delay方法,看看如何計算延遲
static double compute_target_delay(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) {
//如果視頻是從屬的時間鐘,如果延遲比較大的話,那么需要通過重復顯示或者是刪除幀來修正延遲。
//計算兩個時間的pts差值。
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));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
//如果當前的視頻太慢了。就讓他的delay 比duration小,但是不能小于0
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
//如果當前的視頻太快了,而且大于幀持續的時間,則使用diff進行同步,讓他休眠更差的時間
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
//如果實在太快了。就讓它休眠兩個duration
else if (diff >= sync_threshold)
delay = 2 * delay;
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}
最后同步到時間鐘上。
雖然我們設定了睡眠的時間,但是同步時,我們還是用正常的PTS。
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
/* update current video pts */
set_clock(&is->vidclk, pts, serial);
sync_clock_to_slave(&is->extclk, &is->vidclk);
}
雖然改變了睡眠的時間。照樣還是使用pts來同步。這點和音頻的不一樣,音頻同步的是pts+ duration 。
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
最后幾個變量
/* no AV sync correction is done if below the minimum AV sync threshold */
// 最低同步閾值,如果低于該值,則不需要同步校正
#define AV_SYNC_THRESHOLD_MIN 0.04
/* AV sync correction is done if above the maximum AV sync threshold */
// 最大同步閾值,如果大于該值,則需要同步校正
#define AV_SYNC_THRESHOLD_MAX 0.1
/* If a frame duration is longer than this, it will not be duplicated to compensate AV sync */
// 幀補償同步閾值,如果幀持續時間比這更長,則不用來補償同步
#define AV_SYNC_FRAMEDUP_THRESHOLD 0.1
/* no AV correction is done if too big error */
// 同步閾值。如果誤差太大,則不進行校正
#define AV_NOSYNC_THRESHOLD 10.0
double max_frame_duration; // 最大幀顯示時間 // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
image.png
如果是用視頻為主時間的話
音頻的時間賦值
不是主時間鐘的話
static int audio_decode_frame(VideoState *is)
{
//...省略解碼的代碼。
//音頻的同步,是通過控制frame的數量nb_samples,來進行同步的。
wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
//...省略代碼
audio_clock0 = is->audio_clock;
//因為上面結果調整,這里重新根據nb_samples計算一次
/* 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;
is->audio_clock_serial = af->serial;
#ifdef DEBUG
{
static double last_clock;
printf("audio: delay=%0.3f clock=%0.3f clock0=%0.3f\n",
is->audio_clock - last_clock,
is->audio_clock, audio_clock0);
last_clock = is->audio_clock;
}
#endif
return resampled_data_size;
}
主要來看一下synchronize_audio
方法
//如果不是音頻為主的時間鐘,返回samples來進行更好的同步
static int synchronize_audio(VideoState *is, int nb_samples)
{
int wanted_nb_samples = nb_samples;
/* if not master, then we try to remove or add samples to correct the clock */
if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {
double diff, avg_diff;
int min_nb_samples, max_nb_samples;
//同樣,先計算兩個時間鐘之間的diff
diff = get_clock(&is->audclk) - get_master_clock(is);
//兩者的差距,在閥值的范圍內,表示還能調整。AV_NOSYNC_THRESHOLD =10.0
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
//這里這個audio_diff_avg_coef 的算法不理解,使用差值來實現平均值, AUDIO_DIFF_AVG_NB=20
is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
/* not enough measures to have a correct estimate */
//累計的延遲還不夠,繼續累加。會累計20次的差值,來計算上面的平均數
is->audio_diff_avg_count++;
} else {
//進行修正。
//先計算通過累計的diff_cum平均進行估計
/* estimate the A-V difference */
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
//延遲的平均數,確實是大于diff
if (fabs(avg_diff) >= is->audio_diff_threshold) {
//diff*samplerate 可以計算補償的樣本數
wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
//最大和最小的當前的修正參數。SAMPLE_CORRECTION_PERCENT_MAX=10.
//min 90% max 110%
min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
//為了避免音調過高的問題,只能在這個區間補償
wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
}
av_log(NULL, AV_LOG_TRACE, "diff=%f adiff=%f sample_diff=%d apts=%0.3f %f\n",
diff, avg_diff, wanted_nb_samples - nb_samples,
is->audio_clock, is->audio_diff_threshold);
}
} else {
/* too big difference : may be initial PTS errors, so
reset A-V filter */
//差距太大了。說明這里的平均數可能計算錯誤了。重新來統計。
is->audio_diff_avg_count = 0;
is->audio_diff_cum = 0;
}
}
return wanted_nb_samples;
}
時間差值乘以采樣率可以得到用于補償的樣本數,加之原樣本數,即應輸出樣本數。另外考慮到上一節提到的音頻音調變化問題,這里限制了調節范圍在正負10%以內。
所以如果音視頻不同步的差值較大,并不會立即完全同步,最多只調節當前幀樣本數的10%,剩余會在下次調節時繼續校正。
最后,是與視頻同步音頻時類似地,有一個準同步的區間,在這個區間內不去做同步校正,其大小是audio_diff_threshold
:
is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;
即音頻輸出設備內緩沖的音頻時長。
時間同步
一是時間鐘的同步
先來看一下Clock
,這個結構體的定義
// 時鐘
typedef struct Clock {
double pts; // 時鐘基準 /* clock base */
double pts_drift; // 更新時鐘的差值 /* clock base minus time at which we updated the clock */
double last_updated; // 上一次更新的時間
double speed; // 速度
int serial; // 時鐘基于使用該序列的包 /* clock is based on a packet with this serial */
int paused; // 停止標志
int *queue_serial; // 指向當前數據包隊列序列的指針,用于過時的時鐘檢測 /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
// 時鐘同步類型
enum {
AV_SYNC_AUDIO_MASTER, // 音頻作為同步,默認以音頻同步 /* default choice */
AV_SYNC_VIDEO_MASTER, // 視頻作為同步
AV_SYNC_EXTERNAL_CLOCK, // 外部時鐘作為同步 /* synchronize to an external clock */
};
/**
* 更新視頻的pts
* @param is [description]
* @param pts [description]
* @param pos [description]
* @param serial [description]
*/
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
/* update current video pts */
set_clock(&is->vidclk, pts, serial);
sync_clock_to_slave(&is->extclk, &is->vidclk);
}
/**
* 設置時鐘
* @param c [description]
* @param pts [description]
* @param serial [description]
*/
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
/**
* 同步從屬時鐘
* @param c [description]
* @param slave [description]
*/
static void sync_clock_to_slave(Clock *c, Clock *slave)
{
double clock = get_clock(c);
double slave_clock = get_clock(slave);
if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))
set_clock(c, slave_clock, slave->serial);
}
/**
* 獲取時鐘
* @param c [description]
* @return [description]
*/
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) {
return c->pts;
} else {
double time = av_gettime_relative() / 1000000.0;
//pts_drift 是更新的時間鐘的差值?
//最后的時間是 更新的差值+ 當前的時間-當前的時間和上一次更新的時間之間的差值*速度
//默認的情況下,根據上一次的drift計算下一次要出現的時間。
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
/**
* 更新視頻的pts
* @param is [description]
* @param pts [description]
* @param pos [description]
* @param serial [description]
*/
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
/* update current video pts */
set_clock(&is->vidclk, pts, serial);
//將尾部的時間鐘,用視頻的時機鐘來進行同步
sync_clock_to_slave(&is->extclk, &is->vidclk);
}
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
//使用當前的事來計算這幾個值。也就是這一幀送顯之前的操作的時間。
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
c->pts = pts;
c->last_updated = time;
c->pts_drift = c->pts - time;
c->serial = serial;
}
pts_drift
是表示預測的pts
和當前的時間的間隔。通過這個時間來預算下一幀的時間。
最后的同步
static void sync_clock_to_slave(Clock *c, Clock *slave)
{
double clock = get_clock(c);
double slave_clock = get_clock(slave);
if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))
set_clock(c, slave_clock, slave->serial);
}
pts_drift 存在的意義, 是為了 去掉編碼的時間嗎?
image.png
外部時間鐘
如果是以外部時間作為同步的話,上面兩個都需要進行調整。