@memberjunction/realtime-runtime 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 (114) hide show
  1. package/README.md +75 -0
  2. package/dist/channels/base-realtime-channel-client.d.ts +535 -28
  3. package/dist/channels/base-realtime-channel-client.d.ts.map +1 -1
  4. package/dist/channels/base-realtime-channel-client.js +864 -10
  5. package/dist/channels/base-realtime-channel-client.js.map +1 -1
  6. package/dist/channels/channel-action-dispatcher.d.ts +70 -0
  7. package/dist/channels/channel-action-dispatcher.d.ts.map +1 -0
  8. package/dist/channels/channel-action-dispatcher.js +136 -0
  9. package/dist/channels/channel-action-dispatcher.js.map +1 -0
  10. package/dist/channels/channel-catalog-note.d.ts +48 -0
  11. package/dist/channels/channel-catalog-note.d.ts.map +1 -0
  12. package/dist/channels/channel-catalog-note.js +119 -0
  13. package/dist/channels/channel-catalog-note.js.map +1 -0
  14. package/dist/channels/channel-contract-types.d.ts +107 -0
  15. package/dist/channels/channel-contract-types.d.ts.map +1 -0
  16. package/dist/channels/channel-contract-types.js +12 -0
  17. package/dist/channels/channel-contract-types.js.map +1 -0
  18. package/dist/channels/channel-descriptor-synthesis.d.ts +56 -0
  19. package/dist/channels/channel-descriptor-synthesis.d.ts.map +1 -0
  20. package/dist/channels/channel-descriptor-synthesis.js +90 -0
  21. package/dist/channels/channel-descriptor-synthesis.js.map +1 -0
  22. package/dist/channels/channel-perception.d.ts +82 -0
  23. package/dist/channels/channel-perception.d.ts.map +1 -0
  24. package/dist/channels/channel-perception.js +120 -0
  25. package/dist/channels/channel-perception.js.map +1 -0
  26. package/dist/channels/channel-schema-format.d.ts +17 -0
  27. package/dist/channels/channel-schema-format.d.ts.map +1 -0
  28. package/dist/channels/channel-schema-format.js +45 -0
  29. package/dist/channels/channel-schema-format.js.map +1 -0
  30. package/dist/channels/channel-state-delta.d.ts +59 -0
  31. package/dist/channels/channel-state-delta.d.ts.map +1 -0
  32. package/dist/channels/channel-state-delta.js +116 -0
  33. package/dist/channels/channel-state-delta.js.map +1 -0
  34. package/dist/channels/channel-surface-placement.d.ts +25 -0
  35. package/dist/channels/channel-surface-placement.d.ts.map +1 -0
  36. package/dist/channels/channel-surface-placement.js +29 -0
  37. package/dist/channels/channel-surface-placement.js.map +1 -0
  38. package/dist/channels/channel-visual-pump.d.ts +126 -0
  39. package/dist/channels/channel-visual-pump.d.ts.map +1 -0
  40. package/dist/channels/channel-visual-pump.js +174 -0
  41. package/dist/channels/channel-visual-pump.js.map +1 -0
  42. package/dist/hosts/IRealtimeMediaHost.d.ts +86 -34
  43. package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
  44. package/dist/hosts/IRealtimeMediaHost.js +2 -1
  45. package/dist/hosts/IRealtimeMediaHost.js.map +1 -1
  46. package/dist/index.d.ts +26 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +28 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/session/RealtimeSessionRuntime.d.ts +984 -111
  51. package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
  52. package/dist/session/RealtimeSessionRuntime.js +2396 -381
  53. package/dist/session/RealtimeSessionRuntime.js.map +1 -1
  54. package/dist/session/RecordingShardDrain.d.ts +59 -0
  55. package/dist/session/RecordingShardDrain.d.ts.map +1 -0
  56. package/dist/session/RecordingShardDrain.js +135 -0
  57. package/dist/session/RecordingShardDrain.js.map +1 -0
  58. package/dist/session/RecordingShardQueue.d.ts +94 -0
  59. package/dist/session/RecordingShardQueue.d.ts.map +1 -0
  60. package/dist/session/RecordingShardQueue.js +176 -0
  61. package/dist/session/RecordingShardQueue.js.map +1 -0
  62. package/dist/session/RecordingShardStore.d.ts +57 -0
  63. package/dist/session/RecordingShardStore.d.ts.map +1 -0
  64. package/dist/session/RecordingShardStore.js +184 -0
  65. package/dist/session/RecordingShardStore.js.map +1 -0
  66. package/dist/session/RecordingShardWindow.d.ts +42 -0
  67. package/dist/session/RecordingShardWindow.d.ts.map +1 -0
  68. package/dist/session/RecordingShardWindow.js +88 -0
  69. package/dist/session/RecordingShardWindow.js.map +1 -0
  70. package/dist/session/RecordingTransport.d.ts +66 -0
  71. package/dist/session/RecordingTransport.d.ts.map +1 -0
  72. package/dist/session/RecordingTransport.js +61 -0
  73. package/dist/session/RecordingTransport.js.map +1 -0
  74. package/dist/session/app-client-tool-registry.d.ts +76 -0
  75. package/dist/session/app-client-tool-registry.d.ts.map +1 -0
  76. package/dist/session/app-client-tool-registry.js +100 -0
  77. package/dist/session/app-client-tool-registry.js.map +1 -0
  78. package/dist/session/avatar-notice.d.ts +43 -0
  79. package/dist/session/avatar-notice.d.ts.map +1 -0
  80. package/dist/session/avatar-notice.js +60 -0
  81. package/dist/session/avatar-notice.js.map +1 -0
  82. package/dist/session/call-end.d.ts +21 -0
  83. package/dist/session/call-end.d.ts.map +1 -0
  84. package/dist/session/call-end.js +19 -0
  85. package/dist/session/call-end.js.map +1 -0
  86. package/dist/session/channel-exposure-preferences.d.ts +70 -0
  87. package/dist/session/channel-exposure-preferences.d.ts.map +1 -0
  88. package/dist/session/channel-exposure-preferences.js +138 -0
  89. package/dist/session/channel-exposure-preferences.js.map +1 -0
  90. package/dist/session/channel-session-scope.d.ts +142 -0
  91. package/dist/session/channel-session-scope.d.ts.map +1 -0
  92. package/dist/session/channel-session-scope.js +128 -0
  93. package/dist/session/channel-session-scope.js.map +1 -0
  94. package/dist/session/client-session-deadline.d.ts +28 -0
  95. package/dist/session/client-session-deadline.d.ts.map +1 -0
  96. package/dist/session/client-session-deadline.js +50 -0
  97. package/dist/session/client-session-deadline.js.map +1 -0
  98. package/dist/session/realtime-captures.d.ts +236 -0
  99. package/dist/session/realtime-captures.d.ts.map +1 -0
  100. package/dist/session/realtime-captures.js +383 -0
  101. package/dist/session/realtime-captures.js.map +1 -0
  102. package/dist/session/realtime-microphone.d.ts +50 -0
  103. package/dist/session/realtime-microphone.d.ts.map +1 -0
  104. package/dist/session/realtime-microphone.js +51 -0
  105. package/dist/session/realtime-microphone.js.map +1 -0
  106. package/dist/session/session-event-hub.d.ts +72 -0
  107. package/dist/session/session-event-hub.d.ts.map +1 -0
  108. package/dist/session/session-event-hub.js +151 -0
  109. package/dist/session/session-event-hub.js.map +1 -0
  110. package/dist/session/session-launcher.d.ts +128 -0
  111. package/dist/session/session-launcher.d.ts.map +1 -0
  112. package/dist/session/session-launcher.js +160 -0
  113. package/dist/session/session-launcher.js.map +1 -0
  114. package/package.json +10 -9
