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
17 changes: 12 additions & 5 deletions js/room/src/local.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,30 @@ function setup() {
return { local, bandwidth, screen: sources.at(-1) as FakeSource, broadcasts: { camera, screen: screenBroadcast } };
}

// Replays LeavingStopsPublishing in `verify/Room.tla`: the screen broadcast is enabled by a live
// share alone, so a participant who never joined, or who left, still publishes their screen.
test("residual: a screen share publishes while not joined and after leaving", async () => {
// Replays LeavingStopsPublishing in `verify/Room.tla`: a participant who has not joined, or has
// left, neither publishes nor encodes their screen, even with a live share.
test("a screen share publishes only while joined", async () => {
Comment thread
kixelated marked this conversation as resolved.
Resolved
const { local, screen, broadcasts } = setup();
// The screen's hd, sd, and audio encoders are constructed last.
const screenEncoders = () => encoders.slice(-3).map((e) => (e.enabled as Signal<boolean>).peek());
try {
local.screenEnabled.set(true);
screen.out.state.set({ kind: "live", media: { video: {} } });
await flush();
expect(broadcasts.camera.enabled.peek()).toBe(false);
expect(broadcasts.screen.enabled.peek()).toBe(true);
expect(broadcasts.screen.enabled.peek()).toBe(false);
Comment thread
kixelated marked this conversation as resolved.
Resolved
expect(screenEncoders()).toEqual([false, false, false]);

local.enabled.set(true);
await flush();
expect(broadcasts.screen.enabled.peek()).toBe(true);
expect(screenEncoders()).toEqual([true, true, true]);

local.enabled.set(false);
await flush();
expect(broadcasts.camera.enabled.peek()).toBe(false);
expect(broadcasts.screen.enabled.peek()).toBe(true);
expect(broadcasts.screen.enabled.peek()).toBe(false);
expect(screenEncoders()).toEqual([false, false, false]);
} finally {
local.close();
}
Expand Down
18 changes: 13 additions & 5 deletions js/room/src/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export interface LocalProps {
*
* Enable {@link enabled} to join (announce `{identity}/camera.hang`). Camera and
* microphone capture are separate knobs; the screenshare broadcast is
* announced only while a share is live.
* announced only while joined and a share is live, so nothing is published
* before joining or after leaving.
*/
export class Local {
/** Participant identity. */
Expand Down Expand Up @@ -96,6 +97,9 @@ export class Local {
#screenAudioSource = new Signal<Publish.Audio.Source | undefined>(undefined);
#screenLive = new Signal(false);

// The screen broadcast and its encoders run only while joined and sharing.
#screenPublished = new Signal(false);

#signals = new Effect();

#control(value: GetterInit<boolean> | undefined): Signal<boolean> {
Expand Down Expand Up @@ -189,9 +193,13 @@ export class Local {
});
this.#signals.cleanup(() => this.camera.close());

this.#signals.run((effect) => {
this.#screenPublished.set(effect.get(this.enabled) && effect.get(this.#screenLive));
});

this.screen = new Publish.Broadcast({
origin,
enabled: this.#screenLive,
enabled: this.#screenPublished,
name: screenName,
display: this.screenCapture.out.display,
});
Expand Down Expand Up @@ -236,7 +244,7 @@ export class Local {
const screenHd = new Publish.Video.Encoder("video/hd", {
broadcast: this.screen,
capture: this.screenCapture,
enabled: this.#screenLive,
enabled: this.#screenPublished,
bandwidth,
config: { maxPixels: 1920 * 1080 },
});
Expand All @@ -245,7 +253,7 @@ export class Local {
const screenSd = new Publish.Video.Encoder("video/sd", {
broadcast: this.screen,
capture: this.screenCapture,
enabled: this.#screenLive,
enabled: this.#screenPublished,
bandwidth,
config: { maxPixels: 960 * 540 },
});
Expand All @@ -260,7 +268,7 @@ export class Local {
const screenAudio = new Publish.Audio.Encoder("audio", {
broadcast: this.screen,
capture: screenAudioCapture,
enabled: this.#screenLive,
enabled: this.#screenPublished,
bandwidth,
});
this.#signals.cleanup(() => screenAudio.close());
Expand Down
13 changes: 6 additions & 7 deletions js/room/verify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

`Room.tla` is a TLA+ model of you and Bob in a room, written to be drawn as a statechart with
`/statechart`. Your side is `src/local.ts`: joining announces your camera broadcast, and a live
screen share announces your screen broadcast. Your view of Bob is `src/room.ts`: each of his
screen share announces your screen broadcast while joined. Your view of Bob is `src/room.ts`: each of his
announcements adds or removes one of his broadcasts, and your connection dropping removes him
until the reconnect announces him again.

| Rule | Holds when | Today |
| --- | --- | --- |
| `LeavingStopsPublishing` | Someone who has not joined the room, or has left it, publishes nothing to it. | Breaks: the screen broadcast is enabled by a live share alone, so a share started before joining, or kept after leaving, stays published, and the room shows its owner. `residual: a screen share publishes while not joined and after leaving` in `src/local.test.ts` replays it. |
| Rule | Holds when |
| --- | --- |
| `LeavingStopsPublishing` | Someone who has not joined the room, or has left it, publishes nothing to it. |

The chart also shows every participant leaving your screen while your connection is down and
coming back once it reconnects.
Expand All @@ -19,12 +19,11 @@ coming back once it reconnects.
just js tla room/verify/Room.tla
```

TLC exits non-zero while the rule above breaks. To draw the chart, run `/statechart` with
To draw the chart, run `/statechart` with
`Room.tla`, `Room.cfg`, and `Room.view.ts`. `Room.cfg` plays only Bob's camera; add `"screen"` to
`Kinds` to play his share too.

## Left out

Chat, typing, and user metadata, more than one remote participant, a participant restarting under
the same identity, identity and prefix changes, and anything an app does on top of `Local`, such
as turning sharing off when you leave.
the same identity, identity and prefix changes, and anything an app does on top of `Local`.
5 changes: 2 additions & 3 deletions js/room/verify/Room.tla
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
\* You and another participant, Bob, in a room, as you see it.
\*
\* Your side is js/room/src/local.ts: `enabled` joins by announcing `{you}/camera.hang`, and the
\* screen broadcast `{you}/screen.hang` is announced while a share is live (`#screenLive`),
\* whether or not you joined. Your view of Bob is js/room/src/room.ts: each announcement under
\* screen broadcast `{you}/screen.hang` is announced while joined and a share is live. Your view of Bob is js/room/src/room.ts: each announcement under
\* the room adds or removes one of Bob's broadcasts, and Bob is on your screen while one is.
\* Announcements reach you one at a time. When your connection drops, the entries it fed retract
\* at once (reload.ts), and the reconnect announces them again.
Expand All @@ -24,7 +23,7 @@ Init ==
/\ seen = {}

\* What you publish into the room.
Yours == (IF joined THEN {"camera"} ELSE {}) \cup (IF sharing THEN {"screen"} ELSE {})
Yours == (IF joined THEN {"camera"} ELSE {}) \cup (IF joined /\ sharing THEN {"screen"} ELSE {})

\* You.
Join == ~joined /\ joined' = TRUE /\ UNCHANGED <<sharing, conn, bob, seen>>
Expand Down
26 changes: 0 additions & 26 deletions quest/m1/room-screen-joined.md

This file was deleted.