@oxvo/ai-live-assist 7.3.4 → 7.4.15

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 (67) hide show
  1. package/cjs/AiLiveAssist.d.ts +506 -6
  2. package/cjs/AiLiveAssist.js +12280 -1267
  3. package/cjs/client.d.ts +51 -10
  4. package/cjs/client.js +193 -58
  5. package/cjs/context.d.ts +15 -5
  6. package/cjs/context.js +568 -73
  7. package/cjs/control.d.ts +72 -7
  8. package/cjs/control.js +598 -71
  9. package/cjs/dialogFocusScope.d.ts +37 -0
  10. package/cjs/dialogFocusScope.js +227 -0
  11. package/cjs/experienceState.js +1 -1
  12. package/cjs/index.d.ts +1 -1
  13. package/cjs/index.js +2 -2
  14. package/cjs/media.d.ts +13 -3
  15. package/cjs/media.js +125 -19
  16. package/cjs/messages.js +22 -3
  17. package/cjs/messengerLoader.d.ts +3 -0
  18. package/cjs/messengerLoader.js +156 -0
  19. package/cjs/pageAssistancePreference.d.ts +78 -0
  20. package/cjs/pageAssistancePreference.js +488 -0
  21. package/cjs/preview.d.ts +33 -0
  22. package/cjs/preview.js +567 -0
  23. package/cjs/tabLock.d.ts +54 -3
  24. package/cjs/tabLock.js +752 -72
  25. package/cjs/types.d.ts +101 -3
  26. package/cjs/ui.d.ts +192 -10
  27. package/cjs/ui.js +6683 -543
  28. package/cjs/version.d.ts +1 -1
  29. package/cjs/version.js +1 -1
  30. package/cjs/voicePresenceUi.d.ts +42 -9
  31. package/cjs/voicePresenceUi.js +757 -125
  32. package/lib/AiLiveAssist.d.ts +506 -6
  33. package/lib/AiLiveAssist.js +12285 -1275
  34. package/lib/client.d.ts +51 -10
  35. package/lib/client.js +190 -57
  36. package/lib/context.d.ts +15 -5
  37. package/lib/context.js +566 -72
  38. package/lib/control.d.ts +72 -7
  39. package/lib/control.js +598 -71
  40. package/lib/dialogFocusScope.d.ts +37 -0
  41. package/lib/dialogFocusScope.js +223 -0
  42. package/lib/experienceState.js +1 -1
  43. package/lib/index.d.ts +1 -1
  44. package/lib/index.js +2 -2
  45. package/lib/media.d.ts +13 -3
  46. package/lib/media.js +125 -19
  47. package/lib/messages.js +22 -3
  48. package/lib/messengerLoader.d.ts +3 -0
  49. package/lib/messengerLoader.js +151 -0
  50. package/lib/pageAssistancePreference.d.ts +78 -0
  51. package/lib/pageAssistancePreference.js +482 -0
  52. package/lib/preview.d.ts +33 -0
  53. package/lib/preview.js +561 -0
  54. package/lib/tabLock.d.ts +54 -3
  55. package/lib/tabLock.js +752 -72
  56. package/lib/types.d.ts +101 -3
  57. package/lib/ui.d.ts +192 -10
  58. package/lib/ui.js +6684 -544
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/voicePresenceUi.d.ts +42 -9
  62. package/lib/voicePresenceUi.js +757 -125
  63. package/package.json +8 -3
  64. package/cjs/recordingPolicy.d.ts +0 -2
  65. package/cjs/recordingPolicy.js +0 -12
  66. package/lib/recordingPolicy.d.ts +0 -2
  67. package/lib/recordingPolicy.js +0 -8
