Estensioni di sorgenti multimediali

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE) è un'API JavaScript che consente di creare stream per la riproduzione da segmenti di audio o video. Anche se non è trattato in questo articolo, è necessario conoscere MSE se vuoi incorporare nel tuo sito video che, ad esempio:

  • Streaming adattivo, ovvero un altro modo per dire adattamento alle funzionalità del dispositivo e alle condizioni della rete
  • Splicing adattivo, ad esempio l'inserimento di annunci
  • Spostamento temporale
  • Controllo delle prestazioni e delle dimensioni del download
Flusso di dati di base di MSE
Figura 1: Flusso di dati di base di MSE

Puoi quasi considerare l'MSE come una catena. Come illustrato nella figura, tra il file scaricato e gli elementi multimediali sono presenti diversi livelli.

  • Un elemento <audio> o <video> per riprodurre i contenuti multimediali.
  • Un'istanza MediaSource con un SourceBuffer per alimentare l'elemento multimediale.
  • Una chiamata fetch() o XHR per recuperare i dati multimediali in un oggetto Response.
  • Una chiamata a Response.arrayBuffer() per nutrire MediaSource.SourceBuffer.

In pratica, la catena ha il seguente aspetto:

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Se riesci a capire cosa fare dalle spiegazioni finora fornite, non esitare a interrompere la lettura. Se vuoi una spiegazione più dettagliata, continua a leggere. Analizzerò questa catena creando un esempio di MSE di base. Ogni passaggio di compilazione aggiungerà codice al passaggio precedente.

Una nota sulla chiarezza

Questo articolo ti fornirà tutte le informazioni necessarie per riprodurre contenuti multimediali su una pagina web? No, ha lo scopo di aiutarti a comprendere il codice più complicato che potresti trovare altrove. Per chiarezza, questo documento semplifica e esclude molte cose. Pensiamo di potercela cavare perché consigliamo anche di utilizzare una libreria come Shaka Player di Google. Farò notare dove sto semplificando deliberatamente.

Aspetti non coperti

Ecco, in nessun ordine particolare, alcune cose che non tratterò.

  • Controlli di riproduzione. Li riceviamo senza costi grazie all'utilizzo degli elementi HTML5 <audio> e <video>.
  • Gestione degli errori:

Per l'utilizzo in ambienti di produzione

Ecco alcuni aspetti che consiglio per l'utilizzo in produzione delle API correlate a MSE:

  • Prima di effettuare chiamate a queste API, gestisci eventuali eventi di errore o eccezioni API e controlla HTMLMediaElement.readyState e MediaSource.readyState. Questi valori possono cambiare prima che gli eventi associati vengano pubblicati.
  • Assicurati che le chiamate appendBuffer() e remove() precedenti non siano ancora in corso controllando il valore booleano SourceBuffer.updating prima di aggiornare mode, timestampOffset, appendWindowStart, appendWindowEnd di SourceBuffer o di chiamare appendBuffer() o remove() su SourceBuffer.
  • Per tutte le istanze SourceBuffer aggiunte a MediaSource, assicurati che nessuno dei valori updating sia true prima di chiamare MediaSource.endOfStream() o aggiornare MediaSource.duration.
  • Se il valore MediaSource.readyState è ended, chiamate come appendBuffer() e remove() o l'impostazione di SourceBuffer.mode o SourceBuffer.timestampOffset causeranno il passaggio di questo valore a open. Ciò significa che devi essere preparato a gestire più eventi sourceopen.
  • Quando gestisci gli eventi HTMLMediaElement error, i contenuti di MediaError.message possono essere utili per determinare la causa principale dell'errore, soprattutto per gli errori difficili da riprodurre negli ambienti di test.

Collega un'istanza MediaSource a un elemento multimediale

Come per molte cose nello sviluppo web di oggi, iniziamo con il rilevamento delle funzionalità. Quindi, recupera un elemento multimediale, <audio> o <video>. Infine, crea un'istanza di MediaSource. Viene trasformato in un URL e passato all'attributo source dell'elemento multimediale.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
Un attributo di origine come blob
Figura 1: un attributo di origine come blob

Il fatto che un oggetto MediaSource possa essere passato a un attributo src potrebbe sembrare un po' strano. In genere sono stringhe, ma possono anche essere blob. Se ispezioni una pagina con contenuti multimediali incorporati ed esamini l'elemento multimediale, capirai cosa intendo.

L'istanza MediaSource è pronta?

