Media Source Extensions (MSE): как устроен любой веб-плеер изнутри

Автор: Николай СапуновОбновлено: август 202633 мин чтения
Содержание статьи +

TL;DR

Media Source Extensions (сокращённо MSE) – небольшой веб-стандарт W3C, позволяющий JavaScript подавать элементу <video> короткие фрагменты MP4 вместо одного большого файла. На этом стандарте построен каждый адаптивный потоковый плеер, который вы когда-либо запускали в браузере. API простое: один корневой объект MediaSource, один буфер на дорожку SourceBuffer и один метод appendBuffer, – но инженерия вокруг него сложная: строки кодеков, race-условия на флаге updating, политика вытеснения с QuotaExceededError, переключение рендиций через changeType() и десятилетняя изоляция iPhone, завершившаяся лишь в октябре 2023 года с выходом Managed Media Source от Apple. В 2026 году ландшафт MSE формируют два крупных обновления: ManagedMediaSource в iOS 17.1 и новее – впервые функциональность MSE появилась в Safari на iPhone, – и MSE-в-воркере (MSE-in-Worker) в Chrome 108 и новее, который переносит всю работу с буфером из основного потока страницы. Эта статья подробно проходит API от начала до конца, демонстрирует канонический жизненный цикл на реальном коде, перечисляет типичные проблемы в продакшене и точно объясняет, чем отличается поведение при использовании ManagedMediaSource вместо обычного MSE.

Зачем это нужно

Если вы транслируете видео в браузер в любом серьёзном масштабе – вы используете MSE. Неважно, написали ли вы код самостоятельно или подключили hls.js, shaka-player, dash.js или video.js, поручив работу библиотеке. Эти четыре open-source плеера обеспечивают значительную долю воспроизведения на устройствах вне экосистемы Apple, и под их публичными API лежат одни и те же три-четыре вызова MSE – в одинаковом порядке, с теми же гонками состояний. После прочтения этой статьи продакт-менеджер сможет задать инженеру правильный диагностический вопрос, когда плеер зависает на iPhone, а senior-инженер получит полную ментальную модель API – включая обновления 2026 года (ManagedMediaSource, MSE-в-воркере и SourceBuffer.changeType()) – без необходимости читать спецификацию W3C от начала до конца. Знания о HLS или MPEG-DASH не требуются: всё необходимое объясняется по ходу. К концу статьи вы поймёте, почему в Safari на iPhone существует «60-секундная буферная ловушка», что вызывает сбой плеера с ошибкой QuotaExceededError в 23:00 по пятницам, и какая однострочная правка конфигурации плеера это исправляет.

Какую проблему решал MSE

До того как появился MSE, у браузера был ровно один способ проиграть видео. Страница включала элемент <video> с атрибутом src, указывающим на видеофайл, браузер скачивал файл, пользователь нажимал Play. Эта модель работает для трейлера на 10 МБ с маркетингового лендинга. Для адаптивного стриминга она не работает.

Адаптивному стримингу нужны три вещи, которых простой src дать не может. Плеер должен загружать видео небольшими фрагментами – сегментами, а не одним файлом, чтобы запуск происходил за 2 секунды, а не за 2 минуты. Он должен уметь менять качество в процессе воспроизведения – например, снижать разрешение с 1080p до 720p при ухудшении сети – без закрытия video-элемента. И, наконец, плеер должен продолжать выполнять эти действия бесконечно, даже когда пользователь ставит видео на паузу, перематывает, переключает вкладку, подключает наушники или поворачивает устройство. Модель «один атрибут source» не предоставляет API для таких задач.

Поэтому в 2013 году группа инженеров из Microsoft, Google, Netflix и BBC собрала в W3C новый API под названием Media Source Extensions, который дал JavaScript возможность передавать сырые байты в внутренний конвейер video-элемента. Первая Recommendation вышла 17 ноября 2016 года (W3C, Media Source Extensions™, 17 November 2016). Дальнейший документ – Media Source Extensions 2 – находится в статусе Working Draft на 4 ноября 2025 года и содержит ключевые дополнения 2020-х: MediaSourceHandle для работы в Worker, ManagedMediaSource для мобильных устройств и SourceBuffer.changeType() для переключения кодеков.

Всё в этой статье основано на этих двух документах.

Рис. 1. Video-элемент до и после MSE. Тег `<video>` не изменился; изменилось то, чем он питается.

Четыре объекта, с которыми вы фактически работаете

API MSE настолько компактно, что его можно выучить наизусть. Четыре основных объекта и несколько вспомогательных типов – всего около шести методов на все четыре объекта вместе. Эти объекты: MediaSource, SourceBuffer, MediaSourceHandle и ManagedMediaSource.