@@ -0,0 +1,37 @@
1
+ type DialogPresentation = {
2
+ surface: HTMLElement;
3
+ initialFocus: () => HTMLElement | null;
4
+ fallbackFocus: () => HTMLElement | null;
5
+ onEscape: () => void;
6
+ };
7
+ /**
8
+ * Owns keyboard focus for one safety presentation at a time. Presentation
9
+ * replacement is generation-guarded so stale microtasks cannot focus or
10
+ * restore an earlier prompt.
11
+ */
12
+ export declare class DialogFocusScope {
13
+ private readonly root;
14
+ private readonly host;
15
+ private current;
16
+ private restoreTarget;
17
+ private generation;
18
+ private redirectGeneration;
19
+ private containmentBound;
20
+ private destroyed;
21
+ constructor(root: ShadowRoot, host: HTMLElement);
22
+ present(value: DialogPresentation): void;
23
+ dismiss(surface: HTMLElement, options?: {
24
+ restore?: boolean;
25
+ }): void;
26
+ clear(options?: {
27
+ restore?: boolean;
28
+ }): void;
29
+ destroy(): void;
30
+ private bindContainment;
31
+ private unbindContainment;
32
+ private focusPresentation;
33
+ private onFocusIn;
34
+ private activeFocusTarget;
35
+ private onKeyDown;
36
+ }
37
+ export {};
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DialogFocusScope = void 0;
4
+ const focusableSelector = [
5
+ "button:not([disabled])",
6
+ "input:not([disabled])",
7
+ "textarea:not([disabled])",
8
+ "select:not([disabled])",
9
+ "summary",
10
+ "a[href]",
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ ].join(",");
13
+ const isVisibleFocusTarget = (element) => {
14
+ if (!element?.isConnected ||
15
+ element.hidden ||
16
+ element.getAttribute("aria-hidden") === "true" ||
17
+ element.closest("[hidden],[inert]") ||
18
+ ("disabled" in element && element.disabled === true)) {
19
+ return false;
20
+ }
21
+ try {
22
+ const computed = getComputedStyle(element);
23
+ return computed.display !== "none" && computed.visibility !== "hidden";
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ };
29
+ /**
30
+ * Owns keyboard focus for one safety presentation at a time. Presentation
31
+ * replacement is generation-guarded so stale microtasks cannot focus or
32
+ * restore an earlier prompt.
33
+ */
34
+ class DialogFocusScope {
35
+ constructor(root, host) {
36
+ this.root = root;
37
+ this.host = host;
38
+ this.current = null;
39
+ this.restoreTarget = null;
40
+ this.generation = 0;
41
+ this.redirectGeneration = null;
42
+ this.containmentBound = false;
43
+ this.destroyed = false;
44
+ this.onFocusIn = (event) => {
45
+ const presentation = this.current;
46
+ if (!presentation || this.destroyed)
47
+ return;
48
+ const eventPath = event.composedPath?.() ?? [];
49
+ const eventTarget = event.target;
50
+ if (eventPath.includes(presentation.surface) ||
51
+ (eventTarget instanceof Node &&
52
+ presentation.surface.contains(eventTarget))) {
53
+ return;
54
+ }
55
+ const generation = this.generation;
56
+ if (this.redirectGeneration === generation)
57
+ return;
58
+ this.redirectGeneration = generation;
59
+ queueMicrotask(() => {
60
+ if (this.redirectGeneration === generation) {
61
+ this.redirectGeneration = null;
62
+ }
63
+ if (this.destroyed ||
64
+ generation !== this.generation ||
65
+ this.current !== presentation ||
66
+ presentation.surface.contains(this.root.activeElement)) {
67
+ return;
68
+ }
69
+ this.focusPresentation(presentation);
70
+ });
71
+ };
72
+ this.onKeyDown = (event) => {
73
+ const presentation = this.current;
74
+ if (!presentation || event.currentTarget !== presentation.surface)
75
+ return;
76
+ if (event.key === "Escape") {
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ presentation.onEscape();
80
+ return;
81
+ }
82
+ if (event.key !== "Tab")
83
+ return;
84
+ const focusable = [
85
+ ...presentation.surface.querySelectorAll(focusableSelector),
86
+ ].filter(isVisibleFocusTarget);
87
+ const first = focusable[0];
88
+ const last = focusable.at(-1);
89
+ if (!first || !last)
90
+ return;
91
+ const active = this.root.activeElement;
92
+ if (!presentation.surface.contains(active)) {
93
+ event.preventDefault();
94
+ (event.shiftKey ? last : first).focus({ preventScroll: true });
95
+ return;
96
+ }
97
+ if (event.shiftKey && active === first) {
98
+ event.preventDefault();
99
+ last.focus({ preventScroll: true });
100
+ }
101
+ else if (!event.shiftKey && active === last) {
102
+ event.preventDefault();
103
+ first.focus({ preventScroll: true });
104
+ }
105
+ };
106
+ }
107
+ present(value) {
108
+ if (this.destroyed)
109
+ return;
110
+ if (!this.current && !isVisibleFocusTarget(this.restoreTarget)) {
111
+ this.restoreTarget = this.activeFocusTarget(value);
112
+ }
113
+ this.current?.surface.removeEventListener("keydown", this.onKeyDown);
114
+ this.current = value;
115
+ this.generation += 1;
116
+ this.redirectGeneration = null;
117
+ const generation = this.generation;
118
+ value.surface.addEventListener("keydown", this.onKeyDown);
119
+ this.bindContainment();
120
+ const immediateTarget = value.initialFocus();
121
+ if (isVisibleFocusTarget(immediateTarget)) {
122
+ immediateTarget.focus({ preventScroll: true });
123
+ }
124
+ queueMicrotask(() => {
125
+ if (this.destroyed ||
126
+ generation !== this.generation ||
127
+ this.current !== value) {
128
+ return;
129
+ }
130
+ if (value.surface.contains(this.root.activeElement))
131
+ return;
132
+ const target = value.initialFocus();
133
+ if (isVisibleFocusTarget(target))
134
+ target.focus({ preventScroll: true });
135
+ });
136
+ }
137
+ dismiss(surface, options = {}) {
138
+ if (this.current?.surface !== surface)
139
+ return;
140
+ const presentation = this.current;
141
+ presentation.surface.removeEventListener("keydown", this.onKeyDown);
142
+ this.current = null;
143
+ this.generation += 1;
144
+ this.redirectGeneration = null;
145
+ this.unbindContainment();
146
+ const generation = this.generation;
147
+ if (options.restore === false) {
148
+ this.restoreTarget = null;
149
+ return;
150
+ }
151
+ const restoreTarget = this.restoreTarget;
152
+ queueMicrotask(() => {
153
+ if (this.destroyed ||
154
+ generation !== this.generation ||
155
+ this.current) {
156
+ return;
157
+ }
158
+ const fallback = presentation.fallbackFocus();
159
+ const target = isVisibleFocusTarget(restoreTarget)
160
+ ? restoreTarget
161
+ : isVisibleFocusTarget(fallback)
162
+ ? fallback
163
+ : null;
164
+ this.restoreTarget = null;
165
+ target?.focus({ preventScroll: true });
166
+ });
167
+ }
168
+ clear(options = {}) {
169
+ const surface = this.current?.surface;
170
+ if (surface) {
171
+ this.dismiss(surface, options);
172
+ return;
173
+ }
174
+ this.generation += 1;
175
+ this.restoreTarget = null;
176
+ }
177
+ destroy() {
178
+ if (this.destroyed)
179
+ return;
180
+ this.destroyed = true;
181
+ this.current?.surface.removeEventListener("keydown", this.onKeyDown);
182
+ this.unbindContainment();
183
+ this.current = null;
184
+ this.restoreTarget = null;
185
+ this.redirectGeneration = null;
186
+ this.generation += 1;
187
+ }
188
+ bindContainment() {
189
+ if (this.containmentBound)
190
+ return;
191
+ this.containmentBound = true;
192
+ this.root.addEventListener("focusin", this.onFocusIn, true);
193
+ document.addEventListener("focusin", this.onFocusIn, true);
194
+ }
195
+ unbindContainment() {
196
+ if (!this.containmentBound)
197
+ return;
198
+ this.containmentBound = false;
199
+ this.root.removeEventListener("focusin", this.onFocusIn, true);
200
+ document.removeEventListener("focusin", this.onFocusIn, true);
201
+ }
202
+ focusPresentation(presentation) {
203
+ const preferred = presentation.initialFocus();
204
+ if (isVisibleFocusTarget(preferred)) {
205
+ preferred.focus({ preventScroll: true });
206
+ return;
207
+ }
208
+ const first = [
209
+ ...presentation.surface.querySelectorAll(focusableSelector),
210
+ ].find(isVisibleFocusTarget);
211
+ first?.focus({ preventScroll: true });
212
+ }
213
+ activeFocusTarget(value) {
214
+ const shadowActive = this.root.activeElement;
215
+ if (shadowActive instanceof HTMLElement)
216
+ return shadowActive;
217
+ const documentActive = document.activeElement;
218
+ if (documentActive instanceof HTMLElement &&
219
+ documentActive !== document.body &&
220
+ documentActive !== document.documentElement &&
221
+ documentActive !== this.host) {
222
+ return documentActive;
223
+ }
224
+ return value.fallbackFocus();
225
+ }
226
+ }
227
+ exports.DialogFocusScope = DialogFocusScope;
@@ -19,7 +19,7 @@ const transitions = new Map([
19
19
  ["connecting", new Set(["active.ready", "active.reconnecting", "recoverable_error", "terminal_error", "ending"])],
20
20
  ["active.reconnecting", new Set(["connecting", "active.ready", "recoverable_error", "terminal_error", "ending"])],
21
21
  ["recoverable_error", new Set(["launcher", "preflight", "requesting_microphone", "connecting", "active.reconnecting", "ending", "terminal_error"])],
22
- ["terminal_error", new Set(["launcher", "ended"])],
22
+ ["terminal_error", new Set(["launcher", "ended", "ending"])],
23
23
  ["ending", new Set(["ended", "terminal_error"])],
24
24
  ["ended", new Set(["launcher"])],
25
25
  ]);
package/cjs/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { App, Options as BrowserOptions } from '@oxvo/browser';
2
2
  import AiLiveAssist from './AiLiveAssist.js';
3
3
  import type { PluginOptions } from './types.js';
4
4
  import { VERSION } from './version.js';
5
- export type { PluginOptions } from './types.js';
5
+ export type { AiGuideControllerState, AiGuideOpenInput, MessengerCompanionConfig, PluginOptions, } from './types.js';
6
6
  export { AiLiveAssist, VERSION };
7
7
  export default function aiLiveAssist(options?: PluginOptions): (app: App | null) => AiLiveAssist | undefined;
8
8
  export declare function installAiLiveAssist(input: {
package/cjs/index.js CHANGED
@@ -18,8 +18,8 @@ function aiLiveAssist(options = {}) {
18
18
  if (document.querySelector('[data-oxvo-ai-live-assist-root]')) {
19
19
  return undefined;
20
20
  }
21
- if (!app.checkRequiredVersion?.('7.3.0')) {
22
- console.warn('OXVO AI Guide requires @oxvo/browser version 7.3.0 or newer.');
21
+ if (!app.checkRequiredVersion?.('7.4.15')) {
22
+ console.warn('OXVO AI Guide requires @oxvo/browser version 7.4.15 or newer.');
23
23
  return undefined;
24
24
  }
25
25
  const instance = new AiLiveAssist_js_1.default(app, options);
package/cjs/media.d.ts CHANGED
@@ -6,9 +6,11 @@ type MediaCallbacks = {
6
6
  onConnectionState: (state: RTCPeerConnectionState) => void;
7
7
  onMicrophoneUnavailable: () => void;
8
8
  };
9
+ type MediaLifecycleCheck = () => boolean;
9
10
  export declare class RealtimeMedia {
10
11
  private readonly client;
11
12
  private readonly callbacks;
13
+ private readonly isCurrent;
12
14
  private connection;
13
15
  private localStream;
14
16
  private dataChannel;
@@ -16,9 +18,14 @@ export declare class RealtimeMedia {
16
18
  private pushActive;
17
19
  private muted;
18
20
  private replacingDevice;
19
- constructor(client: RuntimeClient, callbacks: MediaCallbacks);
20
- prepare(mode: AssistMode): Promise<void>;
21
- connect(result: LaunchResult, mode: AssistMode, signal: AbortSignal): Promise<number>;
21
+ private closed;
22
+ private generation;
23
+ private deviceGeneration;
24
+ private signal;
25
+ private readonly lifetimeAbort;
26
+ constructor(client: RuntimeClient, callbacks: MediaCallbacks, isCurrent?: MediaLifecycleCheck);
27
+ prepare(mode: AssistMode, signal: AbortSignal): Promise<void>;
28
+ connect(result: LaunchResult, mode: AssistMode, signal: AbortSignal, offerIdempotencyKey?: string, expectedSessionState?: 'active_text' | 'active_voice'): Promise<number>;
22
29
  setMuted(muted: boolean): void;
23
30
  setManualTurn(manual: boolean): void;
24
31
  beginPushToTalk(): void;
@@ -30,5 +37,8 @@ export declare class RealtimeMedia {
30
37
  private onDeviceChange;
31
38
  private replaceUnavailableDevice;
32
39
  private waitForIce;
40
+ private active;
41
+ private assertActive;
42
+ private assertReplacementActive;
33
43
  }
34
44
  export {};
package/cjs/media.js CHANGED
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RealtimeMedia = void 0;
4
+ const abortError = () => new DOMException('The operation was aborted.', 'AbortError');
5
+ const stopStream = (stream) => {
6
+ for (const track of stream?.getTracks() ?? [])
7
+ track.stop();
8
+ };
4
9
  class RealtimeMedia {
5
- constructor(client, callbacks) {
10
+ constructor(client, callbacks, isCurrent = () => true) {
6
11
  this.client = client;
7
12
  this.callbacks = callbacks;
13
+ this.isCurrent = isCurrent;
8
14
  this.connection = null;
9
15
  this.localStream = null;
10
16
  this.dataChannel = null;
@@ -12,17 +18,32 @@ class RealtimeMedia {
12
18
  this.pushActive = false;
13
19
  this.muted = false;
14
20
  this.replacingDevice = false;
21
+ this.closed = false;
22
+ this.generation = 0;
23
+ this.deviceGeneration = 0;
24
+ this.signal = null;
25
+ this.lifetimeAbort = new AbortController();
15
26
  this.onDeviceChange = () => {
27
+ if (!this.connection && this.localStream) {
28
+ const track = this.localStream.getAudioTracks()[0];
29
+ if (!track || track.readyState !== 'live') {
30
+ this.callbacks.onMicrophoneUnavailable();
31
+ }
32
+ return;
33
+ }
16
34
  void this.replaceUnavailableDevice();
17
35
  };
18
36
  }
19
- async prepare(mode) {
37
+ async prepare(mode, signal) {
38
+ this.signal = signal;
39
+ const generation = this.generation;
40
+ this.assertActive(signal, generation);
20
41
  if (mode !== 'voice')
21
42
  return;
22
43
  if (!navigator.mediaDevices?.getUserMedia) {
23
44
  throw new Error('Microphone access is not available in this browser.');
24
45
  }
25
- this.localStream = await navigator.mediaDevices.getUserMedia({
46
+ const stream = await navigator.mediaDevices.getUserMedia({
26
47
  audio: {
27
48
  autoGainControl: true,
28
49
  echoCancellation: true,
@@ -30,12 +51,23 @@ class RealtimeMedia {
30
51
  },
31
52
  video: false,
32
53
  });
54
+ try {
55
+ this.assertActive(signal, generation);
56
+ }
57
+ catch (error) {
58
+ stopStream(stream);
59
+ throw error;
60
+ }
61
+ this.localStream = stream;
33
62
  navigator.mediaDevices.addEventListener?.('devicechange', this.onDeviceChange);
34
63
  this.bindTrackEnd();
35
64
  this.applyTrackState();
36
65
  this.callbacks.onLocalStream(this.localStream);
37
66
  }
38
- async connect(result, mode, signal) {
67
+ async connect(result, mode, signal, offerIdempotencyKey, expectedSessionState) {
68
+ this.signal = signal;
69
+ const generation = this.generation;
70
+ this.assertActive(signal, generation);
39
71
  const connection = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
40
72
  this.connection = connection;
41
73
  this.dataChannel = connection.createDataChannel('oai-events', {
@@ -54,21 +86,40 @@ class RealtimeMedia {
54
86
  connection.addTransceiver('audio', { direction: 'recvonly' });
55
87
  }
56
88
  connection.addEventListener('track', (event) => {
89
+ if (!this.active(signal, generation) ||
90
+ this.connection !== connection) {
91
+ return;
92
+ }
57
93
  const stream = event.streams[0];
58
94
  if (stream)
59
95
  this.callbacks.onRemoteStream(stream);
60
96
  });
61
97
  connection.addEventListener('connectionstatechange', () => {
98
+ if (!this.active(signal, generation) ||
99
+ this.connection !== connection) {
100
+ return;
101
+ }
62
102
  this.callbacks.onConnectionState(connection.connectionState);
63
103
  });
64
104
  const offer = await connection.createOffer();
105
+ this.assertActive(signal, generation);
65
106
  await connection.setLocalDescription(offer);
107
+ this.assertActive(signal, generation);
66
108
  await this.waitForIce(connection, signal);
109
+ this.assertActive(signal, generation);
67
110
  const localSdp = connection.localDescription?.sdp;
68
111
  if (!localSdp)
69
112
  throw new Error('The browser could not create a realtime offer.');
70
- const response = await this.client.offer(result, localSdp, signal);
113
+ const response = offerIdempotencyKey
114
+ ? await this.client.offer(result, localSdp, signal, offerIdempotencyKey)
115
+ : await this.client.offer(result, localSdp, signal);
116
+ this.assertActive(signal, generation);
117
+ if (expectedSessionState &&
118
+ response.sessionState !== expectedSessionState) {
119
+ throw new Error('The realtime session activated in an unexpected mode.');
120
+ }
71
121
  await connection.setRemoteDescription({ type: 'answer', sdp: response.answerSdp });
122
+ this.assertActive(signal, generation);
72
123
  return response.fencingToken;
73
124
  }
74
125
  setMuted(muted) {
@@ -91,23 +142,28 @@ class RealtimeMedia {
91
142
  this.applyTrackState();
92
143
  }
93
144
  switchToTextOnly() {
145
+ this.deviceGeneration += 1;
94
146
  for (const sender of this.connection?.getSenders() ?? []) {
95
147
  if (sender.track?.kind === 'audio') {
96
148
  sender.track.stop();
97
149
  void sender.replaceTrack(null);
98
150
  }
99
151
  }
100
- for (const track of this.localStream?.getTracks() ?? [])
101
- track.stop();
152
+ stopStream(this.localStream);
102
153
  this.localStream = null;
103
154
  this.callbacks.onLocalStream(null);
104
155
  navigator.mediaDevices?.removeEventListener?.('devicechange', this.onDeviceChange);
105
156
  }
106
157
  close() {
158
+ if (!this.closed) {
159
+ this.closed = true;
160
+ this.generation += 1;
161
+ this.deviceGeneration += 1;
162
+ this.lifetimeAbort.abort();
163
+ }
107
164
  this.dataChannel?.close();
108
165
  this.dataChannel = null;
109
- for (const track of this.localStream?.getTracks() ?? [])
110
- track.stop();
166
+ stopStream(this.localStream);
111
167
  this.localStream = null;
112
168
  this.callbacks.onLocalStream(null);
113
169
  navigator.mediaDevices?.removeEventListener?.('devicechange', this.onDeviceChange);
@@ -126,8 +182,16 @@ class RealtimeMedia {
126
182
  }
127
183
  }
128
184
  async replaceUnavailableDevice() {
129
- if (this.replacingDevice || !this.connection || !this.localStream)
185
+ const signal = this.signal;
186
+ const generation = this.generation;
187
+ const deviceGeneration = this.deviceGeneration;
188
+ if (!signal ||
189
+ !this.active(signal, generation) ||
190
+ this.replacingDevice ||
191
+ !this.connection ||
192
+ !this.localStream) {
130
193
  return;
194
+ }
131
195
  const current = this.localStream.getAudioTracks()[0];
132
196
  if (!current) {
133
197
  this.callbacks.onMicrophoneUnavailable();
@@ -136,6 +200,7 @@ class RealtimeMedia {
136
200
  if (current.readyState === 'live') {
137
201
  try {
138
202
  const devices = await navigator.mediaDevices.enumerateDevices();
203
+ this.assertReplacementActive(signal, generation, deviceGeneration);
139
204
  const currentId = current.getSettings().deviceId;
140
205
  if (!currentId ||
141
206
  devices.some((device) => device.kind === 'audioinput' && device.deviceId === currentId)) {
@@ -147,8 +212,11 @@ class RealtimeMedia {
147
212
  }
148
213
  }
149
214
  this.replacingDevice = true;
215
+ let replacement = null;
150
216
  try {
151
- const replacement = await navigator.mediaDevices.getUserMedia({
217
+ const connection = this.connection;
218
+ const priorStream = this.localStream;
219
+ replacement = await navigator.mediaDevices.getUserMedia({
152
220
  audio: {
153
221
  autoGainControl: true,
154
222
  echoCancellation: true,
@@ -156,28 +224,41 @@ class RealtimeMedia {
156
224
  },
157
225
  video: false,
158
226
  });
227
+ this.assertReplacementActive(signal, generation, deviceGeneration);
159
228
  const track = replacement.getAudioTracks()[0];
160
- const sender = this.connection
229
+ const sender = connection
161
230
  .getSenders()
162
231
  .find((candidate) => candidate.track?.kind === 'audio');
163
232
  if (!track || !sender)
164
233
  throw new Error('No replacement microphone track.');
165
234
  await sender.replaceTrack(track);
166
- for (const prior of this.localStream.getTracks())
167
- prior.stop();
168
- this.localStream = replacement;
235
+ this.assertReplacementActive(signal, generation, deviceGeneration);
236
+ if (this.connection !== connection || this.localStream !== priorStream) {
237
+ throw abortError();
238
+ }
239
+ stopStream(priorStream);
240
+ const activeReplacement = replacement;
241
+ this.localStream = activeReplacement;
242
+ replacement = null;
169
243
  this.bindTrackEnd();
170
244
  this.applyTrackState();
171
- this.callbacks.onLocalStream(replacement);
245
+ this.callbacks.onLocalStream(activeReplacement);
172
246
  }
173
- catch {
174
- this.callbacks.onMicrophoneUnavailable();
247
+ catch (error) {
248
+ stopStream(replacement);
249
+ if (!(error instanceof DOMException && error.name === 'AbortError') &&
250
+ this.active(signal, generation)) {
251
+ this.callbacks.onMicrophoneUnavailable();
252
+ }
175
253
  }
176
254
  finally {
177
255
  this.replacingDevice = false;
178
256
  }
179
257
  }
180
258
  waitForIce(connection, signal) {
259
+ if (signal.aborted || this.lifetimeAbort.signal.aborted) {
260
+ return Promise.reject(abortError());
261
+ }
181
262
  if (connection.iceGatheringState === 'complete')
182
263
  return Promise.resolve();
183
264
  return new Promise((resolve, reject) => {
@@ -188,12 +269,13 @@ class RealtimeMedia {
188
269
  };
189
270
  const onAbort = () => {
190
271
  cleanup();
191
- reject(new DOMException('The operation was aborted.', 'AbortError'));
272
+ reject(abortError());
192
273
  };
193
274
  const cleanup = () => {
194
275
  clearTimeout(timeout);
195
276
  connection.removeEventListener('icegatheringstatechange', onState);
196
277
  signal.removeEventListener('abort', onAbort);
278
+ this.lifetimeAbort.signal.removeEventListener('abort', onAbort);
197
279
  };
198
280
  function finish() {
199
281
  cleanup();
@@ -201,7 +283,31 @@ class RealtimeMedia {
201
283
  }
202
284
  connection.addEventListener('icegatheringstatechange', onState);
203
285
  signal.addEventListener('abort', onAbort, { once: true });
286
+ this.lifetimeAbort.signal.addEventListener('abort', onAbort, { once: true });
204
287
  });
205
288
  }
289
+ active(signal, generation) {
290
+ if (signal.aborted ||
291
+ this.lifetimeAbort.signal.aborted ||
292
+ this.closed ||
293
+ generation !== this.generation) {
294
+ return false;
295
+ }
296
+ try {
297
+ return this.isCurrent();
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ assertActive(signal, generation) {
304
+ if (!this.active(signal, generation))
305
+ throw abortError();
306
+ }
307
+ assertReplacementActive(signal, generation, deviceGeneration) {
308
+ this.assertActive(signal, generation);
309
+ if (deviceGeneration !== this.deviceGeneration)
310
+ throw abortError();
311
+ }
206
312
  }
207
313
  exports.RealtimeMedia = RealtimeMedia;