一、前言
最開始設計的時候,只考慮了一個屏幕的情況,這種當然是最理想的情況,實際上雙屏或者多屏的用戶也不在少數,比如我這兩個屏幕,屏幕1是1080P,屏幕2是2K分辨率,打印兩個屏幕的區域是 QRect(0,0 1920x1030), QRect(1920,-208 2560x1390),可以看到有個負數值(可以在操作系統中的排列顯示器拖動調整),而且如果屏幕左右的順序調換下,比如2K的分辨率在前面,打印的屏幕區域是 QRect(0,0 1920x1030), QRect(-2560,-185 2560x1390),可以看到2K的這個屏幕XY座標都是負數,你以爲這就是所有的情況了嗎?那就想錯了,還有可能是上下屏幕排列的,2K屏幕在下面打印區域 QRect(0,0 1920x1030), QRect(-639,1080 2560x1390),2K屏幕在上面打印區域是 QRect(0,0 1920x1030), QRect(-270,-1440 2560x1390),這還支持兩個屏幕的情況,如果是4個或者更多呢,如果要用戶獲取到對應屏幕的區域然後填入桌面錄製參數中,無異於難於上青天,這肯定是不可能的事情,而ffmpeg默認的參數就是要傳入真實的偏移值座標和分辨率,而用戶呢又習慣於在哪個屏幕打開的程序就以當前屏幕的分辨率爲基準,偏移值以左上角(0, 0)爲基準,所以約定用戶只需要填入分辨率和相對偏移值就行,不填入就以當前屏幕整體分辨率爲準,這就需要搞一個專門的轉換函數,專門獲取當前屏幕區域並計算各種情況。
經過上面大費周折的計算,以爲可以關機回家喫飯加雞腿了,又想多了,用戶可能輸入了超過當前分辨率的區域,或者偏移值加上採集分辨率超過了當前屏幕的分辨率,這樣是無法打開的,無法正常採集,程序不會執行,爲了能夠增強健壯性兼容性,有需要做一些調整,比如計算後發現設定的採集區域尺寸超過了屏幕的真實分辨率尺寸,就以設定的偏移值開始到右下角爲準裁剪,這樣無論用戶怎麼錯,程序就是不會錯,都能正常採集,以合理的方式進行調整,這纔是一個好的程序設計。
演示視頻:https://www.bilibili.com/video/BV1D8411B7eP
至此已實現的採集桌面的功能:
- 支持多屏幕,可以指定屏幕索引。
- 支持左右排列和上下排列以及自由調整屏幕位置。
- 支持指定採集區域。
- 自動校正超過屏幕區域的參數設定。
- 指定相對偏移值採集,以桌面左上角爲基準。
- 支持指定採集幀率。
- 不填寫分辨率和各種參數,自動計算默認值。
- 不指定屏幕則以鼠標所在當前屏幕爲準。
- 還有更多的細節在代碼中體現。
格式說明:
- url地址格式說明:desktop=desktop|800x600|25|0|0|0。
- 參數用英文豎槓隔開,其中desktop=是固定前綴,用於區分當前地址是用來採集桌面。
- 第一個參數表示設備標識,比如win上填desktop,linux填:0.0+0,0,mac填0:0。
- 第二個參數表示採集的分辨率,不填則默認取屏幕分辨率。
- 第三個參數表示幀率,基本上在2-30之間,不填的話默認ffmpeg會設定一個值,有時候是30。
- 第四/五個參數表示偏移值XY座標,從屏幕的左上角(0,0)開始。
- 第六個參數表示屏幕索引,不填的話則默認取當前鼠標所在屏幕。
- 寫法1:desktop=desktop,當前屏幕全屏採集。
- 寫法2:desktop=desktop||15|0|0|1,屏幕2全屏採集,幀率15。
- 寫法3:desktop=desktop|800x600|10|50|100,鼠標所在當前屏幕採集,採集區域rect(50,100,800,600),幀率10。
二、效果圖
三、體驗地址
- 國內站點: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。
四、功能特點
- 支持各種本地視頻文件和網絡視頻文件。
- 支持各種網絡視頻流,網絡攝像頭,協議包括rtsp、rtmp、http。
- 支持將本地攝像頭設備推流,可指定分辨率和幀率等。
- 支持將本地桌面推流,可指定屏幕區域和幀率等。
- 自動啓動流媒體服務程序,默認mediamtx(原rtsp-simple-server),可選用srs、EasyDarwin、LiveQing、ZLMediaKit等。
- 可實時切換預覽視頻文件,可切換視頻文件播放進度,切換到哪裏就推流到哪裏。
- 推流的清晰度和質量可調。
- 可動態添加文件、目錄、地址。
- 視頻文件自動循環推流,如果視頻源是視頻流,在掉線後會自動重連。
- 網絡視頻流自動重連,重連成功自動繼續推流。
- 網絡視頻流實時性極高,延遲極低,延遲時間大概在100ms左右。
- 極低CPU佔用,4路主碼流推流只需要佔用0.2%CPU。理論上常規普通PC機器推100路毫無壓力,主要性能瓶頸在網絡。
- 推流可選推流到rtsp/rtmp兩種,推流後的數據支持直接rtsp/rtmp/hls/webrtc四種方式訪問,可以直接瀏覽器打開看實時畫面。
- 可以推流到外網服務器,然後通過手機、電腦、平板等設備播放對應的視頻流。
- 每個推流都可以手動指定唯一標識符(方便拉流/用戶無需記憶複雜的地址),沒有指定則按照策略隨機生成hash值。
- 自動生成測試網頁直接打開播放,可以看到實時效果,自動按照數量對應宮格顯示。
- 推流過程中可以在表格中切換對應推流項,實時預覽正在推流的視頻,並可以切換視頻文件的播放進度。
- 音視頻同步推流,符合264/265/aac格式的自動原數據推流,不符合的自動轉碼再推流(會佔用一定CPU)。
- 轉碼策略支持三種,自動處理(符合要求的原數據/不符合的轉碼),僅限文件(文件類型的轉碼視頻),所有轉碼。
- 表格中實時顯示每一路推流的分辨率和音視頻數據狀態,灰色表示沒有輸入流,黑色表示沒有輸出流,綠色表示原數據推流,紅色表示轉碼後的數據推流。
- 自動重連視頻源,自動重連流媒體服務器,保證啓動後,推流地址和打開地址都實時重連,只要恢復後立即連上繼續採集和推流。
- 提供循環推流示例,一個視頻源同時推流到多個流媒體服務器,比如打開一個視頻同時推流到抖音/快手/B站等,可以作爲錄播推流,列表循環,非常方便實用。
- 根據不同的流媒體服務器類型,自動生成對應的rtsp/rtmp/hls/flv/ws-flv/webrtc地址,用戶可以直接複製該地址到播放器或者網頁中預覽查看。
- 編碼視頻格式可以選擇自動處理(源頭是264就264/源頭是265就265),轉H264(強制轉264),轉H265(強制轉265)。
- 支持Qt4/Qt5/Qt6任意版本,支持任意系統(windows/linux/macos/android/嵌入式linux等)。
五、相關代碼
void AbstractVideoThread::checkDeviceUrl(const QString &url, QString &deviceName, QString &resolution, int &frameRate, int &offsetX, int &offsetY, QString &encodeScale)
{
//無論是否帶分隔符第一個約定是設備名稱
QStringList list = url.split("|");
int count = list.count();
deviceName = list.at(0);
//默認不指定屏幕索引
int screenIndex = -1;
//用一個無用的參數作爲是否是本地攝像頭的標誌位
bool isCamera = (encodeScale == "camera");
//帶分隔符說明還指定了分辨率或幀率
if (count > 1) {
QStringList sizes = WidgetHelper::getSizes(list.at(1));
if (sizes.count() == 2) {
int width = sizes.at(0).toInt();
int height = sizes.at(1).toInt();
resolution = QString("%1x%2").arg(width).arg(height);
} else {
resolution = "0x0";
}
//第三個參數是幀率
if (count >= 3) {
frameRate = list.at(2).toInt();
}
//桌面採集還需要取出其他幾個參數
if (!isCamera) {
//XY座標偏移值
if (count >= 5) {
offsetX = list.at(3).toInt();
offsetY = list.at(4).toInt();
}
//屏幕索引
if (count >= 6) {
screenIndex = list.at(5).toInt();
}
//視頻縮放
if (count >= 7) {
encodeScale = list.at(6);
}
WidgetHelper::checkRect(screenIndex, resolution, offsetX, offsetY);
}
}
//沒有設置分辨率則重新處理
if (resolution == "0x0") {
if (isCamera) {
resolution = "640x480";
} else {
WidgetHelper::checkRect(screenIndex, resolution, offsetX, offsetY);
}
}
}
QList<QRect> WidgetHelper::getScreenRects()
{
QList<QRect> rects;
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
int screenCount = qApp->screens().count();
QList<QScreen *> screens = qApp->screens();
for (int i = 0; i < screenCount; ++i) {
QScreen *screen = screens.at(i);
rects << screen->geometry();
}
#else
int screenCount = qApp->desktop()->screenCount();
QDesktopWidget *desk = qApp->desktop();
for (int i = 0; i < screenCount; ++i) {
rects << desk->screenGeometry(i);
}
#endif
return rects;
}
QRect WidgetHelper::getScreenRect(int screenIndex)
{
//指定了屏幕索引則取指定的(沒有指定則取當前鼠標所在屏幕)
QList<QRect> rects = WidgetHelper::getScreenRects();
if (screenIndex >= 0 && screenIndex < rects.count()) {
return rects.at(screenIndex);
} else {
//當前屏幕區域包含當前鼠標所在座標則說明是當前屏幕
QPoint pos = QCursor::pos();
foreach (QRect rect, rects) {
if (rect.contains(pos)) {
return rect;
}
}
}
}
QString WidgetHelper::getResolution(int width, int height)
{
//取偶數(虛擬機中很可能是奇數的分辨率)
if (width % 2 != 0) {
width--;
}
if (height % 2 != 0) {
height--;
}
return QString("%1x%2").arg(width).arg(height);
}
QString WidgetHelper::getResolution(const QString &resolution)
{
QStringList sizes = WidgetHelper::getSizes(resolution);
return getResolution(sizes.at(0).toInt(), sizes.at(1).toInt());
}
void WidgetHelper::checkRect(int screenIndex, QString &resolution, int &offsetX, int &offsetY)
{
QRect rect = WidgetHelper::getScreenRect(screenIndex);
if (resolution == "0x0") {
resolution = WidgetHelper::getResolution(rect.width(), rect.height());
} else {
resolution = WidgetHelper::getResolution(resolution);
}
//偏移值必須小於分辨率否則重置
if (offsetX > rect.width()) {
offsetX = 0;
}
if (offsetY > rect.height()) {
offsetY = 0;
}
//判斷設定的偏移值加上設定的分辨率是否超出了真實的分辨率
QStringList sizes = WidgetHelper::getSizes(resolution);
int width = sizes.at(0).toInt();
int height = sizes.at(1).toInt();
if (offsetX + width > rect.width()) {
width = rect.width() - offsetX;
}
if (offsetY + height > rect.height()) {
height = rect.height() - offsetY;
}
//如果超出了分辨率則重新設置採集的分辨率
resolution = WidgetHelper::getResolution(width, height);
//多個屏幕需要加上屏幕起始座標
if (offsetX == 0) {
offsetX = rect.x();
} else {
offsetX += rect.x();
}
if (offsetY == 0) {
offsetY = rect.y();
} else {
offsetY += rect.y();
}
//qDebug() << TIMEMS << screenIndex << offsetX << offsetY << resolution;
}