Guida introduttiva ai dispositivi multimediali

Durante lo sviluppo per il web, lo standard WebRTC fornisce API per l'accesso a videocamere e microfoni collegati al computer o allo smartphone. Questi dispositivi sono comunemente indicati come dispositivi multimediali e sono accessibili tramite JavaScript tramite l'oggetto navigator.mediaDevices, che implementa l'interfaccia MediaDevices. Da questo oggetto possiamo enumerare tutti i dispositivi connessi, ascoltare le modifiche ai dispositivi (quando un dispositivo è connesso o disconnesso) e aprire un dispositivo per recuperare uno stream multimediale (vedi di seguito).

Il modo più comune per questo tipo di utilizzo è tramite la funzione getUserMedia(), che restituisce una promessa che corrisponderà a un MediaStream per i dispositivi multimediali corrispondenti. Questa funzione accetta un singolo oggetto MediaStreamConstraints che specifica i requisiti previsti. Ad esempio, per aprire il microfono e la fotocamera predefiniti, procedi nel seguente modo.

Utilizzo di 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);
    });

Utilizzo asincrono/attende

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);
}

La chiamata a getUserMedia() attiverà una richiesta di autorizzazione. Se l'utente accetta l'autorizzazione, la promessa viene risolta con un MediaStream contenente un video e una traccia audio. Se l'autorizzazione viene negata, viene generato un valore PermissionDeniedError. Nel caso in cui non siano connessi dispositivi corrispondenti, verrà generato un NotFoundError.

Il riferimento API completo per l'interfaccia MediaDevices è disponibile nella documentazione web MDN.

Esecuzione di query sui dispositivi multimediali

In un'applicazione più complessa, è molto probabile che vogliamo controllare tutte le videocamere e i microfoni collegati e fornire il feedback appropriato all'utente. A questo scopo, chiama la funzione enumerateDevices(). Verrà restituita una promessa che si risolve in un array di MediaDevicesInfo che descrive ogni dispositivo multimediale noto. Possiamo utilizzarla per presentare all'utente una UI che gli consenta di scegliere quella che preferisce. Ogni MediaDevicesInfo contiene una proprietà denominata kind con il valore audioinput, audiooutput o videoinput, che indica di che tipo di dispositivo multimediale si tratta.

Utilizzo di 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));

Utilizzo asincrono/attende

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);

Ascolto dei cambiamenti dei dispositivi

La maggior parte dei computer supporta il collegamento di vari dispositivi durante il runtime. Potrebbe essere una webcam collegata tramite USB, un paio di cuffie Bluetooth o un gruppo di altoparlanti esterni. Per supportare correttamente ciò, un'applicazione web deve rimanere in ascolto dei cambiamenti dei dispositivi multimediali. A questo scopo, aggiungi un listener a navigator.mediaDevices per l'evento devicechange.

// 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);
});

Vincoli multimediali

L'oggetto vincoli, che deve implementare l'interfaccia MediaStreamConstraints, che passiamo come parametro a getUserMedia(), ci consente di aprire un dispositivo multimediale che soddisfa un determinato requisito. Questo requisito può essere definito in modo molto generico (audio e/o video) o molto specifico (risoluzione minima della videocamera o un ID dispositivo esatto). È consigliabile che le applicazioni che utilizzano l'API getUserMedia() controllino innanzitutto i dispositivi esistenti, quindi specifichino un vincolo che corrisponde esattamente al dispositivo utilizzando il vincolo deviceId. Inoltre, i dispositivi saranno, se possibile, configurati in base ai vincoli. Possiamo abilitare la cancellazione dell'eco sui microfoni o impostare una larghezza e un'altezza specifiche o minime del video dalla videocamera.

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);
}

La documentazione completa per l'interfaccia MediaStreamConstraints è disponibile nei documenti web MDN.

Riproduzione locale

Dopo aver aperto un dispositivo multimediale e aver a disposizione un MediaStream, possiamo assegnarlo a un elemento video o audio per riprodurre lo stream localmente.

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);
    }
}

Il codice HTML necessario per un tipico elemento video utilizzato con getUserMedia() avrà in genere gli attributi autoplay e playsinline. A causa dell'attributo autoplay, i nuovi stream assegnati all'elemento verranno riprodotti automaticamente. L'attributo playsinline consente la riproduzione del video in linea, anziché solo a schermo intero, su alcuni browser mobile. Consigliamo inoltre di utilizzare controls="false" per i live streaming, a meno che l'utente non sia in grado di metterli in pausa.

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