網(wǎng)頁中的音視頻裁剪&拼接&合并

一、需求描述

????????項目中有一個配音需求:

????????1)首先,前臺會拿到一個英語視頻,視頻的內容是A和B用英語交流;

????????2)然后,用戶可以選擇為某一個角色配音,假如選擇為A配音,那么視頻在播放到A的位置時會靜音,并錄制用戶的聲音。以此類推,直到視頻播放結束;

????????3)最后,將用戶的錄音替換到視頻中,并生成新的視頻文件,后續(xù)上傳服務器。

????????另外,已知每個角色說話的起始時間和結束時間(這個由后臺管理來配置)。

1

二、需求分析

? ??????2.1 實現(xiàn)方式

????????實現(xiàn)該功能的方式大體有兩個:

????????1)使用ffmpeg.wasm

????????2)使用Web Audio API等原生JS

????????第二種方式我沒實踐,但理論上應該可以實現(xiàn),只是估計會較復雜,代碼較多;此處,我選擇方式一。

? ??????2.2 功能拆分

????????根據(jù)該功能的操作流程,可將其拆分為:

????????1)音視頻分離,獲得純音頻文件和純視頻文件

????????2)音頻剪切,從上一步得到的音頻文件中裁剪到除配音角色外的其它音頻片段

????????3)錄音,獲取到配音音頻片段

????????4)音頻拼接,將上面兩步得到的音頻片段按順序拼接成一個

????????5)音視頻合并,將純視頻文件和上一步得到的音頻文件合并為一個文件

2

三、代碼實現(xiàn)

? ??????3.1 引入依賴庫

// 注:文末附件會提供,或自行從網(wǎng)上獲取

<script src="/js/ffmpeg/umd/ffmpeg.js"></script>

<script src="/js/util/umd/index.js"></script>

? ??????3.2 初始化ffmpeg

const { fetchFile } = FFmpegUtil;

const { FFmpeg } = FFmpegWASM;

let ffmpeg = new FFmpeg();

await ffmpeg.load({coreURL: "/js/core/umd/ffmpeg-core.js",});

? ??????3.3 音視頻分離

// 在Demo中,視頻文件通過input[type=file]標簽獲得

const { name, size } = files[0];

await ffmpeg.writeFile(name, await fetchFile(files[0]));

// 音視頻分離

await ffmpeg.exec(['-i', name, '-c:v', 'copy', '-an', 'output.mp4'])

await ffmpeg.exec(['-i', name, '-vn', '-acodec', 'libmp3lame', 'output.mp3'])

????????在上面代碼中-acodec標識了使用mp3音頻編碼器,如果使用copy原音頻的編碼方式,在網(wǎng)頁中可能會報錯“Invalid audio stream. Exactly one MP3 audio stream is required”

await ffmpeg.exec(['-i', name, '-vn', '-acodec', 'copy', 'output.mp3']); // 會報錯

? ??????3.4音頻剪切

// -ss 起始時間,-t 持續(xù)時間

await ffmpeg.exec(['-i', 'output.mp3', '-ss', '00:00:00.000', '-t', '00:00:10.000', 'split_0.mp3'])

await ffmpeg.exec(['-i', 'output.mp3', '-ss', '00:00:20.000', '-t', '00:00:10.000', 'split_2.mp3'])

? ??????3.5 配音錄制

const record = (duration, callback) => {

? if (!duration) return;

? // 變量及函數(shù)聲明

? recorder = [];

? recordTimer = null;

? let _isStop = false;

? async function startRecording () {

? ? const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

? ? mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });

? ? mediaRecorder.ondataavailable = handleDataAvailable;

? ? mediaRecorder.start();

? }

? function handleDataAvailable(event) {

? ? if (recorder) { recorder.push(event.data); }

? ? if (_isStop) {callback && callback();}

? }

? function stopRecording() {

? ? mediaRecorder.stop();

? ? _isStop = true;

? }

? // 調用

? startRecording();

? recordTimer = setTimeout(() => {

? ? stopRecording();

? }, duration);

}

????????在上面這段代碼中,需要注意的是:錄音結束后的回調函數(shù)是放在handleDataAvailable中的,這是因為當mediaRecorder.stop()停止錄制后,會再出發(fā)一次dataavailable事件,然后才把最后的數(shù)據(jù)分片存儲到recorder中。所以代碼中定義了一個_isStop變量來輔助完成這個過程。

// 將配音數(shù)據(jù)保存到文件

let split_1 = await audioChunks2Unit8Array(recorder);

await ffmpeg.writeFile('split_1.mp3', split_1);

????????在上面這段代碼中,之前獲得的錄音數(shù)據(jù)是個Blob數(shù)組,ffmpeg不支持直接對其進行操作,所以要將它轉換為Unit8Array才能寫到文件。

? ??????3.6音頻拼接

await ffmpeg.exec(['-i', 'split_0.mp3', '-i', 'split_1.mp3', '-i', 'split_2.mp3', '-filter_complex', '[0:a][1:a][2:a]concat=n=3:v=0:a=1', '-ac', '2', '-c:a', 'libmp3lame', '-q:a', '4', 'merge.mp3'])

參數(shù)解釋:

????[0:a][1:a][2:a]concat=n=3: 將第一段素材的音頻、a1和a2合并,n=3表示三段。

????v=0:a=1: 不要聲音,只要音頻。

????-ac:設定聲音的channel數(shù)

????-c:a:指定音頻編碼器

????libmp3lame:mp3音頻編碼器

????-q:a:表示輸出的音頻質量,一般是1到5之間(1為質量最高)

? ??????3.7音視頻合并

await ffmpeg.exec(['-i', 'output.mp4', '-i', 'merge.mp3', '-c:v', 'copy', '-c:a', 'copy', 'result.mp4'])

參數(shù)解釋:

????-c:v copy:視頻編碼不變。

????-c:a copy :音頻編碼不變。

????????最后得到合并后的視頻數(shù)據(jù)(Unit8Array)。

四、附件

????????之前在網(wǎng)上查找ffmpeg.wasm資源時,很多都殘缺不全,所以把相關的依賴庫放在網(wǎng)盤了(文件來自官方github倉庫,其中的示例頁面我稍微美化了一下樣式)。

????????download.csdn.net/download/xueshen1106/88772981

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容