平台为嵌入式音视频解码器,使用linux作为操作系统,音频采用alsa架构。
一、STC(本地时钟作为一个基准)
如果是ts封装,可以使用PCR;如果为RTP封装,可以使用音频时间戳或者rtcp的本地时钟。
原理就是将PCR或者音频时间戳(为了音频play时自己使用,要有一个偏移,但只要是同源就行)同步到本地STC中。
本地STC在解码端可以使用一个硬件STC维护多组,就是使用time_base+delta的方法。或者使用软件的定时器也可以,就是精度差一些。
二、缓冲
这个是比较重要的,多长时间的缓冲,决定你能抗击的时间波动。
没有缓冲无法进行同步。
可以使用buffer Tab的方式,循环使用。
三、视频
视频同步策略相对比较简单,就是当显示或者解码之前,将此帧的PTS与本地STC进行对比,delta = PTS - STC
如果delta < 0 则说明STC大,则此帧的播放时间已过,则需要加快速度追上去,所以将此帧丢弃不播放。
如果delta 近似为 0,则说明正好播放此帧,则直接播放;
如果delta > 0 则说明此帧的播放时间还没到来,所以需要先睡一会儿,再播。如果想提高精度,在睡的时候,可以1ms判断一下是否时间已到。注意睡的时间要有上限,防止PTS错误导致睡眠时间超长。
四、音频
音频比较麻烦,如果使用与视频近似的策略,由于ALSA架构的音频播放底层还有一个缓冲,如果这个底层缓冲空了的话,就会出现 underRun,属于音频播放的一种异常,会造成可听见的嗒嗒声出现。
如果采用视频的策略,则在睡过后进行播放时,很有可能出现这种underrun。
如果不进行同步,则无法准确的与视频唇齿一致。
总体来讲 ,同步音频应该采用少干涉的策略,就是在每次线程init音频设备后,进行一次缓冲操作,使用此帧的PTS作为基准,知道STC到达PTS时间后,再进行无干涉的循环播放,这是音频缓冲有一定数量的音频帧,播放时间也是准的。
在播放过程中,检测alsa写数据的时候的返回值是否为 -EPIPE,如果是,则发生了underRun,则退出这一级循环,同时deinit掉音频设备,再init音频设备,然后再缓冲一次,然后再进入循环播放,直至再次发生underRun。
static void audio_buffering(ipc_handle ipc_in, audio_play_obj *obj)
{
//音频解码还没有送入音频帧时,一直等待
while(get_ipc_numbers(ipc_in) <= 0)
{
mssleep(20);
//避免死循环
if (obj->startStop == 0)
{
return;
}
continue;
}
//获取一帧,以及此帧时间戳
buf = get_a_audio_frame_from_ipc(ipc_in);
ts = get_timestamp_from_buffer(buf);
while((stc = getSTC(chnId)) != 0)
{
//只到STC追上此帧,则进行播放,9000为90K时钟100ms的时间
if ((ts > (stc - 9000)) || (get_cached_frame_num_from_ipc(ipc_in) < 2))
{
mssleep(20);
continue;
}
break;
}
release_a_frame_to_ipc(buf);
}
void pthread_audio_play(void)
{
while(1)//主循环
{
handle->initDev = 1;
while(handle->initDev)
{
//初始化音频设备,设置hw、sw参数,open设备
init_deivce(&handle);
//进行缓冲操作
if (need_sync)
{
//先清空缓冲,防止缓冲上溢
fflush(ipc_in);
//一直缓冲到STC==PTS为止
audio_buffering(ipc_in, handle);
}
handle->startStop = 1;
while(handle->startStop)
{
//从缓冲中获取一帧
buf = get_a_audio_frame_from_ipc(ipc_in);
//播放这帧
if (audio_play(buf) < 0)
{
//退出两级循环,如果只退出一级循环,则buffering过后,还会遇到underRun,所以需要重新init设备才可以
startStop = 0;
initDev = 0;
}
//向IPC归还buffer
release_a_frame_to_ipc(buf);
}
deinit_device(handle);
}
}
}