likes
comments
collection
share

前端vue项目使用ffmpeg处理视频前言 最近的一项需求要求前端支持视频压缩,并能够播放 .avi 格式的视频。因为浏

作者站长头像
站长
· 阅读数 90

前言

最近的一项需求要求前端支持视频压缩,并能够播放 .avi 格式的视频。因为浏览器本身并不支持 .avi 格式的视频播放,所以在上传之前需要将其转换为 .mp4 格式。本文将介绍如何实现视频的压缩和转码功能。

项目相关依赖和配置

  • vite: 3.0.1
  • @ffmpeg/ffmpeg: 0.12.7
  • @ffmpeg/util: 0.12.1
  • @ffmpeg/core: 0.12.6

Vite配置

export default defineConfig({
 ...
  optimizeDeps: { exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'] },
 ...
})

FFmpeg 资源文件处理

复制node_modules下的@ffmpeg/core esm目录到项目外层的public下, esm目录包含 ffmpeg-core.jsffmpeg-core.wasm两个文件。public 目录下的文件不会被构建工具处理,这些文件将保持原样被复制到构建输出目录。

使用 Pinia 维护 FFmpeg 实例

由于ffmpeg资源包很大(约30M),多个组件第一次使用时,或者组件卸载后重新打开,都需要重新加载资源,因此为了节省时间,我们使用 Pinia 来维护一个全局唯一的 ffmpeg 实例。有个特殊情况是,同时有多个页面都用到这功能, 当前页的视频还在处理中,未完成就切换路由到其它页面需要调用ffmpeg.terminate()终止掉之前的, 而终止后是需要重新调FFmpeg.load()加载资源的。

API: ffmpegwasm.netlify.app/docs/api/ff…

FFmpeg Store 定义

//store/ffmpeg.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';

export const useFFmpegStore = defineStore('ffmpeg', () => {
  const ffmpeg = new FFmpeg();
  const ffmpegLoaded = ref(false);
  const processStatus = ref('idle');
  const baseURL = import.meta.env.BASE_URL;
  const coreURL = `${baseURL}esm/ffmpeg-core.js`;
  const wasmURL = `${baseURL}esm/ffmpeg-core.wasm`;
  const videoUrl = ref('');
  const progressPercent = ref(0);

  // 默认命令行参数
  const defaultCommands = [
    '-i', 'input.mp4',
    '-c:v', 'libx264',
    '-b:v', '1000k',
    '-crf', '32',
    '-preset', 'ultrafast',
    '-c:a', 'aac',
    '-ar', '44100',
    '-ab', '128k',
    'output.mp4'
  ];

  let _loadPromise = null;

  // 加载 FFmpeg
  const loadFfmpeg = async () => {
    if (!ffmpegLoaded.value && !_loadPromise) {
      _loadPromise = ffmpeg.load({
        coreURL: `${window.location.origin}${coreURL}`,
        wasmURL: `${window.location.origin}${wasmURL}`
      }).then(() => {
        ffmpegLoaded.value = true;
      });
    }
    await _loadPromise;
  };

  // 监听进度
  ffmpeg.on('progress', ({ progress }) => {
    progressPercent.value = Math.round(progress * 100);
  });

  // 处理视频
  const processVideo = async (file, config = {}) => {
    try {
      processStatus.value = 'processing';
      await loadFfmpeg();

      const commands = config.commands && config.commands.length ? config.commands : defaultCommands;
      const inputFileName = commands[1];
      const outputFileName = commands[commands.length - 1];

      if (file.type.includes('avi') && !/\.avi$/.test(inputFileName)) {
        commands[1] = 'input.avi';
      }

      await ffmpeg.writeFile(commands[1], await fetchFile(file));
      await ffmpeg.exec(commands);
      const data = await ffmpeg.readFile(outputFileName);
      const processedFile = new File([data], file.name.replace(/\.avi$/, '.mp4'), { type: 'video/mp4' });

      // blob:http:// 开头的视频地址
      videoUrl.value = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));

      processStatus.value = 'done';
      return processedFile;
    } catch (error) {
      console.error(error);
      processStatus.value = 'error';
    }
  };

  // 重置状态
  const resetState = () => {
    if (ffmpeg.loaded && processStatus.value === 'processing') {
      ffmpeg.terminate();
      _loadPromise = null;
      ffmpegLoaded.value = false;
    }
    processStatus.value = 'idle';
    videoUrl.value = '';
    progressPercent.value = 0;
  };

  return {
    ffmpeg,
    ffmpegLoaded,
    videoUrl,
    processStatus,
    progressPercent,
    loadFfmpeg,
    processVideo,
    resetState
  };
});

