前端实现一个视频录制功能?

前言

  不久前写了一个视频聊天室的项目,想到一些有趣的功能需求想去实现,视频录制就是其中之一,话不多说,下面就开始介绍及实现。提前看效果

WebAPI

  每次实现有趣的功能都要了解新的WebAPI,这次也不例外,接下来将逐步介绍需要哪些核心API来实现视频录制。

MediaRecorder

MediaRecorderMediaStream Recording API 提供的用来进行媒体轻松录制的接口,他需要通过调用 MediaRecorder() 构造方法进行实例化。

  首先是使用MediaRecorder来将MediaStream(媒体流)转为Blob,然后将Blob数据保存到磁盘。

  但是MediaStream从哪来?这个要结合实际业务场景确定,在当前这个业务场景下,需要视频录制的是包含所有video元素的div容器,有什么手段可以将DOM元素转为媒体流呢?

HTMLCanvasElement.captureStream()

HTMLCanvasElement.captureStream() 方法返回的 CanvasCaptureMediaStream 是一个实时视频捕获的画布。

HTMLCanvasElement.captureStream()接口返回一个媒体流,实时捕获canvas画布的内容,但我们需要的是从div获取,如何做到?可以将div容器实时转换为canvas,这可以借助html2canvas实现,由于html2canvas比较全面,它考虑的事情更多,所以耗时相对较长,也就导致每一帧的间隔时间较长,略显卡顿,所以我就了解了下html2canvas的实现原理,根据业务场景实现了个简易的html2canvas

js 复制代码
function html2canvas(el) {
  const rect = el.getBoundingClientRect();
  const { width, height, left, top } = rect
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  fillCanvas(ctx, el, {
    parentWidth: width,
    parentHeight: height,
    parentLeft: left,
    parentTop: top,
  });
  return canvas;
}

function fillCanvas(ctx, el, options) {
  const { parentWidth, parentHeight, parentLeft, parentTop } = options;
  const rect = el.getBoundingClientRect();
  const { width, height, left, top } = rect
  if (left + width < parentLeft || top + height < parentTop || left > parentLeft + parentWidth || top > parentTop + parentHeight) {
    next()
    return
  }
  ctx.save();
  const tag = el.tagName;
  switch(tag) {
    case 'IMG':
    case 'SVG':
    case 'VIDEO':
    case 'CANVAS':
      ctx.drawImage(el, left - parentLeft, top - parentTop, width, height);
      return
    default:
      const { backgroundColor } = getComputedStyle(el)
      ctx.fillStyle = backgroundColor;
      ctx.fillRect(left - parentLeft, top - parentTop, width, height);
      break
  }
  ctx.restore();
  next()
  function next() {
    Array.prototype.forEach.call(el.children, (child) => {
      fillCanvas(ctx, child, options)
    })
  }
}

实现

  下面这段代码,接收一个DOM元素作为参数,创建一个canvas用来创建并捕获媒体流,无限执行html2canvas生成canvas并填充给用来捕获媒体流的canvas,将创建的MediaStream传递给MediaRecorder构造函数进行录制,监听MediaRecorder.ondataavailable事件收集Blob碎片,监听MediaRecorder.onstop事件停止录制合并Blob转为File并保存。

js 复制代码
function useVideoRecorder(el) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  let animationFrameId = null
  animationFrameId = requestAnimationFrame(function createCanvas() {
    animationFrameId = requestAnimationFrame(createCanvas)
    const htmlCanvas = html2canvas(el)
    const { width, height } = htmlCanvas
    canvas.width = width
    canvas.height = height
    ctx.save()
    ctx.drawImage(htmlCanvas, 0, 0, width, height)
    ctx.restore()
  })

  const stream = canvas.captureStream(25);
  const recorder = new MediaRecorder(stream);

  let chunks = []
  recorder.onstop = function() {
    cancelAnimationFrame(animationFrameId)
    const blob = new Blob(chunks, { type: 'video/mp4' });
    saveFile(blobToFile(blob, '视频录制'))
    chunks = []
  }
  
  recorder.ondataavailable = function(e) {
    chunks.push(e.data);
  }
  
  recorder.start();
  return recorder;
}
js 复制代码
function blobToFile(blob, fileName) {
  return new File([blob], fileName, { type: blob.type});
}

function saveFile(file) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(file);
  a.download = file.name;
  a.click();
  URL.revokeObjectURL(a.href);
}

音频&背景

  以上代码实现的录制并没有声音,而且当未给DOM元素设置background-color时,录制的视频是没有背景色的(透明色),改造useVideoRecorder实现添加录制音频以及视频背景色。

js 复制代码
function useVideoRecorder(el, options) {
  const { background, audioTracks } = options || {}
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  let animationFrameId = null
  animationFrameId = requestAnimationFrame(function createCanvas() {
    animationFrameId = requestAnimationFrame(createCanvas)
    const htmlCanvas = html2canvas(el)
    const { width, height } = htmlCanvas
    canvas.width = width
    canvas.height = height
    ctx.save()
    if (background) {
      ctx.fillStyle = background
      ctx.fillRect(0, 0, width, height)
    }
    ctx.drawImage(htmlCanvas, 0, 0, width, height)
    ctx.restore()
  })

  const stream = canvas.captureStream(25);
  if (audioTracks) {
    audioTracks.forEach(t => stream.addTrack(t))
  }
  const recorder = new MediaRecorder(stream);

  let chunks = []
  recorder.onstop = function() {
    cancelAnimationFrame(animationFrameId)
    const blob = new Blob(chunks, { type: 'video/mp4' });
    saveFile(blobToFile(blob, '视频录制'))
    chunks = []
  }
  
  recorder.ondataavailable = function(e) {
    chunks.push(e.data);
  }
  
  recorder.start();
  return recorder;
}
  • useVideoRecorder新增一个参数options,包含backgroundaudioTracks属性,分别是背景和音频轨道数组;
  • 再稍加改造,background也可以是一个图片,这样就可以添加图片作为背景了,因为业务场景中用不到,这里就不多描述了。

录制效果

点击看完整效果 在线体验

最后

  以上只介绍了实现视频录制的核心,至于封装组件及UI交互就不多描述了,这取决于你项目的UI设计。

扩展阅读

参考

未经作者授权 禁止转载

相关推荐
程序员爱钓鱼4 小时前
Rust 变量与不可变性:为什么默认不能修改变量?
前端·后端·rust
随风一样自由4 小时前
【前端工程化】前端工程化解耦实践:从问题分析到架构重构
前端·重构·架构
vortex54 小时前
Web 渗透测试:未授权与越权漏洞全流程挖掘思路
前端
我有满天星辰5 小时前
【前端模块化】前端模块化演进之路:从脚本堆砌到标准统一
前端
我有满天星辰5 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
随风一样自由13 小时前
【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?
前端·javascript·vue3·jquery
IMPYLH14 小时前
HTML 的 <data> 元素
前端·html
YHHLAI14 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
kyriewen16 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
前端H17 小时前
微前端 v3 架构升级:从加载隔离到状态协同
前端·架构·状态模式