Skip to content
Merged
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
11 changes: 7 additions & 4 deletions js/hang/src/catalog/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ export const AudioConfigSchema = z.object({
// TODO: Support up to Number.MAX_SAFE_INTEGER
bitrate: u53Schema.optional(),

// Minimum buffer size in milliseconds required for smooth playback.
// This represents the minimum time the player should buffer before starting playback.
// The player should add additional buffer on top of this value.
minBuffer: u53Schema.optional(),
// The maximum jitter before the next frame is emitted in milliseconds.
// The player's jitter buffer should be larger than this value.
// If not provided, the player should assume each frame is flushed immediately.
//
// NOTE: The audio "frame" duration depends on the codec, sample rate, etc.
// ex: AAC often uses 1024 samples per frame, so at 44100Hz, this would be 1024/44100 = 23ms
jitter: u53Schema.optional(),
});

export const AudioSchema = z
Expand Down
13 changes: 9 additions & 4 deletions js/hang/src/catalog/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ export const VideoConfigSchema = z.object({
// Default: true
optimizeForLatency: z.boolean().optional(),

// Minimum buffer size in milliseconds required for smooth playback.
// This represents the minimum time the player should buffer before starting playback.
// The player should add additional buffer on top of this value.
minBuffer: u53Schema.optional(),
// The maximum jitter before the next frame is emitted in milliseconds.
// The player's jitter buffer should be larger than this value.
// If not provided, the player should assume each frame is flushed immediately.
//
// ex:
// - If each frame is flushed immediately, this would be 1000/fps.
// - If there can be up to 3 b-frames in a row, this would be 3 * 1000/fps.
// - If frames are buffered into 2s segments, this would be 2s.
jitter: u53Schema.optional(),
});

// Mirrors VideoDecoderConfig
Expand Down
28 changes: 14 additions & 14 deletions js/hang/src/util/latency.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type * as Moq from "@moq/lite";
import { Effect, type Getter, Signal } from "@moq/signals";

type ConfigWithMinBuffer = { minBuffer?: number; framerate?: number };
type ConfigWithJitter = { jitter?: number; framerate?: number };

export interface LatencyProps {
buffer: Signal<Moq.Time.Milli>;
config: Getter<ConfigWithMinBuffer | undefined>;
config: Getter<ConfigWithJitter | undefined>;
}

/**
* A helper class that computes the final latency based on the catalog's minBuffer and the user's buffer.
* If the minBuffer is not present, then we use framerate to estimate a default.
* A helper class that computes the final latency based on the catalog's jitter and the user's buffer.
* If the jitter is not present, then we use framerate to estimate a default.
*
* Effective latency = catalog.minBuffer + buffer
* Effective latency = catalog.jitter + buffer
*/
export class Latency {
buffer: Signal<Moq.Time.Milli>;
config: Getter<ConfigWithMinBuffer | undefined>;
config: Getter<ConfigWithJitter | undefined>;

signals = new Effect();

Expand All @@ -33,18 +33,18 @@ export class Latency {
#run(effect: Effect): void {
const buffer = effect.get(this.buffer);

// Compute the latency based on the catalog's minBuffer and the user's buffer.
// Compute the latency based on the catalog's jitter and the user's buffer.
const config = effect.get(this.config);

// Use minBuffer from catalog if available, otherwise estimate from framerate
let minBuffer: number | undefined = config?.minBuffer;
if (minBuffer === undefined && config?.framerate !== undefined && config.framerate > 0) {
// Estimate minBuffer as one frame duration if framerate is available
minBuffer = 1000 / config.framerate;
// Use jitter from catalog if available, otherwise estimate from framerate
let jitter: number | undefined = config?.jitter;
if (jitter === undefined && config?.framerate !== undefined && config.framerate > 0) {
// Estimate jitter as one frame duration if framerate is available
jitter = 1000 / config.framerate;
}
minBuffer ??= 0;
jitter ??= 0;

const latency = (minBuffer + buffer) as Moq.Time.Milli;
const latency = (jitter + buffer) as Moq.Time.Milli;
this.#combined.set(latency);
}

Expand Down
4 changes: 2 additions & 2 deletions js/hang/src/watch/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export default class HangWatch extends HTMLElement {
// TODO: Temporarily defaults to false because Cloudflare doesn't support it yet.
reload = new Signal(false);

// Additional buffer in milliseconds on top of the catalog's minBuffer (default: 100ms).
// The effective latency = catalog.minBuffer + buffer
// Additional buffer in milliseconds on top of the catalog's jitter (default: 100ms).
// The effective latency = catalog.jitter + buffer
buffer = new Signal(100 as Time.Milli);

// Set when the element is connected to the DOM.
Expand Down
10 changes: 5 additions & 5 deletions js/hang/src/watch/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ export class Sync {
#runLatency(effect: Effect): void {
const buffer = effect.get(this.buffer);

// Compute the latency based on the catalog's minBuffer and the user's buffer.
// Compute the latency based on the catalog's jitter and the user's buffer.
const video = effect.get(this.video);

// Use minBuffer from catalog if available, otherwise estimate from framerate
let videoBuffer: number | undefined = video?.minBuffer;
// Use jitter from catalog if available, otherwise estimate from framerate
let videoBuffer: number | undefined = video?.jitter;
if (videoBuffer === undefined && video?.framerate !== undefined && video.framerate > 0) {
// Estimate minBuffer as one frame duration if framerate is available
// Estimate jitter as one frame duration if framerate is available
videoBuffer = 1000 / video.framerate;
}
videoBuffer ??= 0;

const audio = effect.get(this.audio);
// TODO if there's no explicit buffer, estimate the audio buffer based on the sample rate and codec?
const audioBuffer = audio?.minBuffer ?? 0;
const audioBuffer = audio?.jitter ?? 0;

const latency = (Math.max(videoBuffer, audioBuffer) + buffer) as Time.Milli;
this.#latency.set(latency);
Expand Down
2 changes: 1 addition & 1 deletion rs/hang/examples/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn create_track(broadcast: &mut moq_lite::BroadcastProducer) -> hang::TrackProdu
display_ratio_height: None,
optimize_for_latency: None,
container: hang::catalog::Container::Legacy,
min_buffer: None,
jitter: None,
};

// Create a map of video renditions
Expand Down
13 changes: 6 additions & 7 deletions rs/hang/src/catalog/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,12 @@ pub struct AudioConfig {
#[serde(default)]
pub container: Container,

/// Minimum buffer size in milliseconds required for smooth playback.
/// The maximum jitter before the next frame is emitted in milliseconds.
/// The player's jitter buffer should be larger than this value.
/// If not provided, the player should assume each frame is flushed immediately.
///
/// This represents the minimum time the player should buffer before starting playback.
/// For HLS imports, this is typically the segment duration.
/// For fMP4 imports, this is detected from the fragment duration.
///
/// The player should add additional jitter buffer on top of this value.
/// NOTE: The audio "frame" duration depends on the codec, sample rate, etc.
/// ex: AAC often uses 1024 samples per frame, so at 44100Hz, this would be 1024/44100 = 23ms
#[serde(default)]
pub min_buffer: Option<moq_lite::Time>,
pub jitter: Option<moq_lite::Time>,
}
4 changes: 2 additions & 2 deletions rs/hang/src/catalog/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ mod test {
framerate: Some(30.0),
optimize_for_latency: None,
container: Container::Legacy,
min_buffer: None,
jitter: None,
},
);

Expand All @@ -346,7 +346,7 @@ mod test {
bitrate: Some(128_000),
description: None,
container: Container::Legacy,
min_buffer: None,
jitter: None,
},
);

Expand Down
15 changes: 8 additions & 7 deletions rs/hang/src/catalog/video/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ pub struct VideoConfig {
#[serde(default)]
pub container: Container,

/// Minimum buffer size in milliseconds required for smooth playback.
/// The maximum jitter before the next frame is emitted in milliseconds.
/// The player's jitter buffer should be larger than this value.
/// If not provided, the player should assume each frame is flushed immediately.
///
/// This represents the minimum time the player should buffer before starting playback.
/// For HLS imports, this is typically the segment duration.
/// For fMP4 imports, this is detected from the fragment duration.
///
/// The player should add additional jitter buffer on top of this value.
/// ex:
/// - If each frame is flushed immediately, this would be 1000/fps.
/// - If there can be up to 3 b-frames in a row, this would be 3 * 1000/fps.
/// - If frames are buffered into 2s segments, this would be 2s.
#[serde(default)]
pub min_buffer: Option<moq_lite::Time>,
pub jitter: Option<moq_lite::Time>,
}
2 changes: 1 addition & 1 deletion rs/hang/src/import/aac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl Aac {
bitrate: None,
description: None,
container: hang::catalog::Container::Legacy,
min_buffer: None,
jitter: None,
};

tracing::debug!(name = ?track.name, ?config, "starting track");
Expand Down
2 changes: 1 addition & 1 deletion rs/hang/src/import/avc3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl Avc3 {
display_ratio_height: None,
optimize_for_latency: None,
container: hang::catalog::Container::Legacy,
min_buffer: None,
jitter: None,
};

if let Some(old) = &self.config
Expand Down
32 changes: 16 additions & 16 deletions rs/hang/src/import/fmp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ struct Fmp4Track {
group: Option<moq_lite::GroupProducer>,

// The minimum buffer required for the track.
min_buffer: Option<Timestamp>,
jitter: Option<Timestamp>,

Comment on lines 71 to 73
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor doc mismatch: field renamed to jitter, comment still says “minimum buffer.”
Consider updating the comment to match the new jitter semantics.

✏️ Proposed doc tweak
-	// The minimum buffer required for the track.
+	// The maximum jitter before the next frame is emitted.
🤖 Prompt for AI Agents
In `@rs/hang/src/import/fmp4.rs` around lines 71 - 73, The field comment for
jitter no longer matches its name: update the doc comment above the jitter:
Option<Timestamp> field in the relevant struct (where jitter is declared) to
describe jitter semantics instead of "minimum buffer"—for example, explain that
it represents timestamp jitter allowance or variability used for track timing
calculations so the comment accurately documents the jitter behavior.

// The last timestamp seen for this track.
last_timestamp: Option<Timestamp>,
Expand All @@ -84,7 +84,7 @@ impl Fmp4Track {
kind,
producer,
group: None,
min_buffer: None,
jitter: None,
last_timestamp: None,
min_duration: None,
}
Expand Down Expand Up @@ -272,7 +272,7 @@ impl Fmp4 {
display_ratio_height: None,
optimize_for_latency: None,
container,
min_buffer: None,
jitter: None,
}
}
mp4_atom::Codec::Hev1(hev1) => self.init_h265(true, &hev1.hvcc, &hev1.visual, container)?,
Expand All @@ -289,7 +289,7 @@ impl Fmp4 {
display_ratio_height: None,
optimize_for_latency: None,
container,
min_buffer: None,
jitter: None,
},
mp4_atom::Codec::Vp09(vp09) => {
// https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238
Expand Down Expand Up @@ -317,7 +317,7 @@ impl Fmp4 {
bitrate: None,
framerate: None,
container,
min_buffer: None,
jitter: None,
}
}
mp4_atom::Codec::Av01(av01) => {
Expand Down Expand Up @@ -351,7 +351,7 @@ impl Fmp4 {
bitrate: None,
framerate: None,
container,
min_buffer: None,
jitter: None,
}
}
mp4_atom::Codec::Unknown(unknown) => anyhow::bail!("unknown codec: {:?}", unknown),
Expand Down Expand Up @@ -393,7 +393,7 @@ impl Fmp4 {
display_ratio_height: None,
optimize_for_latency: None,
container,
min_buffer: None,
jitter: None,
})
}

