Skip to content

outhud/dashpipe

Repository files navigation

dashpipe

A single-process, pure-Go data plane that turns a live MPEG-DASH stream into MPEG-TS — read, in-process CENC decrypt, and mux — with zero external tools.

No ffmpeg, no mp4decrypt, no N_m3u8DL-RE, no FIFOs. One static binary (or one imported library) does fetch → decrypt → mux and writes a TS a player can consume.

live DASH (CENC)  ──►  read @ live edge  ──►  in-proc AES-CTR decrypt  ──►  MPEG-TS  ──►  stdout / HTTP

The pieces exist separately in the Go ecosystem (an fMP4 decrypter here, a TS muxer there, an MPD parser elsewhere). What's uncommon is the live glue: staying at the live edge across manifest refreshes, and not falling over at a DAI/SCTE ad-break Period boundary — cleanly cutting and resuming when content returns.

What it does well (and what it doesn't)

Solid:

  • Live DASH read with a held live-edge cursor: SegmentTimeline ($Time$/$Number$) expansion and $Number$+@duration addressing (live edge from wall-clock vs availabilityStartTime), minimumUpdatePeriod refresh, and non-fatal 404/init retry.
  • In-process CENC (AES-CTR / cenc scheme) decryption using only the Go standard library — you supply the content key; it does not acquire keys.
  • fMP4 → MPEG-TS mux (H.264 AVCC→Annex B with in-band SPS/PPS, AAC→ADTS, PTS/DTS/PCR, PAT/PMT).
  • Survives DAI ad breaks: the two tracks (different timescales/PTOs) are rebased onto one monotonic 90 kHz timeline, the Period splice is marked with a TS discontinuity, and content resumes cleanly. No crash, no desync.

⚠️ Out of scope / known-hard (don't expect these):

  • Filling the ad gap with anything nice. The default (--gap drop) simply emits nothing during the break and resumes after — a buffering player spins for the duration. --gap hold freezes the last frame instead. A branded "slate" still is available in the library (GapSlate) but is experimental: a mid-stream SPS switch into a foreign still can stall some hardware decoders even across a TS discontinuity. See Ad breaks.
  • Playing the ads (ad muxing through the splice).
  • DRM license acquisition (Widevine/PlayReady/FairPlay). You bring the key.
  • cbcs (AES-CBC) scheme, HEVC, multi-audio, subtitles. See the support matrix.

Use as a CLI

Secrets go on stdin as one JSON object, never on argv (/proc/<pid>/cmdline is world-readable):

make build      # ./dashpipe
echo '{"key":"<KID:KEY hex>"}' | ./dashpipe run \
  --mpd-url <live MPD URL> \
  --res 1920x1080 \
  --out -          # "-" = stdout (default); ":PORT" = single-client HTTP TS server
  • stdout = stream bytes only. stderr = NDJSON logs. Never mixed.
  • --gap drop|hold — content-gap behaviour at ad breaks (default drop).
  • Exit codes: 0 ok · 1 fatal · 2 bad args · 3 upstream/manifest · 4 key/decrypt · 5 key-rotation boundary (re-resolve a fresh key and respawn).

Pipe it straight into a player or ffprobe to verify:

echo '{"key":"<KID:KEY>"}' | ./dashpipe run --mpd-url <url> --res 1280x720 | ffprobe -

Use as a library

import (
    "github.com/outhud/dashpipe/dash"
    "github.com/outhud/dashpipe/pipeline"
)

err := pipeline.Run(ctx, pipeline.Config{
    Reader: dash.Config{MPDURL: mpdURL, Width: 1280, Height: 720},
    Key:    contentKey,        // [16]byte AES-128; you already resolved it
    KIDHex: kidHex,
    Output: "ts",
    Gap:    pipeline.GapDrop,
    Stdout: w,                 // any io.Writer
})

For streams whose key rotates, supply a KeyProvider instead of a static Key and the pipeline fetches the new key per KID in-process (no respawn):

cfg.KeyProvider = func(ctx context.Context, kidHex string) (key [16]byte, err error) {
    return myCDM.KeyFor(ctx, kidHex)   // your DRM integration lives here
}

The lower-level packages are usable on their own:

Package Purpose
dash live-edge DASH reader → ordered Fragment stream
cenc DecryptFragment(media, key, ivSize) — in-proc CENC AES-CTR
output TSMuxer: fMP4 samples → MPEG-TS; single-client HTTP Serve
mp4 ISO-BMFF init/fragment parse, sample extraction, AVC/AAC config
keymat KID:KEY parse/validate
pipeline wires reader → decrypt → mux under one context

Ad breaks

Server-side DAI inserts clear ad Periods into the live manifest. dashpipe is content-only: it drops those Periods (dash.DropAds) and rebases the content timeline so playback resumes cleanly afterwards. The gap they leave is handled by the --gap / Gap strategy:

  • GapDrop (default) — emit nothing; resume on the next content sample. Proven.
  • GapHoldLastFrame — re-send the last IDR + silent audio so the player holds a frozen picture instead of underrunning. Emits slightly ahead of wall-clock so a buffering player (ExoPlayer/TiviMate) builds a buffer and leaves the spinner.
  • GapSlate — replay a caller-supplied Annex B still. Experimental. The still carries its own SPS; some hardware decoders silently stall on a mid-stream SPS switch even with a TS discontinuity at the seam. We ship the code path and the discontinuity bracketing, but make no promises that a given player/decoder accepts it. If you need reliable in-break visuals, do it upstream of this tool.

Support

Capability Status
DASH live (type=dynamic), SegmentTimeline
DASH live (type=dynamic), $Number$+@duration (no timeline)
$Time$ and $Number$ addressing
CENC cenc scheme (AES-CTR)
CENC cbcs scheme (AES-CBC)
H.264 / AVC video
HEVC / AV1 video
AAC-LC audio → ADTS
Hold-last-frame silent audio ✅ stereo 48 kHz AAC-LC only
DAI ad-break cut + resume
Ad muxing / slate playback ❌ / experimental
DRM license acquisition ❌ (bring your own key)

Build

Requires Go ≥ 1.22.

make build     # ./dashpipe (dev)
make static    # CGO_ENABLED=0 static linux/amd64 binary
make test      # go test ./...
make vet

make test runs everything; tests that need captured fixtures skip cleanly when the fixtures are absent (see testdata/). The CENC decrypt path is covered by a self-contained synthetic round-trip test that needs no fixtures.

License

Apache-2.0. See LICENSE. This project does not include or link any DRM client; key acquisition is the integrator's responsibility and may carry its own legal and licensing constraints in your jurisdiction.

About

Single-process, pure-Go live MPEG-DASH → MPEG-TS data plane: read, in-process CENC decrypt, mux. No ffmpeg.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors