Skip to content

WebRTC - MediaStream API

MediaStream API(通常被称为 getUserMedia)是 WebRTC 的基石,旨在为 Web 应用提供对本地输入设备(如摄像头和麦克风)媒体流的便捷访问。

MediaStream API 的主要方面:

  • 流表示:实时媒体数据流(音频和/或视频)由一个 MediaStream 对象表示。
  • 用户权限:访问本地设备是安全关键的。该 API 强制执行权限模型,在 Web 应用开始捕获流之前会提示用户。
  • 设备选择:该 API 与 navigator.mediaDevices.enumerateDevices() 结合使用,允许在有多个摄像头或麦克风可用时选择特定的输入设备。

每个 MediaStream 对象包含一个或多个 MediaStreamTrack 对象。轨(track)表示单个媒体源,例如来自特定摄像头的视频或来自特定麦克风的音频。

一个 MediaStreamTrack 本身可以有多个通道(例如,立体声的左声道和右声道)。这些通道是 MediaStream API 模型中最细粒度的组件。

MediaStream 对象可以通过两种主要方式使用:

  1. 在 HTML <video> 或 <audio> 元素中本地渲染。
  2. 通过 RTCPeerConnection 对象发送给远程对等端。

使用 MediaStream API:一个基本示例

Section titled “使用 MediaStream API:一个基本示例”

让我们创建一个简单的 WebRTC 应用。它将显示一个 <video> 元素,请求用户访问摄像头和麦克风的权限,然后在浏览器中显示实时视频流。创建一个 index.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MediaStream API Demo</title>
<style>
video { width: 320px; height: 240px; border: 1px solid black; background-color: #333; }
body { font-family: sans-serif; padding: 20px; }
.controls button { margin: 5px; padding: 8px 12px; }
</style>
</head>
<body>
<h1>My Live Video Stream</h1>
<video id="localVideo" autoplay playsinline muted></video>
<!-- 'autoplay' starts playback, 'playsinline' for mobile, 'muted' prevents local echo -->
<div class="controls">
<button id="btnGetAudioTracks">Log Audio Tracks</button>
<button id="btnGetVideoTracks">Log Video Tracks</button>
<button id="btnGetTrackById">Log First Video Track by ID</button>
<button id="btnGetTracks">Log All Tracks</button>
<button id="btnRemoveAudioTrack">Remove First Audio Track</button>
<button id="btnRemoveVideoTrack">Remove First Video Track</button>
<button id="btnStopAllTracks">Stop All Tracks (End Stream)</button>
</div>
<script src="client.js"></script>
</body>
</html>

然后,创建 client.js:

const localVideo = document.querySelector('#localVideo');
let localStream; // To store the MediaStream object
async function startMedia() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('getUserMedia() is not supported by your browser.');
console.error('getUserMedia() is not supported.');
return;
}
try {
// Request video and audio access
const constraints = { video: true, audio: true };
localStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('Got MediaStream:', localStream);
// Attach the stream to the video element
localVideo.srcObject = localStream;
// Example: Log when the stream becomes inactive
localStream.oninactive = () => {
console.log('Stream became inactive');
};
} catch (error) {
console.error('Error accessing media devices.', error);
alert(`Error accessing media devices: ${error.name}: ${error.message}`);
if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
alert('No camera/microphone found.');
} else if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
alert('Permission to access camera/microphone was denied.');
} else if (error.name === 'OverconstrainedError' || error.name === 'ConstraintNotSatisfiedError') {
alert('The specified constraints could not be satisfied by available devices.');
} else {
alert('An unknown error occurred while accessing media devices.');
}
}
}
// --- Button Event Listeners for API testing ---
document.getElementById('btnGetAudioTracks').addEventListener('click', () => {
if (!localStream) { alert('Stream not started'); return; }
const audioTracks = localStream.getAudioTracks();
console.log('Audio Tracks:', audioTracks);
if (audioTracks.length > 0) console.log('First audio track settings:', audioTracks[0].getSettings());
});
document.getElementById('btnGetVideoTracks').addEventListener('click', () => {
if (!localStream) { alert('Stream not started'); return; }
const videoTracks = localStream.getVideoTracks();
console.log('Video Tracks:', videoTracks);
if (videoTracks.length > 0) console.log('First video track settings:', videoTracks[0].getSettings());
});
document.getElementById('btnGetTrackById').addEventListener('click', () => {
if (!localStream || localStream.getVideoTracks().length === 0) {
alert('Stream not started or no video tracks'); return;
}
const firstVideoTrackId = localStream.getVideoTracks()[0].id;
const trackById = localStream.getTrackById(firstVideoTrackId);
console.log(`Track by ID (${firstVideoTrackId}):`, trackById);
});
document.getElementById('btnGetTracks').addEventListener('click', () => {
if (!localStream) { alert('Stream not started'); return; }
const allTracks = localStream.getTracks();
console.log('All Tracks:', allTracks);
});
document.getElementById('btnRemoveAudioTrack').addEventListener('click', () => {
if (!localStream || localStream.getAudioTracks().length === 0) {
alert('Stream not started or no audio tracks to remove'); return;
}
const audioTrack = localStream.getAudioTracks()[0];
localStream.removeTrack(audioTrack);
console.log('Removed audio track:', audioTrack);
console.log('Remaining audio tracks:', localStream.getAudioTracks());
// Note: Removing the track from the stream doesn't automatically stop the track or release the device.
// To fully stop it, you might need to call track.stop() if it's the last use of that track.
});
document.getElementById('btnRemoveVideoTrack').addEventListener('click', () => {
if (!localStream || localStream.getVideoTracks().length === 0) {
alert('Stream not started or no video tracks to remove'); return;
}
const videoTrack = localStream.getVideoTracks()[0];
localStream.removeTrack(videoTrack);
console.log('Removed video track:', videoTrack);
console.log('Remaining video tracks:', localStream.getVideoTracks());
// The video element might go black or show a paused frame.
});
document.getElementById('btnStopAllTracks').addEventListener('click', () => {
if (!localStream) { alert('Stream not started'); return; }
localStream.getTracks().forEach(track => {
track.stop();
console.log(`Stopped track: ${track.kind} - ${track.label}`);
});
console.log('All tracks stopped. Stream is now inactive.');
localVideo.srcObject = null; // Clear the video element
// localStream is now inactive. You might want to nullify it: localStream = null;
});
// Start the media capture when the script loads
startMedia();

