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ī)的一切功能葱峡。