【Vue3】前端使用 FFmpeg.wasm 完成用户视频录制,并对视频进行压缩处理

news2024/12/16 14:15:40

强烈推荐这篇博客!非常全面的一篇文章,本文是对该博客的简要概括和补充,在不同技术栈中提供一种可行思路,可先阅读该篇文章再阅读本篇:

FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBuffer、跨域隔离、避坑…)_vue ffmpeg-CSDN博客文章浏览阅读1.1w次,点赞71次,收藏96次。本文介绍了FFmpeg在Vue项目中从0到1的使用,从安装 => 配置 => 简单使用 => 认识SharedArrayBuffer和跨域隔离 => 避坑…_vue ffmpeg[这里是图片001]https://zahuopu.blog.csdn.net/article/details/135032429?fromshare=blogdetail&sharetype=blogdetail&sharerId=135032429&sharerefer=PC&sharesource=Andye11&sharefrom=from_link

上述博客技术栈: Vue2 + Webpack + JS

本文技术栈:Vue3 + Vite + TS

本文运行环境: windows11 + Edge浏览器 +22.11 node版本

1、安装配置(等同上述博客):

npm i @ffmpeg/ffmpeg@0.9.8
npm i @ffmpeg/core@0.10.0

2、复制下载的核心js文件到 /public 静态文件夹中(基本等同上述博客)

3、配置Vite配置项(和webpack在代码位置上有细微区别 内容完全一致)

server: {
   ......
   headers: { // 目的是为了消除浏览器报错: SharedArrayBufferis not defined
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp'
   },
   ......
 },

特别注意!

1、配置该配置项之后, 本地运行时 请使用localhost 服务进行开发,因为 ffmpeg仅支持 本地 和 https 环境运行,不支持 http 环境;

2、如果需要打包到线上环境运行,还需要 后端同学对nginx 进行同样头部配置,仅有前端自己配置只能在自己本地服务中运行。

4、完成配置,代码相关内容如下:

预览效果图:

可以看到,压缩率还是很高的,时间性能上大概需要原视频长度的 50% (也就是 10分钟的视频大概需要接近5分钟去压缩(我的设备信息:笔记本电脑CPU - 英特尔 i5-12500H)

在此过程中 页面内存消耗巨大,基本无法与用户进行其他互动),同时牺牲了一定清晰度,但最终可以缩小90%左右的大小。

也就是说:如果你的目标是 使用时间和性能来换取极致的大小压缩,这个库是可用的。

代码原文:

<template>
  <div class="test-page" v-loading="isLoading_fullScreen">
    <div class="flx-center" style="flex-direction: column">
      <video
        ref="videoElement"
        webkit-playsinline
        playsinline
        muted
        autoplay
        style="object-fit: cover"
        width="200px"
        height="150px"
      ></video>
      <div style="margin-top: 10px; column-gap: 30px" class="flx-center">
        <h1 style="display: inline-block">{{ page }}</h1>
        <el-button type="primary" @click="nextPage">翻页</el-button>
      </div>
    </div>
    <div class="card">
      <div style="margin-bottom: 10px; column-gap: 30px" class="flx-center">
        <el-button @click="getMedia" v-if="!isRecording" style="margin-right: 10px">开始录制</el-button>
        <el-text type="primary">{{ timer_min }}</el-text>
        <el-text v-if="isRecording">{{ old_timer_min }}</el-text>
      </div>
      <el-table :data="tableData" border style="width: 100%; min-height: 250px" max-height="260">
        <el-table-column prop="type" label="类型">
          <template #default="scope">
            {{ scope.row.type == 1 ? '原文件' : '压缩后' }}
          </template>
        </el-table-column>
        <el-table-column prop="size" label="大小" />
        <el-table-column prop="spendTime" label="用时" />
        <el-table-column prop="page" label="页码" />
      </el-table>
    </div>
  </div>
</template>

<script setup lang="ts" name="test">
import { ref, nextTick, computed } from 'vue'
import FFmpeg from '@ffmpeg/ffmpeg/dist/ffmpeg.min.js'

const page = ref(1)

