Skip to content

0.17.0: a channel relayed to another nixamp, losslessly compressed (PRD 0001) - #124

Merged
ralyodio merged 4 commits into
mainfrom
worktree-stream-compression
Sep 12, 2026
Merged

0.17.0: a channel relayed to another nixamp, losslessly compressed (PRD 0001)#124
ralyodio merged 4 commits into
mainfrom
worktree-stream-compression

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Implements the P0 baseline of OpenPRD 0001, Add lossless stream compression and efficient HLS delivery to NixAmp, and commits the PRD itself as the first entry of a new prd/ collection (logicsrc prd validate --strict passes).

What this adds

  • The envelope (application/vnd.nixamp.stream, specified byte by byte with test vectors in docs/stream-compression.md): a 16-byte stream header naming boundary and generation, 48-byte frames each with mode, sequence, lengths and SHA-256, and an end marker carrying the generation's total and digest. Every field is validated against negotiated limits before allocation; a stream without its marker is reported as cut off, never as complete.
  • Zstandard from node:zlib (Bun 1.4 and Node 24 both ship it): no native dependency, async on the runtime's thread pool, behind a bounded pool with deadlines and a decode cap that refuses bombs.
  • One compressor per channel however many receivers. Blocks flush at 256 KiB or 100 ms, are sent compressed only when that saves 3% and 512 bytes, stored otherwise; auto backs off after eight stored blocks in a row and resamples after a cooldown. A compressor that falls behind ends its receivers; a slow receiver is cut off alone. Ordinary listeners now also have a ceiling on unsent bytes (a stalled socket's buffer used to grow until the channel ended).
  • Routes: GET/PATCH /api/channels/:id/compression (conditional on If-Match), POST …/compression/analyses (bounded, deduplicated, cancellable jobs; GET/DELETE /api/compression/analyses/:job), GET/POST/DELETE /api/channels/:id/relay (serve, bring in, stop), GET /api/compression and PATCH {enabled} as the server-wide kill switch, GET /api/media/:n/relay for a library file's representation (built once, published by rename, checked against the file on every lookup, byte-budgeted; the original keeps answering ranges, the representation refuses them).
  • CLI nixamp compression analyze|status|set|off|on|pull|fetch over the same routes; analyze FILE needs no server and reports the container by its bytes, TS packet layout and null share, PCR bitrate, and every codec's complete wire size with a checked round trip.
  • fMP4 HLS, opt-in per channel or server-wide: media copied into .m4s with an init segment named per packager run, the key on the EXT-X-MAP line, and the playlist's real segment lengths reported rather than the target.
  • ts-zstd, experimental and flagged: 188-byte packet headers grouped apart from payloads in front of Zstandard, exactly reversible (ragged edges, malformed adaptation lengths, null packets kept whole); offered only when the policy asks and the receiver can undo it, chosen only when smaller.

Everything is off by default. No ordinary playback URL changes.

Not in this PR (PRD P1/P2, or gated on measurements)

  • A live source relayed at the source boundary: needs a tee in front of ffmpeg; asking for it answers SOURCE_BOUNDARY_UNAVAILABLE.
  • PWA/desktop controls and MCP tools (nixamp has no MCP server today); the API is their contract.
  • A lower-bitrate qualityProfile; the API refuses anything but source rather than re-encoding quietly.
  • birnpack; real-feed benchmarks on target hardware; the fMP4 client matrix; the 24-hour soak. These gate any change of default.

Tests

38 new tests: envelope vectors and every refusal code, codec round trips and the bomb guard, the pool, the block builder, the TS transform, the analyzer, relay encoder/decoder end to end with corruption of every kind, policy store, jobs, static cache, fMP4 packaging with a real ffmpeg, and route integration including two servers relaying a channel and a library file served as a verified representation. Full suite: 502 pass; the one fail/error is web/test failing to load @profullstack/player in this worktree, unchanged from main.

A padded testsrc transport stream (84% null packets) measures an 89% saving at the source boundary; an unpadded feed will measure close to zero, which is the honest answer the tool gives.

Note

Conflicts with #123 on the three version lines and two eight-line regions of src/server.ts; whichever merges second rebases.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp

ralyodio and others added 2 commits September 11, 2026 21:14
…RD 0001)

One nixamp can now carry another's channel with fewer bytes on the wire
and every byte restored, through a negotiated envelope on a path of its
own. Nothing about ordinary playback changes, and all of it is off until
a channel's policy says otherwise.

The envelope (application/vnd.nixamp.stream, docs/stream-compression.md):
a 16-byte stream header naming the boundary and generation, then 48-byte
frames each carrying the mode, sequence, lengths and SHA-256 of the bytes
they stand for, ending in a marker with the whole generation's total and
digest. Every field is checked against negotiated limits before a byte is
allocated; a stream that stops without its marker is reported cut off.

Compression is Zstandard from node:zlib (Bun 1.4 and Node 24 both ship
it), off the event loop on the runtime's pool behind a bounded queue with
deadlines. One encoder per channel however many receivers; a late joiner
is sent the channel's opening bytes for itself and then the shared blocks.
A block is flushed when full or after 100 ms, never waiting for a packet
boundary; it is sent compressed only when that saves 3% and 512 bytes,
stored otherwise, and `auto` stops trying after eight stored blocks in a
row until a cooldown. A compressor that falls behind ends its receivers;
a receiver that stops draining is cut off alone. Ordinary listeners now
also have a ceiling on unsent bytes, where before a stalled socket's
buffer grew until the channel ended.

Per channel: GET/PATCH /api/channels/:id/compression (conditional on the
version you saw), POST .../compression/analyses for a bounded, deduped,
cancellable analysis job, GET/POST/DELETE .../relay to serve, bring in,
or stop a relay. Server-wide: GET /api/compression and PATCH {enabled}
as the kill switch. A library file gets a representation at
/api/media/:n/relay, built once, published by rename, checked against
the file on every lookup, under a byte budget; the original keeps
answering ranges and the representation refuses them.

`nixamp compression analyze|status|set|off|on|pull|fetch` speak the same
routes; `analyze FILE` runs here with no server, reporting the container
as told by the bytes, the transport-stream layout and null share, the
bitrate the PCR implies, and every codec's complete wire size with a
checked round trip.

HLS can be packaged as fragmented MP4, opt-in: the same boxes copied
into .m4s files with an init segment named per packager run so an old
init cannot be paired with new media, the key on the EXT-X-MAP line, and
the playlist's real segment lengths reported rather than the target.

The experimental ts-zstd transform groups 188-byte packet headers apart
from payloads in front of Zstandard, reversibly, incomplete tails and
malformed adaptation fields included; it is offered only when a policy
asks and a receiver can undo it, and chosen only when smaller.

Not here yet, and answered honestly when asked for: a live source at the
source boundary (SOURCE_BOUNDARY_UNAVAILABLE), PWA and desktop controls,
MCP tools, a lower-bitrate quality profile, birnpack, and the real-feed,
client-matrix and soak measurements that gate any change of default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp
ralyodio and others added 2 commits September 12, 2026 11:23
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp
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
@ralyodio
ralyodio marked this pull request as ready for review September 12, 2026 11:30
@ralyodio
ralyodio merged commit f68f355 into main Sep 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant