Legacy transport streams
mattebox@0.12.1
This chapter covers HLS content that MSE cannot accept directly: MPEG-TS segments and raw AAC segments.
When you need this
Check the media playlist. .ts segments are MPEG-TS, .aac segments are
packed audio. Both need a container stage to turn them into fragmented MP4
before they reach a SourceBuffer.
If your packager can output CMAF instead, do that. These are the biggest stages.
Container stages
| Stage | Handles |
|---|---|
ts-transmux |
Demuxes MPEG-TS into H.264 and AAC and rewraps them as fMP4, in a Worker |
packed-audio |
Wraps raw AAC frames, with or without a leading ID3 tag, as fMP4 |
import packedAudio from 'mattebox/containers/packed-audio';
import tsTransmux from 'mattebox/containers/ts-transmux';
const engine = mattebox({ stages: [hlsCmaf(), tsTransmux(), packedAudio()] });
Both check each segment and pass CMAF through untouched, so they can stay loaded when you have a mix of both.
Muxed variants beside an audio group
Some packagers mux audio into every variant’s segments and still list a
separate EXT-X-MEDIA audio rendition with its own playlist. The audio
rendition wins: it plays on the audio buffer, and ts-transmux drops the
audio stream from the variant segments so the video buffer only receives the
codec its type declares. The first time that happens the stage emits
transmux:dropped-audio with the track and rendition ids, so the fact shows
up in a trace. A variant without a separate audio rendition keeps its muxed
audio and the video buffer declares both codecs.
Segments cut mid-GOP
A segment that starts mid-GOP carries no H.264 SPS or PPS. ts-transmux
keeps the last ones each rendition had and uses them for such a segment, so
it plays on from the segment before. A rendition’s first segment without
them still has nothing to describe the stream, and the segment fails.
The Worker
ts-transmux runs in a Web Worker so it never blocks the main thread. The
Worker URL is new URL('./transmux.worker.js', import.meta.url), which
Vite, Rollup, Rolldown, esbuild, and webpack 5 all handle without
configuration.
When the Worker cannot start, the same transmux runs on the main thread.
It is loaded with a dynamic import(), so your bundler splits it into a
chunk the page downloads only then.
Two options cover the other cases.
| Option | Use |
|---|---|
workerUrl |
A URL you host yourself, for strict Content Security Policy |
disableWorker |
Runs on the main thread. For tests and headless runs |
tsTransmux({ workerUrl: new URL('/static/mattebox-transmux.js', location.href) });
The CDN bundles carry one copy of the transmux. They start the Worker from
it through a blob URL and call it on the main thread when a worker-src
policy blocks the blob, so a script tag needs neither option. See
chapter 13.
Stages that need a container stage
These stages depend on the container stages.
| Stage | Adds | See |
|---|---|---|
text-cea608 |
Captions from the SEI units in the video. Requires ts-transmux or nal-scan, so it works without the transmuxer too |
Chapter 06 |
meta-id3 |
ID3 timed metadata as cues | Chapter 06 |
cmaf-timing |
Required. It reads the decode time the transmuxer wrote, so the kernel applies no offset on top of it | Chapter 04 |
The codec-probe stage
Manifests leave out or get codec strings wrong, and MSE needs the exact one.
codec-probe reads the init segment and reports what it finds. It requires
mp4-box. Every preset includes both.
import codecProbe from 'mattebox/stages/codec-probe';
import mp4Box from 'mattebox/stages/mp4-box';
const engine = mattebox({ stages: [hlsCmaf(), mp4Box(), codecProbe()] });
engine.on('codecprobe:detected', () => {
console.log(engine.codecProbe.detected, engine.codecProbe.mimeType);
});
It also fixes buffers the manifest cannot type. A bare media playlist
declares no codecs, and Chrome refuses a SourceBuffer opened as plain
video/mp4. With codec-probe loaded, the buffer waits for the first
segment and opens with the codecs read from it. Without it, the bare type
is tried. When the manifest does declare codecs, that string is used, and a
mismatch shows up on the event.
The aes-128 stage
EXT-X-KEY:METHOD=AES-128 encrypts whole segments. This is not DRM: the key
is a plain 16-byte file and the EME stages are not involved. The aes-128
stage fetches keys through the transport, caches them by URL, and decrypts
each segment before the transmuxer, using the playlist’s IV or the media
sequence number. METHOD=NONE stops decryption for the segments after it.
It works on CMAF segments too.
import aes128 from 'mattebox/stages/aes-128';
const engine = mattebox({ stages: [hlsCmaf(), tsTransmux(), aes128()] });
SAMPLE-AES, which encrypts inside the elementary stream, is not covered.
Example
Every kind of HLS, with captions and metadata.
import { mattebox } from 'mattebox';
import packedAudio from 'mattebox/containers/packed-audio';
import tsTransmux from 'mattebox/containers/ts-transmux';
import hlsCmaf from 'mattebox/protocols/hls-cmaf';
import hlsLive from 'mattebox/protocols/hls-live';
import abr from 'mattebox/stages/abr';
import metaId3 from 'mattebox/stages/meta-id3';
import textCea608 from 'mattebox/stages/text-cea608';
const engine = mattebox({
stages: [hlsCmaf(), hlsLive(), abr(), tsTransmux(), packedAudio(), textCea608(), metaId3()],
});
await engine.attach(document.querySelector('video'));
engine.load('https://example.com/legacy/master.m3u8');
Next: 09 Events and errors.