1. 产品需求
要求录制页面指定区域(canvas webgl),结束录制后,导出MP4,windows自带的播放器可播放。
2. 录制
2.1 rrweb
rrweb 是 'record and replay the web' 的简写,旨在利用现代浏览器所提供的强大 API 录制并回放任意 web 界面中的用户操作
-
rrweb对canvas的处理,默认情况下其内容不会被 rrweb 观测到,我们可以通过特定的配置让 rrweb 能够录制并回放 Canvas。
-
录制时包含 Canvas 内的内容:
rrweb.record({
emit(event) {},
// 对 canvas 进行录制
recordCanvas: true,
});
- 回放时对 Canvas 进行回放,回放 Canvas 将会关闭沙盒策略,导致一定风险:
const replayer = new rrweb.Replayer(events, {
UNSAFE_replayCanvas: true,
});
replayer.play();
- 兼容性
由于使用 MutationObserver API,rrweb 不支持 IE11 以下的浏览器
2.2 recordrtc
WebRTC JavaScript Library for Audio+Video+Screen+Canvas (2D+3D animation) Recording。WebRTC库可对音频、视频、屏幕和canvas(2D+3D)进行录制
- 语法
let recorder = RecordRTC(MediaStream || HTMLCanvasElement || HTMLVideoElement || HTMLElement, {});
-
注意:recordrtc依赖
html2canvas库,否则报错。解决方法就是引入后,挂载到window对象上即可 -
console报错信息
- RecordRTC.js报错位置
- recordrtc的使用
import html2canvas from 'html2canvas';
import RecordRTC from 'recordrtc';
window.html2canvas = html2canvas;
// 初始化开始录制
const recorder = new RecordRTC(elementToRecordDOM, {
type: 'canvas',
mimeType: 'video/webm;codecs=h264',
recorderType: RecordRTC.CanvasRecorder,
disableLogs: true,
});
// 结束录制
recorder.stopRecording(() => {
const blob = recorder.getBlob();
// 导出视频,遗憾的是这里windows自带播放器无法播放,chrome浏览器和手机上都可以播放,后面会讲到如何转码
RecordRTC.invokeSaveAsDialog(blob, 'test.mp4');
});
- 兼容性
2.3 navigator.mediaDevices.getDisplayMedia api
也可直接使用OpRec这个库
- 语法
var promise = navigator.mediaDevices.getDisplayMedia(constraints);
- 优点:录制并回放用户任意界面(不限于浏览器中)
- 缺点:会弹出这个确认框,需要用户点击分享,结束后需要点击结束共享
- 需要注意的是: 部署在线上http,浏览器出于安全考虑,
navigator.mediaDevices获取可能会undefined,而本地开发localhost://和file://协议访问及线上https://访问,是没问题的
- html
<video id="video" autoplay muted style="width: 500px; height: 300px; border: 1px solid #ccc;"></video>
- js
async function startCapture(displayMediaOptions) {
try {
captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
document.getElementById("video").srcObject = captureStream;
} catch (err) {
console.error("Error: " + err);
}
}
startCapture({
video: true,
audio: false
});
2.4 选择
这里选择使用 recordrtc进行录制导出
3. WebAssembly将webm格式转码为通用mp4
WebAssembly/wasm WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式,实际上 wasm 是体积小且加载快的二进制格式,其目标就是充分发挥硬件能力以达到原生执行效率
3.1 ffmpeg.wasm
ffmpeg.wasm is a pure Webassembly / Javascript port of FFmpeg. It enables video & audio record, convert and stream right inside browsers. 可以在浏览器里进行视频和音频的录制和转码
3.1.1 安装
npm install @ffmpeg/ffmpeg @ffmpeg/core
3.1.2 使用
import FFmpeg from '@ffmpeg/ffmpeg';
async transAndDownload(blob) {
// https://github.com/muaz-khan/RecordRTC/issues/464
// ffmpeg fails to convert webm to mp4
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
// https://github.com/ffmpegwasm/ffmpeg.wasm#why-it-doesnt-work-in-my-local-environment
// corePath: 'https://unpkg.com/@ffmpeg/core/dist/ffmpeg-core.js',
log: true,
});
await ffmpeg.load();
ffmpeg.FS('writeFile', 'test.webm', await fetchFile(blob));
await ffmpeg.run('-i', 'test.webm', 'viewpoint.mp4');
const data = ffmpeg.FS('readFile', 'viewpoint.mp4');
const newBlob = new Blob([data.buffer], { type: 'video/mp4' });
// 下载
const href = URL.createObjectURL(newBlob);
const a = document.createElement('a');
a.setAttribute('href', href);
a.setAttribute('style', 'display:none');
a.download = 'test.mp4';
document.body.appendChild(a);
a.click();
a.parentNode.removeChild(a);
URL.revokeObjectURL(href);
}
3.1.3 遇到的问题
[CreateFFmpegCore is not undefined](https://github.com/ffmpegwasm/ffmpeg.wasm/issues/199)[Cannot find module '@ffmpeg/core'](https://github.com/ffmpegwasm/ffmpeg.wasm/issues/202),提示缺少核心模块
解决方法:Why it doesn't work in my local environment,需要在初始化createFFmpeg的时候,指定corePath:
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
corePath: "http://localhost:3000/public/ffmpeg-core.js",
// Use public address if you don't want to host your own.
// corePath: 'https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js'
log: true,
});
解决方法:
- 卸载之前安装的,下载指定版本的包
"@ffmpeg/core": "0.8.5",
"@ffmpeg/ffmpeg": "0.9.8",
- webpack项目修改webpack devser配置文件
create-react-app中修改config/webpackDevServer.config.js文件,这里已经执行过npm run eject暴露配置文件,没有执行过了可以通过安装@craco/craco或安装 react-app-rewired+customize-cra来修改配置文件
return {
// 添加如下header
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
}
- 一顿猛如虎的操作后,最后导出的视频还是漆黑一片,使用的姿势还是不对,知道的大佬,帮我看下,哪里出错了,[手动狗头],after converting webm to mp4 using ffmpeg,video played is total black
3.2 官方提供的ffmpeg-demos--demos,成功将webm转为mp4
3.2.1 Web Worker
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。这里将视频转码的任务放在worker线程中运行,转码成功后通知主线程
- 基本使用
- 主线程
// 创建worker线程
var worker = new Worker('work.js');
// 主线程使用worker.postMessage方法,向 Worker发消息
worker.postMessage('hello');
// 主线程使用onmessage接收子线程发回来的消息
worker.onmessage = function (event) { console.log(event.data) }
- Worker子线程
// worker子线程内部使用全局importScripts加载其它脚本
importScripts('script1.js');
// 接收主线程的消息
this.onmessage = function(event) {}
// 向主线程发消息
this.postMessage('hello')
- 需要注意的:
- Worker 线程无法读取本地文件,文件必须加载自网络
- Worker 线程运行的脚本文件,必须与主线程的脚本文件同源
3.2.2 webm-to-mp4
我们将webm-to-mp4 source源码下载后看到:
processInWebWorker初始化创建worker线程- 使用new Blob创建一个类型为
application/javascript的文件 - 文件内部使用importScripts加载ffmpeg_asm.js,postMessage发消息,onmessage监听接收消息
- 使用URL.createObjectURL生成映射链接
- new Worke创建线程
- URL.revokeObjectURL释放引用
- 使用new Blob创建一个类型为
import { baseURL } from '@/config';
// ffmpeg_asm.js路径
const workerPath = `${baseURL}${process.env.PUBLIC_URL}/lib/ffmpeg_asm.js`;
function processInWebWorker() {
const blob = URL.createObjectURL(
new Blob(
[
`importScripts("${workerPath}");var now = Date.now;function print(text) {postMessage({"type" : "stdout","data" : text});};onmessage = function(event) {var message = event.data;if (message.type === "command") {var Module = {print: print,printErr: print,files: message.files || [],arguments: message.arguments || [],TOTAL_MEMORY: message.TOTAL_MEMORY || false};postMessage({"type" : "start","data" : Module.arguments.join(" ")});postMessage({"type" : "stdout","data" : "Received command: " +Module.arguments.join(" ") +((Module.TOTAL_MEMORY) ? ". Processing with " + Module.TOTAL_MEMORY + " bits." : "")});var time = now();var result = ffmpeg_run(Module);var totalTime = now() - time;postMessage({"type" : "stdout","data" : "Finished processing (took " + totalTime + "ms)"});postMessage({"type" : "done","data" : result,"time" : totalTime});}};postMessage({"type" : "ready"});`,
],
{
type: 'application/javascript',
}
)
);
const worker = new Worker(blob);
URL.revokeObjectURL(blob);
return worker;
}
convertStreams将blob转为mp4
import { message as antdMessage } from 'antd';
let worker;
// 将上面recordRTC录制好的blob (recorder.getBlob()) 转为mp4
function convertStreams(videoBlob) {
let aab;
let buffersReady;
// let workerReady;
// let posted;
let postMessage = function () {
if (!worker) return;
// 向子线程发送转码命令
worker.postMessage({
type: 'command',
// ffmpeg命令参数详解
// http://ffmpeg.org/ffmpeg-filters.html
// https://www.jianshu.com/p/049d03705a81
// -i 输入处理视频
// -c:v
// -b:v 将输出文件的视频比特率
// -r 24 24fps帧率
// -s 720 * 480视频大小
arguments: '-i video.webm -c:v mpeg4 -b:v 6400k -r 24 -s 720*480 -strict experimental output.mp4'. split(' '),
files: [
{
data: new Uint8Array(aab),
name: 'video.webm',
},
],
});
};
let fileReader = new FileReader();
fileReader.onload = function () {
aab = this.result;
postMessage();
};
// 使用FileReader读取blob
fileReader.readAsArrayBuffer(videoBlob);
if (!worker) {
// 初始化worker线程
worker = processInWebWorker();
}
// 接收worker子线程的消息
worker.onmessage = function (event) {
let message = event.data;
if (message.type == 'ready') {
console.log(workerPath + ' file has been loaded.');
if (buffersReady) postMessage();
} else if (message.type == 'stdout') {
console.log(message.data);
} else if (message.type == 'start') {
console.log(workerPath + ' file received ffmpeg command.');
} else if (message.type == 'done') {
const key = 'updatable';
// 转码完成
console.log('done');
antdMessage.destroy();
antdMessage.success({ content: '转码成功!正在导出视频', key });
let result = message.data[0];
if (!message.data || !result || !result.data) {
antdMessage.warn('转码错误,请重新操作');
return;
}
let blob = new File([result.data], 'test.mp4', {
type: 'video/mp4',
});
setTimeout(() => {
// 使用RecordRTC.invokeSaveAsDialog方法导出文件
RecordRTC.invokeSaveAsDialog(blob, 'viewpoint.mp4');
antdMessage.success({ content: '导出成功!', duration: 2, key });
}, 800);
}
};
// 监听worker异常
worker.onerror = function () {
antdMessage.destroy();
antdMessage.warn('网络异常,文件加载失败,请重试');
};
}
3. 完整代码请移步github
4. 小结
- 使用到ffmpeg,需要初步了解命令参数的含义
- 使用到Web Worker,需要了解如何基本使用
- 使用到WebAssembly,后期需要自己去跟着文档编译一个wasm