前端 视频录制剖析

前端 视频录制剖析

作者:@ 很菜的小白在分享
时间:2021年12月7日

音视频三部曲

前端 音频录制剖析
前端 视频录制剖析
前端 桌面共享剖析

介绍

身为一个优秀的前端 coder 我们可能会遇到各种各样的需求,昨天我接到了一个新的需求,需要在项目中添加一个视频录制功能【疑问】【疑问】【疑问】,为什么要实现这种东西呢? 身为打工人只能默默接收。
拿到需求的我一顿操作来到了MDN官网,潦草看了一下文档看起来很简单嘛,于是撸起袖子准备开始今天的 codeing。

       1. 目录

           1.1 授权摄像头

           1.2 处理设备返回的流

           1.3 录制视频

           1.4 生成视频文件

           1.5 其他

           1.5 完整代码

流程

1. 授权摄像头

HTML5 提供了Navigation.getUserMedia()【部分浏览器已废弃】MediaDevices.getUserMedia()【新】API,这里我们只讲解新API。
MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可,媒体输入会产生一个 MediaStream(媒体流),里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。 —— MDN

注意

授权摄像头的 API 只能在 localhost 或 https 才可以拿到。
授权
它返回一个 Promise 对象,MediaStream 就是从 resolve 中返回的,若用户拒绝授权或设备不可用则会触发 reject 返回错误信息。

语法

window.navigator.mediaDevices.getUserMedia().then(stream => {// resolve
}).catch(error => {// reject
})

参数

options | Object

名称类型说明例子
audioboolean / Object授权音频Boolean: true / false | Object: {}
videoboolean / Object授权视频Boolean: true / false | Object: {width: 1280,height:720}
············

参考
兼容性

2. 处理设备返回的流

经过权限获取后我们可以在结果中拿到 stream,这时我们要用一个容器来承载这些流数据,HTML5 还提供了另一个组件 <video> ,video 可以说是一个很强大的存在,目前我们在网页中看到的视频播放组件都是由 video 搭载的,同样它也可以搭载我们视频设备返回的流。
下面我们来看看 video 是如何来搭载我们的视频流的。

语法

<!-- HTML --><video id="video-record"></video>
/** JavaScript **/let video = document.querySelector('#video-record')
function getUserMediaPermissions() {// 授权视频设备获取流数据window.navigator.mediaDevices.getUserMedia({video: true}).then(stream => {// 将摄像头返回的流添加的 video 组件的 src 上,srcObject 是一个新属性if ('srcObject' in this.video) {video.srcObject = stream} else {video.src = window.URL.createObjectURL(stream)}	})
}

到这里就实现了将摄像头捕捉到的流通过 video 呈现到我们的网页了。是不是很开心。

我的仙人球!!咳咳~~ 因为摄像头是外接的像素不是特别清楚【呲牙】
效果图

3. 录制视频

重点来了,录制的核心部分。

原理

  1. 实时获取当前视频流轮询绘制到canvas上
  2. 将当前绘制的流(准确说是一个blob数据)添加到一个列表中
  3. 录制结束后将生成的 blobs 进行合并处理成一个整体,这时视频就诞生了。

创建画布视频捕获器

captureStream API 将会返回一个实时视频捕获的画布
语法

let mediaStream = canvas.captureStream(frameRate)
/*
frameRate: 设置双精准度浮点值为每个帧的捕获速率。如果未设置,则每次画布更改时都会捕获一个新帧。如果设置为0,则会捕获单个帧。
*/

轮询绘制 canvas

// JavaScript
let canvasOrigin = document.querySelector('#canvas-originally');
let canvasOriginContext = canvasOrigin.getContext('2d')
let video = document.querySelector('#video-record')getUserMediaPermissions() {// 授权视频设备获取流数据window.navigator.mediaDevices.getUserMedia({video: true}).then(stream => {// 将摄像头返回的流添加的 video 组件的 src 上,srcObject 是一个新属性if ('srcObject' in this.video) {video.srcObject = stream} else {video.src = window.URL.createObjectURL(stream)}	video.onloadedmetadata = (e) => {video.play()canvasDrawLoop()}})
}
canvasDrawLoo() {canvasOriginContext.drawImage(video, 0, 0, 1280, 760);requestAnimationFrame(canvasDrawLoop);
}

初始化媒体录制器

主角MediaRecorder API提供了录制的接口。
参数

名称类型说明
treamstream | DOM将要记录的流,可以是getUserMedia创建的流或者来自
audio、video以及<canvas>DOM元素
optionsObject一个字典对象,包含mimeType(类型)、
audioBitsPerSecond、videoBitsPerSecond、bitsPerSecond

