媒体设备使用入门

在针对 Web 进行开发时,WebRTC 标准提供了用于访问连接到计算机或智能手机的摄像头和麦克风的 API。这些设备通常称为“媒体设备”,可通过实现 MediaDevices 接口的 navigator.mediaDevices 对象使用 JavaScript 进行访问。在此对象中,我们可以枚举所有已连接的设备,监听设备更改(当设备连接或断开连接时),并打开设备以检索媒体流(见下文)。

最常见的用法是通过函数 getUserMedia(),该函数会返回一个针对匹配的媒体设备解析为 MediaStream 的 promise。此函数接受单个 MediaStreamConstraints 对象,用于指定我们的要求。例如,如需简单地打开默认麦克风和摄像头,我们可以执行以下操作。

使用 Promise

const constraints = {
    'video': true,
    'audio': true
}
navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
        console.log('Got MediaStream:', stream);
    })
    .catch(error => {
        console.error('Error accessing media devices.', error);
    });

使用 async/await

const openMediaDevices = async (constraints) => {
    return await navigator.mediaDevices.getUserMedia(constraints);
}

try {
    const stream = openMediaDevices({'video':true,'audio':true});
    console.log('Got MediaStream:', stream);
} catch(error) {
    console.error('Error accessing media devices.', error);
}

调用 getUserMedia() 将触发权限请求。如果用户接受该权限,系统会使用包含一个视频和一个音轨的 MediaStream 解析 promise。如果权限遭拒,系统会抛出 PermissionDeniedError。如果没有连接任何匹配设备,系统会抛出 NotFoundError

有关 MediaDevices 接口的完整 API 参考文档,请参阅 MDN Web 文档

查询媒体设备

在更复杂的应用中,我们很可能需要检查所有连接的摄像头和麦克风,并向用户提供适当的反馈。这可以通过调用函数 enumerateDevices() 来实现。这将返回一个解析为描述每个已知媒体设备的 MediaDevicesInfo 数组的 promise。我们可以使用它向用户显示一个界面,让用户选择他们喜欢的界面。每个 MediaDevicesInfo 都包含一个名为 kind 的属性,其值为 audioinputaudiooutputvideoinput,用于指示媒体设备的类型。

使用 Promise

function getConnectedDevices(type, callback) {
    navigator.mediaDevices.enumerateDevices()
        .then(devices => {
            const filtered = devices.filter(device => device.kind === type);
            callback(filtered);
        });
}

getConnectedDevices('videoinput', cameras => console.log('Cameras found', cameras));

使用 async/await

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

const videoCameras = getConnectedDevices('videoinput');
console.log('Cameras found:', videoCameras);

监听设备更改

大多数计算机都支持在运行时插入各种设备。它可能是通过 USB 连接的摄像头、蓝牙耳机或一组外部扬声器。为了适当地支持此功能,Web 应用应监听媒体设备的更改。为此,可以针对 devicechange 事件向 navigator.mediaDevices 添加监听器。

// Updates the select element with the provided set of cameras
function updateCameraList(cameras) {
    const listElement = document.querySelector('select#availableCameras');
    listElement.innerHTML = '';
    cameras.map(camera => {
        const cameraOption = document.createElement('option');
        cameraOption.label = camera.label;
        cameraOption.value = camera.deviceId;
    }).forEach(cameraOption => listElement.add(cameraOption));
}

// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
updateCameraList(videoCameras);

// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
    const newCameraList = getConnectedDevices('video');
    updateCameraList(newCameraList);
});

媒体限制

约束对象必须实现 MediaStreamConstraints 接口,我们将其作为参数传递给 getUserMedia(),这样我们就能够打开符合特定要求的媒体设备。此要求可以非常宽泛(音频和/或视频),也可以非常具体(最低相机分辨率或确切的设备 ID)。对于使用 getUserMedia() API 的应用,建议先检查现有设备,然后使用 deviceId 约束条件指定与确切设备匹配的约束条件。在可能的情况下,设备还将根据约束条件进行配置。我们可以在麦克风上启用回声消除,也可以为摄像头设置视频的特定或最小宽度和高度。

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Open camera with at least minWidth and minHeight capabilities
async function openCamera(cameraId, minWidth, minHeight) {
    const constraints = {
        'audio': {'echoCancellation': true},
        'video': {
            'deviceId': cameraId,
            'width': {'min': minWidth},
            'height': {'min': minHeight}
            }
        }

    return await navigator.mediaDevices.getUserMedia(constraints);
}

const cameras = getConnectedDevices('videoinput');
if (cameras && cameras.length > 0) {
    // Open first available video camera with a resolution of 1280x720 pixels
    const stream = openCamera(cameras[0].deviceId, 1280, 720);
}

有关 MediaStreamConstraints 接口的完整文档,请参阅 MDN Web 文档

本地播放

打开媒体设备并且我们有可用的 MediaStream 后,我们可以将其分配给视频或音频元素,以在本地播放音频流。

async function playVideoFromCamera() {
    try {
        const constraints = {'video': true, 'audio': true};
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const videoElement = document.querySelector('video#localVideo');
        videoElement.srcObject = stream;
    } catch(error) {
        console.error('Error opening video camera.', error);
    }
}

getUserMedia() 配合使用的典型视频元素所需的 HTML 通常具有 autoplayplaysinline 属性。autoplay 属性会导致分配给元素的新流自动播放。 playsinline 属性允许视频在某些移动浏览器中以内嵌方式播放,而不是仅以全屏模式播放。此外,我们还建议对直播使用 controls="false",除非用户能够暂停直播。

<html>
<head><title>Local video playback</title></head>
<body>
    <video id="localVideo" autoplay playsinline controls="false"/>
</body>
</html>