useFFmpeg hook简易封装

// hooks/useFFmpeg.js
import { onUnmounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useFFmpegStore } from '@/store/ffmpeg'


export default function useFFmpeg() {
  const ffmpegStore = useFFmpegStore()
  const { processVideo, resetState } = ffmpegStore
  // 使用 storeToRefs 确保解构出来的状态变量是响应式的
  const { ffmpeg, ffmpegLoaded, videoUrl, processStatus, progressPercent } =
    storeToRefs(ffmpegStore)

  onUnmounted(() => {
    resetState()
  })

  return {
    ffmpeg,
    ffmpegLoaded,
    videoUrl,
    processStatus,
    progressPercent,
    processVideo,
    resetState
  }
}

在主入口文件预加载 FFmpeg

由于资源包比较大,所以一开始预加载资源,方便后续操作。如果在使用压缩功能的时候,资源包还没加载完,可以通过 ffmpegLoaded状态提示“正在加载视频转码或压缩所需的资源文件...”,

//main.js
import { useFFmpegStore } from '@/store/ffmpeg'

const ffmpegStore = useFFmpegStore()
ffmpegStore
  .loadFfmpeg()
  .then(() => {
    console.log('FFmpeg loaded successfully')
  })
  .catch((error) => {
    console.error('Failed to load FFmpeg:', error)
  })

组件中的使用示例

这里使用 ant-design-vue 的 upload 组件作为例子。我们可以在上传前通过 beforeUpload 钩子处理视频,处理完成后自动上传。对于小于100MB的 .mp4 文件,直接上传;对于 .avi 文件或大于100MB的 .mp4 文件,则进行压缩和格式转换。目前只支持处理单个视频,因为只有一个ffmpeg实例。

import useFFmpeg from '@/hooks/useFFmpeg';

const { ffmpegLoaded, processStatus, progressPercent, processVideo } = useFFmpeg();

const handleBeforeUpload = async (file) => {
  const sizeLimit = 100 * 1024 * 1024; // 100 MB
  if ((file.size > sizeLimit && file.type.includes('mp4')) || file.type.includes('avi')) {
    return await processVideo(file);
  }
  return file;
};

遇到的问题及解决方法

在实际开发过程中遇到了一些问题,例如 ffmpeg.load 方法会阻塞后续代码的执行且无法捕获错误。通过将 Vite 版本从 2.9.9 升级到 3.0.1 解决了这个问题。

另外,在线上环境中,ffmpeg-core.wasm 文件的请求类型变成了普通的 fetch 类型,而不是预期的 wasm 类型。这是因为 Nginx 默认没有正确识别 .wasm 文件的 MIME 类型。解决方法是在 Nginx 配置中添加对 .wasm 文件的支持:

server {
    listen       80;
    server_name  localhost;

    # 新增的 location 块,专门处理所有 .wasm 文件
    location ~* /myproject/esm/.*\.wasm$ {
        alias /usr/share/nginx/html/myproject/esm;
        types {
            application/wasm wasm;
        }
        default_type application/wasm;
    }
}
转载自:https://juejin.cn/post/7423704609178288166
评论
请登录