纯前端FFmpeg.js实现长视频切割,音频提取,关键帧提取

背景:

最近遇到一个需求:需要对长视频的音频做提取,最后对音频识别出文字,算法和后端都可以对视频进行视频切割和提取,那我就想前端能不能实现呢,就开始调研方案。

后端和算法使用的都是FFmpeg这个库实现视频的操作,其实咱们前端也有FFmpeg.js,能对视频进行操作。

一、FFmpeg.wasm 简介

先讲FFmpeg,FFmpeg 是一个开源的音视频处理工具,支持录制、转换和流式传输音视频。它提供了丰富的功能,如格式转换、视频剪辑、音频提取等。

FFmpeg官网链接;其本身是一个开源的客户端工具,同时也提供了多种语言的api支持。

FFmpeg.wasm是c语言编写的程序,通过编译后可以在web平台上使用(浏览器),

原理:WebAssembly(简称wasm)是一个虚拟指令集体系架构(virtual ISA),整体架构包括核心的ISA定义、二进制编码、程序语义的定义与执行,以及面向不同的嵌入环境(如Web)的应用编程接口(WebAssembly API)。其目标是为 C/C++等语言编写的程序经过编译,在确保安全和接近原生应用的运行速度更好地在 Web 平台上运行。

简单来说:WebAssembly 是一种高效的二进制格式,能够在现代浏览器中快速执行。

FFmpegWasm:github链接

运行原理:

  • 加载和初始化 : 当用户在浏览器中加载 ffmpeg.js 时,浏览器会下载编译后的 WebAssembly 模块和相关的 JavaScript 代码。

  • 调用 FFmpeg 功能 : 用户可以通过 JavaScript 调用 FFmpeg 的命令和功能,传递参数和数据。ffmpeg.js 会将这些调用转换为相应的 WebAssembly 函数调用。

  • 处理音视频: FFmpeg 在浏览器中执行音视频处理任务,使用虚拟文件系统读取输入文件,进行处理,然后将结果输出到虚拟文件系统中。

  • 下载结果: 处理完成后,用户可以通过 JavaScript 下载处理后的文件,所有操作都在客户端完成,无需与服务器交互。

二、常用FFmpeg方法

ffmpeg.js 提供了一系列方法来处理音视频文件。以下是一些常用的方法和功能:

1. 基本方法

  • FFmpeg.load():

    • 加载 FFmpeg 的核心库。这个方法需要在使用其他 FFmpeg 功能之前调用。
  • FFmpeg.run(...args):

    • 执行 FFmpeg 命令。参数是一个字符串数组,表示要执行的命令及其参数。例如:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', 'output.avi');

2. 文件操作

  • FFmpeg.FS(command, filename, data):

    • 在虚拟文件系统中执行文件操作。常用的命令包括:
      • writeFile: 将数据写入虚拟文件系统。
      • readFile: 从虚拟文件系统读取文件。
      • unlink: 删除虚拟文件系统中的文件。
    javascript 复制代码
    // 写入文件
    ffmpeg.FS('writeFile', 'input.mp4', await fetchFile('path/to/local/file.mp4'));
    
    // 读取文件
    const data = ffmpeg.FS('readFile', 'output.avi');

3. 音视频处理

  • 格式转换:

    • 使用 run 方法进行格式转换。例如,将 MP4 转换为 AVI:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', 'output.avi');
  • 视频剪辑:

    • 可以通过指定时间戳来剪辑视频。例如,剪辑从 10 秒到 20 秒的片段:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', '-ss', '10', '-to', '20', 'output.mp4');
  • 音频提取:

    • 从视频中提取音频:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', '-q:a', '0', '-map', 'a', 'output.mp3');

4. 视频压缩和调整

  • 视频压缩:

    • 可以通过调整比特率来压缩视频:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', '-b:v', '1000k', 'output.mp4');
  • 调整分辨率:

    • 改变视频的分辨率:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', '-vf', 'scale=640:360', 'output.mp4');

5. 其他功能

  • 添加水印:

    • 可以在视频上添加水印:

      javascript 复制代码
      await ffmpeg.run('-i', 'input.mp4', '-i', 'watermark.png', '-filter_complex', 'overlay=10:10', 'output.mp4');
  • 合并视频:

    • 合并多个视频文件:

      javascript 复制代码
      await ffmpeg.run('-i', 'concat:input1.mp4|input2.mp4', '-c', 'copy', 'output.mp4');

6. 获取处理结果

  • 获取处理后的文件 :
    • 使用 readFile 方法获取处理后的文件数据,并可以将其转换为 Blob 以便下载:

      javascript 复制代码
      const data = ffmpeg.FS('readFile', 'output.mp4');
      const blob = new Blob([data.buffer], { type: 'video/mp4' });
      const url = URL.createObjectURL(blob);

