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
32 changes: 16 additions & 16 deletions js/hang/src/container/consumer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ test("Legacy Producer omits a reordered group's presentation endpoint marker", a
producer.encode(new Uint8Array([1]), timestamp as Time.Micro, index === 0);
}
producer.encode(new Uint8Array([1]), 160_000 as Time.Micro, true);
producer.cut(200_000 as Time.Micro);
producer.discontinuity(200_000 as Time.Micro);
producer.close();
const group = await subscriber.recvGroup();
expect(group).toBeDefined();
Expand Down Expand Up @@ -199,14 +199,14 @@ test("Legacy Producer estimates the tail from the current cadence", async () =>
expect(end).toBe(131_000);
});

test("Legacy Producer rejects a backwards cut without closing the group", async () => {
test("Legacy Producer rejects a backwards discontinuity without closing the group", async () => {
const track = new Track.Producer("test");
const subscriber = track.subscribe({ maxAge: Time.Milli(30_000) });
const producer = new LegacyProducer(track, new LegacyFormat("video"));
producer.encode(new Uint8Array([1]), 20_000 as Time.Micro, true);
expect(() => producer.cut(10_000 as Time.Micro)).toThrow();
expect(() => producer.discontinuity(10_000 as Time.Micro)).toThrow();
producer.encode(new Uint8Array([2]), 30_000 as Time.Micro, false);
producer.cut(35_000 as Time.Micro);
producer.discontinuity(35_000 as Time.Micro);
producer.close();
const group = await subscriber.recvGroup();
const timestamps = [];
Expand Down Expand Up @@ -245,14 +245,14 @@ async function readGroups(subscriber: Track.Subscriber, last: number) {
}
}

test("Legacy Producer cut marks the break with one empty frame at the live edge", async () => {
test("Legacy Producer discontinuity marks the break with one empty frame at the live edge", async () => {
const track = new Track.Producer("test");
const subscriber = replay(track);
const producer = new LegacyProducer(track, new LegacyFormat("audio"));
producer.encode(new Uint8Array([1]), 0 as Time.Micro, true);
producer.encode(new Uint8Array([1]), 20_000 as Time.Micro, true);
producer.cut();
producer.cut(); // nothing new to mark
producer.discontinuity();
producer.discontinuity(); // nothing new to mark
producer.encode(new Uint8Array([1]), 5_000_000 as Time.Micro, true);
producer.close();

Expand All @@ -264,12 +264,12 @@ test("Legacy Producer cut marks the break with one empty frame at the live edge"
]);
});

test("Legacy Producer cut marks the break at the caller's end", async () => {
test("Legacy Producer discontinuity marks the break at the caller's end", async () => {
const track = new Track.Producer("test");
const subscriber = replay(track);
const producer = new LegacyProducer(track, new LegacyFormat("video"));
producer.encode(new Uint8Array([1]), 0 as Time.Micro, true);
producer.cut(33_000 as Time.Micro);
producer.discontinuity(33_000 as Time.Micro);
producer.close();

expect(await readGroups(subscriber, 1)).toEqual([
Expand All @@ -284,26 +284,26 @@ test("Legacy Producer cut marks the break at the caller's end", async () => {
]);
});

test("Legacy Producer cut marks nothing on a data track or before any frame", async () => {
test("Legacy Producer discontinuity marks nothing on a data track or before any frame", async () => {
const data = new Track.Producer("data");
const producer = new LegacyProducer(data, new LegacyFormat("data"));
producer.encode(new Uint8Array([1]), 0 as Time.Micro, true);
producer.cut();
producer.discontinuity();
expect(data.appendGroup().sequence).toBe(1);

const empty = new Track.Producer("empty");
new LegacyProducer(empty, new LegacyFormat("video")).cut();
new LegacyProducer(empty, new LegacyFormat("video")).discontinuity();
expect(empty.appendGroup().sequence).toBe(0);
});

// A group's reach runs to its successor's first frame, so without the marker the group before a
// pause would stretch across the whole gap and read as live to anyone joining after the resume.
test("Legacy Producer cut keeps pre-pause media from reading as live", async () => {
test("Legacy Producer discontinuity keeps pre-pause media from reading as live", async () => {
const track = new Track.Producer("test");
const producer = new LegacyProducer(track, new LegacyFormat("video"));
producer.encode(new Uint8Array([1]), 0 as Time.Micro, true);
producer.encode(new Uint8Array([1]), 33_000 as Time.Micro, false);
producer.cut();
producer.discontinuity();
producer.encode(new Uint8Array([1]), 5_000_000 as Time.Micro, true);
producer.close();

Expand Down Expand Up @@ -1530,12 +1530,12 @@ test("live duration marker follows an immediately delivered video frame", async
track.close();
});

test("audio cut writes no duration marker", async () => {
test("audio discontinuity writes no duration marker", async () => {
const track = new Track.Producer("test");
const subscriber = track.subscribe();
const producer = new LegacyProducer(track, new LegacyFormat("audio"));
producer.encode(new Uint8Array([1]), 0 as Time.Micro, true);
producer.cut(15_000 as Time.Micro);
producer.discontinuity(15_000 as Time.Micro);
const group = await subscriber.recvGroup();
expect(await group?.readFrame()).toBeDefined();
expect(await group?.readFrame()).toBeUndefined();
Expand Down
4 changes: 2 additions & 2 deletions js/hang/src/container/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class Producer {
#liveEdge?: Time.Micro;
// Gap between consecutive timestamps, used to close the last group when no successor exists.
#interval?: Time.Micro;
// A cut's marker is the newest group, so another one would say nothing new.
// A discontinuity's marker is the newest group, so another one would say nothing new.
#marked = false;

/** Wrap a track to publish legacy-container frames into it. */
Expand Down Expand Up @@ -138,7 +138,7 @@ export class Producer {
* joiner lands on. Data tracks only close the group, since an empty payload is data. No marker
* is written until a frame follows the last one. Throws if `end` precedes the last video frame.
*/
cut(end?: Time.Micro) {
discontinuity(end?: Time.Micro) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve a skipped sequence for data discontinuities

When this newly named method is used with a data format, the branch at line 146 returns after closing the current group, so the next group receives the immediately adjacent sequence, as the changed test explicitly expects. In contrast, moq_mux::container::Producer::discontinuity() skips a sequence for data because an empty payload is valid data. Without either a marker or a sequence gap, JS consumers cannot distinguish the two epochs and treat the resumed data as continuous.

Useful? React with 👍 / 👎.

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.

Leaving the data-track path unchanged. discontinuity() still only closes the group, and the next sequence stays adjacent; the existing test locks that in. Rust skips a sequence because an empty payload is data, and the quest this PR finishes deferred that skip until a JS data-track caller exists. None does. Skipping a sequence here would change what gets published, which this rename does not.

(Written by Grok 4.7)

this.#close(end);
// Nothing is measured across the break.
this.#interval = undefined;
Expand Down
2 changes: 1 addition & 1 deletion js/publish/src/audio/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export class Encoder {
});

// When demand disappears, end the epoch with a discontinuity marker (see
// Container.Legacy.Producer.cut) so a later subscriber resumes on the same track without the
// Container.Legacy.Producer.discontinuity) so a later subscriber resumes on the same track without the
// pre-gap frames reading as live. Its empty payload marks where the submitted media ends.
effect.run((effect) => {
const track = effect.get(rendition.track);
Expand Down
12 changes: 6 additions & 6 deletions js/publish/src/video/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ test("encoding tracks encoder config in its child effect", async () => {
}
});

test("a demand gap cuts the group and leaves the broadcast-owned track open for resume", async () => {
test("a demand gap marks a discontinuity and leaves the broadcast-owned track open for resume", async () => {
using _videoEncoder = installFakeVideoEncoder();
const cut = spyOn(Container.Legacy.Producer.prototype, "cut");
const discontinuity = spyOn(Container.Legacy.Producer.prototype, "discontinuity");

const track = new Moq.Track.Producer("video").accept({ priority: 60 });
const live = new Signal<Moq.Track.Producer | undefined>(track);
Expand All @@ -119,20 +119,20 @@ test("a demand gap cuts the group and leaves the broadcast-owned track open for
live.set(undefined);
await settle();
expect(track.closed.peek()).toBeUndefined();
expect(cut).toHaveBeenCalledTimes(1);
expect(discontinuity).toHaveBeenCalledTimes(1);

live.set(track);
await settle();
expect(track.closed.peek()).toBeUndefined();

cut.mockClear();
discontinuity.mockClear();
track.close();
encoder.close();
expect(cut).not.toHaveBeenCalled();
expect(discontinuity).not.toHaveBeenCalled();
} finally {
encoder.close();
track.close();
cut.mockRestore();
discontinuity.mockRestore();
}
});

Expand Down
4 changes: 2 additions & 2 deletions js/publish/src/video/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ export class Encoder {
this.#lastCaptureWall = performance.now();

const producer = new Container.Legacy.Producer(track, new Container.Legacy.Format("video"));
// The broadcast owns this static track across demand gaps. When demand disappears, cut the
// The broadcast owns this static track across demand gaps. When demand disappears, close the
// current group, marking the break so a later subscriber resumes on the same track without
// the pre-gap group reading as live. A fatal encoder error still aborts the track through
// producer.close(err) below.
effect.cleanup(() => {
if (track.closed.peek() === undefined) producer.cut();
if (track.closed.peek() === undefined) producer.discontinuity();
Comment thread
kixelated marked this conversation as resolved.
});

let lastKeyframe: Time.Micro | undefined;
Expand Down
1 change: 0 additions & 1 deletion quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ upstream release waits in [m4](/quest/m4/README.md).
- [Audio loss recovery](/quest/m2/audio-loss-recovery.md) - prove a useful Opus recovery policy before exposing another option
- [Opus implementation](/quest/m2/audio-opus-backend.md) - compare current codec quality, CPU, and optional build costs
- [Latency ledger](/quest/m2/latency-ledger.md) - a session reports where its end-to-end audio delay went, stage by stage
- [JS discontinuity](/quest/m2/js-discontinuity.md) - JS names its timeline break `discontinuity()` like Rust, so `cut` means the same group close in both
- [Synced data playback](/quest/m2/watch-data-sync.md) - js/watch releases JSON and binary payloads on the media playhead, and a slow data track holds media back
- [Media Foundation decode](/quest/m2/audio-decode-mediafoundation.md) - Windows decodes HE-AAC, multichannel AAC, and what else the MFTs offer
- [Media Foundation encode](/quest/m2/audio-encode-mediafoundation.md) - Windows encodes AAC-LC
Expand Down
24 changes: 0 additions & 24 deletions quest/m2/js-discontinuity.md

This file was deleted.

Loading