MediaSource – JavaScript-прокси для медиа-потока элемента video. Это объект, который вы создаёте первым, прикрепляете к <video> и передаёте контроллеру плеера на странице. Его задача – координировать один или несколько SourceBuffer и предоставлять три события жизненного цикла (sourceopen, sourceended, sourceclose), а также перечисление (enum) readyState с тремя значениями ("closed", "open", "ended"). Наиболее важны методы на границах: addSourceBuffer(mimeType) создаёт буфер для дорожки, endOfStream() сообщает, что новых сегментов больше не будет, а статический метод MediaSource.isTypeSupported(mimeType) проверяет, поддерживает ли браузер кодек, указанный в манифесте.

SourceBuffer – это JavaScript-объект, управляющий буфером декодируемого медиа внутри браузера. Каждый SourceBuffer представляет одну группу дорожек – обычно отдельно для видео и аудио, хотя обе могут находиться в одном буфере, если сегменты предварительно замуксованы. Через буфер проходит весь поток байтов: appendBuffer(arrayBuffer) добавляет скачанный сегмент внутрь, remove(start, end) удаляет временной диапазон, а changeType(newMimeType) позволяет плееру переключаться между кодеками H.264 и HEVC в процессе воспроизведения без пересоздания буфера. У SourceBuffer есть булевый флаг updating, который становится true, пока выполняется append или remove. Самая частая ошибка при работе с API – вызвать ещё один append, пока updating всё ещё true, и получить в ответ InvalidStateError. Перед следующим вызовом всегда слушайте событие updateend.

MediaSourceHandle – маленькая ручка, пересекающая границу между основным потоком страницы и dedicated Worker. Это ключ к MSE-in-Worker, которому будет посвящён отдельный раздел ниже. Сейчас достаточно запомнить один факт: MediaSourceHandle передаётся через postMessage как transferable – после передачи вы присваиваете её video.srcObject (не video.src), и владельцем буфера становится Worker.

ManagedMediaSource – подкласс MediaSource, поставляемый в Safari 17.1+ на iPhone, iPad и Mac. Его API добавляет два события (startstreaming и endstreaming) и одно свойство (streaming), а его SourceBuffer – экземпляры ManagedSourceBuffer, которые браузер может вытеснить в любой момент, сопровождая это событием bufferedchange. Это – и единственный путь – через который адаптивный стриминг стал доступен в Safari на iPhone. Всё, что в этой статье касается iPhone, на самом деле – история про ManagedMediaSource.

W3C включает это в черновик спецификации MSE 2 (W3C, Media Source Extensions™ 2, Working Draft, 4 ноября 2025).

Каноничный жизненный цикл – в рабочем коде

Жизненный цикл от «страница загрузилась» до «первый кадр на экране» одинаков в каждом плеере: шесть шагов, всегда в одном порядке.

// 1. Проверяем поддержку API и кодека.
const VIDEO_MIME = 'video/mp4; codecs="avc1.640028,mp4a.40.2"';
if (!('MediaSource' in window) || !MediaSource.isTypeSupported(VIDEO_MIME)) {
  throw new Error('MSE или кодек не поддерживаются');
}

// 2. Создаём MediaSource и прикрепляем к <video>.
const video = document.querySelector('video');
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);

// 3. Ждём событие 'sourceopen' прежде чем что-либо делать.
mediaSource.addEventListener('sourceopen', async () => {
  URL.revokeObjectURL(video.src);   // освобождаем blob URL — он больше не нужен
  const sourceBuffer = mediaSource.addSourceBuffer(VIDEO_MIME);
  sourceBuffer.mode = 'segments';   // 'segments' использует timestamps каждого сегмента

  // 4. Сначала кладём инициализационный сегмент.
  const init = await (await fetch('/init.mp4')).arrayBuffer();
  await appendAndWait(sourceBuffer, init);

  // 5. Кладём медиа-сегменты по одному.
  for (const url of segmentUrls) {
    const seg = await (await fetch(url)).arrayBuffer();
    await appendAndWait(sourceBuffer, seg);
  }

  // 6. Сообщаем браузеру, что сегментов больше нет.
  mediaSource.endOfStream();
});

function appendAndWait(sourceBuffer, data) {
  return new Promise((resolve, reject) => {
    sourceBuffer.addEventListener('updateend', resolve, { once: true });
    sourceBuffer.addEventListener('error', reject, { once: true });
    sourceBuffer.appendBuffer(data);
  });
}

Шесть вещей, которые стоит подчеркнуть в этом коде. Первое: проверка поддержки – всего одна строка, но она обязательна; плеер без isTypeSupported не заработает на Tizen-телевизоре без AV1. Второе: порядок вызовов важен – addSourceBuffer работает только после sourceopen, но ни в коем случае раньше; ранний вызов приведёт к выбросу InvalidStateError. Третье: инициализационный сегмент должен быть первым в append, потому что он содержит moov-атом, описывающий всю дорожку – частоту дискретизации, параметры кодека и разрешение. Четвёртое: каждый appendBuffer выполняется асинхронно, и перед следующим вызовом вы обязаны дождаться разрешения промиса updateend. Пятое: endOfStream() – это способ сообщить браузеру «я закончил», после чего MediaSource переходит из состояния "open" в "ended", и элемент video понимает, что общая длительность теперь окончательная. Шестое: URL-адреса blob нужно освобождать сразу после подключения source – иначе они будут удерживать ссылку на MediaSource и мешать сборщику мусора очистить страницу.

По сути, этот фрагмент – то, что делает любой браузерный плеер, включая те, что используются в Netflix. Различия заключаются в парсере манифеста, который формирует URL сегментов, в ABR-алгоритме, выбирающем нужную рендицию, и в политике управления буфером, определяющей, когда вызывать remove(). API MSE остаётся одинаковым.

Рис. 2. Шесть шагов жизненного цикла MSE. Video-элемент никогда не видит целого файла – он воспринимает поток коротких append.

Codec strings и строгий режим, который невозможно обойти

Каждый вызов в MSE передаёт строку MIME-тип с кодеками, точно описывающую содержимое байтов, которые вы собираетесь загрузить. Формат описан в RFC 6381 (IETF, RFC 6381, August 2011). Типичный пример для H.264 + AAC в формате HLS выглядит так: video/mp4; codecs="avc1.640028,mp4a.40.2". Часть avc1.640028 идентифицирует H.264 High Profile Level 4.0, а часть mp4a.40.2 – AAC-LC.

Два правила, на которые часто попадаются новички в MSE, таковы. Первое: MediaSource.isTypeSupported(mime) – это проба; она возвращает true, если браузер считает, что справится, но при этом ничего не фиксирует. Второе: mediaSource.addSourceBuffer(mime) строже пробы – он реально создаёт экземпляр парсера и конвейер декодера, и строка кодека, прошедшая isTypeSupported на десктопе, всё равно может вызвать ошибку NotSupportedError на Smart TV, чей конвейер декодера не поддерживает именно эту комбинацию профиля и уровня. Урок: тестировать оба вызова на каждом устройстве, на которое поставляется продукт, и показывать пользователю понятное сообщение «этот кодек не поддерживается на этом устройстве», а не позволять addSourceBuffer падать без объяснений.

Codec strings, используемые в 2026 году: avc1.<profile><level> для H.264, hvc1.<profile_space>.<profile>.<tier>L<level>.<constraints> для HEVC, vp09.<profile>.<level>.<bit_depth>... для VP9, av01.<profile>.<level>L<tier>.<bit_depth> для AV1 и mp4a.40.<object_type> или opus для аудио. Страница MDN «The codecs parameter in common media types» (Mozilla Developer Network, accessed May 2026) – практический справочник; мы указали ссылку в разделе References ниже.

Гонка на `updating` и очередь, которую вы обязаны построить

Самая частая ошибка в коде MSE – вызов appendBuffer, пока SourceBuffer ещё обрабатывает предыдущий append. SourceBuffer выставляет булев updating в true с момента вызова appendBuffer до срабатывания события updateend; вызов appendBuffer, remove, abort, changeType или мутация timestampOffset либо append window, пока updating равен true, бросают InvalidStateError мгновенно. Внутренней очереди в API нет.

Поэтому каждый продакшен-плеер строит свою очередь. Её структура одинакова в любой кодовой базе: массив отложенных операций, одна – в процессе выполнения, и слушатель на updateend, который запускает следующую операцию, как только текущая завершится. Паттерн короткий – приведу целиком.

class SbQueue {
  constructor(sourceBuffer) {
    this.sb = sourceBuffer;
    this.pending = [];
    this.sb.addEventListener('updateend', () => this.next());
  }
  enqueue(op) {
    this.pending.push(op);
    if (!this.sb.updating) this.next();
  }
  next() {
    const op = this.pending.shift();
    if (op) op();
  }
}

const q = new SbQueue(sourceBuffer);
q.enqueue(() => sourceBuffer.appendBuffer(init));
q.enqueue(() => sourceBuffer.appendBuffer(seg1));
q.enqueue(() => sourceBuffer.remove(0, 5));
q.enqueue(() => sourceBuffer.appendBuffer(seg2));

