51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

JS audio加图片序列或canvas转webM/MP4的实现


视频合成封面图

一、生成WebM视频

Web端基于图像素材生成webM格式的视频算是比较成熟的技术了,实现也比较简单。

我之前是使用whammy.js实现的:https://github.com/antimatter15/whammy

不过此项目已经多年不更新了,看看时间,七八年了。

时间

对于webP格式的图片序列,whammy会有黑屏的问题,因为21年的时候Chrome修改了 webp 图片的编码方法,从 WEBPVP8 改为了 WEBPVP8X,其并没有进行及时的更新。

不过JPG或者PNG图片序列功能不受影响,大家如果想使用也是可以的。

webm-writer

w3c官方项目demo使用的是更新的 webm-writer.js,这个项目是基于 Whammy 实现的,比较新,最近更新是2年前。

项目地址:https://github.com/thenickdude/webm-writer-js

这里,我就使用webm-writer示意下如何使用纯JS在前端生成webM视频。

您可以狠狠地点击这里:webm-writer.js实现webm视频合成并下载demo

点击页面的"生成webM视频"按钮,就可以在下方看到生成好的webM视频了,点击播放,可以预览效果,点击下载,可以下载该webM视频。

支持Chrome,Firefox以及Safari浏览器(桌面Safari 16+)。

例如,下面的MP4录屏就是我Chrome 112版本下的实现效果(不动点击播放):

代码示意

// 点击按钮的webM生成
button.onclick = function () {
    // 构造webm生成器
    var videoWriter = new WebMWriter({
        // 每秒30帧
        frameRate: 30
    });
    // 创建屏幕外 canvas
    var canvas = document.createElement('canvas');
    // 总共50帧canvas绘制
    var currentFrame = 1;
    var maxFrame = 50;
    for (var index = 1; index <= maxFrame; index++) {
        currentFrame = index;
        // 绘制函数略
        draw();
        videoWriter.addFrame(canvas);
        // 如果是最后一帧,那就完成
        if (currentFrame === maxFrame && complete) {
          videoWriter.complete().then(function(webMBlob) {
            var blobUrl = URL.createObjectURL(webMBlob);
            // blobUrl就是webM视频地址了,可播放,可下载
          });     
        }
    }
};

webM视频合成的实现其实很简单,主要就是下面三步:

  1. 构造:

    var videoWriter = new WebMWriter(options);
    
  2. 添加帧:

    videoWriter.addFrame(canvas);
    
  3. 结束添加,完成生成:

    videoWriter.complete().then(function(webMBlob) {
        video.src = URL.createObjectURL(webMBlob);
    })
    

其他

  • webm-writer似乎不支持直接使用图像元素作为帧内容添加,具体我没测,我是看源码得出的结论,这个就没有 Whammy 好用。
  • webm-writer好像都是用的webP图像进行视频合成的,所以,图像一定会有损失,不可能100%还原。

其实,有了canvas流,借助MediaRecorder API,我们无需使用第三方开源的JS组件,也能实现webM视频的合并,下面是案例,适合摄像头的视频流保存为视频的场景。

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"
        });
``    //数据记录的起始时间 `time || 4000 ms`
    mediaRecorder.start(time || 4000);

    mediaRecorder.ondataavailable = function (event) {
        recordedChunks.push(event.data);
         //`dataavilable`事件结束后执行,只会执行一次
        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);
    }
})
``
}

然而,无论是whammy还是webm-writer,虽然用来合成webM视频很方便,但是有个问题,那就是无法添加音频进行,也就是视频合成的时候,没法带声音。

这个问题就是我之前遇到的问题,所以后来寻求了使用ffmpeg.wasm来实现,也就是之前这篇文章"借助ffmpeg.wasm纯前端实现多音频和视频的合成"的由来。

长久以来,我一直以为此事无解,但是,随着浏览器的发展,事情出现了转机,Chrome 94开始支持了WebCodecs API(兼容性见下图),这是个图像、音频和视频编解码API集合,很强。

WebCodecs兼容性

有了WebCodecs API,纯前端,不依赖任何插件,迅速实现带音乐的视频合成成为了可能,这是本文要介绍的重点。

二、带audio声音的webM合成

实现原理也比较好理解,AudioEncoder编码音频,VideoEncoder编码视频,然后进行合并封装。

其中,编码视频和编码音频相关的API并不完全对等(根据我看一些老的资料,一开始,也就是规范阶段,浏览器还没支持的时候,是近似的,例如音频中还有AudioFrame的概念,现在没有了,只有VideoFrame)。

