Расширения медиа-источников

Франсуа Бофор
François Beaufort
Джо Медли
Joe Medley

Media Source Extensions (MSE) — это API JavaScript, который позволяет создавать потоки для воспроизведения из сегментов аудио или видео. Хотя это и не рассматривается в этой статье, понимание MSE необходимо, если вы хотите встроить на свой сайт видео, которые выполняют такие функции, как:

  • Адаптивная потоковая передача, иначе говоря, адаптация к возможностям устройства и условиям сети.
  • Адаптивное сращивание, например вставка рекламы.
  • Сдвиг времени
  • Контроль производительности и размера загрузки
Базовый поток данных MSE
Рисунок 1. Базовый поток данных MSE.

Вы можете думать о MSE как о сети. Как показано на рисунке, между загруженным файлом и медиа-элементами находится несколько слоев.

  • Элемент <audio> или <video> для воспроизведения мультимедиа.
  • Экземпляр MediaSource с SourceBuffer для подачи медиа-элемента.
  • Вызов fetch() или XHR для получения медиаданных в объекте Response .
  • Вызов Response.arrayBuffer() для подачи MediaSource.SourceBuffer .

На практике цепочка выглядит так:

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

Если вы можете разобраться во всем из объяснений, не стесняйтесь прекратить чтение прямо сейчас. Если вы хотите более подробное объяснение, продолжайте читать. Я собираюсь пройти по этой цепочке, построив базовый пример MSE. Каждый из шагов сборки добавит код к предыдущему шагу.

Примечание о ясности

Расскажет ли эта статья все, что вам нужно знать о воспроизведении мультимедиа на веб-странице? Нет, он предназначен только для того, чтобы помочь вам понять более сложный код, который вы можете найти где-то еще. Для ясности этот документ упрощает и исключает многие вещи. Мы думаем, что нам это сойдет с рук, поскольку мы также рекомендуем использовать такую ​​библиотеку, как Google Shaka Player . Я буду отмечать все места, где намеренно упрощаю.

Несколько вещей, не охваченных

Вот несколько вещей, которые я не буду освещать в произвольном порядке.

  • Управление воспроизведением. Мы получаем их бесплатно благодаря использованию элементов HTML5 <audio> и <video> .
  • Обработка ошибок.

Для использования в производственных средах

Вот некоторые вещи, которые я бы порекомендовал при использовании API, связанных с MSE:

  • Прежде чем выполнять вызовы этих API, обработайте все события ошибок или исключения API и проверьте HTMLMediaElement.readyState и MediaSource.readyState . Эти значения могут измениться до доставки связанных событий.
  • Убедитесь, что предыдущие вызовы appendBuffer() и remove() еще не выполняются, проверив логическое значение SourceBuffer.updating перед обновлением mode SourceBuffer , timestampOffset , appendWindowStart , appendWindowEnd или вызовом appendBuffer() или remove() в SourceBuffer .
  • Для всех экземпляров SourceBuffer , добавленных в ваш MediaSource , убедитесь, что ни одно из их updating значений не является истинным, прежде чем вызывать MediaSource.endOfStream() или обновлять MediaSource.duration .
  • Если значение MediaSource.readyState ended , вызовы типа appendBuffer() и remove() или установка SourceBuffer.mode или SourceBuffer.timestampOffset приведут к переходу этого значения на open . Это означает, что вы должны быть готовы обрабатывать несколько событий sourceopen .
  • При обработке событий HTMLMediaElement error содержимое MediaError.message может быть полезно для определения основной причины сбоя, особенно для ошибок, которые трудно воспроизвести в тестовых средах.

Прикрепите экземпляр MediaSource к медиа-элементу.

Как и многое другое в современной веб-разработке, вы начинаете с обнаружения функций. Затем получите медиа-элемент: <audio> или <video> . Наконец создайте экземпляр MediaSource . Он преобразуется в URL-адрес и передается в исходный атрибут медиа-элемента.

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.');
}
Атрибут источника в виде большого двоичного объекта
Рисунок 1. Атрибут источника в виде большого двоичного объекта.

То, что объект MediaSource можно передать в атрибут src , может показаться немного странным. Обычно это строки, но они также могут быть BLOB-объектами . Если вы проверите страницу со встроенным мультимедиа и изучите ее медиа-элемент, вы поймете, что я имею в виду.

