前端视频录制方案

是这样的,现在有一个 canvas,我需要录制 canvas 上的变化。

有一个立即可以想到的方案,定时获取 canvas 的图片数据(getImageData),然后通过某些库,比如 ffmpeg.wasm 合成视频。这个方案确实是可行的,虽然我没实践,之所以不实践,是因为暂时不想引入 ffmpeg.wasm 这个库,毕竟这个库挺大的。

除了 ffmpeg,还可以通过 canvas.captureStream 和 MediaRecorder 实现视频录制。

startVideoRecord() {
  if (this.konvaInstance) {
    const canvas = this.konvaInstance.getStage().content
      .children[0] as HTMLCanvasElement
    const stream = canvas.captureStream()
    const recorder = new MediaRecorder(stream, {
      mimeType: 'video/webm',
    })
    this.videoData = []
    this.videoRecorder = undefined
    this.videoBlob = undefined
    this.videoStartTime = Date.now()
    recorder.ondataavailable = (event) => {
      if (event.data && event.data.size) {
        console.log(event.data)
        this.videoData.push(event.data)
      }
    }
    recorder.onstop = async () => {
      this.videoBlob = new Blob(this.videoData, { type: 'video/webm' })
    }
    recorder.onerror = (e) => {
      console.error(e)
    }
    this.videoRecorder = recorder

    recorder.start()

    return recorder
  }
}

录制的代码差不多就这些,很简单,需要结束的时候只需要调用 recorder.stop() 就可以了。但这样得到的视频文件是有问题的,比如没有 duration,播放器播放的时候无法正确显示时间,也没法拖动进度条。这是因为元数据在录制结束之前就写进数据流的开始位置了,结束之后浏览器也没有修正,所以这实际上是浏览器的 bug,但浏览器开发人员并不准备修复。https://stackoverflow.com/questions/50586612/seeking-is-not-working-in-recorded-video-with-mediarecorder-api/55054346#55054346

关于这个问题的第三方库也是有的,比如 webm-duration-fix 之类的,这种库不止一个,我用了两个库,引入阶段报错,一个修复阶段报错,后来我是用的 ts-ebml 这个库。

/**
 * 修复 webm 视频没有 duration 的问题
 * 但修复后的视频还是有问题,应该是浏览器录制的数据就有问题,暂时无解
 * https://stackoverflow.com/questions/50586612/seeking-is-not-working-in-recorded-video-with-mediarecorder-api/55054346#55054346
 * @param blob
 * @returns
 */
export async function getSeekableBlobAsync(blob: Blob) {
  const { Buffer } = await import('buffer')
  // https://github.com/legokichi/ts-ebml/issues/25
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  window!.Buffer = Buffer
  const EBML = await import('ts-ebml')
  const arrayBuffer = await blob.arrayBuffer()
  const reader = new EBML.Reader()
  const decoder = new EBML.Decoder()
  const tools = EBML.tools

  const ebmlElms = decoder.decode(arrayBuffer)
  ebmlElms.forEach((element) => {
    reader.read(element)
  })
  reader.stop()
  const refinedMetadataBuf = tools.makeMetadataSeekable(
    reader.metadatas,
    reader.duration,
    reader.cues
  )
  const body = arrayBuffer.slice(reader.metadataSize)
  const newBlob = new Blob([refinedMetadataBuf, body], {
    type: blob.type,
  })

  return newBlob
}

这个库使用过程中有两个问题,一个是 _tools is undefined 这个报错。解决方案是配置 alias。https://github.com/legokichi/ts-ebml/issues/48

另一个问题是 Buffer 不存在,解决方案是引入 Buffer,挂到 window 上。https://github.com/legokichi/ts-ebml/issues/25

这样一来就可以生成 duration 正确的视频了,但是,在播放的时候还是有点问题,比如内容不对。我录制的一个视频后半段内容就没变过,但实际上 canvas 一直在变。

如此一来,我放弃了该方案。这个功能也直接废掉吧,反正内容简单,我也不是一定要用视频,直接将几帧图片合成 GIF 就可以了,虽然说合成速度比较慢(❁´◡`❁)。

等哪天我把那个 GIF 库优化优化。