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

打印 上一主题 下一主题

主题 1014|帖子 1014|积分 3042

强烈保举这篇博客!非常全面的一篇文章,本文是对该博客的简要概括和补充,在不同技术栈中提供一种可行思绪,可先阅读该篇文章再阅读本篇:
FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBuffer、跨域隔离、避坑...)_vue ffmpeg-CSDN博客文章浏览阅读1.1w次,点赞71次,收藏96次。本文先容了FFmpeg在Vue项目中从0到1的使用,从安装 => 配置 => 简单使用 => 认识SharedArrayBuffer和跨域隔离 => 避坑..._vue ffmpeg
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、安装配置(等同上述博客):
  1. npm i @ffmpeg/ffmpeg@0.9.8
  2. npm i @ffmpeg/core@0.10.0
复制代码
2、复制下载的核心js文件到 /public 静态文件夹中(根本等同上述博客)

3、配置Vite配置项(和webpack在代码位置上有渺小区别  内容完全划一)
  1. server: {
  2.    ......
  3.    headers: { // 目的是为了消除浏览器报错: SharedArrayBufferis not defined
  4.       'Cross-Origin-Opener-Policy': 'same-origin',
  5.       'Cross-Origin-Embedder-Policy': 'require-corp'
  6.    },
  7.    ......
  8. },
复制代码
  特别注意!
  1、配置该配置项之后, 当地运行时 请使用 localhost 服务举行开发,因为 ffmpeg仅支持 当地 和 https 环境运行,不支持 http 环境;
  2、如果须要打包到线上环境运行,还须要 后端同学对nginx 举行同样头部配置,仅有前端本身配置只能在本身当地服务中运行。
  4、完成配置,代码相关内容如下:

预览结果图:
    

可以看到,压缩率还是很高的,时间性能上大概须要原视频长度的 50% (也就是 10分钟的视频大概须要靠近5分钟去压缩(我的设备信息:笔记本电脑 CPU - 英特尔 i5-12500H)
在此过程中 页面内存消耗巨大,根本无法与用户举行其他互动),同时捐躯了一定清晰度,但最终可以缩小90%左右的大小。
也就是说:如果你的目标是 使用时间和性能来调换极致的大小压缩,这个库是可用的。
代码原文:
  1. <template>
  2.   <div class="test-page" v-loading="isLoading_fullScreen">
  3.     <div class="flx-center" style="flex-direction: column">
  4.       <video
  5.         ref="videoElement"
  6.         webkit-playsinline
  7.         playsinline
  8.         muted
  9.         autoplay
  10.         style="object-fit: cover"
  11.         width="200px"
  12.         height="150px"
  13.       ></video>
  14.       <div style="margin-top: 10px; column-gap: 30px" class="flx-center">
  15.         <h1 style="display: inline-block">{{ page }}</h1>
  16.         <el-button type="primary" @click="nextPage">翻页</el-button>
  17.       </div>
  18.     </div>
  19.     <div class="card">
  20.       <div style="margin-bottom: 10px; column-gap: 30px" class="flx-center">
  21.         <el-button @click="getMedia" v-if="!isRecording" style="margin-right: 10px">开始录制</el-button>
  22.         <el-text type="primary">{{ timer_min }}</el-text>
  23.         <el-text v-if="isRecording">{{ old_timer_min }}</el-text>
  24.       </div>
  25.       <el-table :data="tableData" border style="width: 100%; min-height: 250px" max-height="260">
  26.         <el-table-column prop="type" label="类型">
  27.           <template #default="scope">
  28.             {{ scope.row.type == 1 ? '原文件' : '压缩后' }}
  29.           </template>
  30.         </el-table-column>
  31.         <el-table-column prop="size" label="大小" />
  32.         <el-table-column prop="spendTime" label="用时" />
  33.         <el-table-column prop="page" label="页码" />
  34.       </el-table>
  35.     </div>
  36.   </div>
  37. </template>
  38. <script setup lang="ts" name="test">
  39. import { ref, nextTick, computed } from 'vue'
  40. import FFmpeg from '@ffmpeg/ffmpeg/dist/ffmpeg.min.js'
  41. const page = ref(1)
  42. // ============================================= 时间相关 =============================================
  43. const timer_sec = ref(0)
  44. const timer_min = computed(() => {
  45.   let mm = Math.floor(timer_sec.value / 60)
  46.   let ss = Math.floor(timer_sec.value % 60)
  47.   return (mm < 10 ? '0' + mm : mm) + ':' + (ss < 10 ? '0' + ss : ss)
  48. })
  49. const old_timer_min = ref('00:00')
  50. // ============================================= 表格相关 =============================================
  51. type TableDataItem = {
  52.   type: number // 1代表原文件  2代表压缩文件
  53.   size: string | number
  54.   page: number
  55.   time: number
  56.   spendTime: string
  57. }
  58. const tableData = ref<TableDataItem[]>([])
  59. const addTableData = (type: number, blobSize: number) => {
  60.   if (type == 1) {
  61.     let spendTime = ''
  62.     let lastItem = tableData.value.findLast(item => item.type == 1)
  63.     if (lastItem) {
  64.       spendTime = secondToMinute((Date.now() - lastItem.time) / 1000)
  65.     } else {
  66.       spendTime = secondToMinute(timer_sec.value)
  67.     }
  68.     tableData.value.push({
  69.       type: 1,
  70.       size: (blobSize / 1024 / 1024).toFixed(2),
  71.       time: Date.now(),
  72.       spendTime: spendTime,
  73.       page: page.value
  74.     })
  75.   } else {
  76.     let spendTime2 = secondToMinute((Date.now() - tableData.value[tableData.value.length - 1].time) / 1000)
  77.     tableData.value.push({
  78.       type: 2,
  79.       page: page.value,
  80.       time: Date.now(),
  81.       spendTime: spendTime2,
  82.       size: (blobSize / 1024 / 1024).toFixed(2)
  83.     })
  84.   }
  85. }
  86. //  ===================================== 视频录制相关 =====================================
  87. const videoElement = ref<HTMLVideoElement | null>(null) // 用于播放的 VideoElement 对象
  88. let mediaRecorder: MediaRecorder | null = null // 用于录制的 MediaRecorder 对象
  89. const isRecording = ref(false) // 是否正在录制
  90. const recordBlob = ref<Blob[]>([]) // 记录视频Blob数据
  91. const isLoading_fullScreen = ref(false)
  92. const timeSlice = 10 * 1000
  93. // 获取摄像头和麦克风访问权限
  94. async function getMedia() {
  95.   isLoading_fullScreen.value = true
  96.   await ffmpeg.load()
  97.   // 配置视频录制分辨率为480p 帧率为24,但是在实际测试中发现各大浏览器基本都不支持此配置,设置与否不影响视频大小
  98.   let mediaStreamConstraints: MediaStreamConstraints = {
  99.     video: {
  100.       width: { ideal: 640 },
  101.       height: { ideal: 480 },
  102.       frameRate: { ideal: 24, max: 24 }
  103.     },
  104.     audio: true
  105.   }
  106.   navigator.mediaDevices
  107.     .getUserMedia(mediaStreamConstraints)
  108.     .then(stream => {
  109.       isRecording.value = true
  110.       // 1、记录视频数据
  111.       mediaRecorder = new MediaRecorder(stream)
  112.       // 2、开始录制
  113.       mediaRecorder.start(timeSlice)
  114.       // 处理录制数据
  115.       mediaRecorder.ondataavailable = (e: BlobEvent) => {
  116.         if (e.data.size > 0) {
  117.           recordBlob.value.push(e.data)
  118.         } else {
  119.           console.error('最近10秒内数据异常')
  120.         }
  121.       }
  122.       mediaRecorder.onstop = async () => {
  123.         if (recordBlob.value.length > 0) {
  124.           let fullBlob = new Blob(recordBlob.value, { type: 'video/mp4' })
  125.           // 处理原文件数据到表格中
  126.           addTableData(1, fullBlob.size)
  127.           let compressedBlob = await compressVideoBlob(fullBlob)
  128.           // 处理压缩文件数据到表格中
  129.           addTableData(2, compressedBlob.size)
  130.           page.value += 1 // 页数+1
  131.           recordBlob.value = [] // 清空数组
  132.           mediaRecorder!.start(timeSlice) // 重新开始录制
  133.         } else {
  134.           console.error('录制异常,请重新录制本页,可尝试缩短录制时长')
  135.           mediaRecorder!.start(timeSlice)
  136.         }
  137.       }
  138.       mediaRecorder.onerror = (e: Event) => {
  139.         console.error('触发了 error 事件' + mediaRecorder?.state)
  140.       }
  141.       // 3、实时显示摄像画面
  142.       nextTick(() => {
  143.         videoElement.value!.srcObject = stream
  144.         videoElement.value!.play()
  145.         setInterval(() => {
  146.           timer_sec.value += 1
  147.         }, 1000)
  148.       })
  149.       isLoading_fullScreen.value = false
  150.     })
  151.     .catch(err => {
  152.       console.error('错误:', err)
  153.       alert('无法访问摄像头或麦克风')
  154.     })
  155. }
  156. const nextPage = async () => {
  157.   if (mediaRecorder) {
  158.     mediaRecorder.stop()
  159.     old_timer_min.value = timer_min.value
  160.   }
  161. }
  162. const { createFFmpeg, fetchFile } = FFmpeg
  163. const ffmpeg = createFFmpeg({
  164.   corePath: '/FFMPEG/ffmpeg-core.js', // 核心文件的路径
  165.   log: true // 是否在控制台打印日志,true => 打印
  166. })
  167. // 压缩视频Blob数据
  168. const compressVideoBlob = async (blob: Blob) => {
  169.   const data = await blob.arrayBuffer()
  170.   const inputName = 'input.mp4'
  171.   ffmpeg.FS('writeFile', inputName, new Uint8Array(data))
  172.   await ffmpeg.run('-i', inputName, '-vcodec', 'libx264', '-crf', '28', '-acodec', 'copy', 'output.mp4')
  173.   const outputData = ffmpeg.FS('readFile', 'output.mp4')
  174.   const compressedBlob = new Blob([outputData.buffer], { type: 'video/mp4' })
  175.   return compressedBlob
  176. }
  177. // 处理时间 把秒数处理为分钟
  178. const secondToMinute = (second: number, pad: string = '') => {
  179.   second = Math.floor(second)
  180.   return Math.floor(second / 60) + '分' + pad + (second % 60) + '秒'
  181. }
  182. </script>
  183. <style lang="scss" scoped>
  184. .test-page {
  185.   padding: 20px;
  186. }
  187. .flx-center {
  188.   display: flex;
  189.   align-items: center;
  190.   justify-content: center;
  191. }
  192. .card {
  193.   box-sizing: border-box;
  194.   padding: 20px;
  195.   overflow-x: hidden;
  196.   background-color: #ffffff;
  197.   border: 1px solid #e4e7ed;
  198.   border-radius: 6px;
  199.   box-shadow: 0 0 12px rgb(0 0 0 / 5%);
  200. }
  201. </style>
复制代码
  注意:请自行删除 HTML中 element-plus UI库的相关内容,使用它们仅为了样式美观,不影响实际代码运行
  额外注意事项:

1、import 引入 FFmpeg时 须要指定完整路径:纵然用
  1. import FFmpeg from '@ffmpeg/ffmpeg/dist/ffmpeg.min.js'
  2. // 虽然这样会损失代码提示,但起码能在Vite环境跑起来 这点和博主原文不同
复制代码

2、在 createFFmpeg 函数中, 须要额外注意路径一定要使用绝对路径
  1. // 我把那3个文件复制到 /public/FFMPEG下面,但是这里不要带 /public ,主要是为了防止线上环境路径失效, 具体为什么可查询我的【Vite项目中静态资源路径处理】一文
  2. const { createFFmpeg, fetchFile } = FFmpeg
  3. const ffmpeg = createFFmpeg({
  4.     corePath: '/FFMPEG/ffmpeg-core.js',
  5. })
复制代码


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端未被压缩视频大小)
https://blog.csdn.net/Andye11/article/details/144084235?fromshare=blogdetail&sharetype=blogdetail&sharerId=144084235&sharerefer=PC&sharesource=Andye11&sharefrom=from_link
  





免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

怀念夏天

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表