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

前言

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

WebAPI

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

MediaRecorder

MediaRecorder 是 MediaStream 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,包含background、audioTracks属性,分别是背景和音频轨道数组;
  • 再稍加改造,background也可以是一个图片,这样就可以添加图片作为背景了,因为业务场景中用不到,这里就不多描述了。

录制效果

点击看完整效果 在线体验

最后

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

扩展阅读

参考

未经作者授权 禁止转载

相关推荐
时间的情敌5 小时前
Cesium 流动管线、管道动画、管线流向仿真,管网、油气、水利孪生高频需求
前端·交互·cesium
wangxiaoxuan2177 小时前
SLAM 的基本结构:前端、后端、回环与地图优化
前端·机器人·科沃斯八界
Agent世界10 小时前
用 CDP 做发布自动化:四道机器闸门与产物验收
前端·javascript
FungLeo10 小时前
成为全栈·Vue 管理后台篇·编辑器图片上传:从文件选择到文章附件关联
前端·vue.js·编辑器·markdown·文件上传·formdata·附件管理
某哈压力大10 小时前
SSH 突然 Connection reset?不是密钥坏了,是 OpenSSL 把 sshd 干趴了
前端·node.js·命令行
程序员大阳10 小时前
副队长ES6+教程(3)--解构赋值
前端·javascript·es6·赋值·解构
神膘护体小月半11 小时前
css - 元素背景图由 hover 触发,初次加载时闪动的解决办法
前端·javascript·css
程序员大阳11 小时前
副队长ES6+教程(2)--let 与 const
前端·javascript·es6·const·let·var
deli00711 小时前
写满 200GB 后 SSD 为什么掉到 95MB/s?SLC 缓存模型实测
前端
曦樂~12 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript