一、前言
首先潑一盆冷水,在不同的電腦上實現完完全全的幀同步理論上是不可能的,市面上所有號稱幀同步的播放器,同一臺電腦不同拼接影片可以透過合併成一張圖片來繪製實現完完全全的幀同步,不同電腦,受限於網路的延遲,命令互動的時間佔用,不同硬體之間的主頻偏差等,肯定會有些許的誤差,只要誤差控制在1幀以內,人的肉眼是完全看不出來的,比如誤差5ms,看不出來的。這個和零延遲的推流軟體道理一樣,不可能零延遲的,只能夠做到肉眼分不清的延遲,就已經可以了。
搞幀同步播放核心就兩點,第一點保證幀序號一致,第二點保證重新整理的時間一致。兩者缺一不可,否則無法實現真正的幀同步。序號一致這個搞音影片開發的都能做到,可以先快取也好,暫停也好,程式底層肯定是知道當前要播放的是第幾幀。保證重新整理時間一致這個也非常關鍵,哪怕是在同一臺電腦,由於分時多工作業系統是透過中斷來併發執行指令的,指令的傳遞和最終的繪製都有時間偏差,尤其是在資源佔用很多的時候,所以一個技巧就是,等待,等到所有影片幀全部解碼完整就差繪製的時候,然後讓多個介面同時繪製,這樣就能將誤差控制在極低極低範圍,基本上控制在1幀以內比如5ms。在現在的多工作業系統中,完全一致肯定是不可能的,一般可能會有1-2箇中斷的時間差,可能有5-10ms的差,不過沒關係,一般25fps也要40ms才有一幀,哪怕是60fps的也要16.7ms一幀,這個誤差幾乎不影響。
二、效果圖
三、相關地址
- 國內站點:https://gitee.com/feiyangqingyun
- 國際站點:https://github.com/feiyangqingyun
- 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 檔案地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 檔名:bin_video_sync。
四、功能特點
- 實時幀同步,本地無縫拼接多個影片。
- 支援網路同步,可選主控端和被控端,主控端將本地播放的進度實時同步到被控端。
- 網路同步支援組播、廣播、單播三種模式,預設組播,既可以跨網段,也可以避免廣播資料風暴。
- 預設開啟自動同步,也可以手動同步和復位同步,手動同步是立即執行一次同步,將第一個影片的進度同步到其他影片檔案,復位同步是將所有影片播放進度切換到最開始0的位置。
- 支援各種視音訊檔案,包括但不限於mp4/mov/mkv/rmvb/avi等格式。
- 硬解碼和GPU繪製,最大化利用硬體資源,支援qsv/cuda/dxva2/d3d11va/vaapi等硬解碼。
- 極低的CPU佔用,8K30fps只佔不到1%的CPU,解碼和繪製全部交給GPU。
- 提供示例按照行列生成多個影片播放視窗,每個視窗可以選擇不同的影片檔案,在手動同步模式下,可以切換任意一個影片播放進度,會將所有的影片按照這個進度同步。
- 自動迴圈播放影片檔案,無縫切換迴圈播放,看起來非常絲滑。
- 支援Qt4/Qt5/Qt6所有版本,支援各種作業系統包括國產OS和嵌入式OS。
五、相關程式碼
#include "synclocal.h"
#include "qthelper.h"
#include "frmplay.h"
SINGLETON_IMPL(SyncLocal)
QDateTime SyncLocal::SyncTime = QDateTime::currentDateTime().addDays(-1);
SyncLocal::SyncLocal(QObject *parent) : QThread(parent)
{
isStop = false;
this->reset();
syncInterval = 5;
syncOffset = 15;
syncSleep = 500;
updateInterval = 10;
}
SyncLocal::~SyncLocal()
{
this->stop();
}
void SyncLocal::run()
{
while (!isStop) {
this->checkPosition();
this->checkSync();
this->checkPause();
this->updateWidget();
count++;
msleep(updateInterval);
//qDebug() << TIMEMS << "111" << updateInterval << count;
}
isStop = false;
this->reset();
}
void SyncLocal::checkPosition()
{
//同步間隔0表示不啟用/至少要2個窗體才需要同步
int size = frmPlay::widgets.size();
if (size < 2 || isSync || isPasue) {
count = 0;
return;
}
//永遠同步到到第一個窗體/處於非播放狀態或者暫停狀態不用繼續
frmPlay *widget = frmPlay::widgets.first();
if (!widget->isPlaying() || widget->isPaused()) {
return;
}
//優先執行手動同步指令/-1則同步到第一個窗體/>=0則同步到對應位置
if (syncPosition >= -1) {
position = (syncPosition == -1 ? widget->position() : syncPosition);
count = 0;
isSync = true;
qDebug() << TIMEMS << "hand" << position;
return;
}
//同步間隔0表示不啟用
if (syncInterval == 0) {
count = 0;
return;
}
//計算同步間隔需要迴圈多少次
int maxCount = syncInterval * 1000 / updateInterval;
//到了需要同步的時候執行同步
if (count < maxCount) {
return;
}
count = 0;
position = widget->position();
//剛開始或者快結束先不同步
if (position < 1000 || qAbs(widget->duration() - position) < 1000) {
return;
}
for (int i = 1; i < size; ++i) {
offset = position - frmPlay::widgets.at(i)->position();
qDebug() << TIMEMS << "posi" << position << "\t" << offset;
if (qAbs(offset) >= syncOffset) {
isSync = true;
break;
}
}
}
void SyncLocal::checkSync()
{
//同步標誌位為真則先同步
if (isSync) {
count = 0;
isSync = false;
isPasue = true;
SyncTime = QDateTime::currentDateTime();
qDebug() << TIMEMS << "seek" << position;
//先暫停再執行設定進度
foreach (frmPlay *widget, frmPlay::widgets) {
widget->pause();
widget->seek(position);
}
}
}
void SyncLocal::checkPause()
{
//暫停階段說明剛才執行過同步/等待一段時間重新播放
if (isPasue) {
qint64 time = SyncTime.msecsTo(QDateTime::currentDateTime());
if (time >= syncSleep) {
foreach (frmPlay *widget, frmPlay::widgets) {
widget->next();
}
count = 0;
isPasue = false;
syncPosition = -2;
emit receiveSync(offset);
qDebug() << TIMEMS << "play" << position;
}
}
}
void SyncLocal::updateWidget()
{
//重新整理介面用來觸發繪製
foreach (frmPlay *widget, frmPlay::widgets) {
widget->updateVideo();
}
}
void SyncLocal::setSyncInterval(int syncInterval)
{
this->reset();
this->syncInterval = syncInterval;
}
void SyncLocal::setSyncOffset(int syncOffset)
{
this->syncOffset = syncOffset;
}
void SyncLocal::setSyncSleep(int syncSleep)
{
this->syncSleep = syncSleep;
}
void SyncLocal::setUpdateInterval(int updateInterval)
{
this->updateInterval = updateInterval;
}
void SyncLocal::stop()
{
if (this->isRunning()) {
this->isStop = true;
this->wait();
}
}
void SyncLocal::reset()
{
this->count = 0;
this->isSync = false;
this->isPasue = false;
this->syncPosition = -2;
}
//-1則同步到第一個窗體/>=0則同步到對應位置
void SyncLocal::sync(qint64 position)
{
//至少要兩個窗體才能同步/處於暫停階段說明上一個同步還沒執行完成
if (frmPlay::widgets.size() >= 2 && !isPasue && syncPosition == -2) {
this->syncPosition = position;
}
}