Skip to content

Commit 3be4903

Browse files
kixelatedclaude
andcommitted
feat(moq-net): make payload compression per-hop, per-frame (drafts#39)
Implements the moq-lite half of moq-dev/drafts#39: the publisher only signals *whether* a track is worth compressing, while the algorithm is negotiated per hop and recorded per frame. A single frame can opt out — e.g. a small JSON merge-patch delta that DEFLATE would only enlarge — instead of bloating it. Wire (moq-lite-05-wip): - TRACK_INFO `Publisher Compression` becomes a boolean hint (>1 reserved, decodes as true so it stays additive). It names no algorithm. - New SETUP `Compression` parameter (id 0x3): each endpoint advertises the algorithms it can decompress, packed varints, none(0) omitted. moq-net advertises [deflate]. Per hop, per direction; never forwarded by a relay. - New per-frame `Compression` field in FRAME, present iff the track is hinted, after the timestamp delta and before the length. Names the codec actually used (none/deflate); it doubles as the group's end-of-frames sentinel when no timestamp precedes it. Behavior: - The publisher reads the peer's advertised algorithms (PeerSetup) before serving a compress-hinted track. It forwards an already-DEFLATE cached frame verbatim when the peer can inflate it (relay passthrough, no inflate/deflate), and otherwise picks per frame whether DEFLATE actually shrinks the payload, sending `none` when it wouldn't (or the peer can't inflate). Empty payloads always use `none`. - The subscriber decodes the per-frame field and caches each frame in that codec, so the cache holds compressed bytes (billing counts the wire size) and inflates only at delivery. No datagram path exists in moq-net yet, so that part of the draft is N/A here. The IETF moqt compression extension (moq-compression) is left for a follow-up; the IETF path keeps sending verbatim, which is spec-compliant "extension not negotiated". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5c0a615 commit 3be4903

8 files changed

Lines changed: 337 additions & 123 deletions

File tree

rs/moq-native/tests/broadcast.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,11 @@ async fn broadcast_moq_lite_05_fetch_webtransport() {
355355
lite05_fetch_roundtrip("https").await;
356356
}
357357

358-
/// A modern (lite-05) subscriber caches a `compress` track's frames in their
359-
/// Deflate-compressed form: the model holds the packed bytes (so its byte count
360-
/// matches the wire/compressed size and exposes the codec), and only inflates
361-
/// them when the payload is actually read. This is the relay-billing invariant —
362-
/// the cache counts compressed bytes — and the decode-at-delivery contract.
358+
/// A modern (lite-05) subscriber caches a `compress` track's frames in the codec
359+
/// each frame names on the wire: a compressible frame is DEFLATE (so the cached
360+
/// byte count matches the wire/compressed size the relay-billing invariant), a
361+
/// tiny frame DEFLATE would only enlarge opts out and is cached verbatim, and both
362+
/// inflate back to the original only when read (the decode-at-delivery contract).
363363
async fn lite05_compress_caches_compressed(scheme: &str) {
364364
use moq_net::{Compression, Timescale, Timestamp};
365365

@@ -378,6 +378,9 @@ async fn lite05_compress_caches_compressed(scheme: &str) {
378378
// unmistakably smaller than the decoded payload. Written verbatim, the way an
379379
// origin produces frames (the publisher compresses on egress).
380380
let payload = bytes::Bytes::from(vec![b'a'; 4096]);
381+
// A second, tiny payload DEFLATE would only enlarge: the publisher must send it
382+
// verbatim (per-frame opt-out) even though the track is compress-hinted.
383+
let tiny = bytes::Bytes::from_static(b"hi");
381384
let mut group = track.append_group().expect("failed to append group");
382385
let mut writer = group
383386
.create_frame(moq_net::Frame {
@@ -388,6 +391,15 @@ async fn lite05_compress_caches_compressed(scheme: &str) {
388391
.expect("failed to create frame");
389392
writer.write(payload.clone()).expect("failed to write frame");
390393
writer.finish().expect("failed to finish frame");
394+
let mut writer = group
395+
.create_frame(moq_net::Frame {
396+
size: tiny.len() as u64,
397+
timestamp: Some(Timestamp::new(2_000, Timescale::MICRO).unwrap()),
398+
compression: Compression::None,
399+
})
400+
.expect("failed to create tiny frame");
401+
writer.write(tiny.clone()).expect("failed to write tiny frame");
402+
writer.finish().expect("failed to finish tiny frame");
391403
group.finish().expect("failed to finish group");
392404

393405
let mut server_config = moq_native::ServerConfig::default();
@@ -462,6 +474,15 @@ async fn lite05_compress_caches_compressed(scheme: &str) {
462474
let decoded = frame_sub.read_all().await.expect("failed to read frame");
463475
assert_eq!(decoded, payload);
464476

477+
// The tiny frame opted out: cached verbatim (no per-frame DEFLATE), reads back unchanged.
478+
let mut tiny_sub = tokio::time::timeout(TIMEOUT, group_sub.next_frame())
479+
.await
480+
.expect("next_frame timed out")
481+
.expect("next_frame failed")
482+
.expect("expected a second frame");
483+
assert_eq!(tiny_sub.compression, Compression::None);
484+
assert_eq!(tiny_sub.read_all().await.expect("failed to read tiny frame"), tiny);
485+
465486
drop(session);
466487
server_handle
467488
.await

rs/moq-net/src/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ impl Client {
161161
let our_setup = lite::Setup {
162162
probe: lite::ProbeLevel::Report,
163163
path: self.setup_path.clone(),
164+
// We can inflate DEFLATE, so a peer may compress what it sends us.
165+
compression: vec![crate::Compression::Deflate],
164166
};
165167

166168
let (recv_bw, connecting) = lite::start(

0 commit comments

Comments
 (0)