Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions rs/moq-hls/src/export/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
Expand Down Expand Up @@ -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);

Expand Down
7 changes: 4 additions & 3 deletions rs/moq-hls/src/export/rendition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +195 to +198

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Refresh the duration fallback without rebuilding the rendition

Framerate also controls sample durations: Muxer::video sets default_frame = 1 / framerate, and duration inference uses it for duration-less group tails (or all reordered samples). After an initial None becomes Some(25.0), this now keeps the old rendition, but refresh updates only bitrate and muxer() keeps using the original config. Supported Legacy/LOC groups without duration markers consequently keep getting 33.33 ms fallback durations instead of 40 ms for the rest of the broadcast; previously the replacement picked up the estimate. Preserve rendition identity and its established timescale while updating the cadence used for missing-duration synthesis, and extend the regression to inspect emitted sample durations after the estimate changes.

config
}

Expand Down
Loading