Estensioni origine multimediale

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

L'MSE è quasi come una catena. Come illustrato in figura, tra il file scaricato e gli elementi multimediali ci sono 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 si presenta così:

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, puoi interrompere la lettura. Per 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 maggiore chiarezza, questo documento semplifica ed esclude molte cose. Pensiamo di poter fare l'una sull'altra, perché consigliamo anche di usare 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 consigli 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 la transizione 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.

Collegamento di un'istanza MediaSource a un elemento multimediale

Come per molti aspetti dello sviluppo web di questi tempi, si inizia con il rilevamento delle funzionalità. Poi, recupera un elemento multimediale, che può essere un elemento <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. Di solito sono stringhe, ma possono anche essere blob. Se esamini una pagina con contenuti multimediali incorporati ed esamini il relativo 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 eseguire qualsiasi operazione 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 è collegata 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 dirmi quando recuperare e buffering 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 appropriato, 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. Piuttosto che cercare di risolvere tutti i problemi, è 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 esempi di MSE su internet, 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 cercando di farlo in Safari, non funzionerà senza un polyfill fetch().

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 l'adattamento alle condizioni della rete durante la riproduzione di 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 risposta all'istanza MediaSource è ottenere un ArrayBuffer dall'oggetto risposta e passarlo a SourceBuffer. Inizia chiamando response.arrayBuffer(), che restituisce una promessa al buffer. Nel mio codice, ho passato questa promessa a una seconda clausola then() in cui la aggiungo alla 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 su Media Source Extensions.

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