原理好懂,但是实操细节却不简单。

因为涉及到非常多的音视频概念,和各种API对象,例如,WebCodecs API中的Audio编码有个名叫AudioData的概念,这是新的概念,和Web Audio API中decodeAudioData(audioData)中的audioData参数不是一个东西。

想要完全了解,非一朝一夕,哪怕一周一月,都不太可能,也没有必要。

除非你是专门从事音视频开发的前端。

所以,我们可以借助社区的力量帮助我们上手WebCodecs API的使用。

不过,由于WebCodecs API刚出来没多久,目前学习资源有限,MDN上虽然有对API的介绍,但基本上都看不到example演示,所以,相关的学习与尝试,还是花了我不少时间的。

webm-muxer项目

找到了一个基于WebCodecs API实现的Webm视频合并项目:https://github.com/Vanilagy/webm-muxer

muxer 其实是计算机中的一个术语,指合并,例如将视频文件、音频文件和字幕文件合并为某一个视频格式就是典型的muxer,还有个demuxer,表示拆分。

此项目提供了两个demo,可以将麦克风录音和canvas绘制合并成webm视频,如下截图示意。

demo示意

不知道是不是我设备的问题,我保存的视频是绿屏。

视频绿屏

不过这个不重要,重要的是此demo为我们的需求解决提供了重要的参考。

demo之canvas+mp3=>webm视频

你可以狠狠地点击这里:webm-muxer实现带音乐的webm视频demo

在Chrome浏览器下,点击"生成webm"按钮,稍等数秒,就可以看到合成的webm视频了,如下图所示:

视频生成效果截图

还可以点击右侧的"下载"按钮下载此webM视频。

实现过程概述

1. 构造

构造合并器muxer,音视频解码器,下面这些代码结构都是固定的,一些参数值,例如视频尺寸和帧率根据实际情况自行设置。

// 构造包装器
var muxer = new WebMMuxer.Muxer({
    target: new WebMMuxer.ArrayBufferTarget(),
    video: {
        codec: 'V_VP9',
        width: 600,
        height: 400,
        frameRate: 30
    },
    audio: {
        codec: 'A_OPUS',
        sampleRate: 48000,
        numberOfChannels: 1
    },
    firstTimestampBehavior: 'offset'
});
// 音视频编码器,这里使用的是WebCodese API
var videoEncoder = new VideoEncoder({
output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
error: e => console.error(e)
});
videoEncoder.configure({
codec: 'vp09.00.10.08',
width: 600,
height: 400,
bitrate: 1e6
});
// 音频的
var audioEncoder = new AudioEncoder({
output: (chunk, meta) => muxer.addAudioChunk(chunk, meta),
error: e => console.error(e)
});
audioEncoder.configure({
codec: 'opus',
numberOfChannels: 1,
sampleRate: 22050,
bitrate: 64000
});

提示:如果遇到不兼容的提示(Input audio buffer is incompatible with codec parameters),一般都是configure()中的sampleRate设置的不对,和音频文件的采样率不匹配。

2. 音频编码

这里是实现的难点,我一开始是尝试直接fetch MP3音频文件流,然后借助AudioEncoder进行编码,结果发现路走不通,总是提示,不是需要的音频数据。

查了很多资料,也未见相关实现案例,隐隐觉得,技术上就不通,不然不会都是借助MediaDevices的getUserMedia()方法实现的例子,也就是麦克风或摄像头捕捉的数据。

所以,我就退而求其次,音频播放的时候,基于播放的音频流进行编码,这也是为何demo页面合成时候,会播放音频的原因。

// 音频资源获取
const myAudio = new Audio();
fetch(audio.src).then(res => {
    var reader = res.body.getReader();
    return reader.read().then(result => {
        return result;
    });
}).then(data => {
    var blob = new Blob([data.value], {
        type: 'audio/mp3'
    });
    var blobUrl = URL.createObjectURL(blob);
`// 创建音频对象
myAudio.src = blobUrl;
// 隐藏不可见
myAudio.hidden = true;
// 静音,避免干扰(静音后,合成的视频也会没声音)
// myAudio.muted = true;
// 在页面内,方便播放
document.body.append(myAudio);
`
});
// 捕捉播放的音轨
const audioTrack = myAudio.captureStream().getAudioTracks()[0];
// MediaStreamTrackProcessor可以用来生成媒体帧流
let trackProcessor = new MediaStreamTrackProcessor({
track: audioTrack
});
// 音频播放,并实时抓取视频流
// 交给webcodecs API进行编码
myAudio.play();
// 编码音频数据
let consumer = new WritableStream({
write(audioData) {
if (!audioEncoder) {
return;
}
audioEncoder.encode(audioData);
audioData.close();
}
});
trackProcessor.readable.pipeTo(consumer);