// ============================================= 时间相关 =============================================
const timer_sec = ref(0)
const timer_min = computed(() => {
  let mm = Math.floor(timer_sec.value / 60)
  let ss = Math.floor(timer_sec.value % 60)
  return (mm < 10 ? '0' + mm : mm) + ':' + (ss < 10 ? '0' + ss : ss)
})
const old_timer_min = ref('00:00')

// ============================================= 表格相关 =============================================
type TableDataItem = {
  type: number // 1代表原文件  2代表压缩文件
  size: string | number
  page: number
  time: number
  spendTime: string
}
const tableData = ref<TableDataItem[]>([])

const addTableData = (type: number, blobSize: number) => {
  if (type == 1) {
    let spendTime = ''
    let lastItem = tableData.value.findLast(item => item.type == 1)
    if (lastItem) {
      spendTime = secondToMinute((Date.now() - lastItem.time) / 1000)
    } else {
      spendTime = secondToMinute(timer_sec.value)
    }
    tableData.value.push({
      type: 1,
      size: (blobSize / 1024 / 1024).toFixed(2),
      time: Date.now(),
      spendTime: spendTime,
      page: page.value
    })
  } else {
    let spendTime2 = secondToMinute((Date.now() - tableData.value[tableData.value.length - 1].time) / 1000)
    tableData.value.push({
      type: 2,
      page: page.value,
      time: Date.now(),
      spendTime: spendTime2,
      size: (blobSize / 1024 / 1024).toFixed(2)
    })
  }
}

//  ===================================== 视频录制相关 =====================================
const videoElement = ref<HTMLVideoElement | null>(null) // 用于播放的 VideoElement 对象
let mediaRecorder: MediaRecorder | null = null // 用于录制的 MediaRecorder 对象
const isRecording = ref(false) // 是否正在录制
const recordBlob = ref<Blob[]>([]) // 记录视频Blob数据
const isLoading_fullScreen = ref(false)
const timeSlice = 10 * 1000

// 获取摄像头和麦克风访问权限
async function getMedia() {
  isLoading_fullScreen.value = true
  await ffmpeg.load()

  // 配置视频录制分辨率为480p 帧率为24,但是在实际测试中发现各大浏览器基本都不支持此配置,设置与否不影响视频大小
  let mediaStreamConstraints: MediaStreamConstraints = {
    video: {
      width: { ideal: 640 },
      height: { ideal: 480 },
      frameRate: { ideal: 24, max: 24 }
    },
    audio: true
  }

  navigator.mediaDevices
    .getUserMedia(mediaStreamConstraints)
    .then(stream => {
      isRecording.value = true

      // 1、记录视频数据
      mediaRecorder = new MediaRecorder(stream)

      // 2、开始录制
      mediaRecorder.start(timeSlice)

      // 处理录制数据
      mediaRecorder.ondataavailable = (e: BlobEvent) => {
        if (e.data.size > 0) {
          recordBlob.value.push(e.data)
        } else {
          console.error('最近10秒内数据异常')
        }
      }

      mediaRecorder.onstop = async () => {
        if (recordBlob.value.length > 0) {
          let fullBlob = new Blob(recordBlob.value, { type: 'video/mp4' })

          // 处理原文件数据到表格中
          addTableData(1, fullBlob.size)

          let compressedBlob = await compressVideoBlob(fullBlob)

          // 处理压缩文件数据到表格中
          addTableData(2, compressedBlob.size)

          page.value += 1 // 页数+1
          recordBlob.value = [] // 清空数组
          mediaRecorder!.start(timeSlice) // 重新开始录制
        } else {
          console.error('录制异常,请重新录制本页,可尝试缩短录制时长')
          mediaRecorder!.start(timeSlice)
        }
      }

      mediaRecorder.onerror = (e: Event) => {
        console.error('触发了 error 事件' + mediaRecorder?.state)
      }

      // 3、实时显示摄像画面
      nextTick(() => {
        videoElement.value!.srcObject = stream
        videoElement.value!.play()

        setInterval(() => {
          timer_sec.value += 1
        }, 1000)
      })

      isLoading_fullScreen.value = false
    })
    .catch(err => {
      console.error('错误:', err)
      alert('无法访问摄像头或麦克风')
    })
}

const nextPage = async () => {
  if (mediaRecorder) {
    mediaRecorder.stop()
    old_timer_min.value = timer_min.value
  }
}

const { createFFmpeg, fetchFile } = FFmpeg
const ffmpeg = createFFmpeg({
  corePath: '/FFMPEG/ffmpeg-core.js', // 核心文件的路径
  log: true // 是否在控制台打印日志,true => 打印
})

// 压缩视频Blob数据
const compressVideoBlob = async (blob: Blob) => {
  const data = await blob.arrayBuffer()
  const inputName = 'input.mp4'
  ffmpeg.FS('writeFile', inputName, new Uint8Array(data))
  await ffmpeg.run('-i', inputName, '-vcodec', 'libx264', '-crf', '28', '-acodec', 'copy', 'output.mp4')
  const outputData = ffmpeg.FS('readFile', 'output.mp4')
  const compressedBlob = new Blob([outputData.buffer], { type: 'video/mp4' })
  return compressedBlob
}

// 处理时间 把秒数处理为分钟
const secondToMinute = (second: number, pad: string = '') => {
  second = Math.floor(second)
  return Math.floor(second / 60) + '分' + pad + (second % 60) + '秒'
}
</script>

<style lang="scss" scoped>
.test-page {
  padding: 20px;
}
.flx-center {
  display: flex;
  align-items: center;
  justify-content: center;
}
.card {
  box-sizing: border-box;
  padding: 20px;
  overflow-x: hidden;
  background-color: #ffffff;
  border: 1px solid #e4e7ed;
  border-radius: 6px;
  box-shadow: 0 0 12px rgb(0 0 0 / 5%);
}


</style>

注意:请自行删除 HTML中element-plus UI库的相关内容,使用它们仅为了样式美观,不影响实际代码运行

额外注意事项:

1、import 引入 FFmpeg时 需要指定完整路径:即使用

import FFmpeg from '@ffmpeg/ffmpeg/dist/ffmpeg.min.js'
// 虽然这样会损失代码提示,但起码能在Vite环境跑起来 这点和博主原文不同

2、在 createFFmpeg 函数中, 需要额外注意路径一定要使用绝对路径

// 我把那3个文件复制到 /public/FFMPEG下面,但是这里不要带 /public ,主要是为了防止线上环境路径失效, 具体为什么可查询我的【Vite项目中静态资源路径处理】一文
const { createFFmpeg, fetchFile } = FFmpeg

const ffmpeg = createFFmpeg({
    corePath: '/FFMPEG/ffmpeg-core.js', 
})

3、最后,我在0.9.8版本的 ffmpeg.wasm 中发现:

微信内置浏览器乃至移动设备大部分浏览器都不兼容 SharedArrayBuffer,我在win11的 edge和chrome浏览器中可以非常非常顺利地运行代码,哪怕是打包到线上环境也是可以正常运行,

但手机端无论是 微信浏览器还是 夸克浏览器、safari浏览器 均会报 SharedArrayBuffer is not defined 错误,IOS手机还会报出 Range Error 错误。

听说最新0.12版本已经可以修复这个问题了,如果有机会,我也会补充到这里来。

Q:0.12版本ffmpeg.wasm可以在移动设备上正常使用了吗?

A:十分抱歉,笔者在使用0.12版本的库时遇到了 ffmpeg.load 不执行也不报错的未知情况,至今仍未能成功运行。
但笔者发现canvas也可以间接完成视频压缩的功能,大概可以把原视频压缩到原本的25%~35%大小,链接放在这里:
【Vue3】使用canvas来实现H5页面摄像头录制的【视频压缩】功能-CSDN博客文章浏览阅读2次。用户边说话边进行摄像头录制,同时要把用户说的话转为字幕显示在评测结果中,但iPhone手机10秒钟基本就能录制一个大约12MB大小的视频,如果考试时间达到30分钟,那么消耗的流量将十分恐怖,而且极其容易出现网络问题,导致视频录制失败。,效果虽然不算非常好,但是没有引入任何第三方库,代码量非常少,所以也算有它的价值所在。之前尝试使用 FFmege.wasm失败,一是性能比较差,需要大概原视频50%的时间来进行压缩,二是移动设备兼容性比较差,没办法成功跑起来。代码效果图:(大小为 PC端未被压缩视频大小)[这里是图片005]https://blog.csdn.net/Andye11/article/details/144084235?fromshare=blogdetail&sharetype=blogdetail&sharerId=144084235&sharerefer=PC&sharesource=Andye11&sharefrom=from_link

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2260536.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

17、ConvMixer模型原理及其PyTorch逐行实现

文章目录 1. 重点2. 思维导图 1. 重点 patch embedding : 将图形分割成不重叠的块作为图片样本特征depth wise point wise new conv2d : 将传统的卷积转换成通道隔离卷积和像素空间隔离两个部分&#xff0c;在保证精度下降不多的情况下大大减少参数量 2. 思维导图 后续再整…

金蝶云苍穹踩过的坑(慢慢更新)

IDEA不能用最新版&#xff0c;不然搜不到金蝶的插件。 我用的是2024.1.7/2023.1.7 IDEA里增加金蝶插件库的地址也变了&#xff0c;现在是 https://tool.kingdee.com/kddt/idea-updatePlugins.xml 金蝶云苍穹部署在服务器 MAC本地IDEA调试的时候&#xff0c;登录N次能成功一次…

springboot438校园志愿者管理系统(论文+源码)_kaic

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统校园志愿者管理系统信息管理难度大&#xff0c;容错率低&…

高项 - 信息系统管理

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 博文更新参考时间点&#xff1a;2024-11-09 高项 - 章节与知识点汇总&#xff1a;点击跳转 文章目录 高项 - 信息系统管理管理方法管理基础规划和组织设计和实施运维和服务优化和持续改进 管理要点数据管理运维管…

05、GC基础知识

JVM程序在跑起来之后&#xff0c;在数据的交互过程中&#xff0c;就会有一些数据是过期不用的&#xff0c;这些数据可以看做是垃圾&#xff0c;JVM中&#xff0c;这些垃圾是不用开发者管的&#xff0c;它自己会有一套垃圾回收系统自动回收这些内存垃圾&#xff0c;以备后面继续…

【前端开发】HTML+CSS网页,可以拿来当作业(免费开源)

HTML代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content_lizhongyu"widthdevice-width, initial-scale1.0"><title>小兔鲜儿-新鲜、惠民、快捷<…

【Unity3D】无限循环列表(扩展版)

基础版&#xff1a;【Unity技术分享】UGUI之ScrollRect优化_ugui scrollrect 优化-CSDN博客 using UnityEngine; using UnityEngine.UI; using System.Collections.Generic;public delegate void OnBaseLoopListItemCallback(GameObject cell, int index); public class BaseLo…

Git-基础操作命令

目录 Git基础操作命令 case *查看提交日志 log 版本回退 get add . Git基础操作命令 我们创建并且初始化这个仓库以后&#xff0c;我们就要在里面进行操作。 Git 对于文件的增删改查存在几个状态&#xff0c;这些修改状态会随着我们执行Git的命令而发生变化。 untracked、…

Flutter Navigator2.0的原理和Web端实践

01 背景与动机 在Navigator 2.0推出之前&#xff0c;Flutter主要通过Navigator 1.0和其提供的 API&#xff08;如push(), pop(), pushNamed()等&#xff09;来管理页面路由。然而&#xff0c;Navigator 1.0存在一些局限性&#xff0c;如难以实现复杂的页面操作&#xff08;如移…

【容器】k8s学习笔记基础部分(三万字超详细)

概念 应用部署方式演变 在部署应用程序的方式上&#xff0c;主要经历了三个时代&#xff1a; 传统部署&#xff1a;互联网早期&#xff0c;会直接将应用程序部署在物理机上 优点&#xff1a;简单&#xff0c;不需要其它技术的参与 缺点&#xff1a;不能为应用程序定义资源使…

PostgreSQL 常用运维SQL整理

一、查询并杀会话 -- 查询会话 select pid,usename,client_addr,client_port,query_start,query,wait_event from pg_stat_activity; -- 杀会话 select pg_terminate_backend(pid号); -- 使用如下命令自动生成杀会话语句 select datid,datname,pid,usesysid,usename,applicat…

前端0基础用Cursor完成管理系统页面 - 1

Cursor下载 下载链接: https://www.cursor.com/ Hello World! 作为完全不会前端的人&#xff0c;首先需要让AI帮我们搭建一个HelloWorld界面 确定语言框架 首先要给AI框定好前端语言和框架&#xff0c;由于AI的物料大量来自网上的开源项目&#xff0c;所以越是受欢迎的开源…

系统组件优化的思考框架

我之前的文章里有分享过自己总结的做技术选型的思考框架&#xff0c;本文将会分享一下我总结的做系统组件调优/优化的思考框架。 组件优化的思考框架 常见的互联网架构基本离不开数据库、缓存、消息队列、搜索、数据处理等等各种组件&#xff0c;虽然组件的形态不一、功能不同…

Linux shell的七大功能 ---自动补齐、管道机制、别名

1、自动补齐---TAB 输入命令的前几个字符&#xff0c;按下tab键&#xff0c;会自动补齐完整的字符&#xff0c;若有多个命令、文件或目录的前几个字符相同&#xff0c;按下tab将会全部列举出来 2、管道机制---| 例如&#xff1a;ls -- help |more 将有关ls的帮助内容传递给“|…

计算机网络-基础概念(HTTP,TPC/IP, DNS,URL)

HTTP不同的版本 HTTP0.9于1990年问世&#xff0c;此时HTTP并没有作为正式的标准被建立。HTTP正式被公布是1996年的5月&#xff0c;版本命名为HTTP/1.0。HTTP1.1&#xff0c;1997年1月公布&#xff0c;目前仍然是主流版本的HTTP协议版本。 TCP/IP 通常使用的网络是在TCP/IP协…

12.3【JAVA-EXP4-DEBUGSTUDY】

java升级版本 JDK 1.8 是 Java Development Kit 的第 8 版本&#xff0c;发布于 2014 年 3 月 18 日。这个版本是 Java SE&#xff08;Standard Edition&#xff09;的一部分&#xff0c;包含了 Java 编程语言的实现、编译器、调试工具和其他相关组件 JDK 1.8: 这里的 1.8 表…

在Windows上运行mediapipe:适合新手的AI框架

一、mediapipe简介 mediapipe可以被视为谷歌版的onnx&#xff0c;其设计目的在于跨平台部署AI模型&#xff0c;并提供一系列工具来监测不同平台、不同设备运行人工智能模型时的性能表现。 尽管mediapipe已经陆续支持训练自定义模型&#xff0c;但博主更推荐使用Pytorch/Tenso…

自然语言处理:我的学习心得与笔记

Pytorch 1.Pytorch基本语法 1.1 认识Pytorch 1.2 Pytorch中的autograd 2.Pytorch初步应用 2.1 使用Pytorch构建一个神经网络 2.2 使用Pytorch构建一个分类器 小节总结 学习了什么是Pytorch. 。Pytorch是一个基于Numpy的科学计算包,作为Numpy的替代者,向用户提供使用GPU强大…

IAR环境下STM32静态库编译及使用

IAR环境下STM32静态库编译及使用 前言 最近了解到了STM32的静态库与动态库&#xff0c;在此记录一下STM32静态库的生成与使用。 静态库的作用主要是对代码进行封装及保护&#xff0c;使其他使用者只知其然而不知其所以然&#xff0c;因为封装后的静态库只有.h文件没有.c文件。…

【常考前端面试题总结】---2025

React fiber架构 1.为什么会出现 React fiber 架构? React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行&#xff0c;所以更新一旦开始&#xff0c;中途就无法中断。当层级很深时&#xff0c;递归更新时间超过了 16ms&#xff0c;用户交互就会卡顿。对于特别庞…