草庐IT

前端 视频录制剖析

很菜的小白在分享 2023-07-13 原文

前端 视频录制剖析

作者:@ 很菜的小白在分享
时间: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.videoRecord
    this.canvasOrigin = this.$refs.canvasOrigin
    this.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 = true
            this.canvasDrawLoop()
          }
        }).catch(error => {
          console.log('获取用户 Media 权限失败', error);
        })
    },
    /**
     * @description: 开始录制
     * @param  {*}
     * @return {*}
     */
    startRecord() {
      this.recorderStatus = true
      this.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 = 0
      let startTime = 0

      // 3. 监听开始录制事件
      this.recorder.onstart = () => {
        	startTime = new Date().getTime()
        	this.recorderProgress = setInterval(() => {
	        	// 我的需求是录制10秒,所有这么写的
	          	this.cameraInfo.time += 1
	          	if (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() - startTime
        
        let blob = new Blob(this.streams, {
          type: 'video/mp4'
        })
        
        fixWebmDuration(blob, duration, (fixedBlob) => {
          blob = fixedBlob
          this.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 = 0
      this.streams = []
      this.curStream && this.curStream.getTracks()[0].stop()
      this.canvasOriginContext && this.canvasOriginContext.clearRect(0, 0, this.cameraInfo.width, this.cameraInfo.height);
      this.recorderStatus = false
      this.cameraStatus = false
      this.eventType = 'auto'
      this.recorder = null
      clearInterval(this.recorderProgress)
    },
    /**
     * @description: 关闭相机
     * @param  {*}
     * @return {*}
     */
    closeCamera() {
      this.eventType = 'close'
      if (this.recorder && this.recorder.stop) {
        this.recorder.stop()
      } else {
        this.resetCamera()
      }
    }
  }
}
</script>

音视频三部曲

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

有关前端 视频录制剖析的更多相关文章

  1. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  2. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  3. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  4. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  5. ruby - 如何更改此正则表达式以从未指定 v 参数的 Youtube URL 获取 Youtube 视频 ID? - 2

    目前我正在使用这个正则表达式从YoutubeURL中提取视频ID:url.match(/v=([^&]*)/)[1]我怎样才能改变它,以便它也可以从这个没有v参数的YoutubeURL获取视频ID:http://www.youtube.com/user/SHAYTARDS#p/u/9/Xc81AajGUMU感谢阅读。编辑:我正在使用ruby​​1.8.7 最佳答案 对于Ruby1.8.7,这就可以了。url_1='http://www.youtube.com/watch?v=8WVTOUh53QY&feature=feedf'url

  6. ruby-on-rails - 在 Rails 应用程序的前端获取实时日志 - 2

    在Rails3.x应用程序中,我正在使用net::ssh并向远程pc运行一些命令。我想向用户的浏览器显示实时日志。比如,如果两个命令在net中运行::ssh执行即echo"Hello",echo"Bye"被传递然后"Hello"应该在执行后立即显示在浏览器中。这是代码我在ruby​​onrails应用程序中使用ssh连接和运行命令Net::SSH.start(@servers['local'],@machine_name,:password=>@machine_pwd,:timeout=>30)do|ssh|ssh.open_channeldo|channel|channel.requ

  7. ruby - 如何在转换器插件中访问页面属性(YAML 前端) - 2

    我正在为Jekyll编写一个转换器插件,需要访问一些页眉(YAML前端)属性。只有内容被传递给主要的转换器方法,似乎无法访问上下文。例子:moduleJekyllclassUpcaseConverter关于如何在转换器插件中访问页眉数据有什么想法吗? 最佳答案 基于Jekyll源代码,无法在转换器中检索YAML前端内容。根据您的情况,我看到了两种可行的解决方案。您的文件扩展名可以具有足够的描述性,以提供您本应包含在前言中的信息。看起来Converter插件的设计就是这么基本的。如果修改Jekyll是一个选项,您可以更改Convert

  8. ruby-on-rails - 如何从服务器目录制作 Paperclip 进程文件? - 2

    我想对服务器目录中的所有文件运行Paperclip。基本上,我想允许用户将一些文件通过FTP传输到我的网络服务器,然后我可以手动运行rake任务让Paperclip处理所有文件(调整图像大小、更新数据库等)。我该怎么做? 最佳答案 我不确定我是否理解您的问题-您是在询问远程运行rake任务还是如何导入图像?在后一种情况下有一个答案。首先你需要一些模型来保存图像和一些其他数据,像这样:classPicture{:thumb=>"100x100>",:big=>"500x500>"}end您可以在lib/tasks文件夹中创建简单的ra

  9. 续集来了丨UI自动化测试(二):带视频,实在RPA高效进行web项目UI自动化测试 - 2

    一、什么是web项目ui自动化测试?通过测试工具模拟人为操控浏览器,使软件按照测试人员的预定计划自动执行测试的一种方式,可以完成许多手工测试无法完成或者不易实现的繁琐工作。正确使用自动化测试,可以更全面的对软件进行测试,从而提高软件质量进而缩短迭代周期。二、构建测试用例的“九部曲”(一)创建流程包划分功能模块日常测试活动中,都会根据功能模块进行拆分,所以在设计器中我们可以通过创建流程包的方式来拆分需要测试的功能模块,如下图中操作创建一个电脑流程包并且取名为对应的功能模块名称,如果有多个功能模块就创建多个对应的流程包,实在RPA设计器有易用的图形可视化界面,方便管理较多的功能模块。(二)在流程包

  10. Java调用ffmpeg处理视频,并记录下遇到的坑 - 2

    目录需求基于JavaCV跨平台执行ffmpeg命令[^1]坑一内存不足坑二多个ffmpeg进程并行导致IO负载大,进而导致ioerror?坑三使用Java操作ffmpeg时,有时会卡死坑四Process的waitFor死锁问题及解决办法需求给透明背景的视频自动叠加一张背景图片基于JavaCV跨平台执行ffmpeg命令1我测试发现的本需求的最小依赖:dependency>groupId>org.bytedecogroupId>artifactId>ffmpeg-platform-gplartifactId>version>5.0-1.5.7version>dependency>核心代码:Stri

随机推荐