# transmux — any-to-any media container muxing hub
Demux any supported container into one neutral in-memory IR (`Media`/`Track`) and
mux from it into any supported container — so every `{input} → {output}` composes.
Built to spec (ISO/IEC 14496-12, 13818-1, 23009-1; RFC 8216/3550; [MS-SSTR]).
No transcode, no codec bitstream en/decode. `no_std` + `alloc`.
The spokes are the `broadcast_common` inverse-pair traits **`Unpackage`** (container
→ IR) and **`Package`** (IR → container):
| Demux → IR (`Unpackage`) | IR → mux (`Package`) |
|---|---|
| MPEG-2 TS (`TsDemux`, or streaming `StreamingTsDemux`) | CMAF/fMP4 (`CmafMux`) · progressive MP4 (`ProgressiveMux`) |
| fMP4/CMAF (`Fmp4Demux`) · progressive MP4 (`ProgressiveDemux`) | MPEG-2 TS (`TsMux`) |
| MPEG Program Stream (`PsDemux`) | CMAF-HLS (`HlsPackager`) · TS-HLS (`TsHlsPackager`, batch or streaming `StreamingTsHlsSegmenter`) |
| WebM/Matroska (`WebmDemux`) | DASH MPD (`DashPackager`) · LL-DASH (`LlDashPackager`) · Smooth (`SmoothPackager`) |
| RTMP chunk stream (`RtmpDemux`) | RTMP chunk stream (`RtmpMux`) |
Plus transforms — resegment/trim/track-select (`Repackage`), streaming CMAF
(`Segmenter`), IR timeline conditioning — PTS/DTS rebase-to-zero / offset /
discontinuity-gap insertion (`rebase_to_zero`, `apply_offset`,
`insert_discontinuity_gap`, over each `Track::start_decode_time` anchor) —
CENC/CBCS decrypt (`CencDecryptor`) and encrypt (`CencEncryptor`, plus
DASH/HLS DRM signalling), and RTP de/packetise + SDP
(`RtpPacketiser`/`RtpDepacketiser`).
Every `Sample`'s `dts`/`pts` are **absolute**, optional ticks in the track's
own media timescale (`Option<i64>`; `None` only for a genuinely
timestamp-less, section-carried sample), `duration` is `Option<u32>`, and
`data` is a `bytes::Bytes` (cheap fan-out, not a copy). 33-bit MPEG-2 Systems
(ISO/IEC 13818-1 §2.4.3.7) and 32-bit RTP (RFC 3550 §5.1) rollover is
unwrapped once, at the demux edge — never re-derived downstream, and no
longer a caller-invoked transform. The IR types (`Media`, `Track`,
`TrackSpec`, `Sample`, `DemuxEvent`, …) live in `transmux::ir` and are
re-exported at the crate root for convenience.
## Scope — container muxing only
`transmux` **packages** coded media; it never encodes or decodes it. It parses
codec *config/parameter headers* (SPS/PPS/VPS, AAC ASC, syncframe BSI, EBML/box
config) only far enough to build the container boxes and derive metadata
(dimensions, profile, sample rate, `codecs=` MIME). Compressed samples are
**opaque payloads copied through byte-for-byte** — decode/encode is the caller's
job (WebCodecs, FFmpeg, hardware).
Every box has a symmetric `Parse` / `Serialize` with byte-identical round-trip
coverage against real fixtures; every demux/mux spoke is gated on byte-exact
round-trips through the IR.
## Feature matrix
### Container — ISOBMFF / CMAF boxes
| Group | Boxes | Status |
|---|---|---|
| File structure | `ftyp`, `styp`, `mdat`, `free`/`skip`, `uuid` | ✅ |
| Movie | `moov` · `mvhd` · `trak` · `tkhd` · `mdia` · `mdhd` · `hdlr` · `minf` · `vmhd`/`smhd` · `dinf`/`dref` | ✅ |
| Sample tables | `stbl` · `stsd` · `stts` · `ctts` · `cslg` · `stsc` · `stsz` · `stco`/`co64` · `stss` · `stsh` | ✅ |
| Edit / fragment-init | `edts`/`elst`, `mvex`/`trex` | ✅ |
| Movie fragments | `moof` · `mfhd` · `traf` · `tfhd` · `tfdt` · `trun` | ✅ |
| Random access / index | `sidx`, `mfra`/`tfra`/`mfro` | ✅ |
| Inband events / refs | `emsg`, `tref` | ✅ |
| Encryption (CENC) | `senc`/`saiz`/`saio`/`tenc`/`pssh`/`sinf`/`schm`/`frma` | ✅ |
| Multi-DRM `pssh` init data | Widevine (proto) · PlayReady (PRO/WRMHEADER) · FairPlay (`skd://`) — `drm` module | ✅ |
| Sample-entry ext | `colr` (HDR), `pasp`, `clap` | ✅ |
| Live / grouping | `prft`, `sbgp`/`sgpd`, `subs` | ✅ |
### Codecs — sample entries + config (header parse only)
| Codec | Sample entry | Config box / decode | RFC 6381 | Status |
|---|---|---|---|---|
| H.264 / AVC | `avc1` (`avc2`/`avc3`/`avc4`) | `avcC` + **SPS/PPS decode** (profile/level/chroma/bit-depth/dims/interlaced) | `avc1.PPCCLL` | ✅ |
| H.265 / HEVC | `hvc1` (`hev1`) | `hvcC` + **VPS/SPS + profile_tier_level decode** | `hvc1.…` | ✅ |
| AAC | `mp4a` | `esds` / ES_Descriptor + `AudioSpecificConfig` (+ADTS) | `mp4a.40.N` | ✅ |
| AC-3 | `ac-3` | `dac3` + syncframe BSI parse | `ac-3` | ✅ |
| E-AC-3 | `ec-3` | `dec3` + syncframe BSI parse | `ec-3` | ✅ |
| HE-AAC (SBR/PS) | `mp4a` | explicit-SBR/PS ASC | `mp4a.40.5/29` | ✅ |
| AC-4 | `ac-4` | `dac4` | `ac-4` | ✅ |
| MPEG-H 3D | `mha1`/`mhm1` | `mhaC` (record decode) | — | ✅ |
| AV1 | `av01` | `av1C` | `av01.…` | ✅ |
| Opus / FLAC / VP9 | `Opus`/`fLaC`/`vp09` | `dOps`/`dfLa`/`vpcC` | — | ✅ |
| DTS | `dtsc`/`dtsh`/`dtsl`/`dtse` | `ddts` + core-substream frame-header parse (TS) | `dtsc` | ✅ |
| **H.266 / VVC** | `vvc1`/`vvi1` | `vvcC` + **SPS/profile_tier_level decode** | `vvc1.…` | ✅ |
| **VP8** | (WebM `V_VP8`) | keyframe-header dims (RFC 6386) | — | ✅ |
| **Vorbis** | (WebM `A_VORBIS`) | `CodecPrivate` id-header decode | — | ✅ |
| **MPEG-2 video (H.262)** | `mp4v` / TS 0x02 | `esds` + sequence-header dims | `mp4v.61` | ✅ |
| **MPEG-1/2 audio (MP1/2/3)** | `mp4a` / TS 0x03/0x04 | `esds` + frame-header decode | `mp4a.6B/69` | ✅ |
### Text / captions
| Format | Sample entry | Status |
|---|---|---|
| WebVTT / TTML | `wvtt` / `stpp` | ✅ |
| CEA-608/708 (in-band SEI) | SEI extraction | ⬜ [#430](https://github.com/fishloa/rust-broadcast/issues/430) |
### Pipeline & packaging
| Feature | API | Status |
|---|---|---|
| Init segment | `build_init_segment` (ftyp + fragmented-init moov) | ✅ |
| Media segment (batch) | `build_media_segment` (styp + moof + mdat) from `TrackSpec` / `Sample` | ✅ |
| **Streaming segmenter** | `Segmenter` — `push` samples → `take_ready` segments, keyframe-cut at a target duration | ✅ |
| **LL-HLS segmenter** | `LlHlsSegmenter` — `with_part_target` + `take_ready_parts` → partial-segment CMAF chunks (RFC 8216bis §4.4.4.9) before the segment closes | ✅ |
| **Streaming TS-HLS segmenter** | `StreamingTsHlsSegmenter` — `push`→`Option<TsSegment>` (keyframe-cut `.ts` segments) + rolling media playlist for live input; shares the batch `TsHlsPackager` cut logic | ✅ |
| NAL conversion | Annex B ↔ length-prefixed (`annexb_to_length_prefixed` / `length_prefixed_to_annexb`) | ✅ |
| NAL keyframe classification | `nal_unit_type` / `is_keyframe_nal` / `access_unit_is_keyframe` (`NalCodec` AVC/HEVC/VVC) | ✅ |
| RTCP control packets | `RtcpPacket` — SR/RR/SDES/BYE/APP + `CompoundPacket` (RFC 3550 §6) | ✅ |
| HLS playlists | `MediaPlaylist` / `MasterPlaylist` (RFC 8216); `#EXT-X-DISCONTINUITY` / `#EXT-X-DISCONTINUITY-SEQUENCE` (RFC 8216 §4.3.4.3/§4.3.3.3) | ✅ |
| LL-HLS playlist directives | `MediaPlaylist::low_latency` (`LowLatencyConfig`) → `#EXT-X-SERVER-CONTROL` · `#EXT-X-PART-INF` · `#EXT-X-PART` · `#EXT-X-PRELOAD-HINT` (RFC 8216bis §4.4.3.7/§4.4.3.8/§4.4.4.9/§4.4.5.3); `MediaPlaylist::open_segment` (`hls::OpenSegment`) renders an in-progress live-edge segment as trailing `#EXT-X-PART` lines with no `#EXTINF` (RFC 8216bis §4.4.4.9) | ✅ |
### Hub spokes (`Unpackage` / `Package`)
| Spoke | Type | API | Status |
|---|---|---|---|
| TS demux | `Unpackage` | `TsDemux` (PAT→PMT, PES, in-band config: H.264 `avcC` · H.265 `hvcC` · MPEG-2 video `esds` · AAC/MPEG audio `esds` · AC-3/E-AC-3 · DTS core `ddts`); every other `stream_type` carried as an opaque `Data` track (PES or reassembled sections) — nothing dropped. `is_sync` marks IDR **and** open-GOP RAPs (recovery-point SEI / SPS-led I-frame) so `Segmenter` anchors broadcast H.264. Every track carries `TrackSpec::source_pid` + `es_info_descriptors` (verbatim PMT ES_info) for player track-selection/labeling | ✅ |
| TS demux (streaming) | `feed`/`poll_event`/`finish` | `StreamingTsDemux` (event-driven incremental core; `TsDemux` is a batch wrapper over it) | ✅ |
| fMP4 demux | `Unpackage` | `Fmp4Demux` (moov/moof → IR, all codecs) | ✅ |
| Progressive MP4 demux | `Unpackage` | `ProgressiveDemux` (non-fragmented `moov` sample tables: `stts`/`ctts`/`stss`/`stsz`/`stsc`+`stco`/`co64` → IR; `sidx` v0/v1) | ✅ |
| MPEG-PS demux | `Unpackage` | `PsDemux` | ✅ |
| WebM demux | `Unpackage` | `WebmDemux` (EBML) | ✅ |
| CMAF / progressive / TS mux | `Package` | `CmafMux` · `ProgressiveMux` · `TsMux` | ✅ |
| DASH / LL-DASH / Smooth | `Package` | `DashPackager` (static + dynamic/live MPD; `$Number$` or `$Time$`/SegmentTimeline addressing; Role/`@lang`/InbandEventStream; auto `ContentProtection` from `Track::encryption` + caller-supplied per-DRM-system `cenc:pssh`) · `LlDashPackager` · `SmoothPackager` | ✅ |
| TS-HLS | `Package` | `TsHlsPackager` (batch); `StreamingTsHlsSegmenter` (live: `push`→`TsSegment`, rolling media playlist with sliding window + advancing `#EXT-X-MEDIA-SEQUENCE`) | ✅ |
| Repackage (resegment/trim/select) | — | `Repackage` | ✅ |
| IR timeline conditioning (rebase / offset / gap) | — | `rebase_to_zero` · `apply_offset` · `insert_discontinuity_gap` (over `Track::start_decode_time`; 33-bit MPEG-2/32-bit RTP wrap unroll happens once at the demux edge, not as a caller transform) | ✅ |
| IR timeline splice / concat → SSAI | — | `concat` · `splice_insert` (keyframe-snapped via `snap_to_preceding_sync`, → `SpliceResult` with `discontinuity_points`) | ✅ |
| CENC/CBCS decrypt + encrypt | `Decrypt`/`Encrypt` | `CencDecryptor` · `CencEncryptor` (`cenc` AES-CTR, `cbcs` AES-CBC pattern; populates `Track::encryption` for `protect_init_segment`/`protect_media_segment`'s `sinf`/`senc`/`saio`/`saiz` emission) | ✅ |
| CENC/CBCS DRM signalling | — | DASH: `DashPackager::content_protection` auto-derives the generic-CENC `ContentProtection` from `Track::encryption`. HLS: `cenc_ext_x_key` renders `#EXT-X-KEY` for `cbcs` (`cenc`/CTR has no HLS `METHOD` — DASH-only) | ✅ |
| HLS Sample-AES / AES-128 encrypt+decrypt | — | `sample_aes` (`h264_encrypt_nal` · `aac_encrypt_frame` · `ac3_encrypt_frame` · `aes128_encrypt_segment` · `ExtXKey`; feature `sample-aes`) | ✅ |
| fMP4/CMAF conformance validator | — | `validate_init_segment` / `validate_media_segment` / `validate_cmaf_track` (ISO 14496-12 + CMAF structural checks → `ConformanceIssue`) | ✅ |
| RTP de/packetise + SDP | `Package`/`Unpackage` | `RtpPacketiser` / `RtpDepacketiser` | ✅ |
| RTP streaming depayload (live) | — | `RtpStreamDepacketiser` (`push`/`flush` → timed `Sample`s: per-AU duration from RTP-timestamp deltas, `is_sync` from IDR; v1 low-delay H.264 / 1 AU-per-packet AAC / in-order feed) | ✅ |
| SDP fmtp → codec config | — | `rtp_sdp::fmtp_param` (generic `key=value` fmtp lookup) · `rtp_sdp::avc_config_from_fmtp` (full fmtp line → avcC) · `rtp_sdp::avc_config_from_sprop` (RFC 6184 §8.1 `sprop-parameter-sets` → avcC) · `rtp_sdp::aac_config_from_fmtp` (full RFC 3640 §4.1 fmtp line → esds) · `rtp_sdp::aac_config_from_asc_hex` (value-level `config=` hex → esds; the old value-level behavior of `aac_config_from_fmtp`) · `rtp_sdp::rtpmap_clock_rate` (`rtpmap` → RTP clock rate) | ✅ |
| KLV metadata (SMPTE ST 336 / MISB ST 0601) | — | `KlvItem` · `UasLocalSet` (BER length + BER-OID tags, tag 2 precision timestamp, tag 1 CRC-16/CCITT checksum) | ✅ |
| KLV-over-RTP | — | `packetise_klv` / `depacketise_klv` (RFC 6597 `smpte336m`, timestamp-shared fragmentation, marker on last) | ✅ |
| RTMP transport (carries FLV A/V) | `Unpackage`/`Package` | `RtmpDemux` / `RtmpMux` (chunk stream, AMF0, → FLV spoke) | ✅ |
| FLV demux/mux | `Unpackage`/`Package` | `FlvDemux` / `FlvMux` (H.264 + AAC, Adobe FLV v10.1 Annex E) | ✅ |
| I-frame trick-play track | — | `derive_iframe_track` / `append_iframe_track` (sync-sample-only, timeline-conserving) | ✅ |
✅ = implemented + round-trip-tested · ⬜ = planned (issue linked)
## Quick start
```rust
use transmux::{build_init_segment, build_media_segment, CodecConfig, TrackSpec,
Sample, FragmentTrackData};
// Describe each track (codec config synthesised by the caller from SPS/PPS, ADTS…).
let tracks: Vec<TrackSpec> = /* … */ Vec::new();
let init = build_init_segment(&tracks, 1000)?; // ftyp + moov
// Feed samples per fragment.
let video: Vec<Sample> = /* … */ Vec::new();
let media = build_media_segment(1, &[FragmentTrackData::new(1, 0, &video)])?;
// styp + moof + mdat
# Ok::<(), transmux::Error>(())
```
For a live/streaming source, `Segmenter` owns the segment-cutting state machine:
```rust
use transmux::{Segmenter, Sample};
# fn tracks() -> Vec<transmux::TrackSpec> { Vec::new() }
# if false {
let mut seg = Segmenter::new(tracks(), 1000, 2.0)?; // ~2 s segments
let init = seg.init_segment()?; // ftyp + moov, once
seg.push(1, /* Sample */ unimplemented!())?; // coded AUs, decode order
for media in seg.take_ready() { /* write out */ }
seg.flush()?; // trailing segment at EOS
# }
# Ok::<(), transmux::Error>(())
```
Codec metadata comes straight from the parsed headers — no external SPS parser:
```rust
# if false {
let sps = transmux::AvcSps(sps_nal_bytes);
let info = sps.decode()?; // profile / level / width·height / chroma …
let mime = sps.rfc6381()?; // e.g. "avc1.4D400D" for WebCodecs / MSE
# }
```
`tests/ts_to_cmaf.rs` demonstrates the full path end-to-end: demux a real
H.264+AAC TS, synthesise `avcC`/`esds`, and emit byte-identical-config CMAF.
## Command-line packager (`cli` feature)
The optional `cli` feature builds a `transmux` binary that wires the demux and
mux spokes into an any-to-any packager: it autodetects the input container from
its leading bytes, runs it through the [`Media`] hub IR, and writes the chosen
output format. The library itself stays `no_std`; only this feature pulls
`clap` + `std`. It follows the workspace CLI standard
([`docs/CLI-STANDARD.md`](../docs/CLI-STANDARD.md)) — clap derive, named flags,
auto `--help`/`--version`.
```console
$ cargo install transmux --features cli # or: cargo run -p transmux --features cli --
# TS → CMAF (autodetected input; format inferred from the .cmaf extension)
$ transmux in.ts -o out.cmaf
# MP4 → CMAF-HLS, 4-second segments
$ transmux in.mp4 -o out.m3u8 -f hls --segment-duration 4
# WebM → progressive single-file MP4
$ transmux in.webm -o out.mp4 -f progressive
# PS → DASH MPD (low-latency chunked variant)
$ transmux in.ps -o out.mpd -f dash --ll
```
| Flag | Meaning |
|---|---|
| `<IN>` / `-i, --input <PATH>` | input file (container autodetected) |
| `-o, --output <PATH>` | output file / playlist / manifest path |
| `-f, --format <FMT>` | `cmaf` \| `hls` \| `ts-hls` \| `dash` \| `ts` \| `progressive` (else inferred from the output extension) |
| `--segment-duration <SECS>` | target segment duration (default 6) |
| `--ll` | low-latency variant where supported (LL-DASH) |
| `--tracks <IDS>` | restrict to these track IDs (comma-separated) |
| `--decrypt` / `--key <KID:KEY>` | decrypt CENC input (requires the `cenc` feature) |
Input autodetect signatures: MPEG-TS (`0x47` sync at 0 and +188), MP4/CMAF
(`ftyp`/`styp`/`moov`/`moof` box at offset 4), MPEG-PS (`00 00 01 BA`),
WebM/Matroska (EBML `1A 45 DF A3`), FLV (`"FLV"`).
## Spec grounding
Every layout cites its source (ISO/IEC 14496-12 boxes; 14496-1 §7.2.6 esds;
ITU-T H.264 §7.3.2.1.1 / H.265 §7.3.2.2 SPS; ETSI TS 102 366 Annex F AC-3/E-AC-3).
Codec-config tests decode **real ffmpeg-encoded fixtures** and assert against an
independent oracle (`trace_headers` fields; ffmpeg's own MP4-muxer config boxes).
## License
MIT OR Apache-2.0.