Senior-читатель увидит в этой очереди два открытых вопроса. Первый – что делать, когда вместо updateend приходит error: очередь застопорится, и плееру потребуется механизм восстановления. Второй – как поступить, если JavaScript должен вызвать abort(), чтобы прервать длительный append? Сам по себе abort сбрасывает updating в false и активирует updateend, из-за чего очередь продолжает движение, но фрагмент байт может оказаться неполным, и следующий append должен будет это компенсировать. У каждого зрелого плеера есть обёртка вокруг очереди, обрабатывающая оба случая; и у hls.js, и у shaka-player есть файл вроде media-buffer.ts или sourcebuffer-controller.js, который почти исключительно этим и занимается.

QuotaExceededError и история вытеснения

Плеер стриминга поддерживает буфер вперёд (forward-буфер) длительностью до 30 секунд для live-трансляций и до 2 минут для VOD-контента в SourceBuffer в любой момент времени. На первый взгляд это немного, но 1080p VOD-видео с битрейтом 8 Мбит/с создаёт около 60 МБ декодированных данных в минуту, а объём памяти медиа-конвейера браузера ограничен. Когда буфер заполняется, appendBuffer выбрасывает QuotaExceededError.

Спецификация W3C разрешает браузеру самостоятельно вытеснять старые данные при нехватке памяти, но не обязывает это делать; на практике Chromium удаляет QuotaExceededError, а Safari исторически более агрессивно применяет тихое вытеснение. В 2014 году команда Chrome опубликовала понятный пост, который остаётся актуальным и по сей день (Chrome Developers, «Handling QuotaExceededError», 2014, accessed May 2026), и в 2026 году практический подход не изменился: при возникновении QuotaExceededError следует вызвать sourceBuffer.remove(0, currentTime - keepBack), чтобы освободить часть старого буфера, дождаться updateend и повторить операцию append.

Реальное значение keepBack – около 5 секунд назад по времени, и оно отражает разницу между рабочим плеером и плеером, который тихо ломает перемотку. Типичная продакшен-настройка – держать около минуты позади курсора и около минуты впереди, с максимальным значением 90 секунд, и вызывать remove, когда разрыв превышает эти пределы.

Это одно из мест, где ManagedMediaSource в Safari на iPhone существенно отличается от десктопной версии MSE – об этом подробнее в разделе «исключение iPhone» ниже.

«Типичная ошибка. Наивный «фикс» на QuotaExceededError – «обернуть appendBuffer в try/catch и проглотить исключение». Такое решение приводит к тому, что плеер перестаёт загружать новые сегменты, а курсор продолжает двигаться вперёд. Через 20 секунд пользователь оказывается в тупике, из которого уже не выбраться. Правильный подход – освободить буфер через remove() и повторить попытку; если это не помогает – отправить ошибку в телеметрию и перевести плеер в режим восстановления. Ошибки – это сигнал, а не шум.»

Переключение кодеков без пересоздания: `changeType()`

Долгое время смена кодека в процессе воспроизведения означала необходимость уничтожить SourceBuffer и MediaSource и начать всё сначала. Такой подход работал, но вызывал заметную паузу и терял back-буфер; к тому же он нарушал работу любого плеера, который хотел переключаться между рендициями H.264 и HEVC или между H.264 Main Profile и High Profile посреди стрима.

В 2018 году W3C добавила SourceBuffer.changeType(mimeType) в спецификацию, а основные браузеры реализовали поддержку в 2019–2020 годах. Метод – всего одна строка в вызывающем коде – sourceBuffer.changeType('video/mp4; codecs="hvc1.1.6.L93.B0"') – сообщает объекту SourceBuffer, что последующие вызовы append будут содержать байты нового кодека, при этом уже загруженное медиа остаётся в буфере. Короткое демо доступно на официальном сайте примеров (Google Chrome Samples, «SourceBuffer.changeType», accessed May 2026): в одной сессии воспроизведения H.264 последовательно заменяется на HEVC, а затем на VP9.

Практический кейс 2026 года – двойной. Первый: переключение кодеков в ABR – плеер, у которого в лестнице битрейтов присутствует смесь кодеков (H.264 как резервный вариант на низких битрейтах и HEVC или AV1 на высоких), может переходить между ними при событиях буфера без необходимости пересоздания буфера. Второй: склейка рекламы с другим кодеком – server-side ad insertion, вставляющий креатив другого профиля, может опираться на changeType, чтобы плеер принял склейку без заметных для пользователя разрывов.

Две оговорки. Браузер должен поддерживать как исходящие, так и входящие MIME-type, и isTypeSupported следует вызывать на новом типе перед changeType. А changeType ставится в очередь через флаг updating – как и любой другой write, он находится в той же самой очереди, что описана выше.

MSE-in-Worker: разблокировка Chrome 108

До конца 2022 года все операции MSE выполнялись в главном потоке страницы. Парсинг буфера, проверка кодеков и сама appendBuffer конкурировали за CPU с рендер-циклом страницы. На вкладке с умеренно тяжёлым SPA парсинг 6-секундного сегмента мог превысить рендер-бюджет в 16,7 мс, из-за чего страница заметно подёргивалась.

В ноябре 2022 года в Chrome 108 появился MSE-in-Worker (Chromium, «MSE in Workers», Chrome Status feature 5177263249162240). Механизм основан на новом объекте MediaSourceHandle. Вы создаёте MediaSource внутри dedicated Worker, читаете его свойство .handle (MediaSourceHandle) и передаёте handle в главный поток через postMessage. В главном потоке вы присваиваете handle свойству video.srcObject (не video.src), и с этого момента вся ветка MSE работает внутри Worker – загрузка сегментов, транскодирование, добавление, вытеснение, всё.

Страница демонстрирует заметное снижение нагрузки на главный поток, а на устройствах с двумя и более ядрами эффект особенно выражен: 4K-воспроизведение на среднем ноутбуке, ранее прерывавшееся при прокрутке страницы, теперь работает без задержек. Спецификация W3C и черновик MSE 2 Working Draft детально описывают API; каноничное демо от Wolenetz на wolenetz.github.io/mse-in-workers-demo/ – самый простой способ увидеть его в действии от начала до конца.

// worker.js
const ms = new MediaSource();
const handle = ms.handle;            // MediaSourceHandle, transferable
postMessage({ handle }, [handle]);   // передаём в главный поток

ms.addEventListener('sourceopen', () => {
  const sb = ms.addSourceBuffer('video/mp4; codecs="avc1.640028,mp4a.40.2"');
  // ... fetch, transmux, append — всё происходит здесь, вне главного потока
});
// main.js
const worker = new Worker('worker.js');
worker.onmessage = (e) => {
  document.querySelector('video').srcObject = e.data.handle;  // не src
};

Внедрение в 2026 году идёт неравномерно. Браузеры на базе Chromium (Chrome, Edge, Opera, Brave и все Smart TV-браузеры на Chromium, включая Tizen и webOS) поддерживают новую функцию. Firefox и Safari пока не реализуют её. Крупные open-source плееры – hls.js начиная с версии 1.5, shaka-player начиная с версии 4.7, dash.js экспериментально, а также Streaming Processor Framework в video.js v10 – определяют поддержку во время выполнения и при возможности используют путь через Worker.

Флаг feature-detect – MediaSource.canConstructInDedicatedWorker. Если true, можно собирать Worker-конвейер; если false или undefined – использовать fallback на главный поток. Плееры, поддерживающие оба пути, обычно включают это через единый флаг конфигурации вроде worker: true.

Рис. 3. MSE-in-Worker. Любая CPU-ёмкая операция переносится с главного потока; на странице остаются только video-элемент и handle.

Исключение iPhone и как оно закончилось в октябре 2023

С момента запуска iPhone в 2007 году и до октября 2023 года MSE в Safari на iPhone просто не существовало. В спецификации W3C не было специальной оговорки по этому поводу – Apple просто не реализовала API в мобильной версии WebKit. Официальная причина, озвученная на WWDC и в блоге WebKit, – это батарея и память: если каждая страница может свободно буферизовать произвольно большие фрагменты видео, радио никогда не перейдёт в режим ожидания, страница будет потреблять неограниченный объём памяти, а батарея разрядится уже к обеду. Была и практическая причина – Apple хотела, чтобы разработчики использовали <video src="…m3u8"> и позволяли нативному HLS-плееру Safari выполнять всю работу, что означало меньше кода для тестирования и меньше браузерных багов.

Следствие – десятилетие дорогих обходных путей. Каждый веб-стриминг-продукт, чтобы работать на iPhone, вынужден был поддерживать две сборки: одну – с использованием MSE для десктопов и планшетов, и другую – «нативную HLS»-сборку для iPhone. А та, что под iPhone, вынуждена была мириться с ограничениями тега <video src>: отсутствие реального контроля ABR из JavaScript, невозможность доступа к in-band metadata без отдельного запроса, отсутствие тонкого переключения качества и сильно урезанная поддержка DRM. hls.js, самый популярный open-source плеер в мире, просто отказывался запускаться в Safari на iPhone и перенаправлял пользователя на нативный HLS-путь.

