Video Players Explained: MSE, EME, hls.js, dash.js, and Shaka
How browser video players work: MSE and EME, the buffering model, ABR hooks, how hls.js/dash.js/Shaka compare, native vs MSE on Safari, and common pitfalls.
The player is where every other part of the streaming stack — your ladder, your ABR design, your DRM, your CDN — gets cashed out into an actual viewing experience. Modern web players are surprisingly deep pieces of software built on two browser APIs: MSE for feeding media and EME for decrypting it. This post explains how players work under the hood, how the major JavaScript players differ, and the pitfalls that bite teams in production. Inspect manifests these players consume with the manifest tool.
MSE: feeding the decoder yourself
A plain <video src="movie.mp4"> lets the browser handle everything. Adaptive
streaming can't work that way — the player needs to choose renditions and splice
segments dynamically. MSE (Media Source Extensions) is the API that makes
this possible: instead of a URL, you attach a MediaSource and push media
bytes into SourceBuffers yourself.
The model:
- Create a
MediaSource, attach it viavideo.src = URL.createObjectURL(ms). - On
sourceopen, add aSourceBufferwith a codec string (addSourceBuffer('video/mp4; codecs="avc1.640028"')). - Fetch each media segment and
sourceBuffer.appendBuffer(bytes). - The browser demuxes, decodes, and renders from the buffer.
Everything else — manifest parsing, segment scheduling, ABR, error recovery — is your JavaScript. That's exactly what hls.js, dash.js, and Shaka implement on top of MSE.
The buffering model
MSE exposes a buffered TimeRanges object: the spans of media currently held,
in presentation time. The player's job is to keep the buffer ahead of the
playhead:
- Forward buffer (buffer length /
bl) — how many seconds are buffered ahead ofcurrentTime. When this hits zero, playback stalls (a rebuffer event). - Buffer targets — players aim for a target (e.g. 30s for VOD), fetching the next segment whenever the buffer drops below it; low-latency live runs a much thinner buffer.
- Eviction —
SourceBuffers have memory limits; players evict already-played ranges to stay under quota (aQuotaExceededErroron append means trim first). - Gaps — discontinuities or missing frames create gaps the player must detect and "jump" over, or playback wedges.
The player fires waiting/stalled when the buffer starves and playing when
it recovers — these are exactly the events a QoE beacon SDK hooks (see
QoE metrics that matter).
ABR hooks
The ABR controller decides which rendition to fetch next. It typically blends:
- Throughput estimation — bandwidth measured from recent segment downloads.
- Buffer occupancy — how full the buffer is; buffer-based algorithms (e.g. BOLA in dash.js) pick quality from buffer level rather than raw bandwidth.
- Constraints — screen size, max bitrate caps, and player state.
Good players expose hooks so you can override the choice — cap quality on metered networks, force a starting rendition for fast startup, or feed your own estimate. Players also emit CMCD on each request so the CDN can see buffer and bitrate state; see the CMCD tool.
The major JavaScript players
hls.js
- Scope: HLS only, on MSE. The de facto way to play HLS in Chrome/Firefox/ Edge, which lack native HLS.
- Strengths: lightweight, fast startup, huge adoption, great for HLS-first catalogs. Handles LL-HLS parts.
- Notes: doesn't play DASH; DRM via EME is supported but less central than in Shaka.
dash.js
- Scope: the DASH reference player (DASH-IF), MSE-based.
- Strengths: the most complete DASH implementation, advanced ABR (BOLA, dynamic), strong low-latency DASH support, the place new DASH features land first.
- Notes: DASH only.
Shaka Player
- Scope: Google's player; plays both DASH and HLS on MSE, with offline storage and strong multi-DRM.
- Strengths: unified API across formats, robust EME handling for Widevine/ PlayReady/FairPlay, good for services spanning both protocols.
- Notes: larger and more configuration surface than hls.js.
A practical rule: HLS-only catalog → hls.js; DASH-centric → dash.js; both formats and serious DRM → Shaka. Commercial players (Video.js with plugins, THEOplayer, Bitmovin) wrap these or ship their own engines with unified UIs and support.
Native vs MSE playback — the Safari problem
Safari (and iOS) is the big asymmetry:
- Safari/iOS play HLS natively through the
<video>element — no MSE needed. iOS historically restricted or lacked full MSE on the phone, so on iPhone you often must use native HLS playback. - That means on Safari your JS player frequently hands the URL to the native player rather than driving MSE itself, so you lose fine-grained control over buffering and ABR and inherit Safari's behavior.
- DASH does not play natively anywhere; it always needs MSE. So a DASH-only service needs an HLS fallback (or CMAF served as HLS) for iPhone.
This is why CMAF matters: one set of CMAF segments can be addressed by an HLS manifest (for native Safari/iOS) and a DASH manifest (for MSE elsewhere) without re-encoding. See HLS vs MPEG-DASH.
Common player pitfalls
- Codec string mismatches.
addSourceBufferwith the wrongcodecsprofile (e.g. claimingavc1.640028but shipping a different level) silently fails to decode on some devices. - Discontinuity handling. Ad splices and timeline gaps need correct
EXT-X-DISCONTINUITY/ period boundaries, or the player wedges at the seam. - Audio/video desync. Independent
SourceBuffers drifting, or mismatched segment durations across tracks. - Buffer eviction bugs. Not trimming leads to
QuotaExceededError; trimming too aggressively causes re-downloads. - Startup rendition too high. A high first rendition spikes startup time and exits-before-start; too low wastes the connection. Tune the initial choice.
- EME/DRM edge cases. Wrong key system order, missing PSSH, or
cbcscontent on a system expectingcenc— playback just fails (see DRM and content protection). - Safari assumptions. Code that assumes MSE everywhere breaks on iPhone; always test the native HLS path.
Takeaways
Web players are MSE-driven byte pumps with an ABR brain and an EME path for DRM; hls.js, dash.js, and Shaka each occupy a clear niche. The recurring traps are codec strings, discontinuities, buffer management, and the Safari native-HLS split — all of which CMAF and careful manifest authoring mitigate. Validate the manifests your player ingests in the manifest tool.
Try it yourself
Reading about streaming is one thing — pull apart a real manifest, play a stream, and measure its QoE with the toolkit.
Browse all tools