Estensioni origine multimediale

François Beaufort
François Beaufort
Mario Rossi
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 comprendere la MSE se vuoi incorporare nel tuo sito video che includono:

  • Streaming adattivo, ovvero un altro modo per adattarsi alle funzionalità del dispositivo e alle condizioni della rete
  • Accoppiamento adattivo, ad esempio l'inserimento di annunci
  • Variazione temporale
  • Controllo del rendimento e delle dimensioni del download
Flusso di dati MSE di base
Figura 1: flusso di dati MSE di base

Possiamo quasi pensare a MSE come a 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 il feed dell'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 risolvere il problema tra le spiegazioni fornite finora, non esitare a smettere di leggere ora. Per una spiegazione più dettagliata, continua a leggere. Analizzerò questa catena creando un esempio di MSE di base. Ciascuno dei passaggi di build aggiungerà codice al passaggio precedente.

Una nota sulla chiarezza

Questo articolo ti spiega tutto ciò che devi sapere sulla riproduzione di contenuti multimediali su una pagina web? No, è destinata solo ad aiutarti a comprendere codice più complicato che potresti trovare altrove. Per maggiore chiarezza, questo documento semplifica ed esclude molti aspetti. Riteniamo che possiamo farla franca perché ti consigliamo di usare anche una libreria come Google's Shaka Player. Ne avrò volutamente nota la semplificazione.

Alcuni aspetti non trattati

Qui, senza ordine particolare, ci sono alcuni degli aspetti che non tratterò.

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

Per l'utilizzo in ambienti di produzione

Ecco alcuni suggerimenti per un utilizzo in produzione di API relative a MSE:

  • Prima di effettuare chiamate su queste API, gestisci eventuali eventi di errore o eccezioni alle API e controlla i criteri HTMLMediaElement.readyState e MediaSource.readyState. Questi valori possono cambiare prima della pubblicazione degli eventi associati.
  • 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 e SourceBuffer di SourceBuffer o chiamare appendBuffer() o remove() su SourceBuffer.
  • Per tutte le istanze SourceBuffer aggiunte a MediaSource, assicurati che nessuno dei rispettivi valori updating sia vero prima di chiamare MediaSource.endOfStream() o aggiornare MediaSource.duration.
  • Se il valore di 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 dovresti essere pronto 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, in particolare per gli errori difficili da riprodurre negli ambienti di test.

Collega un'istanza MediaSource a un elemento multimediale

Come per molte altre cose nello sviluppo web oggigiorno, il rilevamento delle funzionalità inizia. A questo punto, ottieni un elemento multimediale, che sia <audio> o <video>. Infine, crea un'istanza di MediaSource. Viene trasformato in un URL e trasmesso all'attributo sorgente 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 sotto forma di blob

Il fatto che un oggetto MediaSource possa essere trasmesso 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() è a sua volta sincrono, ma elabora l'allegato in modo asincrono. Questo causa un leggero ritardo prima di poter eseguire qualsiasi operazione con l'istanza MediaSource. Fortunatamente, esistono dei modi per farlo. Il modo più semplice consiste nell'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 o ricevere dati.
  • ended. L'istanza MediaSource è collegata a un elemento multimediale e tutti i suoi dati sono stati trasmessi a questo elemento.

L'esecuzione di query dirette su queste opzioni può influire negativamente sulle prestazioni. Fortunatamente, MediaSource attiva anche gli eventi quando readyState cambia, in particolare sourceopen, sourceclosed, sourceended. Per l'esempio che sto creando, utilizzerò l'evento sourceopen per indicarmi quando recuperare e eseguire il buffering del 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>

Noterai che ho chiamato anche revokeObjectURL(). So che potrebbe sembrare prematuro, ma posso farlo in qualsiasi momento dopo che l'attributo src dell'elemento multimediale è stato connesso a un'istanza MediaSource. La chiamata di questo metodo non elimina alcun oggetto. Consente alla piattaforma di gestire la garbage collection in un momento appropriato, motivo per cui la chiamo immediatamente.

Crea un buffer di origine

Ora è il momento di creare SourceBuffer, l'oggetto che di fatto esegue il trasferimento dei dati tra le origini multimediali e gli elementi multimediali. Un elemento SourceBuffer deve essere specifico per il tipo di file multimediale che stai caricando.

In pratica, puoi eseguire questa operazione chiamando addSourceBuffer() con il valore appropriato. Nota che nell'esempio seguente la stringa di tipo MIME contiene un tipo MIME e due codec. Questa è una stringa MIME per un file video, ma utilizza codec separati per le parti audio e video.

La versione 1 della specifica MSE consente agli user agent di differire in base alla necessità o meno di un tipo MIME e un codec. Alcuni user agent non richiedono, ma consentono solo il tipo MIME. Alcuni user agent, Chrome ad esempio, richiedono un codec per i tipi MIME che non descrivono autonomamente i relativi codec. Invece di cercare di risolvere tutto questo, è meglio includerli 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>;
}

Scarica il file multimediale

Se cerchi esempi di MSE su Internet, ne troverai moltissimi che recuperano i file multimediali utilizzando XHR. Per essere più all'avanguardia, utilizzerò l'API Fetch e la funzionalità Promise 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 della qualità in 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 di file multimediali a risoluzioni diverse, in modo da potersi adattare alle diverse funzionalità del dispositivo e alle diverse condizioni della rete. Un'applicazione di questo tipo è in grado di caricare e riprodurre i video in blocchi utilizzando richieste di intervalli o segmenti. Ciò consente l'adattamento alle condizioni della rete durante la riproduzione di contenuti multimediali. Potresti aver già sentito i termini DASH o HLS, che sono due metodi per raggiungere questo obiettivo. Una discussione completa su questo argomento va oltre l'ambito di questa introduzione.

Elabora l'oggetto di risposta

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

Il modo tipico per passare i dati dall'oggetto risposta all'istanza MediaSource è ottenere un ArrayBuffer dall'oggetto di risposta e passarlo a SourceBuffer. Inizia chiamando response.arrayBuffer(), che restituisce una promessa al buffer. Nel mio codice ho trasmesso questa promessa a una seconda clausola then(), in cui 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 dopo che non sono previsti ulteriori dati dei contenuti multimediali, chiama MediaSource.endOfStream(). 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