在这个更新的 client.js 中:

  • 我们使用 navigator.mediaDevices.getUserMedia(),它是基于 Promise 的,也是当前的标准。
  • 获取到的 MediaStream (localStream) 被赋值给 localVideo.srcObject 以显示它。这取代了之前用于流的 URL.createObjectURL() 方法。
  • getUserMedia 的错误处理更加健壮,捕获了特定的错误类型。
  • <video> 元素具有 playsinline(对于 iOS Safari 阻止全屏很重要)和 muted(对于本地视频防止音频反馈/回声至关重要,如果也捕获了音频)。
  • 为按钮添加了事件监听器,以演示各种 MediaStream API 方法。

要运行此示例,请在现代浏览器中打开 index.html(最好通过本地 Web 服务器,例如使用 npx serve 或 Python 的 http.server)。浏览器会请求使用您的摄像头和麦克风的权限。授予权限后,您的视频应该会出现在页面上。原始教程展示了此权限提示和产生的视频流的图片。

  • active(只读 boolean):如果 MediaStream 处于活动状态(即,至少一个轨道未结束),则返回 true;否则返回 false。
  • id(只读 string):MediaStream 对象的唯一标识符(GUID,全局唯一标识符)。
  • ended(只读 boolean, Deprecated):如果 ended 事件已触发,表示流已完全结束,则返回 true。请改用 active。

原始教程包含了一张图片,显示了浏览器控制台中的这些属性。您可以在开发者控制台中检查 localStream 对象以查看这些值。

事件处理程序(在 MediaStream 对象上)

Section titled “事件处理程序(在 MediaStream 对象上)”
  • onactive:当 MediaStream 对象变为活动状态时触发。(较少直接使用;通常检查 active 属性)。
  • oninactive:当 MediaStream 对象变为非活动状态时触发(例如,其所有轨道都已结束)。
  • onaddtrack:当新的 MediaStreamTrack 对象被添加到此 MediaStream 时触发(例如,通过 addTrack())。
  • onremovetrack:当 MediaStreamTrack 对象从此 MediaStream 中移除时触发(例如,通过 removeTrack())。
  • onended(Deprecated):当流终止时触发。请监听单个轨道的 ended 事件或检查 MediaStream.active。
  • addTrack(track):将给定的 MediaStreamTrack 添加到此 MediaStream。
  • clone():返回一个新 MediaStream 对象,它是此流的克隆。新流将具有一个新的唯一 id,但将包含相同的 MediaStreamTrack 对象集(按引用)。
  • getAudioTracks():返回此流中所有 kind 属性为 "audio" 的 MediaStreamTrack 对象数组。
  • getVideoTracks():返回此流中所有 kind 属性为 "video" 的 MediaStreamTrack 对象数组。
  • getTracks():返回此流中所有 MediaStreamTrack 对象数组,无论其类型如何。
  • getTrackById(trackId):返回此流中具有指定 id 的 MediaStreamTrack 对象。如果没有匹配的轨道,则返回 null。
  • removeTrack(track):从此 MediaStream 中移除指定的 MediaStreamTrack。这不会停止轨道本身;它只是将其从此特定流中移除。

上面的 client.js 中的按钮驱动示例演示了这些方法。例如,点击“Log Audio Tracks”使用 getAudioTracks()。点击“Remove First Video Track”使用 getVideoTracks() 获取轨道,然后使用 removeTrack() 移除它。原始教程包含了一些图片,展示了点击类似按钮后控制台输出的轨道数组或特定轨道信息。

一个 MediaStreamTrack 对象也有重要的方法,例如 stop()(停止轨道并释放硬件资源)以及属性,例如 kind('audio' 或 'video')、label(描述性名称)和 enabled(用于静音/取消静音或隐藏/显示)。

本章概述了 MediaStream API 并演示了其基本用法。您现在应该对如何访问本地媒体设备以及管理媒体流和轨道有了更清晰的理解,它们是任何涉及音频或视频的 WebRTC 应用的基本构建块。