用Electron實(shí)現(xiàn)支持動態(tài)下發(fā)視頻資源的類OBS推流解決方案


title: 用Electron實(shí)現(xiàn)支持動態(tài)下發(fā)視頻資源的類OBS推流解決方案
tags:

  • 個人成長
    categories:
  • 雜談

最近在研發(fā)數(shù)字人相關(guān)的項(xiàng)目准潭,基于Electron實(shí)現(xiàn)了類似OBS的推流效果,很好玩振惰,也有一些技術(shù)難點(diǎn),本文分享一波贮勃。

我查閱了很多博客魔眨,雖然有一些ffmpeg推流的方案,但都是淺嘗輒止底扳。

本文的方案铸抑,可以在Web層實(shí)時查看ffmpeg推流的效果,并可以借助fabric.js實(shí)時編輯推流畫面衷模,實(shí)現(xiàn)復(fù)雜的展示效果鹊汛,也支持服務(wù)端動態(tài)下發(fā)視頻資源蒲赂,在前端進(jìn)行定制化實(shí)時推流合成,極大減輕服務(wù)端的負(fù)擔(dān)刁憋。

首先是采樣視頻流的方案

軟件的Node.js層通過CDN獲取實(shí)時生成的視頻片段滥嘴,將視頻緩存到用戶本計(jì)算機(jī),將視頻路徑轉(zhuǎn)換為file:// 開頭的路徑至耻,Web層可以通過<Video />標(biāo)簽直接播放視頻若皱,借助fabric.js可以將<Video />輕易繪制到canvas畫板上,然后我們對畫板進(jìn)行采樣尘颓,獲取文檔的視頻流.

以下代碼走触,可以對canvas進(jìn)行每秒20幀的采樣,獲取的視頻流疤苹,存儲在videoTrack

const canvasStream = canvas.captureStream(20);
const videoTrack = canvasStream.getVideoTracks()[0];

我們還可以借助fabric.js在canvas繪制gif動圖互广,各類自定義字體,各種類型圖片卧土,為視頻添加豐富的多媒體元素兜辞,進(jìn)一步豐富視頻流的效果。

然后是采樣音頻流的方案

我們可以通過<Video />標(biāo)簽夸溶,對正在播放的視頻進(jìn)行采樣逸吵,生成穩(wěn)定的音頻流,由于我們要不斷切換視頻缝裁,直接采集<Video />的音頻流扫皱,會遇到音頻流中斷,音視頻流合并出錯的情況捷绑。

我們需要使用 createGain() 生成一個gainNode管道進(jìn)行匯流 (在視頻采樣中韩脑,其實(shí)Canvas本身也是承擔(dān)了類似gainNode匯流的功能),這個gainNode管道可以隨時接受<Video /> 音頻流的匯入和斷開粹污。

為了后續(xù)為gainNode擴(kuò)展更多的功能(比如調(diào)節(jié)音量段多,加入背景混音),我們不直接使用gainNode作為最終音軌, 我們可以另外創(chuàng)建一個finalAudioTrack作為最終音軌壮吩,將gainNode匯入finalAudioTrack即可

const audioContext = new AudioContext();
const gainNode = audioContext.createGain();
let audioSource = null;
const handleVideoChange = (video) => {
    // 斷開前一次的連接
    if (audioSource) {
        audioSource.disconnect(gainNode);
    }
    // 獲取 video 的音頻軌道并連接到目標(biāo)節(jié)點(diǎn)
    const audioTrack = video.captureStream().getAudioTracks()[0];
    // 創(chuàng)建新的 MediaStreamAudioSourceNode 對象
    audioSource = audioContext.createMediaStreamSource(new MediaStream([audioTrack]));
    
    // 連接到增益節(jié)點(diǎn)
    audioSource.connect(gainNode);
};

const destination = audioContext.createMediaStreamDestination();
const finalAudioTrack = destination.stream.getAudioTracks()[0];
//<video id="videoA" controls>
//        <source src="path/to/your/video.mp4" type="video/mp4">
//        Your browser does not support the video tag.
//</video> 
// 獲取頁面上id為videoA 的視頻元素 
const videoA = document.getElementById('videoA');
// 在videoA切換src后, 調(diào)用以下函數(shù)
handleVideoChange(videoA);

將視頻軌道和音頻軌道合成并推流給Electron Node.js層的ffmpeg

使用MediaStream對音視頻流進(jìn)行合并后进苍,我們可以獲取到包含實(shí)時音視頻流信息的combinedStream

const combinedStream = new MediaStream([
    videoTrack,
    finalAudioTrack,
]);

對combinedStream啟動錄制,并推流給Electron 的 Node.js層

// 創(chuàng)建一個 MediaRecorder 實(shí)例鸭叙,設(shè)置 timeslice 參數(shù)為 1000ms
const mediaRecorder = new MediaRecorder(combinedStream, { timeslice: 1000 });

