Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fc07eee
chore(quest): claim quest/m2/js-hls.md
kixelated Oct 30, 2026
c01909c
feat(hang,publish): publish HLS segment records from JS by default
kixelated Oct 30, 2026
99d887e
test(interop): check JS HLS records against moq-mux's
kixelated Oct 30, 2026
423a1a4
fix: format the interop example and resolve a doc link
kixelated Oct 30, 2026
ce595f2
chore(js): refresh API reports for the HLS recorder
kixelated Oct 30, 2026
94b11f7
quest: finish the JS HLS field
kixelated Oct 30, 2026
4f73d1e
fix(publish): declare HLS durations up front and keep record names free
kixelated Oct 30, 2026
e59ffbd
chore(js): refresh the publish API report
kixelated Oct 30, 2026
43171af
fix(publish): hint zero-duration audio groups and reserve record name…
kixelated Oct 30, 2026
1da9ebb
fix(publish): keep a rendition's media name from becoming another's r…
kixelated Oct 30, 2026
80c58a0
fix(publish): hint all-keyframe video so on-demand HLS discovery starts
kixelated Oct 31, 2026
f22e99e
Merge remote-tracking branch 'origin/main' into merge-7367
kixelated Nov 1, 2026
1a35588
fix(publish): HLS record readers start on-demand audio after merging …
kixelated Nov 1, 2026
cc0cfea
fix(publish): end HLS records whenever their media track closes
kixelated Nov 1, 2026
0a007ef
Merge origin/main into quest/m2/js-hls
kixelated Nov 1, 2026
f3182a9
Merge base and preserve HLS wiring through publication refactor
Nov 2, 2026
b4f3979
Merge branch 'base' into pr-7367
Nov 2, 2026
4f71ff8
fix(relay): gate pacer constructor with its callers
Nov 2, 2026
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
3 changes: 2 additions & 1 deletion js/hang/api/container.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Format as Format_1 } from "@moq/hang/container";
import { Micro } from "@moq/clock";
import { Milli } from "@moq/clock";
import { Net } from "@moq/hang";
import { Recorder } from "@moq/hang/hls";
import { Signals } from "@moq/hang";
import { Video } from "@moq/hang/catalog";

Expand Down Expand Up @@ -64,7 +65,7 @@ export namespace Legacy {
}
export { Frame };
export class Producer {
constructor(track: Net.Track.Producer, format: Legacy.Format);
constructor(track: Net.Track.Producer, format: Legacy.Format, recorder?: Recorder);
close(err?: Error): void;
discontinuity(end?: Micro): void;
encode(data: Uint8Array | Legacy.Source, timestamp: Micro, keyframe: boolean): void;
Expand Down
72 changes: 72 additions & 0 deletions js/hang/api/hls.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# `@moq/hang/hls`

```ts
import { Hls } from "@moq/hang/catalog";
import { Micro } from "@moq/clock";
import { Net } from "@moq/hang";
import { Signals } from "@moq/hang";
import { Window } from "@moq/json";

export interface Config {
duration?: Micro;
durationHint?: Micro;
}

export class Consumer implements AsyncIterable<Event> {
[Symbol.asyncIterator](): AsyncIterator<Event>;
close(): void;
next(): Promise<Event | undefined>;
static subscribe(broadcast: Net.Broadcast.Consumer, hls: Hls): Consumer;
}

export const DEFAULT_DURATION: Micro;

export type Entry = Omit<Record, "pts" | "duration"> & {
duration?: Micro;
pts: Micro;
};

export type Event = {
push: {
entry: Entry;
index: number;
};
} | {
pop: Window.Span;
} | {
skip: Window.Span;
};

export interface Record {
duration?: number;
group: number;
pts: number;
sequence: number;
}

export class Recorder {
constructor(track: Net.Track.Producer, config?: Config);
static readonly INFO: Partial<Net.Track.Info>;
abort(err: Error): void;
end(pts: Micro): void;
finish(): void;
frame(group: number, pts: Micro): void;
readonly hls: Signals.Getter<Hls | undefined>;
stop(group: number): void;
readonly track: Net.Track.Producer;
}

export const SUFFIX: ".hls";

export class Segmenter {
constructor(config?: Config);
close(): void;
get declared(): Micro | undefined;
end(pts: Micro): void;
frame(group: number, pts: Micro): void;
stop(group: number): void;
take(): Record[];
}

export function defaultName(track: string): string;
```
32 changes: 1 addition & 31 deletions js/hang/api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { $ZodBranded } from "zod/v4/core";
import { $brand } from "@zod/mini";
import { $loose } from "zod/v4/core";
import { $strip } from "zod/v4/core";
import { Micro } from "@moq/clock";
import { Window } from "@moq/json";
import { ZodMiniNullable } from "@zod/mini";
import { ZodMiniNumber } from "@zod/mini";
import { ZodMiniObject } from "@zod/mini";
Expand Down Expand Up @@ -158,35 +156,7 @@ export namespace Health {
export function viewer(sample: Health.Sample<Echo.Snapshot>, thresholds?: Health.Thresholds): Health.Health;
}

export namespace Hls {
export class Consumer implements AsyncIterable<Hls.Event> {
[Symbol.asyncIterator](): AsyncIterator<Hls.Event>;
close(): void;
next(): Promise<Hls.Event | undefined>;
static subscribe(broadcast: Net.Broadcast.Consumer, hls: import("@moq/hang/catalog").Hls): Hls.Consumer;
}
export type Entry = Omit<Hls.Record, "pts" | "duration"> & {
duration?: Micro;
pts: Micro;
};
export type Event = {
push: {
entry: Hls.Entry;
index: number;
};
} | {
pop: Window.Span;
} | {
skip: Window.Span;
};
export interface Record {
duration?: number;
group: number;
pts: number;
sequence: number;
}
export const SUFFIX: ".hls";
}
export * as Hls from "@moq/hang/hls";

export * as Net from "@moq/net";

Expand Down
1 change: 1 addition & 0 deletions js/hang/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"./cmaf": "./src/cmaf/index.ts",
"./catalog": "./src/catalog/index.ts",
"./container": "./src/container/index.ts",
"./hls": "./src/hls.ts",
"./util": "./src/util/index.ts"
},
"scripts": {
Expand Down
24 changes: 21 additions & 3 deletions js/hang/src/container/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type { BufferedRange, BufferedRanges, Frame } from "./types";

import type { Micro } from "@moq/clock";
import type { AudioConfig, VideoConfig } from "../catalog";
import type { Recorder } from "../hls";
import type { Format as ContainerFormat } from "./format";
import type { Frame } from "./types";

Expand Down Expand Up @@ -78,11 +79,18 @@ export class Producer {
#interval?: Micro;
// A discontinuity's marker is the newest group, so another one would say nothing new.
#marked = false;
#recorder?: Recorder;

/** Wrap a track to publish legacy-container frames into it. */
constructor(track: Moq.Track.Producer, format: Format) {
/**
* Wrap a track to publish legacy-container frames into it.
*
* With a `recorder`, each written group is reported into the track's HLS records. The recorder
* outlives this producer, so another producer taking over the track continues its records.
*/
constructor(track: Moq.Track.Producer, format: Format, recorder?: Recorder) {
this.#format = format;
this.#track = track;
this.#recorder = recorder;
}

/** Encode and append a frame; a keyframe starts a new group. Throws if the first frame is not a keyframe, a group start goes backwards, or a frame is below the previous group start. */
Expand All @@ -107,6 +115,7 @@ export class Producer {
payload: encodeFrame(data, timestamp),
timestamp: Time.Timestamp.fromMicros(timestamp),
});
if (this.#group) this.#recorder?.frame(this.#group.sequence, timestamp);

this.#reordered ||= this.#previous !== undefined && timestamp < this.#previous;
if (this.#previous !== undefined && timestamp > this.#previous) {
Expand Down Expand Up @@ -147,6 +156,8 @@ export class Producer {
timestamp: Time.Timestamp.fromMicros(timestamp),
});
group.finish();
// The marker is contiguous, so without this the open HLS segment would absorb the pause.
this.#recorder?.stop(group.sequence);
this.#liveEdge = this.#liveEdge === undefined ? timestamp : (Math.max(this.#liveEdge, timestamp) as Micro);
this.#marked = true;
}
Expand All @@ -167,6 +178,11 @@ export class Producer {
this.#end !== undefined && this.#interval !== undefined
? ((this.#end + this.#interval) as Micro)
: undefined;
// Where this group's content stops: the duration marker when there is one, else the furthest
// point written.
const reached =
end === undefined ? this.#end : this.#end === undefined ? end : (Math.max(end, this.#end) as Micro);
if (reached !== undefined) this.#recorder?.end(reached);
// A presentation endpoint cannot bound the decode-order tail after reordering.
this.#format.finishGroup(this.#group, this.#reordered ? undefined : end);
this.#group.finish();
Expand All @@ -179,9 +195,11 @@ export class Producer {
this.#reordered = false;
}

/** Close the track and current group, optionally with an error. */
/** Close the track and current group, optionally with an error. Finishes or aborts the HLS records with it. */
close(err?: Error) {
if (!err) this.#close();
if (err) this.#recorder?.abort(err);
else this.#recorder?.finish();
if (err) this.#group?.abort(err);
else this.#group?.finish();
if (err) this.#track.abort(err);
Expand Down
Loading