一、前言
用ffmpeg来做音视频同步,个人认为这个是ffmpeg基础处理中最难的一个,无数人就卡在这里,怎么也不准,本人也是尝试过网上各种demo,基本上都是渣渣,要么仅仅支持极其少量的视频文件比如收到的数据包是一帧视频一帧音频的,要么根本没法同步歪七八糟的,要么进度跳过去直接蹦蹦蹦崩溃的,其实最完美的音视频同步处理demo就是ffplay,我亲测过几十种各种各样的音视频本地文件,数十种视频流文件,都是非常完美,当然啦这是亲生的啦,不完美还玩个屁。
如果仅仅是播放视频流(不带音频流),可能不需要音视频同步,所以最开始只做rtsp视频流播放的时候根本没有考虑同步的问题,因为没遇到也不需要,等到后期发现各种rtmp、http、m3u8这种视频流的时候,问题大了去了,他是hls格式的视频流文件一次性过来的,一个个小视频文件过来的,如果没有同步的话,意味着突然之间刷刷刷的图片过去很多,下一次来的又是刷刷的,这就需要自己计算同步了,上次接收到的数据包放入队列,到了需要显示的时候就显示。
常用的音视频同步方法:
- 通过fps来控制,fps表示一秒钟播放多少帧,比如25帧,可以自行计算一帧解码用掉的时间,一帧占用(1000/25=40毫秒),通过延时来处理,这其实是最渣渣的办法。
- 记住开始解码的时间startTime,通过av_rescale_q计算pts时间,两者的差值就是需要延时的时间,调用av_usleep来延时,这种只有部分文件正常,很多时候不正常。
- 音频同步到视频,视频时钟作为主时钟,没试过,网上很多人说这个办法不好。
- 视频同步到音频,音频时钟作为主时钟,没试过,据说大部分人采用的此办法。
- 音视频同步到外部时钟,外部时钟作为主时钟,最终采用的办法,容易理解互不干扰,各自按照外部时钟去同步自己。
- ffplay自身内置了三种同步策略,可以通过参数来控制采用何种策略,默认是视频同步到音频。
二、功能特点
- 多线程实时播放视频流+本地视频+USB摄像头等。
- 支持windows+linux+mac,支持ffmpeg3和ffmpeg4,支持32位和64位。
- 多线程显示图像,不卡主界面。
- 自动重连网络摄像头。
- 可设置边框大小即偏移量和边框颜色。
- 可设置是否绘制OSD标签即标签文本或图片和标签位置。
- 可设置两种OSD位置和风格。
- 可设置是否保存到文件以及文件名。
- 可直接拖曳文件到ffmpegwidget控件播放。
- 支持h265视频流+rtmp等常见视频流。
- 可暂停播放和继续播放。
- 支持存储单个视频文件和定时存储视频文件。
- 自定义顶部悬浮条,发送单击信号通知,可设置是否启用。
- 可设置画面拉伸填充或者等比例填充。
- 可设置解码是速度优先、质量优先、均衡处理。
- 可对视频进行截图(原始图片)和截屏。
- 录像文件存储支持裸流和MP4文件。
- 音视频完美同步,采用外部时钟同步策略。
- 支持seek定位播放位置。
- 支持qsv、dxva2、d3d11va等硬解码。
- 支持opengl绘制视频数据,极低CPU占用。
- 支持安卓和嵌入式linux,交叉编译即可。
三、效果图
四、相关站点
- 国内站点:https://gitee.com/feiyangqingyun/QWidgetDemo
- 国际站点:https://github.com/feiyangqingyun/QWidgetDemo
- 知乎主页:https://www.zhihu.com/people/feiyangqingyun/
五、核心代码
void FFmpegSync::run()
{
reset();
while (!stopped) {
//暂停状态或者队列中没有帧则不处理
if (!thread->isPause && packets.count() > 0) {
mutex.lock();
AVPacket *packet = packets.first();
mutex.unlock();
//h264的裸流文件同步有问题,获取不到pts和dts,暂时用最蠢的办法延时解决
if (thread->formatName == "h264") {
int sleepTime = (1000 / thread->videoFps) - 5;
msleep(sleepTime);
}
//计算当前帧显示时间 外部时钟同步
ptsTime = getPtsTime(thread->formatCtx, packet);
if (!this->checkPtsTime()) {
msleep(1);
continue;
}
//显示当前的播放进度
checkShowTime();
//0-表示音频 1-表示视频
if (type == 0) {
thread->decodeAudio(packet);
} else if (type == 1) {
thread->decodeVideo(packet);
}
//释放资源并移除
thread->free(packet);
mutex.lock();
packets.removeFirst();
mutex.unlock();
}
msleep(1);
}
clear();
stopped = false;
}
bool FFmpegSync::checkPtsTime()
{
bool ok = false;
if (ptsTime > 0) {
if (ptsTime > offsetTime + 100000) {
bufferTime = ptsTime - offsetTime + 100000;
}
int offset = (type == 0 ? 1000 : 5000);
offsetTime = av_gettime() - startTime + bufferTime;
if ((offsetTime <= ptsTime && ptsTime - offsetTime <= offset) || (offsetTime > ptsTime)) {
ok = true;
}
} else {
ok = true;
}
return ok;
}