一、生成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视频合成的实现其实很简单,主要就是下面三步:
-
构造:
var videoWriter = new WebMWriter(options);
-
添加帧:
videoWriter.addFrame(canvas);
-
结束添加,完成生成:
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 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之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视频所有浏览器,所有操作系统都能播放,因此,上面这个demo可能反而会更加实用。
五、终于结语了
继上一篇使用"WebCodecs API之ImageDecoder解码GIF"已经过去两周了,一直在盘这篇文章,不容易,终于要发布了。
算是目前关于浏览器音视频合成,尤其WebCodecs API这块比较稀缺的内容了。
好东西就是要让大家都知道的,欢迎分享,点赞。
在学习的过程中,还是发现自己对各种stream、buffer(如sharedArrayBuffer、ringbuffer)、chunk、track等理解还不够深入,还需要多多积累。
以写作为契机,逼迫自己学深入自己未曾深入的领域,是非常高效的一种学习方法,推荐给大家。
好了,就说这么多。
如果你觉得本文的内容对你的学习很有帮助,嗯......买本《CSS新世界》支持下吧。
(本篇完)