4. 媒体流处理:MediaStreamTrack详解、音视频轨道的启用与禁用、屏幕共享的实现(getDisplayMedia)、媒体流的录制与保存
好,我们进入第四章。这一章,咱们要跟媒体流里的「最小单元」打交道——MediaStreamTrack。说白了,一个视频流里可能有好几路轨道:一路摄像头、一路麦克风、甚至一路屏幕共享。每一路轨道,都是一个 MediaStreamTrack 对象。
我个人习惯把 MediaStreamTrack 比作「水龙头」。你可以打开它(启用),也可以关上它(禁用),甚至可以把它接到录像机里(录制)。这一章,我们就来拧拧这些水龙头。
4.1 MediaStreamTrack 详解
先看一个最基础的问题:MediaStreamTrack 到底是什么?
当你调用 getUserMedia 拿到一个 MediaStream 后,这个流里包含一个或多个 MediaStreamTrack。每个 track 代表一种媒体类型:"video" 或 "audio"。
举个例子:
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
const tracks = stream.getTracks();
console.log(tracks); // 输出两个 MediaStreamTrack 对象
tracks.forEach(track => {
console.log(track.kind); // "video" 或 "audio"
console.log(track.label); // 比如 "FaceTime HD Camera"
console.log(track.readyState); // "live" 或 "ended"
});
});
这里有几个关键属性,我列个表方便你查阅:
| 属性 | 类型 | 说明 |
|---|---|---|
kind |
string | "audio" 或 "video" |
label |
string | 设备名称,比如 "内置麦克风" |
enabled |
boolean | 控制轨道是否输出数据 |
muted |
boolean | 是否被系统静音(只读) |
readyState |
string | "live" 或 "ended" |
enabled 和 muted 不一样。enabled 是你主动控制的开关;muted 是系统层面的静音,比如你把麦克风物理拔掉了,浏览器会把它标记为 muted。
4.2 音视频轨道的启用与禁用
这是实际开发中最常见的操作之一。比如用户想关掉摄像头,只留声音。或者反过来,静音麦克风但保留画面。
实现方式非常简单:
// 假设 stream 是 getUserMedia 返回的
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];
// 禁用视频轨道(摄像头灯会灭)
videoTrack.enabled = false;
// 启用音频轨道
audioTrack.enabled = true;
嗯,这里要注意:enabled = false 只是让轨道停止输出数据,但并没有释放设备资源。摄像头灯虽然灭了,但其他应用还是拿不到这个摄像头,因为 getUserMedia 的权限还没释放。
enabled = false。结果用户再点「开启摄像头」时,发现摄像头灯不亮,但画面还是黑的。排查了半天,发现是 enabled 虽然设为 true,但轨道状态已经变成了 "ended"。正确的做法是:如果只是临时关闭,用 enabled;如果彻底不用了,调用 track.stop() 释放设备。
如果你想彻底释放设备,让其他应用也能用:
// 停止所有轨道
stream.getTracks().forEach(track => track.stop());
调用 stop() 后,readyState 会变成 "ended",这个轨道就废了,不能再复用。
4.3 屏幕共享的实现(getDisplayMedia)
屏幕共享是 WebRTC 里一个非常实用的功能。你想想看,远程会议、在线教学、游戏直播,哪个离得开它?
实现屏幕共享的核心 API 是 getDisplayMedia。它跟 getUserMedia 很像,但区别在于:
getUserMedia获取摄像头/麦克风getDisplayMedia获取屏幕/窗口/浏览器标签页
基本用法:
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always" // 显示鼠标指针
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
});
// 把 stream 赋值给 video 元素
const video = document.getElementById("screenVideo");
video.srcObject = stream;
} catch (err) {
console.error("屏幕共享失败:", err);
}
}
调用 getDisplayMedia 后,浏览器会弹出一个选择框,让用户选择共享整个屏幕、某个窗口、还是某个标签页。这个选择框是浏览器自带的,你没法自定义样式。
getDisplayMedia 返回的 MediaStream 里,视频轨道是必须的,音频轨道是可选的。而且音频轨道只能捕获标签页的音频,不能捕获系统音频。如果你想捕获系统音频,需要借助其他手段(比如浏览器扩展)。
我个人习惯在屏幕共享时,把摄像头画面和屏幕画面合成一个流。怎么做呢?用 MediaStream 的 addTrack 方法:
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// 把摄像头轨道加到屏幕流里
cameraStream.getTracks().forEach(track => {
screenStream.addTrack(track);
});
// 现在 screenStream 里既有屏幕画面,又有摄像头画面
// 可以传给 RTCPeerConnection 了
嗯,这里有个坑:屏幕共享的轨道在用户点击「停止共享」后,会自动变成 "ended" 状态。你需要监听这个事件:
const screenTrack = screenStream.getVideoTracks()[0];
screenTrack.onended = () => {
console.log("用户停止了屏幕共享");
// 清理 UI,切换回摄像头模式
};
4.4 媒体流的录制与保存
录制媒体流,说白了就是把 MediaStream 里的数据一块块存下来。浏览器提供了 MediaRecorder API,用起来非常顺手。
基本流程:
- 拿到一个
MediaStream(来自摄像头、屏幕共享、或者远程 peer) - 创建
MediaRecorder实例 - 监听
ondataavailable事件,把数据块存起来 - 录制结束后,把数据块合并成一个 Blob,下载或上传
代码示例:
let mediaRecorder;
let recordedChunks = [];
function startRecording(stream) {
recordedChunks = [];
// 指定 MIME 类型,浏览器支持的话就用 webm
const options = { mimeType: "video/webm; codecs=vp9" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.warn("当前浏览器不支持 vp9,回退到默认格式");
mediaRecorder = new MediaRecorder(stream);
} else {
mediaRecorder = new MediaRecorder(stream, options);
}
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: "video/webm" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "recording.webm";
a.click();
URL.revokeObjectURL(url);
};
// 每 1 秒触发一次 ondataavailable
mediaRecorder.start(1000);
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
}
}
start(1000) 里的参数是时间片,单位毫秒。意思是每 1 秒生成一个数据块。如果你不传这个参数,默认会在 stop() 时一次性生成所有数据。我个人习惯传一个时间片,这样即使录制过程中崩溃了,也能拿到部分数据。
录制时要注意几个问题:
- MIME 类型兼容性: 不同浏览器支持的编码格式不一样。Chrome 支持
video/webm; codecs=vp9,Safari 可能只支持video/mp4。建议先用MediaRecorder.isTypeSupported()检测一下。 - 音频录制: 如果流里有音频轨道,录制的文件会包含声音。但要注意,屏幕共享的音频轨道只能捕获标签页音频,不能捕获系统音频。
- 性能问题: 录制高清视频(比如 1080p)时,CPU 占用会比较高。如果用户设备性能差,可以考虑降低分辨率再录制。
4.5 本章知识体系
为了让你更直观地理解这一章的内容,我画了一张图:
这张图把 MediaStreamTrack 的「来源」和「操作」分成了左右两半。左边是三种获取 track 的方式,右边是三种常见的操作。中间就是我们的主角——MediaStreamTrack。
好了,这一章的内容就到这里。媒体流处理是 WebRTC 开发中绕不开的一环,尤其是屏幕共享和录制,在实际项目中几乎天天用到。希望你能动手敲一遍代码,把每个 API 都跑通。遇到问题别慌,多看看控制台的报错,多查查 MDN 文档。
公众号:蓝海资料掘金营,微信deep3321