В октябре 2023 года Apple выпустила Safari 17.1 на iOS 17.1, и ситуация изменилась. Safari 17.1 представил Managed Media Source – API на основе MSE, которое впервые сделало адаптивный стриминг через JavaScript возможным на iPhone (WebKit Blog, «WebKit Features in Safari 17.1», 24 октября 2023, accessed May 2026). ManagedMediaSource – это не обычный MSE: он добавляет два новых события, возвращающих контроль над пропускной способностью и памятью браузеру, – о них мы поговорим ниже. Однако на вопрос «можно ли запустить hls.js на iPhone в 2026 году» ответ однозначный: да. Каждый крупный веб-плеер добавил поддержку ManagedMediaSource в 2024–2025 годах.

Маленькое уточнение: что означает «MSE на iPhone» в 2026 году. У iPad с iPadOS 13 (2019) – настоящий MSE. У Safari на macOS – настоящий MSE с версии 8 (2014). iPhone – единственное устройство Apple, которое требует именно ManagedMediaSource: на всех остальных платформах Apple доступны оба API, и ManagedMediaSource предпочтителен даже на iPad и Mac из-за экономии заряда батареи и памяти. Паттерн feature-детект, который большинство плееров используют в 2026 году: const MediaSource = window.ManagedMediaSource ?? window.MediaSource – он выбирает MMS, если тот доступен, и иначе переходит на обычный MSE.

ManagedMediaSource, события и «буферная ловушка»

ManagedMediaSource добавляет два события, меняющих поведение плеера: startstreaming и endstreaming. Контракт простой: когда браузер генерирует событие startstreaming, плеер должен начать загружать новые сегменты; когда генерирует событие endstreaming – должен прекратить. Браузер – главный.

Поведение по умолчанию в Safari 17.1+ следующее: endstreaming срабатывает, когда forward-буфер опережает курсор примерно на 30 секунд, а startstreaming снова активируется, когда объём forward-буфера падает ниже примерно 10 секунд. Эти значения не являются официальными – команда WebKit может изменить их в любой момент, – но именно такие параметры используются в продакшене сегодня.

Сообщественное прозвище для этого поведения – «60-секундная буферная ловушка iPhone». Прозвище не вполне точное – реальный предел зависит от условий и обычно составляет от 30 до 60 секунд в зависимости от объёма памяти и используемого кодека, – но основная идея остаётся той же: на iPhone в Safari вы не можете сами выбирать, сколько видео буферизовать вперёд. Это решает браузер. Плеер реагирует на startstreaming и endstreaming и подчиняется им.

Для большинства live-стриминга это не проблема. Live-плеер стремится держаться у live-границы, а 30-секундный forward-буфер – это больше, чем запросил бы live-профиль. В случае long-форм VOD политика становится заметнее: плеер, который на десктопе мог бы заранее загрузить до двух минут контента вперёд, на iPhone Safari ограничен 30-секундным окном и подгружает данные короткими порциями. Эффективность использования сети немного страдает, а время автономной работы – заметно растёт.

Стоит упомянуть два дополнительных хука. Экземпляр ManagedSourceBuffer может срабатывать bufferedchange, когда браузер сам вытесняет данные (страница не вызывала remove – это сделал браузер, и объём буфера уменьшился). Экземпляр ManagedMediaSource устанавливает свойство quality и генерирует событие qualitychange со значениями "low", "medium" или "high", которые страница должна учитывать при выборе рендера: Safari устанавливает "low" в сотовой сети или в режиме энергосбережения, и плеер должен снижать битрейт. Оба хука описаны в том же посте в блоге WebKit.

Рис. 4. Поток событий ManagedMediaSource. Браузер управляет процессом, плеер реагирует. Это противоположность десктопной модели MSE, где плеер управляет, а браузер следует за ним.

Поддержка браузеров и кодеков в 2026 году

Точная картина – на одной странице. Ниже приведена сводка, актуальная на май 2026 года.

Браузер / runtimeMSEMSE-in-WorkerManagedMediaSource
Chrome desktop, Edge, OperaДа (Chrome 23, 2013)Да (108, 2022-11)Нет (используется обычный MSE)
Firefox desktopДа (42, 2015)Нет (на 2026)Нет (на 2026)
Safari macOSДа (Safari 8, 2014)НетДа (Safari 17.0, 2023-09)
Safari iPadOSДа (iPadOS 13, 2019)НетДа (Safari 17.0, 2023-09)
Safari iPhoneНетНетДа (Safari 17.1, 2023-10) – единственный путь на iPhone
Smart TV на Chromium (Tizen, webOS, Android TV, Chromecast, Fire TV)ДаДа на свежих сборкахОграниченно; зависит от вендора
RokuНет – –
Кодек в MSEChromiumFirefoxSafari
H.264 / AVCДаДаДа
HEVC / H.265Да (по умолчанию с Chrome 105, 2022)Нет (ограниченно по платформе)Да
VP9ДаДаДа (Safari 14)
AV1Да (Chrome 90)Да (FF 75)Ограниченно; зависит от hardware-декодера
AAC, Opus, FLAC, MP3 (аудио)ДаДаДа

