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.

Read this page's source on GitHub