【Vue3】前端使用 FFmpeg.wasm 完成用户视频录制,并对视频进行压缩处理
强烈推荐这篇博客!非常全面的一篇文章,本文是对该博客的简要概括和补充,在不同技术栈中提供一种可行思路,可先阅读该篇文章再阅读本篇:
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"><videoref="videoElement"webkit-playsinlineplaysinlinemutedautoplaystyle="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 | numberpage: numbertime: numberspendTime: 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 = trueawait 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 // 页数+1recordBlob.value = [] // 清空数组mediaRecorder!.start(timeSlice) // 重新开始录制} else {console.error('录制异常,请重新录制本页,可尝试缩短录制时长')mediaRecorder!.start(timeSlice)}}mediaRecorder.onerror = (e: Event) => {console.error('触发了 error 事件' + mediaRecorder?.state)}// 3、实时显示摄像画面nextTick(() => {videoElement.value!.srcObject = streamvideoElement.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 } = FFmpegconst 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
相关文章:

【Vue3】前端使用 FFmpeg.wasm 完成用户视频录制,并对视频进行压缩处理
强烈推荐这篇博客!非常全面的一篇文章,本文是对该博客的简要概括和补充,在不同技术栈中提供一种可行思路,可先阅读该篇文章再阅读本篇: FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBu…...

基础算法——前缀和
由于比赛基本都是采用Dev-C所以,算法篇基本都是采用Dev-C来解释(版本5.11,c11) 首先介绍一下前缀和算法 给定一个数组,有q次询问,每次询问: 两个整数l,r,求出数组 l 到 r的结果 遇…...

spring实例化对象的几种方式(使用XML配置文件)
前言 Spring框架作为一个轻量级的控制反转(IoC)容器,为开发者提供了多种对象实例化的策略。通过这些策略,开发者可以更加灵活地控制对象的生命周期和依赖关系。无论是通过XML配置、注解配置还是Java配置,Spring都能…...

【二叉树】力扣 129.求根节点到叶子节点数字之和
一、题目 二、思路 每找到一个非空节点,之前路径上的所有节点的数量级都要增加1个单位。例如,当前节点为3,之前的节点路径为1 -> 2,presum 1 * 10 2 12,现在路径变为了 1 -> 2 -> 3,sum pres…...

深度学习物体检测之YOLOV5源码解读
V5比前面版本偏工程化,项目化,更贴合实战 一.V5版本项目配置 (1)整体项目概述 首先github直接查找yolov5,下载下来即可。在训练时,数据是怎么处理的?网络模型架构是怎么设计的(如各层的设计)?yolov5要求是大于python3.8与大于等…...
音频数据采样入门详解 - 给Python初学者的简单解释
音频数据采样入门详解 - 给Python初学者的简单解释 声音是如何变成数字的?什么是采样率?为什么要懂这个?Python小例子总结 大家好!今天我们来聊一个有趣的话题:音频数据是如何在计算机中处理的。让我用最简单的方式来解…...

Unity类银河战士恶魔城学习总结(P179 Enemy Archer 弓箭手)
教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/ 本章节实现了敌人弓箭手的制作 Enemy_Archer.cs 核心功能 状态机管理敌人的行为 定义了多个状态对象(如 idleState、moveState、attackState 等),通过状态机管理敌人的…...

SpringCloud集成sleuth和zipkin实现微服务链路追踪
文章目录 前言技术积累spring cloud sleuth介绍zipkin介绍Zipkin与Sleuth的协作 SpringCloud多模块搭建Zipkin Server部署docker pull 镜像启动zipkin server SpringCloud 接入 Sleuth 与 Zipkinpom引入依赖 (springboot2.6)appilication.yml配置修改增加测试链路代码 调用微服…...

Python随机抽取Excel数据并在处理后整合为一个文件
本文介绍基于Python语言,针对一个文件夹下大量的Excel表格文件,基于其中每一个文件,随机从其中选取一部分数据,并将全部文件中随机获取的数据合并为一个新的Excel表格文件的方法。 首先,我们来明确一下本文的具体需求。…...

Linux+Docker onlyoffice 启用 HTTPS 端口支持
文章目录 一、需求二、配置2.1 创建容器2.2 进入容器2.3 生成私钥和证书 2.4 测试访问 一、需求 上篇文章介绍了如何搭建一个 onlyoffice 在线预览服务,但是我们实际场景调用该服务的网站是协议是 https 的 ,但是 onlyoffice 服务还没做配置,…...