Таблица кодеков приблизительная, потому что реальный ответ всегда зависит от устройства. На ноутбуке с процессором Intel старше 2018 года нет аппаратного декодера AV1; в этом случае Chrome перейдёт на программный декодер, и MediaSource.isTypeSupported('video/mp4; codecs="av01.0.05M.08"') может всё равно вернуть true, но воспроизведение будет нагружать CPU и заметно подёргиваться. Урок тот же: проверяйте isTypeSupported, но оценивайте производительность декодера по телеметрии и полагайтесь на данные, когда ответ может оказаться неверным на реальном железе.

Ошибки, которые попадают в продакшен

Каждая команда, строящая плеер на MSE, выпускает в продакшен минимум три бага из этого списка. Большинство – все, и не по одному разу.

1. Append при updating === true. Разобрано выше. Фикс – паттерн SbQueue.

2. Вызов addSourceBuffer до sourceopen. MediaSource начинает работу в состоянии "closed" и переходит в состояние "open" только после подключения к элементу <video>. Вызов addSourceBuffer в контексте "closed" вызывает ошибку InvalidStateError. Решение – выполняйте все действия внутри обработчика события sourceopen.

3. Не выставленный mediaSource.duration для VOD. Для VOD-стрима необходимо явно задать duration через mediaSource.duration = totalSeconds после первого append; иначе scrub-bar отображает бесконечность, а перемотка работает некорректно. Live-стримы этого не требуют и используют setLiveSeekableRange().

4. Забытый URL.revokeObjectURL blob-URL. Каждый URL.createObjectURL содержит ссылку на MediaSource; неосвобождённые ресурсы приводят к утечке памяти при навигации по странице.

5. Смешивание MPEG-TS и fragmented MP4 в одном SourceBuffer. Реестр форматов байтовых потоков (byte stream) MSE (W3C, MSE Byte Stream Format Registry, accessed May 2026) включает fMP4 и WebM в качестве зарегистрированных форматов, а также устаревший MPEG-2 TS, который Apple и Safari на macOS не поддерживают. В 2026 году единственным безопасным выбором для нового кода остаётся fMP4. Low-Latency HLS специально требует CMAF (то есть fMP4), поскольку он сегментирует данные на уровне moof. Если ваш origin по-прежнему выдаёт MPEG-TS, конвертируйте его на входе с помощью transmuxing.

6. Переключение кодеков без changeType. Разобрано выше. Без changeType приложенные байты другого профиля молча роняют декодирование, и курсор встаёт.

7. Вызов endOfStream("decode") на каждую recoverable-ошибку. endOfStream разрушителен – он переводит MediaSource в "ended" и блокирует дальнейшие appends. Считайте его финальным сигналом, а не уведомлением об ошибке. Многие recoverable-ошибки (одиночный таймаут сегмента, преходящий 404) заслуживают повторной попытки и, при необходимости, понижения качества рендера, а не endOfStream.

8. Буфер больше, чем устройство потянет. Это тот же случай, что с iPhone выше, но и средние Android-устройства – ноутбуки и планшеты – тоже подвержены этой проблеме. Forward-буфер на 2 минуты при скорости 8 Мбит/с составляет около 120 МБ декодированного медиа, что почти полностью заполняет доступную видеопамять у Chromebook.

9. Расхождение codec string между манифестом и SourceBuffer. Атрибут CODECS в HLS-манифесте (или значение codecs в MPD у DASH) должен совпадать с тем, что было передано в addSourceBuffer, иначе SourceBuffer примет байты, но парсер отбросит их как чужие.

10. Необработанное событие error на SourceBuffer. Promise-обёртка выше отслеживает error, но не анализирует его; в продакшене необходимо логировать тип события, состояние буфера и URL сегмента, вызвавшего его. Каждая платформа аналитики качества (Mux Data, Conviva, Bitmovin Analytics, Datazoom, NPAW) требует эти поля в своих событиях.

Пример rebuffer ratio с числами

Смысл MSE в продакшене – двигать курсор вперёд. Самая отслеживаемая метрика для этого – rebuffer ratio, доля задуманного времени просмотра, которую пользователь провёл на спиннере вместо контента. Числа маленькие, но математику стоит показать вслух один раз.

