Содержание статьи +
- TL;DR
- Зачем это нужно
- Какую проблему решал MSE
- Четыре объекта, с которыми вы фактически работаете
- Каноничный жизненный цикл – в рабочем коде
- Codec strings и строгий режим, который не обойти
- Race на `updating` и очередь, которую вы обязаны построить
- QuotaExceededError и история вытеснения
- Переключение кодеков без пересоздания: `changeType()`
- MSE-in-Worker: разблокировка Chrome 108
- Исключение iPhone и как оно закончилось в октябре 2023
- ManagedMediaSource, события и «буферная ловушка»
- Поддержка браузеров и кодеков в 2026 году
- Ошибки, которые попадают в продакшен
- Пример rebuffer ratio с числами
- Где здесь Фора Софт
- Что меняется в 2026 году и дальше
- Ключевые выводы
- Что почитать дальше
- CTA
TL;DR
Media Source Extensions (сокращённо MSE) – это небольшой веб-стандарт W3C, который позволяет JavaScript кормить элемент <video> короткими кусками MP4 вместо одного большого файла. На этом стандарте построен каждый адаптивный потоковый плеер, который вы когда-либо запускали в браузере. API маленький (один корневой объект MediaSource, один буфер на дорожку SourceBuffer, один метод appendBuffer), а инженерия вокруг него большая: codec strings, race-condition на флаге updating, политика вытеснения с QuotaExceededError, переключения рендиций через changeType() и десятилетняя изоляция iPhone, которая закончилась только в октябре 2023 года, когда Apple выпустила Managed Media Source. В 2026 году ландшафт MSE формируют два больших обновления: ManagedMediaSource в iOS 17.1 и новее – это первый раз, когда функциональность класса MSE появилась в Safari на iPhone, – и MSE-in-Worker в Chrome 108 и новее, переносящий всю работу с буфером с главного потока страницы. Эта статья проходит API от начала до конца, показывает каноничный жизненный цикл в рабочем коде, называет каждую типичную проблему продакшена и точно объясняет, что меняется, когда runtime – ManagedMediaSource, а не обычный MSE.
Зачем это нужно
Если вы поставляете видео в браузер в любом сколько-нибудь серьёзном масштабе – вы поставляете MSE. Не важно, написали ли вы код сами или подключили hls.js, shaka-player, dash.js либо video.js и позволили библиотеке написать его за вас. Эти четыре open-source плеера обслуживают огромную долю воспроизведения вне Apple-устройств, и под их публичным API идут одни и те же три-четыре вызова MSE – в одном и том же порядке, с теми же race-condition. После этой статьи продакт-менеджер сможет задать инженеру правильный диагностический вопрос, когда плеер встал на iPhone, а senior-инженер получит полную ментальную модель API – включая обновления 2026 года (ManagedMediaSource, MSE-in-Worker и SourceBuffer.changeType()) – без необходимости читать спецификацию W3C от корки до корки. Предварительных знаний об HLS или MPEG-DASH не требуется; всё нужное определено по мере появления. К концу вы поймёте, почему существует «60-секундная буферная ловушка» в Safari на iPhone, что вызывает падение плеера с 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() для переключения кодеков.
Всё в этой статье построено на этих двух документах.
Четыре объекта, с которыми вы фактически работаете
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 Working Draft (W3C, Media Source Extensions™ 2, Working Draft 4 November 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-атом, описывающий всю дорожку – sample rate, параметры кодека, разрешение. Четвёртое: каждый appendBuffer асинхронен, и вы обязаны дождаться updateend перед следующим вызовом. Пятое: endOfStream() – способ сказать браузеру «я закончил», и он переводит MediaSource из "open" в "ended", после чего video-элемент знает, что общая длительность теперь финальная. Шестое: blob URL вы освобождаете, как только source прикреплён – иначе он держит ссылку на MediaSource и мешает сборщику мусора чистить страницу.
В сущности, этот фрагмент – то, что делает каждый браузерный плеер, включая те, что крутятся в масштабе Netflix. Различия – в парсере манифеста, поставляющем URL сегментов, в ABR-алгоритме, выбирающем рендицию, и в политике управления буфером, решающей, когда звать remove(). API MSE – одинаков.
Codec strings и строгий режим, который не обойти
Каждый вызов в MSE несёт строку MIME-type-plus-codecs, точно описывающую, что в байтах, которые вы собираетесь толкнуть. Формат описан в 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) строже пробы – он реально инстанцирует парсер и конвейер декодера, и codec string, прошедшая isTypeSupported на десктопе, может всё равно бросить NotSupportedError на Smart TV, конвейер декодера которого не принимает именно эту связку profile-level. Урок: тестировать оба вызова на каждом устройстве, на которое поставляется продукт, и выводить пользователю понятную ошибку «этот кодек не поддерживается на этом устройстве», а не давать 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 ниже.
Race на `updating` и очередь, которую вы обязаны построить
Самая частая ошибка в коде MSE – вызов appendBuffer, пока SourceBuffer ещё обрабатывает предыдущий append. SourceBuffer выставляет булев updating в true с момента вызова appendBuffer до срабатывания события updateend; вызов appendBuffer, remove, abort, changeType или мутация timestampOffset либо append window, пока updating равен true, бросают InvalidStateError мгновенно. Внутренней очереди в API нет.
Поэтому каждый продакшен-плеер строит свою. Форма очереди одинакова в любой кодовой базе: массив отложенных операций, одна in-flight, listener на updateend, который снимает следующую операцию, когда in-flight завершилась. Паттерн короткий – приведу целиком.
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 исторически агрессивнее с тихим вытеснением. Команда Chrome в 2014 году опубликовала ясный пост, который до сих пор актуален (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-buffer; это же ломало любой плеер, который хотел переключаться между рендициями 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, что последующие appends будут содержать байты нового кодека, при этом уже забуферизованное медиа сохраняется. Короткое демо живёт на официальном сайте примеров (Google Chrome Samples, «SourceBuffer.changeType», accessed May 2026): H.264 сплайсится в HEVC и потом в VP9 в одной сессии воспроизведения.
Практический use case в 2026 году двойной. Первое: codec-switching ABR – плеер, у которого ladder содержит микс кодеков (H.264 как fallback на низких битрейтах и HEVC или AV1 на верхних), может шагнуть между ними по событию буфера без пересоздания буфера. Второе: ad-stitching с другим кодеком – server-side ad insertion, вставляющий креатив другого профиля, может опираться на changeType, чтобы плеер принял склейку без видимой пользователю разрывности.
Две оговорки. Браузер должен поддерживать и исходящий, и входящий MIME-type, и isTypeSupported стоит звать на новом типе перед changeType. И changeType ставится в очередь через флаг updating, как любой другой write, – он живёт в той же самой очереди, что описана выше.
MSE-in-Worker: разблокировка Chrome 108
До конца 2022 года все операции MSE шли в главном потоке страницы. Парсинг буфера, проверки кодеков, сам appendBuffer – всё это боролось за CPU с render-loop страницы. На вкладке с умеренно тяжёлым 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 – fetch сегментов, transmuxing, append, вытеснение, всё.
Страница видит измеримое снижение нагрузки на главный поток, а на устройствах с двумя и более ядрами эффект большой: 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 и каждый Chromium-based Smart TV-браузер, включая Tizen и webOS) поддерживают. Firefox пока не поставляет. Safari пока не поставляет. Большие open-source плееры – hls.js начиная с 1.5, shaka-player начиная с 4.7, dash.js экспериментально, и Streaming Processor Framework в video.js v10 – детектят поддержку в runtime и оппортунистически включают путь через Worker.
Флаг feature-detect – MediaSource.canConstructInDedicatedWorker. Если true, можно собирать Worker-конвейер; если false или undefined – fallback на главный поток. Плееры, поддерживающие оба пути, обычно включают это через единственный флаг конфигурации вроде worker: true.
Исключение iPhone и как оно закончилось в октябре 2023
С запуска iPhone в 2007 году и до октября 2023 года MSE на iPhone Safari просто не существовало. У спецификации W3C не было специальной оговорки про это; Apple просто не реализовала API на своей мобильной сборке WebKit. Официальная причина, объяснявшаяся на WWDC и в блоге WebKit, – батарея и память: если каждая страница свободно буферизует произвольно большие куски видео, радио никогда не уйдёт в idle, страница съест неограниченную память, и батарея сядет к обеду. Была и практическая причина – Apple хотела, чтобы разработчики использовали <video src="…m3u8"> и позволяли нативному HLS-плееру Safari делать всё, что означало меньше кода для тестов и меньше браузерных багов.
Следствие – десятилетие дорогих обходных путей. Каждый веб-стриминг-продукт, который хотел жить на iPhone, имел две сборки – MSE-сборку для десктопа и планшета и «native HLS»-сборку для iPhone, – и iPhone-сборка должна была мириться с ограничениями тега <video src> (нет реального контроля ABR из JavaScript, нет доступа к in-band metadata без отдельного запроса, нет тонкого переключения качества и сильно урезанная история DRM). hls.js, самый устанавливаемый open-source плеер в мире, просто отказывался грузиться на iPhone Safari и отправлял пользователя на native 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 October 2023, accessed May 2026). ManagedMediaSource – это не обычный MSE: он добавляет два новых события, возвращающих контроль над bandwidth и памятью браузеру, и мы их разберём ниже; но для вопроса «можно ли запустить hls.js на iPhone в 2026 году» ответ – да. Каждый крупный веб-плеер добавил ветку ManagedMediaSource в 2024–2025 годах.
Маленькое уточнение, что значит «MSE на iPhone» в 2026 году. У iPad настоящий MSE с iPadOS 13 (2019). У Safari macOS – настоящий MSE с Safari 8 (2014). iPhone – единственное Apple-устройство, требующее именно ManagedMediaSource; на любой другой Apple-платформе доступны оба API, и ManagedMediaSource – предпочтительный API даже на iPad и Mac из-за выгод по батарее и памяти. Паттерн feature-detect, который большинство плееров поставляет в 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-form VOD политика заметнее – плеер, который на десктопе префетчил бы 2 минуты вперёд, на iPhone Safari сидит на 30-секундном окне и подкачивает короткими порциями. Эффективность по сети чуть страдает; время работы от батареи – заметно растёт.
Два дополнительных хука стоит упомянуть. Экземпляр ManagedSourceBuffer может стрелять bufferedchange, когда браузер вытеснил данные сам (страница не звала remove, это сделал браузер, и буфер уменьшился). Экземпляр ManagedMediaSource выставляет свойство quality и событие qualitychange со значениями "low", "medium" или "high", которое страница должна читать при выборе рендиции – Safari ставит "low" на сотовой сети или в Low Power Mode, и плеер должен подрезать битрейт. Тот же пост в блоге WebKit описывает оба хука.
Поддержка браузеров и кодеков в 2026 году
Точная картина – на одной странице. Сводка ниже актуальна на май 2026 года.
| Браузер / runtime | MSE | MSE-in-Worker | ManagedMediaSource |
|---|---|---|---|
| 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 | Нет | – | – |
| Кодек в MSE | Chromium | Firefox | Safari |
|---|---|---|---|
| H.264 / AVC | Да | Да | Да |
| HEVC / H.265 | Да (по умолчанию с Chrome 105, 2022) | Нет (ограниченно по платформе) | Да |
| VP9 | Да | Да | Да (Safari 14) |
| AV1 | Да (Chrome 90) | Да (FF 75) | Ограниченно; зависит от hardware-декодера |
| AAC, Opus, FLAC, MP3 (аудио) | Да | Да | Да |
Таблица кодеков приблизительная, потому что реальный ответ – всегда per-device. На ноутбуке с Intel CPU старше 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. Фикс – делайте всё внутри listener'а 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, transmux-ите его на входе.
6. Переключение кодеков без changeType. Разобрано выше. Без changeType приложенные байты другого профиля молча роняют декодирование, и курсор встаёт.
7. Вызов endOfStream("decode") на каждую recoverable-ошибку. endOfStream разрушителен – он переводит MediaSource в "ended" и блокирует дальнейшие appends. Считайте его финальным сигналом, а не нотификацией ошибок. Многие recoverable-ошибки (одиночный таймаут сегмента, преходящий 404) заслуживают retry и возможного downshift рендиции, а не endOfStream.
8. Буфер больше, чем устройство потянет. Это кейс iPhone выше, но и средние Android-ноутбуки и планшеты тоже подвержены. Forward-буфер на 2 минуты при 8 Мбит/с – это около 120 МБ декодированного медиа, что почти весь доступный video-memory у Chromebook.
9. Расхождение codec string между манифестом и SourceBuffer. Атрибут CODECS в HLS-манифесте (или значение codecs в MPD у DASH) должен совпадать с тем, что было передано в addSourceBuffer, иначе SourceBuffer примет байты, но parser отбросит их как чужие.
10. Не обрабатываемое событие error на SourceBuffer. Promise-обёртка выше слушает error, но не анализирует его; в продакшене вам нужно логировать тип, состояние буфера и URL сегмента, его спровоцировавшего. Каждая QoE-платформа (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 на updating, дропающий сегмент, и QuotaExceededError, проглоченный catch-блоком. Оба выглядят как «курсор встал в середине буфера без видимой причины». Оба видны, только если слой телеметрии ловит событие error SourceBuffer плюс состояние буфера в момент сбоя. Раздел про pitfall – не теоретический; это ровно то, что даёт всплеск в дашборде в 23:00 в пятницу.
Где здесь Фора Софт
Фора Софт с 2005 года поставляет продукты в видеостриминге, WebRTC-конференциях, видеонаблюдении, e-learning, телемедицине, OTT и AR/VR – 239 проектов на сегодня. Большинство из тех, что выходят в браузер, идут через MSE-based плеер – обычно hls.js или shaka-player, иногда форк. Перечисленные выше pitfall – не вытяжка из документации; это баги, которые мы разбирали в продакшене, паттерны очередей, которые держим в внутреннем плеер-тулките, и ветки ManagedMediaSource, которые добавили в 2024 году, когда iPhone Safari снова стал реальной целью. Плеер – это часть продукта, которой пользователь буквально касается; слой MSE – часть плеера, которую нельзя позволить себе сделать неправильно. Прежде чем поставлять свой path в продакшен, поговорите с нами.
Что меняется в 2026 году и дальше
Три вещи стоит отслеживать ближайшие 12 месяцев.
Worker-MSE в Firefox и Safari. Оба вендора публично работают; у Mozilla есть tracking bug, команда WebKit в Apple дала сигнал о заинтересованности. Когда оба поставят, per-platform feature-detect больше не нужен, и Worker-путь становится дефолтом во всех крупных плеерах. Следить – за записью Chrome Platform Status на апгрейд W3C Recommendation.
Detachable MediaSource и бесшовный picture-in-picture. WebKit в 2026 году добавил поддержку detach MediaSource от одного video-элемента и re-attach к другому без пересборки буфера; use case – переход в picture-in-picture и переключение вкладок. Текст спецификации – в Editor's Draft на GitHub (w3c/media-source, accessed May 2026).
In-band text tracks внутри MSE. Маленькое, но давно ожидаемое дополнение: in-band caption-дорожки (CEA-608, CEA-708, IMSC1 в fMP4) парсятся и выдаются как события TextTrack через MediaSource. W3C Media Working Group провела мартовский 2025 breakout по теме; в WebKit экспериментальная поддержка, в Chromium ведётся работа. Ожидать нормативного дополнения в MSE 2 до финализации Recommendation.
Это три изменения, которые повлияют на архитектурные решения плееров до 2027 года. Ни одно не настолько большое, чтобы ломать существующий код, но каждое убирает класс обходных путей, которые крупные плееры тащат с конца 2010-х.
Ключевые выводы
- MSE – небольшой W3C API, позволяющий JavaScript кормить <video> короткими MP4-сегментами вместо одного файла.
- Четыре объекта: MediaSource, SourceBuffer, MediaSourceHandle, ManagedMediaSource. Шесть методов делают всю работу.
- Флаг updating – главная ловушка API: всегда очередь, всегда ждите updateend.
- QuotaExceededError – это «освободи буфер через remove() и повтори», а не «проглоти ошибку».
- iPhone Safari использует ManagedMediaSource – единственный MSE-класс API на iPhone – с iOS 17.1.
- MSE-in-Worker (Chrome 108+) убирает работу с сегментами с главного потока и снижает страничные стопоры.
Что почитать дальше
- Что плеер стриминга делает от начала до конца – обзор семи подсистем, в которые эта статья заглядывает глубже.
- Encrypted Media Extensions (EME): как DRM живёт в браузере – сестринский API, отвечающий за обмен лицензиями.
- hls.js: подробный разбор – доминирующий open-source потребитель MSE в браузерах вне Safari.
CTA
Поговорить со streaming-инженером · Посмотреть наши кейсы · Скачать MSE Engineer's Quick Reference (./downloads/mse-engineers-quick-reference.pdf)