FFmpeg.js实现视频音频合并输出MP4需求 vue2和vue3都可以使用亲测有效

2,504 阅读4分钟

声明:本文只是对原文作者:前端杂货铺功能补充~~
关于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、版本

Snipaste_2025-04-21_14-11-55.png

更新----------------------------------------------------------------------------------------

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依赖
image.png
复制 ffmpeg-core.js、 ffmpeg-core.wasm、ffmpeg-core.worker.js 文件,放入静态资源文件夹中【说明:Vue项目创建完之后默认生成 public 文件夹(用来存放静态资源的),所以我们需要把它们三个复制一份放入 public 文件夹中(若静态资源文件夹名称改了,就放入改到的地方即可)】

image.png

原文链接: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、如果音频比视频长,视频会显示最后一帧静止图像,直到音频播放完毕。

这个功能你可以按照自己项目需求来做取舍
功能按照需求走