最近做了一个实验项目,目标是在浏览器请求摄像头时,不是调用本机摄像头,而是播放指定视频,以达到“模拟摄像头”的效果。

区别于传统做法(比如使用虚拟驱动/fakevideo),我采用的是拦截 + hook JS 的思路,更轻量,也无需系统级驱动支持。


一、浏览器摄像头调用原理

网页通过如下方式调用摄像头:

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
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
video.srcObject = stream;
})
````

调用后,浏览器会请求系统的物理摄像头,生成一个 MediaStream 并赋给 `<video>` 元素的 `srcObject` 属性。

---

## 二、hookVideo 的思路与实现

核心思路:**拦截网页中 `getUserMedia()` 的调用**,将返回的摄像头流替换为我们自己的 **本地视频流**(通过 `canvas.captureStream()` 实现)。

### 示例:

```js
function hookVideo(videoPath) {
const video = document.createElement('video');
video.src = videoPath;
video.muted = true;
video.loop = true;
video.play();

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

video.addEventListener('loadeddata', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;

function draw() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
requestAnimationFrame(draw);
}
draw();
});

const fakeStream = canvas.captureStream();

const originalGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
navigator.mediaDevices.getUserMedia = async function (constraints) {
if (constraints.video) {
return Promise.resolve(fakeStream);
}
return originalGetUserMedia(constraints);
};
}

✅ 上述代码实现了:替换网页摄像头请求 → 本地循环播放视频流 → 模拟“虚拟摄像头”效果。


三、为什么不是 fakevideo

传统做法 fakevideo(如 v4l2loopback)是在系统层注册一个虚拟摄像头设备,需要:

  • 安装驱动
  • Linux 专属
  • 权限高,复杂

但我只是想在某个网站上替换视频,不需要改系统设备,所以选择了 JS Hook。


四、Python + OBS 的实现思路(扩展)

有时候我们希望传入的是实时生成的画面,而不是本地视频文件,可以用 Python + OBS 来实现一个“伪直播摄像头”:

  • Python 脚本实时绘制画面(如 OpenCV + 图像识别)
  • OBS 推流输出
  • 使用 OBS-VirtualCam 插件创建虚拟摄像头设备
  • 这样网页就能读取到 OBS 的合成画面作为摄像头输入

虽然这种方式仍需虚拟设备支持,但它适用于高级场景(如直播、AI 实时标注等)。


如果你也有这种定制需求,比如只想在某个学习/会议/网课网站替换摄像头,JS hook + 视频 canvas 是个非常轻便的方式,几行代码即可完成。