Skip to content

Commit 7c12495

Browse files
kixelatedclaude
andauthored
Add media source labels to sync latency tracking (#1222)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent fdb139d commit 7c12495

4 files changed

Lines changed: 41 additions & 18 deletions

File tree

js/watch/src/audio/mse.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class Mse implements Backend {
118118

119119
// Extract the timestamp from the CMAF segment and mark when we received it.
120120
const timestamp = Container.Cmaf.decodeTimestamp(frame, timescale);
121-
this.source.sync.received(Moq.Time.Milli.fromMicro(timestamp));
121+
this.source.sync.received(Moq.Time.Milli.fromMicro(timestamp), "audio");
122122

123123
await this.#appendBuffer(sourceBuffer, frame);
124124

@@ -162,7 +162,8 @@ export class Mse implements Backend {
162162
pending = next.frame;
163163

164164
// Mark that we received this frame for latency calculation.
165-
this.source.sync.received(Moq.Time.Milli.fromMicro(pending.timestamp as Moq.Time.Micro));
165+
const timestamp = Moq.Time.Milli.fromMicro(pending.timestamp as Moq.Time.Micro);
166+
this.source.sync.received(timestamp, "audio");
166167

167168
break;
168169
}
@@ -178,7 +179,8 @@ export class Mse implements Backend {
178179
duration = Moq.Time.Micro.sub(frame.timestamp, pending.timestamp);
179180

180181
// Mark that we received this frame for latency calculation.
181-
this.source.sync.received(Moq.Time.Milli.fromMicro(frame.timestamp as Moq.Time.Micro));
182+
const timestamp = Moq.Time.Milli.fromMicro(frame.timestamp as Moq.Time.Micro);
183+
this.source.sync.received(timestamp, "audio");
182184
}
183185

184186
// Wrap raw frame in moof+mdat

js/watch/src/sync.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ export class Sync {
2929
// There's probably a way to use Effect, but lets keep it simple for now.
3030
#update: PromiseWithResolvers<void>;
3131

32-
// Batched late-frame tracking: accumulate count and max lateness, log on recovery.
33-
#lateCount = 0;
34-
#lateMaxMs = 0;
32+
// Per-label late-frame tracking: accumulate count and max lateness, flush on recovery.
33+
#late = new Map<string, { count: number; maxMs: number }>();
3534

3635
signals = new Effect();
3736

@@ -58,7 +57,7 @@ export class Sync {
5857
}
5958

6059
// Update the reference if this is the earliest frame we've seen, relative to its timestamp.
61-
received(timestamp: Time.Milli): void {
60+
received(timestamp: Time.Milli, label = ""): void {
6261
const now = Time.Milli.now();
6362
const ref = Time.Milli.sub(now, timestamp);
6463
const currentRef = this.#reference.peek();
@@ -69,12 +68,21 @@ export class Sync {
6968
// Otherwise, chained `wait()` calls would cause a false-positive during CPU starvation.
7069
const sleep = Time.Milli.add(Time.Milli.sub(currentRef, ref), this.#latency.peek());
7170
if (sleep < 0) {
72-
this.#lateCount++;
73-
this.#lateMaxMs = Math.max(this.#lateMaxMs, -sleep);
74-
} else if (this.#lateCount > 0) {
75-
console.warn(`sync: ${this.#lateCount} late frame(s), max ${Math.round(this.#lateMaxMs)}ms behind`);
76-
this.#lateCount = 0;
77-
this.#lateMaxMs = 0;
71+
const entry = this.#late.get(label);
72+
if (entry) {
73+
entry.count++;
74+
entry.maxMs = Math.max(entry.maxMs, -sleep);
75+
} else {
76+
this.#late.set(label, { count: 1, maxMs: -sleep });
77+
}
78+
} else {
79+
const entry = this.#late.get(label);
80+
if (entry) {
81+
const prefix = label ? `sync[${label}]` : "sync";
82+
const behind = Sync.#formatDuration(entry.maxMs);
83+
console.warn(`${prefix}: ${entry.count} late frame(s), max ${behind} behind`);
84+
this.#late.delete(label);
85+
}
7886
}
7987

8088
if (ref >= currentRef) {
@@ -114,6 +122,15 @@ export class Sync {
114122
}
115123
}
116124

125+
static #formatDuration(ms: number): string {
126+
ms = Math.round(ms);
127+
if (ms < 1000) return `${ms}ms`;
128+
const s = ms / 1000;
129+
if (s < 60) return `${Math.round(s * 10) / 10}s`;
130+
const m = s / 60;
131+
return `${Math.round(m * 10) / 10}m`;
132+
}
133+
117134
close() {
118135
this.signals.close();
119136
}

js/watch/src/video/decoder.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ class DecoderTrack {
320320
}
321321

322322
// Mark that we received this frame right now.
323-
this.source.sync.received(Time.Milli.fromMicro(frame.timestamp as Time.Micro));
323+
const timestamp = Time.Milli.fromMicro(frame.timestamp as Time.Micro);
324+
this.source.sync.received(timestamp, "video");
324325

325326
const chunk = new EncodedVideoChunk({
326327
type: frame.keyframe ? "key" : "delta",
@@ -398,7 +399,8 @@ class DecoderTrack {
398399
});
399400

400401
// Mark that we received this frame right now.
401-
this.source.sync.received(Time.Milli.fromMicro(sample.timestamp as Time.Micro));
402+
const timestamp = Time.Milli.fromMicro(sample.timestamp as Time.Micro);
403+
this.source.sync.received(timestamp, "video");
402404

403405
// Track stats
404406
this.stats.update((current) => ({

js/watch/src/video/mse.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class Mse implements Backend {
122122

123123
// Extract the timestamp from the CMAF segment and mark when we received it.
124124
const timestamp = Container.Cmaf.decodeTimestamp(frame, timescale);
125-
this.source.sync.received(Moq.Time.Milli.fromMicro(timestamp));
125+
this.source.sync.received(Moq.Time.Milli.fromMicro(timestamp), "video");
126126

127127
await this.#appendBuffer(sourceBuffer, frame);
128128

@@ -170,7 +170,8 @@ export class Mse implements Backend {
170170
pending = next.frame;
171171

172172
// Mark that we received this frame right now.
173-
this.source.sync.received(Moq.Time.Milli.fromMicro(pending.timestamp as Moq.Time.Micro));
173+
const timestamp = Moq.Time.Milli.fromMicro(pending.timestamp as Moq.Time.Micro);
174+
this.source.sync.received(timestamp, "video");
174175

175176
break;
176177
}
@@ -185,7 +186,8 @@ export class Mse implements Backend {
185186
duration = Moq.Time.Micro.sub(frame.timestamp, pending.timestamp);
186187

187188
// Mark that we received this frame right now for latency calculation.
188-
this.source.sync.received(Moq.Time.Milli.fromMicro(frame.timestamp as Moq.Time.Micro));
189+
const timestamp = Moq.Time.Milli.fromMicro(frame.timestamp as Moq.Time.Micro);
190+
this.source.sync.received(timestamp, "video");
189191
}
190192

191193
// Wrap raw frame in moof+mdat

0 commit comments

Comments
 (0)