From 0e1aebef3625b5bcc0baf2145205a200cd134465 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 30 Sep 2026 15:46:54 -0700 Subject: [PATCH] fix(hls): keep a rendition when only its estimated framerate changes Co-Authored-By: Claude Opus 5.5 --- rs/moq-hls/src/export/mod.rs | 7 +++++-- rs/moq-hls/src/export/rendition.rs | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index 1d3cc5df92..a7102e2570 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -638,8 +638,8 @@ mod tests { mp4a.esds.es_desc.dec_config.avg_bitrate } - // A publisher's estimator republishes the catalog every time its measured bitrate or jitter - // moves, writing those two fields and nothing else. That must not churn the rendition: + // A publisher's estimator republishes the catalog every time its measured bitrate, jitter, or + // framerate moves, writing those fields and nothing else. That must not churn the rendition: // rebuilding resets the playlist window, the cached init segment and EXT-X-MEDIA-SEQUENCE, // and makes every recording cursor see a Removed followed by an Added. #[tokio::test] @@ -651,6 +651,8 @@ mod tests { let upstream = empty_upstream(&origin, "live").await; let mut catalog = catalog_with_both(); + // Declared by nobody: the estimator measures it once media flows. + catalog.video.renditions.get_mut("video0").unwrap().framerate = None; let renditions = renditions::Producer::new(Config::default().window); let mut cursor = renditions.subscribe(); renditions.sync(&upstream, &catalog); @@ -678,6 +680,7 @@ mod tests { let video_config = catalog.video.renditions.get_mut("video0").unwrap(); video_config.bitrate = Some(1_800_000); video_config.jitter = Some(Duration::from_millis(33)); + video_config.framerate = Some(25.0); catalog.audio.renditions.get_mut("audio0").unwrap().bitrate = Some(110_000); renditions.sync(&upstream, &catalog); diff --git a/rs/moq-hls/src/export/rendition.rs b/rs/moq-hls/src/export/rendition.rs index 7d30ee5f32..060ff1abf2 100644 --- a/rs/moq-hls/src/export/rendition.rs +++ b/rs/moq-hls/src/export/rendition.rs @@ -192,9 +192,10 @@ fn normalize_video(config: &VideoConfig) -> VideoConfig { config.delay = None; config.label = None; config.stalled = None; - // The muxer ignores a non-finite framerate, and NaN never equals itself, so a catalog - // carrying one would otherwise look different from itself on every republish. - config.framerate = config.framerate.filter(|fps| fps.is_finite()); + // The publisher's estimator fills in the framerate after its first second of media. The + // muxer only sizes its timescale from it, and the rendition keeps muxing at the one its + // init already declared, so a measured framerate is no reason to rebuild. + config.framerate = None; config }