三、具体功能实现

需求:
本地选择长视频,允许对长视频选择时间段裁剪,音频提取,图片帧提取。

1、ffmpeg引入:

npm install vue video.js @ffmpeg/ffmpeg

这里需要注意,安装依赖的时候我出现报错了 ,所以参考FFmpeg安装避坑,最后有效安装。

步骤:

1、手动在package.json文件中加入配置后下载

2、将nodemodules中的这三个文件复制到pubilc下

启动项目之后可以使用ffmpeg的能力了

2、切割视频

vue 复制代码
  /**
     * 切割视频方法
     * 使用FFmpeg对视频进行时间段切割
     * 步骤:
     * 1. 设置处理状态并加载FFmpeg
     * 2. 获取原视频文件并写入FFmpeg文件系统
     * 3. 执行FFmpeg命令进行切割
     * 4. 读取切割后的视频并创建下载链接
     * 5. 将结果添加到处理文件列表中
     */
    async cutVideo() {
      try {
        // 设置处理状态
        this.processing = true;
        this.processingStatus = '正在切割视频...';
        
        // 确保FFmpeg已加载
        await this.loadFFmpeg();

        // 获取视频文件并写入FFmpeg虚拟文件系统
        const videoBlob = await fetch(this.videoSource).then(r => r.blob());
        ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoBlob));

        // 执行FFmpeg命令进行视频切割
        // -i: 输入文件
        // -ss: 开始时间点(秒)
        // -t: 持续时间(秒)
        // -c copy: 复制编解码器(不重新编码,速度快)
        await ffmpeg.run(
          '-i', 'input.mp4',
          '-ss', `${this.startTime}`,
          '-t', `${this.endTime - this.startTime}`,
          '-c', 'copy',
          'output.mp4'
        );

        // 从FFmpeg文件系统读取切割后的视频
        const data = ffmpeg.FS('readFile', 'output.mp4');
        // 创建Blob对象用于下载
        const blob = new Blob([data.buffer], { type: 'video/mp4' });
        
        // 将处理结果添加到文件列表
        this.processedFiles.push({
          name: 'cut_video.mp4', // 文件名
          type: 'video', // 文件类型
          blob: blob, // 文件数据
          url: this.createDownloadUrl(blob) // 下载链接
        });

      } catch (error) {
        console.error('视频切割失败:', error);
      } finally {
        // 重置处理状态
        this.processing = false;
      }
    },

打印如下: 结果如下:

3、提取音频

/** 复制代码
     * 提取音频方法
     * 使用FFmpeg从视频中提取音频轨道并转换为MP3格式
     * 步骤:
     * 1. 设置处理状态并加载FFmpeg
     * 2. 从processedFiles中获取已处理的视频文件
     * 3. 将视频写入FFmpeg文件系统
     * 4. 执行FFmpeg命令提取音频
     * 5. 读取提取的音频并创建下载链接
     * 6. 将结果添加到处理文件列表中
     */
    async extractAudio() {
      try {
        // 设置处理状态
        this.processing = true;
        this.processingStatus = '正在提取音频...';
        
        // 确保FFmpeg已加载
        await this.loadFFmpeg();

        // 从处理文件列表中获取视频文件
        const videoFile = this.processedFiles.find(f => f.type === 'video');
        if (!videoFile) throw new Error('未找到视频文件');

        // 将视频文件写入FFmpeg虚拟文件系统
        ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile.blob));

        // 执行FFmpeg命令提取音频
        // -i: 输入文件
        // -vn: 禁用视频输出
        // -acodec libmp3lame: 使用LAME编码器将音频编码为MP3格式
        await ffmpeg.run(
          '-i', 'input.mp4',
          '-vn',
          '-acodec', 'libmp3lame',
          'audio.mp3'
        );

        // 从FFmpeg文件系统读取提取的音频
        const data = ffmpeg.FS('readFile', 'audio.mp3');
        // 创建Blob对象用于下载
        const blob = new Blob([data.buffer], { type: 'audio/mp3' });
        
        // 将处理结果添加到文件列表
        this.processedFiles.push({
          name: 'extracted_audio.mp3', // 文件名
          type: 'audio', // 文件类型
          blob: blob, // 文件数据
          url: this.createDownloadUrl(blob) // 下载链接
        });

      } catch (error) {
        // 错误处理
        console.error('音频提取失败:', error);
      } finally {
        // 重置处理状态
        this.processing = false;
      }
    },

音频提取打印:

结果如下:

4、视频帧提取