Допустим, пользователь смотрит 45-минутный эпизод (2 700 секунд) сериала в OTT. За эту сессию плеер ловит две ребуферизации – одну на 14 секунд, когда сеть просела в поезде, и одну на 9 секунд, когда пользователь открыл другую вкладку, и страница потеряла фокус. Время ребуферизации – 14 + 9, то есть 23 секунды. Rebuffer ratio – 23 / 2700, то есть 0,0085, около 0,85 %.

Это значение ниже целевого 1 %, к которому стремятся многие продукты. Сессия, которая поднимает ratio выше 3 %, в большинстве QoE-дашбордов помечается как продакшен-инцидент.

При чём тут MSE? Две самые частые причины внезапного скачка rebuffer ratio ночью – это как раз баги выше: race condition на updating, из-за которой сегмент теряется, и ошибка в QuotaExceededError, проглоченная catch-блоком. Оба случая выглядят одинаково – «курсор остановился посреди буфера без видимой причины». Оба проявляются только тогда, когда слой телеметрии ловит событие error SourceBuffer вместе с состоянием буфера в момент сбоя. Раздел про pitfall – не теоретический; это именно то, что вызывает всплеск на дашборде в 23:00 по пятницам.

Где здесь Фора Софт

Фора Софт с 2005 года поставляет решения в сфере видеостриминга, WebRTC-конференций, видеонаблюдения, e-learning, телемедицины, OTT и AR/VR – на сегодняшний день реализовано 250+ проектов. Большинство решений, работающих в браузере, используют плеер на базе MSE – чаще всего hls.js или shaka-player, иногда – форк. Перечисленные выше проблемы – не выдержки из документации; это баги, с которыми мы сталкивались в продакшене, паттерны работы с очередями, которые закрепили в нашем внутреннем плеер-утилите, и ветки ManagedMediaSource, добавленные в 2024 году, когда iPhone Safari снова стал актуальной платформой. Плеер – это та часть продукта, с которой пользователь напрямую взаимодействует; слой MSE – часть плеера, которую нельзя допустить сделать неправильно. Перед тем как внедрять свой путь в продакшен, свяжитесь с нами.

Что меняется в 2026 году и дальше

Три вещи, за которыми стоит следить в ближайшие 12 месяцев.

Worker-MSE в Firefox и Safari. Оба вендора активно работают над реализацией: у Mozilla есть отслеживаемый баг, команда WebKit в Apple подтвердила заинтересованность. Как только оба браузера внедрят поддержку, проверка по платформам больше не понадобится, и Worker-путь станет стандартным во всех крупных плеерах. Следить стоит за записью на Chrome Platform Status об обновлении до статуса W3C Recommendation.

Отсоединяемый MediaSource и бесшовный режим picture-in-picture. В 2026 году WebKit добавил поддержку отсоединения MediaSource от одного элемента video и повторного подключения к другому без пересборки буфера; сценарий использования – переход в режим picture-in-picture и переключение между вкладками. Текст спецификации доступен в черновике редактора на GitHub (w3c/media-source, accessed May 2026).

In-band text tracks внутри MSE. Небольшое, но долгое время ожидаемое дополнение: in-band-текстовые дорожки (CEA-608, CEA-708, IMSC1 в fMP4) парсятся и выдаются как события TextTrack через MediaSource. Рабочая группа W3C по медиа провела в марте 2025 года breakout-сессию по этой теме; в WebKit реализована экспериментальная поддержка, в Chromium ведётся работа. Ожидается нормативное дополнение к MSE 2 до финализации Recommendation.

Это три изменения, которые повлияют на архитектурные решения плееров до 2027 года. Ни одно из них не настолько масштабное, чтобы ломать существующий код, но каждое устраняет класс обходных путей, которые крупные плееры тащат с конца 2010-х.

Ключевые выводы

  • MSE – небольшой API от W3C, позволяющий JavaScript подавать <video> короткие MP4-сегменты вместо одного файла.
  • Четыре объекта: MediaSource, SourceBuffer, MediaSourceHandle, ManagedMediaSource. Шесть методов выполняют всю работу.
  • Флаг updating – главная ловушка API: всегда очередь, всегда ждите updateend.
  • QuotaExceededError означает «освободи буфер через remove() и повтори», а не «проглоти ошибку».
  • iPhone Safari использует ManagedMediaSource – единственный класс MSE API на iPhone – с iOS 17.1.
  • MSE-в-воркере (Chrome 108+) переносит обработку сегментов с главного потока, снижая задержки страницы.

Что почитать дальше

CTA

Поговорить со streaming-инженером · Посмотреть наши кейсы · Скачать MSE Engineer's Quick Reference (./downloads/mse-engineers-quick-reference.pdf)

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.