这里可能是我的积累还不够,如果有谁知道直接fetch音频并encode()的方法,欢迎不吝赐教。

更新于2023年9月20日

找到了直接fetch音频的方法了,可以参见下面的MP4合成demo示意。

3. 视频编码

视频编码案例比较多,相对简单的多。

视频中,有专门的VideoFrame,只需要进行制定每一帧的资源、时间间隔,以及关键帧即可。

可以直接使用canvas作为资源,IMG也是可以的。

demo中的实现示意:

// 创建屏幕外 canvas
var canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 400;
// 编码视频数据
var startTime = document.timeline.currentTime;
var frameCounter = 0;
// handleDraw源码可右键页面查看
// 每次绘制都会走一遍后面的function函数
handleDraw(canvas, function () {
// 定义视频帧
let frame = new VideoFrame(canvas, {
timestamp: (frameCounter * 1000 / 30) * 1000
});
`frameCounter++;
// 制定关键帧,这是规范要求,最多多少秒之内一定要有个关键帧的
videoEncoder.encode(frame, { 
    keyFrame: frameCounter % 30 === 0 
});
frame.close();
`
});

4. 结束编码

当视频完全绘制结束,同时确保音频播放到视频时长,可以结束。

async () => {
    await videoEncoder?.flush();
    await audioEncoder?.flush();
    muxer.finalize();
};

5. 视频生成

let { buffer } = muxer.target;
// buffer就是视频数据
// 我们可以将其作为blobURL地址进行播放或下载
var blobUrl = URL.createObjectURL(new Blob([buffer]));

以上就是实现全部过程。

三、MP4视频的合成

webm-muxer项目的作者还弄了个类似的项目,mp4-muxer,可以辅助我们在Web浏览器中合成MP4视频。

项目地址:https://github.com/Vanilagy/mp4-muxer

语法和webm-muxer项目类似,我就不赘述了,我们直接看demo实现的效果。

您可以狠狠地点击这里:mp4-muxer实现带声音的mp4视频demo

下面的视频就是生成的(点击播放,有音频,注意场合):

虽说代码类似,但也完全不一样。

其中一个区别就是,MP4视频合成对音频文件的质量要求更高。

采样率至少是44100,品质较低的音频是不行的。

可能是mp4a编码格式要求的,为此,我专门下载了个高质量的MP3背景音乐,这是相关的配置:

audioEncoder.configure({
    codec: 'mp4a.40.2',
    numberOfChannels: 2,
    sampleRate: 44100,
    bitrate: 128000
});

代码实现逻辑和webm-muxer类似,我就不重复展示了,代码都在demo页面上,都是原生的,非常适合大家学习上手WebCodecs API。

更新于2023-09-20

无需播放音频,直接根据audioData数据合成MP4的demo弄出来了。

您可以狠狠地点击这里:canvas图片序列加声音合成mp4视频demo

1.67s的视频,80ms就完成了,强无敌。

兴奋,激动,ffmpeg再见了。

不带音频的MP4合成

如果不带音频,使用WebCodecs API合成mp4视频的速度极快,秒生成。

有兴趣想要体验的可以狠狠地点击这里:mp4-muxer实现纯画面mp4视频demo

点击按钮后,只需要0.02秒就可以得到视频,快如闪电。

MP4视频时间示意

MP4视频所有浏览器,所有操作系统都能播放,因此,上面这个demo可能反而会更加实用。

五、终于结语了

继上一篇使用"WebCodecs API之ImageDecoder解码GIF"已经过去两周了,一直在盘这篇文章,不容易,终于要发布了。

算是目前关于浏览器音视频合成,尤其WebCodecs API这块比较稀缺的内容了。

好东西就是要让大家都知道的,欢迎分享,点赞。

在学习的过程中,还是发现自己对各种stream、buffer(如sharedArrayBuffer、ringbuffer)、chunk、track等理解还不够深入,还需要多多积累。

以写作为契机,逼迫自己学深入自己未曾深入的领域,是非常高效的一种学习方法,推荐给大家。

好了,就说这么多。

如果你觉得本文的内容对你的学习很有帮助,嗯......买本《CSS新世界》支持下吧。

(本篇完)

赞(0)
未经允许不得转载:工具盒子 » JS audio加图片序列或canvas转webM/MP4的实现