Готов ли экземпляр MediaSource?

URL.createObjectURL() сам по себе синхронен; однако он обрабатывает вложение асинхронно. Это вызывает небольшую задержку, прежде чем вы сможете что-либо сделать с экземпляром MediaSource . К счастью, есть способы проверить это. Самый простой способ — использовать свойство MediaSource под названием readyState . Свойство readyState описывает связь между экземпляром MediaSource и медиа-элементом. Он может иметь одно из следующих значений:

  • closed — экземпляр MediaSource не прикреплен к элементу мультимедиа.
  • open — экземпляр MediaSource прикреплен к элементу мультимедиа и готов к приему данных или принимает данные.
  • ended — экземпляр MediaSource прикреплен к элементу мультимедиа, и все его данные были переданы этому элементу.

Непосредственный запрос этих параметров может отрицательно повлиять на производительность. К счастью, MediaSource также генерирует события при изменении readyState , в частности, sourceopen , sourceclosed , sourceended . В примере, который я создаю, я собираюсь использовать событие sourceopen , чтобы сообщить мне, когда следует получить и буферизовать видео.

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>

Обратите внимание, что я также вызвал revokeObjectURL() . Я знаю, что это кажется преждевременным, но я могу сделать это в любое время после того, как атрибут src медиа-элемента будет подключен к экземпляру MediaSource . Вызов этого метода не уничтожает никакие объекты. Это позволяет платформе обрабатывать сбор мусора в подходящее время, поэтому я немедленно вызываю его.

Создайте исходный буфер

Теперь пришло время создать SourceBuffer — объект, который фактически выполняет работу по перераспределению данных между медиа-источниками и медиа-элементами. SourceBuffer должен соответствовать типу загружаемого медиафайла.

На практике это можно сделать, вызвав addSourceBuffer() с соответствующим значением. Обратите внимание, что в приведенном ниже примере строка типа mime содержит тип mime и два кодека. Это mime-строка для видеофайла, но она использует отдельные кодеки для видео- и аудиочастей файла.

Версия 1 спецификации MSE позволяет пользовательским агентам по-разному решать, требуют ли они как MIME-тип, так и кодек. Некоторые пользовательские агенты не требуют, но допускают только тип mime. Некоторым пользовательским агентам, например Chrome, требуется кодек для типов mime, которые не описывают свои кодеки самостоятельно. Вместо того, чтобы пытаться разобраться во всем этом, лучше просто включить оба.

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

Получить медиа-файл

Если вы выполните поиск в Интернете по примерам MSE, вы найдете множество примеров извлечения медиафайлов с использованием XHR. Чтобы быть более передовым, я собираюсь использовать Fetch API и возвращаемый им Promise . Если вы пытаетесь сделать это в Safari, это не сработает без полифила 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>;
}

Проигрыватель производственного качества будет иметь один и тот же файл в нескольких версиях для поддержки разных браузеров. Он может использовать отдельные файлы для аудио и видео, чтобы можно было выбирать звук в зависимости от языковых настроек.

Реальный код также будет иметь несколько копий медиафайлов с разными разрешениями, чтобы его можно было адаптировать к различным возможностям устройства и условиям сети. Такое приложение может загружать и воспроизводить видео частями, используя запросы диапазона или сегменты. Это позволяет адаптироваться к условиям сети во время воспроизведения мультимедиа . Возможно, вы слышали термины DASH или HLS — два метода достижения этой цели. Полное обсуждение этой темы выходит за рамки данного введения.

Обработать объект ответа

Код выглядит почти готовым, но медиафайлы не воспроизводятся. Нам нужно получить медиаданные из объекта Response в SourceBuffer .

Типичный способ передачи данных из объекта ответа в экземпляр MediaSource — получить ArrayBuffer из объекта ответа и передать его в SourceBuffer . Начните с вызова response.arrayBuffer() , который возвращает обещание в буфер. В моем коде я передал это обещание во второе предложение then() , где добавляю его в 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>
}

Вызов endOfStream()

После добавления всех ArrayBuffers и отсутствия дополнительных мультимедийных данных вызовите MediaSource.endOfStream() . Это изменит MediaSource.readyState на ended и вызовет событие 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);
    });
}

Окончательная версия

Вот полный пример кода. Надеюсь, вы узнали что-то о расширениях медиа-источников.

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

Обратная связь