URL.createObjectURL() è sincrono, ma elabora l'allegato in modo asincrono. Ciò causa un leggero ritardo prima che tu possa fare qualsiasi cosa con l'istanza MediaSource. Fortunatamente, esistono dei modi per verificare. Il modo più semplice è utilizzare una proprietà MediaSource denominata readyState. La proprietà readyState descrive la relazione tra un'istanza MediaSource e un elemento multimediale. Può avere uno dei seguenti valori:

  • closed: l'istanza MediaSource non è associata a un elemento multimediale.
  • open: l'istanza MediaSource è collegata a un elemento multimediale ed è pronta a ricevere dati o sta ricevendo dati.
  • ended: l'istanza MediaSource è collegata a un elemento multimediale e tutti i suoi dati sono stati passati a quell'elemento.

Eseguire query direttamente su queste opzioni può influire negativamente sul rendimento. Fortunatamente, MediaSource attiva anche eventi quando readyState cambia, in particolare sourceopen, sourceclosed, sourceended. Per l'esempio che sto creando, utilizzerò l'evento sourceopen per sapere quando recuperare e mettere in buffer il video.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

Tieni presente che ho chiamato anche revokeObjectURL(). So che sembra prematuro, ma posso farlo in qualsiasi momento dopo che l'attributo src dell'elemento multimediale è stato collegato a un'istanza MediaSource. La chiamata di questo metodo non distrugge alcun oggetto. Consente alla piattaforma di gestire la raccolta dei rifiuti in un momento opportuno, motivo per cui la richiamo immediatamente.

Creare un SourceBuffer

Ora è il momento di creare SourceBuffer, l'oggetto che effettivamente si occupa di trasferire i dati tra origini multimediali ed elementi multimediali. Un valore SourceBuffer deve essere specifico per il tipo di file multimediale che stai caricando.

In pratica, puoi farlo chiamando addSourceBuffer() con il valore appropriato. Tieni presente che nell'esempio seguente la stringa del tipo MIME contiene un tipo MIME e due codec. Si tratta di una stringa MIME per un file video, ma utilizza codec distinti per le parti video e audio del file.

La versione 1 della specifica MSE consente agli user agent di differire in base al fatto che richiedono o meno sia un tipo MIME sia un codec. Alcuni user agent non richiedono, ma consentono solo il tipo MIME. Alcuni user agent, ad esempio Chrome, richiedono un codec per i tipi mime che non descrivono autonomamente i propri codec. Invece di provare a risolvere il problema, è meglio includere entrambi.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

Recuperare il file multimediale

Se cerchi su internet esempi di MSE, ne troverai molti che recuperano i file multimediali utilizzando XHR. Per essere più all'avanguardia, utilizzerò l'API Fetch e la promessa che restituisce. Se stai tentando di eseguire questa operazione in Safari, non funzionerà senza un fetch() polyfill.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

Un player di qualità di produzione avrebbe lo stesso file in più versioni per supportare browser diversi. Potrebbe utilizzare file separati per audio e video per consentire la selezione dell'audio in base alle impostazioni della lingua.

Il codice reale avrebbe anche più copie dei file multimediali a risoluzioni diverse, in modo da potersi adattare alle diverse funzionalità dei dispositivi e alle condizioni della rete. Questa applicazione è in grado di caricare e riprodurre i video in blocchi utilizzando richieste di intervallo o segmenti. Ciò consente di adattarsi alle condizioni della rete durante la riproduzione dei contenuti multimediali. Potresti aver sentito i termini DASH o HLS, che sono due metodi per farlo. Una discussione completa di questo argomento esula dall'ambito di questa introduzione.

Elabora l'oggetto di risposta

Il codice sembra quasi completo, ma i contenuti multimediali non vengono riprodotti. Dobbiamo trasferire i dati media dall'oggetto Response all'oggetto SourceBuffer.

Il modo tipico per passare i dati dall'oggetto di risposta all'istanza MediaSource è ottenere un ArrayBuffer dall'oggetto di risposta e passarlo al SourceBuffer. Inizia chiamando response.arrayBuffer(), che restituisce una promessa al buffer. Nel mio codice, ho passato questa promessa a una seconda clausola then() dove la aggiungo a SourceBuffer.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

Chiama endOfStream()

Dopo aver aggiunto tutti i ArrayBuffers e non sono previsti ulteriori dati multimediali, chiama MediaSource.endOfStream(). In questo modo, MediaSource.readyState diventerà ended e verrà attivato l'evento sourceended.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

La versione finale

Ecco l'esempio di codice completo. Spero che tu abbia imparato qualcosa sulle estensioni Media Source.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Feedback