Expand Down Expand Up @@ -428,7 +428,7 @@ impl Fmp4 {
bitrate: Some(bitrate.into()),
description: None, // TODO?
container,
min_buffer: None,
jitter: None,
}
}
mp4_atom::Codec::Opus(opus) => {
Expand All @@ -439,7 +439,7 @@ impl Fmp4 {
bitrate: None,
description: None, // TODO?
container,
min_buffer: None,
jitter: None,
}
}
mp4_atom::Codec::Unknown(unknown) => anyhow::bail!("unknown codec: {:?}", unknown),
Expand Down Expand Up @@ -487,7 +487,7 @@ impl Fmp4 {
anyhow::bail!("missing trun box");
}

// Keep track of the minimum and maximum timestamp for this track to compute the min_buffer.
// Keep track of the minimum and maximum timestamp for this track to compute the jitter.
// Ideally these should both be the same value (a single frame lul).
let mut min_timestamp = None;
let mut max_timestamp = None;
Expand Down Expand Up @@ -643,12 +643,12 @@ impl Fmp4 {
// We report the minimum buffer required as the difference between the min and max frames.
// We also add the duration between frames to account for the frame rate.
// ex. for 2s fragments, this should be exactly 2s if we did everything correctly.
let min_buffer = max - min + min_duration;
let jitter = max - min + min_duration;

if min_buffer < track.min_buffer.unwrap_or(Timestamp::MAX) {
track.min_buffer = Some(min_buffer);
if jitter < track.jitter.unwrap_or(Timestamp::MAX) {
track.jitter = Some(jitter);

// Update the catalog with the new min_buffer
// Update the catalog with the new jitter
let mut catalog = self.broadcast.catalog.lock();

match track.kind {
Expand All @@ -658,15 +658,15 @@ impl Fmp4 {
.renditions
.get_mut(&track.producer.info.name)
.context("missing video config")?;
config.min_buffer = Some(min_buffer.convert()?);
config.jitter = Some(jitter.convert()?);
}
TrackKind::Audio => {
let audio = catalog.audio.as_mut().context("missing audio")?;
let config = audio
.renditions
.get_mut(&track.producer.info.name)
.context("missing audio config")?;
config.min_buffer = Some(min_buffer.convert()?);
config.jitter = Some(jitter.convert()?);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion rs/hang/src/import/hev1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl Hev1 {
display_ratio_height: vui_data.display_ratio_height,
optimize_for_latency: None,
container: hang::catalog::Container::Legacy,
min_buffer: None,
jitter: None,
};

if let Some(old) = &self.config
Expand Down
2 changes: 1 addition & 1 deletion rs/hang/src/import/opus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl Opus {
bitrate: None,
description: None,
container: hang::catalog::Container::Legacy,
min_buffer: None,
jitter: None,
};

tracing::debug!(name = ?track.name, ?config, "starting track");
Expand Down
Loading