Skip to main content

ADR-0003: overrideNative, JS (MSE) playback where it helps, native HLS on Safari/iOS

  • Status: Accepted (amended 2026-09-04, originally "overrideNative: true on 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 from tech.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 and HTMLMediaElement.error stays null. The MediaSource never reaches open. Reproduced in Safari against test-streams.mux.dev HLS; 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 (videojs logs "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: true on 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.js directly 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.vhs exists.
  • 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 systemBandwidth distortion 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.