在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c
在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c 1. Installing the extension (在 Visual Studio Code 中安装插件)1.1. Extensions for Visual Studio Code1.2. C/C1.2.1. Pre-requisites 1.3. Makefile Tools 2. Configuring your project (配置项目)2.1.…...
python中math模块常用函数
文章目录 math模块简介各种三角函数反三角函数取整函数欧几里得距离绝对值最大公约数开根号幂阶乘函数 math模块简介 math模块是python标准库的一部分,提供了对于浮点数相关的数学运算,下面是常用的一些function 各种三角函数反三角函数 math.cos、ma…...
优化 Vue 3 开发体验:配置 Vite 使用 WebStorm 作为 Vue DevTools 的默认编辑器
优化 Vue 3 开发体验:配置 Vite 使用 WebStorm 替代 VS Code 作为 Vue DevTools 的默认编辑器 在 Vue 3 项目开发中,合理配置开发工具可以大大提升我们的工作效率。本文将介绍如何配置 Vite,使其在使用 Vue DevTools 时将默认编辑器从 VS Co…...

【C语言练习(9)—有一个正整数,求是几位数然后逆序打印】
C语言练习(9) 文章目录 C语言练习(9)前言题目题目解析结果总结 前言 主要到整数的取余(%)和整数的取商(/),判断语句if…else if …else的使用 题目 给一个不多于3位的正整数,要求:一、求它是几位数&…...
热敏打印机的控制
首次接触热敏打印机,本来没有特别之处,花了大概十天时间完成一款猫学王热敏打印机,给到客户体验后,客户反馈说打字看起来不明显,打印照片有条纹,所以引起了我对于他的关注,几点不足之处需要优化…...

【closerAI ComfyUI】电商赋能,AI模特套图生产,各种姿势自定义,高度保持人物服饰场景一致性,摆拍街拍专用
closerAIGCcloserAI,一个深入探索前沿人工智能与AIGC领域的资讯平台,我们旨在让AIGC渗入我们的工作与生活中,让我们一起探索AIGC的无限可能性!aigc.douyoubuy.cn 【closerAI ComfyUI】电商赋能,AI模特套图生产,各种姿势自定义,高度保持人物服饰场景一致性,摆拍街拍专用…...

ARM学习(36)静态扫描规则学习以及工具使用
笔者来学习了解一下静态扫描以及其规则,并且亲身是实践一下对arm 架构的代码进行扫描。 1、静态扫描认识 静态扫描:对代码源文件按照一定的规则进行扫描,来发现一些潜在的问题或者风险,因为不涉及代码运行,所以其一般只是发现一些规范或则一些质量问题,当然这些可能存在潜…...
使用 Docker Compose 部署 Redis 主从与 Sentinel 高可用集群
文章目录 使用 Docker Compose 部署 Redis 主从与 Sentinel 高可用集群Redis 主从架构简介Redis Sentinel 简介配置文件1. 主节点配置 (redis-master.conf)2. 从节点配置 (redis-slave1.conf 和 redis-slave2.conf)redis-slave1.confredis-slave2.conf3. Sentinel 配置 (sentin…...

警惕!手动调整服务器时间可能引发的系统灾难
警惕!手动调整服务器时间可能引发的系统灾难 1. 鉴权机制1.1 基于时间戳的签名验证1.2 基于会话的认证机制(JWT、TOTP) 2. 雪花算法生成 ID 的影响2.1 时间戳回拨导致 ID 冲突2.2 ID 顺序被打乱 3. 日志记录与审计3.1 日志顺序错误3.2 审计日…...

MySQL追梦旅途之性能优化
1、索引优化 索引可以显著加速查询操作,但过多或不适当的索引也会带来负面影响(如增加写入开销)。因此,选择合适的索引至关重要。 创建索引: 为经常用于WHERE子句、JOIN条件和ORDER BY排序的列创建索引。 CREATE I…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...

Java后端检查空条件查询
通过抛出运行异常:throw new RuntimeException("请输入查询条件!");BranchWarehouseServiceImpl.java // 查询试剂交易(入库/出库)记录Overridepublic List<BranchWarehouseTransactions> queryForReagent(Branch…...

基于谷歌ADK的 智能产品推荐系统(2): 模块功能详解
在我的上一篇博客:基于谷歌ADK的 智能产品推荐系统(1): 功能简介-CSDN博客 中我们介绍了个性化购物 Agent 项目,该项目展示了一个强大的框架,旨在模拟和实现在线购物环境中的智能导购。它不仅仅是一个简单的聊天机器人,更是一个集…...