@memberjunction/ai-realtime-client 6.2.0-edge.3 → 6.2.0-edge.5

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.
Files changed (209) hide show
  1. package/README.md +156 -29
  2. package/dist/audio/audioMeter.d.ts +71 -0
  3. package/dist/audio/audioMeter.d.ts.map +1 -1
  4. package/dist/audio/audioMeter.js +83 -0
  5. package/dist/audio/audioMeter.js.map +1 -1
  6. package/dist/audio/mediaElementAudioRouter.d.ts +41 -0
  7. package/dist/audio/mediaElementAudioRouter.d.ts.map +1 -0
  8. package/dist/audio/mediaElementAudioRouter.js +82 -0
  9. package/dist/audio/mediaElementAudioRouter.js.map +1 -0
  10. package/dist/audio/micCapture.d.ts +12 -3
  11. package/dist/audio/micCapture.d.ts.map +1 -1
  12. package/dist/audio/micCapture.js +15 -1
  13. package/dist/audio/micCapture.js.map +1 -1
  14. package/dist/audio/pcmPlayback.d.ts +72 -4
  15. package/dist/audio/pcmPlayback.d.ts.map +1 -1
  16. package/dist/audio/pcmPlayback.js +101 -1
  17. package/dist/audio/pcmPlayback.js.map +1 -1
  18. package/dist/drivers/assemblyAIRealtimeClient.d.ts +2 -0
  19. package/dist/drivers/assemblyAIRealtimeClient.d.ts.map +1 -1
  20. package/dist/drivers/assemblyAIRealtimeClient.js +14 -0
  21. package/dist/drivers/assemblyAIRealtimeClient.js.map +1 -1
  22. package/dist/drivers/elevenLabsRealtimeClient.d.ts +2 -0
  23. package/dist/drivers/elevenLabsRealtimeClient.d.ts.map +1 -1
  24. package/dist/drivers/elevenLabsRealtimeClient.js +14 -0
  25. package/dist/drivers/elevenLabsRealtimeClient.js.map +1 -1
  26. package/dist/drivers/geminiAvatarOutput.d.ts +132 -0
  27. package/dist/drivers/geminiAvatarOutput.d.ts.map +1 -0
  28. package/dist/drivers/geminiAvatarOutput.js +310 -0
  29. package/dist/drivers/geminiAvatarOutput.js.map +1 -0
  30. package/dist/drivers/geminiEnterpriseRealtimeClient.d.ts +38 -0
  31. package/dist/drivers/geminiEnterpriseRealtimeClient.d.ts.map +1 -0
  32. package/dist/drivers/geminiEnterpriseRealtimeClient.js +99 -0
  33. package/dist/drivers/geminiEnterpriseRealtimeClient.js.map +1 -0
  34. package/dist/drivers/geminiRealtimeClient.d.ts +325 -50
  35. package/dist/drivers/geminiRealtimeClient.d.ts.map +1 -1
  36. package/dist/drivers/geminiRealtimeClient.js +782 -184
  37. package/dist/drivers/geminiRealtimeClient.js.map +1 -1
  38. package/dist/drivers/openAILiveClient.d.ts +5 -8
  39. package/dist/drivers/openAILiveClient.d.ts.map +1 -1
  40. package/dist/drivers/openAILiveClient.js +19 -32
  41. package/dist/drivers/openAILiveClient.js.map +1 -1
  42. package/dist/drivers/openAIRealtimeClient.d.ts +13 -26
  43. package/dist/drivers/openAIRealtimeClient.d.ts.map +1 -1
  44. package/dist/drivers/openAIRealtimeClient.js +28 -55
  45. package/dist/drivers/openAIRealtimeClient.js.map +1 -1
  46. package/dist/drivers/xaiRealtimeClient.d.ts.map +1 -1
  47. package/dist/drivers/xaiRealtimeClient.js +19 -14
  48. package/dist/drivers/xaiRealtimeClient.js.map +1 -1
  49. package/dist/generic/baseRealtimeClient.d.ts +183 -29
  50. package/dist/generic/baseRealtimeClient.d.ts.map +1 -1
  51. package/dist/generic/baseRealtimeClient.js +246 -14
  52. package/dist/generic/baseRealtimeClient.js.map +1 -1
  53. package/dist/generic/openAIProtocolClient.d.ts +2 -0
  54. package/dist/generic/openAIProtocolClient.d.ts.map +1 -1
  55. package/dist/generic/openAIProtocolClient.js +17 -0
  56. package/dist/generic/openAIProtocolClient.js.map +1 -1
  57. package/dist/index.d.ts +2 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3 -3
  60. package/dist/index.js.map +1 -1
  61. package/dist/media/attachVideoSource.d.ts +23 -0
  62. package/dist/media/attachVideoSource.d.ts.map +1 -0
  63. package/dist/media/attachVideoSource.js +49 -0
  64. package/dist/media/attachVideoSource.js.map +1 -0
  65. package/dist/media/channelVideoSource.d.ts +110 -14
  66. package/dist/media/channelVideoSource.d.ts.map +1 -1
  67. package/dist/media/channelVideoSource.js +183 -18
  68. package/dist/media/channelVideoSource.js.map +1 -1
  69. package/dist/media/decoders/frameScheduler.d.ts +119 -0
  70. package/dist/media/decoders/frameScheduler.d.ts.map +1 -0
  71. package/dist/media/decoders/frameScheduler.js +238 -0
  72. package/dist/media/decoders/frameScheduler.js.map +1 -0
  73. package/dist/media/decoders/imageFrameDecoder.d.ts +58 -0
  74. package/dist/media/decoders/imageFrameDecoder.d.ts.map +1 -0
  75. package/dist/media/decoders/imageFrameDecoder.js +110 -0
  76. package/dist/media/decoders/imageFrameDecoder.js.map +1 -0
  77. package/dist/media/decoders/mseFmp4Decoder.d.ts +130 -0
  78. package/dist/media/decoders/mseFmp4Decoder.d.ts.map +1 -0
  79. package/dist/media/decoders/mseFmp4Decoder.js +402 -0
  80. package/dist/media/decoders/mseFmp4Decoder.js.map +1 -0
  81. package/dist/media/decoders/streamCanvas.d.ts +46 -0
  82. package/dist/media/decoders/streamCanvas.d.ts.map +1 -0
  83. package/dist/media/decoders/streamCanvas.js +163 -0
  84. package/dist/media/decoders/streamCanvas.js.map +1 -0
  85. package/dist/media/decoders/videoMimeTypes.d.ts +29 -0
  86. package/dist/media/decoders/videoMimeTypes.d.ts.map +1 -0
  87. package/dist/media/decoders/videoMimeTypes.js +50 -0
  88. package/dist/media/decoders/videoMimeTypes.js.map +1 -0
  89. package/dist/media/decoders/webCodecsChunkDecoder.d.ts +99 -0
  90. package/dist/media/decoders/webCodecsChunkDecoder.d.ts.map +1 -0
  91. package/dist/media/decoders/webCodecsChunkDecoder.js +265 -0
  92. package/dist/media/decoders/webCodecsChunkDecoder.js.map +1 -0
  93. package/dist/media/displayCapture.d.ts +128 -0
  94. package/dist/media/displayCapture.d.ts.map +1 -0
  95. package/dist/media/displayCapture.js +245 -0
  96. package/dist/media/displayCapture.js.map +1 -0
  97. package/dist/media/frameCapture.d.ts +25 -8
  98. package/dist/media/frameCapture.d.ts.map +1 -1
  99. package/dist/media/frameCapture.js +42 -72
  100. package/dist/media/frameCapture.js.map +1 -1
  101. package/dist/media/frameSampler.d.ts +94 -0
  102. package/dist/media/frameSampler.d.ts.map +1 -0
  103. package/dist/media/frameSampler.js +134 -0
  104. package/dist/media/frameSampler.js.map +1 -0
  105. package/dist/media/index.d.ts +32 -0
  106. package/dist/media/index.d.ts.map +1 -0
  107. package/dist/media/index.js +32 -0
  108. package/dist/media/index.js.map +1 -0
  109. package/dist/media/localMediaController.d.ts +116 -0
  110. package/dist/media/localMediaController.d.ts.map +1 -0
  111. package/dist/media/localMediaController.js +281 -0
  112. package/dist/media/localMediaController.js.map +1 -0
  113. package/dist/media/mediaLayout.d.ts +93 -0
  114. package/dist/media/mediaLayout.d.ts.map +1 -0
  115. package/dist/media/mediaLayout.js +187 -0
  116. package/dist/media/mediaLayout.js.map +1 -0
  117. package/dist/media/mediaPreview.d.ts +94 -0
  118. package/dist/media/mediaPreview.d.ts.map +1 -0
  119. package/dist/media/mediaPreview.js +154 -0
  120. package/dist/media/mediaPreview.js.map +1 -0
  121. package/dist/media/mediaStage.d.ts +99 -0
  122. package/dist/media/mediaStage.d.ts.map +1 -0
  123. package/dist/media/mediaStage.js +171 -0
  124. package/dist/media/mediaStage.js.map +1 -0
  125. package/dist/media/model.d.ts +154 -0
  126. package/dist/media/model.d.ts.map +1 -0
  127. package/dist/media/model.js +9 -0
  128. package/dist/media/model.js.map +1 -0
  129. package/dist/media/playbackClock.d.ts +20 -0
  130. package/dist/media/playbackClock.d.ts.map +1 -0
  131. package/dist/media/playbackClock.js +13 -0
  132. package/dist/media/playbackClock.js.map +1 -0
  133. package/dist/media/videoFrameDecoder.d.ts +86 -0
  134. package/dist/media/videoFrameDecoder.d.ts.map +1 -0
  135. package/dist/media/videoFrameDecoder.js +2 -0
  136. package/dist/media/videoFrameDecoder.js.map +1 -0
  137. package/dist/media/videoFrameDecoderRegistry.d.ts +40 -0
  138. package/dist/media/videoFrameDecoderRegistry.d.ts.map +1 -0
  139. package/dist/media/videoFrameDecoderRegistry.js +69 -0
  140. package/dist/media/videoFrameDecoderRegistry.js.map +1 -0
  141. package/dist/media/videoPacing.d.ts +32 -0
  142. package/dist/media/videoPacing.d.ts.map +1 -0
  143. package/dist/media/videoPacing.js +37 -0
  144. package/dist/media/videoPacing.js.map +1 -0
  145. package/dist/media/videoPlayout.d.ts +168 -0
  146. package/dist/media/videoPlayout.d.ts.map +1 -0
  147. package/dist/media/videoPlayout.js +217 -0
  148. package/dist/media/videoPlayout.js.map +1 -0
  149. package/dist/media/videoSourceArbiter.d.ts +250 -0
  150. package/dist/media/videoSourceArbiter.d.ts.map +1 -0
  151. package/dist/media/videoSourceArbiter.js +423 -0
  152. package/dist/media/videoSourceArbiter.js.map +1 -0
  153. package/dist/testing/conformanceAssertions.d.ts +25 -0
  154. package/dist/testing/conformanceAssertions.d.ts.map +1 -0
  155. package/dist/testing/conformanceAssertions.js +64 -0
  156. package/dist/testing/conformanceAssertions.js.map +1 -0
  157. package/dist/testing/conformanceFixtures.d.ts +76 -0
  158. package/dist/testing/conformanceFixtures.d.ts.map +1 -0
  159. package/dist/testing/conformanceFixtures.js +169 -0
  160. package/dist/testing/conformanceFixtures.js.map +1 -0
  161. package/dist/testing/conformanceGates.d.ts +32 -0
  162. package/dist/testing/conformanceGates.d.ts.map +1 -0
  163. package/dist/testing/conformanceGates.js +35 -0
  164. package/dist/testing/conformanceGates.js.map +1 -0
  165. package/dist/testing/conformanceMicrophone.d.ts +10 -0
  166. package/dist/testing/conformanceMicrophone.d.ts.map +1 -0
  167. package/dist/testing/conformanceMicrophone.js +71 -0
  168. package/dist/testing/conformanceMicrophone.js.map +1 -0
  169. package/dist/testing/conformanceTimeline.d.ts +128 -0
  170. package/dist/testing/conformanceTimeline.d.ts.map +1 -0
  171. package/dist/testing/conformanceTimeline.js +32 -0
  172. package/dist/testing/conformanceTimeline.js.map +1 -0
  173. package/dist/testing/conformanceVideoStream.d.ts +29 -0
  174. package/dist/testing/conformanceVideoStream.d.ts.map +1 -0
  175. package/dist/testing/conformanceVideoStream.js +39 -0
  176. package/dist/testing/conformanceVideoStream.js.map +1 -0
  177. package/dist/testing/index.d.ts +29 -0
  178. package/dist/testing/index.d.ts.map +1 -0
  179. package/dist/testing/index.js +28 -0
  180. package/dist/testing/index.js.map +1 -0
  181. package/dist/testing/realtimeVideoConformance.d.ts +23 -0
  182. package/dist/testing/realtimeVideoConformance.d.ts.map +1 -0
  183. package/dist/testing/realtimeVideoConformance.js +139 -0
  184. package/dist/testing/realtimeVideoConformance.js.map +1 -0
  185. package/dist/testing/realtimeVideoConformanceChecks.d.ts +4 -0
  186. package/dist/testing/realtimeVideoConformanceChecks.d.ts.map +1 -0
  187. package/dist/testing/realtimeVideoConformanceChecks.js +164 -0
  188. package/dist/testing/realtimeVideoConformanceChecks.js.map +1 -0
  189. package/dist/testing/realtimeVideoConformanceFrameChecks.d.ts +4 -0
  190. package/dist/testing/realtimeVideoConformanceFrameChecks.d.ts.map +1 -0
  191. package/dist/testing/realtimeVideoConformanceFrameChecks.js +63 -0
  192. package/dist/testing/realtimeVideoConformanceFrameChecks.js.map +1 -0
  193. package/dist/testing/realtimeVideoConformanceSession.d.ts +108 -0
  194. package/dist/testing/realtimeVideoConformanceSession.d.ts.map +1 -0
  195. package/dist/testing/realtimeVideoConformanceSession.js +270 -0
  196. package/dist/testing/realtimeVideoConformanceSession.js.map +1 -0
  197. package/dist/testing/realtimeVideoConformanceTurnChecks.d.ts +4 -0
  198. package/dist/testing/realtimeVideoConformanceTurnChecks.d.ts.map +1 -0
  199. package/dist/testing/realtimeVideoConformanceTurnChecks.js +248 -0
  200. package/dist/testing/realtimeVideoConformanceTurnChecks.js.map +1 -0
  201. package/dist/testing/realtimeVideoConformanceTypes.d.ts +155 -0
  202. package/dist/testing/realtimeVideoConformanceTypes.d.ts.map +1 -0
  203. package/dist/testing/realtimeVideoConformanceTypes.js +2 -0
  204. package/dist/testing/realtimeVideoConformanceTypes.js.map +1 -0
  205. package/dist/testing/recordingPlayout.d.ts +84 -0
  206. package/dist/testing/recordingPlayout.d.ts.map +1 -0
  207. package/dist/testing/recordingPlayout.js +120 -0
  208. package/dist/testing/recordingPlayout.js.map +1 -0
  209. package/package.json +29 -4
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @fileoverview VIDEO PLAYOUT: plays a model's video into one `<video>` element at a time, whatever form it arrives in.
3
+ * A provider hands each frame to {@link VideoPlayout.Append} as a `RealtimeVideoFrame` from `@memberjunction/ai`; a
4
+ * renderer shows {@link VideoPlayout.Source} with `AttachVideoSource`.
5
+ *
6
+ * - **A decoder per frame type.** The player keeps one decoder at a time, chosen from the page's
7
+ * {@link VideoFrameDecoderRegistry} by the frame's kind and MIME type: `'mse-fmp4'` plays fragmented MP4 (a Gemini Live
8
+ * avatar) through Media Source Extensions, `'webcodecs'` plays encoded chunks (H.264, VP8, VP9, AV1) through WebCodecs,
9
+ * `'image'` plays still images. A frame of another type switches decoders; a type no decoder plays is dropped and
10
+ * reported once (`'no-decoder'`).
11
+ * - **Ready before the first frame.** When an element is attached first, the player starts the decoder for its
12
+ * configured type ({@link VideoPlayoutOptions.MimeType}), so MSE has its media source open when the first piece comes.
13
+ * A configured type no decoder plays reports `'unsupported'` and leaves the element alone.
14
+ * - **Fallback.** A decoder that gives up (`Failed`) is disposed and not chosen again for that type; the next one that can
15
+ * play it starts at the next init segment or key frame. Out of decoders, frames of that type are dropped; the voice is
16
+ * unaffected, and the tile's stall fallback shows in place of the video.
17
+ * - **The voice.** An avatar's MP4 carries its voice, so the MSE decoder unmutes the element and
18
+ * {@link VideoPlayout.IsPlaying} says whether the agent is audibly speaking. When the voice plays elsewhere,
19
+ * {@link VideoPlayout.CarriesVoice} `false` mutes the element. Chunk and image decoders carry no voice.
20
+ * {@link VideoPlayoutOptions.OnElementAttached} lets the realtime client route the element's audio into its own Web
21
+ * Audio graph, so its meter and its recording carry the avatar's voice.
22
+ * - **Lip sync.** With {@link VideoPlayoutOptions.Clock}, the voice's playback clock, the chunk and image decoders show
23
+ * each frame when the voice reaches its `PresentationTimeMs`. MSE needs no clock: an MP4 that carries the voice keeps
24
+ * voice and face together on its own timestamps.
25
+ * - **End of turn, barge-in, gaps.** {@link VideoPlayout.EndOfTurn} lets playback run to the true end of the turn;
26
+ * {@link VideoPlayout.Flush} drops everything not yet played; between turns, or while a connection resumes, the element
27
+ * holds the last frame. Each decoder does this its own way.
28
+ * - **Browsers.** {@link VideoPlayout.IsSupported} says, at once, whether some decoder can play a type here.
29
+ *
30
+ * @module @memberjunction/ai-realtime-client/media
31
+ */
32
+ import { type RealtimeVideoFrame } from '@memberjunction/ai';
33
+ import type { MediaVideoSource } from './model.js';
34
+ import type { IPlaybackClock } from './playbackClock.js';
35
+ /** The type of a Gemini Live avatar: H.264 Constrained Baseline 3.1 video, AAC-LC audio. */
36
+ export declare const GEMINI_AVATAR_MP4_TYPE = "video/mp4; codecs=\"avc1.42c01f, mp4a.40.2\"";
37
+ export interface VideoPlayoutOptions {
38
+ /**
39
+ * The type, with codecs, the player expects: the decoder for it starts when an element is attached, before any frame
40
+ * arrives, and the MSE source buffer opens with it until an init segment names its own codecs (or names one the reader
41
+ * doesn't know). Defaults to {@link GEMINI_AVATAR_MP4_TYPE}.
42
+ */
43
+ MimeType?: string;
44
+ /** Seconds of played media to keep behind the playhead. Defaults to 10. */
45
+ BackBufferSeconds?: number;
46
+ /**
47
+ * Whether the element plays the media's audio. Defaults to `true`: an avatar's MP4 carries its voice. `false`
48
+ * mutes the element, for a voice that plays elsewhere. {@link VideoPlayout.CarriesVoice} changes it later.
49
+ */
50
+ CarriesVoice?: boolean;
51
+ /**
52
+ * Called with each element the player takes over, before playback starts, such as to route the element's audio
53
+ * into a Web Audio graph. A player that moves to another element calls it again with that one.
54
+ */
55
+ OnElementAttached?: (element: HTMLVideoElement) => void;
56
+ /**
57
+ * The voice's playback clock, for lip sync: the chunk and image decoders show each frame when the voice reaches its
58
+ * `PresentationTimeMs`. Give it when the driver times its PCM (`IRealtimePcmPlayback.Enqueue(pcm16, mediaTimeMs)`) and
59
+ * its frames on one media timeline; `RealtimePcmPlayback` is such a clock. Without it, frames go by their own times.
60
+ * MSE ignores it.
61
+ */
62
+ Clock?: IPlaybackClock;
63
+ }
64
+ /** Why playout reported a problem. */
65
+ export type VideoPlayoutProblem =
66
+ /** The browser cannot play this type: no decoder plays the configured type, or a decoder can't decode the stream's codec. */
67
+ 'unsupported'
68
+ /** No decoder in this browser plays the frame's kind and type; it was dropped. */
69
+ | 'no-decoder'
70
+ /** A media fragment arrived before any init segment, or an encoded chunk before any key frame, and was dropped. */
71
+ | 'fragment-before-init'
72
+ /** The browser rejected a piece (decode error, or out of buffer space after trimming). */
73
+ | 'append-failed'
74
+ /** Media arrived faster than it could be played, or while no element was attached, and the oldest was dropped. */
75
+ | 'pending-overflow';
76
+ /** Whether a piece is an MP4 init segment: its first box is `ftyp`. */
77
+ export declare function IsMp4InitSegment(piece: ArrayBuffer): boolean;
78
+ /**
79
+ * What a realtime driver needs from an avatar's video player: {@link VideoPlayout} in a browser, a fake in tests.
80
+ */
81
+ export interface IAvatarVideoPlayout {
82
+ /** The video to show. A driver hands it to the host once. */
83
+ readonly Source: MediaVideoSource;
84
+ /** Whether it plays forward with media buffered ahead of the playhead: the avatar is audibly speaking. */
85
+ readonly IsPlaying: boolean;
86
+ /**
87
+ * Where the playhead is, in frames: how many of the frames handed to {@link Append} are still to play (waiting,
88
+ * being decoded or appended, or buffered ahead of the playhead). A frame that was dropped, or that no decoder plays,
89
+ * is not counted. Frames play in the order they were handed over, so the frames ahead are the latest ones handed
90
+ * over, less any of those that were dropped: a driver that counts what it hands over can tell whether a given frame
91
+ * has played.
92
+ */
93
+ readonly FramesAhead: number;
94
+ /** Whether the element plays the media's audio; `false` mutes it. */
95
+ CarriesVoice: boolean;
96
+ /**
97
+ * Hands over one frame, in the order they arrived: a piece of fragmented MP4 (an init segment or a media fragment),
98
+ * an encoded chunk or an image. A frame the player has no decoder for is dropped and reported once (`'no-decoder'`).
99
+ */
100
+ Append(frame: RealtimeVideoFrame): void;
101
+ /** The turn's last piece has arrived: playback runs to its true end and holds the last frame. */
102
+ EndOfTurn(): void;
103
+ /** Barge-in: drops everything not yet played and stops at once, holding the last frame. */
104
+ Flush(): void;
105
+ /** Reports problems, once per kind. Returns a function that removes the handler. */
106
+ OnProblem(handler: (problem: VideoPlayoutProblem, message: string) => void): () => void;
107
+ /** Stops playout and releases the element. */
108
+ Dispose(): void;
109
+ }
110
+ /** Plays a model's video frames into one `<video>` element at a time, through the decoder each frame type needs. */
111
+ export declare class VideoPlayout implements IAvatarVideoPlayout {
112
+ private readonly mimeType;
113
+ private readonly backBufferSeconds;
114
+ private readonly onElementAttached;
115
+ private readonly clock;
116
+ private readonly problemHandlers;
117
+ private readonly reported;
118
+ /** Decoders that gave up, by frame type: they are not chosen again for it. */
119
+ private readonly failed;
120
+ private carriesVoice;
121
+ private element;
122
+ private active;
123
+ private disposed;
124
+ /** Whether some decoder can play `mimeType` in this browser, answered at once (for negotiation, before connect). */
125
+ static IsSupported(mimeType?: string): boolean;
126
+ constructor(options?: VideoPlayoutOptions);
127
+ /** The video to show: the player takes over the element it is attached to. */
128
+ get Source(): MediaVideoSource;
129
+ /**
130
+ * Whether the element plays the media's audio. `false` mutes it: the voice plays elsewhere, such as separate PCM
131
+ * audio when the MP4 has no audio track. Takes effect at once on an attached element.
132
+ */
133
+ get CarriesVoice(): boolean;
134
+ set CarriesVoice(value: boolean);
135
+ /** Whether the decoder's own audio is audibly playing: an MP4 that carries the voice, buffered ahead of the playhead. */
136
+ get IsPlaying(): boolean;
137
+ /**
138
+ * How many of the frames handed over are still to play, as the decoder in use counts them. Frames a decoder that
139
+ * gave up held, and frames no decoder plays, never play, so they are not counted.
140
+ */
141
+ get FramesAhead(): number;
142
+ /**
143
+ * Hands over one frame, in the order they arrived, to the decoder for its type: the one in use, or the first the
144
+ * registry offers. A frame no decoder plays is dropped and reported once (`'no-decoder'`).
145
+ */
146
+ Append(frame: RealtimeVideoFrame): void;
147
+ /** The turn's last frame has arrived: playback runs to its true end and holds the last frame. */
148
+ EndOfTurn(): void;
149
+ /** Barge-in: drops everything not yet played and stops at once. The last frame stays on screen. */
150
+ Flush(): void;
151
+ /** Reports problems, once per kind. Returns a function that removes the handler. */
152
+ OnProblem(handler: (problem: VideoPlayoutProblem, message: string) => void): () => void;
153
+ /** Stops playout, releases the element and disposes the decoder. */
154
+ Dispose(): void;
155
+ private attach;
156
+ private detachElement;
157
+ /** The decoder for the configured type, started before any frame so it is ready when the first one comes. */
158
+ private startConfiguredDecoder;
159
+ /** The decoder for a frame type: the one in use when it plays that type, else a new one from the registry. */
160
+ private decoderFor;
161
+ /** What a decoder chosen for `type` plays with and reports through. */
162
+ private contextFor;
163
+ /** A decoder gave up: it is disposed and not chosen again for its type. The next frame of the type picks another. */
164
+ private decoderFailed;
165
+ private releaseDecoder;
166
+ private report;
167
+ }
168
+ //# sourceMappingURL=videoPlayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"videoPlayout.d.ts","sourceRoot":"","sources":["../../src/media/videoPlayout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @fileoverview VIDEO PLAYOUT: plays a model's video into one `<video>` element at a time, whatever form it arrives in.
3
+ * A provider hands each frame to {@link VideoPlayout.Append} as a `RealtimeVideoFrame` from `@memberjunction/ai`; a
4
+ * renderer shows {@link VideoPlayout.Source} with `AttachVideoSource`.
5
+ *
6
+ * - **A decoder per frame type.** The player keeps one decoder at a time, chosen from the page's
7
+ * {@link VideoFrameDecoderRegistry} by the frame's kind and MIME type: `'mse-fmp4'` plays fragmented MP4 (a Gemini Live
8
+ * avatar) through Media Source Extensions, `'webcodecs'` plays encoded chunks (H.264, VP8, VP9, AV1) through WebCodecs,
9
+ * `'image'` plays still images. A frame of another type switches decoders; a type no decoder plays is dropped and
10
+ * reported once (`'no-decoder'`).
11
+ * - **Ready before the first frame.** When an element is attached first, the player starts the decoder for its
12
+ * configured type ({@link VideoPlayoutOptions.MimeType}), so MSE has its media source open when the first piece comes.
13
+ * A configured type no decoder plays reports `'unsupported'` and leaves the element alone.
14
+ * - **Fallback.** A decoder that gives up (`Failed`) is disposed and not chosen again for that type; the next one that can
15
+ * play it starts at the next init segment or key frame. Out of decoders, frames of that type are dropped; the voice is
16
+ * unaffected, and the tile's stall fallback shows in place of the video.
17
+ * - **The voice.** An avatar's MP4 carries its voice, so the MSE decoder unmutes the element and
18
+ * {@link VideoPlayout.IsPlaying} says whether the agent is audibly speaking. When the voice plays elsewhere,
19
+ * {@link VideoPlayout.CarriesVoice} `false` mutes the element. Chunk and image decoders carry no voice.
20
+ * {@link VideoPlayoutOptions.OnElementAttached} lets the realtime client route the element's audio into its own Web
21
+ * Audio graph, so its meter and its recording carry the avatar's voice.
22
+ * - **Lip sync.** With {@link VideoPlayoutOptions.Clock}, the voice's playback clock, the chunk and image decoders show
23
+ * each frame when the voice reaches its `PresentationTimeMs`. MSE needs no clock: an MP4 that carries the voice keeps
24
+ * voice and face together on its own timestamps.
25
+ * - **End of turn, barge-in, gaps.** {@link VideoPlayout.EndOfTurn} lets playback run to the true end of the turn;
26
+ * {@link VideoPlayout.Flush} drops everything not yet played; between turns, or while a connection resumes, the element
27
+ * holds the last frame. Each decoder does this its own way.
28
+ * - **Browsers.** {@link VideoPlayout.IsSupported} says, at once, whether some decoder can play a type here.
29
+ *
30
+ * @module @memberjunction/ai-realtime-client/media
31
+ */
32
+ import { SniffFmp4Piece } from '@memberjunction/ai';
33
+ import { VideoFrameDecoderRegistry } from './videoFrameDecoderRegistry.js';
34
+ import { FrameKindOfMimeType, FrameTypeKey } from './decoders/videoMimeTypes.js';
35
+ /** The type of a Gemini Live avatar: H.264 Constrained Baseline 3.1 video, AAC-LC audio. */
36
+ export const GEMINI_AVATAR_MP4_TYPE = 'video/mp4; codecs="avc1.42c01f, mp4a.40.2"';
37
+ /** Seconds of already-played media kept behind the playhead before it is removed. */
38
+ const DEFAULT_BACK_BUFFER_SECONDS = 10;
39
+ /** Whether a piece is an MP4 init segment: its first box is `ftyp`. */
40
+ export function IsMp4InitSegment(piece) {
41
+ return SniffFmp4Piece(piece) === 'init';
42
+ }
43
+ /** Plays a model's video frames into one `<video>` element at a time, through the decoder each frame type needs. */
44
+ export class VideoPlayout {
45
+ /** Whether some decoder can play `mimeType` in this browser, answered at once (for negotiation, before connect). */
46
+ static IsSupported(mimeType = GEMINI_AVATAR_MP4_TYPE) {
47
+ const kind = FrameKindOfMimeType(mimeType);
48
+ return kind !== null && VideoFrameDecoderRegistry.Instance.Candidates(kind, mimeType).length > 0;
49
+ }
50
+ constructor(options = {}) {
51
+ this.problemHandlers = new Set();
52
+ this.reported = new Set();
53
+ /** Decoders that gave up, by frame type: they are not chosen again for it. */
54
+ this.failed = new Map();
55
+ this.element = null;
56
+ this.active = null;
57
+ this.disposed = false;
58
+ this.mimeType = options.MimeType ?? GEMINI_AVATAR_MP4_TYPE;
59
+ this.backBufferSeconds = options.BackBufferSeconds ?? DEFAULT_BACK_BUFFER_SECONDS;
60
+ this.carriesVoice = options.CarriesVoice ?? true;
61
+ this.onElementAttached = options.OnElementAttached;
62
+ this.clock = options.Clock;
63
+ }
64
+ /** The video to show: the player takes over the element it is attached to. */
65
+ get Source() {
66
+ return { Kind: 'element', Attach: (element) => this.attach(element) };
67
+ }
68
+ /**
69
+ * Whether the element plays the media's audio. `false` mutes it: the voice plays elsewhere, such as separate PCM
70
+ * audio when the MP4 has no audio track. Takes effect at once on an attached element.
71
+ */
72
+ get CarriesVoice() {
73
+ return this.carriesVoice;
74
+ }
75
+ set CarriesVoice(value) {
76
+ this.carriesVoice = value;
77
+ if (this.element) {
78
+ this.element.muted = !value;
79
+ }
80
+ }
81
+ /** Whether the decoder's own audio is audibly playing: an MP4 that carries the voice, buffered ahead of the playhead. */
82
+ get IsPlaying() {
83
+ return this.active?.Decoder.IsPlaying ?? false;
84
+ }
85
+ /**
86
+ * How many of the frames handed over are still to play, as the decoder in use counts them. Frames a decoder that
87
+ * gave up held, and frames no decoder plays, never play, so they are not counted.
88
+ */
89
+ get FramesAhead() {
90
+ return this.active?.Decoder.FramesAhead ?? 0;
91
+ }
92
+ /**
93
+ * Hands over one frame, in the order they arrived, to the decoder for its type: the one in use, or the first the
94
+ * registry offers. A frame no decoder plays is dropped and reported once (`'no-decoder'`).
95
+ */
96
+ Append(frame) {
97
+ if (this.disposed) {
98
+ return;
99
+ }
100
+ const decoder = this.decoderFor(frame.Kind, frame.MimeType);
101
+ if (!decoder) {
102
+ // A frame it can't play adds no media: the decoder in use, and a turn it was about to end, carry on.
103
+ this.report('no-decoder', `No decoder in this browser plays ${frame.Kind} frames of type ${frame.MimeType}; the frame was dropped.`);
104
+ return;
105
+ }
106
+ decoder.Append(frame);
107
+ }
108
+ /** The turn's last frame has arrived: playback runs to its true end and holds the last frame. */
109
+ EndOfTurn() {
110
+ this.active?.Decoder.EndOfTurn();
111
+ }
112
+ /** Barge-in: drops everything not yet played and stops at once. The last frame stays on screen. */
113
+ Flush() {
114
+ this.active?.Decoder.Flush();
115
+ }
116
+ /** Reports problems, once per kind. Returns a function that removes the handler. */
117
+ OnProblem(handler) {
118
+ this.problemHandlers.add(handler);
119
+ return () => this.problemHandlers.delete(handler);
120
+ }
121
+ /** Stops playout, releases the element and disposes the decoder. */
122
+ Dispose() {
123
+ this.disposed = true;
124
+ this.releaseDecoder();
125
+ this.element = null;
126
+ this.problemHandlers.clear();
127
+ }
128
+ attach(element) {
129
+ if (this.disposed) {
130
+ return () => undefined;
131
+ }
132
+ this.detachElement();
133
+ const decoder = this.active?.Decoder ?? this.startConfiguredDecoder();
134
+ if (!decoder) {
135
+ this.report('unsupported', `This browser cannot play ${this.mimeType}: no decoder plays it.`);
136
+ return () => undefined;
137
+ }
138
+ this.element = element;
139
+ decoder.Attach(element);
140
+ return () => {
141
+ if (this.element === element) {
142
+ this.detachElement();
143
+ }
144
+ };
145
+ }
146
+ detachElement() {
147
+ if (this.element) {
148
+ this.active?.Decoder.Detach();
149
+ this.element = null;
150
+ }
151
+ }
152
+ /** The decoder for the configured type, started before any frame so it is ready when the first one comes. */
153
+ startConfiguredDecoder() {
154
+ const kind = FrameKindOfMimeType(this.mimeType);
155
+ return kind ? this.decoderFor(kind, this.mimeType) : null;
156
+ }
157
+ /** The decoder for a frame type: the one in use when it plays that type, else a new one from the registry. */
158
+ decoderFor(kind, mimeType) {
159
+ const type = FrameTypeKey(kind, mimeType);
160
+ if (this.active?.Type === type) {
161
+ return this.active.Decoder;
162
+ }
163
+ const failed = this.failed.get(type);
164
+ const registration = VideoFrameDecoderRegistry.Instance.Candidates(kind, mimeType).find((candidate) => !failed?.has(candidate.Name));
165
+ if (!registration) {
166
+ return null;
167
+ }
168
+ this.releaseDecoder();
169
+ const decoder = registration.Create(this.contextFor(type, registration.Name));
170
+ this.active = { Type: type, Name: registration.Name, Decoder: decoder };
171
+ if (this.element) {
172
+ decoder.Attach(this.element);
173
+ }
174
+ return decoder;
175
+ }
176
+ /** What a decoder chosen for `type` plays with and reports through. */
177
+ contextFor(type, name) {
178
+ const carriesVoice = () => this.carriesVoice;
179
+ return {
180
+ MimeType: this.mimeType,
181
+ BackBufferSeconds: this.backBufferSeconds,
182
+ get CarriesVoice() {
183
+ return carriesVoice();
184
+ },
185
+ OnElementAttached: this.onElementAttached,
186
+ Clock: this.clock,
187
+ Report: (problem, message) => this.report(problem, message),
188
+ Failed: (message) => this.decoderFailed(type, name, message),
189
+ };
190
+ }
191
+ /** A decoder gave up: it is disposed and not chosen again for its type. The next frame of the type picks another. */
192
+ decoderFailed(type, name, message) {
193
+ console.warn(`[VideoPlayout] The ${name} decoder stopped: ${message}`);
194
+ const failed = this.failed.get(type) ?? new Set();
195
+ failed.add(name);
196
+ this.failed.set(type, failed);
197
+ if (this.active?.Type === type && this.active.Name === name) {
198
+ this.releaseDecoder();
199
+ }
200
+ }
201
+ releaseDecoder() {
202
+ const active = this.active;
203
+ this.active = null;
204
+ active?.Decoder.Dispose();
205
+ }
206
+ report(problem, message) {
207
+ if (this.reported.has(problem)) {
208
+ return;
209
+ }
210
+ this.reported.add(problem);
211
+ console.warn(`[VideoPlayout] ${message}`);
212
+ for (const handler of [...this.problemHandlers]) {
213
+ handler(problem, message);
214
+ }
215
+ }
216
+ }
217
+ //# sourceMappingURL=videoPlayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"videoPlayout.js","sourceRoot":"","sources":["../../src/media/videoPlayout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @fileoverview The VIDEO SOURCE ARBITER: the single writer of a session's inbound video.
3
+ *
4
+ * A realtime session can have many things worth showing the model at once: a shared screen, the camera,
5
+ * a whiteboard, a remote browser. Today's video models accept ONE inbound stream. Before this existed,
6
+ * every source wrote to that one stream directly: whichever happened to send last "won", and the model
7
+ * was never told what it was looking at. The arbiter owns the decision:
8
+ *
9
+ * - **Sources register** (`RegisterSource`) with an id, a label and a kind, and push frames to the
10
+ * arbiter (`PushFrame`) rather than to the client.
11
+ * - **The arbiter maps N live sources onto the model's `MaxInboundVideoStreams`.** With enough streams
12
+ * for every enabled source (a future multi-stream model) frames pass through untouched, each tagged
13
+ * with its source id. Otherwise it ranks the sources by a POLICY (data, overridable) and forwards only
14
+ * the top N.
15
+ * - **The model is told when the choice changes** (`[You can now see: Camera]`), because a model
16
+ * that silently starts seeing something else will describe the old thing.
17
+ * - **The user can switch a source off.** A disabled source is not a candidate, sends nothing, and the
18
+ * model is told it was turned off (`[You can no longer see: Camera (the user turned it off)]`).
19
+ * - **The rate comes only from the negotiated track.** Pacing is read from the sink
20
+ * (`InboundVideoRate`), never a constant here.
21
+ *
22
+ * The notes' wording comes from `@memberjunction/ai` ({@link VideoSourceSeenNote}, {@link VideoSourceTurnedOffNote},
23
+ * {@link VideoSourceTurnedOnNote}), where a meeting's bridge engine gets its notes too, so the model reads one style of
24
+ * note in a call and in a meeting.
25
+ *
26
+ * It deliberately depends only on the narrow {@link IVideoFrameSink} interface (which
27
+ * `BaseRealtimeClient` satisfies) and imports nothing from `generic/` or the drivers, so the media code
28
+ * can be split into its own subpath without untangling it.
29
+ *
30
+ * The default policy and the rationale for its ordering are on {@link DEFAULT_VIDEO_SOURCE_POLICY}.
31
+ *
32
+ * @module @memberjunction/ai-realtime-client
33
+ */
34
+ import { type RealtimeTrackDirection } from '@memberjunction/ai';
35
+ /**
36
+ * The slice of a realtime client the arbiter writes to. `BaseRealtimeClient` satisfies it structurally,
37
+ * and tests satisfy it with a plain object.
38
+ */
39
+ export interface IVideoFrameSink {
40
+ /**
41
+ * How many concurrent inbound video streams the model accepts. `0` when none is live (no video track
42
+ * negotiated), `1` for every model shipped so far.
43
+ */
44
+ readonly MaxInboundVideoStreams: number;
45
+ /** The negotiated inbound video rate in frames per second, or `undefined` when none was negotiated. */
46
+ readonly InboundVideoRate: number | undefined;
47
+ /** Whether a track of the given modality and direction is established and live. */
48
+ IsTrackEstablished(modality: string, direction: RealtimeTrackDirection): boolean;
49
+ /**
50
+ * Streams one frame to the model, tagged with the source that produced it. Optional because most
51
+ * drivers have no video; a sink without it makes the arbiter inert.
52
+ *
53
+ * @returns `true` if the frame was sent.
54
+ */
55
+ SendVideoFrame?(base64Image: string, mimeType?: string, sourceId?: string): boolean;
56
+ /** Injects a context note into the conversation without triggering a reply. */
57
+ SendContextNote(text: string): void;
58
+ }
59
+ /**
60
+ * What kind of source this is. Open vocabulary: `'camera'` and `'screen'` are CAPTURES (the user chose to
61
+ * share them), `'surface'` is a channel surface (a whiteboard, a remote browser); anything else is treated
62
+ * as a surface unless the policy says otherwise.
63
+ */
64
+ export type VideoSourceKind = 'camera' | 'screen' | 'surface' | (string & {});
65
+ /** Whether a source kind is a capture (as opposed to a channel surface). */
66
+ export declare function IsVideoCaptureKind(kind: VideoSourceKind | undefined): boolean;
67
+ /** What a source registers itself as. */
68
+ export interface VideoSourceDescriptor {
69
+ /** Stable id of the source within the session. Registering an existing id updates it in place. */
70
+ SourceID: string;
71
+ /** Human-readable name, used in the notes sent to the model and in the UI's "agent can see" list. */
72
+ Label: string;
73
+ /** What kind of source this is. Defaults to `'surface'`. */
74
+ Kind?: VideoSourceKind;
75
+ /**
76
+ * The channel this source belongs to, when it comes from one. The UI maps a source to the user's
77
+ * persisted per-channel visual-perception choice by this key.
78
+ */
79
+ ChannelKey?: string;
80
+ /**
81
+ * Whether a newly registered source starts enabled. Default `true`. A source the agent may not see yet registers
82
+ * disabled, so the model is never told it is looking at it. Ignored for an id that is already registered; use
83
+ * {@link VideoSourceArbiter.SetSourceEnabled} for that.
84
+ */
85
+ Enabled?: boolean;
86
+ }
87
+ /** A source as the arbiter currently sees it. */
88
+ export interface VideoSourceState extends VideoSourceDescriptor {
89
+ /** `false` when the user (or exposure policy) turned the source off: it is not a candidate and sends nothing. */
90
+ Enabled: boolean;
91
+ /** Whether the arbiter is currently forwarding this source's frames to the model. */
92
+ Active: boolean;
93
+ /** Frames forwarded to the model so far. */
94
+ FramesSent: number;
95
+ /** Epoch ms of the last forwarded frame; `undefined` before the first. */
96
+ LastFrameAt?: number;
97
+ /**
98
+ * `true` while it is the user's pick ({@link VideoSourceArbiter.SelectSource}), which beats every other rule. Absent
99
+ * otherwise, and while nothing is picked the policy decides.
100
+ */
101
+ Picked?: boolean;
102
+ }
103
+ /**
104
+ * One rule of the source-selection policy. Rules apply in order and each contributes the sources it
105
+ * prefers; the arbiter forwards the first `MaxInboundVideoStreams` of the combined ranking.
106
+ *
107
+ * - `'user-pick'`: the source the user explicitly chose ({@link VideoSourceArbiter.SelectSource}).
108
+ * - `'recent-capture'`: captures (camera, screen), the most recently started first.
109
+ * - `'focused'`: the source belonging to the surface the user is looking at
110
+ * ({@link VideoSourceArbiter.SetFocusedSource}).
111
+ * - `'recent'`: any source at all, the most recently started first. The catch-all, so a session whose
112
+ * only source is a surface with no focus still shows it.
113
+ */
114
+ export type VideoSourcePolicyRule = 'user-pick' | 'recent-capture' | 'focused' | 'recent';
115
+ /**
116
+ * The default selection policy, as DATA so an agent or host can reorder or trim it.
117
+ *
118
+ * Order: an explicit user pick beats everything (the user is the authority on what the agent sees);
119
+ * then the most recently started CAPTURE, because starting a camera or share is a deliberate "look at
120
+ * this" and a surface is always there to come back to; then the focused surface; then simply the newest
121
+ * source, so a lone unfocused surface is never invisible.
122
+ *
123
+ * "Most recently started" is applied to captures rather than to every source. Applied to everything it
124
+ * would always produce a winner and the focused-surface rule could never be reached.
125
+ */
126
+ export declare const DEFAULT_VIDEO_SOURCE_POLICY: readonly VideoSourcePolicyRule[];
127
+ /** Options for a {@link VideoSourceArbiter}. */
128
+ export interface VideoSourceArbiterOptions {
129
+ /** The selection policy. Defaults to {@link DEFAULT_VIDEO_SOURCE_POLICY}. */
130
+ Policy?: readonly VideoSourcePolicyRule[];
131
+ /**
132
+ * The note sent when the set of sources the model sees changes while sources are being arbitrated.
133
+ * Receives the labels now in view. Return `null` to send nothing. Default: {@link VideoSourceSeenNote}
134
+ * with every label in view.
135
+ */
136
+ FormatSwitchNote?: (labels: readonly string[]) => string | null;
137
+ /** The note sent when a source is turned off. Return `null` to send nothing. Default: {@link VideoSourceTurnedOffNote}. */
138
+ FormatDisabledNote?: (label: string) => string | null;
139
+ /** The note sent when a source is turned back on. Return `null` to send nothing. Default: {@link VideoSourceTurnedOnNote}. */
140
+ FormatEnabledNote?: (label: string) => string | null;
141
+ /** Reports a problem (a throwing sink). Defaults to `console.error`; the arbiter never throws into a caller's timer. */
142
+ OnError?: (context: string, error: unknown) => void;
143
+ /** Clock, injectable for tests. Defaults to `Date.now`. */
144
+ Now?: () => number;
145
+ }
146
+ /** A sink or a way to find the current one (the client changes across a reconnect). */
147
+ export type VideoFrameSinkSource = IVideoFrameSink | (() => IVideoFrameSink | null | undefined) | null | undefined;
148
+ /**
149
+ * The per-session arbiter. Obtain the one for a client with {@link VideoSourceArbiter.ForSink} so every
150
+ * writer shares it; constructing a second one for the same client would make two writers again.
151
+ */
152
+ export declare class VideoSourceArbiter {
153
+ private readonly sinkSource;
154
+ private readonly options;
155
+ private static readonly bySink;
156
+ /**
157
+ * The arbiter for a sink, created on first use. One arbiter per sink is the whole point (a single
158
+ * writer), so every bridge and channel that gets its arbiter this way shares it.
159
+ *
160
+ * @param sink The client (anything satisfying {@link IVideoFrameSink}).
161
+ * @param options Options applied only when the arbiter is created.
162
+ */
163
+ static ForSink(sink: IVideoFrameSink, options?: VideoSourceArbiterOptions): VideoSourceArbiter;
164
+ private readonly sources;
165
+ private readonly policy;
166
+ private readonly handlers;
167
+ private nextSeq;
168
+ private userPick;
169
+ private focused;
170
+ private focusedChannel;
171
+ /** The ids forwarded as of the last reconcile — the baseline a change is detected against. */
172
+ private lastActive;
173
+ /** Whether the previous reconcile had more enabled sources than streams (arbitration in effect). */
174
+ private lastArbitrating;
175
+ constructor(sinkSource: VideoFrameSinkSource, options?: VideoSourceArbiterOptions);
176
+ private get sink();
177
+ /**
178
+ * Registers a source, or updates its label/kind if the id is already registered (its recency, enabled
179
+ * state and counters are kept). A newly registered source counts as the most recently STARTED.
180
+ */
181
+ RegisterSource(descriptor: VideoSourceDescriptor): void;
182
+ /** Removes a source. Its explicit pick or focus, if it held one, is cleared. */
183
+ UnregisterSource(sourceId: string): void;
184
+ /** Whether a source id is registered. */
185
+ HasSource(sourceId: string): boolean;
186
+ /**
187
+ * The user explicitly picks the source the agent sees (`null` clears the pick). A pick beats every
188
+ * other rule. Returns `false` for an unknown or disabled source (nothing changes).
189
+ */
190
+ SelectSource(sourceId: string | null): boolean;
191
+ /**
192
+ * Marks the CHANNEL whose surface the user is looking at (`null` clears it): every source registered for that
193
+ * channel counts as focused, including one that registers after this call (a surface the user is already on
194
+ * before its first frame). Used by the `'focused'` rule alongside {@link SetFocusedSource}.
195
+ */
196
+ SetFocusedChannel(channelKey: string | null): void;
197
+ /** Marks the source whose surface the user is looking at (`null` clears it). Used by the `'focused'` rule. */
198
+ SetFocusedSource(sourceId: string | null): void;
199
+ /**
200
+ * Turns a source on or off. A disabled source is not a candidate and forwards nothing.
201
+ *
202
+ * @param sourceId The source.
203
+ * @param enabled The new state.
204
+ * @param notify Whether to tell the model. Default `true`. A caller that sends its own note about the
205
+ * same change (a channel lowering its exposure) passes `false` so the model is told once.
206
+ * @returns `false` for an unknown source; `true` otherwise (including "already in that state").
207
+ */
208
+ SetSourceEnabled(sourceId: string, enabled: boolean, notify?: boolean): boolean;
209
+ /** A snapshot of every registered source and whether it is being forwarded, in registration order. */
210
+ GetSources(): VideoSourceState[];
211
+ /** The ids the arbiter would forward right now, best first. */
212
+ GetActiveSourceIDs(): string[];
213
+ /**
214
+ * Subscribes to structural changes (a source registered, removed, enabled, disabled, picked, focused,
215
+ * or the forwarded set changing). Not called per frame.
216
+ *
217
+ * @returns An unsubscribe function.
218
+ */
219
+ OnChange(handler: () => void): () => void;
220
+ /**
221
+ * A source offers a frame. It is forwarded to the model only if the source is enabled, among the
222
+ * sources the model should see, and not inside the negotiated pacing interval.
223
+ *
224
+ * @param sourceId The producing source (it must be registered).
225
+ * @param base64Image The frame, base64-encoded.
226
+ * @param mimeType Image MIME type. Defaults to `'image/jpeg'`.
227
+ * @returns `true` if the frame was sent to the model.
228
+ */
229
+ PushFrame(sourceId: string, base64Image: string, mimeType?: string): boolean;
230
+ /** Drops every source and subscription. The arbiter stays usable (it simply has no sources). */
231
+ Dispose(): void;
232
+ /** The enabled sources, ranked by policy, best first. */
233
+ private rank;
234
+ /** The ids to forward: the top N of the ranking, N being the model's inbound video stream count. */
235
+ private computeActive;
236
+ /**
237
+ * Re-derives the forwarded set and, when it changed while sources are being arbitrated, tells the model.
238
+ *
239
+ * @param quiet Update the baseline without sending a switch note (the caller sends its own).
240
+ */
241
+ private reconcile;
242
+ private formatDisabledNote;
243
+ private formatEnabledNote;
244
+ private now;
245
+ private send;
246
+ private sendNote;
247
+ private emitChange;
248
+ private reportError;
249
+ }
250
+ //# sourceMappingURL=videoSourceArbiter.d.ts.map