声明:本文只是对原文作者:前端杂货铺功能补充~~
关于FFmpeg.js使用方面或其他需求问题请移步--->前端杂货铺
更新----- 2025年4月21日 回应下评论区不能用的问题:
1、先看录屏吧 午休写了个demo
2、demo源码
<template>
<div class="login">
<h1>视频和音频混合</h1>
<input type="file" @change="onVideoSelected" accept="video/*" />
<input type="file" @change="onAudioSelected" accept="audio/*" />
<button @click="mergeFiles">混合并下载</button>
</div>
</template>
<script>
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
import axios from 'axios'
export default {
name: "Login",
data() {
return {
ffmpeg: null,
videoFile: null,
audioFile: null,
// createFFmpeg: ()=>{},
// fetchFile: ()=>{}
};
},
created() {
},
mounted() {
this.ffmpeg = createFFmpeg({corePath: "./ffmpeg-core.js", log: true });
this.loadFFmpeg();
// axios.get('http://192.168.5.119:9000/dmbucket/dm/video/3c1a6e6a-4c1e-47c6-bfb7-b8af3a65ffb8.mp3',{ responseType: "blob" }).then((res)=>{
// const formartData = new File([res.data], 'test.mp3', { type: 'audio/mp3' });
// // const resElement = new Audio(URL.createObjectURL(formartData));
// console.log(formartData);
// // resElement.play(); // 检查音频是否可以播放
// }).catch(e=>{
// console.error(e);
// })
// console.log('qqqq',this.ffmpeg,createFFmpeg, fetchFile);
},
methods: {
async loadFFmpeg() {
await this.ffmpeg.load();
},
onVideoSelected(event) {
this.videoFile = event.target.files[0];
},
onAudioSelected(event) {
this.audioFile = event.target.files[0];
},
async mergeFiles() {
if (!this.videoFile || !this.audioFile) {
alert('请先选择视频和音频文件');
return;
}
this.ffmpeg.FS('writeFile', 'input_video.mp4', await fetchFile(this.videoFile));
this.ffmpeg.FS('writeFile', 'input_audio.mp3', await fetchFile(this.audioFile));
try{
// await this.ffmpeg.run('-i', 'input_video.mp4', '-i', 'input_audio.mp3', '-c:v', 'copy', '-c:a', 'aac', 'output.mp4');
await this.ffmpeg.run(
'-i', 'input_video.mp4',
'-i', 'input_audio.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v',
'-map', '1:a',
'-shortest',
'output.mp4'
);
// 查看文件系统中是否存在 output.mp4
console.log(this.ffmpeg.FS('readdir', '/'));
// debugger
// const audio = this.ffmpeg.FS('readFile', 'input_audio.mp3');
// const audioBlob = new Blob([audio.buffer], { type: 'audio/mp3' });
// const audioUrl = URL.createObjectURL(audioBlob);
// const audioElement = new Audio(audioUrl);
// audioElement.play(); // 检查音频是否可以播放
const data = this.ffmpeg.FS('readFile', 'output.mp4');
const outputBlob = new Blob([data.buffer], { type: 'video/mp4' });
const link = document.createElement('a');
link.href = URL.createObjectURL(outputBlob);
link.download = 'merged_output.mp4';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}catch(e){
console.error(e);
}
},
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
</style>
3、版本
更新----------------------------------------------------------------------------------------
1、关于ffmpeg.js版本问题
无论vue3或者vue2版本,使用过最新版 0.11.* 以上都会出现各种问题,webpack解析问题、vite配置问题等等...
最好的办法使用声明中作者的版本(vue2和vue3都可以使用亲测有效):
"@ffmpeg/core": "^0.10.0",
"@ffmpeg/ffmpeg": "^0.9.8",
2、config配置问题
webpack版本:
module.exports = {
*****其他配置
devServer: {
****其他配置
headers: {
"Cross-Origin-Opener-Policy": "same-origin", // 保护你的源站点免受攻击
"Cross-Origin-Embedder-Policy": "require-corp", // 保护受害者免受你的源站点的影响
},
}
vite版本:
export default defineConfig({
***其他配置
server: {
****其他配置
headers: {
"Cross-Origin-Opener-Policy": "same-origin", // 保护你的源站点免受攻击
"Cross-Origin-Embedder-Policy": "require-corp", // 保护受害者免受你的源站点的影响
},
},
})
3、ffmpeg.js使用
找到node_modules下的ffmpeg依赖
复制 ffmpeg-core.js、 ffmpeg-core.wasm、ffmpeg-core.worker.js 文件,放入静态资源文件夹中【说明:Vue项目创建完之后默认生成 public 文件夹(用来存放静态资源的),所以我们需要把它们三个复制一份放入 public 文件夹中(若静态资源文件夹名称改了,就放入改到的地方即可)】
原文链接:blog.csdn.net/qq_45902692…
3.1 vue中使用
PS:只做vue3中使用代码(vue2版本相信你会写)
<template>
<div class="app">
<input type="file" @change="handleVideoUpload" accept="video/mp4" />
<input type="file" @change="handleAudioUpload" accept="audio/mp3" />
<button @click="addAudioTrack">Add Audio Track</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = ref(null);
const videoFile = ref(null);
const audioFile = ref(null);
const handleVideoUpload = (event) => {
videoFile.value = event.target.files[0];
};
const handleAudioUpload = (event) => {
audioFile.value = event.target.files[0];
};
const addAudioTrack = async () => {
if (!videoFile.value || !audioFile.value) {
alert('Please select both video and audio files.');
return;
}
ffmpeg.value.FS('writeFile', 'input_video.mp4', await fetchFile(videoFile.value));
ffmpeg.value.FS('writeFile', 'input_audio.mp3', await fetchFile(audioFile.value));
await ffmpeg.value.run(
'-i', 'input_video.mp4',
'-i', 'input_audio.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v',
'-map', '1:a',
// '-shortest',
'output.mp4'
);
const data = ffmpeg.value.FS('readFile', 'output.mp4');
// 查看文件系统中是否存在 output.mp4
console.log(ffmpeg.value.FS('readdir', '/'));
// debugger
// const audio = this.ffmpeg.FS('readFile', 'input_audio.mp3');
// const audioBlob = new Blob([audio.buffer], { type: 'audio/mp3' });
// const audioUrl = URL.createObjectURL(audioBlob);
// const audioElement = new Audio(audioUrl);
// audioElement.play(); // 检查音频是否可以播放
const outputBlob = new Blob([data.buffer], { type: 'video/mp4' });
const link = document.createElement('a');
link.href = URL.createObjectURL(outputBlob);
link.download = 'merged_output.mp4';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function reload(){
await ffmpeg.value.load();
}
onMounted(() => {
ffmpeg.value = createFFmpeg({ corePath: "./ffmpeg-core.js", log: true });
reload()
})
</script>
<style scoped>
</style>
4、踩坑记录
const addAudioTrack = async () => {
if (!videoFile.value || !audioFile.value) {
alert('Please select both video and audio files.');
return;
}
ffmpeg.value.FS('writeFile', 'input_video.mp4', await fetchFile(videoFile.value));
ffmpeg.value.FS('writeFile', 'input_audio.mp3', await fetchFile(audioFile.value));
await ffmpeg.value.run(
'-i', 'input_video.mp4',
'-i', 'input_audio.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-strict', 'experimental', // 问题出在这
'-shortest',
'output.mp4'
);
const data = ffmpeg.value.FS('readFile', 'output.mp4');
// 查看文件系统中是否存在 output.mp4
console.log(ffmpeg.value.FS('readdir', '/'));
// debugger
// const audio = this.ffmpeg.FS('readFile', 'input_audio.mp3');
// const audioBlob = new Blob([audio.buffer], { type: 'audio/mp3' });
// const audioUrl = URL.createObjectURL(audioBlob);
// const audioElement = new Audio(audioUrl);
// audioElement.play(); // 检查音频是否可以播放
const outputBlob = new Blob([data.buffer], { type: 'video/mp4' });
const link = document.createElement('a');
link.href = URL.createObjectURL(outputBlob);
link.download = 'merged_output.mp4';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
视频音频合并输出成功,但是输出MP4文件没有正确添加音轨
~~~裂大开~~~
查看ffmpeg.js文档 各种debugger验证问题,尝试到切换编解码器,输出成功添加音轨
~~~成功解决~~~
-strict experimental 选项,是实验性的编解码器,切换到稳定的编解码器'-map', '0:v',
'-map', '1:a',
其中 -map 选项显式地指定使用第一个输入 (0) 的视频轨道和第二个输入 (1) 的音频轨道
await ffmpeg.value.run(
'-i', 'input_video.mp4',
'-i', 'input_audio.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v',
'-map', '1:a',
// '-shortest',
'output.mp4'
);
遗留taps: 因为合并输出的MP4可以通过对比视频、音频两个文件的时间长度来进行截取裁剪,
await ffmpeg.value.run(
'-i', 'input_video.mp4',
'-i', 'input_audio.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v',
'-map', '1:a',
// '-shortest', //属性含义:使输出视频长度与最短的输入(视频或音频)匹配
'output.mp4'
);
如果不配置 '-shortest' 属性,则会按照规则:
1、如果视频和音频流长度不同,FFmpeg 会让输出文件的长度与最长的输入流匹配。
2、这意味着,如果视频比音频长,音频将会静音以匹配视频的长度。
3、如果音频比视频长,视频会显示最后一帧静止图像,直到音频播放完毕。
这个功能你可以按照自己项目需求来做取舍
功能按照需求走