Skip to content

Commit 598b99a

Browse files
kixelatedclaude
andauthored
feat(net): callable Time unit constructors + brand the Opus frameDuration knob (#1703)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b1580b4 commit 598b99a

12 files changed

Lines changed: 52 additions & 41 deletions

File tree

js/hang/src/util/latency.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class Latency {
2121

2222
signals = new Effect();
2323

24-
#combined = new Signal<Moq.Time.Milli>(0 as Moq.Time.Milli);
24+
#combined = new Signal<Moq.Time.Milli>(Time.Milli(0));
2525
readonly combined: Signal<Moq.Time.Milli> = this.#combined;
2626

2727
constructor(props: LatencyProps) {
@@ -45,7 +45,7 @@ export class Latency {
4545
}
4646
jitter ??= 0;
4747

48-
const latency = Time.Milli.add(jitter as Moq.Time.Milli, buffer);
48+
const latency = Time.Milli.add(Time.Milli(jitter), buffer);
4949
this.#combined.set(latency);
5050
}
5151

js/net/src/lite/subscriber.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Compression, decompress } from "../compression.ts";
66
import { Group } from "../group.ts";
77
import * as Path from "../path.ts";
88
import { type Reader, Stream } from "../stream.ts";
9-
import type * as Time from "../time.ts";
9+
import * as Time from "../time.ts";
1010
import type { TrackProducer } from "../track.ts";
1111
import { error } from "../util/error.ts";
1212
import { withTimeout } from "../util/timeout.ts";
@@ -540,7 +540,7 @@ export class Subscriber {
540540
if (!probe) break;
541541
this.#recvBandwidth.set(probe.bitrate ?? undefined);
542542
if (this.#rtt && probe.rtt !== undefined) {
543-
this.#rtt.set(probe.rtt as Time.Milli);
543+
this.#rtt.set(Time.Milli(probe.rtt));
544544
}
545545
}
546546
} catch (err: unknown) {

js/net/src/time.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export type Nano = number & { readonly _brand: "nano" };
22

3-
export const Nano = {
3+
// Calling `Nano(x)` brands a raw number as nanoseconds. The unit is the caller's assertion: no
4+
// conversion happens, so reach for `fromMicro`/`fromMilli`/`fromSecond` when the source has a unit.
5+
export const Nano = Object.assign((value: number): Nano => value as Nano, {
46
zero: 0 as Nano,
57
fromMicro: (us: Micro): Nano => (us * 1_000) as Nano,
68
fromMilli: (ms: Milli): Nano => (ms * 1_000_000) as Nano,
@@ -15,11 +17,13 @@ export const Nano = {
1517
div: (a: Nano, b: number): Nano => (a / b) as Nano,
1618
max: (a: Nano, b: Nano): Nano => Math.max(a, b) as Nano,
1719
min: (a: Nano, b: Nano): Nano => Math.min(a, b) as Nano,
18-
} as const;
20+
});
1921

2022
export type Micro = number & { readonly _brand: "micro" };
2123

22-
export const Micro = {
24+
// Calling `Micro(x)` brands a raw number as microseconds. See the `Nano` note: this asserts the unit
25+
// rather than converting, so use `fromNano`/`fromMilli`/`fromSecond` to convert from another unit.
26+
export const Micro = Object.assign((value: number): Micro => value as Micro, {
2327
zero: 0 as Micro,
2428
fromNano: (ns: Nano): Micro => (ns / 1_000) as Micro,
2529
fromMilli: (ms: Milli): Micro => (ms * 1_000) as Micro,
@@ -34,11 +38,13 @@ export const Micro = {
3438
div: (a: Micro, b: number): Micro => (a / b) as Micro,
3539
max: (a: Micro, b: Micro): Micro => Math.max(a, b) as Micro,
3640
min: (a: Micro, b: Micro): Micro => Math.min(a, b) as Micro,
37-
} as const;
41+
});
3842

3943
export type Milli = number & { readonly _brand: "milli" };
4044

41-
export const Milli = {
45+
// Calling `Milli(x)` brands a raw number as milliseconds. See the `Nano` note: this asserts the unit
46+
// rather than converting, so use `fromNano`/`fromMicro`/`fromSecond` to convert from another unit.
47+
export const Milli = Object.assign((value: number): Milli => value as Milli, {
4248
zero: 0 as Milli,
4349
fromNano: (ns: Nano): Milli => (ns / 1_000_000) as Milli,
4450
fromMicro: (us: Micro): Milli => (us / 1_000) as Milli,
@@ -53,11 +59,13 @@ export const Milli = {
5359
div: (a: Milli, b: number): Milli => (a / b) as Milli,
5460
max: (a: Milli, b: Milli): Milli => Math.max(a, b) as Milli,
5561
min: (a: Milli, b: Milli): Milli => Math.min(a, b) as Milli,
56-
} as const;
62+
});
5763

5864
export type Second = number & { readonly _brand: "second" };
5965

60-
export const Second = {
66+
// Calling `Second(x)` brands a raw number as seconds. See the `Nano` note: this asserts the unit
67+
// rather than converting, so use `fromNano`/`fromMicro`/`fromMilli` to convert from another unit.
68+
export const Second = Object.assign((value: number): Second => value as Second, {
6169
zero: 0 as Second,
6270
fromNano: (ns: Nano): Second => (ns / 1_000_000_000) as Second,
6371
fromMicro: (us: Micro): Second => (us / 1_000_000) as Second,
@@ -72,4 +80,4 @@ export const Second = {
7280
div: (a: Second, b: number): Second => (a / b) as Second,
7381
max: (a: Second, b: Second): Second => Math.max(a, b) as Second,
7482
min: (a: Second, b: Second): Second => Math.min(a, b) as Second,
75-
} as const;
83+
});

js/publish/src/audio/encoder.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import * as Catalog from "@moq/hang/catalog";
22
import * as Container from "@moq/hang/container";
33
import * as Util from "@moq/hang/util";
44
import type * as Moq from "@moq/net";
5-
import type { Time } from "@moq/net";
5+
import { Time } from "@moq/net";
66
import { Effect, type Getter, Signal } from "@moq/signals";
77
import type * as Capture from "./capture";
88
import { type Kind, normalizeSource, type Source } from "./types";
99

1010
const GAIN_MIN = 0.001;
1111
const FADE_TIME = 0.2;
1212
const OPUS_BITRATE_PER_CHANNEL = 32_000;
13-
const OPUS_FRAME_DURATION_MS = 20;
13+
const OPUS_FRAME_DURATION = Time.Milli(20);
1414
const AAC_BITRATE_PER_CHANNEL = 64_000;
1515
const AAC_FRAME_SAMPLES = 1024; // AAC-LC encodes a fixed 1024 samples per frame.
1616

@@ -42,7 +42,8 @@ export type OpusConfig = {
4242
mime: "opus";
4343

4444
bitrate?: number; // bits/sec, defaults to channelCount * 32kbps
45-
frameDuration?: number; // ms, Opus supports 2.5-60ms, defaults to 20ms (the real-time default)
45+
// The type carries the unit (ms): build with Time.Milli(20). Opus supports 2.5-60ms, defaults to 20ms.
46+
frameDuration?: Time.Milli;
4647
complexity?: number; // 0-10, higher is better quality but more CPU
4748
packetlossperc?: number; // 0-100, expected loss the encoder optimizes for
4849
useinbandfec?: boolean; // in-band forward error correction
@@ -225,7 +226,7 @@ export class Encoder {
225226
bitrate: Catalog.u53(codec.bitrate ?? captured.channelCount * OPUS_BITRATE_PER_CHANNEL),
226227
container: { kind: "legacy" } as const,
227228
// jitter doubles as the Opus frame duration; toEncoderConfig converts it to µs for WebCodecs.
228-
jitter: Catalog.u53(codec.frameDuration ?? OPUS_FRAME_DURATION_MS),
229+
jitter: Catalog.u53(codec.frameDuration ?? OPUS_FRAME_DURATION),
229230
};
230231
}
231232

@@ -424,7 +425,7 @@ function toEncoderConfig(
424425

425426
// jitter carries the frame duration in ms; WebCodecs wants µs.
426427
if (config.jitter !== undefined) {
427-
opus.frameDuration = config.jitter * 1000;
428+
opus.frameDuration = Time.Micro.fromMilli(Time.Milli(config.jitter));
428429
}
429430

430431
if (Object.keys(opus).length > 0) {

js/publish/src/video/polyfill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function TrackProcessor(track: StreamTrack): ReadableStream<VideoFrame> {
5757
async pull(controller) {
5858
while (true) {
5959
const now = Time.Milli.now();
60-
if (Time.Milli.sub(now, last) < ((1000 / frameRate) as Time.Milli)) {
60+
if (Time.Milli.sub(now, last) < Time.Milli(1000 / frameRate)) {
6161
await new Promise((r) => requestAnimationFrame(r));
6262
continue;
6363
}

js/watch/src/audio/source.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as Catalog from "@moq/hang/catalog";
22
import type * as Moq from "@moq/net";
3+
import { Time } from "@moq/net";
34
import { Effect, type Getter, getter, type Inputs, type Readonlys, readonlys, Signal } from "@moq/signals";
45
import type { Broadcast } from "../broadcast";
56

@@ -123,7 +124,7 @@ export class Source {
123124
const codecJitter = selected.config.jitter ?? defaultAudioJitter(selected.config) ?? 0;
124125
const overhead = Math.ceil((WORKLET_QUANTUM / selected.config.sampleRate) * 1000);
125126
const jitter = codecJitter + overhead;
126-
effect.set(this.#output.jitter, jitter as Moq.Time.Milli);
127+
effect.set(this.#output.jitter, Time.Milli(jitter));
127128
}
128129

129130
/**

js/watch/src/element.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default class MoqWatch extends HTMLElement {
224224

225225
#setLatencyNumber(value: string | null) {
226226
const parsed = value ? Number.parseFloat(value) : Number.NaN;
227-
this.controls.latency.set((Number.isFinite(parsed) ? parsed : 100) as Time.Milli);
227+
this.controls.latency.set(Moq.Time.Milli(Number.isFinite(parsed) ? parsed : 100));
228228
}
229229

230230
attributeChangedCallback(name: Observed, oldValue: string | null, newValue: string | null) {
@@ -325,7 +325,7 @@ export default class MoqWatch extends HTMLElement {
325325

326326
/** @deprecated Use `latency = <number>` instead. */
327327
set jitter(value: number) {
328-
this.controls.latency.set(value as Time.Milli);
328+
this.controls.latency.set(Moq.Time.Milli(value));
329329
}
330330

331331
get catalogFormat(): CatalogFormat | undefined {

js/watch/src/sync.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { Effect, type Getter, getter, type Inputs, type Readonlys, readonlys, Si
55
/** Latency: `"real-time"` auto-computes jitter from RTT; a `Time.Milli` sets a fixed jitter. */
66
export type Latency = "real-time" | Time.Milli;
77

8-
const MIN_JITTER = 20 as Time.Milli;
9-
const FALLBACK_JITTER = 100 as Time.Milli;
8+
const MIN_JITTER = Time.Milli(20);
9+
const FALLBACK_JITTER = Time.Milli(100);
1010

1111
type SyncInput = {
1212
// The latency setting: "real-time" auto-computes jitter from RTT, a number sets a fixed jitter.
@@ -94,7 +94,7 @@ export class Sync {
9494
this.#minRtt = this.#minRtt !== undefined ? Math.min(this.#minRtt, rtt) : rtt;
9595

9696
// Buffer enough for a retransmit (1 RTT for ACK + retransmit).
97-
const jitter = Math.max(MIN_JITTER, this.#minRtt * 1.25) as Time.Milli;
97+
const jitter = Time.Milli(Math.max(MIN_JITTER, this.#minRtt * 1.25));
9898
this.#output.jitter.set(jitter);
9999
return;
100100
}

js/watch/src/ui/components/buffer-control.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Moq } from "@moq/hang";
1+
import { Moq } from "@moq/hang";
22
import type { Effect } from "@moq/signals";
33
import type { BufferedRanges } from "../..";
44
import type MoqWatch from "../../element";
55

6-
const MIN_RANGE = 0 as Moq.Time.Milli;
7-
const RANGE_STEP = 10 as Moq.Time.Milli;
8-
const DEFAULT_MAX = 4000 as Moq.Time.Milli;
6+
const MIN_RANGE = Moq.Time.Milli(0);
7+
const RANGE_STEP = Moq.Time.Milli(10);
8+
const DEFAULT_MAX = Moq.Time.Milli(4000);
99
const LABEL_WIDTH = 48;
1010

1111
function drawRanges(
@@ -41,8 +41,8 @@ function drawRanges(
4141

4242
for (let i = 0; i < ranges.length; i++) {
4343
const range = ranges[i];
44-
const startMs = (range.start - timestamp) as Moq.Time.Milli;
45-
const endMs = (range.end - timestamp) as Moq.Time.Milli;
44+
const startMs = Moq.Time.Milli(range.start - timestamp);
45+
const endMs = Moq.Time.Milli(range.end - timestamp);
4646
const visibleStart = Math.max(0, startMs);
4747
const visibleEnd = Math.min(endMs, max);
4848

@@ -139,8 +139,8 @@ export function bufferControl(parent: Effect, watch: MoqWatch, max: Moq.Time.Mil
139139
const trackWidth = rect.width - LABEL_WIDTH;
140140
const x = Math.max(0, Math.min(clientX - rect.left - LABEL_WIDTH, trackWidth));
141141
const ms = (x / trackWidth) * max;
142-
const snapped = (Math.round(ms / RANGE_STEP) * RANGE_STEP) as Moq.Time.Milli;
143-
const clamped = Math.max(MIN_RANGE, Math.min(max, snapped)) as Moq.Time.Milli;
142+
const snapped = Moq.Time.Milli(Math.round(ms / RANGE_STEP) * RANGE_STEP);
143+
const clamped = Moq.Time.Milli(Math.max(MIN_RANGE, Math.min(max, snapped)));
144144
watch.controls.latency.set(clamped);
145145
};
146146

@@ -169,18 +169,18 @@ export function bufferControl(parent: Effect, watch: MoqWatch, max: Moq.Time.Mil
169169
});
170170

171171
parent.event(viz, "keydown", (e) => {
172-
let delta = 0 as Moq.Time.Milli;
172+
let delta = Moq.Time.Milli(0);
173173
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
174174
delta = RANGE_STEP;
175175
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
176-
delta = -RANGE_STEP as Moq.Time.Milli;
176+
delta = Moq.Time.Milli(-RANGE_STEP);
177177
} else {
178178
return;
179179
}
180180
e.preventDefault();
181181
interact();
182182
const current = watch.backend.output.jitter.peek();
183-
const value = Math.max(MIN_RANGE, Math.min(max, current + delta)) as Moq.Time.Milli;
183+
const value = Moq.Time.Milli(Math.max(MIN_RANGE, Math.min(max, current + delta)));
184184
watch.controls.latency.set(value);
185185
});
186186

js/watch/src/ui/components/latency.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Moq } from "@moq/hang";
1+
import { Moq } from "@moq/hang";
22
import type { Effect } from "@moq/signals";
33
import * as DOM from "@moq/signals/dom";
44
import type MoqWatch from "../../element";
@@ -25,7 +25,7 @@ export function latencyTab(parent: Effect, watch: MoqWatch): HTMLElement {
2525
const buttons = PRESETS.map((preset) => {
2626
const chip = DOM.create("button", { className: "chip", type: "button" }, preset.label);
2727
parent.event(chip, "click", () => {
28-
watch.controls.latency.set(preset.value === "real-time" ? "real-time" : (preset.value as Moq.Time.Milli));
28+
watch.controls.latency.set(preset.value === "real-time" ? "real-time" : Moq.Time.Milli(preset.value));
2929
});
3030
chips.appendChild(chip);
3131
return { preset, chip };

0 commit comments

Comments
 (0)