@@ -0,0 +1,25 @@
1
+ import type { MJAIAgentChannelEntity_IChannelUIConfig } from '@memberjunction/core-entities';
2
+ import { type MediaPlacement } from '@memberjunction/ai-realtime-client';
3
+ /**
4
+ * Where a channel's surface shows when a call starts, and where the user may move it. It comes from the channel's
5
+ * registry row (`MJ: AI Agent Channels.UIConfig`: `Placement` and `AllowedPlacements`, read by
6
+ * {@link ReadChannelSurfacePlacement}); the host's surface layout starts each surface at {@link Default} and keeps the
7
+ * user's moves within {@link Allowed}.
8
+ */
9
+ export interface ChannelSurfacePlacement {
10
+ /** Where the surface first shows. Always one of {@link Allowed}. */
11
+ Default: MediaPlacement;
12
+ /** Where the user may move it: at least one placement. */
13
+ Allowed: readonly MediaPlacement[];
14
+ }
15
+ /** On its tab and movable anywhere: a channel whose registry row says nothing about placement, or that has no row. */
16
+ export declare const DEFAULT_CHANNEL_SURFACE_PLACEMENT: ChannelSurfacePlacement;
17
+ /**
18
+ * Reads a channel's placement from its registry row. The row is edited by hand, so anything that is not a placement is
19
+ * ignored: no `Placement` means the tab, no usable `AllowedPlacements` means anywhere, and a `Placement` the list does not
20
+ * allow gives way to the first placement it lists.
21
+ *
22
+ * @param config The row's parsed `UIConfig` (`UIConfigObject`), or `null` when it has none.
23
+ */
24
+ export declare function ReadChannelSurfacePlacement(config: MJAIAgentChannelEntity_IChannelUIConfig | null | undefined): ChannelSurfacePlacement;
25
+ //# sourceMappingURL=channel-surface-placement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-surface-placement.d.ts","sourceRoot":"","sources":["../../src/channels/channel-surface-placement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAoB,KAAK,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,OAAO,EAAE,cAAc,CAAC;IACxB,0DAA0D;IAC1D,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CACpC;AAED,sHAAsH;AACtH,eAAO,MAAM,iCAAiC,EAAE,uBAAsF,CAAC;AAEvI;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,uCAAuC,GAAG,IAAI,GAAG,SAAS,GAAG,uBAAuB,CAKvI"}
@@ -0,0 +1,29 @@
1
+ import { MEDIA_PLACEMENTS } from '@memberjunction/ai-realtime-client';
2
+ /** On its tab and movable anywhere: a channel whose registry row says nothing about placement, or that has no row. */
3
+ export const DEFAULT_CHANNEL_SURFACE_PLACEMENT = Object.freeze({ Default: 'tab', Allowed: MEDIA_PLACEMENTS });
4
+ /**
5
+ * Reads a channel's placement from its registry row. The row is edited by hand, so anything that is not a placement is
6
+ * ignored: no `Placement` means the tab, no usable `AllowedPlacements` means anywhere, and a `Placement` the list does not
7
+ * allow gives way to the first placement it lists.
8
+ *
9
+ * @param config The row's parsed `UIConfig` (`UIConfigObject`), or `null` when it has none.
10
+ */
11
+ export function ReadChannelSurfacePlacement(config) {
12
+ const allowed = readAllowedPlacements(config?.AllowedPlacements);
13
+ const placement = config?.Placement;
14
+ const wanted = isPlacement(placement) ? placement : DEFAULT_CHANNEL_SURFACE_PLACEMENT.Default;
15
+ return { Default: allowed.includes(wanted) ? wanted : allowed[0], Allowed: allowed };
16
+ }
17
+ /** The placements a row allows, in its order and without repeats; every placement when it lists none. */
18
+ function readAllowedPlacements(raw) {
19
+ if (!Array.isArray(raw)) {
20
+ return MEDIA_PLACEMENTS;
21
+ }
22
+ const allowed = [...new Set(raw.filter(isPlacement))];
23
+ return allowed.length > 0 ? allowed : MEDIA_PLACEMENTS;
24
+ }
25
+ /** Whether a value read from the row is a placement. */
26
+ function isPlacement(value) {
27
+ return typeof value === 'string' && MEDIA_PLACEMENTS.includes(value);
28
+ }
29
+ //# sourceMappingURL=channel-surface-placement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-surface-placement.js","sourceRoot":"","sources":["../../src/channels/channel-surface-placement.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAuB,MAAM,oCAAoC,CAAC;AAe3F,sHAAsH;AACtH,MAAM,CAAC,MAAM,iCAAiC,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAEvI;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAAC,MAAkE;IAC5G,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,MAAM,EAAE,SAAS,CAAC;IACpC,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iCAAiC,CAAC,OAAO,CAAC;IAC9F,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACvF,CAAC;AAED,yGAAyG;AACzG,SAAS,qBAAqB,CAAC,GAAiE;IAC9F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACzD,CAAC;AAED,wDAAwD;AACxD,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,gBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @fileoverview The **change-driven visual frame pump** — lifted out of the Whiteboard channel so
3
+ * every channel that can show the model pixels does it the same, well-behaved way.
4
+ *
5
+ * The pump decides *when* a frame is worth sending, never *what* a frame looks like (that is the
6
+ * channel's `GetLatestFrame`) and never *how* it reaches the model (that is the injected sink).
7
+ * Its behavior is exactly what the Whiteboard shipped:
8
+ *
9
+ * - **Change-driven, no idle heartbeat.** A frame is produced only because state changed. A static
10
+ * surface costs nothing: no timer runs, no frame is rendered, no tokens are spent.
11
+ * - **Negotiated cadence.** Frames are paced to the rate the inbound video track was negotiated at
12
+ * (a model-profile ceiling), clamped to a floor so a fast model never makes the channel spin.
13
+ * - **Leading edge, then trailing settle.** The first change after quiet sends a frame immediately;
14
+ * changes inside the cooldown arm ONE trailing timer, so the model always ends up seeing the
15
+ * final resting state of a burst — not an intermediate frame it will never be corrected from.
16
+ * - **Dedupe.** A frame identical to the last one sent is dropped (a user nudging a shape back to
17
+ * where it was costs nothing).
18
+ * - **One confirmation frame after an agent edit**, with a note telling the model not to narrate
19
+ * its own change — the agent should *see* what it did without announcing it.
20
+ * - **Only a frame the sink takes counts as sent.** The sink can refuse one: another video source
21
+ * holds the model's only stream, the channel's source is switched off, or the frame came too soon
22
+ * after the last. A refused frame is not the dedupe baseline, is not reported, and no confirmation
23
+ * note goes out without its frame. When the model can see the channel again, {@link VisualPerceptionPump.Refresh}
24
+ * sends the current picture even though it has not changed.
25
+ *
26
+ * Every frame the sink takes is reported with the **change id** of the state it was captured at, so a
27
+ * consumer can verify that pixels and the state events describing the same moment agree.
28
+ *
29
+ * Standalone (injected clock-free: it reads `Date.now()` and `setTimeout`, both fakeable) so the
30
+ * whole behavior is unit-testable with fake timers and no channel, session or DOM.
31
+ *
32
+ * @module @memberjunction/realtime-runtime
33
+ */
34
+ /** Why a frame was pushed. */
35
+ export type VisualFrameReason = 'change' | 'settle' | 'confirmation';
36
+ /** The slice of the shared video bridge the pump writes to. */
37
+ export interface VisualFrameSink {
38
+ /**
39
+ * Pushes one base64 JPEG frame; returns whether it was dispatched to the model. `false` when the frame was refused
40
+ * (the session's video source arbiter is sending the model another source, this source is off, or the frame came
41
+ * inside the pacing interval), and the pump then treats it as never sent.
42
+ */
43
+ PushFrame(base64Jpeg: string): boolean;
44
+ }
45
+ /** How the pump reaches the channel and the session. */
46
+ export interface VisualPerceptionPumpHost {
47
+ /** The sink frames are written to, creating it if needed; `null` when there is none (no session/client yet). */
48
+ GetSink(): VisualFrameSink | null;
49
+ /** Whether the session has an established inbound video track — without one the pump is inert. */
50
+ IsInboundVideoEstablished(): boolean;
51
+ /**
52
+ * Whether exposure policy currently lets this channel show the model pixels. Optional: absent means
53
+ * permitted. While `false` the pump captures and sends nothing and drops any pending settle.
54
+ */
55
+ IsPermitted?(): boolean;
56
+ /** The frame cadence the track negotiated, in ms (the caller clamps it to its floor). */
57
+ GetCadenceMs(): number;
58
+ /** Renders the current frame as a base64 JPEG, or `null` when none is available. */
59
+ CaptureFrame(): Promise<string | null>;
60
+ /** The id of the latest state change — read at capture time to tag the frame. */
61
+ GetChangeId(): number;
62
+ /** Reports a frame the sink took (for `Events$`, diagnostics, tests). A refused frame is not reported. */
63
+ OnFramePushed(frame: string, reason: VisualFrameReason, changeId: number): void;
64
+ /** Sends the "do not narrate your own change" note after a confirmation frame the sink took. */
65
+ SendConfirmationNote(changeId: number): void;
66
+ /** Logs a problem; the pump never throws into a timer or an unawaited promise. */
67
+ OnError(context: string, error: unknown): void;
68
+ }
69
+ /**
70
+ * The change-driven pump. One per channel instance, created by `EnableVisualPerception`.
71
+ */
72
+ export declare class VisualPerceptionPump {
73
+ private readonly host;
74
+ /**
75
+ * Epoch ms of the last frame offered to the sink, taken or not; 0 before the first. It paces the next capture, so a
76
+ * burst of changes while the sink refuses frames still costs one capture per cadence.
77
+ */
78
+ private lastPushTimestamp;
79
+ /** The last frame the sink took, for dedupe: the picture the model has. */
80
+ private lastPushedFrame;
81
+ /** The trailing settle timer; non-null while armed. */
82
+ private trailingTimer;
83
+ constructor(host: VisualPerceptionPumpHost);
84
+ /**
85
+ * A user-originated (or scene-replacing) change happened: push a frame, respecting the cadence.
86
+ * Resolves once the decision has been carried out; never rejects.
87
+ */
88
+ OnChange(): Promise<void>;
89
+ /**
90
+ * An AGENT change succeeded: push exactly one confirmation frame immediately (cancelling any
91
+ * pending settle — the confirmation IS the settled state) and tell the model not to narrate it.
92
+ * No frame, no note, when nothing could be captured; no note when the sink refused the frame, since
93
+ * the model would be told to look at a change it was not shown.
94
+ */
95
+ Confirm(): Promise<void>;
96
+ /**
97
+ * The model can see this channel again after a time it could not: the channel's video source is being sent to the
98
+ * model again, after another source held its only stream or after the source was off. The model's picture of the
99
+ * channel is stale, so the dedupe frame is forgotten and the current picture is pushed even if it has not changed,
100
+ * respecting the cadence. Resolves once the decision has been carried out; never rejects.
101
+ */
102
+ Refresh(): Promise<void>;
103
+ /** Cancels a pending trailing settle without forgetting the dedupe frame (used when a channel is re-initialized). */
104
+ CancelPending(): void;
105
+ /** Cancels the trailing timer and forgets the dedupe frame. Call at teardown. */
106
+ Dispose(): void;
107
+ /** Whether a frame can be pushed right now (permitted, a sink exists and the video track is up). */
108
+ private canPush;
109
+ /** Exposure policy's verdict; a host that has no policy permits everything. */
110
+ private isPermitted;
111
+ /** Captures the current frame and pushes it unless it duplicates the last one. */
112
+ private captureAndPush;
113
+ /** Arms the single trailing settle timer if it is not already armed. */
114
+ private armTrailing;
115
+ /** The trailing settle: push the resting frame if the track is still up and it changed. */
116
+ private settle;
117
+ private cancelTrailing;
118
+ /**
119
+ * Offers a frame to the sink. Only a frame the sink took becomes the dedupe baseline and is reported; the attempt
120
+ * paces the next one either way.
121
+ *
122
+ * @returns Whether the sink took the frame.
123
+ */
124
+ private push;
125
+ }
126
+ //# sourceMappingURL=channel-visual-pump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-visual-pump.d.ts","sourceRoot":"","sources":["../../src/channels/channel-visual-pump.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,8BAA8B;AAC9B,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,CAAC;AAErE,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC5B;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1C;AAED,wDAAwD;AACxD,MAAM,WAAW,wBAAwB;IACrC,gHAAgH;IAChH,OAAO,IAAI,eAAe,GAAG,IAAI,CAAC;IAClC,kGAAkG;IAClG,yBAAyB,IAAI,OAAO,CAAC;IACrC;;;OAGG;IACH,WAAW,CAAC,IAAI,OAAO,CAAC;IACxB,yFAAyF;IACzF,YAAY,IAAI,MAAM,CAAC;IACvB,oFAAoF;IACpF,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,iFAAiF;IACjF,WAAW,IAAI,MAAM,CAAC;IACtB,0GAA0G;IAC1G,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAChF,gGAAgG;IAChG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,kFAAkF;IAClF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAClD;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAWjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVjC;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAAK;IAC9B,2EAA2E;IAC3E,OAAO,CAAC,eAAe,CAAuB;IAC9C,uDAAuD;IACvD,OAAO,CAAC,aAAa,CAA8C;gBAEtC,IAAI,EAAE,wBAAwB;IAE3D;;;OAGG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBtC;;;;;OAKG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBrC;;;;;OAKG;IACI,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B,qHAAqH;IAC9G,aAAa,IAAI,IAAI;IAI5B,iFAAiF;IAC1E,OAAO,IAAI,IAAI;IAKtB,oGAAoG;IACpG,OAAO,CAAC,OAAO;IAIf,+EAA+E;IAC/E,OAAO,CAAC,WAAW;IAInB,kFAAkF;YACpE,cAAc;IAQ5B,wEAAwE;IACxE,OAAO,CAAC,WAAW;IAUnB,2FAA2F;YAC7E,MAAM;IAWpB,OAAO,CAAC,cAAc;IAOtB;;;;;OAKG;IACH,OAAO,CAAC,IAAI;CASf"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview The **change-driven visual frame pump** — lifted out of the Whiteboard channel so
3
+ * every channel that can show the model pixels does it the same, well-behaved way.
4
+ *
5
+ * The pump decides *when* a frame is worth sending, never *what* a frame looks like (that is the
6
+ * channel's `GetLatestFrame`) and never *how* it reaches the model (that is the injected sink).
7
+ * Its behavior is exactly what the Whiteboard shipped:
8
+ *
9
+ * - **Change-driven, no idle heartbeat.** A frame is produced only because state changed. A static
10
+ * surface costs nothing: no timer runs, no frame is rendered, no tokens are spent.
11
+ * - **Negotiated cadence.** Frames are paced to the rate the inbound video track was negotiated at
12
+ * (a model-profile ceiling), clamped to a floor so a fast model never makes the channel spin.
13
+ * - **Leading edge, then trailing settle.** The first change after quiet sends a frame immediately;
14
+ * changes inside the cooldown arm ONE trailing timer, so the model always ends up seeing the
15
+ * final resting state of a burst — not an intermediate frame it will never be corrected from.
16
+ * - **Dedupe.** A frame identical to the last one sent is dropped (a user nudging a shape back to
17
+ * where it was costs nothing).
18
+ * - **One confirmation frame after an agent edit**, with a note telling the model not to narrate
19
+ * its own change — the agent should *see* what it did without announcing it.
20
+ * - **Only a frame the sink takes counts as sent.** The sink can refuse one: another video source
21
+ * holds the model's only stream, the channel's source is switched off, or the frame came too soon
22
+ * after the last. A refused frame is not the dedupe baseline, is not reported, and no confirmation
23
+ * note goes out without its frame. When the model can see the channel again, {@link VisualPerceptionPump.Refresh}
24
+ * sends the current picture even though it has not changed.
25
+ *
26
+ * Every frame the sink takes is reported with the **change id** of the state it was captured at, so a
27
+ * consumer can verify that pixels and the state events describing the same moment agree.
28
+ *
29
+ * Standalone (injected clock-free: it reads `Date.now()` and `setTimeout`, both fakeable) so the
30
+ * whole behavior is unit-testable with fake timers and no channel, session or DOM.
31
+ *
32
+ * @module @memberjunction/realtime-runtime
33
+ */
34
+ /**
35
+ * The change-driven pump. One per channel instance, created by `EnableVisualPerception`.
36
+ */
37
+ export class VisualPerceptionPump {
38
+ constructor(host) {
39
+ this.host = host;
40
+ /**
41
+ * Epoch ms of the last frame offered to the sink, taken or not; 0 before the first. It paces the next capture, so a
42
+ * burst of changes while the sink refuses frames still costs one capture per cadence.
43
+ */
44
+ this.lastPushTimestamp = 0;
45
+ /** The last frame the sink took, for dedupe: the picture the model has. */
46
+ this.lastPushedFrame = null;
47
+ /** The trailing settle timer; non-null while armed. */
48
+ this.trailingTimer = null;
49
+ }
50
+ /**
51
+ * A user-originated (or scene-replacing) change happened: push a frame, respecting the cadence.
52
+ * Resolves once the decision has been carried out; never rejects.
53
+ */
54
+ async OnChange() {
55
+ try {
56
+ if (!this.canPush()) {
57
+ return;
58
+ }
59
+ const elapsed = Date.now() - this.lastPushTimestamp;
60
+ const cadenceMs = this.host.GetCadenceMs();
61
+ if (elapsed >= cadenceMs) {
62
+ this.cancelTrailing();
63
+ await this.captureAndPush('change');
64
+ }
65
+ else {
66
+ this.armTrailing(Math.max(0, cadenceMs - elapsed));
67
+ }
68
+ }
69
+ catch (error) {
70
+ this.host.OnError('change', error);
71
+ }
72
+ }
73
+ /**
74
+ * An AGENT change succeeded: push exactly one confirmation frame immediately (cancelling any
75
+ * pending settle — the confirmation IS the settled state) and tell the model not to narrate it.
76
+ * No frame, no note, when nothing could be captured; no note when the sink refused the frame, since
77
+ * the model would be told to look at a change it was not shown.
78
+ */
79
+ async Confirm() {
80
+ try {
81
+ if (!this.canPush()) {
82
+ return;
83
+ }
84
+ this.cancelTrailing();
85
+ const changeId = this.host.GetChangeId();
86
+ const frame = await this.host.CaptureFrame();
87
+ if (frame && this.push(frame, 'confirmation', changeId)) {
88
+ this.host.SendConfirmationNote(changeId);
89
+ }
90
+ }
91
+ catch (error) {
92
+ this.host.OnError('confirmation', error);
93
+ }
94
+ }
95
+ /**
96
+ * The model can see this channel again after a time it could not: the channel's video source is being sent to the
97
+ * model again, after another source held its only stream or after the source was off. The model's picture of the
98
+ * channel is stale, so the dedupe frame is forgotten and the current picture is pushed even if it has not changed,
99
+ * respecting the cadence. Resolves once the decision has been carried out; never rejects.
100
+ */
101
+ Refresh() {
102
+ this.lastPushedFrame = null;
103
+ return this.OnChange();
104
+ }
105
+ /** Cancels a pending trailing settle without forgetting the dedupe frame (used when a channel is re-initialized). */
106
+ CancelPending() {
107
+ this.cancelTrailing();
108
+ }
109
+ /** Cancels the trailing timer and forgets the dedupe frame. Call at teardown. */
110
+ Dispose() {
111
+ this.cancelTrailing();
112
+ this.lastPushedFrame = null;
113
+ }
114
+ /** Whether a frame can be pushed right now (permitted, a sink exists and the video track is up). */
115
+ canPush() {
116
+ return this.isPermitted() && this.host.GetSink() !== null && this.host.IsInboundVideoEstablished();
117
+ }
118
+ /** Exposure policy's verdict; a host that has no policy permits everything. */
119
+ isPermitted() {
120
+ return this.host.IsPermitted ? this.host.IsPermitted() : true;
121
+ }
122
+ /** Captures the current frame and pushes it unless it duplicates the last one. */
123
+ async captureAndPush(reason) {
124
+ const changeId = this.host.GetChangeId();
125
+ const frame = await this.host.CaptureFrame();
126
+ if (frame && frame !== this.lastPushedFrame) {
127
+ this.push(frame, reason, changeId);
128
+ }
129
+ }
130
+ /** Arms the single trailing settle timer if it is not already armed. */
131
+ armTrailing(delayMs) {
132
+ if (this.trailingTimer !== null) {
133
+ return;
134
+ }
135
+ this.trailingTimer = setTimeout(() => {
136
+ this.trailingTimer = null;
137
+ void this.settle();
138
+ }, delayMs);
139
+ }
140
+ /** The trailing settle: push the resting frame if the track is still up and it changed. */
141
+ async settle() {
142
+ try {
143
+ if (!this.isPermitted() || !this.host.IsInboundVideoEstablished()) {
144
+ return;
145
+ }
146
+ await this.captureAndPush('settle');
147
+ }
148
+ catch (error) {
149
+ this.host.OnError('settle', error);
150
+ }
151
+ }
152
+ cancelTrailing() {
153
+ if (this.trailingTimer !== null) {
154
+ clearTimeout(this.trailingTimer);
155
+ this.trailingTimer = null;
156
+ }
157
+ }
158
+ /**
159
+ * Offers a frame to the sink. Only a frame the sink took becomes the dedupe baseline and is reported; the attempt
160
+ * paces the next one either way.
161
+ *
162
+ * @returns Whether the sink took the frame.
163
+ */
164
+ push(frame, reason, changeId) {
165
+ this.lastPushTimestamp = Date.now();
166
+ if (!this.host.GetSink()?.PushFrame(frame)) {
167
+ return false;
168
+ }
169
+ this.lastPushedFrame = frame;
170
+ this.host.OnFramePushed(frame, reason, changeId);
171
+ return true;
172
+ }
173
+ }
174
+ //# sourceMappingURL=channel-visual-pump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-visual-pump.js","sourceRoot":"","sources":["../../src/channels/channel-visual-pump.ts"],"names":[],"mappings":"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"}
@@ -19,7 +19,8 @@
19
19
  *
20
20
  * ## Implementations
21
21
  *
22
- * - **Browser** — `getUserMedia({ audio: true })` and the global `RTCPeerConnection`.
22
+ * - **Browser** — a `LocalMediaController` (from `@memberjunction/ai-realtime-client/media`) that opens
23
+ * the microphone and follows device changes, and the global `RTCPeerConnection`.
23
24
  * - **React Native** — the `react-native-webrtc` equivalents, which polyfill both APIs natively
24
25
  * and additionally give the platform's acoustic echo cancellation, noise suppression and jitter
25
26
  * buffering for free.
@@ -33,6 +34,7 @@
33
34
  * runtime never *constructs* one; it only receives it from the host and hands it to the realtime
34
35
  * client driver, whose `Connect()` already takes exactly this type.
35
36
  */
37
+ import type { DisplayCaptureOptions, DisplayCaptureResult, FrameSamplerOptions, ILocalMediaController } from '@memberjunction/ai-realtime-client';
36
38
  /**
37
39
  * The platform capabilities the realtime session runtime needs but cannot provide itself.
38
40
  *
@@ -63,11 +65,46 @@ export interface IRealtimeMediaHost {
63
65
  * route, and that setting outlives the call — every later sound in the app plays through the
64
66
  * call route, at call volume, until something puts it back.
65
67
  *
66
- * Called by the runtime on teardown, after the microphone tracks are stopped, on every exit
67
- * path including a failed start. Best-effort: a rejection is logged and swallowed, because
68
- * restoring audio state is never worth failing the end of a call over.
68
+ * Called by the runtime once each time it asks for the microphone (through
69
+ * {@link AcquireMicrophone} or the session's controller), after the request returns, opened or
70
+ * failed, and the microphone tracks are stopped: when the session ends, on every exit path
71
+ * including a failed start, or, when the session ended while the request was pending, once the
72
+ * request returns, so the call undoes what the request did. When a newer session has asked for
73
+ * the microphone by then, that session's end makes the call instead. A session that ends before
74
+ * asking for the microphone gets no call. Best-effort: a rejection is logged and swallowed,
75
+ * because restoring audio state is never worth failing the end of a call over.
69
76
  */
70
77
  ReleaseMicrophone?(): Promise<void> | void;
78
+ /**
79
+ * OPTIONAL: creates a controller for the user's camera and microphone, once per session.
80
+ *
81
+ * When a host offers one, the runtime starts the microphone through it instead of
82
+ * {@link AcquireMicrophone}. The call then follows a device switch and survives a lost device:
83
+ * the controller swaps the new track into the same stream (falling back to the system default
84
+ * when a device goes away), and the runtime moves the driver and the recorder onto it. The
85
+ * runtime disposes the controller at teardown. Hosts without one keep {@link AcquireMicrophone}.
86
+ */
87
+ CreateLocalMediaController?(): ILocalMediaController;
88
+ /**
89
+ * OPTIONAL: asks the user for a screen, window or browser tab to share (or one panel of the page), for the
90
+ * session's screen share. Always resolves: a cancelled picker or a refusal is a result.
91
+ *
92
+ * A browser host passes it to `RequestDisplayCapture` from `@memberjunction/ai-realtime-client/media`. A host
93
+ * without it cannot share a screen; the runtime reports that instead of asking.
94
+ */
95
+ RequestDisplayCapture?(options?: DisplayCaptureOptions): Promise<DisplayCaptureResult>;
96
+ /**
97
+ * OPTIONAL: creates the sampler that takes still frames from a camera or a screen share for the agent: from `stream`,
98
+ * at `options.Rate` frames per second (the rate the model's video track negotiated), each handed to `options.OnFrame`
99
+ * as a base64 image. The runtime creates one per capture, starts it once the device or share is open, and stops it
100
+ * when the capture stops; the stream's tracks belong to the capture, not the sampler.
101
+ *
102
+ * A host without it gets `FrameSampler` from `@memberjunction/ai-realtime-client/media`, which draws through a hidden
103
+ * `<video>` and a `<canvas>`: what a browser host wants, so it can omit this. A host without a DOM (React Native, a
104
+ * native shell, a test harness) supplies its own here. When the sampler cannot start (`Start()` returns `false`, as
105
+ * the DOM default does without a DOM), the capture fails as `unsupported` and the agent is never told it can see it.
106
+ */
107
+ CreateFrameSampler?(stream: MediaStream, options: FrameSamplerOptions): IRealtimeFrameSampler;
71
108
  /**
72
109
  * OPTIONAL: creates a recorder for this session's audio, when the platform can record and the
73
110
  * user has consented.
@@ -82,26 +119,37 @@ export interface IRealtimeMediaHost {
82
119
  */
83
120
  CreateRecorder?(): IRealtimeSessionRecorder | null;
84
121
  }
122
+ /**
123
+ * A frame sampler a host supplies through {@link IRealtimeMediaHost.CreateFrameSampler}. `FrameSampler` from
124
+ * `@memberjunction/ai-realtime-client/media` is one.
125
+ */
126
+ export interface IRealtimeFrameSampler {
127
+ /**
128
+ * Starts taking frames.
129
+ *
130
+ * @returns `false` when the sampler cannot take frames on this platform; the runtime then fails the capture.
131
+ */
132
+ Start(): boolean;
133
+ /** Stops taking frames and releases what the sampler holds. Never stops the stream's tracks. */
134
+ Stop(): void;
135
+ }
85
136
  /**
86
137
  * The recording capability a host may optionally provide.
87
138
  *
88
- * Mirrors the browser recorder's existing surface so the runtime's call sites are unchanged by the
89
- * extraction. Every method is best-effort by contract: a recorder that fails mid-session must
90
- * degrade to "no recording" rather than failing the call, because audio capture is never worth
91
- * dropping a live conversation over.
139
+ * The host captures the call (mic + agent mix) at **48 kHz** and runs ONE continuous Opus encoder
140
+ * for the whole call: mono, 32 kbps, 20 ms frames (960 samples). The runtime pulls the encoded
141
+ * packets every few seconds, cuts them into indexed `MJOPUS1` shards, keeps each one in a durable
142
+ * local store until the server acknowledges it, and sends a terminal request when the call ends.
143
+ * The host never builds a container (no WAV, no Ogg) and never base64-encodes anything: the server
144
+ * assembles the final `recording.wav` from the shards.
145
+ *
146
+ * Every method is best-effort by contract: a recorder that fails mid-session must degrade to
147
+ * "no recording" rather than failing the call, because audio capture is never worth dropping a live
148
+ * conversation over.
92
149
  */
93
150
  export interface IRealtimeSessionRecorder {
94
- /** `false` when capture could not start (unsupported platform, no permission). */
151
+ /** `false` when capture could not start (unsupported platform, no permission, no encoder). */
95
152
  readonly IsRecording: boolean;
96
- /** Capture sample rate, used to label the header-less PCM16 crash-recovery shards. */
97
- readonly SampleRate: number;
98
- /**
99
- * Container MIME type of the FINAL consolidated recording.
100
- *
101
- * Read it BEFORE {@link StopAndEncode} — implementations are permitted to clear it on stop,
102
- * which is why the runtime captures it first.
103
- */
104
- readonly MimeType: string;
105
153
  /**
106
154
  * Begins capture of the microphone, mixing in the agent's audio when it is already available.
107
155
  *
@@ -111,26 +159,30 @@ export interface IRealtimeSessionRecorder {
111
159
  Start(micStream: MediaStream, remoteStream: MediaStream | null): void;
112
160
  /** Mixes the agent's audio in once its track arrives, so the recording carries both sides. */
113
161
  AttachRemoteStream(stream: MediaStream): void;
114
- /** Milliseconds into the recording, used to stamp per-turn cue offsets into a seekable file. */
162
+ /**
163
+ * OPTIONAL: records the microphone stream's current track from now on, after its track was
164
+ * replaced (a device switch, or a lost device replaced by the default). A recorder bound to the
165
+ * old track would otherwise record silence. A recorder without it keeps its original binding.
166
+ */
167
+ ReplaceMicrophone?(micStream: MediaStream): void;
168
+ /**
169
+ * Milliseconds into the recording, derived from the samples captured so far on the 48 kHz
170
+ * sample clock (never wall clock), used to stamp per-turn cue offsets into the assembled file.
171
+ */
115
172
  NowOffsetMs(): number;
116
- /** Waveform peaks computed during capture; survives {@link StopAndEncode}. */
117
- GetPeaks(): number[];
118
173
  /**
119
- * Returns audio captured since the previous snapshot, **already base64-encoded**, or `null`
120
- * when nothing new is pending.
121
- *
122
- * Encoding lives behind this seam deliberately: browsers reach for `Blob` + `FileReader`,
123
- * React Native reads a file off disk, and a test harness may hold bytes in memory. The runtime
124
- * only forwards the string to the server, so it should never learn which of those it is.
125
- *
126
- * Shards are header-less raw little-endian PCM16 — recovery is concatenate-in-order then
127
- * WAV-wrap. The canonical seekable file is the consolidated {@link StopAndEncode} upload.
174
+ * Returns the 20 ms Opus packets encoded since the previous call, in capture order, and forgets
175
+ * them; an empty array when none are ready. Synchronous and cheap: it hands over what the
176
+ * encoder already produced and never encodes on the caller's thread — the runtime calls it from
177
+ * `pagehide`, where there is no time to wait for an encoder.
128
178
  */
129
- SnapshotNewSegmentBase64(): Promise<string | null>;
179
+ TakeEncodedPackets(): Uint8Array[];
130
180
  /**
131
- * Stops capture and returns the consolidated recording **already base64-encoded**, or `null`
132
- * when nothing was captured. Idempotent.
181
+ * Stops capture, flushes the encoder (its last packets become available to
182
+ * {@link TakeEncodedPackets}), and resolves the total number of samples captured at 48 kHz. That
183
+ * count may be less than `packets × 960` when the encoder padded its last frame; the runtime
184
+ * trims the final shard to it. Idempotent: a second call resolves the same count.
133
185
  */
134
- StopAndEncode(): Promise<string | null>;
186
+ Stop(): Promise<number>;
135
187
  }
136
188
  //# sourceMappingURL=IRealtimeMediaHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"IRealtimeMediaHost.d.ts","sourceRoot":"","sources":["../../src/hosts/IRealtimeMediaHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;;;;OAUG;IACH,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAE1C;;;;;;;;;;;;;OAaG;IACH,iBAAiB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE3C;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAI,wBAAwB,GAAG,IAAI,CAAC;CACtD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACrC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IAEtE,8FAA8F;IAC9F,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAE9C,gGAAgG;IAChG,WAAW,IAAI,MAAM,CAAC;IAEtB,8EAA8E;IAC9E,QAAQ,IAAI,MAAM,EAAE,CAAC;IAErB;;;;;;;;;;OAUG;IACH,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEnD;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3C"}
1
+ {"version":3,"file":"IRealtimeMediaHost.d.ts","sourceRoot":"","sources":["../../src/hosts/IRealtimeMediaHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAElJ;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;;;;OAUG;IACH,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE3C;;;;;;;;OAQG;IACH,0BAA0B,CAAC,IAAI,qBAAqB,CAAC;IAErD;;;;;;OAMG;IACH,qBAAqB,CAAC,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEvF;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,GAAG,qBAAqB,CAAC;IAE9F;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAI,wBAAwB,GAAG,IAAI,CAAC;CACtD;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC;IAEjB,gGAAgG;IAChG,IAAI,IAAI,IAAI,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACrC,8FAA8F;IAC9F,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;;OAKG;IACH,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IAEtE,8FAA8F;IAC9F,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAE9C;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;IAEjD;;;OAGG;IACH,WAAW,IAAI,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,kBAAkB,IAAI,UAAU,EAAE,CAAC;IAEnC;;;;;OAKG;IACH,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3B"}
@@ -19,7 +19,8 @@
19
19
  *
20
20
  * ## Implementations
21
21
  *
22
- * - **Browser** — `getUserMedia({ audio: true })` and the global `RTCPeerConnection`.
22
+ * - **Browser** — a `LocalMediaController` (from `@memberjunction/ai-realtime-client/media`) that opens
23
+ * the microphone and follows device changes, and the global `RTCPeerConnection`.
23
24
  * - **React Native** — the `react-native-webrtc` equivalents, which polyfill both APIs natively
24
25
  * and additionally give the platform's acoustic echo cancellation, noise suppression and jitter
25
26
  * buffering for free.
@@ -1 +1 @@
1
- {"version":3,"file":"IRealtimeMediaHost.js","sourceRoot":"","sources":["../../src/hosts/IRealtimeMediaHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG"}
1
+ {"version":3,"file":"IRealtimeMediaHost.js","sourceRoot":"","sources":["../../src/hosts/IRealtimeMediaHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG"}
package/dist/index.d.ts CHANGED
@@ -28,9 +28,32 @@
28
28
  * @module @memberjunction/realtime-runtime
29
29
  */
30
30
  export { RealtimeSessionRuntime } from './session/RealtimeSessionRuntime.js';
31
- export { REALTIME_RECORDING_CONSENT_KEY, type RealtimeConnectionState, type RealtimeCaption, type RealtimeDelegationProgress, type RealtimeDelegationResult, type RealtimeClientToolHandler, type RealtimeChannelFocusEvent, type RealtimeDelegationNarration, type RealtimeThoughtNarration, type StartRealtimeClientSessionResult, type RealtimeSessionRunOptions, } from './session/RealtimeSessionRuntime.js';
32
- export { type IRealtimeMediaHost, type IRealtimeSessionRecorder, } from './hosts/IRealtimeMediaHost.js';
33
- export { BaseRealtimeChannelClient, type RealtimeChannelContext, type RealtimeSurfaceComponentType, type ChannelOnboardingDetails, } from './channels/base-realtime-channel-client.js';
31
+ export { REALTIME_RECORDING_CONSENT_KEY, RECORDING_TERMINAL_ACK_TIMEOUT_MS, RECORDER_STOP_TIMEOUT_MS, type RealtimeConnectionState, type RealtimeCaption, type RealtimeDelegationProgress, type RealtimeDelegationResult, type RealtimeClientToolHandler, type RealtimeChannelFocusEvent, type RealtimeDelegationNarration, type RealtimeThoughtNarration, type StartRealtimeClientSessionResult, type RealtimeSessionRunOptions, type RecordingTerminalOptions, } from './session/RealtimeSessionRuntime.js';
32
+ export { ResolveAvatarNotice, type RealtimeAvatarNotice } from './session/avatar-notice.js';
33
+ export { DefaultRealtimeSessionLauncher, type IRealtimeSessionLauncher, type RealtimeSessionLaunchRequest, type RealtimeSessionLaunchContext, } from './session/session-launcher.js';
34
+ export { RealtimeSessionEventHub, type IRealtimeSessionEventSource, type RealtimeSessionEventHubOptions, type RealtimeSessionStreamEvent, type RealtimeSessionVerificationSnapshot, } from './session/session-event-hub.js';
35
+ export { ClientSessionDeadline } from './session/client-session-deadline.js';
36
+ export { DescribeCallEnd, REALTIME_CALL_TIME_LIMIT_END, type RealtimeCallEndDescription } from './session/call-end.js';
37
+ export { REALTIME_CAPTURES_OFF, REALTIME_CAPTURE_OFFERS_NONE, REALTIME_CAPTURE_SOURCE_IDS, type RealtimeCaptureOffers, type RealtimeCaptureFailure, type RealtimeCaptureKind, type RealtimeCaptureState, type RealtimeCaptureStates, } from './session/realtime-captures.js';
38
+ export { REALTIME_MICROPHONE_NONE, type RealtimeMicrophoneState } from './session/realtime-microphone.js';
39
+ export { type RealtimeHostChannelDeclaration, type RealtimeSessionStartOptions, } from './session/channel-session-scope.js';
40
+ export { InMemoryChannelExposurePreferences, ParseExposurePreferences, SerializeExposurePreferences, UserSettingsExposurePreferences, VISUAL_PERCEPTION_SETTING_KEY, type IChannelExposurePreferences, } from './session/channel-exposure-preferences.js';
41
+ export { DEFAULT_APP_TOOL_OWNER, type AppClientToolRegistration, type RealtimeAppClientToolHandler, } from './session/app-client-tool-registry.js';
42
+ export { type IRealtimeFrameSampler, type IRealtimeMediaHost, type IRealtimeSessionRecorder, } from './hosts/IRealtimeMediaHost.js';
43
+ export { BaseRealtimeChannelClient, type RealtimeChannelContext, type RealtimeSurfaceComponentType, type ChannelOnboardingDetails, type VisualPerceptionOptions, type ChannelExposureSettings, } from './channels/base-realtime-channel-client.js';
44
+ export { DEFAULT_CHANNEL_SURFACE_PLACEMENT, ReadChannelSurfacePlacement, type ChannelSurfacePlacement, } from './channels/channel-surface-placement.js';
45
+ export { type RealtimeChannelVerbResult, type RealtimeChannelEvent, type RealtimeChannelOutput, type RealtimeChannelTarget, type RealtimeContextActionRequest, type RealtimeContextActionResult, type RealtimeContextErrorCode, } from './channels/channel-contract-types.js';
46
+ export { SynthesizeChannelDescriptor, BuildToolBackedVerbs, VerbNameForTool } from './channels/channel-descriptor-synthesis.js';
47
+ export { ChannelActionDispatcher, CHANNEL_OPEN_ACTION, type ChannelDispatchHost, type DispatchableChannel } from './channels/channel-action-dispatcher.js';
48
+ export { ComputeStateDelta, FormatChannelNote, ListChangedPaths, type StateDelta } from './channels/channel-state-delta.js';
49
+ export { FormatParameterList } from './channels/channel-schema-format.js';
50
+ export { BuildChannelCatalogNote, type ChannelCatalogEntry } from './channels/channel-catalog-note.js';
51
+ export { VisualPerceptionPump, type VisualFrameReason, type VisualFrameSink, type VisualPerceptionPumpHost, } from './channels/channel-visual-pump.js';
52
+ export { ChannelPerceptionCoalescer, DEFAULT_CHANNEL_PERCEPTION_OPTIONS, type ChannelPerceptionHost, type ChannelPerceptionOptions, } from './channels/channel-perception.js';
34
53
  export { ParseDelegationResultJson, type ParsedDelegationArtifact, type ParsedDelegationResult, FormatToolName, } from './session/delegation-result-parser.js';
35
54
  export { BuildNarrationInstructions, DefaultNarrationInstructions, type NarrationBuildOptions, } from './narration/narration-template.js';
55
+ export { type RecordingShard } from './session/RecordingShardWindow.js';
56
+ export { CreateRecordingShardStore, NullRecordingShardStore, RECORDING_SHARD_RETENTION_MS, type IRecordingShardStore, type PendingRecordingShard, } from './session/RecordingShardStore.js';
57
+ export { FetchRecordingTransport, RecordingBaseURLFromGraphQLURL, type FetchRecordingTransportConfig, type IRecordingTransport, type RecordingTerminalInput, type RecordingUploadOptions, type RecordingUploadOutcome, } from './session/RecordingTransport.js';
58
+ export { CreateRecordingTransportForProvider, DeleteStoredRecordingShard, DrainStoredRecordingShardsOnce, MAX_DRAINED_SHARDS_PER_LOAD, RECORDING_LOGOUT_SEND_TIMEOUT_MS, ReleaseRecordingShardsAtLogout, ReleaseUserRecordingShards, type RecordingShardDrainPass, type RecordingShardRelease, } from './session/RecordingShardDrain.js';
36
59
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAG1E,OAAO,EACH,8BAA8B,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,gCAAgC,EACrC,KAAK,yBAAyB,GACjC,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACH,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,GAChC,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACH,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,GAChC,MAAM,yCAAyC,CAAC;AAGjD,OAAO,EACH,yBAAyB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,cAAc,GACjB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACH,0BAA0B,EAC1B,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}