Raw 1080p and 4K transport streams play, go live, and reach a browser - #126
Merged
Conversation
A `.ts` file off a capture card, a receiver or an IPTV recorder is how most high-definition television arrives, and it was the one shape of media nixamp could not take. The library walk skipped it, the router read it as a song and sent back its soundtrack, and what did reach a browser was re-encoded when it did not need to be. - A library lists transport streams. `.m2ts`, `.mts`, `.m2t`, `.trp` and `.tp` are taken on their names; a `.ts` is opened and asked, because it is as often a TypeScript file as it is 4K television and a checkout is not a playlist. Three sync bytes at one packet's spacing, at 188, 192 or 204, and from any offset, since a recording cut mid-stream does not start on a packet boundary. The answer is remembered: it is asked once per track per listing. - A transport stream is read further into before anything decides what is in it -- 20 MB and 10 seconds rather than ffprobe's default -- and decoded with `+genpts+discardcorrupt`, because a 4K recording can carry seconds of null padding and no timestamps on its first frames. - H.264 is copied at whatever size it is. 1080p and 4K cost the same to copy as 720p, so a 4K recording is watched and put on the air without an encoder running at all. Only the transport stream's ADTS audio is redone, as before. - H.265 is copied when the client says it can decode one -- Safari, phones, televisions -- with the `hvc1` tag every player wants rather than the `hev1` ffmpeg writes. The browser says so for itself in the URL (`?hevc=1`); it is never guessed from a user agent. Where it cannot, the re-encode comes down to 1080p: measured here, 4K through libx264 -preset veryfast runs at 0.52x real time and 1080p at 1.65x, and an encode slower than real time is not a stream. A channel has one encode and a whole audience, so it re-encodes H.265 by default; NIXAMP_HEVC_CHANNELS=1 copies it through. - A channel's backlog is six seconds again rather than four megabytes. Four megabytes is six seconds of 720p and under one second of copied 4K, which puts a joiner on the live edge with no cushion -- the play-wait-play loop the backlog exists to prevent. The cap now follows the rate the channel is measured running at, between the old floor and a 48 MB ceiling, and the rate is only believed after a window has closed: the burst at the start of a pull is not a bitrate. - An H.265 channel is packaged into fMP4 HLS segments rather than transport ones, keyed off what the channel emits rather than what its source holds. HLS in TS is defined for H.264 only, and Safari -- the browser that needs HLS and the one that decodes H.265 -- plays an HEVC channel packaged as TS as sound over a black screen. - In the browser, a picked `.ts` is media rather than something filtered out of the list, and goes to the mpegts.js engine instead of a <video> that cannot open it. Tests build their own fixtures with ffmpeg -- 1080p and 4K H.264, and 4K HEVC where libx265 is there -- and skip cleanly without it. Verified end to end against a running server: a 4K H.264 recording comes back as 3840x2160 avc1 over /api/media and on its channel, a 4K HEVC one as 1080p H.264 or, with ?hevc=1, as 3840x2160 hvc1, and an HEVC channel's playlist carries #EXT-X-MAP with .m4s parts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
ralyodio
marked this pull request as ready for review
September 12, 2026 11:24
ralyodio
added a commit
that referenced
this pull request
Sep 12, 2026
Two fragmented-MP4 packagings met: the relay's, with a Packaging type and a per-run token on the init segment, and the transport-stream work's, which asks for fMP4 for an H.265 channel. The relay's shape stays and the H.265 rule folds into it: playlist(id, fmp4) picks fmp4 when asked, else the compression policy's packaging. One segmentType, the finer one. The transport tests learn the tokened init name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEguYxw44VkpjFrxxGrNGx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
.tsfile off a capture card, a receiver or an IPTV recorder is how most high-definition television arrives, and it was the one shape of media nixamp could not take. The library walk skipped the extension, the router read it as a song and handed back its soundtrack, and what did reach a browser was re-encoded when it did not need to be.What was broken
findAudiohad no transport-stream extension on its list, so a folder of recordings was an empty library.hasPicturedid not know them either, so/api/mediasent a 4K film throughtranscode()—-vn, MP3, no picture.videoArgsre-encoded every H.265 source at its own size. Measured on this box, 4K through libx264-preset veryfastruns at 0.52x real time; 1080p at 1.65x. A 4K HEVC film therefore arrived slower than it played.live-fmp4-needs-backlogfinding).What changed
.m2ts,.mts,.m2t,.trp,.tpare taken on their names. A.tsis opened and asked — three sync bytes at one packet's spacing, at 188, 192 or 204 bytes, from any offset — because it is as often a TypeScript file as it is 4K television, and a checkout is not a playlist. Memoized, since it is asked once per track per listing.transportProbeArgs/transportInputArgs: 20 MB probesize, 10 s analyzeduration,-fflags +genpts+discardcorrupt.codecsOfnow also reports the picture's width and height, and those survive a restart inchannels.json.hvc1tag (ffmpeg writeshev1, which Safari plays as a black panel) when the client says it can decode one — the browser answers for itself via?hevc=1, never a user-agent guess. Where it cannot, the re-encode comes down to 1080p. A channel has one encode and a whole audience, so it re-encodes H.265 by default;NIXAMP_HEVC_CHANNELS=1copies it through.#EXT-X-MAP,.m4s) rather than transport segments, keyed off what the channel emits rather than what its source holds. HLS in TS is defined for H.264 only..tsis media rather than something filtered out of the list, and goes to the mpegts.js engine instead of a<video>that cannot open it.Testing
test/transport.test.tsbuilds its own fixtures with ffmpeg — 1080p and 4K H.264, 4K HEVC where libx265 is present — and every test that needs one skips cleanly without it. Nothing large is committed.bun test test web/test— 530 pass, 1 skip (pre-existing), 0 failbun run typecheck(root + web + backtoschool) — cleanbun run web:build— cleanVerified against a running server on a library of raw
.tsfiles:.tsover/api/media.tsover/api/media.ts, plain.ts,?hevc=1NIXAMP_HEVC_CHANNELS=1#EXT-X-MAPand.m4sStill open
yuv420p.🤖 Generated with Claude Code
https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f