@memberjunction/ai-realtime-client 6.2.0-edge.2 → 6.2.0-edge.4
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/README.md +156 -29
- package/dist/audio/audioMeter.d.ts +71 -0
- package/dist/audio/audioMeter.d.ts.map +1 -1
- package/dist/audio/audioMeter.js +83 -0
- package/dist/audio/audioMeter.js.map +1 -1
- package/dist/audio/mediaElementAudioRouter.d.ts +41 -0
- package/dist/audio/mediaElementAudioRouter.d.ts.map +1 -0
- package/dist/audio/mediaElementAudioRouter.js +82 -0
- package/dist/audio/mediaElementAudioRouter.js.map +1 -0
- package/dist/audio/micCapture.d.ts +12 -3
- package/dist/audio/micCapture.d.ts.map +1 -1
- package/dist/audio/micCapture.js +15 -1
- package/dist/audio/micCapture.js.map +1 -1
- package/dist/audio/pcmPlayback.d.ts +72 -4
- package/dist/audio/pcmPlayback.d.ts.map +1 -1
- package/dist/audio/pcmPlayback.js +101 -1
- package/dist/audio/pcmPlayback.js.map +1 -1
- package/dist/drivers/assemblyAIRealtimeClient.d.ts +2 -0
- package/dist/drivers/assemblyAIRealtimeClient.d.ts.map +1 -1
- package/dist/drivers/assemblyAIRealtimeClient.js +14 -0
- package/dist/drivers/assemblyAIRealtimeClient.js.map +1 -1
- package/dist/drivers/elevenLabsRealtimeClient.d.ts +2 -0
- package/dist/drivers/elevenLabsRealtimeClient.d.ts.map +1 -1
- package/dist/drivers/elevenLabsRealtimeClient.js +14 -0
- package/dist/drivers/elevenLabsRealtimeClient.js.map +1 -1
- package/dist/drivers/geminiAvatarOutput.d.ts +132 -0
- package/dist/drivers/geminiAvatarOutput.d.ts.map +1 -0
- package/dist/drivers/geminiAvatarOutput.js +310 -0
- package/dist/drivers/geminiAvatarOutput.js.map +1 -0
- package/dist/drivers/geminiEnterpriseRealtimeClient.d.ts +38 -0
- package/dist/drivers/geminiEnterpriseRealtimeClient.d.ts.map +1 -0
- package/dist/drivers/geminiEnterpriseRealtimeClient.js +107 -0
- package/dist/drivers/geminiEnterpriseRealtimeClient.js.map +1 -0
- package/dist/drivers/geminiRealtimeClient.d.ts +314 -48
- package/dist/drivers/geminiRealtimeClient.d.ts.map +1 -1
- package/dist/drivers/geminiRealtimeClient.js +759 -176
- package/dist/drivers/geminiRealtimeClient.js.map +1 -1
- package/dist/drivers/openAILiveClient.d.ts +5 -8
- package/dist/drivers/openAILiveClient.d.ts.map +1 -1
- package/dist/drivers/openAILiveClient.js +19 -32
- package/dist/drivers/openAILiveClient.js.map +1 -1
- package/dist/drivers/openAIRealtimeClient.d.ts +23 -26
- package/dist/drivers/openAIRealtimeClient.d.ts.map +1 -1
- package/dist/drivers/openAIRealtimeClient.js +42 -57
- package/dist/drivers/openAIRealtimeClient.js.map +1 -1
- package/dist/generic/baseRealtimeClient.d.ts +188 -30
- package/dist/generic/baseRealtimeClient.d.ts.map +1 -1
- package/dist/generic/baseRealtimeClient.js +250 -14
- package/dist/generic/baseRealtimeClient.js.map +1 -1
- package/dist/generic/openAIProtocolClient.d.ts +57 -1
- package/dist/generic/openAIProtocolClient.d.ts.map +1 -1
- package/dist/generic/openAIProtocolClient.js +89 -4
- package/dist/generic/openAIProtocolClient.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/media/attachVideoSource.d.ts +23 -0
- package/dist/media/attachVideoSource.d.ts.map +1 -0
- package/dist/media/attachVideoSource.js +49 -0
- package/dist/media/attachVideoSource.js.map +1 -0
- package/dist/media/channelVideoSource.d.ts +110 -14
- package/dist/media/channelVideoSource.d.ts.map +1 -1
- package/dist/media/channelVideoSource.js +183 -18
- package/dist/media/channelVideoSource.js.map +1 -1
- package/dist/media/decoders/frameScheduler.d.ts +119 -0
- package/dist/media/decoders/frameScheduler.d.ts.map +1 -0
- package/dist/media/decoders/frameScheduler.js +238 -0
- package/dist/media/decoders/frameScheduler.js.map +1 -0
- package/dist/media/decoders/imageFrameDecoder.d.ts +58 -0
- package/dist/media/decoders/imageFrameDecoder.d.ts.map +1 -0
- package/dist/media/decoders/imageFrameDecoder.js +110 -0
- package/dist/media/decoders/imageFrameDecoder.js.map +1 -0
- package/dist/media/decoders/mseFmp4Decoder.d.ts +130 -0
- package/dist/media/decoders/mseFmp4Decoder.d.ts.map +1 -0
- package/dist/media/decoders/mseFmp4Decoder.js +402 -0
- package/dist/media/decoders/mseFmp4Decoder.js.map +1 -0
- package/dist/media/decoders/streamCanvas.d.ts +46 -0
- package/dist/media/decoders/streamCanvas.d.ts.map +1 -0
- package/dist/media/decoders/streamCanvas.js +163 -0
- package/dist/media/decoders/streamCanvas.js.map +1 -0
- package/dist/media/decoders/videoMimeTypes.d.ts +29 -0
- package/dist/media/decoders/videoMimeTypes.d.ts.map +1 -0
- package/dist/media/decoders/videoMimeTypes.js +50 -0
- package/dist/media/decoders/videoMimeTypes.js.map +1 -0
- package/dist/media/decoders/webCodecsChunkDecoder.d.ts +99 -0
- package/dist/media/decoders/webCodecsChunkDecoder.d.ts.map +1 -0
- package/dist/media/decoders/webCodecsChunkDecoder.js +265 -0
- package/dist/media/decoders/webCodecsChunkDecoder.js.map +1 -0
- package/dist/media/displayCapture.d.ts +128 -0
- package/dist/media/displayCapture.d.ts.map +1 -0
- package/dist/media/displayCapture.js +245 -0
- package/dist/media/displayCapture.js.map +1 -0
- package/dist/media/frameCapture.d.ts +25 -8
- package/dist/media/frameCapture.d.ts.map +1 -1
- package/dist/media/frameCapture.js +42 -72
- package/dist/media/frameCapture.js.map +1 -1
- package/dist/media/frameSampler.d.ts +94 -0
- package/dist/media/frameSampler.d.ts.map +1 -0
- package/dist/media/frameSampler.js +134 -0
- package/dist/media/frameSampler.js.map +1 -0
- package/dist/media/index.d.ts +32 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +32 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/localMediaController.d.ts +116 -0
- package/dist/media/localMediaController.d.ts.map +1 -0
- package/dist/media/localMediaController.js +281 -0
- package/dist/media/localMediaController.js.map +1 -0
- package/dist/media/mediaLayout.d.ts +93 -0
- package/dist/media/mediaLayout.d.ts.map +1 -0
- package/dist/media/mediaLayout.js +187 -0
- package/dist/media/mediaLayout.js.map +1 -0
- package/dist/media/mediaPreview.d.ts +94 -0
- package/dist/media/mediaPreview.d.ts.map +1 -0
- package/dist/media/mediaPreview.js +154 -0
- package/dist/media/mediaPreview.js.map +1 -0
- package/dist/media/mediaStage.d.ts +99 -0
- package/dist/media/mediaStage.d.ts.map +1 -0
- package/dist/media/mediaStage.js +171 -0
- package/dist/media/mediaStage.js.map +1 -0
- package/dist/media/model.d.ts +154 -0
- package/dist/media/model.d.ts.map +1 -0
- package/dist/media/model.js +9 -0
- package/dist/media/model.js.map +1 -0
- package/dist/media/playbackClock.d.ts +20 -0
- package/dist/media/playbackClock.d.ts.map +1 -0
- package/dist/media/playbackClock.js +13 -0
- package/dist/media/playbackClock.js.map +1 -0
- package/dist/media/videoFrameDecoder.d.ts +86 -0
- package/dist/media/videoFrameDecoder.d.ts.map +1 -0
- package/dist/media/videoFrameDecoder.js +2 -0
- package/dist/media/videoFrameDecoder.js.map +1 -0
- package/dist/media/videoFrameDecoderRegistry.d.ts +40 -0
- package/dist/media/videoFrameDecoderRegistry.d.ts.map +1 -0
- package/dist/media/videoFrameDecoderRegistry.js +69 -0
- package/dist/media/videoFrameDecoderRegistry.js.map +1 -0
- package/dist/media/videoPacing.d.ts +32 -0
- package/dist/media/videoPacing.d.ts.map +1 -0
- package/dist/media/videoPacing.js +37 -0
- package/dist/media/videoPacing.js.map +1 -0
- package/dist/media/videoPlayout.d.ts +168 -0
- package/dist/media/videoPlayout.d.ts.map +1 -0
- package/dist/media/videoPlayout.js +217 -0
- package/dist/media/videoPlayout.js.map +1 -0
- package/dist/media/videoSourceArbiter.d.ts +250 -0
- package/dist/media/videoSourceArbiter.d.ts.map +1 -0
- package/dist/media/videoSourceArbiter.js +423 -0
- package/dist/media/videoSourceArbiter.js.map +1 -0
- package/dist/testing/conformanceAssertions.d.ts +25 -0
- package/dist/testing/conformanceAssertions.d.ts.map +1 -0
- package/dist/testing/conformanceAssertions.js +64 -0
- package/dist/testing/conformanceAssertions.js.map +1 -0
- package/dist/testing/conformanceFixtures.d.ts +76 -0
- package/dist/testing/conformanceFixtures.d.ts.map +1 -0
- package/dist/testing/conformanceFixtures.js +169 -0
- package/dist/testing/conformanceFixtures.js.map +1 -0
- package/dist/testing/conformanceGates.d.ts +32 -0
- package/dist/testing/conformanceGates.d.ts.map +1 -0
- package/dist/testing/conformanceGates.js +35 -0
- package/dist/testing/conformanceGates.js.map +1 -0
- package/dist/testing/conformanceMicrophone.d.ts +10 -0
- package/dist/testing/conformanceMicrophone.d.ts.map +1 -0
- package/dist/testing/conformanceMicrophone.js +71 -0
- package/dist/testing/conformanceMicrophone.js.map +1 -0
- package/dist/testing/conformanceTimeline.d.ts +128 -0
- package/dist/testing/conformanceTimeline.d.ts.map +1 -0
- package/dist/testing/conformanceTimeline.js +32 -0
- package/dist/testing/conformanceTimeline.js.map +1 -0
- package/dist/testing/conformanceVideoStream.d.ts +29 -0
- package/dist/testing/conformanceVideoStream.d.ts.map +1 -0
- package/dist/testing/conformanceVideoStream.js +39 -0
- package/dist/testing/conformanceVideoStream.js.map +1 -0
- package/dist/testing/index.d.ts +29 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/realtimeVideoConformance.d.ts +23 -0
- package/dist/testing/realtimeVideoConformance.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformance.js +139 -0
- package/dist/testing/realtimeVideoConformance.js.map +1 -0
- package/dist/testing/realtimeVideoConformanceChecks.d.ts +4 -0
- package/dist/testing/realtimeVideoConformanceChecks.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformanceChecks.js +164 -0
- package/dist/testing/realtimeVideoConformanceChecks.js.map +1 -0
- package/dist/testing/realtimeVideoConformanceFrameChecks.d.ts +4 -0
- package/dist/testing/realtimeVideoConformanceFrameChecks.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformanceFrameChecks.js +63 -0
- package/dist/testing/realtimeVideoConformanceFrameChecks.js.map +1 -0
- package/dist/testing/realtimeVideoConformanceSession.d.ts +108 -0
- package/dist/testing/realtimeVideoConformanceSession.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformanceSession.js +270 -0
- package/dist/testing/realtimeVideoConformanceSession.js.map +1 -0
- package/dist/testing/realtimeVideoConformanceTurnChecks.d.ts +4 -0
- package/dist/testing/realtimeVideoConformanceTurnChecks.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformanceTurnChecks.js +248 -0
- package/dist/testing/realtimeVideoConformanceTurnChecks.js.map +1 -0
- package/dist/testing/realtimeVideoConformanceTypes.d.ts +155 -0
- package/dist/testing/realtimeVideoConformanceTypes.d.ts.map +1 -0
- package/dist/testing/realtimeVideoConformanceTypes.js +2 -0
- package/dist/testing/realtimeVideoConformanceTypes.js.map +1 -0
- package/dist/testing/recordingPlayout.d.ts +84 -0
- package/dist/testing/recordingPlayout.d.ts.map +1 -0
- package/dist/testing/recordingPlayout.js +120 -0
- package/dist/testing/recordingPlayout.js.map +1 -0
- package/package.json +29 -4
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview SHARED CHANNEL INBOUND VIDEO BRIDGE (Phase F5).
|
|
3
3
|
*
|
|
4
|
-
* Single shared implementation used by
|
|
5
|
-
*
|
|
4
|
+
* Single shared implementation used by the Whiteboard, Remote Browser and any other channel that
|
|
5
|
+
* shows the model pixels. It no longer writes to the model directly: it REGISTERS its channel as a
|
|
6
|
+
* source with the session's {@link VideoSourceArbiter} and pushes frames to it, so the arbiter stays
|
|
7
|
+
* the single writer of inbound video and decides which source the model sees when several are live.
|
|
6
8
|
*
|
|
7
9
|
* Enforces:
|
|
8
10
|
* 1. Model capability & track gate: if the session has not established an inbound video track,
|
|
9
11
|
* falls back to tool-only behaviour — NO frames sent and NO errors raised.
|
|
10
|
-
* 2. Cadence
|
|
11
|
-
*
|
|
12
|
+
* 2. Cadence: the poller (when used) runs at the NEGOTIATED inbound rate, read from the client;
|
|
13
|
+
* there is no rate constant here. The arbiter and the driver enforce the same rate as a spacing.
|
|
14
|
+
* 3. Clean teardown on Stop(), which also unregisters the source.
|
|
15
|
+
*
|
|
16
|
+
* An owner that sends frames only when its picture changes learns from {@link ChannelInboundVideoBridge.OnSourceActivated}
|
|
17
|
+
* when the arbiter starts sending its source to the model again, so it can send the picture the model missed.
|
|
12
18
|
*
|
|
13
19
|
* @module @memberjunction/ai-realtime-client
|
|
14
20
|
* @author MemberJunction.com
|
|
15
21
|
*/
|
|
22
|
+
import { NominalVideoFrameIntervalMs } from './videoPacing.js';
|
|
23
|
+
import { VideoSourceArbiter } from './videoSourceArbiter.js';
|
|
24
|
+
/** Counter behind the generated source ids. */
|
|
25
|
+
let generatedSourceCounter = 0;
|
|
16
26
|
/**
|
|
17
|
-
* Shared bridge connecting a visual channel surface to the session's inbound video track
|
|
27
|
+
* Shared bridge connecting a visual channel surface to the session's inbound video track, by way of
|
|
28
|
+
* the session's {@link VideoSourceArbiter}.
|
|
18
29
|
*/
|
|
19
30
|
export class ChannelInboundVideoBridge {
|
|
31
|
+
/**
|
|
32
|
+
* @param clientOrGetter The session's client, or a getter for it (a reconnect replaces the client). Any
|
|
33
|
+
* {@link IVideoFrameSink} works; a `BaseRealtimeClient` is one.
|
|
34
|
+
* @param provider Produces the frames.
|
|
35
|
+
* @param options Source identity, MIME type and the deprecated rate cap.
|
|
36
|
+
*/
|
|
20
37
|
constructor(clientOrGetter, provider, options = {}) {
|
|
21
38
|
this.clientOrGetter = clientOrGetter;
|
|
22
39
|
this.provider = provider;
|
|
@@ -24,10 +41,131 @@ export class ChannelInboundVideoBridge {
|
|
|
24
41
|
this.timer = null;
|
|
25
42
|
this.active = false;
|
|
26
43
|
this.frameErrorLogged = false;
|
|
44
|
+
/** The arbiter this bridge is currently registered with; `null` when it is not registered. */
|
|
45
|
+
this.registeredWith = null;
|
|
46
|
+
/** Whether the owner switched this source off (exposure policy or the user). Survives re-registration. */
|
|
47
|
+
this.sourceEnabled = true;
|
|
48
|
+
/** The owner's handlers for {@link OnSourceActivated}. They survive re-registration. */
|
|
49
|
+
this.activatedHandlers = new Set();
|
|
50
|
+
/** Stops following the arbiter's changes; `null` while not following (not registered, or no handler). */
|
|
51
|
+
this.stopFollowingArbiter = null;
|
|
52
|
+
/** Whether the arbiter was sending this source to the model as of its last change. */
|
|
53
|
+
this.forwarded = false;
|
|
54
|
+
this.sourceId = options.SourceID ?? `channel-video-${++generatedSourceCounter}`;
|
|
27
55
|
}
|
|
28
56
|
get client() {
|
|
29
57
|
return typeof this.clientOrGetter === 'function' ? this.clientOrGetter() : this.clientOrGetter;
|
|
30
58
|
}
|
|
59
|
+
/** The id this bridge's source is registered under. */
|
|
60
|
+
get SourceID() {
|
|
61
|
+
return this.sourceId;
|
|
62
|
+
}
|
|
63
|
+
/** The arbiter for the current client, or `null` when there is no client yet. */
|
|
64
|
+
resolveArbiter() {
|
|
65
|
+
if (this.options.Arbiter) {
|
|
66
|
+
return this.options.Arbiter;
|
|
67
|
+
}
|
|
68
|
+
const client = this.client;
|
|
69
|
+
return client ? VideoSourceArbiter.ForSink(client) : null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Registers this bridge's source with the session's arbiter (idempotent). A new session means a new
|
|
73
|
+
* client and so a new arbiter: the source is re-registered there and removed from the old one, so a
|
|
74
|
+
* reconnect never leaves a ghost source behind.
|
|
75
|
+
*
|
|
76
|
+
* @returns The arbiter, or `null` when there is no client to register with.
|
|
77
|
+
*/
|
|
78
|
+
ensureRegistered() {
|
|
79
|
+
const arbiter = this.resolveArbiter();
|
|
80
|
+
if (!arbiter) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (this.registeredWith !== arbiter) {
|
|
84
|
+
this.unfollowArbiter();
|
|
85
|
+
this.registeredWith?.UnregisterSource(this.sourceId);
|
|
86
|
+
arbiter.RegisterSource({
|
|
87
|
+
SourceID: this.sourceId,
|
|
88
|
+
Label: this.options.Label ?? this.sourceId,
|
|
89
|
+
Kind: this.options.Kind ?? 'surface',
|
|
90
|
+
ChannelKey: this.options.ChannelKey,
|
|
91
|
+
});
|
|
92
|
+
this.registeredWith = arbiter;
|
|
93
|
+
if (!this.sourceEnabled) {
|
|
94
|
+
arbiter.SetSourceEnabled(this.sourceId, false, false);
|
|
95
|
+
}
|
|
96
|
+
// Followed from here, once registered as it will stay: the registration itself is not an activation.
|
|
97
|
+
this.followArbiter();
|
|
98
|
+
}
|
|
99
|
+
return arbiter;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Calls `handler` each time the arbiter starts sending this source to the model after it was not: another source
|
|
103
|
+
* that held the model's only stream stopped or was set aside, or this source was switched back on. The source's
|
|
104
|
+
* first registration does not count. A frame offered while the source was not sent never reached the model, so an
|
|
105
|
+
* owner that sends frames only when its picture changes sends its current picture here.
|
|
106
|
+
*
|
|
107
|
+
* The handler is kept across a re-registration (a reconnect's new client) and called until removed.
|
|
108
|
+
*
|
|
109
|
+
* @param handler Called synchronously from the arbiter's change event; it should not throw.
|
|
110
|
+
* @returns A function that removes the handler.
|
|
111
|
+
*/
|
|
112
|
+
OnSourceActivated(handler) {
|
|
113
|
+
this.activatedHandlers.add(handler);
|
|
114
|
+
this.followArbiter();
|
|
115
|
+
return () => {
|
|
116
|
+
this.activatedHandlers.delete(handler);
|
|
117
|
+
if (this.activatedHandlers.size === 0) {
|
|
118
|
+
this.unfollowArbiter();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Starts following the arbiter this source is registered with, when an owner asked to hear of activations. */
|
|
123
|
+
followArbiter() {
|
|
124
|
+
const arbiter = this.registeredWith;
|
|
125
|
+
if (!arbiter || this.stopFollowingArbiter || this.activatedHandlers.size === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.forwarded = arbiter.GetActiveSourceIDs().includes(this.sourceId);
|
|
129
|
+
this.stopFollowingArbiter = arbiter.OnChange(() => {
|
|
130
|
+
const forwarded = arbiter.GetActiveSourceIDs().includes(this.sourceId);
|
|
131
|
+
const activated = forwarded && !this.forwarded;
|
|
132
|
+
this.forwarded = forwarded;
|
|
133
|
+
if (activated) {
|
|
134
|
+
this.notifyActivated();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Stops following the arbiter's changes. */
|
|
139
|
+
unfollowArbiter() {
|
|
140
|
+
this.stopFollowingArbiter?.();
|
|
141
|
+
this.stopFollowingArbiter = null;
|
|
142
|
+
this.forwarded = false;
|
|
143
|
+
}
|
|
144
|
+
/** Calls each activation handler, containing a handler that throws. */
|
|
145
|
+
notifyActivated() {
|
|
146
|
+
for (const handler of [...this.activatedHandlers]) {
|
|
147
|
+
try {
|
|
148
|
+
handler();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error('[ChannelInboundVideoBridge] Error in a source-activated handler:', err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Registers this bridge's source with the session's arbiter now, without starting any poll — so the
|
|
157
|
+
* source is LISTED (and can be switched on or off) before it has pushed a frame. A source that is
|
|
158
|
+
* switched off never pushes one, so without this it could never be switched back on.
|
|
159
|
+
*
|
|
160
|
+
* @returns `true` if registered; `false` when there is no client or no inbound video track (nothing to register with).
|
|
161
|
+
*/
|
|
162
|
+
Register() {
|
|
163
|
+
const client = this.client;
|
|
164
|
+
if (!client || !client.IsTrackEstablished('video', 'inbound')) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return this.ensureRegistered() !== null;
|
|
168
|
+
}
|
|
31
169
|
/**
|
|
32
170
|
* Starts the periodic frame pump if the session has an established inbound video track.
|
|
33
171
|
*
|
|
@@ -38,43 +176,66 @@ export class ChannelInboundVideoBridge {
|
|
|
38
176
|
if (this.active) {
|
|
39
177
|
return true;
|
|
40
178
|
}
|
|
41
|
-
|
|
179
|
+
const client = this.client;
|
|
180
|
+
if (!client || !client.IsTrackEstablished('video', 'inbound')) {
|
|
42
181
|
// Non-video model or unrequested video track: graceful fallback with no error
|
|
43
182
|
return false;
|
|
44
183
|
}
|
|
184
|
+
this.ensureRegistered();
|
|
45
185
|
this.active = true;
|
|
46
|
-
const rate = Math.min(Math.max(this.options.Rate ?? 1, 0.1), 1);
|
|
47
|
-
const intervalMs = Math.floor(1000 / rate);
|
|
48
186
|
this.timer = setInterval(() => {
|
|
49
187
|
void this.pumpFrame();
|
|
50
|
-
},
|
|
188
|
+
}, this.pollIntervalMs(client));
|
|
51
189
|
return true;
|
|
52
190
|
}
|
|
191
|
+
/** The poll interval: the negotiated rate, capped by the (deprecated) `Rate` option when one was given. */
|
|
192
|
+
pollIntervalMs(client) {
|
|
193
|
+
const negotiated = client.InboundVideoRate;
|
|
194
|
+
const requested = this.options.Rate;
|
|
195
|
+
const rate = typeof requested === 'number' && requested > 0
|
|
196
|
+
? Math.max(0.1, typeof negotiated === 'number' ? Math.min(requested, negotiated) : requested)
|
|
197
|
+
: negotiated;
|
|
198
|
+
return NominalVideoFrameIntervalMs(rate);
|
|
199
|
+
}
|
|
53
200
|
/**
|
|
54
|
-
*
|
|
55
|
-
* and the
|
|
201
|
+
* Offers a frame to the arbiter, which forwards it only if this source is among those the model
|
|
202
|
+
* should see (and enabled, and inside the negotiated pacing interval).
|
|
56
203
|
*
|
|
57
|
-
* @returns `true` if dispatched to the
|
|
204
|
+
* @returns `true` if dispatched to the model; `false` otherwise.
|
|
58
205
|
*/
|
|
59
206
|
sendFrameDirect(base64Jpeg) {
|
|
60
|
-
|
|
207
|
+
const client = this.client;
|
|
208
|
+
if (!client || !client.IsTrackEstablished('video', 'inbound')) {
|
|
61
209
|
return false;
|
|
62
210
|
}
|
|
63
|
-
if (typeof
|
|
211
|
+
if (typeof client.SendVideoFrame !== 'function') {
|
|
64
212
|
return false;
|
|
65
213
|
}
|
|
66
|
-
|
|
214
|
+
const arbiter = this.ensureRegistered();
|
|
215
|
+
return arbiter ? arbiter.PushFrame(this.sourceId, base64Jpeg, this.options.MimeType ?? 'image/jpeg') : false;
|
|
67
216
|
}
|
|
68
217
|
/**
|
|
69
218
|
* Pushes an event-driven frame (e.g. screencast push from RemoteBrowser or stroke completion).
|
|
70
|
-
*
|
|
219
|
+
* Registers the source on first use, so a purely event-driven channel never needs {@link Start}.
|
|
71
220
|
*
|
|
72
221
|
* @param base64Jpeg The base64-encoded image frame.
|
|
73
|
-
* @returns `true` if the frame was sent; `false` if dropped (throttled,
|
|
222
|
+
* @returns `true` if the frame was sent; `false` if dropped (throttled, not the source the model is
|
|
223
|
+
* viewing, turned off, track unestablished, or driver lacks video support).
|
|
74
224
|
*/
|
|
75
225
|
PushFrame(base64Jpeg) {
|
|
76
226
|
return this.sendFrameDirect(base64Jpeg);
|
|
77
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Turns this source on or off at the arbiter (a no-op before the first registration other than
|
|
230
|
+
* remembering the choice). A disabled source is not a candidate and forwards nothing.
|
|
231
|
+
*
|
|
232
|
+
* @param enabled Whether the model may see this source.
|
|
233
|
+
* @param notify Whether the arbiter tells the model. Pass `false` when the caller sends its own note.
|
|
234
|
+
*/
|
|
235
|
+
SetSourceEnabled(enabled, notify = true) {
|
|
236
|
+
this.sourceEnabled = enabled;
|
|
237
|
+
this.registeredWith?.SetSourceEnabled(this.sourceId, enabled, notify);
|
|
238
|
+
}
|
|
78
239
|
/**
|
|
79
240
|
* Periodic poll tick that fetches the latest frame from the provider and pushes it.
|
|
80
241
|
* Uses `sendFrameDirect` so periodic polling paced by `setInterval` does not fight
|
|
@@ -99,7 +260,8 @@ export class ChannelInboundVideoBridge {
|
|
|
99
260
|
}
|
|
100
261
|
}
|
|
101
262
|
/**
|
|
102
|
-
* Stops the bridge and
|
|
263
|
+
* Stops the bridge: clears the poll timer and removes this source from the arbiter. Activation handlers stay, and
|
|
264
|
+
* are called again if the source registers again.
|
|
103
265
|
*/
|
|
104
266
|
Stop() {
|
|
105
267
|
this.active = false;
|
|
@@ -108,6 +270,9 @@ export class ChannelInboundVideoBridge {
|
|
|
108
270
|
clearInterval(this.timer);
|
|
109
271
|
this.timer = null;
|
|
110
272
|
}
|
|
273
|
+
this.unfollowArbiter();
|
|
274
|
+
this.registeredWith?.UnregisterSource(this.sourceId);
|
|
275
|
+
this.registeredWith = null;
|
|
111
276
|
}
|
|
112
277
|
/** Whether the video bridge is currently actively pumping frames. */
|
|
113
278
|
get IsActive() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channelVideoSource.js","sourceRoot":"","sources":["../../src/media/channelVideoSource.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"channelVideoSource.js","sourceRoot":"","sources":["../../src/media/channelVideoSource.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FRAME SCHEDULER: shows decoded pictures on a {@link StreamCanvas} when their presentation time comes,
|
|
3
|
+
* for the decoders that aren't MSE (WebCodecs chunks, images). MSE paces itself on the element's clock.
|
|
4
|
+
*
|
|
5
|
+
* - **On the voice's clock, when the player has one** ({@link FrameSchedulerOptions.Clock}): the meeting bot's rules, on
|
|
6
|
+
* media time. A picture shows when the clock reaches its time; one more than {@link MAX_CLOCK_LATE_MS} behind the clock
|
|
7
|
+
* is dropped, never shown; one ahead of it waits, and one more than {@link MAX_LEAD_MS} ahead is dropped (it belongs to
|
|
8
|
+
* a stream the voice has left). A picture timed before the last one shown starts a new stream: it waits while the clock
|
|
9
|
+
* still plays the old stream, instead of counting as late.
|
|
10
|
+
* - **When the clock reads nothing.** After a reading, pictures carry on by their own times from the clock's last reading,
|
|
11
|
+
* as if the voice went on (the tail of a turn whose video outlasts its voice, or idle video between turns); a new stream
|
|
12
|
+
* then waits for its voice. Before any reading (at the start, or after a barge-in) pictures wait up to
|
|
13
|
+
* {@link CLOCK_WAIT_MS} for the voice to start, then go by their own times.
|
|
14
|
+
* - **By their own times, without a clock.** The first timed picture after a start, a flush or a new stream anchors media
|
|
15
|
+
* time to the wall clock; each later picture shows when its time comes. When several are due, the newest shows and the
|
|
16
|
+
* older ones are dropped. A picture shown more than {@link MAX_LATE_MS} late anchors the pacing again; so does a
|
|
17
|
+
* picture timed before the last one shown, or more than {@link MAX_LEAD_MS} ahead.
|
|
18
|
+
* - **Untimed pictures** (an image that carries no time) show as soon as they arrive.
|
|
19
|
+
* - **Bounded.** The decoders hold encoded frames back while {@link FrameScheduler.Size} pictures wait, and the scheduler
|
|
20
|
+
* calls back each time it shows one, so a decoder never holds many decoded pictures (a browser decoder stalls when its
|
|
21
|
+
* output frames aren't closed).
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/ai-realtime-client/media
|
|
24
|
+
*/
|
|
25
|
+
import type { IPlaybackClock } from '../playbackClock.js';
|
|
26
|
+
import type { StreamCanvas, StreamPicture } from './streamCanvas.js';
|
|
27
|
+
/** Without a clock, a picture shown this much later than its time anchors the pacing again, in milliseconds. */
|
|
28
|
+
export declare const MAX_LATE_MS = 250;
|
|
29
|
+
/** A picture due this much ahead starts a new stream (without a clock) or is dropped (on the clock), in milliseconds. */
|
|
30
|
+
export declare const MAX_LEAD_MS = 2000;
|
|
31
|
+
/** On the voice's clock, a picture this far behind it is dropped rather than shown, in milliseconds. */
|
|
32
|
+
export declare const MAX_CLOCK_LATE_MS = 500;
|
|
33
|
+
/** How long pictures wait for a clock that has read nothing yet before they go by their own times, in milliseconds. */
|
|
34
|
+
export declare const CLOCK_WAIT_MS = 500;
|
|
35
|
+
/** How often waiting pictures look at the clock again, at most, in milliseconds. */
|
|
36
|
+
export declare const CLOCK_POLL_MS = 20;
|
|
37
|
+
/** What a scheduler paces by, besides the pictures' own times. */
|
|
38
|
+
export interface FrameSchedulerOptions {
|
|
39
|
+
/** The voice's playback clock: pictures show when it reaches their times. */
|
|
40
|
+
Clock?: IPlaybackClock;
|
|
41
|
+
/** The wall clock in milliseconds; injectable for tests. */
|
|
42
|
+
Now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
/** Shows pictures on a stream canvas at their presentation times: on the voice's clock when there is one. */
|
|
45
|
+
export declare class FrameScheduler {
|
|
46
|
+
private readonly canvas;
|
|
47
|
+
private readonly onShown;
|
|
48
|
+
private readonly queue;
|
|
49
|
+
private readonly clock;
|
|
50
|
+
private readonly now;
|
|
51
|
+
/** Own-time pacing: where the current stream's media time sits on the wall clock. */
|
|
52
|
+
private anchor;
|
|
53
|
+
/** The time of the last timed picture shown, to see a stream restart. */
|
|
54
|
+
private lastShownMs;
|
|
55
|
+
/** The clock's last reading and when it was taken, until the clock stops reading. */
|
|
56
|
+
private lastReading;
|
|
57
|
+
/** Whether own-time pacing carries on from the clock's last reading (the voice stopped). */
|
|
58
|
+
private continuing;
|
|
59
|
+
/** When the pictures began waiting for the voice's clock to read. */
|
|
60
|
+
private waitingSince;
|
|
61
|
+
private timer;
|
|
62
|
+
private disposed;
|
|
63
|
+
/**
|
|
64
|
+
* @param canvas Where pictures show.
|
|
65
|
+
* @param onShown Called after each picture shown, so the decoder can decode more.
|
|
66
|
+
* @param options The voice's clock, and a wall clock for tests.
|
|
67
|
+
*/
|
|
68
|
+
constructor(canvas: StreamCanvas, onShown: () => void, options?: FrameSchedulerOptions);
|
|
69
|
+
/** How many pictures wait to be shown. */
|
|
70
|
+
get Size(): number;
|
|
71
|
+
/**
|
|
72
|
+
* Queues a picture: it shows when its time comes, or at once when it has no time.
|
|
73
|
+
*
|
|
74
|
+
* @param picture The decoded picture; the scheduler closes it, shown or dropped.
|
|
75
|
+
* @param timeMs Its presentation time on the stream's timeline, when it has one.
|
|
76
|
+
*/
|
|
77
|
+
Push(picture: StreamPicture, timeMs?: number): void;
|
|
78
|
+
/** Drops every waiting picture and forgets both clocks: the pacing starts over. The last picture shown stays. */
|
|
79
|
+
Clear(): void;
|
|
80
|
+
/** Clears, and closes every picture pushed later. */
|
|
81
|
+
Dispose(): void;
|
|
82
|
+
/** Shows the picture that is due now, if any, by the voice's clock or by the pictures' own times; then waits for the next. */
|
|
83
|
+
private tick;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the pictures wait for the voice's clock, which reads nothing now. A clock that read before stopped: pacing
|
|
86
|
+
* carries on from its last reading, but a picture that starts a new stream waits for its voice. Pictures wait up to
|
|
87
|
+
* {@link CLOCK_WAIT_MS}, then go by their own times. Never without a clock.
|
|
88
|
+
*/
|
|
89
|
+
private holdForClock;
|
|
90
|
+
/** Whether the next picture is timed before the last one shown: it starts a new stream. */
|
|
91
|
+
private headStartsNewStream;
|
|
92
|
+
/** Shows the newest picture the clock has reached, dropping the stale ones before it ({@link clockVerdict}). */
|
|
93
|
+
private showByClock;
|
|
94
|
+
/**
|
|
95
|
+
* What the clock says about a picture: it waits when it starts a new stream whose voice hasn't started, or is ahead of
|
|
96
|
+
* the clock; it is dropped when more than {@link MAX_CLOCK_LATE_MS} behind the clock or more than {@link MAX_LEAD_MS}
|
|
97
|
+
* ahead of it; otherwise it is due. An untimed picture is due at once.
|
|
98
|
+
*/
|
|
99
|
+
private clockVerdict;
|
|
100
|
+
/**
|
|
101
|
+
* Whether a picture starts a new stream whose voice hasn't started: it is timed before the last picture shown, and the
|
|
102
|
+
* clock still reads the old stream (at or after that picture).
|
|
103
|
+
*/
|
|
104
|
+
private awaitsNewStream;
|
|
105
|
+
/** Shows the newest picture whose own time has come (dropping older due ones). */
|
|
106
|
+
private showByOwnTime;
|
|
107
|
+
/** When a picture is due on the wall clock. A first picture, or one that starts a new stream, anchors the pacing to now. */
|
|
108
|
+
private dueAt;
|
|
109
|
+
/** A picture timed before the last one shown, or far ahead of the pacing, belongs to a new stream. */
|
|
110
|
+
private startsNewStream;
|
|
111
|
+
private show;
|
|
112
|
+
/** By own time: until the next picture is due. */
|
|
113
|
+
private ownTimeDelay;
|
|
114
|
+
/** On the clock: until the clock should reach the next picture, but never longer than {@link CLOCK_POLL_MS}. */
|
|
115
|
+
private clockDelay;
|
|
116
|
+
private schedule;
|
|
117
|
+
private cancelTimer;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=frameScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frameScheduler.d.ts","sourceRoot":"","sources":["../../../src/media/decoders/frameScheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAElE,gHAAgH;AAChH,eAAO,MAAM,WAAW,MAAM,CAAC;AAE/B,yHAAyH;AACzH,eAAO,MAAM,WAAW,OAAO,CAAC;AAEhC,wGAAwG;AACxG,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,uHAAuH;AACvH,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,oFAAoF;AACpF,eAAO,MAAM,aAAa,KAAK,CAAC;AAchC,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IAClC,6EAA6E;IAC7E,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,6GAA6G;AAC7G,qBAAa,cAAc;IAuBnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAvB5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAA4B;IAC1C,yEAAyE;IACzE,OAAO,CAAC,WAAW,CAAuB;IAC1C,qFAAqF;IACrF,OAAO,CAAC,WAAW,CAA4B;IAC/C,4FAA4F;IAC5F,OAAO,CAAC,UAAU,CAAS;IAC3B,qEAAqE;IACrE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;OAIG;gBAEkB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,IAAI,EACpC,OAAO,GAAE,qBAA0B;IAMvC,0CAA0C;IAC1C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED;;;;;OAKG;IACI,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAW1D,iHAAiH;IAC1G,KAAK,IAAI,IAAI;IAYpB,qDAAqD;IAC9C,OAAO,IAAI,IAAI;IAKtB,8HAA8H;IAC9H,OAAO,CAAC,IAAI;IAsBZ;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAqBpB,2FAA2F;IAC3F,OAAO,CAAC,mBAAmB;IAK3B,gHAAgH;IAChH,OAAO,CAAC,WAAW;IAqBnB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAIvB,kFAAkF;IAClF,OAAO,CAAC,aAAa;IAqBrB,4HAA4H;IAC5H,OAAO,CAAC,KAAK;IAWb,sGAAsG;IACtG,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,IAAI;IAOZ,kDAAkD;IAClD,OAAO,CAAC,YAAY;IAIpB,gHAAgH;IAChH,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,WAAW;CAMtB"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/** Without a clock, a picture shown this much later than its time anchors the pacing again, in milliseconds. */
|
|
2
|
+
export const MAX_LATE_MS = 250;
|
|
3
|
+
/** A picture due this much ahead starts a new stream (without a clock) or is dropped (on the clock), in milliseconds. */
|
|
4
|
+
export const MAX_LEAD_MS = 2000;
|
|
5
|
+
/** On the voice's clock, a picture this far behind it is dropped rather than shown, in milliseconds. */
|
|
6
|
+
export const MAX_CLOCK_LATE_MS = 500;
|
|
7
|
+
/** How long pictures wait for a clock that has read nothing yet before they go by their own times, in milliseconds. */
|
|
8
|
+
export const CLOCK_WAIT_MS = 500;
|
|
9
|
+
/** How often waiting pictures look at the clock again, at most, in milliseconds. */
|
|
10
|
+
export const CLOCK_POLL_MS = 20;
|
|
11
|
+
/** Shows pictures on a stream canvas at their presentation times: on the voice's clock when there is one. */
|
|
12
|
+
export class FrameScheduler {
|
|
13
|
+
/**
|
|
14
|
+
* @param canvas Where pictures show.
|
|
15
|
+
* @param onShown Called after each picture shown, so the decoder can decode more.
|
|
16
|
+
* @param options The voice's clock, and a wall clock for tests.
|
|
17
|
+
*/
|
|
18
|
+
constructor(canvas, onShown, options = {}) {
|
|
19
|
+
this.canvas = canvas;
|
|
20
|
+
this.onShown = onShown;
|
|
21
|
+
this.queue = [];
|
|
22
|
+
/** Own-time pacing: where the current stream's media time sits on the wall clock. */
|
|
23
|
+
this.anchor = null;
|
|
24
|
+
/** The time of the last timed picture shown, to see a stream restart. */
|
|
25
|
+
this.lastShownMs = null;
|
|
26
|
+
/** The clock's last reading and when it was taken, until the clock stops reading. */
|
|
27
|
+
this.lastReading = null;
|
|
28
|
+
/** Whether own-time pacing carries on from the clock's last reading (the voice stopped). */
|
|
29
|
+
this.continuing = false;
|
|
30
|
+
/** When the pictures began waiting for the voice's clock to read. */
|
|
31
|
+
this.waitingSince = null;
|
|
32
|
+
this.timer = null;
|
|
33
|
+
this.disposed = false;
|
|
34
|
+
this.clock = options.Clock;
|
|
35
|
+
this.now = options.Now ?? (() => performance.now());
|
|
36
|
+
}
|
|
37
|
+
/** How many pictures wait to be shown. */
|
|
38
|
+
get Size() {
|
|
39
|
+
return this.queue.length;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Queues a picture: it shows when its time comes, or at once when it has no time.
|
|
43
|
+
*
|
|
44
|
+
* @param picture The decoded picture; the scheduler closes it, shown or dropped.
|
|
45
|
+
* @param timeMs Its presentation time on the stream's timeline, when it has one.
|
|
46
|
+
*/
|
|
47
|
+
Push(picture, timeMs) {
|
|
48
|
+
if (this.disposed) {
|
|
49
|
+
picture.close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.queue.push({ Picture: picture, TimeMs: timeMs });
|
|
53
|
+
if (this.timer === null) {
|
|
54
|
+
this.tick();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Drops every waiting picture and forgets both clocks: the pacing starts over. The last picture shown stays. */
|
|
58
|
+
Clear() {
|
|
59
|
+
this.cancelTimer();
|
|
60
|
+
for (const scheduled of this.queue.splice(0)) {
|
|
61
|
+
scheduled.Picture.close();
|
|
62
|
+
}
|
|
63
|
+
this.anchor = null;
|
|
64
|
+
this.lastShownMs = null;
|
|
65
|
+
this.lastReading = null;
|
|
66
|
+
this.continuing = false;
|
|
67
|
+
this.waitingSince = null;
|
|
68
|
+
}
|
|
69
|
+
/** Clears, and closes every picture pushed later. */
|
|
70
|
+
Dispose() {
|
|
71
|
+
this.disposed = true;
|
|
72
|
+
this.Clear();
|
|
73
|
+
}
|
|
74
|
+
/** Shows the picture that is due now, if any, by the voice's clock or by the pictures' own times; then waits for the next. */
|
|
75
|
+
tick() {
|
|
76
|
+
this.timer = null;
|
|
77
|
+
const now = this.now();
|
|
78
|
+
const clockMs = this.clock?.CurrentTimeMs ?? null;
|
|
79
|
+
let shown = false;
|
|
80
|
+
if (clockMs !== null) {
|
|
81
|
+
this.lastReading = { WallMs: now, MediaMs: clockMs };
|
|
82
|
+
shown = this.showByClock(clockMs);
|
|
83
|
+
}
|
|
84
|
+
else if (this.holdForClock(now)) {
|
|
85
|
+
this.schedule(CLOCK_POLL_MS);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
shown = this.showByOwnTime(now);
|
|
90
|
+
}
|
|
91
|
+
if (this.queue.length > 0) {
|
|
92
|
+
this.schedule(clockMs === null ? this.ownTimeDelay(now) : this.clockDelay(clockMs));
|
|
93
|
+
}
|
|
94
|
+
if (shown) {
|
|
95
|
+
this.onShown();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether the pictures wait for the voice's clock, which reads nothing now. A clock that read before stopped: pacing
|
|
100
|
+
* carries on from its last reading, but a picture that starts a new stream waits for its voice. Pictures wait up to
|
|
101
|
+
* {@link CLOCK_WAIT_MS}, then go by their own times. Never without a clock.
|
|
102
|
+
*/
|
|
103
|
+
holdForClock(now) {
|
|
104
|
+
if (!this.clock) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (this.lastReading) {
|
|
108
|
+
this.anchor = this.lastReading;
|
|
109
|
+
this.lastReading = null;
|
|
110
|
+
this.continuing = true;
|
|
111
|
+
}
|
|
112
|
+
if (this.continuing && this.headStartsNewStream()) {
|
|
113
|
+
this.anchor = null;
|
|
114
|
+
this.continuing = false;
|
|
115
|
+
this.waitingSince = now;
|
|
116
|
+
}
|
|
117
|
+
if (this.anchor) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
this.waitingSince ??= now;
|
|
121
|
+
return now - this.waitingSince < CLOCK_WAIT_MS;
|
|
122
|
+
}
|
|
123
|
+
/** Whether the next picture is timed before the last one shown: it starts a new stream. */
|
|
124
|
+
headStartsNewStream() {
|
|
125
|
+
const time = this.queue[0]?.TimeMs;
|
|
126
|
+
return time !== undefined && this.lastShownMs !== null && time < this.lastShownMs;
|
|
127
|
+
}
|
|
128
|
+
/** Shows the newest picture the clock has reached, dropping the stale ones before it ({@link clockVerdict}). */
|
|
129
|
+
showByClock(clockMs) {
|
|
130
|
+
let due = null;
|
|
131
|
+
while (this.queue.length > 0) {
|
|
132
|
+
const verdict = this.clockVerdict(this.queue[0].TimeMs, clockMs);
|
|
133
|
+
if (verdict === 'wait') {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const head = this.queue.shift();
|
|
137
|
+
if (verdict === 'drop') {
|
|
138
|
+
head.Picture.close();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
due?.Picture.close();
|
|
142
|
+
due = head;
|
|
143
|
+
}
|
|
144
|
+
if (due) {
|
|
145
|
+
this.show(due);
|
|
146
|
+
}
|
|
147
|
+
return due !== null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What the clock says about a picture: it waits when it starts a new stream whose voice hasn't started, or is ahead of
|
|
151
|
+
* the clock; it is dropped when more than {@link MAX_CLOCK_LATE_MS} behind the clock or more than {@link MAX_LEAD_MS}
|
|
152
|
+
* ahead of it; otherwise it is due. An untimed picture is due at once.
|
|
153
|
+
*/
|
|
154
|
+
clockVerdict(timeMs, clockMs) {
|
|
155
|
+
if (timeMs === undefined) {
|
|
156
|
+
return 'show';
|
|
157
|
+
}
|
|
158
|
+
if (this.awaitsNewStream(timeMs, clockMs)) {
|
|
159
|
+
return 'wait';
|
|
160
|
+
}
|
|
161
|
+
if (clockMs - timeMs > MAX_CLOCK_LATE_MS || timeMs - clockMs > MAX_LEAD_MS) {
|
|
162
|
+
return 'drop';
|
|
163
|
+
}
|
|
164
|
+
return timeMs > clockMs ? 'wait' : 'show';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether a picture starts a new stream whose voice hasn't started: it is timed before the last picture shown, and the
|
|
168
|
+
* clock still reads the old stream (at or after that picture).
|
|
169
|
+
*/
|
|
170
|
+
awaitsNewStream(timeMs, clockMs) {
|
|
171
|
+
return this.lastShownMs !== null && timeMs < this.lastShownMs && clockMs >= this.lastShownMs;
|
|
172
|
+
}
|
|
173
|
+
/** Shows the newest picture whose own time has come (dropping older due ones). */
|
|
174
|
+
showByOwnTime(now) {
|
|
175
|
+
let due = null;
|
|
176
|
+
while (this.queue.length > 0) {
|
|
177
|
+
const dueMs = this.dueAt(this.queue[0], now);
|
|
178
|
+
if (dueMs > now) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
due?.Scheduled.Picture.close();
|
|
182
|
+
due = { Scheduled: this.queue.shift(), DueMs: dueMs };
|
|
183
|
+
}
|
|
184
|
+
if (!due) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
this.show(due.Scheduled);
|
|
188
|
+
const time = due.Scheduled.TimeMs;
|
|
189
|
+
if (time !== undefined && now - due.DueMs > MAX_LATE_MS) {
|
|
190
|
+
this.anchor = { WallMs: now, MediaMs: time };
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/** When a picture is due on the wall clock. A first picture, or one that starts a new stream, anchors the pacing to now. */
|
|
195
|
+
dueAt(scheduled, now) {
|
|
196
|
+
const time = scheduled.TimeMs;
|
|
197
|
+
if (time === undefined) {
|
|
198
|
+
return now;
|
|
199
|
+
}
|
|
200
|
+
if (!this.anchor || this.startsNewStream(time, now)) {
|
|
201
|
+
this.anchor = { WallMs: now, MediaMs: time };
|
|
202
|
+
}
|
|
203
|
+
return this.anchor.WallMs + (time - this.anchor.MediaMs);
|
|
204
|
+
}
|
|
205
|
+
/** A picture timed before the last one shown, or far ahead of the pacing, belongs to a new stream. */
|
|
206
|
+
startsNewStream(timeMs, now) {
|
|
207
|
+
const anchor = this.anchor;
|
|
208
|
+
const backwards = this.lastShownMs !== null && timeMs < this.lastShownMs;
|
|
209
|
+
return backwards || anchor.WallMs + (timeMs - anchor.MediaMs) - now > MAX_LEAD_MS;
|
|
210
|
+
}
|
|
211
|
+
show(scheduled) {
|
|
212
|
+
this.canvas.Show(scheduled.Picture);
|
|
213
|
+
if (scheduled.TimeMs !== undefined) {
|
|
214
|
+
this.lastShownMs = scheduled.TimeMs;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** By own time: until the next picture is due. */
|
|
218
|
+
ownTimeDelay(now) {
|
|
219
|
+
return Math.max(0, this.dueAt(this.queue[0], now) - now);
|
|
220
|
+
}
|
|
221
|
+
/** On the clock: until the clock should reach the next picture, but never longer than {@link CLOCK_POLL_MS}. */
|
|
222
|
+
clockDelay(clockMs) {
|
|
223
|
+
const time = this.queue[0].TimeMs;
|
|
224
|
+
return time !== undefined && time > clockMs ? Math.min(CLOCK_POLL_MS, time - clockMs) : CLOCK_POLL_MS;
|
|
225
|
+
}
|
|
226
|
+
schedule(delayMs) {
|
|
227
|
+
if (this.timer === null && !this.disposed) {
|
|
228
|
+
this.timer = setTimeout(() => this.tick(), delayMs);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
cancelTimer() {
|
|
232
|
+
if (this.timer !== null) {
|
|
233
|
+
clearTimeout(this.timer);
|
|
234
|
+
this.timer = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=frameScheduler.js.map
|