前言:利用web浏览器原生API进对Canvas画面进行录屏并转换成mp4的具体操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
(async function () {
// 定义要选择录制的内容标签
const SELECTOR = "canvas";
// 倒计时秒数
const COUNTDOWN = 10;
// 帧率
const FPS = 60;
// 输出宽度
const WIDTH = 1920;
// 输出高度
const HEIGHT = 1080;
// 查找定义的内容标签
const srcCanvas = document.querySelector(SELECTOR);
if (!srcCanvas) {
console.error("未找到 canvas 元素,请检查选择器:", SELECTOR);
return;
}

console.log(`原 canvas 尺寸: ${srcCanvas.width}x${srcCanvas.height}, devicePixelRatio: ${window.devicePixelRatio}`);

// 1. 创建精确 1920x1080 的离屏 canvas(忽略 DPR)
const offscreen = document.createElement("canvas");
offscreen.width = WIDTH;
offscreen.height = HEIGHT;
offscreen.style.width = WIDTH + "px";
offscreen.style.height = HEIGHT + "px";
const ctx = offscreen.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";

let drawing = true;
// 这是一个循环绘制函数,每帧把源 canvas 的内容复制到离屏 canvas
function drawFrame() {
if (!drawing) return;
// 源图像 源起点 (sx, sy) 源宽度 源高度 目标起点 (dx, dy) 目标宽度 (1920) 目标高度 (1080)
ctx.drawImage(srcCanvas, 0, 0, srcCanvas.width, srcCanvas.height, 0, 0, WIDTH, HEIGHT);
// 下一帧再调用自己
requestAnimationFrame(drawFrame);
}
// 启动循环
drawFrame();


// 2. 获取离屏 canvas 的视频流
const canvasStream = offscreen.captureStream(FPS);


// 3. 获取当前标签页音频

let displayStream = null;

let audioTrack = null;

try {

console.log("请在弹窗中选择当前标签页,并勾选「分享标签页音频」");

displayStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
preferCurrentTab: true,
selfBrowserSurface: "include",
});
audioTrack = displayStream.getAudioTracks()[0];

if (!audioTrack) {
throw new Error("未获得音轨:请选择「Chrome 标签页」并勾选「分享标签页音频」");
}

console.log("已获取标签页音频:", audioTrack.label || "未命名音轨");

} catch (e) {

drawing = false;
displayStream?.getTracks().forEach((t) => t.stop());
canvasStream.getTracks().forEach((t) => t.stop());
console.error("无法录制当前标签页音频,已取消录制:", e.message);
return;
}

// 4. 合并视频+音频流

const combinedStream = new MediaStream();

canvasStream.getVideoTracks().forEach((t) => combinedStream.addTrack(t));

if (audioTrack) combinedStream.addTrack(audioTrack);


// 5. 开始倒计时

console.log(`${COUNTDOWN} 秒后开始录制...`);
for (let i = COUNTDOWN; i > 0; i--) {

console.log(` ${i}...`);
await new Promise((r) => setTimeout(r, 1000));
}



// 6. 开始录制

const chunks = [];

const recorder = new MediaRecorder(combinedStream, {

mimeType: "video/webm;codecs=vp9,opus",

videoBitsPerSecond: 8000000,

});

recorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};



const startTime = Date.now();



recorder.onstop = () => {

drawing = false;

displayStream.getTracks().forEach((t) => t.stop());

canvasStream.getTracks().forEach((t) => t.stop());


const duration = ((Date.now() - startTime) / 1000).toFixed(1);

const blob = new Blob(chunks, { type: "video/webm" });

const url = URL.createObjectURL(blob);

const a = document.createElement("a");

a.href = url;

a.download = "canvas-recording.webm";

a.click();

URL.revokeObjectURL(url);

console.log(`录制完成!时长 ${duration}s,视频已下载为 canvas-recording.webm`);

console.log("转 mp4: ffmpeg -i canvas-recording.webm -c:v libx264 -c:a aac -crf 18 output.mp4");

delete window.stop;

};



recorder.start();

console.log(`正在录制 ${WIDTH}x${HEIGHT} @ ${FPS}fps...`);
console.log("在控制台输入 stop() 停止录制");


// 7. 注册全局 stop() 方法,手动停止
window.stop = () => {
recorder.stop();
console.log("录制结束,正在生成视频...");
};

})();