/** 复制代码
     * 从视频中提取关键帧
     * 使用FFmpeg每60秒提取一帧图像,并保存为JPEG格式
     * 提取的帧会被添加到processedFiles数组中供后续使用
     */
    async extractFrames() {
      try {
        // 设置处理状态为正在进行中
        this.processing = true;
        this.processingStatus = '正在抽取关键帧...';

        // 确保FFmpeg已加载完成
        await this.loadFFmpeg();

        // 从处理文件列表中查找视频文件
        const videoFile = this.processedFiles.find(f => f.type === 'video');
        if (!videoFile) throw new Error('未找到视频文件');

        // 将视频文件写入FFmpeg虚拟文件系统
        ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile.blob));

        // 执行FFmpeg命令提取帧
        // -i: 指定输入文件
        // -vf fps=1/60: 设置视频过滤器,每60秒提取1帧
        // -frame_pts 1: 在输出文件名中包含显示时间戳
        // frame_%d.jpg: 输出文件名格式,_%d会被替换为帧序号
        await ffmpeg.run(
          '-i', 'input.mp4',
          '-vf', 'fps=1/60',
          '-frame_pts', '1',
          'frame_%d.jpg'
        );

        // 存储提取的帧
        const frames = [];
        // 遍历提取的帧,最多提取maxFrames个
        for (let i = 1; i <= this.maxFrames; i++) {
          try {
            // 从FFmpeg文件系统读取帧数据
            const data = ffmpeg.FS('readFile', `frame_${i}.jpg`);
            // 创建Blob对象用于预览和下载
            const blob = new Blob([data.buffer], { type: 'image/jpeg' });
            // 将帧信息添加到数组
            frames.push({
              name: `frame_${i}.jpg`, // 帧文件名
              type: 'image', // 文件类型
              blob: blob, // 帧数据
              url: this.createDownloadUrl(blob) // 创建下载链接
            });
          } catch (e) {
            // 如果读取失败,说明没有更多帧了,退出循环
            break;
          }
        }

        // 将提取的帧添加到处理文件列表
        this.processedFiles.push(...frames);

      } catch (error) {
        // 错误处理
        console.error('帧提取失败:', error);
      } finally {
        // 重置处理状态
        this.processing = false;
      }
    }
  },

结果如下:

遇到的问题:

在引入之后发现跨域问题:ReferenceError: SharedArrayBuffer is not defined

别慌:在vue.config.js中增加下面的配置:(我使用的是vite,webpack自行修改)

js 复制代码
  devServer: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin", // 保护你的源站点免受攻击
      "Cross-Origin-Embedder-Policy": "require-corp", // 保护受害者免受你的源站点的影响
    },
  }

上线后还有这样的问题,增加nginx配置:

js 复制代码
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;

完整的项目代码我放在github,欢迎star,下载下来可以直接对本地视频进行剪辑

后续我尝试一下前端实现音频文字的提取。

四、总结

ffmpeg.js 提供了丰富的功能来处理音视频文件,包括格式转换、剪辑、提取音频、压缩、调整分辨率、添加水印等。通过这些方法,开发者可以在浏览器中实现强大的音视频处理功能。 能够在浏览器中运行而不需要安装其他工具,主要是因为它利用了 WebAssembly 和 Emscripten 技术,将 FFmpeg 编译为可以在浏览器中高效执行的格式。通过虚拟文件系统和浏览器 API,ffmpeg.js 实现了音视频处理的完整功能,提供了一个强大的客户端解决方案。

相关推荐
祈澈菇凉1 小时前
如何结合使用thread-loader和cache-loader以获得最佳效果?
前端
垣宇1 小时前
Vite 和 Webpack 的区别和选择
前端·webpack·node.js
java1234_小锋1 小时前
一周学会Flask3 Python Web开发-客户端状态信息Cookie以及加密
前端·python·flask·flask3
化作繁星1 小时前
如何在 React 中测试高阶组件?
前端·javascript·react.js
Au_ust1 小时前
千峰React:函数组件使用(2)
前端·javascript·react.js
爱吃南瓜的北瓜2 小时前
npm install 卡在“sill idealTree buildDeps“
前端·npm·node.js
TTc_2 小时前
记录首次安装远古时代所需的运行环境成功npm install --save-dev node-sass
前端·npm·sass
翻滚吧键盘2 小时前
npm使用了代理,但是代理软件已经关闭导致创建失败
前端·npm·node.js
烂蜻蜓2 小时前
Uniapp 设计思路全分享
前端·css·vue.js·uni-app·html
GAMESLI-GIS2 小时前
【WebGL】fbo双pass案例
前端·javascript·webgl