纯前端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 实现了音视频处理的完整功能,提供了一个强大的客户端解决方案。

相关推荐
极小狐20 分钟前
极狐GitLab 容器镜像仓库功能介绍
java·前端·数据库·npm·gitlab
程序猿阿伟32 分钟前
《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
前端·flutter
rafael(一只小鱼)35 分钟前
黑马点评实战笔记
前端·firefox
weifont36 分钟前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情40 分钟前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
影子信息1 小时前
css 点击后改变样式
前端·css
几何心凉1 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程1 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
hnlucky2 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
懒羊羊我小弟3 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts