将HTML5 Canvas序列转换为视频文件

29
我想将HTML5画布中的动画转换为可上传到YouTube的视频文件。是否有屏幕捕获API或其他可以让我以编程方式执行此操作的工具?
6个回答

49

回到2020年

使用MediaRecorder API解决了这个问题,它专门用于完成此任务等其他任务。

以下是一种解决方案,可以记录X毫秒的画布视频,您可以通过按钮UI扩展它以开始、暂停、恢复、停止、生成URL。

function record(canvas, time) {
    var recordedChunks = [];
    return new Promise(function (res, rej) {
        var stream = canvas.captureStream(25 /*fps*/);
        mediaRecorder = new MediaRecorder(stream, {
            mimeType: "video/webm; codecs=vp9"
        });
        
        //ondataavailable will fire in interval of `time || 4000 ms`
        mediaRecorder.start(time || 4000);

        mediaRecorder.ondataavailable = function (event) {
            recordedChunks.push(event.data);
             // after stop `dataavilable` event run one more time
            if (mediaRecorder.state === 'recording') {
                mediaRecorder.stop();
            }

        }

        mediaRecorder.onstop = function (event) {
            var blob = new Blob(recordedChunks, {type: "video/webm" });
            var url = URL.createObjectURL(blob);
            res(url);
        }
    })
}

如何使用:

const recording = record(canvas, 10000)
// play it on another video element
var video$ = document.createElement('video')
document.body.appendChild(video$)
recording.then(url => video$.setAttribute('src', url) )

// download it
var link$ = document.createElement('a')
link$.setAttribute('download','recordingVideo') 
recording.then(url => {
 link$.setAttribute('href', url) 
 link$.click()
})



2
不用客气。一个帧可能可以使用 canvas.toDataURL("image/png"); 来完成。一个有趣的问题是是否可以将视频分解成一系列图片。 - pery mimon
2
这个能导出其他视频格式吗?还是只能使用webm格式? - Crashalot
尝试并告诉我们 :) - pery mimon
嗨,@pery mimon,又见面了。你的回答给了我很好的启示,谢谢。我的第一个示例是你想要的吗? - Carson
1
注意,在Firefox上,我使用mimeType:"video/webm;codecs:vp9"使其工作。 - cdrini
显示剩余5条评论

17

3
除IE外,所有浏览器现在都支持captureStreamPery Mimon的答案甚至提供了完整的实现示例。 - Nino Filiu

12

有一个名为whammy的库声称可以使用JavaScript从静态图片生成webm视频:
http://antimatter15.com/wp/2012/08/whammy-a-real-time-javascript-webm-encoder/

请注意,它存在一些限制(这是可以预料的)。该编码器基于当前仅在Chrome中支持的webp图像格式。这意味着,除非您找到一种将要使用的图像编码为webp图像的方法(请参见this link),否则您无法在其他浏览器中进行编码。

除此之外,没有办法使用本机浏览器API从图像中创建视频文件。


4

FileSaver.js + 命令行中的 ffmpeg

使用FileSaver.js,我们可以将每个画布帧下载为 PNG 格式:从 Blob 保存到本地文件

然后,我们只需要使用命令行中的 ffmpeg 将 PNG 转换为任何视频格式:如何使用 FFmpeg 从图像创建视频?

Chromium 75 会询问您是否允许其保存多个图像。一旦您选择允许,它就会自动将图像一个接一个地下载到您的下载文件夹中,并以 0.png1.png 等命名。

在 Firefox 68 中也可以使用,但效果较差,因为浏览器会打开一堆“是否要保存此文件”的窗口。它们确实有一个“针对类似下载执行相同操作”的弹出窗口,但您必须迅速选择它并按下回车键,否则它会弹出新的窗口!

要停止它,您必须关闭选项卡,或添加一个停止按钮和一些 JavaScript 逻辑。

var canvas = document.getElementById("my-canvas");
var ctx = canvas.getContext("2d");
var pixel_size = 1;
var t = 0;

/* We need this to fix t because toBlob calls are asynchronous. */
function createBlobFunc(t) {
  return function(blob) {
    saveAs(blob, t.toString() + '.png');
  };
}

function draw() {
    console.log("draw");
    for (x = 0; x < canvas.width; x += pixel_size) {
        for (y = 0; y < canvas.height; y += pixel_size) {
            var b = ((1.0 + Math.sin(t * Math.PI / 16)) / 2.0);
            ctx.fillStyle =
                "rgba(" +
                (x / canvas.width) * 255 + "," +
                (y / canvas.height) * 255 + "," +
                b * 255 +
                ",255)"
            ;
            ctx.fillRect(x, y, pixel_size, pixel_size);
        }
    }
    canvas.toBlob(createBlobFunc(t));
    t++;
    window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);