方法

名称参数说明
isTypeSupported()-返回一个Boolean值,来表示设置的MIME类型
是否被当前用户的设备支持。
pause()-暂停媒体录音
requestData()-请求一个从开始到当前接收到的,存储为Blob类型的录音内容。
或者是返回从上一次调用requestData()方法之后的内容)。
调用这个方法后,创建一个记录继续进行,但会出现新的Blob对象
resume()-继续录制之后被暂停的动作。
start()timeslice / Number开始录制媒体
stop()-停止侵权。再次触发dataavailable事件,
返回一个存储Blob内容的录音数据。之后不再记录

Event

名称参数说明
ondataavailable()-该事件可用于获取摄像的媒体资源
(在事件的 data属性中会提供一个可用的Blob对象。)
onstart()-处理 start事件,该事件在媒体开始录制时触发MediaRecorder.start()。
stop()-处理stop事件,该事件会在媒体录制结束时、媒体流(MediaStream)
结束时、或者调用MediaRecorder.stop() (en-US)方法后触发。
·········

参考
代码实现

<canvas id="canvas-originally" :width="cameraInfo.width" :height="cameraInfo.height"></canvas>
// JavaScript
// 用来存放视频 blob 数据
let streams = []
let canvas = document.querySelector('#canvas-originally');
let canvasStream = canvas.captureStream(25) // 该方法返回的是一个 canvas 实时视频捕获的画布// 初始化视频录制器
let options = { mimeType: "video/webm; codecs=vp9" };
let recorder = new MediaRecorder(canvasStream, options)
recorder.start(100)
// 监听获取媒体资源
recorder.ondataavailable = (event) => {streams.push(event.data)
}
recorder.onstop = () => {// 合并 blobs let blob = new Blob(streams, {type: 'video/mp4'})// 生成文件generateFile(blob)// do something
}
recorder.onstart = () => {/*do something*/}
recorder.onerror = (error) => {/*do something*/}

在合并 blob 后可以通过 URL.createObjectURL(blob) 来生成一个 blobUrl 可以在浏览器中预览了,到这里我们的工作已经完成一半了。

4. 生成视频文件

现实场景中我们可能并不是单纯的去录制就OK了,我们要的是将这个视频保存到服务器,这个时候我们就需要将这个视频生成文件上传到服务器,因为这时的视频其实只是一个 blob 数据流,与File还是不同的。
直接上代码。

// JavaScript
generateFile(blob) {let filename = new Date().getTime() + '.mp4';let file = new File([blob], name, {type: 'video/mp4'})
}

是不是感觉很简单,没错,就是这么两行代码。下面介绍一下 File 这个API。

通常情况下, File 对象是来自用户在一个 <input> 元素上选择文件后返回的 FileList 对象,也可以是来自由拖放操作生成的 DataTransfer 对象,或者来自 HTMLCanvasElement 上的 mozGetAsFile() API。在Gecko中,特权代码可以创建代表任何本地文件的File对象,而无需用户交互。

File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。比如说, FileReader, URL.createObjectURL(), createImageBitmap() (en-US), 及 XMLHttpRequest.send() 都能处理 Blob 和 File。

语法

new File(bits, name, options)

参数

名称类型说明
bitsArrayBuffer
ArrayBufferView
Blob
Array
一个包含ArrayBuffer,ArrayBufferView,Blob,
或者 DOMString 对象的 Array — 或者任何这些对象的组合。
这是 UTF-8 编码的文件内容。
nameString文件名称,或者文件路径。
optionsObject包含文件可选属性:{type, lastModified}

属性

名称说明
File.lastModified返回当前 File 对象所引用文件最后修改时间,
自 UNIX 时间起始值(1970年1月1日 00:00:00 UTC)以来的毫秒数。
File.lastModifiedDate返回当前 File 对象所引用文件最后修改时间的 Date 对象。
File.name返回当前 File 对象所引用文件的名字。
File.size返回文件的大小。
File.webkitRelativePath返回 File 相关的 path 或 URL。
File.type返回文件的类型

参考

5. 其他

细心的同学可能发现了,我们生成的视频在本地播放器中无法拖动进度条。
这是个很严重的问题吗?
是的,灰常严重。
会有哪些问题?

  1. 首先产品经理肯定不会同意
  2. 用户体验不好
  3. 如果是要做视频切片处理的话会发现切出来的图片只有一张,别问为什么,因为我出现了。我的理解是,虽然我们录制了很久但始终为一帧,因为我们的进度无法拖动,也就没有时长的概念,导致获取到的视频长度为0,此时就只将视频的第一帧切出来了。

如何解决?
我因为时间问题就没太去研究这块了,找了一个插件 后续会研究一下这块

// JavaScript
// duration 长度可以通过开始录制时间和结束的时间算出来
fixWebmDuration(blob, duration, (fixedBlob) => {let blob = fixedBlob// 将处理后的 blob 生成文件this.generateFile(blob)
});

( 完 )
到这里就完成了视频录制的所有流程。如果在过程中遇到什么问题,可以私信我进行交流。

后续会更新一篇关于录屏的实现,敬请期待!!

前端 桌面共享剖析
前端 音频录制剖析

附完整代码:

<!-- HTML -->
<div class="video-record" v-show="cameraStatus"><div class="canvas-originally-container"><video id="video-record" ref="videoRecord"></video><div class="status" v-if="recorderStatus"></div><img src="../../../public/img/close.png" alt="" class="close-icon" @click="closeCamera"><canvas id="canvas-originally" :width="cameraInfo.width" :height="cameraInfo.height" ref="canvasOrigin"></canvas><div class="start-record" @click="startRecord"><div class="start-record-inner"></div></div></div>
</div>
// JavaScript<script>
const fixWebmDuration = require('../../utils/duration')
export default {name: 'videoFragmentation',data() {return {videoFile: {fileName: '20211009204948_1605318046468.mp4',url: 'http://demo-face-detection.obs.cn-east-3.myhuaweicloud.com/image/20211009204948_1605318046468.mp4'},// 相机状态cameraStatus: false,cameraInfo: {time: 0,width: 1280,height: 760},// 录制的视频播放器video: null,// 视频流列表streams: [],// 当前流数据curStream: null,// 录制实例化对象recorder: null,// 画布canvasOrigin: null,canvasOriginContext: null,// canvas 视频流canvasStream: null,// 录制后上传OBS生成的结果recorderVideo: {file: null,type: 2},// 录制时间recorderTime: 10,// 录制进度recorderProgress: null,// 录制状态recorderStatus: false,loading: null,eventType: 'auto',}},mounted() {this.video = this.$refs.videoRecordthis.canvasOrigin = this.$refs.canvasOriginthis.canvasOriginContext = this.canvasOrigin.getContext('2d')this.canvasStream = this.canvasOrigin.captureStream(25)},methods: {/*** @description: 获取设备摄像头权限* @param  {*}* @return {*}*/getUserMediaPermissions() {if (!window.navigator.mediaDevices.getUserMedia) {return;}// 1. 获取用户摄像头权限window.navigator.mediaDevices.getUserMedia({video: { width: { ideal: 1024 },height: { ideal: 776 }}}).then(stream => {this.curStream = stream// 2. 将摄像头返回的流赋给视频组件if ('srcObject' in this.video) {this.video.srcObject = stream} else {this.video.src = window.URL.createObjectURL(stream)}// 3. 监听数据加载完成this.video.onloadedmetadata = (e) => {// 4. 开始播放,并轮询绘制this.video.play()this.cameraStatus = truethis.canvasDrawLoop()}}).catch(error => {console.log('获取用户 Media 权限失败', error);})},/*** @description: 开始录制* @param  {*}* @return {*}*/startRecord() {this.recorderStatus = truethis.initMediaRecorder(() => {// 关闭摄像头使用this.curStream.getTracks()[0].stop()this.curStream = null})},/*** @description: 生成mp4文件* @param  {*}* @return {*}* @param {*} blob 需要转 file 的 blob 数据*/generateFile(blob) {let name = new Date().getTime()+'.mp4'let file = new File([blob], name, {type: 'video/mp4'})this.recorderVideo.file = file},/*** @description: 初始化视频流记录* @param  {*}* @return {*}*/initMediaRecorder(callback) {let options = { mimeType: "video/webm; codecs=vp9" };// 1. 初始化视频录制this.recorder = new MediaRecorder(this.canvasStream, options);// 2. 获取媒体资源,ondataavailable 函数的回调中将返回每一帧的 blob 流文件this.recorder.ondataavailable = (event) => {this.streams.push(event.data)}this.recorder.start(100)let duration = 0let startTime = 0// 3. 监听开始录制事件this.recorder.onstart = () => {startTime = new Date().getTime()this.recorderProgress = setInterval(() => {// 我的需求是录制10秒,所有这么写的this.cameraInfo.time += 1if (this.cameraInfo.time == this.recorderTime) {this.recorder.stop()}}, 1000)}// 4. 监听录制失败this.recorder.onerror = function (error) {console.log('error', error);}// 5. 监听录制结束,结束后通过 Blob将流文件整合成类型为 mp4 的视频 blob 流this.recorder.onstop = (event) => {if (this.eventType == 'close') {this.resetCamera()return}duration = new Date().getTime() - startTimelet blob = new Blob(this.streams, {type: 'video/mp4'})fixWebmDuration(blob, duration, (fixedBlob) => {blob = fixedBlobthis.recorderUrl = URL.createObjectURL(blob)console.log('recorderUrl', this.recorderUrl);// 6. 将 blob 转化为 File 文件this.generateFile(blob)callback()this.resetCamera()});}},/*** @description: 在 canvas 上轮询绘制当前视频* @param  {*}* @return {*}*/canvasDrawLoop() {this.canvasOriginContext.drawImage(this.video, 0, 0, this.cameraInfo.width, this.cameraInfo.height);requestAnimationFrame(this.canvasDrawLoop);},/*** @description: 重置相机* @param  {*}* @return {*}*/resetCamera() {this.cameraInfo.time = 0this.streams = []this.curStream && this.curStream.getTracks()[0].stop()this.canvasOriginContext && this.canvasOriginContext.clearRect(0, 0, this.cameraInfo.width, this.cameraInfo.height);this.recorderStatus = falsethis.cameraStatus = falsethis.eventType = 'auto'this.recorder = nullclearInterval(this.recorderProgress)},/*** @description: 关闭相机* @param  {*}* @return {*}*/closeCamera() {this.eventType = 'close'if (this.recorder && this.recorder.stop) {this.recorder.stop()} else {this.resetCamera()}}}
}
</script>

音视频三部曲

前端 音频录制剖析
前端 视频录制剖析
前端 桌面共享剖析

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

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

相关文章

python实现屏幕视频录制_用Python来做一个屏幕录制工具

一、写在前面 作为一名测试,有时候经常会遇到需要录屏记录自己操作,方便后续开发同学定位。以前都是用screentogif来录屏制作成动态图,偶尔的机会看到python也能实现。那就赶紧学习下。 二、效果展示 三、知识串讲 这次要讲的东西可能比较多了,涉及到pyqt5 gui软件的制作、…

ffmpeg视频录制

录制视频的基本步骤是&#xff1a; 1. 初始化ffmpeg的基本对象&#xff0c;并将这些对象关联起来&#xff0c;然后打开文件并写入文件头。 2. 编码视频&#xff0c;并将编码后数据存写到文件中。 3. 写入文件尾&#xff0c;并清理ffmpeg对象。 首先&#xff0c;需要初始化f…

html 视频录制插件,网页视频录制插件工具(浏览器插件 Recorder)

网页视频录制插件工具(浏览器插件 Recorder)Loom Video Recorder是一款视频录制工具扩展插件&#xff0c;可以一键录制浏览器的单个标签页,&#xff0c;也支持摄像头录制&#xff0c;能够将录制的视频内容下载道本地&#xff0c;有需要的用户不要错过了&#xff0c;欢迎下载使用…

如何录制一个高品质的网页直播视频,录制在线直播视频的快速教程

怎么录制网页上的直播视频&#xff1f;怎么录制网页上正在播放的视频&#xff1f;怎么把别人在线直播的视频快速录制下来&#xff1f; 今天就教大家用超级捕快来快速录制一个高品质的网页直播视频&#xff0c;高清无损&#xff0c;没有水印。不仅能录制在线直播视频&#xff0…

浏览器怎么录制网页视频?3种网页视频录制方法

我们每天都会在浏览器上观看大量的视频&#xff0c;尤其是在爱奇艺、腾讯、哔哩哔哩等网页上。有时候就会观看到一些精彩的视频画面&#xff0c;就想要将这些画面给下载。 那怎么把网页视频录制下来&#xff1f;今天本文就给大家分享3种有效的网页视频录制方法&#xff0c;有需…

k8s进阶5——AppArmor、Seccomp、ImagePolicyWebhook

文章目录 一、AppArmor限制容器对资源访问1.1 实现步骤1.1.1 定义策略1.1.2 加载策略1.1.3 引用策略 2.2 案例 二、Seccomp 限制容器进程系统调用案例一&#xff1a;使用自定义策略案例二&#xff1a;使用容器运行时默认策略 三、动态准入控制Webhook3.1 ImagePolicyWebhook控制…

数组如何转对象

最近做项目的时候需要把拿到的数组转为对象使用 let arr [{id:1,value:正式},{id:2,value:非正式}]//首先定义一个空对象let obj{}//循环遍历数组arr.forEach(item>{//把id作为对象的键 把value作为对象的值值obj[item.id]item.value})console.log(obj);运行结果如下 这样…

对象转为数组

对象转成数组 在开发中&#xff0c;前端处理数据&#xff0c;很多时候都会用到把对象转成数组&#xff0c;话不多说&#xff0c;咱们直接上代码 // 对象转数组&#xff0c;想要转成什么字段根据需求 function fmObjToArr (data) {let arr []for (let key in data) {arr.push…

js数组对象转对象

将数组对象的数据转换为普通对象键值对key:value的形式 let arr [{id:1,employeeNo:110,name:张三},{id:2,employeeNo:111,name:李四},{id:3,employeeNo:112,name:王五},{id:4,employeeNo:113,name:章雨},{id:5,employeeNo:114,name:小红}, ] let newObj {} arr.map(item >…

JS对象转数组

一、JS对象转数组方法如下(不考虑for相关遍历): 1.[...res] 2.Array.from(res) 3.Object.values(res) 4. Array.prototype.slice.call(res) let obj {name:李四, age:22, gender:男} //可枚举 let obj2 {0:李四, 1:22, 2:男, length:3} //可枚举 伪数组 let set new Set([…

3种JavaScript 对象转数组的方法

来源 | https://www.fly63.com 我们在项目开发的时候&#xff0c;有时需要将js对象转换为数组&#xff0c;下面小编给大家具体演示一下怎么转换&#xff0c;主要是介绍一些常用、简洁的转换方法。 比如JavaScript对象如下&#xff1a; let obj {name: 前端,url: https://www.w…

对象数据如何转化成数组

文章目录 前言一、后端给的数据二、如何转换三、最终结果 前言 项目中后端给的数据有时候不可能完全符合前端的数据要求&#xff0c;比如element-uivue项目组合中&#xff0c;下拉框数据是数组的形式。然而后端同事给的确是对象数据&#xff0c;当然你可以和后端协商改成你想要…

json对象转对象数组

对象的两种取值方式 let obj {name: wan}; console.log(obj.name); //wan这是最普通的一种方式&#xff0c;还有一种方式我们用的不太多&#xff0c;就是使用[]包住属性名取值 let obj {name: wan}; console.log(obj[name]); //wan将对象转化为数组 let obj {未完成:5, 已…

将数组转换为对象

本文翻译自&#xff1a;Convert Array to Object What is the best way to convert: 转换的最佳方式是什么&#xff1a; [a,b,c]to: 至&#xff1a; {0: a,1: b,2: c }#1楼 参考&#xff1a;https://stackoom.com/question/Hghl/将数组转换为对象 #2楼 Quick and dirty #2…

js把对象转换成数组

1.把类似数组的对象转换成数组 【1】什么是类似数组的对象 比如&#xff1a; let arrayLike {0:"z",1:"y",2:"k",length:3 }; 本质是有length属性&#xff0c;可以类似数组的获取元素的方式arrayLike[0]、arrayLike[1]去获取元素&#xff0…

js数组转对象

js数组转对象 1.js var list {}; var arr ["123","456","789"]; for (var key in arr) {list[key] arr[key]; } console.log(list); 效果&#xff1a;

JS 中的类数组对象如何转换为数组?

大家好&#xff0c;我是前端西瓜哥&#xff0c;今天说一下 JS 的类数组对象是什么&#xff0c;以及如何将类数组对象转为数组。 类数组对象是什么&#xff1f; 类数组对象&#xff0c;就是含有 length 属性的对象&#xff0c;但这个对象不是数组。 通常来说还会有 0 &#x…

js中对象转数组

今天睡前看到小组群里贴了这么一张图&#xff0c;印象中曾经面试的时候好像也是遇到过&#xff0c;对于大佬们来说这肯定是很基础的一道题&#xff0c;在此分享给正在学习前端和正在面试的小伙伴们。 这里我用fo……in……实现了两种取值方式的改变 let obj {json:0,production…

(十六)创建Lua脚本模板

Unity里能创建 c#脚本模板&#xff0c;但是如果我想创建Lua脚本模板怎么办呢&#xff1f;拓展一下编辑器吧。 设置一下Lua脚本的模板地址 &#xff1a; Assets/Editor/Lua/Template/lua.lua using UnityEngine; using UnityEditor; using System; using System.IO; using Sys…

Unity(二十):创建自定义脚本模板

编辑器内部自定义脚本存储位置展示 创建自己的自定义脚本 using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.ProjectWindowCallback; using UnityEngine;internal class C…