一、前言
算下來這個推流的項目作品寫了有四年多了,最初第一個版本只有文件點播的功能,用的純QTcpSocket通信實現,屬於比較簡單的功能。由於文件點播只支持文件形式的推流,不支持網絡流或者本地設備採集,所以迫切需要打破這個瓶頸,而後加入核心的網絡推流功能,這也是本項目的核心功能,不僅支持各種各樣的流媒體服務,推流這塊支持文件、網絡音視頻流、本地設備採集、本地桌面採集。自定義各種參數,視音頻同步推流等,不斷的迭代和完善。也不是一開始就具備這些功能的,而是隨着視頻播放組件的保存視頻功能不斷完善後改進的,因爲推流其實就是保存功能,只不過保存到一個推流地址就行,然後推流的格式換下,所以是和保存功能完全公用的。整個推流組件是負責管理一堆的保存類,拿到當前推流狀態,當前音視頻是否存在以及是否編碼推流的狀態顯示到表格中。
一開始也是沒有網頁預覽的功能,後面用戶對這塊要求比較強烈,都是希望推流後能夠通過一個簡單的方式,能夠直接網頁中預覽,有多少個通道就顯示多少個通道,這樣可以判斷推流是否成功,不然要一個個手動的打開播放器輸入播放地址驗證,很麻煩。而且推流的主要應用場景就是希望推流後給網頁或者手機app拉流顯示。直接網頁預覽還可以對比實時性,用戶對兩個指標特別敏感,一個是延遲,一個是流暢。所以一直在這塊功能精心打磨,儘量做到極致。
二、效果圖
三、體驗地址
- 國內站點: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_push。
- 視頻主頁:https://space.bilibili.com/687803542
四、功能特點
- 支持各種本地音視頻文件和網絡音視頻文件,格式包括mp3、aac、wav、wma、mp4、mkv、rmvb、wmv、mpg、flv、asf等。
- 支持各種網絡音視頻流,網絡攝像頭,協議包括rtsp、rtmp、http等。
- 支持本地攝像頭設備推流,可指定分辨率、幀率、格式等。
- 支持本地桌面採集推流,可指定屏幕索引、採集區域、起始座標、幀率等,也支持指定窗口標題進行採集。
- 可實時切換預覽視頻文件,可切換音視頻文件播放進度,切換到哪裏就推流到哪裏。預覽過程中可以切換靜音狀態和暫停推流。
- 可指定重新編碼推流,任意源頭格式可選強轉264或265格式。
- 可轉換分辨率推流,設置等比例縮放或者指定分辨率進行轉換。
- 推流的清晰度、質量、碼率都可調,可以節約網絡帶寬和拉流端的壓力。
- 音視頻文件自動循環不間斷推流。
- 音視頻流有自動掉線重連機制,重連成功自動繼續推流。
- 支持各種流媒體服務程序,包括但不限於mediamtx、ZLMediaKit、srs、LiveQing、nginx-rtmp、EasyDarwin、ABLMediaServer。
- 通過配置文件自動加載對應流媒體程序的協議和端口,自動生成推流地址和各種協議的拉流地址。可以通過配置文件自己增加流媒體程序。
- 可選rtmp、rtmp格式推流,推流成功後,支持多種格式拉流,包括但不限於rtsp、rtmp、hls、flv、ws-flv、webrtc等。
- 在軟件上推流成功後,可以直接單擊網頁預覽,實時預覽推流後拉流的畫面,多畫面網頁展示。
- 軟件界面上可單擊對應按鈕,動態添加文件和目錄,可手動輸入地址。
- 推拉流實時性極高,延遲極低,延遲時間大概在100ms左右。
- 極低CPU資源佔用,4路主碼流推流只需要佔用0.2%CPU。理論上常規普通PC機器推100路毫無壓力,主要性能瓶頸在網絡。
- 可以推流到外網服務器,然後通過手機、電腦、平板等設備播放對應的視頻流。
- 每路推流都可以手動指定唯一標識符(方便拉流/用戶無需記憶複雜的地址),沒有指定則按照策略隨機生成hash值。也支持自動按照指定標識後面加數字的方式遞增命名。比如設置標識爲字母v,策略爲標識遞增,則每添加一個對應的推流碼命名依次是v1、v2、v3等。
- 根據推流協議自動轉碼格式,默認策略按照選擇的推流協議,比如rtsp支持265而rtmp不支持,如果是265的文件而選擇rtmp推流,則自動轉碼成264格式再推流。
- 音視頻同步推流,在拉流和採集的時候就會自動處理好同步,同步後的數據再推流。
- 表格中實時顯示每一路推流的分辨率和音視頻數據狀態,灰色表示沒有輸入流,黑色表示沒有輸出流,綠色表示原數據推流,紅色表示轉碼後的數據推流。
- 自動重連視頻源,自動重連流媒體服務器,保證啓動後,推流地址和打開地址都實時重連,只要恢復後立即連上繼續採集和推流。
- 根據不同的流媒體服務器類型,自動生成對應的rtsp、rtmp、hls、flv、ws-flv、webrtc拉流地址,用戶可以直接複製該地址到播放器或者網頁中預覽查看。
- 添加的推流地址等信息自動存儲到文件,可以手動打開進行修改,默認啓動後自動加載歷史記錄。
- 可以指定生成的網頁文件保存位置,方便作爲網站網頁發佈,可以直接在瀏覽器中輸入網址進行訪問,發佈後可以直接在局域網其他設備比如手機或者電腦打開對應網址訪問。
- 可選是否開機啓動、後臺運行等。網絡推流添加的rtsp地址可勾選是否隱藏地址中的用戶信息。
- 自帶設備推流模塊,自動識別本地設備,包括本地的攝像頭和桌面,可以手動選擇不同的是視頻和音頻採集設備進行推流。
- 自帶文件點播模塊,添加文件後用戶可以拉取地址點播,用戶端可以任意切換播放進度。支持各種瀏覽器(谷歌chromium、微軟edge、火狐firefox等)、各種播放器(vlc、mpv、ffplay、potplayer、mpchc等)打開請求。
- 文件點播模塊實時統計顯示每個文件對應的訪問數量、總訪問數量、不同IP地址訪問數量。
- 文件點播模塊採用純QTcpSocket通信,不依賴流媒體服務程序,核心源碼不到500行,註釋詳細,功能完整。
- 支持任意Qt版本(Qt4、Qt5、Qt6),支持任意系統(windows、linux、macos、android、嵌入式linux等)。
五、相關代碼
void NetPushClient::record()
{
if (ffmpegSave) {
//取出推流碼
QString flag = pushUrl.split("/").last();
//文件名不能包含特殊字符/需要替換成固定字母
QString pattern("[\\\\/:|*?\"<>]|[cC][oO][mM][1-9]|[lL][pP][tT][1-9]|[cC][oO][nM]|[pP][rR][nN]|[aA][uU][xX]|[nN][uU][lL]");
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QRegularExpression rx(pattern);
#else
QRegExp rx(pattern);
#endif
flag.replace(rx, "X");
//文件名加上時間結尾
QString path = QString("%1/video/%2").arg(qApp->applicationDirPath()).arg(QDATE);
QString name = QString("%1/%2_%3.mp4").arg(path).arg(flag).arg(STRDATETIME);
//目錄不存在則新建
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(path);
}
//先停止再打開重新錄製
ffmpegSave->stop();
ffmpegSave->open(name);
recordTime = QDateTime::currentDateTime();
}
}
void NetPushClient::receivePlayStart(int time)
{
//演示添加OSD後推流
#ifdef betaversion
int height = ffmpegThread->getVideoHeight();
QList<OsdInfo> osds = WidgetHelper::getTestOsd(height);
ffmpegThread->setOsdInfo(osds);
#endif
//打開後才能啓動錄像
ffmpegThread->recordStart(pushUrl);
//推流以外還單獨存儲
if (!ffmpegSave && recordType > 0) {
//源頭保存沒成功就不用繼續
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
if (!saveFile->getIsOk()) {
return;
}
ffmpegSave = new FFmpegSave(this);
//重新編碼過的則取視頻保存類的對象
AVStream *videoStreamIn = saveFile->getVideoEncode() ? saveFile->getVideoStream() : ffmpegThread->getVideoStream();
AVStream *audioStreamIn = saveFile->getAudioEncode() ? saveFile->getAudioStream() : ffmpegThread->getAudioStream();
ffmpegSave->setSavePara(ffmpegThread->getMediaType(), SaveVideoType_Mp4, videoStreamIn, audioStreamIn);
this->record();
timerRecord->start();
}
}
void NetPushClient::receivePacket(AVPacket *packet)
{
if (ffmpegSave && ffmpegSave->getIsOk()) {
ffmpegSave->writePacket2(packet);
}
FFmpegHelper::freePacket(packet);
}
void NetPushClient::recorderStateChanged(const RecorderState &state, const QString &file)
{
int width = 0;
int height = 0;
int videoStatus = 0;
int audioStatus = 0;
if (ffmpegThread) {
width = ffmpegThread->getVideoWidth();
height = ffmpegThread->getVideoHeight();
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
if (saveFile->getIsOk()) {
if (saveFile->getVideoIndexIn() >= 0) {
if (saveFile->getVideoIndexOut() >= 0) {
videoStatus = (saveFile->getVideoEncode() ? 3 : 2);
} else {
videoStatus = 1;
}
}
if (saveFile->getAudioIndexIn() >= 0) {
if (saveFile->getAudioIndexOut() >= 0) {
audioStatus = (saveFile->getAudioEncode() ? 3 : 2);
} else {
audioStatus = 1;
}
}
}
}
//只有處於錄製中才表示正常推流開始
bool start = (state == RecorderState_Recording);
emit pushStart(mediaUrl, width, height, videoStatus, audioStatus, start);
}
void NetPushClient::receiveSaveStart()
{
emit pushChanged(mediaUrl, 0);
}
void NetPushClient::receiveSaveFinsh()
{
emit pushChanged(mediaUrl, 1);
}
void NetPushClient::receiveSaveError(int error)
{
emit pushChanged(mediaUrl, 2);
}
void NetPushClient::setMediaUrl(const QString &mediaUrl)
{
this->mediaUrl = mediaUrl;
}
void NetPushClient::setPushUrl(const QString &pushUrl)
{
this->pushUrl = pushUrl;
}
void NetPushClient::start()
{
if (ffmpegThread || mediaUrl.isEmpty() || pushUrl.isEmpty()) {
return;
}
//實例化視頻採集線程
ffmpegThread = new FFmpegThread;
//關聯播放開始信號用來啓動推流
connect(ffmpegThread, SIGNAL(receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
//關聯錄製信號變化用來判斷是否推流成功
connect(ffmpegThread, SIGNAL(recorderStateChanged(RecorderState, QString)), this, SLOT(recorderStateChanged(RecorderState, QString)));
//設置播放地址
ffmpegThread->setMediaUrl(mediaUrl);
//設置解碼內核
ffmpegThread->setVideoCore(VideoCore_FFmpeg);
//設置視頻模式
#ifdef openglx
ffmpegThread->setVideoMode(VideoMode_Opengl);
#else
ffmpegThread->setVideoMode(VideoMode_Painter);
#endif
//設置通信協議(如果是rtsp視頻流建議設置tcp)
//ffmpegThread->setTransport("tcp");
//設置硬解碼(和推流無關/只是爲了加速顯示/推流只和硬編碼有關)
//ffmpegThread->setHardware("dxva2");
//設置緩存大小(如果分辨率幀率碼流很大需要自行加大緩存)
ffmpegThread->setCaching(8192000);
//設置解碼策略(推流的地址再拉流建議開啓最快速度)
//ffmpegThread->setDecodeType(DecodeType_Fastest);
//設置讀取超時時間超時後會自動重連
ffmpegThread->setReadTimeout(5 * 1000);
//設置連接超時時間(0表示一直連)
ffmpegThread->setConnectTimeout(0);
//設置重複播放相當於循環推流
ffmpegThread->setPlayRepeat(true);
//設置默認不播放音頻(界面上切換到哪一路就開啓)
ffmpegThread->setPlayAudio(false);
//設置默認不預覽視頻(界面上切換到哪一路就開啓)
ffmpegThread->setPushPreview(false);
//設置保存視頻類將數據包信號發出來用於保存文件
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
saveFile->setProperty("checkB", true);
saveFile->setSendPacket(recordType > 0, false);
connect(saveFile, SIGNAL(receivePacket(AVPacket *)), this, SLOT(receivePacket(AVPacket *)));
connect(saveFile, SIGNAL(receiveSaveStart()), this, SLOT(receiveSaveStart()));
connect(saveFile, SIGNAL(receiveSaveFinsh()), this, SLOT(receiveSaveFinsh()));
connect(saveFile, SIGNAL(receiveSaveError(int)), this, SLOT(receiveSaveError(int)));
//如果是本地設備或者桌面錄屏要取出其他參數
VideoHelper::initVideoPara(ffmpegThread, mediaUrl, encodeVideoScale);
//設置視頻編碼格式/視頻壓縮比率/視頻縮放比例
ffmpegThread->setEncodeVideo((EncodeVideo)encodeVideo);
ffmpegThread->setEncodeVideoRatio(encodeVideoRatio);
ffmpegThread->setEncodeVideoScale(encodeVideoScale);
//啓動播放
ffmpegThread->play();
}
void NetPushClient::stop()
{
//停止推流和採集並徹底釋放對象
if (ffmpegThread) {
ffmpegThread->recordStop();
ffmpegThread->stop();
ffmpegThread->deleteLater();
ffmpegThread = NULL;
}
//停止錄製
if (ffmpegSave) {
timerRecord->stop();
ffmpegSave->stop();
ffmpegSave->deleteLater();
ffmpegSave = NULL;
}
}