<canvas id="my-canvas" width="512" height="512" style="border:1px solid black;"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>

GitHub上游

这里有一个使用它的图像到GIF输出: https://askubuntu.com/questions/648244/how-do-i-create-an-animated-gif-from-still-images-preferably-with-the-command-l

enter image description here

如果FPS太高,则会跳过帧

可以通过减小上述演示中画布的大小以加快速度来观察到这一点。在32x32下,我的Chromium 77以大约10个文件的块下载,并在50个文件之间跳过约50个文件...

不幸的是,没有办法等待下载完成...在 FileSaver.js 中保存文件后关闭窗口

所以我唯一能想到的解决办法就是如果您具有高帧率,则限制帧率...使用requestAnimationFrame控制帧率?这里是一个现场演示:https://cirosantilli.com/#html-canvas

也许有一天有人会回答:

然后我们就能直接下载视频了!

如果你决定不使用浏览器,这里有一个OpenGL版本: 如何使用GLUT/OpenGL渲染到文件?

在Ubuntu 19.04中测试通过。


2
这应该是新的最佳答案。 - Isaac Dozier
1
@IsaacDozier 我有法律义务同意 :-) 来自 https://dev59.com/J2Ik5IYBdhLWcg3we-L5#62065826 的 MediaRecorder 也看起来很有趣,不过我想知道它是否可以捕获单个图像。 - Ciro Santilli OurBigBook.com
感谢您提供的详细答案。您知道这种架构是否会影响视频质量吗?用户可以使用HTML5画布编辑器来为文本添加动画,上传多个视频格式(.mp4、.mov、.avi、.wmv)和音频轨道;画布将导出WebM格式;服务器使用FFmpeg将WebM转换为多个格式(.mp4、.mov、.avi、.wmv)。 - Crashalot
@Crashalot 我不知道 :-) 我不认为会有影响。但是一定要查看另一个答案中的MediaRecorder API。 - Ciro Santilli OurBigBook.com

1

1

纯JavaScript,没有其他第三方包。

如果您有一个视频并想要提取一些帧,可以尝试以下方法:

class Video2Canvas {
  /**
   * @description Create a canvas and save the frame of the video that you are giving.
   * @param {HTMLVideoElement} video
   * @param {Number} fps
   * @see https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
   * */
  constructor(video, fps) {
    this.video = video
    this.fps = fps
    this.canvas = document.createElement("canvas");
    [this.canvas.width, this.canvas.height] = [video.width, video.height]
    document.querySelector("body").append(this.canvas)
    this.ctx =  this.canvas.getContext('2d')
    this.initEventListener()
  }

  initEventListener() {
    this.video.addEventListener("play", ()=>{
      const timeout = Math.round(1000/this.fps)
      const width = this.video.width
      const height = this.video.height
      const recordFunc = ()=> {
        if (this.video.paused || this.video.ended) {
          return
        }
        this.ctx.drawImage(this.video, 0, 0, width, height)
        const frame = this.ctx.getImageData(0, 0, width, height)
        // ... // you can make some modifications to change the frame. For example, create the grayscale frame: https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation

        //  Below is the options. That saves each frame as a link. If you wish, then you can click the link to download the picture.
        const range = document.createRange()
        const frag = range.createContextualFragment('<div><a></a></div>')
        const tmpCanvas = document.createElement('canvas')
        tmpCanvas.width = this.canvas.width
        tmpCanvas.height = this.canvas.height
        tmpCanvas.getContext('2d').putImageData(frame, 0, 0)
        const a = frag.querySelector('a')
        a.innerText = "my.png"
        a.download = "my.png"
        const quality = 1.0
        a.href = tmpCanvas.toDataURL("image/png", quality)
        a.append(tmpCanvas)
        document.querySelector('body').append(frag)
        setTimeout(recordFunc, timeout)
      }
      setTimeout(recordFunc, timeout)
    })
  }
}
const v2c = new Video2Canvas(document.querySelector("video"), 1)
<video id="my-video" controls="true" width="480" height="270" crossorigin="anonymous">
  <source src="http://jplayer.org/video/webm/Big_Buck_Bunny_Trailer.webm" type="video/webm">
</video>

如果你想编辑视频(例如,取5~8秒+12~15秒然后创建一个新的视频),你可以尝试。

