@tribe-nest/forge 3.39.0 → 3.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/_tests/publishedResolvability.spec.ts +81 -3
- package/src/data/queries/useAuthActions.ts +15 -2
- package/src/data/queries/useBroadcasts.ts +60 -0
- package/src/i18n/de.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/ui/headless/broadcast/useBroadcastPoll.ts +225 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +12 -2
- package/src/ui/styled/_tests/broadcastPollVote.spec.ts +127 -0
- package/src/ui/styled/_tests/broadcastStage.spec.ts +41 -0
- package/src/ui/styled/_tests/pollResults.spec.ts +66 -0
- package/src/ui/styled/broadcast/BroadcastMoqStage.tsx +124 -0
- package/src/ui/styled/broadcast/BroadcastPlayer.tsx +178 -10
- package/src/ui/styled/broadcast/BroadcastPollPanel.tsx +124 -0
- package/src/ui/styled/broadcast/BroadcastRealtimeStage.tsx +6 -3
- package/src/ui/styled/broadcast/broadcastStage.ts +38 -2
- package/src/ui/styled/broadcast/pollResults.ts +66 -0
- package/src/ui/styled/broadcast/vendor/moq-watch.bundle.d.ts +12 -0
- package/src/ui/styled/broadcast/vendor/moq-watch.bundle.js +549 -0
- package/src/ui/styled/forge-utilities.css +7 -0
|
@@ -28,7 +28,16 @@ import type { ConnectionState, DisconnectCause, ProducerEntry, RoomState } from
|
|
|
28
28
|
* picture.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* `moq` is a relay, `realtime` is the media plane, and `hls` is the stream
|
|
33
|
+
* everybody can always watch.
|
|
34
|
+
*
|
|
35
|
+
* `moq` outranks `realtime` because the two show the same picture at the same
|
|
36
|
+
* latency and cost very different amounts: a relay viewer is one more
|
|
37
|
+
* subscriber on a cached object, and a plane viewer is a WebRTC consumer with
|
|
38
|
+
* its own SRTP context and its own bandwidth estimate, capped at
|
|
39
|
+
* `STREAM_V2_REALTIME_VIEWER_CAP` because the node runs out.
|
|
40
|
+
*/
|
|
32
41
|
/**
|
|
33
42
|
* Which stage the player shows.
|
|
34
43
|
*
|
|
@@ -37,7 +46,7 @@ import type { ConnectionState, DisconnectCause, ProducerEntry, RoomState } from
|
|
|
37
46
|
* was showing HLS meanwhile, and an HLS engine starts fetching the moment it
|
|
38
47
|
* mounts, so a realtime viewer pulled segments they would never watch.
|
|
39
48
|
*/
|
|
40
|
-
export type BroadcastStage = "realtime" | "hls" | "pending";
|
|
49
|
+
export type BroadcastStage = "moq" | "realtime" | "hls" | "pending";
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
52
|
* The viewer-token endpoint said no.
|
|
@@ -78,6 +87,25 @@ export type BroadcastStageInput = {
|
|
|
78
87
|
/** The refusal, when the fetch failed. `null` while it has not. */
|
|
79
88
|
credentialsError: BroadcastCredentialsError | null;
|
|
80
89
|
roomState?: BroadcastRoomState;
|
|
90
|
+
/** The token endpoint returned a relay ticket for this viewer. */
|
|
91
|
+
moqAvailable?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The relay lane was tried and is over.
|
|
94
|
+
*
|
|
95
|
+
* Separate from `moqAvailable` for the same reason `realtimeIsOver` is
|
|
96
|
+
* separate from `credentialsReady`: a ticket that exists is not a lane that
|
|
97
|
+
* is working, and the fallback has to be able to happen after a connection
|
|
98
|
+
* that started fine.
|
|
99
|
+
*/
|
|
100
|
+
moqFailed?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Is the plane lane offered at all?
|
|
103
|
+
*
|
|
104
|
+
* `false` on an instance that runs relays and no media plane, which is what
|
|
105
|
+
* `credentials: null` from `viewer-token` means. Without it the chooser would
|
|
106
|
+
* fall to `realtime` after a relay failure and mount a room that is not there.
|
|
107
|
+
*/
|
|
108
|
+
planeAvailable?: boolean;
|
|
81
109
|
};
|
|
82
110
|
|
|
83
111
|
/**
|
|
@@ -115,7 +143,12 @@ export function chooseBroadcastStage(input: BroadcastStageInput): BroadcastStage
|
|
|
115
143
|
// about to be replaced, so a realtime viewer downloaded the broadcast twice.
|
|
116
144
|
// Nothing is drawn until the answer is known.
|
|
117
145
|
if (!input.credentialsReady) return "pending";
|
|
146
|
+
// The relay first, and only while it is actually working.
|
|
147
|
+
if (input.moqAvailable && !input.moqFailed) return "moq";
|
|
118
148
|
if (realtimeIsOver(input.roomState)) return "hls";
|
|
149
|
+
// The relay failed and there is no plane behind it. `planeAvailable` defaults
|
|
150
|
+
// to true so every existing caller keeps its behaviour unchanged.
|
|
151
|
+
if (input.planeAvailable === false) return "hls";
|
|
119
152
|
return "realtime";
|
|
120
153
|
}
|
|
121
154
|
|
|
@@ -135,6 +168,9 @@ export function broadcastFellBack(input: BroadcastStageInput): boolean {
|
|
|
135
168
|
if (!input.realtimeAvailable) return false;
|
|
136
169
|
if (input.credentialsError) return true;
|
|
137
170
|
if (!input.credentialsReady) return false;
|
|
171
|
+
// A working relay is not a fallback, it is the best lane there is.
|
|
172
|
+
if (input.moqAvailable && !input.moqFailed) return false;
|
|
173
|
+
if (input.moqAvailable && input.moqFailed && input.planeAvailable === false) return true;
|
|
138
174
|
return realtimeIsOver(input.roomState);
|
|
139
175
|
}
|
|
140
176
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll tallies as the studio, the stage overlay and the player all render them.
|
|
3
|
+
*
|
|
4
|
+
* One function because three surfaces show the same numbers, and three
|
|
5
|
+
* roundings of the same votes would disagree with each other on screen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type PollLike = {
|
|
9
|
+
totalVotes: number;
|
|
10
|
+
options: Array<{ id: string; label: string; voteCount: number }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PollResult = {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
voteCount: number;
|
|
17
|
+
/** 0..100, integer. */
|
|
18
|
+
percent: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Percentages that add up to 100.
|
|
23
|
+
*
|
|
24
|
+
* Rounding each share on its own is what usually goes wrong: three options on
|
|
25
|
+
* one vote each show as 33% three times and a viewer reads 99, and 1/3 + 2/3
|
|
26
|
+
* shows as 33 + 67 only by luck. This uses largest remainder, so the displayed
|
|
27
|
+
* numbers sum to exactly 100 whenever anybody has voted at all.
|
|
28
|
+
*
|
|
29
|
+
* With no votes every option is 0, NOT an even split. A poll that has just
|
|
30
|
+
* opened showing "50% / 50%" claims two votes that were never cast.
|
|
31
|
+
*/
|
|
32
|
+
export function pollPercentages(poll: PollLike): PollResult[] {
|
|
33
|
+
const options = poll.options ?? [];
|
|
34
|
+
const total = options.reduce((sum, option) => sum + option.voteCount, 0);
|
|
35
|
+
if (total <= 0) {
|
|
36
|
+
return options.map((option) => ({ ...option, percent: 0 }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const exact = options.map((option) => {
|
|
40
|
+
const share = (option.voteCount / total) * 100;
|
|
41
|
+
return { option, floor: Math.floor(share), remainder: share - Math.floor(share) };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let left = 100 - exact.reduce((sum, entry) => sum + entry.floor, 0);
|
|
45
|
+
// Biggest remainder first; a tie goes to the option with more votes, then to
|
|
46
|
+
// the one listed first, so the same tally always renders the same way.
|
|
47
|
+
const order = [...exact].sort(
|
|
48
|
+
(a, b) =>
|
|
49
|
+
b.remainder - a.remainder ||
|
|
50
|
+
b.option.voteCount - a.option.voteCount ||
|
|
51
|
+
options.indexOf(a.option) - options.indexOf(b.option),
|
|
52
|
+
);
|
|
53
|
+
const bonus = new Set<string>();
|
|
54
|
+
for (const entry of order) {
|
|
55
|
+
if (left <= 0) break;
|
|
56
|
+
bonus.add(entry.option.id);
|
|
57
|
+
left--;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return exact.map(({ option, floor }) => ({
|
|
61
|
+
id: option.id,
|
|
62
|
+
label: option.label,
|
|
63
|
+
voteCount: option.voteCount,
|
|
64
|
+
percent: floor + (bonus.has(option.id) ? 1 : 0),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@moq/watch`, vendored as a self-contained ESM bundle.
|
|
3
|
+
*
|
|
4
|
+
* Side-effect only: importing it registers the `<moq-watch>` custom element.
|
|
5
|
+
* There is nothing to import by name, which is why this declares an empty
|
|
6
|
+
* module rather than a surface.
|
|
7
|
+
*
|
|
8
|
+
* Built by `scripts/build-moq-player.mjs`; see its header for why this is an
|
|
9
|
+
* asset rather than a dependency.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: void;
|
|
12
|
+
export default _default;
|