// 當(dāng)有數(shù)據(jù)可用時觸發(fā)該事件
mediaRecorder.ondataavailable = function (e) {
    if (e.data.size > 0) {
        const reader = new FileReader();
        // 當(dāng)文件讀取完成后觸發(fā)該事件
        reader.onloadend = () => {
            const arrayBuffer = reader.result;
            try {
                // 如果 window.electron 存在觉啊,則發(fā)送幀數(shù)據(jù)
                if (window.electron) {
                    console.log("==sendFrame==", arrayBuffer.byteLength);
                    window.electron.sendFrame(arrayBuffer);
                }
            } catch (error) {
                console.error("==sendFrame error==", error);
            }
        };
        
        // 讀取 Blob 數(shù)據(jù)并將其轉(zhuǎn)換為 ArrayBuffer
        reader.readAsArrayBuffer(e.data);
    }
};

// 開始錄制,每 1000ms 收集一次數(shù)據(jù)
mediaRecorder.start(1000);

Electron 的Node.js層接收數(shù)據(jù)流沈贝,并發(fā)送給ffmpeg進(jìn)行推流

以下是精簡后的代碼杠人,我們使用new PassThrough()創(chuàng)建了一個管道,管道入口接收數(shù)據(jù),出口將數(shù)據(jù)輸出給ffmpeg

const pathToFfmpeg = require("ffmpeg-static");
const videoStreams = new PassThrough();
const serverUrl = "rtmp://********"
    // 構(gòu)建 ffmpeg輸出文件命令
const ffmpegArgs = [
    "-fflags",
    "+genpts",
    "-re",
    "-r",
    "20", // 設(shè)置輸入幀率
    "-i",
    "pipe:0", // 從標(biāo)準(zhǔn)輸入讀取視頻數(shù)據(jù)
    "-c:v",
    "libx264", // 使用軟件編碼器
    "-preset",
    "ultrafast", // 使用更高效的預(yù)設(shè)
    "-tune",
    "zerolatency", // 低延遲調(diào)優(yōu)
    "-maxrate",
    "2500k", // 降低視頻碼率
    "-bufsize",
    "1000k", // 調(diào)整緩沖區(qū)大小
    "-pix_fmt",
    "yuv420p",
    "-g",
    "20", // 調(diào)整 GOP 大小
    "-c:a",
    "aac", // 音頻編碼器
    "-b:a",
    "128k", // 音頻碼率
    "-ac",
    "2",
    "-ar",
    "48000", // 音頻采樣率
    "-f",
    "flv", // 輸出格式
    serverUrl, // 輸出 URL
];
const ffmpegCommands = spawn(pathToFfmpeg, ffmpegArgs)
function sendFrame(sendFrame){
    videoStreams.write(Buffer.from(arrayBuffer));
    videoStreams.pipe(ffmpegCommands.stdin);
}

完成以上設(shè)置后嗡善,仿OBS的前端Canvas和ffmpge推流的架構(gòu)辑莫,就可以穩(wěn)定運(yùn)行了。

小結(jié)

ffmpeg作為老牌的經(jīng)典開源軟件罩引,功能強(qiáng)大各吨,在實(shí)現(xiàn)ffmpeg推流的過程中,我也遇到了很多問題蜒程,比如視頻流或音頻流只要有一個斷流绅你,ffmpeg就會立刻停止推流伺帘,而且無法支持動態(tài)添加視頻文件昭躺,這些工程上的問題,都要通過外層的邏輯封裝去解決伪嫁。

前端技術(shù)日新月異领炫,其實(shí)本文提到的技術(shù)方案,配合 https://github.com/ffmpegwasm/ffmpeg.wasm 在純Web端也能實(shí)現(xiàn)大部分张咳,希望瀏覽器開放更多的能力帝洪,在Web端能運(yùn)行更多擁有強(qiáng)大功能的開源軟件,用戶打開網(wǎng)頁脚猾,就能使用計(jì)算機(jī)的一切功能葱峡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市龙助,隨后出現(xiàn)的幾起案子砰奕,更是在濱河造成了極大的恐慌,老刑警劉巖提鸟,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件军援,死亡現(xiàn)場離奇詭異,居然都是意外死亡称勋,警方通過查閱死者的電腦和手機(jī)胸哥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赡鲜,“玉大人空厌,你說我怎么就攤上這事∫辏” “怎么了蝇庭?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捡硅。 經(jīng)常有香客問我哮内,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任北发,我火速辦了婚禮纹因,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘琳拨。我一直安慰自己瞭恰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布狱庇。 她就那樣靜靜地躺著惊畏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪密任。 梳的紋絲不亂的頭發(fā)上颜启,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音浪讳,去河邊找鬼缰盏。 笑死,一個胖子當(dāng)著我的面吹牛淹遵,可吹牛的內(nèi)容都是我干的口猜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼透揣,長吁一口氣:“原來是場噩夢啊……” “哼济炎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辐真,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤须尚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拆祈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恨闪,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年放坏,在試婚紗的時候發(fā)現(xiàn)自己被綠了咙咽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡淤年,死狀恐怖钧敞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情麸粮,我是刑警寧澤溉苛,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站弄诲,受9級特大地震影響愚战,放射性物質(zhì)發(fā)生泄漏娇唯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一寂玲、第九天 我趴在偏房一處隱蔽的房頂上張望塔插。 院中可真熱鬧,春花似錦拓哟、人聲如沸想许。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽流纹。三九已至,卻和暖如春违诗,著一層夾襖步出監(jiān)牢的瞬間漱凝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工较雕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碉哑,地道東北人挚币。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓亮蒋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親妆毕。 傳聞我的和親對象是個殘疾皇子慎玖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容