class CanvasRecord {
  /**
   * @param {HTMLCanvasElement} canvas
   * @param {Number} fps
   * @param {string} mediaType: video/webm, video/mp4(not support yet) ...
   * */
  constructor(canvas, fps, mediaType) {
    this.canvas = canvas
    const stream = canvas.captureStream(25) // fps // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream
    this.mediaRecorder = new MediaRecorder(stream, { // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder
      mimeType: mediaType
    })
    this.initControlBtn()

    this.chunks = []
    this.mediaRecorder.ondataavailable = (event) => {
      this.chunks.push(event.data)
    }
    this.mediaRecorder.onstop = (event) => {
      const blob = new Blob(this.chunks, {
        type: mediaType
      })
      const url = URL.createObjectURL(blob)

      //  Below is a test code for you to know you are successful. Also, you can download it if you wish.
      const video = document.createElement('video')
      video.src = url
      video.onend = (e) => {
        URL.revokeObjectURL(this.src);
      }
      document.querySelector("body").append(video)
      video.controls = true
    }
  }

  initControlBtn() {
    const range = document.createRange()
    const frag = range.createContextualFragment(`<div>
    <button id="btn-start">Start</button>
    <button id="btn-pause">Pause</button>
    <button id="btn-resume">Resume</button>
    <button id="btn-end">End</button>
    </div>
    `)
    const btnStart = frag.querySelector(`button[id="btn-start"]`)
    const btnPause = frag.querySelector(`button[id="btn-pause"]`)
    const btnResume  = frag.querySelector(`button[id="btn-resume"]`)
    const btnEnd   = frag.querySelector(`button[id="btn-end"]`)
    document.querySelector('body').append(frag)
    btnStart.onclick = (event) => {
      this.chunks = [] // clear
      this.mediaRecorder.start() // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start
      console.log(this.mediaRecorder.state) // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/state

    }

    btnPause.onclick = (event) => { // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/pause
      this.mediaRecorder.pause()
      console.log(this.mediaRecorder.state)
    }

    btnResume.onclick = (event) => {
      this.mediaRecorder.resume()
      console.log(this.mediaRecorder.state)
    }

    btnEnd.onclick = (event) => {
      this.mediaRecorder.requestData() // trigger ``ondataavailable``  // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/requestData
      this.mediaRecorder.stop()
      console.log(this.mediaRecorder.state)
    }
  }
}


class Video2Canvas {
  /**
   * @description Create a canvas and save the frame of the video that you are giving.
   * @param {HTMLVideoElement} video
   * @param {Number} fps
   * @see https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
   * */
  constructor(video, fps) {
    this.video = video
    this.fps = fps
    this.canvas = document.createElement("canvas");
    [this.canvas.width, this.canvas.height] = [video.width, video.height]
    document.querySelector("body").append(this.canvas)
    this.ctx =  this.canvas.getContext('2d')
    this.initEventListener()
  }

  initEventListener() {
    this.video.addEventListener("play", ()=>{
      const timeout = Math.round(1000/this.fps)
      const width = this.video.width
      const height = this.video.height
      const recordFunc = ()=> {
        if (this.video.paused || this.video.ended) {
          return
        }
        this.ctx.drawImage(this.video, 0, 0, width, height)
        /*
        const frame = this.ctx.getImageData(0, 0, width, height)
        // ... // you can make some modifications to change the frame. For example, create the grayscale frame: https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation

        //  Below is the options. That saves each frame as a link. If you wish, then you can click the link to download the picture.
        const range = document.createRange()
        const frag = range.createContextualFragment('<div><a></a></div>')
        const tmpCanvas = document.createElement('canvas')
        tmpCanvas.width = this.canvas.width
        tmpCanvas.height = this.canvas.height
        tmpCanvas.getContext('2d').putImageData(frame, 0, 0)
        const a = frag.querySelector('a')
        a.innerText = "my.png"
        a.download = "my.png"
        const quality = 1.0
        a.href = tmpCanvas.toDataURL("image/png", quality)
        a.append(tmpCanvas)
        document.querySelector('body').append(frag)
        */
        setTimeout(recordFunc, timeout)
      }
      setTimeout(recordFunc, timeout)
    })
  }
}

(()=>{
  const v2c = new Video2Canvas(document.querySelector("video"), 60)
  const canvasRecord = new CanvasRecord(v2c.canvas, 25, 'video/webm')

  v2c.video.addEventListener("play", (event)=>{
    if (canvasRecord.mediaRecorder.state === "inactive") {
      return
    }
    document.getElementById("btn-resume").click()
  })

  v2c.video.addEventListener("pause", (event)=>{
    if (canvasRecord.mediaRecorder.state === "inactive") {
      return
    }
    document.getElementById("btn-pause").click()
  })
})()
<video id="my-video" controls="true" width="480" height="270" crossorigin="anonymous">
  <source src="http://jplayer.org/video/webm/Big_Buck_Bunny_Trailer.webm" type="video/webm">
</video>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接