ADR-0003: overrideNative, JS (MSE) playback where it helps, native HLS on Safari/iOS
- Status: Accepted (amended 2026-09-04, originally "
overrideNative: trueon every browser") - Date: 2026-08-18
Context
The library must play HLS and DASH (CMAF) uniformly and expose a rendition ladder for the "Auto" / quality UI. Browser support for adaptive formats is uneven: Safari plays HLS natively but not DASH; Chromium and Firefox play neither without MSE. Native playback also exposes no usable quality-selection API and no consistent cross-browser event surface.
Decision
Set html5.vhs.overrideNative to !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS):
- Chromium / Firefox:
@videojs/http-streaming("VHS") drives HLS and DASH in JS through MSE. Quality levels come fromtech.vhs.representations();getQualityLevels()/setQualityLevel()and the custom ABR selector (see ADR-0001) are all built on that. - Safari / iOS: HLS is left to the native engine. DASH still goes through
MSE there anyway: Safari can't play it natively, so
Vhs.supportsTypeNatively('dash')is false and VHS takes the source regardless of the flag. That means the quality API and custom ABR keep working for DASH. For HLS on Safari they return nothing (the native player exposes no rendition list); that is accepted. - Progressive MP4 has no rendition concept and is always the native element.
This is also VHS's own default for overrideNative.
Amendment (2026-09-04): why the original "true everywhere" was reversed
overrideNative: true was unconditional from the initial commit. On Safari it
forces HLS through MSE + JS transmux, and that path is unreliable there:
- HLS (VOD and with in-manifest CEA-608 captions): playback wedges: a
visible first frame, seeking repaints it, but
play()never resolves or rejects andHTMLMediaElement.errorstaysnull. TheMediaSourcenever reachesopen. Reproduced in Safari againsttest-streams.mux.devHLS; a bare<video>with the same URL (native HLS) plays immediately. - Live fMP4/CMAF: the video plane drops (black / "disappears") on certain init/discontinuity appends.
- Contributing factor found while debugging: Storybook builds the player and
calls
setSource()on a detached element (videojslogs "element … not included in the DOM"); Safari MSE does not recover from the detached-then-attached sequence, native HLS does. Fixed separately in the stories, but native HLS is robust to it regardless.
The README format-support matrix already claimed "✅ (native)" for Safari HLS, so this change brings the code in line with the documented behaviour.
Alternatives considered
overrideNative: trueon every browser (the original decision). Rejected on the evidence above: MSE HLS on Safari wedges. One playback path is not worth a broken one.hls.js/dash.jsdirectly with a hand-built UI. Rejected (see ADR-0002): the Video.js UI is most of the value.- Native everywhere, defer ABR to the browser. Rejected: no DASH off Safari, no quality API on any browser, inconsistent events.
Consequences
Good
- HLS is reliable on Safari and iOS (the native engine).
- DASH still works everywhere, MSE-driven, including on Safari.
- On Chromium/Firefox (the browsers where the quality UI and custom ABR actually matter), the representation list is always present for HLS and DASH.
Costs / watch-outs
- Two playback paths again: MSE on Chromium/Firefox (+ DASH on Safari), native
HLS on Safari/iOS. Playback-adjacent code must not assume
tech.vhsexists. getQualityLevels()returns[]and the settings-menu "Quality" section does not appear for HLS on Safari/iOS. Documented in the Quality & ABR guide and the Architecture guide. DASH on Safari is unaffected.- On Safari/iOS native HLS the rendition is chosen by AVPlayer, and its ABR is
size-gated: it picks the variant matching the video element's rendered
pixel box (CSS size ×
devicePixelRatio), not just bandwidth. A small player gets a low rendition and will not climb (by design, AVPlayer will not pull 1080p into a 400px box). Verified: the same stream on the same Safari picks 360p in a 640px-wide player and 1080p at full width. This is not specific to this library (Castr's player behaves identically); there is no web API to override AVPlayer's peak bitrate / max resolution. Nothing to do beyond sizing the player sensibly. - Chromium/Firefox keep the JS-transmux CPU/battery cost and the
systemBandwidthdistortion that drives ADR-0001. - We still ride VHS internals (
selectPlaylist,mainSegmentLoader_,representations()) on the MSE path; a VHS upgrade can break the ABR/quality code with no type coverage over those.
History
overrideNative: true was set from the first implementation, and the ABR work
(culminating in ADR-0001) was built on that MSE path (including one attempt to
disable VHS's Network Information API bandwidth override that was tried and then
reverted).
On 2026-09-04 overrideNative was made Safari/iOS-aware after MSE-HLS playback
was found to wedge on Safari; playsinline was added to the <video> in the
same change (iOS inline playback).
Update, 2026-09-11: alternate-audio switching is slow on Safari/iOS native HLS (confirmed independent of this library)
Switching the audio language on Safari/iOS native HLS stalls for a few seconds
before the new audio starts (observed in Storybook: buffered-ahead drops to 0
for the duration; currentTime briefly has no buffered range at all).
Confirmed independent of this library: the same stream opened directly in
Safari (paste the .m3u8 URL straight into the address bar, no page, no
Video.js, no player code at all) shows the same multi-second delay on
Safari's own built-in audio-language picker. This is AVFoundation flushing and
re-buffering around the current position on an alternate-audio switch, not
something setAudioTrack() can avoid or speed up (it only flips
track.enabled on Video.js's AudioTrackList).
On Chromium/Firefox the same switch goes through VHS/MSE and swaps the audio
SourceBuffer's content in place (typically no visible stall). Nothing to do
here; documented so a future "audio-language switch is slow" report on Safari
isn't mistaken for a regression in this library.