@ikonai/sdk 1.8.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/app-messaging.d.ts +13 -2
  2. package/app-messaging.d.ts.map +1 -0
  3. package/assets/audio-playback-worker-BA5LbdyM.js +1 -0
  4. package/assets/protocol-worker-Byc-OmNW.js +3 -0
  5. package/assets/video-capture-worker-DRYxkwR0.js +1 -0
  6. package/assets/video-playback-worker-v179IJ-C.js +1 -0
  7. package/assets/viseme-worker-CmvzHAbf.js +1 -0
  8. package/audio-constants-0i9B4zLJ.js +4 -0
  9. package/backend/account-functions.d.ts +22 -0
  10. package/backend/account-functions.d.ts.map +1 -0
  11. package/backend/user-account.d.ts +58 -0
  12. package/backend/user-account.d.ts.map +1 -0
  13. package/capabilities-ljRyAKpA.js +248 -0
  14. package/capture/capture-functions.d.ts +34 -0
  15. package/capture/capture-functions.d.ts.map +1 -0
  16. package/capture/view-capture.d.ts +35 -0
  17. package/capture/view-capture.d.ts.map +1 -0
  18. package/channel/channel-manager.d.ts +3 -2
  19. package/channel/channel-manager.d.ts.map +1 -0
  20. package/channel/channel.d.ts +6 -2
  21. package/channel/channel.d.ts.map +1 -0
  22. package/channel/index.d.ts +1 -0
  23. package/channel/index.d.ts.map +1 -0
  24. package/channel-manager-ehgy3BJp.js +878 -0
  25. package/client/connection-state.d.ts +1 -0
  26. package/client/connection-state.d.ts.map +1 -0
  27. package/client/endpoint-selector.d.ts +2 -1
  28. package/client/endpoint-selector.d.ts.map +1 -0
  29. package/client/ikon-client-config.d.ts +14 -1
  30. package/client/ikon-client-config.d.ts.map +1 -0
  31. package/client/ikon-client.d.ts +146 -5
  32. package/client/ikon-client.d.ts.map +1 -0
  33. package/connection/authenticator.d.ts +6 -1
  34. package/connection/authenticator.d.ts.map +1 -0
  35. package/connection/urls.d.ts +1 -0
  36. package/connection/urls.d.ts.map +1 -0
  37. package/consent/consent-api.d.ts +61 -0
  38. package/consent/consent-api.d.ts.map +1 -0
  39. package/dist-BrkppKKs.js +801 -0
  40. package/errors/index.d.ts +11 -0
  41. package/errors/index.d.ts.map +1 -0
  42. package/feedback/feedback-api.d.ts +63 -0
  43. package/feedback/feedback-api.d.ts.map +1 -0
  44. package/functions/browser-functions.d.ts +2 -1
  45. package/functions/browser-functions.d.ts.map +1 -0
  46. package/functions/download-functions.d.ts +20 -0
  47. package/functions/download-functions.d.ts.map +1 -0
  48. package/functions/function-registry.d.ts +2 -1
  49. package/functions/function-registry.d.ts.map +1 -0
  50. package/functions/index.d.ts +2 -0
  51. package/functions/index.d.ts.map +1 -0
  52. package/functions/media-capture-functions.d.ts +1 -0
  53. package/functions/media-capture-functions.d.ts.map +1 -0
  54. package/functions/sound-functions.d.ts +3 -1
  55. package/functions/sound-functions.d.ts.map +1 -0
  56. package/functions/type-conversion.d.ts +1 -0
  57. package/functions/type-conversion.d.ts.map +1 -0
  58. package/functions/types.d.ts +2 -1
  59. package/functions/types.d.ts.map +1 -0
  60. package/ikon-media-BQjL59S_.js +2074 -0
  61. package/ikon-media-capture-BiGydwdg.js +1422 -0
  62. package/index.d.ts +24 -11
  63. package/index.d.ts.map +1 -0
  64. package/index.js +6707 -10749
  65. package/log-type-DewKVFaU.js +832 -0
  66. package/logger-gjegmH6K.js +80 -0
  67. package/login/index.d.ts +1 -0
  68. package/login/index.d.ts.map +1 -0
  69. package/login/login-error-interceptor.d.ts +1 -0
  70. package/login/login-error-interceptor.d.ts.map +1 -0
  71. package/login/login-store.d.ts +1 -0
  72. package/login/login-store.d.ts.map +1 -0
  73. package/login/reprovision.d.ts +1 -0
  74. package/login/reprovision.d.ts.map +1 -0
  75. package/media/audio-constants.d.ts +1 -0
  76. package/media/audio-constants.d.ts.map +1 -0
  77. package/media/capabilities.d.ts +1 -0
  78. package/media/capabilities.d.ts.map +1 -0
  79. package/media/device-detection.d.ts +1 -0
  80. package/media/device-detection.d.ts.map +1 -0
  81. package/media/ikon-audio-capture.d.ts +23 -0
  82. package/media/ikon-audio-capture.d.ts.map +1 -0
  83. package/media/ikon-audio-playback.d.ts +3 -1
  84. package/media/ikon-audio-playback.d.ts.map +1 -0
  85. package/media/ikon-image-capture.d.ts +1 -0
  86. package/media/ikon-image-capture.d.ts.map +1 -0
  87. package/media/ikon-media-capture.d.ts +1 -0
  88. package/media/ikon-media-capture.d.ts.map +1 -0
  89. package/media/ikon-media.d.ts +1 -0
  90. package/media/ikon-media.d.ts.map +1 -0
  91. package/media/ikon-video-capture.d.ts +9 -0
  92. package/media/ikon-video-capture.d.ts.map +1 -0
  93. package/media/ikon-video-playback.d.ts +2 -1
  94. package/media/ikon-video-playback.d.ts.map +1 -0
  95. package/media/index.d.ts +1 -0
  96. package/media/index.d.ts.map +1 -0
  97. package/media/media-permission.d.ts +1 -0
  98. package/media/media-permission.d.ts.map +1 -0
  99. package/media/media-session-helper.d.ts +1 -0
  100. package/media/media-session-helper.d.ts.map +1 -0
  101. package/media/ring-buffer.d.ts +1 -0
  102. package/media/ring-buffer.d.ts.map +1 -0
  103. package/media/video-constants.d.ts +1 -0
  104. package/media/video-constants.d.ts.map +1 -0
  105. package/media/viseme-tracker.d.ts +2 -1
  106. package/media/viseme-tracker.d.ts.map +1 -0
  107. package/media-session-helper-Do-OxyU6.js +51 -0
  108. package/package.json +1 -1
  109. package/reactive/reactive-registry.d.ts +1 -0
  110. package/reactive/reactive-registry.d.ts.map +1 -0
  111. package/sha2-DPTGfEM2.js +261 -0
  112. package/storage.d.ts +2 -1
  113. package/storage.d.ts.map +1 -0
  114. package/transport/http-stream-transport.d.ts +17 -1
  115. package/transport/http-stream-transport.d.ts.map +1 -0
  116. package/transport/keepalive-watchdog.d.ts +32 -0
  117. package/transport/keepalive-watchdog.d.ts.map +1 -0
  118. package/transport/message-framer.d.ts +1 -0
  119. package/transport/message-framer.d.ts.map +1 -0
  120. package/transport/transport-probe.d.ts +2 -1
  121. package/transport/transport-probe.d.ts.map +1 -0
  122. package/transport/transport.d.ts +16 -1
  123. package/transport/transport.d.ts.map +1 -0
  124. package/transport/web-socket-transport.d.ts +4 -1
  125. package/transport/web-socket-transport.d.ts.map +1 -0
  126. package/transport/web-transport-support.d.ts +5 -0
  127. package/transport/web-transport-support.d.ts.map +1 -0
  128. package/transport/web-transport-transport.d.ts +4 -8
  129. package/transport/web-transport-transport.d.ts.map +1 -0
  130. package/upload/file-upload.d.ts +62 -0
  131. package/upload/file-upload.d.ts.map +1 -0
  132. package/utils/abort-signal.d.ts +1 -0
  133. package/utils/abort-signal.d.ts.map +1 -0
  134. package/utils/app-routes.d.ts +1 -0
  135. package/utils/app-routes.d.ts.map +1 -0
  136. package/utils/boot-signal.d.ts +1 -0
  137. package/utils/boot-signal.d.ts.map +1 -0
  138. package/utils/browser-compat.d.ts +1 -0
  139. package/utils/browser-compat.d.ts.map +1 -0
  140. package/utils/browser-support.d.ts +1 -0
  141. package/utils/browser-support.d.ts.map +1 -0
  142. package/utils/client-identity.d.ts +14 -0
  143. package/utils/client-identity.d.ts.map +1 -0
  144. package/utils/connect-timeline.d.ts +1 -0
  145. package/utils/connect-timeline.d.ts.map +1 -0
  146. package/utils/debug-mode.d.ts +1 -0
  147. package/utils/debug-mode.d.ts.map +1 -0
  148. package/utils/deferred.d.ts +1 -0
  149. package/utils/deferred.d.ts.map +1 -0
  150. package/utils/delay.d.ts +1 -0
  151. package/utils/delay.d.ts.map +1 -0
  152. package/utils/embedded-oauth.d.ts +1 -0
  153. package/utils/embedded-oauth.d.ts.map +1 -0
  154. package/utils/external-link.d.ts +1 -0
  155. package/utils/external-link.d.ts.map +1 -0
  156. package/utils/inspect-mode.d.ts +1 -0
  157. package/utils/inspect-mode.d.ts.map +1 -0
  158. package/utils/log-sender.d.ts +1 -0
  159. package/utils/log-sender.d.ts.map +1 -0
  160. package/utils/log-sink.d.ts +1 -0
  161. package/utils/log-sink.d.ts.map +1 -0
  162. package/utils/logger.d.ts +1 -0
  163. package/utils/logger.d.ts.map +1 -0
  164. package/utils/oauth-start-url.d.ts +22 -0
  165. package/utils/oauth-start-url.d.ts.map +1 -0
  166. package/utils/opcode-names.d.ts +1 -0
  167. package/utils/opcode-names.d.ts.map +1 -0
  168. package/utils/parent-log-forward.d.ts +23 -0
  169. package/utils/parent-log-forward.d.ts.map +1 -0
  170. package/utils/pkce.d.ts +1 -0
  171. package/utils/pkce.d.ts.map +1 -0
  172. package/utils/platform.d.ts +1 -0
  173. package/utils/platform.d.ts.map +1 -0
  174. package/utils/query-params.d.ts +21 -0
  175. package/utils/query-params.d.ts.map +1 -0
  176. package/utils/user-gesture.d.ts +1 -0
  177. package/utils/user-gesture.d.ts.map +1 -0
  178. package/utils/uuid.d.ts +1 -0
  179. package/utils/uuid.d.ts.map +1 -0
  180. package/viseme-tracker-CAtczW00.js +183 -0
  181. package/web-transport-support-Cdyk4mQe.js +229 -0
  182. package/webrtc/index.d.ts +1 -0
  183. package/webrtc/index.d.ts.map +1 -0
  184. package/webrtc/webrtc-config.d.ts +1 -0
  185. package/webrtc/webrtc-config.d.ts.map +1 -0
  186. package/webrtc/webrtc-connection.d.ts +10 -0
  187. package/webrtc/webrtc-connection.d.ts.map +1 -0
  188. package/webrtc/webrtc-reachability.d.ts +1 -0
  189. package/webrtc/webrtc-reachability.d.ts.map +1 -0
  190. package/webrtc/webrtc-signaling.d.ts +19 -1
  191. package/webrtc/webrtc-signaling.d.ts.map +1 -0
  192. package/worker/audio-capture-worker.d.ts +1 -0
  193. package/worker/audio-capture-worker.d.ts.map +1 -0
  194. package/worker/audio-playback-worker.d.ts +2 -1
  195. package/worker/audio-playback-worker.d.ts.map +1 -0
  196. package/worker/protocol-worker.d.ts +1 -0
  197. package/worker/protocol-worker.d.ts.map +1 -0
  198. package/worker/video-capture-worker.d.ts +1 -0
  199. package/worker/video-capture-worker.d.ts.map +1 -0
  200. package/worker/video-playback-worker.d.ts +2 -1
  201. package/worker/video-playback-worker.d.ts.map +1 -0
  202. package/worker/viseme-worker.d.ts +2 -1
  203. package/worker/viseme-worker.d.ts.map +1 -0
  204. package/assets/audio-playback-worker-BXZR_9iK.js +0 -1
  205. package/assets/protocol-worker-PNQ9dfGq.js +0 -3
  206. package/assets/video-capture-worker-mvwKyjOn.js +0 -1
  207. package/assets/video-playback-worker-DqNjooZJ.js +0 -1
  208. package/assets/viseme-worker-D0AKWUTO.js +0 -1
  209. package/capabilities-xDnE0vqq.js +0 -105
  210. package/vite.config.d.ts +0 -2
@@ -0,0 +1,2074 @@
1
+ import { n as e } from "./log-type-DewKVFaU.js";
2
+ import { c as t, n } from "./logger-gjegmH6K.js";
3
+ import { a as r, f as i, g as a, i as o, l as s, n as c, r as l, t as u } from "./capabilities-ljRyAKpA.js";
4
+ import { t as d } from "./media-session-helper-Do-OxyU6.js";
5
+ import { i as f, n as p, r as m, t as h } from "./audio-constants-0i9B4zLJ.js";
6
+ //#region src/worker/audio-playback-worker.ts?worker
7
+ function g(e) {
8
+ return new Worker(new URL("assets/audio-playback-worker-BA5LbdyM.js", import.meta.url).href, {
9
+ type: "module",
10
+ name: e?.name
11
+ });
12
+ }
13
+ //#endregion
14
+ //#region src/media/ikon-audio-playback.ts
15
+ var _ = n("IkonAudioPlayback"), v = {
16
+ enabled: !1,
17
+ statusIntervalMs: 1e3
18
+ };
19
+ function y(e) {
20
+ return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
21
+ }
22
+ function b(e) {
23
+ return `
24
+ // Audio buffer constants
25
+ const OUTPUT_SAMPLE_RATE_HZ = ${e.outputSampleRateHz};
26
+ const DEFAULT_FRAME_SAMPLES_PER_CHANNEL = Math.max(1, Math.round(OUTPUT_SAMPLE_RATE_HZ * 0.02));
27
+ const BUFFER_TARGET_MIN_MS = 120;
28
+ const BUFFER_TARGET_MAX_MS = 300;
29
+ const JITTER_MULTIPLIER = 4;
30
+ const LOCAL_BUFFER_MAX_MS = ${h};
31
+ const CONTROL_PERIOD_MS = 200;
32
+ const UNDERRUN_JUMP_MS = 100;
33
+ const STABLE_TIME_TO_DECREASE_MS = ${f};
34
+ const DECREASE_STEP_MS = 10;
35
+ const DEFAULT_STATS_INTERVAL_SAMPLES = OUTPUT_SAMPLE_RATE_HZ;
36
+ const PLAYBACK_REPORT_INTERVAL_SAMPLES = Math.round(OUTPUT_SAMPLE_RATE_HZ / 2);
37
+
38
+ // Pre-computed reciprocals for division-free calculations
39
+ const INV_OUTPUT_SAMPLE_RATE_HZ = 1 / OUTPUT_SAMPLE_RATE_HZ;
40
+ const MS_PER_SAMPLE = 1000 * INV_OUTPUT_SAMPLE_RATE_HZ;
41
+
42
+ // Fade envelope constants
43
+ const FADE_IN_DURATION_MS = 100;
44
+ const FADE_OUT_DURATION_MS = 50;
45
+ const FADE_IN_DURATION_SAMPLES = Math.round(OUTPUT_SAMPLE_RATE_HZ * FADE_IN_DURATION_MS / 1000);
46
+ const FADE_OUT_DURATION_SAMPLES = Math.round(OUTPUT_SAMPLE_RATE_HZ * FADE_OUT_DURATION_MS / 1000);
47
+ const FADE_IN_INCREMENT = 1.0 / FADE_IN_DURATION_SAMPLES;
48
+ const FADE_OUT_INCREMENT = 1.0 / FADE_OUT_DURATION_SAMPLES;
49
+
50
+ // Render quantum size (standard Web Audio API value)
51
+ const RENDER_QUANTUM_SIZE = 128;
52
+
53
+ // Buffer reduction crossfade constants (align to quantum boundary to avoid partial quanta)
54
+ const BUFFER_REDUCTION_CROSSFADE_MS = 20;
55
+ const BUFFER_REDUCTION_CROSSFADE_SAMPLES_RAW = Math.round(OUTPUT_SAMPLE_RATE_HZ * BUFFER_REDUCTION_CROSSFADE_MS / 1000);
56
+ const BUFFER_REDUCTION_CROSSFADE_SAMPLES = Math.ceil(BUFFER_REDUCTION_CROSSFADE_SAMPLES_RAW / RENDER_QUANTUM_SIZE) * RENDER_QUANTUM_SIZE;
57
+
58
+ class RingBuffer {
59
+ static getStorageForCapacity(capacity, type) {
60
+ const indexBytes = Int32Array.BYTES_PER_ELEMENT * 2;
61
+ const dataBytes = type.BYTES_PER_ELEMENT * capacity;
62
+ return new SharedArrayBuffer(indexBytes + dataBytes);
63
+ }
64
+
65
+ constructor(sharedArrayBuffer, type, channels = 1) {
66
+ this.buf = sharedArrayBuffer;
67
+ this.readWrite = new Int32Array(sharedArrayBuffer, 0, 2);
68
+ const dataOffset = Int32Array.BYTES_PER_ELEMENT * 2;
69
+ const dataLength = (sharedArrayBuffer.byteLength - dataOffset) / Float32Array.BYTES_PER_ELEMENT;
70
+ this.storage = new type(sharedArrayBuffer, dataOffset, dataLength);
71
+ this.channels = channels;
72
+ }
73
+
74
+ get capacity() {
75
+ return this.storage.length;
76
+ }
77
+
78
+ availableRead() {
79
+ const read = Atomics.load(this.readWrite, 0);
80
+ const write = Atomics.load(this.readWrite, 1);
81
+ if (write >= read) return write - read;
82
+ return this.capacity - (read - write);
83
+ }
84
+
85
+ pop(target) {
86
+ const rawReadable = Math.min(this.availableRead(), target.length);
87
+ const readable = Math.floor(rawReadable / this.channels) * this.channels;
88
+ if (readable <= 0) {
89
+ target.fill(0);
90
+ return 0;
91
+ }
92
+
93
+ let read = Atomics.load(this.readWrite, 0);
94
+ const firstPart = Math.min(readable, this.capacity - read);
95
+ for (let i = 0; i < firstPart; i++) {
96
+ target[i] = this.storage[read + i];
97
+ }
98
+ const remaining = readable - firstPart;
99
+ if (remaining > 0) {
100
+ for (let i = 0; i < remaining; i++) {
101
+ target[firstPart + i] = this.storage[i];
102
+ }
103
+ }
104
+ if (readable < target.length) {
105
+ target.fill(0, readable);
106
+ }
107
+ read = (read + readable) % this.capacity;
108
+ Atomics.store(this.readWrite, 0, read);
109
+ return readable;
110
+ }
111
+
112
+ skip(count) {
113
+ const rawReadable = Math.min(this.availableRead(), count);
114
+ const readable = Math.floor(rawReadable / this.channels) * this.channels;
115
+ if (readable <= 0) return 0;
116
+
117
+ let read = Atomics.load(this.readWrite, 0);
118
+ read = (read + readable) % this.capacity;
119
+ Atomics.store(this.readWrite, 0, read);
120
+ return readable;
121
+ }
122
+
123
+ flush() {
124
+ Atomics.store(this.readWrite, 0, 0);
125
+ Atomics.store(this.readWrite, 1, 0);
126
+ }
127
+ }
128
+
129
+ // Simple queue buffer for non-SAB fallback
130
+ class QueueBuffer {
131
+ constructor(maxSamples, channels = 1) {
132
+ this.chunks = [];
133
+ this.headIndex = 0;
134
+ this.offset = 0;
135
+ this.maxSamples = maxSamples;
136
+ this.totalSamples = 0;
137
+ this.channels = channels;
138
+ this.needsCompaction = false;
139
+ }
140
+
141
+ available() {
142
+ return this.totalSamples;
143
+ }
144
+
145
+ availableRead() {
146
+ return this.totalSamples;
147
+ }
148
+
149
+ compact() {
150
+ if (!this.needsCompaction || this.headIndex === 0) return;
151
+ const activeCount = this.chunks.length - this.headIndex;
152
+ for (let i = 0; i < activeCount; i++) {
153
+ this.chunks[i] = this.chunks[this.headIndex + i];
154
+ }
155
+ this.chunks.length = activeCount;
156
+ this.headIndex = 0;
157
+ this.needsCompaction = false;
158
+ }
159
+
160
+ push(chunk) {
161
+ this.chunks.push(chunk);
162
+ this.totalSamples += chunk.length;
163
+ if (typeof this.maxSamples === 'number' && this.maxSamples > 0) {
164
+ if (this.totalSamples > this.maxSamples) {
165
+ this.skip(this.totalSamples - this.maxSamples);
166
+ }
167
+ }
168
+ }
169
+
170
+ pop(target) {
171
+ if (this.totalSamples <= 0) {
172
+ target.fill(0);
173
+ return 0;
174
+ }
175
+
176
+ const rawRequired = Math.min(target.length, this.totalSamples);
177
+ const required = Math.floor(rawRequired / this.channels) * this.channels;
178
+ if (required <= 0) {
179
+ target.fill(0);
180
+ return 0;
181
+ }
182
+
183
+ let written = 0;
184
+ while (written < required && this.headIndex < this.chunks.length) {
185
+ const current = this.chunks[this.headIndex];
186
+ const available = current.length - this.offset;
187
+ const toCopy = Math.min(available, required - written);
188
+ for (let i = 0; i < toCopy; i++) {
189
+ target[written + i] = current[this.offset + i];
190
+ }
191
+ written += toCopy;
192
+ this.offset += toCopy;
193
+ this.totalSamples -= toCopy;
194
+ if (this.offset >= current.length) {
195
+ this.headIndex += 1;
196
+ this.offset = 0;
197
+ }
198
+ }
199
+
200
+ if (written < target.length) {
201
+ target.fill(0, written);
202
+ }
203
+
204
+ if (this.headIndex > 32 && this.headIndex > this.chunks.length / 2) {
205
+ this.needsCompaction = true;
206
+ }
207
+
208
+ return written;
209
+ }
210
+
211
+ skip(count) {
212
+ if (this.totalSamples <= 0 || count <= 0) return 0;
213
+
214
+ const alignedCount = Math.floor(count / this.channels) * this.channels;
215
+ if (alignedCount <= 0) return 0;
216
+
217
+ let remaining = alignedCount;
218
+ while (remaining > 0 && this.headIndex < this.chunks.length) {
219
+ const current = this.chunks[this.headIndex];
220
+ const available = current.length - this.offset;
221
+ if (remaining < available) {
222
+ this.offset += remaining;
223
+ this.totalSamples -= remaining;
224
+ return alignedCount;
225
+ }
226
+ remaining -= available;
227
+ this.totalSamples -= available;
228
+ this.headIndex += 1;
229
+ this.offset = 0;
230
+ }
231
+
232
+ if (this.headIndex > 32 && this.headIndex > this.chunks.length / 2) {
233
+ this.needsCompaction = true;
234
+ }
235
+ return alignedCount - remaining;
236
+ }
237
+
238
+ flush() {
239
+ this.chunks.length = 0;
240
+ this.headIndex = 0;
241
+ this.offset = 0;
242
+ this.totalSamples = 0;
243
+ this.needsCompaction = false;
244
+ }
245
+ }
246
+
247
+ class IkonAudioPlayerProcessor extends AudioWorkletProcessor {
248
+ constructor(options) {
249
+ super();
250
+ const cfg = options?.processorOptions?.outputAudioConfig || {};
251
+ const outputChannels = options?.processorOptions?.outputChannels;
252
+ this.channels = typeof outputChannels === 'number' && outputChannels > 0 ? outputChannels : ${e.outputChannels};
253
+ this.useSharedArrayBuffer = options?.processorOptions?.useSharedArrayBuffer !== false;
254
+ this.queueMaxSamples = Math.floor((OUTPUT_SAMPLE_RATE_HZ * this.channels * LOCAL_BUFFER_MAX_MS) / 1000);
255
+ this.suspendedMaxSamples = Math.floor((OUTPUT_SAMPLE_RATE_HZ * this.channels * BUFFER_TARGET_MIN_MS) / 1000);
256
+ this.isPlaybackActive = false;
257
+
258
+ // Diagnostics config
259
+ const diagCfg = cfg.diagnostics || {};
260
+ this.diagnosticsEnabled = diagCfg.enabled === true;
261
+ const statusIntervalMs = typeof diagCfg.statusIntervalMs === 'number' && diagCfg.statusIntervalMs > 0 ? diagCfg.statusIntervalMs : 1000;
262
+ this.statsIntervalSamples = Math.max(1, Math.round((OUTPUT_SAMPLE_RATE_HZ * statusIntervalMs) / 1000));
263
+
264
+ // Normalization config (hardcoded)
265
+ this.normalizationEnabled = true;
266
+
267
+ // Stream states for audio buffer
268
+ this.streams = new Map();
269
+ this.ringBuffers = new Map();
270
+ this.queueBuffers = new Map();
271
+ this.tempInterleaved = new Float32Array(RENDER_QUANTUM_SIZE * this.channels);
272
+
273
+ // Pre-allocated crossfade buffers for buffer reduction (no allocations in render path)
274
+ const crossfadeSamples = BUFFER_REDUCTION_CROSSFADE_SAMPLES * this.channels;
275
+ this.crossfadePreSkip = new Float32Array(crossfadeSamples);
276
+ this.crossfadePostSkip = new Float32Array(crossfadeSamples);
277
+ this.crossfadeResult = new Float32Array(crossfadeSamples);
278
+ this.crossfadeOffset = 0;
279
+ this.crossfadeRemaining = 0;
280
+
281
+ // Stats reporting
282
+ this.samplesSinceLastStats = 0;
283
+ this.lastControlTime = 0;
284
+ this.drainedStreamIds = [];
285
+ this.activeStreamIds = [];
286
+ this.activeStreams = [];
287
+ this.activeStreamIdIndices = new Map();
288
+
289
+ // Pre-allocated message objects to avoid allocation in hot path
290
+ this._drainedMsg = { type: 'streamDrained', streamId: '' };
291
+ this._statsMsg = { type: 'stats', streamId: '', bufferMs: 0, targetMs: 0, state: '', inputDrained: false, jitterMs: 0, underrunCount: 0, dropCount: 0 };
292
+ this._playbackMsg = { type: 'playback', streamId: '', epoch: 0, playedSeconds: 0, bufferMs: 0 };
293
+ this.samplesSincePlaybackReport = 0;
294
+
295
+ this.port.onmessage = (event) => {
296
+ const data = event.data;
297
+ if (!data || typeof data !== 'object') return;
298
+
299
+ if (data.type === 'streamBegin') {
300
+ const streamId = data.streamId;
301
+ const sab = data.sharedArrayBuffer;
302
+ const useSab = this.useSharedArrayBuffer && sab;
303
+
304
+ let buffer;
305
+ if (useSab) {
306
+ buffer = new RingBuffer(sab, Float32Array, this.channels);
307
+ this.ringBuffers.set(streamId, buffer);
308
+ this.queueBuffers.delete(streamId);
309
+ } else {
310
+ buffer = new QueueBuffer(this.queueMaxSamples, this.channels);
311
+ this.queueBuffers.set(streamId, buffer);
312
+ this.ringBuffers.delete(streamId);
313
+ }
314
+
315
+ // Initialize audio buffer stream state
316
+ const stream = {
317
+ buffer,
318
+ state: 'BUFFERING',
319
+ currentEpoch: -1,
320
+ streamEnding: false,
321
+ inputDrained: false,
322
+ epochSet: false,
323
+ epochTimestampInterleavedSamples: 0,
324
+ playoutInterleavedSamples: 0,
325
+ jitterMs: 0,
326
+ bufferTargetMs: BUFFER_TARGET_MIN_MS,
327
+ stableDurationMs: 0,
328
+ totalUnderrunCount: 0,
329
+ totalDropCount: 0,
330
+ frameSamplesPerChannel: DEFAULT_FRAME_SAMPLES_PER_CHANNEL,
331
+ lastOut: new Float32Array(RENDER_QUANTUM_SIZE * this.channels),
332
+ hasLastOut: false,
333
+ transport: useSab ? 'sab' : 'queue',
334
+ // Fade envelope state
335
+ fadeGain: 0.0,
336
+ fadeDirection: 'none',
337
+ needsFadeIn: true,
338
+ needsFadeOut: false,
339
+ underrunPending: false,
340
+ // Diagnostic logging counter
341
+ bufferingLogCounter: 0
342
+ };
343
+ this.streams.set(streamId, stream);
344
+ this.activeStreamIdIndices.set(streamId, this.activeStreamIds.length);
345
+ this.activeStreamIds.push(streamId);
346
+ this.activeStreams.push(stream);
347
+ return;
348
+ }
349
+
350
+ if (data.type === 'streamEnd') {
351
+ const streamId = data.streamId;
352
+ const stream = this.streams.get(streamId);
353
+ if (stream) {
354
+ stream.streamEnding = true;
355
+ }
356
+ return;
357
+ }
358
+
359
+ if (data.type === 'pcm') {
360
+ const q = this.queueBuffers.get(data.streamId);
361
+ if (!q) return;
362
+ q.compact();
363
+ if (data.pcm instanceof Float32Array) {
364
+ q.push(data.pcm);
365
+ // Apply buffer limit based on playback state
366
+ const maxSamples = this.isPlaybackActive ? this.queueMaxSamples : this.suspendedMaxSamples;
367
+ if (q.totalSamples > maxSamples) {
368
+ q.skip(q.totalSamples - maxSamples);
369
+ }
370
+ }
371
+ return;
372
+ }
373
+
374
+ if (data.type === 'frame') {
375
+ const stream = this.streams.get(data.streamId);
376
+ if (!stream) return;
377
+
378
+ const epoch = data.epoch;
379
+ const timestampInterleavedSamples = data.timestampInterleavedSamples;
380
+
381
+ // Epoch change → reset stream state
382
+ if (stream.currentEpoch !== epoch) {
383
+ this.port.postMessage({
384
+ type: 'log',
385
+ level: 'info',
386
+ message: 'Epoch change ' + stream.currentEpoch + '->' + epoch + ', bufferTargetMs=' + stream.bufferTargetMs.toFixed(0) + 'ms (not reset)',
387
+ streamId: data.streamId
388
+ });
389
+ stream.currentEpoch = epoch;
390
+ stream.epochSet = false;
391
+ stream.playoutInterleavedSamples = 0;
392
+ stream.streamEnding = false;
393
+ stream.inputDrained = false;
394
+ stream.state = 'BUFFERING';
395
+ stream.frameSamplesPerChannel = DEFAULT_FRAME_SAMPLES_PER_CHANNEL;
396
+ stream.hasLastOut = false;
397
+ // Reset fade state for new epoch
398
+ stream.fadeGain = 0.0;
399
+ stream.fadeDirection = 'none';
400
+ stream.needsFadeIn = true;
401
+ stream.needsFadeOut = false;
402
+ stream.underrunPending = false;
403
+
404
+ const ring = this.ringBuffers.get(data.streamId);
405
+ if (ring) ring.flush();
406
+ const q = this.queueBuffers.get(data.streamId);
407
+ if (q) q.flush();
408
+ }
409
+
410
+ // Establish initial timestamp mapping
411
+ if (!stream.epochSet) {
412
+ stream.epochSet = true;
413
+ stream.epochTimestampInterleavedSamples = timestampInterleavedSamples;
414
+ }
415
+
416
+ // Update jitter estimate
417
+ stream.jitterMs = data.jitterMs;
418
+ if (typeof data.frameSizeInterleavedSamples === 'number' && data.frameSizeInterleavedSamples > 0) {
419
+ stream.frameSamplesPerChannel = Math.max(1, Math.round(data.frameSizeInterleavedSamples / this.channels));
420
+ }
421
+
422
+ // Trigger fade-in if receiving audio on a drained stream (even without isFirst)
423
+ if (stream.inputDrained && !data.isFirst) {
424
+ stream.needsFadeIn = true;
425
+ stream.needsFadeOut = false;
426
+ stream.fadeGain = 0.0;
427
+ stream.fadeDirection = 'in';
428
+ stream.inputDrained = false;
429
+ stream.underrunPending = false;
430
+ }
431
+
432
+ // Track isFirst/isLast for fade triggers
433
+ if (data.isFirst) {
434
+ // New audio segment starting - fade in from silence
435
+ stream.needsFadeIn = true;
436
+ stream.needsFadeOut = false;
437
+ stream.fadeGain = 0.0;
438
+ stream.fadeDirection = 'in';
439
+ stream.inputDrained = false;
440
+ stream.underrunPending = false;
441
+ }
442
+ if (data.isLast) {
443
+ // Audio segment ending - schedule fade out
444
+ stream.needsFadeOut = true;
445
+ stream.inputDrained = true;
446
+ this.port.postMessage({
447
+ type: 'log',
448
+ level: 'info',
449
+ message: 'Received isLast=true, setting inputDrained=true',
450
+ streamId: data.streamId
451
+ });
452
+ }
453
+
454
+ // Enforce buffer limit when not actively playing (for SAB mode)
455
+ if (!this.isPlaybackActive) {
456
+ const maxSamples = this.suspendedMaxSamples;
457
+ const ring = this.ringBuffers.get(data.streamId);
458
+ if (ring) {
459
+ const available = ring.availableRead();
460
+ if (available > maxSamples) {
461
+ ring.skip(available - maxSamples);
462
+ }
463
+ }
464
+ }
465
+ return;
466
+ }
467
+
468
+ if (data.type === 'playbackActive') {
469
+ const wasActive = this.isPlaybackActive;
470
+ this.isPlaybackActive = data.active;
471
+
472
+ // When switching to inactive, trim all buffers to suspended limit
473
+ if (wasActive && !data.active) {
474
+ const maxSamples = this.suspendedMaxSamples;
475
+ for (const q of this.queueBuffers.values()) {
476
+ if (q.totalSamples > maxSamples) {
477
+ q.skip(q.totalSamples - maxSamples);
478
+ }
479
+ }
480
+ for (const ring of this.ringBuffers.values()) {
481
+ const available = ring.availableRead();
482
+ if (available > maxSamples) {
483
+ ring.skip(available - maxSamples);
484
+ }
485
+ }
486
+ }
487
+ return;
488
+ }
489
+ };
490
+ }
491
+
492
+ availableSamples(stream) {
493
+ const buf = stream.buffer;
494
+ return buf ? buf.availableRead() : 0;
495
+ }
496
+
497
+ skipSamples(stream, count) {
498
+ const buf = stream.buffer;
499
+ return buf ? buf.skip(count) : 0;
500
+ }
501
+
502
+ readInto(stream, target) {
503
+ const buf = stream.buffer;
504
+ if (buf) return buf.pop(target);
505
+ target.fill(0);
506
+ return 0;
507
+ }
508
+
509
+ // Crossfade skip for buffer reduction - smoothly skips samples with crossfade to avoid clicks
510
+ crossfadeSkipSamples(stream, samplesToSkip) {
511
+ const fadeLen = BUFFER_REDUCTION_CROSSFADE_SAMPLES * this.channels;
512
+
513
+ // 1. Read pre-skip tail into pre-allocated buffer
514
+ const preRead = this.readInto(stream, this.crossfadePreSkip);
515
+ if (preRead < fadeLen) return 0;
516
+
517
+ // 2. Skip the excess (minus crossfade portion)
518
+ const skipAmount = samplesToSkip - fadeLen;
519
+ if (skipAmount > 0) {
520
+ this.skipSamples(stream, skipAmount);
521
+ }
522
+
523
+ // 3. Read post-skip head into pre-allocated buffer
524
+ const postRead = this.readInto(stream, this.crossfadePostSkip);
525
+ if (postRead < fadeLen) {
526
+ // Copy pre-skip to result buffer, output what we have
527
+ for (let i = 0; i < fadeLen; i++) {
528
+ this.crossfadeResult[i] = this.crossfadePreSkip[i];
529
+ }
530
+ this.crossfadeOffset = 0;
531
+ this.crossfadeRemaining = postRead > 0 ? fadeLen : 0;
532
+ return samplesToSkip;
533
+ }
534
+
535
+ // 4. Linear crossfade in-place to pre-allocated result buffer
536
+ const invFadeLen = 1.0 / fadeLen;
537
+ for (let i = 0; i < fadeLen; i++) {
538
+ const t = i * invFadeLen;
539
+ this.crossfadeResult[i] = this.crossfadePreSkip[i] * (1 - t) + this.crossfadePostSkip[i] * t;
540
+ }
541
+
542
+ this.crossfadeOffset = 0;
543
+ this.crossfadeRemaining = fadeLen;
544
+
545
+ return samplesToSkip;
546
+ }
547
+
548
+ // Returns the minimum safe audio buffer target based on jitter
549
+ getBufferFloorMs(stream) {
550
+ return BUFFER_TARGET_MIN_MS + JITTER_MULTIPLIER * stream.jitterMs;
551
+ }
552
+
553
+ // ALL buffer target changes go through this function
554
+ updateBufferTarget(stream, action) {
555
+ const floorMs = this.getBufferFloorMs(stream);
556
+ let target = Math.max(stream.bufferTargetMs, floorMs);
557
+
558
+ if (action === 'underrun') {
559
+ // Simple: every underrun adds a fixed amount
560
+ target += UNDERRUN_JUMP_MS;
561
+ stream.stableDurationMs = 0;
562
+ } else if (action === 'stable-decrease') {
563
+ // Decrease: gradual, never below floor
564
+ target = Math.max(floorMs, target - DECREASE_STEP_MS);
565
+ }
566
+ // action === 'enforce-floor' just enforces floor, no other changes
567
+
568
+ stream.bufferTargetMs = Math.min(BUFFER_TARGET_MAX_MS, target);
569
+ }
570
+
571
+ // Immediate response to underrun - called from process() after fade-out completes
572
+ handleUnderrun(streamId, stream) {
573
+ stream.totalUnderrunCount++;
574
+ stream.state = 'BUFFERING';
575
+ stream.needsFadeIn = true;
576
+ stream.underrunPending = false;
577
+ // Note: fadeGain/fadeDirection not reset here - fade-out has already completed
578
+ this.updateBufferTarget(stream, 'underrun');
579
+
580
+ this.port.postMessage({
581
+ type: 'log',
582
+ level: 'warn',
583
+ message: 'Buffer underrun, count=' + stream.totalUnderrunCount + ', newTarget=' + stream.bufferTargetMs.toFixed(0) + 'ms',
584
+ streamId
585
+ });
586
+ }
587
+
588
+ controlTick(streamId, stream, nowMs) {
589
+ if (!stream.epochSet || stream.state === 'ENDED') return;
590
+
591
+ // Always enforce buffer floor
592
+ this.updateBufferTarget(stream, 'enforce-floor');
593
+
594
+ // Stability tracking and gradual decrease (only when playing, not ending)
595
+ if (stream.state === 'PLAYING' && !stream.streamEnding) {
596
+ stream.stableDurationMs += CONTROL_PERIOD_MS;
597
+
598
+ if (stream.stableDurationMs >= STABLE_TIME_TO_DECREASE_MS) {
599
+ this.updateBufferTarget(stream, 'stable-decrease');
600
+ stream.stableDurationMs = 0;
601
+ }
602
+ }
603
+ }
604
+
605
+ process(_inputs, outputs) {
606
+ const output = outputs[0];
607
+ if (!output || output.length === 0) {
608
+ return true;
609
+ }
610
+
611
+ const now = currentTime;
612
+ const nowMs = now * 1000;
613
+ const outputChannels = output.length;
614
+ const requestSamples = RENDER_QUANTUM_SIZE * this.channels;
615
+
616
+ for (let ch = 0; ch < outputChannels; ch++) {
617
+ output[ch].fill(0);
618
+ }
619
+
620
+ // Run control loop periodically
621
+ if (nowMs - this.lastControlTime >= CONTROL_PERIOD_MS) {
622
+ for (let i = 0; i < this.activeStreamIds.length; i++) {
623
+ const streamId = this.activeStreamIds[i];
624
+ const stream = this.activeStreams[i];
625
+ if (stream) this.controlTick(streamId, stream, nowMs);
626
+ }
627
+ this.lastControlTime = nowMs;
628
+ }
629
+
630
+ let activeStreamCount = 0;
631
+ const drainedStreamIds = this.drainedStreamIds;
632
+ drainedStreamIds.length = 0;
633
+
634
+ for (let i = 0; i < this.activeStreamIds.length; i++) {
635
+ const streamId = this.activeStreamIds[i];
636
+ const stream = this.activeStreams[i];
637
+ if (!stream || stream.state === 'ENDED') {
638
+ continue;
639
+ }
640
+
641
+ const available = this.availableSamples(stream);
642
+
643
+ if (stream.state === 'BUFFERING') {
644
+ if (stream.streamEnding) {
645
+ if (available <= 0) {
646
+ stream.state = 'ENDED';
647
+ drainedStreamIds.push(streamId);
648
+ continue;
649
+ }
650
+ this.port.postMessage({
651
+ type: 'log',
652
+ level: 'info',
653
+ message: 'BUFFERING->PLAYING (streamEnding)',
654
+ streamId
655
+ });
656
+ stream.state = 'PLAYING';
657
+ } else {
658
+ const bufferSamples = available / this.channels;
659
+ const bufferLevelMs = bufferSamples * MS_PER_SAMPLE;
660
+ if (bufferLevelMs >= stream.bufferTargetMs) {
661
+ this.port.postMessage({
662
+ type: 'log',
663
+ level: 'info',
664
+ message: 'BUFFERING->PLAYING, buffer=' + bufferLevelMs.toFixed(0) + 'ms, target=' + stream.bufferTargetMs.toFixed(0) + 'ms',
665
+ streamId
666
+ });
667
+ stream.state = 'PLAYING';
668
+ stream.bufferingLogCounter = 0;
669
+ if (stream.needsFadeIn) {
670
+ stream.fadeDirection = 'in';
671
+ } else {
672
+ stream.fadeGain = 1.0;
673
+ }
674
+ } else {
675
+ // Log periodically when stuck in BUFFERING with data available
676
+ if (available > 0) {
677
+ stream.bufferingLogCounter = (stream.bufferingLogCounter || 0) + 1;
678
+ // Log every ~1 second (approximately 43 calls at 128 samples / 48kHz)
679
+ if (stream.bufferingLogCounter >= 43) {
680
+ this.port.postMessage({
681
+ type: 'log',
682
+ level: 'info',
683
+ message: 'BUFFERING: buffer=' + bufferLevelMs.toFixed(0) + 'ms, target=' + stream.bufferTargetMs.toFixed(0) + 'ms, waiting...',
684
+ streamId
685
+ });
686
+ stream.bufferingLogCounter = 0;
687
+ }
688
+ }
689
+ continue;
690
+ }
691
+ }
692
+ }
693
+
694
+ // Buffer reduction: if buffer exceeds target max, drain with crossfade
695
+ // Skip if we're still outputting a previous crossfade
696
+ const bufferSamplesForReduction = available / this.channels;
697
+ const bufferLevelMsForReduction = bufferSamplesForReduction * MS_PER_SAMPLE;
698
+ if (this.crossfadeRemaining <= 0 && bufferLevelMsForReduction > BUFFER_TARGET_MAX_MS && stream.state === 'PLAYING' && !stream.streamEnding) {
699
+ const excessMs = bufferLevelMsForReduction - BUFFER_TARGET_MAX_MS;
700
+ const fadeLen = BUFFER_REDUCTION_CROSSFADE_SAMPLES * this.channels;
701
+ // Need: fadeLen (pre-skip read) + skip amount + fadeLen (post-skip read)
702
+ // Skip amount must be > fadeLen for crossfade to make sense
703
+ // Cap at ~2x speed: skip up to 1 quantum worth per render, but at least fadeLen + 1
704
+ const excessSamples = Math.round((excessMs * OUTPUT_SAMPLE_RATE_HZ * this.channels) / 1000);
705
+ const samplesToSkip = Math.min(
706
+ Math.max(fadeLen + RENDER_QUANTUM_SIZE, RENDER_QUANTUM_SIZE * this.channels * 2),
707
+ excessSamples
708
+ );
709
+ // Check we have enough buffer: need fadeLen + samplesToSkip for the operation
710
+ if (samplesToSkip > fadeLen && available >= fadeLen + samplesToSkip) {
711
+ this.crossfadeSkipSamples(stream, samplesToSkip);
712
+ stream.totalDropCount++;
713
+ }
714
+ }
715
+
716
+ // Output pending crossfade samples first (from processor-level pre-allocated buffer)
717
+ if (this.crossfadeRemaining > 0) {
718
+ const framesToOutput = Math.min(Math.floor(this.crossfadeRemaining / this.channels), RENDER_QUANTUM_SIZE);
719
+ let idx = this.crossfadeOffset;
720
+ for (let frame = 0; frame < framesToOutput; frame++) {
721
+ // Update fade envelope during crossfade output (critical for initial unlock scenario)
722
+ if (stream.fadeDirection === 'in') {
723
+ stream.fadeGain = Math.min(1.0, stream.fadeGain + FADE_IN_INCREMENT);
724
+ if (stream.fadeGain >= 1.0) {
725
+ stream.fadeDirection = 'none';
726
+ stream.needsFadeIn = false;
727
+ }
728
+ } else if (stream.fadeDirection === 'out') {
729
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
730
+ if (stream.fadeGain <= 0.0) {
731
+ stream.fadeDirection = 'none';
732
+ stream.needsFadeOut = false;
733
+ }
734
+ }
735
+ for (let ch = 0; ch < this.channels && ch < outputChannels; ch++) {
736
+ output[ch][frame] += this.crossfadeResult[idx++] * stream.fadeGain;
737
+ }
738
+ }
739
+ this.crossfadeOffset += framesToOutput * this.channels;
740
+ this.crossfadeRemaining -= framesToOutput * this.channels;
741
+ // Always continue after crossfade output to avoid overlapping with normal playback
742
+ activeStreamCount++;
743
+ continue;
744
+ }
745
+
746
+ const bufferMs = (available / this.channels) * MS_PER_SAMPLE;
747
+
748
+ // Start fade-out when buffer is low and isLast was received
749
+ if (stream.needsFadeOut && stream.fadeDirection !== 'out' && bufferMs <= FADE_OUT_DURATION_MS * 2.0) {
750
+ stream.fadeDirection = 'out';
751
+ }
752
+
753
+ const countRead = this.readInto(stream, this.tempInterleaved);
754
+ const framesRead = Math.floor(countRead / this.channels);
755
+
756
+ if (countRead >= requestSamples) {
757
+ stream.lastOut.set(this.tempInterleaved);
758
+ stream.hasLastOut = true;
759
+
760
+ // Apply per-sample fade envelope
761
+ let sampleIdx = 0;
762
+ for (let frame = 0; frame < RENDER_QUANTUM_SIZE; frame++) {
763
+ // Update fade envelope
764
+ if (stream.fadeDirection === 'in') {
765
+ stream.fadeGain = Math.min(1.0, stream.fadeGain + FADE_IN_INCREMENT);
766
+ if (stream.fadeGain >= 1.0) {
767
+ stream.fadeDirection = 'none';
768
+ stream.needsFadeIn = false;
769
+ }
770
+ } else if (stream.fadeDirection === 'out') {
771
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
772
+ if (stream.fadeGain <= 0.0) {
773
+ stream.fadeDirection = 'none';
774
+ stream.needsFadeOut = false;
775
+ }
776
+ }
777
+ for (let ch = 0; ch < this.channels && ch < outputChannels; ch++) {
778
+ output[ch][frame] += this.tempInterleaved[sampleIdx++] * stream.fadeGain;
779
+ }
780
+ }
781
+ stream.playoutInterleavedSamples += countRead;
782
+ activeStreamCount++;
783
+ } else if (countRead > 0) {
784
+ // Partial data: smooth tail to avoid hard discontinuities
785
+ const endingOrDrained = stream.streamEnding || stream.inputDrained;
786
+
787
+ const holdFrameIndex = Math.max(0, framesRead - 1);
788
+ const holdBaseIdx = holdFrameIndex * this.channels;
789
+ let fillIdx = framesRead * this.channels;
790
+ for (let frame = framesRead; frame < RENDER_QUANTUM_SIZE; frame++) {
791
+ for (let ch = 0; ch < this.channels; ch++) {
792
+ this.tempInterleaved[fillIdx++] = this.tempInterleaved[holdBaseIdx + ch];
793
+ }
794
+ }
795
+
796
+ stream.lastOut.set(this.tempInterleaved);
797
+ stream.hasLastOut = true;
798
+
799
+ // Render valid frames first with the current fade state
800
+ let sampleIdx = 0;
801
+ for (let frame = 0; frame < framesRead; frame++) {
802
+ if (stream.fadeDirection === 'in') {
803
+ stream.fadeGain = Math.min(1.0, stream.fadeGain + FADE_IN_INCREMENT);
804
+ if (stream.fadeGain >= 1.0) {
805
+ stream.fadeDirection = 'none';
806
+ stream.needsFadeIn = false;
807
+ }
808
+ } else if (stream.fadeDirection === 'out') {
809
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
810
+ if (stream.fadeGain <= 0.0) {
811
+ stream.fadeDirection = 'none';
812
+ stream.needsFadeOut = false;
813
+ }
814
+ }
815
+ for (let ch = 0; ch < this.channels && ch < outputChannels; ch++) {
816
+ output[ch][frame] += this.tempInterleaved[sampleIdx++] * stream.fadeGain;
817
+ }
818
+ }
819
+
820
+ // Tail: force fade-out so the transition remains bounded
821
+ if (stream.fadeGain > 0.001) {
822
+ stream.fadeDirection = 'out';
823
+ sampleIdx = framesRead * this.channels;
824
+ for (let frame = framesRead; frame < RENDER_QUANTUM_SIZE; frame++) {
825
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
826
+ if (stream.fadeGain <= 0.0) {
827
+ stream.fadeDirection = 'none';
828
+ stream.needsFadeOut = false;
829
+ }
830
+ for (let ch = 0; ch < this.channels && ch < outputChannels; ch++) {
831
+ output[ch][frame] += this.tempInterleaved[sampleIdx++] * stream.fadeGain;
832
+ }
833
+ }
834
+ }
835
+
836
+ stream.playoutInterleavedSamples += countRead;
837
+
838
+ if (endingOrDrained) {
839
+ if (stream.fadeGain <= 0.001 && stream.streamEnding) {
840
+ stream.state = 'ENDED';
841
+ drainedStreamIds.push(streamId);
842
+ }
843
+ } else {
844
+ // Mark underrun pending - will transition to BUFFERING after fade-out completes
845
+ stream.underrunPending = true;
846
+ if (stream.fadeGain <= 0.001) {
847
+ this.handleUnderrun(streamId, stream);
848
+ }
849
+ }
850
+ activeStreamCount++;
851
+ } else {
852
+ // No data available
853
+ const endingOrDrained = stream.streamEnding || stream.inputDrained;
854
+
855
+ if (stream.hasLastOut && stream.fadeGain > 0.001) {
856
+ // Continue fade-out using sample-hold from lastOut
857
+ stream.fadeDirection = 'out';
858
+ if (!endingOrDrained) {
859
+ stream.underrunPending = true;
860
+ }
861
+ // Hold the LAST sample constant (not cycling through the buffer)
862
+ const holdBaseIdx = (RENDER_QUANTUM_SIZE - 1) * this.channels;
863
+ for (let frame = 0; frame < RENDER_QUANTUM_SIZE; frame++) {
864
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
865
+ if (stream.fadeGain <= 0.0) {
866
+ stream.fadeDirection = 'none';
867
+ stream.needsFadeOut = false;
868
+ }
869
+ for (let ch = 0; ch < this.channels && ch < outputChannels; ch++) {
870
+ output[ch][frame] += stream.lastOut[holdBaseIdx + ch] * stream.fadeGain;
871
+ }
872
+ }
873
+ } else if (endingOrDrained && stream.fadeDirection === 'out' && stream.fadeGain > 0.001) {
874
+ for (let frame = 0; frame < RENDER_QUANTUM_SIZE; frame++) {
875
+ stream.fadeGain = Math.max(0.0, stream.fadeGain - FADE_OUT_INCREMENT);
876
+ if (stream.fadeGain <= 0.001) {
877
+ stream.fadeGain = 0;
878
+ stream.fadeDirection = 'none';
879
+ stream.needsFadeOut = false;
880
+ break;
881
+ }
882
+ }
883
+ }
884
+
885
+ if (endingOrDrained) {
886
+ if (stream.fadeGain <= 0.001) {
887
+ if (stream.streamEnding) {
888
+ stream.state = 'ENDED';
889
+ drainedStreamIds.push(streamId);
890
+ } else if (stream.inputDrained) {
891
+ this.port.postMessage({
892
+ type: 'log',
893
+ level: 'info',
894
+ message: 'PLAYING->BUFFERING (inputDrained), bufferTargetMs=' + stream.bufferTargetMs.toFixed(0) + 'ms',
895
+ streamId
896
+ });
897
+ stream.state = 'BUFFERING';
898
+ stream.needsFadeIn = true;
899
+ }
900
+ }
901
+ } else if (stream.underrunPending && stream.fadeGain <= 0.001) {
902
+ // Fade-out complete, now transition to BUFFERING
903
+ this.handleUnderrun(streamId, stream);
904
+ } else if (!stream.underrunPending && !stream.hasLastOut) {
905
+ // No lastOut to fade from, immediate underrun
906
+ stream.underrunPending = true;
907
+ this.handleUnderrun(streamId, stream);
908
+ }
909
+ }
910
+ }
911
+
912
+ // Cleanup drained streams
913
+ if (drainedStreamIds.length > 0) {
914
+ for (let i = 0; i < drainedStreamIds.length; i++) {
915
+ const streamId = drainedStreamIds[i];
916
+ // Remove from activeStreamIds/activeStreams using O(1) index lookup + swap-remove
917
+ const idx = this.activeStreamIdIndices.get(streamId);
918
+ if (idx !== undefined) {
919
+ const lastIdx = this.activeStreamIds.length - 1;
920
+ if (idx !== lastIdx) {
921
+ const lastStreamId = this.activeStreamIds[lastIdx];
922
+ this.activeStreamIds[idx] = lastStreamId;
923
+ this.activeStreams[idx] = this.activeStreams[lastIdx];
924
+ this.activeStreamIdIndices.set(lastStreamId, idx);
925
+ }
926
+ this.activeStreamIds.length--;
927
+ this.activeStreams.length--;
928
+ this.activeStreamIdIndices.delete(streamId);
929
+ }
930
+ this.ringBuffers.delete(streamId);
931
+ this.queueBuffers.delete(streamId);
932
+ this.streams.delete(streamId);
933
+ try {
934
+ this._drainedMsg.streamId = streamId;
935
+ this.port.postMessage(this._drainedMsg);
936
+ } catch {
937
+ // ignore
938
+ }
939
+ }
940
+ }
941
+
942
+ // Combined normalization + soft clipping (single pass)
943
+ const needsNormalization = this.normalizationEnabled && activeStreamCount > 1;
944
+ const normFactor = needsNormalization ? 1.0 / Math.sqrt(activeStreamCount) : 1.0;
945
+ for (let frame = 0; frame < RENDER_QUANTUM_SIZE; frame++) {
946
+ for (let ch = 0; ch < outputChannels; ch++) {
947
+ let v = output[ch][frame];
948
+ if (needsNormalization) {
949
+ v *= normFactor;
950
+ }
951
+ // NaN check: v !== v is true only for NaN; clipping handles ±Infinity
952
+ if (v !== v) v = 0;
953
+ output[ch][frame] = v > 1 ? 1 : v < -1 ? -1 : v;
954
+ }
955
+ }
956
+
957
+ // Periodic playback-position reports (always on — feeds client->server audibility
958
+ // reporting, which apps use to pace speech to what listeners actually heard)
959
+ this.samplesSincePlaybackReport += RENDER_QUANTUM_SIZE;
960
+ if (this.samplesSincePlaybackReport >= PLAYBACK_REPORT_INTERVAL_SAMPLES) {
961
+ this.samplesSincePlaybackReport = 0;
962
+
963
+ for (let i = 0; i < this.activeStreamIds.length; i++) {
964
+ const stream = this.activeStreams[i];
965
+ if (!stream || !stream.epochSet) continue;
966
+
967
+ this._playbackMsg.streamId = this.activeStreamIds[i];
968
+ this._playbackMsg.epoch = stream.currentEpoch;
969
+ this._playbackMsg.playedSeconds = stream.playoutInterleavedSamples / (OUTPUT_SAMPLE_RATE_HZ * this.channels);
970
+ this._playbackMsg.bufferMs = (this.availableSamples(stream) / this.channels) * MS_PER_SAMPLE;
971
+ this.port.postMessage(this._playbackMsg);
972
+ }
973
+ }
974
+
975
+ // Stats reporting
976
+ if (!this.diagnosticsEnabled) {
977
+ return true;
978
+ }
979
+
980
+ this.samplesSinceLastStats += RENDER_QUANTUM_SIZE;
981
+ if (this.samplesSinceLastStats >= this.statsIntervalSamples) {
982
+ this.samplesSinceLastStats = 0;
983
+
984
+ for (let i = 0; i < this.activeStreamIds.length; i++) {
985
+ const streamId = this.activeStreamIds[i];
986
+ const stream = this.activeStreams[i];
987
+ if (!stream) continue;
988
+ const bufferSamples = this.availableSamples(stream) / this.channels;
989
+ const bufferMs = bufferSamples * MS_PER_SAMPLE;
990
+
991
+ this._statsMsg.streamId = streamId;
992
+ this._statsMsg.bufferMs = bufferMs;
993
+ this._statsMsg.targetMs = stream.bufferTargetMs;
994
+ this._statsMsg.state = stream.state;
995
+ this._statsMsg.inputDrained = stream.inputDrained;
996
+ this._statsMsg.jitterMs = stream.jitterMs;
997
+ this._statsMsg.underrunCount = stream.totalUnderrunCount;
998
+ this._statsMsg.dropCount = stream.totalDropCount;
999
+ this.port.postMessage(this._statsMsg);
1000
+ }
1001
+ }
1002
+
1003
+ return true;
1004
+ }
1005
+ }
1006
+
1007
+ registerProcessor('ikon-audio-player', IkonAudioPlayerProcessor);
1008
+ `;
1009
+ }
1010
+ var x = class n {
1011
+ client;
1012
+ config;
1013
+ enabled = !1;
1014
+ stateUnsubscribe = null;
1015
+ isMobile = o();
1016
+ boundVisibilityChange = null;
1017
+ boundPageHide = null;
1018
+ unlockHandler = null;
1019
+ awaitingUnlock = !1;
1020
+ recoveryTask = null;
1021
+ static RESUME_TIMEOUT_MS = 100;
1022
+ static MAX_WORKER_REBUILD_ATTEMPTS = 3;
1023
+ workerRebuildAttempts = 0;
1024
+ workerGivenUp = !1;
1025
+ audioContext = null;
1026
+ outputSampleRateHz = m;
1027
+ audioWorkletNode = null;
1028
+ scriptProcessorNode = null;
1029
+ scriptDiagnosticsTimer = null;
1030
+ workletModuleUrl = null;
1031
+ workletModuleKey = null;
1032
+ workletReady = null;
1033
+ pendingWorkletMessages = [];
1034
+ audioWorker = null;
1035
+ protocolPort = null;
1036
+ mediaSessionHelper = new d();
1037
+ hiddenMuted = !1;
1038
+ lastReportedPlaybackState = null;
1039
+ lastPlaybackByStream = /* @__PURE__ */ new Map();
1040
+ fallbackQueues = /* @__PURE__ */ new Map();
1041
+ activeStreams = /* @__PURE__ */ new Set();
1042
+ mediaSessionConfig = null;
1043
+ constructor(e, t) {
1044
+ this.client = e, this.config = t ?? {}, _.debug(`Audio playback initialized, isMobile=${this.isMobile}`);
1045
+ }
1046
+ setMediaSession(e) {
1047
+ this.mediaSessionConfig = e;
1048
+ }
1049
+ async setEnabled(e, t) {
1050
+ if (e && t?.userGesture && this.audioContext) try {
1051
+ await this.resumeWithTimeout(this.audioContext), this.audioContext.state === "running" && this.removeUnlockHandler();
1052
+ } catch {}
1053
+ if (e === this.enabled) {
1054
+ e && (await this.ensureAudioGraph(t, { promptForUnlock: !0 }), await this.ensureWorkers());
1055
+ return;
1056
+ }
1057
+ if (this.enabled = e, this.ensureStateSubscription(), !e) {
1058
+ this.stopAudioGraph(), this.stopWorkers();
1059
+ return;
1060
+ }
1061
+ await this.ensureAudioGraph(t, { promptForUnlock: !0 }), await this.ensureWorkers();
1062
+ }
1063
+ enable(e) {
1064
+ return this.setEnabled(!0, e);
1065
+ }
1066
+ disable() {
1067
+ return this.setEnabled(!1);
1068
+ }
1069
+ pause() {
1070
+ if (this.audioContext) {
1071
+ this.notifyPlaybackActive(!1);
1072
+ try {
1073
+ this.audioWorkletNode && this.audioWorkletNode.disconnect(), this.scriptProcessorNode && this.scriptProcessorNode.disconnect();
1074
+ } catch {}
1075
+ }
1076
+ }
1077
+ resume() {
1078
+ if (!this.audioContext) return;
1079
+ try {
1080
+ this.audioWorkletNode && this.audioWorkletNode.connect(this.audioContext.destination), this.scriptProcessorNode && this.scriptProcessorNode.connect(this.audioContext.destination);
1081
+ } catch {}
1082
+ let e = this.getContextState(this.audioContext) === "running";
1083
+ this.notifyPlaybackActive(e);
1084
+ }
1085
+ requestRecovery() {
1086
+ if (this.enabled && l() && this.audioContext) {
1087
+ this.stopAudioGraph();
1088
+ try {
1089
+ let e = new AudioContext({
1090
+ sampleRate: m,
1091
+ latencyHint: p
1092
+ });
1093
+ this.audioContext = e, this.outputSampleRateHz = e.sampleRate, e.resume().catch(() => void 0), this.setupAudioGraphAfterRecreate();
1094
+ } catch (e) {
1095
+ _.error(`iOS audio recovery failed: ${y(e)}`), this.addUnlockHandler();
1096
+ }
1097
+ return;
1098
+ }
1099
+ }
1100
+ async setupAudioGraphAfterRecreate() {
1101
+ if (!this.audioContext) return;
1102
+ let e = this.audioContext, t = this.resolveDiagnosticsConfig();
1103
+ try {
1104
+ await this.ensureAudioWorklet(e, t, void 0, { promptForUnlock: !1 });
1105
+ } catch {
1106
+ this.ensureScriptProcessor(e, t);
1107
+ }
1108
+ }
1109
+ syncStreamsFromGlobalState(e) {
1110
+ if (!this.audioWorker) return;
1111
+ let t = Object.values(e.AudioStreams ?? {}).map((e) => ({
1112
+ senderId: e.ClientSessionId,
1113
+ trackId: e.TrackId,
1114
+ info: e.Info
1115
+ }));
1116
+ if (t.length !== 0) try {
1117
+ this.audioWorker.postMessage({
1118
+ type: "syncStreams",
1119
+ streams: t
1120
+ });
1121
+ } catch {}
1122
+ }
1123
+ setWebRtcActive(e) {
1124
+ if (this.audioWorker) try {
1125
+ this.audioWorker.postMessage({
1126
+ type: "setWebRtcEnabled",
1127
+ enabled: e
1128
+ });
1129
+ } catch {}
1130
+ }
1131
+ handleProtocolTransportLost() {
1132
+ this.audioWorker && this.stopWorkers();
1133
+ }
1134
+ dispose() {
1135
+ this.enabled = !1, this.activeStreams.clear(), this.removeUnlockHandler(), this.removeVisibilityHandler(), this.stateUnsubscribe?.(), this.stateUnsubscribe = null, this.stopAudioGraph(), this.stopWorkers();
1136
+ }
1137
+ resolveDiagnosticsConfig() {
1138
+ let e = this.config.diagnostics ?? {};
1139
+ return {
1140
+ enabled: e.enabled ?? v.enabled,
1141
+ statusIntervalMs: e.statusIntervalMs ?? v.statusIntervalMs
1142
+ };
1143
+ }
1144
+ resolveBackgroundConfig() {
1145
+ let e = this.config.background ?? {};
1146
+ return {
1147
+ allowOnDesktop: e.allowOnDesktop ?? !0,
1148
+ allowOnMobile: e.allowOnMobile ?? !1
1149
+ };
1150
+ }
1151
+ async ensureAudioGraph(e, t) {
1152
+ if (typeof window > "u") return;
1153
+ if (!this.audioContext) {
1154
+ let e = m, t = p;
1155
+ try {
1156
+ this.audioContext = new AudioContext({
1157
+ sampleRate: e,
1158
+ latencyHint: t
1159
+ });
1160
+ } catch (e) {
1161
+ try {
1162
+ this.audioContext = new AudioContext({ latencyHint: t });
1163
+ } catch (e) {
1164
+ try {
1165
+ this.audioContext = new AudioContext();
1166
+ } catch (e) {
1167
+ _.warn(`Failed to create AudioContext: ${y(e)}`), this.audioContext = null;
1168
+ return;
1169
+ }
1170
+ _.debug(`AudioContext latencyHint unsupported: ${y(e)}`);
1171
+ }
1172
+ _.debug(`AudioContext sample rate request unsupported: ${y(e)}`);
1173
+ }
1174
+ this.outputSampleRateHz = this.audioContext.sampleRate || 48e3, this.outputSampleRateHz !== e && _.debug(`AudioContext running at ${this.outputSampleRateHz}Hz (requested ${e}Hz)`), this.attachAudioContextHealthHandlers(this.audioContext);
1175
+ }
1176
+ l() && this.mediaSessionHelper.ensureSilentPlaybackElement();
1177
+ let n = this.audioContext, r = this.resolveDiagnosticsConfig();
1178
+ if ((this.config.performance?.preferAudioWorklet ?? !0) && u(n)) {
1179
+ await this.ensureAudioWorklet(n, r, e, t);
1180
+ return;
1181
+ }
1182
+ this.ensureScriptProcessor(n, r), await this.resumeAudioContextIfPossible(n, e, t), this.rebindWorkerTransportIfNeeded();
1183
+ }
1184
+ attachAudioContextHealthHandlers(e) {
1185
+ e.__ikonAudioHealthAttached !== !0 && (Object.defineProperty(e, "__ikonAudioHealthAttached", {
1186
+ value: !0,
1187
+ enumerable: !1
1188
+ }), e.onstatechange = () => {
1189
+ let t = this.getContextState(e), n = t === "running";
1190
+ this.notifyPlaybackActive(n), this.enabled && !n && (_.warn(`AudioContext state changed to '${t}', attempting recovery`), this.recoverAudioContext({ reason: "statechange" }));
1191
+ }, this.attachVisibilityHandler());
1192
+ }
1193
+ attachVisibilityHandler() {
1194
+ if (this.boundVisibilityChange || typeof document > "u") return;
1195
+ let e = this.resolveBackgroundConfig(), t = this.isMobile ? e.allowOnMobile : e.allowOnDesktop;
1196
+ t && this.setupMediaSession();
1197
+ let n = () => {
1198
+ if (!(!this.enabled || !this.audioContext || t)) {
1199
+ this.hiddenMuted = !0, this.notifyPlaybackActive(!1);
1200
+ try {
1201
+ this.audioWorkletNode && this.audioWorkletNode.disconnect(), this.scriptProcessorNode && this.scriptProcessorNode.disconnect();
1202
+ } catch {}
1203
+ }
1204
+ }, r = () => {
1205
+ if (!this.enabled || !this.audioContext) return;
1206
+ this.hiddenMuted = !1;
1207
+ try {
1208
+ this.audioWorkletNode && this.audioWorkletNode.connect(this.audioContext.destination), this.scriptProcessorNode && this.scriptProcessorNode.connect(this.audioContext.destination);
1209
+ } catch {}
1210
+ let e = this.getContextState(this.audioContext) === "running";
1211
+ this.notifyPlaybackActive(e);
1212
+ }, i = () => {
1213
+ if (this.enabled) {
1214
+ if (document.visibilityState === "visible") {
1215
+ r(), this.audioContext && this.getContextState(this.audioContext) !== "running" && this.recoverAudioContext({ reason: "visibilityVisible" });
1216
+ return;
1217
+ }
1218
+ n();
1219
+ }
1220
+ }, a = () => {
1221
+ n();
1222
+ };
1223
+ this.boundVisibilityChange = i, this.boundPageHide = a, document.addEventListener("visibilitychange", i), t || window.addEventListener("pagehide", a);
1224
+ }
1225
+ setupMediaSession() {
1226
+ this.mediaSessionHelper.setup(this.mediaSessionConfig ?? void 0, {
1227
+ onPlay: () => this.resume(),
1228
+ onPause: () => this.pause()
1229
+ });
1230
+ }
1231
+ cleanupMediaSession() {
1232
+ this.mediaSessionHelper.cleanup();
1233
+ }
1234
+ removeVisibilityHandler() {
1235
+ typeof document < "u" && this.boundVisibilityChange && document.removeEventListener("visibilitychange", this.boundVisibilityChange), typeof window < "u" && this.boundPageHide && (window.removeEventListener("pagehide", this.boundPageHide), window.removeEventListener("blur", this.boundPageHide)), this.boundVisibilityChange = null, this.boundPageHide = null, this.cleanupMediaSession();
1236
+ }
1237
+ getContextState(e) {
1238
+ return e?.state ?? "suspended";
1239
+ }
1240
+ addUnlockHandler() {
1241
+ if (this.enabled && !this.awaitingUnlock && !(typeof document > "u")) {
1242
+ this.awaitingUnlock = !0, this.unlockHandler = () => {
1243
+ let e = this.audioContext;
1244
+ if (e && this.getContextState(e) !== "running") try {
1245
+ e.resume().catch(() => void 0);
1246
+ } catch {}
1247
+ let t = this.mediaSessionHelper.silentAudioElement;
1248
+ if (t && t.paused) try {
1249
+ t.play().catch(() => void 0);
1250
+ } catch {}
1251
+ if ((e ? this.getContextState(e) : "suspended") === "running") {
1252
+ this.removeUnlockHandler();
1253
+ return;
1254
+ }
1255
+ this.recoveryTask || this.recoverAudioContext({
1256
+ reason: "gesture",
1257
+ fromUserGesture: !0
1258
+ }).finally(() => {
1259
+ let e = this.audioContext;
1260
+ e && this.getContextState(e) === "running" && this.removeUnlockHandler();
1261
+ });
1262
+ };
1263
+ for (let e of [
1264
+ "touchstart",
1265
+ "touchend",
1266
+ "click",
1267
+ "pointerdown",
1268
+ "pointerup",
1269
+ "keydown",
1270
+ "mouseup"
1271
+ ]) document.addEventListener(e, this.unlockHandler, { capture: !0 });
1272
+ }
1273
+ }
1274
+ removeUnlockHandler() {
1275
+ if (!this.unlockHandler) {
1276
+ this.awaitingUnlock = !1;
1277
+ return;
1278
+ }
1279
+ if (typeof document < "u") for (let e of [
1280
+ "touchstart",
1281
+ "touchend",
1282
+ "click",
1283
+ "pointerdown",
1284
+ "pointerup",
1285
+ "keydown",
1286
+ "mouseup"
1287
+ ]) document.removeEventListener(e, this.unlockHandler, { capture: !0 });
1288
+ this.unlockHandler = null, this.awaitingUnlock = !1, this.reportPlaybackStateIfChanged();
1289
+ }
1290
+ async resumeWithTimeout(e) {
1291
+ let t = e.resume(), r = new Promise((e, t) => {
1292
+ setTimeout(() => t(/* @__PURE__ */ Error("AudioContext.resume() timed out")), n.RESUME_TIMEOUT_MS);
1293
+ });
1294
+ await Promise.race([t, r]);
1295
+ }
1296
+ async ensureAudioWorklet(e, t, n, r) {
1297
+ if (this.audioWorkletNode) {
1298
+ await this.resumeAudioContextIfPossible(e, n, r);
1299
+ return;
1300
+ }
1301
+ if (n?.userGesture && this.getContextState(e) !== "running") try {
1302
+ await this.resumeWithTimeout(e), this.getContextState(e) === "running" && this.removeUnlockHandler();
1303
+ } catch {}
1304
+ let i = `${this.outputSampleRateHz}_2`;
1305
+ if (!this.workletModuleUrl || this.workletModuleKey !== i) {
1306
+ if (this.workletModuleUrl) try {
1307
+ URL.revokeObjectURL(this.workletModuleUrl);
1308
+ } catch {}
1309
+ this.workletModuleKey = i, this.workletReady = null, this.workletModuleUrl = URL.createObjectURL(new Blob([b({
1310
+ outputSampleRateHz: this.outputSampleRateHz,
1311
+ outputChannels: 2
1312
+ })], { type: "text/javascript" }));
1313
+ }
1314
+ this.workletReady ||= e.audioWorklet.addModule(this.workletModuleUrl).catch((e) => {
1315
+ throw _.warn(`Failed to load AudioWorklet module, falling back: ${y(e)}`), e;
1316
+ });
1317
+ try {
1318
+ await this.workletReady;
1319
+ } catch {
1320
+ this.workletReady = null, this.ensureScriptProcessor(e, t);
1321
+ return;
1322
+ }
1323
+ this.stopScriptProcessor();
1324
+ let a = (this.config.performance?.preferSharedArrayBuffer ?? !0) && c();
1325
+ this.audioWorkletNode = new AudioWorkletNode(e, "ikon-audio-player", {
1326
+ numberOfInputs: 0,
1327
+ numberOfOutputs: 1,
1328
+ outputChannelCount: [2],
1329
+ processorOptions: {
1330
+ outputAudioConfig: { diagnostics: t },
1331
+ useSharedArrayBuffer: a,
1332
+ outputChannels: 2
1333
+ }
1334
+ }), this.audioWorkletNode.onprocessorerror = () => {
1335
+ _.error("Audio playback worklet processor crashed, rebuilding audio pipeline"), this.stopAudioWorklet(), this.setupAudioGraphAfterRecreate();
1336
+ }, this.audioWorkletNode.port.onmessage = (e) => {
1337
+ let n = e.data;
1338
+ if (!(!n || typeof n != "object")) {
1339
+ if (n.type === "log") {
1340
+ let e = n, t = e.streamId ? ` [stream=${e.streamId.substring(0, 8)}]` : "";
1341
+ e.level === "error" ? _.error(`[Worklet]${t} ${e.message}`) : e.level === "warn" ? _.warn(`[Worklet]${t} ${e.message}`) : _.info(`[Worklet]${t} ${e.message}`);
1342
+ return;
1343
+ }
1344
+ if (n.type === "stats") {
1345
+ if (!t.enabled) return;
1346
+ let e = n;
1347
+ _.debug(`[Audio] stream=${e.streamId.substring(0, 8)} state=${e.state} buffer=${e.bufferMs.toFixed(0)}ms target=${e.targetMs.toFixed(0)}ms jitter=${e.jitterMs.toFixed(1)}ms underrun=${e.underrunCount} drop=${e.dropCount}`);
1348
+ return;
1349
+ }
1350
+ if (n.type === "playback") {
1351
+ let e = n;
1352
+ this.lastPlaybackByStream.set(e.streamId, {
1353
+ epoch: e.epoch,
1354
+ playedSeconds: e.playedSeconds,
1355
+ bufferMs: e.bufferMs
1356
+ }), this.sendPlaybackReport(e.streamId, e.epoch, e.playedSeconds, e.bufferMs);
1357
+ return;
1358
+ }
1359
+ if (n.type === "streamDrained" && "streamId" in n && typeof n.streamId == "string") {
1360
+ let e = n.streamId;
1361
+ this.activeStreams.delete(e), this.lastPlaybackByStream.delete(e), this.activeStreams.size === 0 && this.removeUnlockHandler();
1362
+ return;
1363
+ }
1364
+ }
1365
+ }, this.audioWorkletNode.connect(e.destination), this.stopScriptProcessorDiagnostics(), await this.resumeAudioContextIfPossible(e, n, r), this.rebindWorkerTransportIfNeeded();
1366
+ let o = this.pendingWorkletMessages.splice(0);
1367
+ for (let e of o) this.postToWorklet(e);
1368
+ let s = this.getContextState(e) === "running";
1369
+ this.notifyPlaybackActive(s);
1370
+ }
1371
+ ensureScriptProcessor(e, t) {
1372
+ if (this.scriptProcessorNode) return;
1373
+ this.stopAudioWorklet(), this.pendingWorkletMessages = [];
1374
+ let n = 1024, r = new Float32Array(n * 2), i = Math.round(this.outputSampleRateHz * 100 / 1e3), a = Math.round(this.outputSampleRateHz * 50 / 1e3), o = 1 / i, s = 1 / a, c = [, ,], l = [], u = e.createScriptProcessor(n, 0, 2);
1375
+ u.onaudioprocess = (e) => {
1376
+ let t = e.outputBuffer, n = t.numberOfChannels, i = t.length;
1377
+ for (let e = 0; e < n; e++) {
1378
+ let n = t.getChannelData(e);
1379
+ n.fill(0), c[e] = n;
1380
+ }
1381
+ let a = 0;
1382
+ l.length = 0;
1383
+ for (let [e, t] of this.fallbackQueues.entries()) {
1384
+ t.totalSamples > 0 && (a += 1), (t.ending || t.inputDrained) && t.fadeDirection !== "out" && t.totalSamples / n * 1e3 / this.outputSampleRateHz <= 100 && (t.fadeDirection = "out"), this.readFromFallbackQueue(t, r), (t.totalSamples > 0 || !t.hasLastOut) && ((!t.lastOut || t.lastOut.length !== r.length) && (t.lastOut = new Float32Array(r.length)), t.lastOut.set(r), t.hasLastOut = !0);
1385
+ for (let e = 0; e < i; e++) {
1386
+ t.fadeDirection === "in" ? (t.fadeGain = Math.min(1, t.fadeGain + o), t.fadeGain >= 1 && (t.fadeDirection = "none")) : t.fadeDirection === "out" && (t.fadeGain = Math.max(0, t.fadeGain - s), t.fadeGain <= 0 && (t.fadeDirection = "none"));
1387
+ for (let i = 0; i < n; i++) {
1388
+ let a = r[e * n + i];
1389
+ c[i][e] += a * t.fadeGain;
1390
+ }
1391
+ }
1392
+ let u = t.ending || t.inputDrained;
1393
+ if (u && t.totalSamples <= 0) {
1394
+ if (t.fadeDirection === "out" && t.fadeGain > .001) {
1395
+ for (let e = 0; e < i; e++) if (t.fadeGain = Math.max(0, t.fadeGain - s), t.fadeGain <= .001) {
1396
+ t.fadeGain = 0, t.fadeDirection = "none";
1397
+ break;
1398
+ }
1399
+ }
1400
+ t.fadeGain <= .001 && t.ending && l.push(e);
1401
+ } else if (!u && t.totalSamples <= 0 && t.hasLastOut && t.lastOut && t.fadeGain > .001) {
1402
+ t.fadeDirection = "out", t.underrunPending = !0;
1403
+ let e = t.lastOut, r = i - 1;
1404
+ for (let a = 0; a < i; a++) {
1405
+ t.fadeGain = Math.max(0, t.fadeGain - s), t.fadeGain <= 0 && (t.fadeDirection = "none");
1406
+ for (let i = 0; i < n; i++) {
1407
+ let o = e[r * n + i];
1408
+ c[i][a] += o * t.fadeGain;
1409
+ }
1410
+ }
1411
+ }
1412
+ }
1413
+ if (l.length > 0) {
1414
+ for (let e of l) this.fallbackQueues.delete(e), this.activeStreams.delete(e);
1415
+ this.activeStreams.size === 0 && this.removeUnlockHandler();
1416
+ }
1417
+ if (a > 1) {
1418
+ let e = 1 / Math.sqrt(a);
1419
+ for (let t = 0; t < i; t++) for (let r = 0; r < n; r++) c[r][t] *= e;
1420
+ }
1421
+ for (let e = 0; e < i; e++) for (let t = 0; t < n; t++) {
1422
+ let n = c[t][e];
1423
+ c[t][e] = n > 1 ? 1 : n < -1 ? -1 : n;
1424
+ }
1425
+ }, u.connect(e.destination), this.scriptProcessorNode = u, this.ensureScriptProcessorDiagnostics(t);
1426
+ }
1427
+ compactFallbackQueue(e) {
1428
+ if (e.headIndex <= 32 || e.headIndex <= e.chunks.length / 2) return;
1429
+ let t = e.chunks.length - e.headIndex;
1430
+ for (let n = 0; n < t; n++) e.chunks[n] = e.chunks[e.headIndex + n];
1431
+ e.chunks.length = t, e.headIndex = 0;
1432
+ }
1433
+ skipFromFallbackQueue(e, t) {
1434
+ if (t <= 0) return;
1435
+ let n = t;
1436
+ for (; n > 0 && e.headIndex < e.chunks.length;) {
1437
+ let t = e.chunks[e.headIndex], r = t.length - e.offset, i = Math.min(r, n);
1438
+ e.offset += i, e.totalSamples -= i, n -= i, e.offset >= t.length && (e.headIndex += 1, e.offset = 0);
1439
+ }
1440
+ e.totalSamples < 0 && (e.totalSamples = 0), this.compactFallbackQueue(e);
1441
+ }
1442
+ trimFallbackQueue(e) {
1443
+ let t = Math.floor(this.outputSampleRateHz * 2 * h / 1e3), n = e.totalSamples - t;
1444
+ n <= 0 || this.skipFromFallbackQueue(e, n);
1445
+ }
1446
+ stopScriptProcessorDiagnostics() {
1447
+ this.scriptDiagnosticsTimer &&= (clearInterval(this.scriptDiagnosticsTimer), null);
1448
+ }
1449
+ ensureScriptProcessorDiagnostics(e) {
1450
+ if (!e.enabled) {
1451
+ this.stopScriptProcessorDiagnostics();
1452
+ return;
1453
+ }
1454
+ if (this.scriptDiagnosticsTimer) return;
1455
+ let t = Math.max(250, e.statusIntervalMs);
1456
+ this.scriptDiagnosticsTimer = setInterval(() => {
1457
+ if (this.scriptProcessorNode) for (let [e, t] of this.fallbackQueues.entries()) {
1458
+ if (t.totalSamples <= 0) continue;
1459
+ let n = t.totalSamples / 2 * 1e3 / this.outputSampleRateHz;
1460
+ _.debug(`[Audio] stream=${e.substring(0, 8)} buffer=${n.toFixed(0)}ms renderer=ScriptProcessor`);
1461
+ }
1462
+ }, t);
1463
+ }
1464
+ readFromFallbackQueue(e, t) {
1465
+ let n = 0;
1466
+ for (; n < t.length && e.headIndex < e.chunks.length;) {
1467
+ let r = e.chunks[e.headIndex], i = r.length - e.offset, a = Math.min(i, t.length - n);
1468
+ t.set(r.subarray(e.offset, e.offset + a), n), n += a, e.offset += a, e.totalSamples = Math.max(0, e.totalSamples - a), e.offset >= r.length && (e.headIndex += 1, e.offset = 0);
1469
+ }
1470
+ n < t.length && t.fill(0, n), this.compactFallbackQueue(e);
1471
+ }
1472
+ async resumeAudioContextIfPossible(e, t, n) {
1473
+ if (this.getContextState(e) === "running") {
1474
+ this.removeUnlockHandler();
1475
+ return;
1476
+ }
1477
+ let r = n?.promptForUnlock ?? !0;
1478
+ if (!(t?.userGesture ?? !1)) {
1479
+ if (!r) return;
1480
+ this.addUnlockHandler();
1481
+ return;
1482
+ }
1483
+ try {
1484
+ await this.resumeWithTimeout(e), this.removeUnlockHandler();
1485
+ } catch (e) {
1486
+ _.warn(`Failed to resume AudioContext: ${y(e)}`), this.addUnlockHandler();
1487
+ }
1488
+ }
1489
+ async recoverAudioContext(e) {
1490
+ if (e.fromUserGesture && this.recoveryTask && this.audioContext) try {
1491
+ if (await this.resumeWithTimeout(this.audioContext), this.audioContext.state === "running") {
1492
+ this.removeUnlockHandler();
1493
+ return;
1494
+ }
1495
+ } catch {}
1496
+ if (this.recoveryTask) {
1497
+ await this.recoveryTask;
1498
+ return;
1499
+ }
1500
+ this.recoveryTask = this.recoverAudioContextInternal(e).finally(() => {
1501
+ this.recoveryTask = null;
1502
+ }), await this.recoveryTask;
1503
+ }
1504
+ async recoverAudioContextInternal(e) {
1505
+ if (!this.enabled) return;
1506
+ let t = this.activeStreams.size > 0, n = this.audioContext;
1507
+ if (!n) {
1508
+ if (!t) return;
1509
+ await this.ensureAudioGraph(void 0, { promptForUnlock: !0 });
1510
+ return;
1511
+ }
1512
+ if (n.sampleRate !== this.outputSampleRateHz) {
1513
+ _.warn(`AudioContext sample rate drifted (${n.sampleRate} Hz vs worklet ${this.outputSampleRateHz} Hz), rebuilding audio graph`), this.stopAudioGraph(), t && await this.ensureAudioGraph(void 0, { promptForUnlock: !0 });
1514
+ return;
1515
+ }
1516
+ if (this.getContextState(n) === "running") {
1517
+ this.removeUnlockHandler();
1518
+ return;
1519
+ }
1520
+ if (t) {
1521
+ try {
1522
+ if (await this.resumeWithTimeout(n), this.getContextState(n) === "running") {
1523
+ this.removeUnlockHandler();
1524
+ return;
1525
+ }
1526
+ } catch (t) {
1527
+ _.debug(`AudioContext resume failed or timed out (${e.reason}): ${t}`);
1528
+ }
1529
+ this.addUnlockHandler();
1530
+ }
1531
+ }
1532
+ rebindWorkerTransportIfNeeded() {
1533
+ if (!this.audioWorker || !this.audioContext) return;
1534
+ let e = (this.config.performance?.preferSharedArrayBuffer ?? !0) && this.audioWorkletNode && c() ? "sab" : "pcm";
1535
+ try {
1536
+ this.audioWorker.postMessage({
1537
+ type: "rebind",
1538
+ transport: e,
1539
+ outputSampleRate: this.outputSampleRateHz,
1540
+ outputChannels: 2
1541
+ });
1542
+ } catch {}
1543
+ }
1544
+ async ensureWorkers() {
1545
+ if (this.audioWorker) return;
1546
+ if (typeof Worker > "u") {
1547
+ _.warn("Audio decode worker unavailable (Workers not supported)");
1548
+ return;
1549
+ }
1550
+ let e = this.client.createProtocolMessagePort({ opcodeGroupsMask: t.GROUP_AUDIO });
1551
+ if (!e) return;
1552
+ this.protocolPort = e;
1553
+ try {
1554
+ this.audioWorker = new g();
1555
+ } catch (e) {
1556
+ _.warn(`Failed to create audio worker: ${y(e)}`), this.protocolPort.close(), this.protocolPort = null;
1557
+ return;
1558
+ }
1559
+ let n = this.audioWorker;
1560
+ n.addEventListener("message", (e) => this.onAudioWorkerMessage(e.data)), n.addEventListener("error", (e) => {
1561
+ let t = [
1562
+ e.message,
1563
+ e.filename,
1564
+ e.lineno,
1565
+ e.colno
1566
+ ].filter((e) => e != null && String(e).length > 0);
1567
+ _.warn(`Audio worker failed, rebuilding: ${t.join(" ")}`.trim()), this.handleWorkerCrash();
1568
+ });
1569
+ let r = (this.config.performance?.preferSharedArrayBuffer ?? !0) && !!this.audioWorkletNode && c(), i = this.config.performance?.preferWebCodecs ?? !0, a = this.client.isWebRtcMediaActive, o = {
1570
+ type: "configure",
1571
+ transport: r ? "sab" : "pcm",
1572
+ outputSampleRate: this.outputSampleRateHz,
1573
+ outputChannels: 2,
1574
+ preferWebCodecs: i,
1575
+ webRtcEnabled: a
1576
+ };
1577
+ n.postMessage(o), n.postMessage({
1578
+ type: "attachProtocolPort",
1579
+ port: e.port
1580
+ }, [e.port]);
1581
+ let s = this.client.globalState;
1582
+ s && this.syncStreamsFromGlobalState(s);
1583
+ let l = this.audioWorkletNode ? "AudioWorklet" : "ScriptProcessor", u = r ? "SharedArrayBuffer" : "MessagePort", d = a ? "WebRTC (native)" : i && typeof AudioDecoder < "u" ? "WebCodecs" : "WASM Opus";
1584
+ _.debug(`Audio initialized: renderer=${l}, transport=${u}, decoder=${d}, output=${this.outputSampleRateHz}Hz/2ch`);
1585
+ }
1586
+ onAudioWorkerMessage(e) {
1587
+ if (e.type === "streamBegin") {
1588
+ this.activeStreams.add(e.streamId);
1589
+ let t = this.audioContext;
1590
+ if (this.enabled && t && this.getContextState(t) !== "running" && this.recoverAudioContext({ reason: "streamBegin" }), this.audioWorkletNode) {
1591
+ this.postToWorklet({
1592
+ type: "streamBegin",
1593
+ streamId: e.streamId,
1594
+ sharedArrayBuffer: e.sharedArrayBuffer
1595
+ });
1596
+ return;
1597
+ }
1598
+ this.fallbackQueues.set(e.streamId, {
1599
+ chunks: [],
1600
+ headIndex: 0,
1601
+ offset: 0,
1602
+ totalSamples: 0,
1603
+ ending: !1,
1604
+ inputDrained: !1,
1605
+ fadeGain: 0,
1606
+ fadeDirection: "in",
1607
+ lastOut: null,
1608
+ hasLastOut: !1,
1609
+ underrunPending: !1
1610
+ });
1611
+ return;
1612
+ }
1613
+ if (e.type === "streamEnd") {
1614
+ let t = this.fallbackQueues.get(e.streamId);
1615
+ if (t) {
1616
+ t.ending = !0, t.totalSamples <= 0 && (this.fallbackQueues.delete(e.streamId), this.activeStreams.delete(e.streamId), this.activeStreams.size === 0 && this.removeUnlockHandler());
1617
+ return;
1618
+ }
1619
+ if (this.audioWorkletNode) {
1620
+ this.postToWorklet({
1621
+ type: "streamEnd",
1622
+ streamId: e.streamId
1623
+ });
1624
+ return;
1625
+ }
1626
+ this.activeStreams.delete(e.streamId), this.activeStreams.size === 0 && this.removeUnlockHandler();
1627
+ return;
1628
+ }
1629
+ if (e.type === "pcm") {
1630
+ if (this.audioWorkletNode) {
1631
+ this.postToWorklet({
1632
+ type: "pcm",
1633
+ streamId: e.streamId,
1634
+ pcm: e.pcm
1635
+ }, [e.pcm.buffer]);
1636
+ return;
1637
+ }
1638
+ let t = this.fallbackQueues.get(e.streamId);
1639
+ t && (t.underrunPending && t.totalSamples === 0 && (t.fadeGain = 0, t.fadeDirection = "in", t.underrunPending = !1), t.chunks.push(e.pcm), t.totalSamples += e.pcm.length, this.trimFallbackQueue(t));
1640
+ return;
1641
+ }
1642
+ if (e.type === "frame") {
1643
+ let t = this.fallbackQueues.get(e.streamId);
1644
+ t && (t.inputDrained && !e.isFirst && (t.inputDrained = !1, t.fadeGain = 0, t.fadeDirection = "in", t.underrunPending = !1), e.isFirst && (t.inputDrained = !1, t.fadeGain = 0, t.fadeDirection = "in", t.underrunPending = !1), e.isLast && (t.inputDrained = !0)), this.postToWorklet({
1645
+ type: "frame",
1646
+ streamId: e.streamId,
1647
+ epoch: e.epoch,
1648
+ timestampInterleavedSamples: e.timestampInterleavedSamples,
1649
+ frameSizeInterleavedSamples: e.frameSizeInterleavedSamples,
1650
+ isFirst: e.isFirst,
1651
+ isLast: e.isLast,
1652
+ jitterMs: e.jitterMs
1653
+ });
1654
+ return;
1655
+ }
1656
+ e.type === "error" && _.warn(`Audio worker error${e.streamId ? ` (stream ${e.streamId})` : ""}: ${e.error.message}`);
1657
+ }
1658
+ postToWorklet(e, t) {
1659
+ if (!this.audioWorkletNode) {
1660
+ if (this.scriptProcessorNode) return;
1661
+ this.enqueuePendingWorkletMessage(e);
1662
+ return;
1663
+ }
1664
+ try {
1665
+ this.audioWorkletNode.port.postMessage(e, t ?? []);
1666
+ } catch {
1667
+ this.enqueuePendingWorkletMessage(e);
1668
+ }
1669
+ }
1670
+ notifyPlaybackActive(e) {
1671
+ this.postToWorklet({
1672
+ type: "playbackActive",
1673
+ active: e
1674
+ }), this.reportPlaybackStateIfChanged();
1675
+ }
1676
+ computePlaybackState() {
1677
+ let t = this.audioContext;
1678
+ return this.workerGivenUp || !t || this.getContextState(t) !== "running" || this.awaitingUnlock ? e.Blocked : this.hiddenMuted ? e.Hidden : e.Playing;
1679
+ }
1680
+ sendPlaybackReport(e, t, n, r) {
1681
+ let o = e.indexOf("_");
1682
+ if (o <= 0) return;
1683
+ let s = Number(e.substring(0, o)), c = Number(e.substring(o + 1));
1684
+ if (!Number.isFinite(s) || !Number.isFinite(c)) return;
1685
+ let l = this.computePlaybackState();
1686
+ this.lastReportedPlaybackState = l;
1687
+ try {
1688
+ let e = i({
1689
+ Epoch: t,
1690
+ PlayedSeconds: n,
1691
+ BufferedMs: r,
1692
+ State: l
1693
+ });
1694
+ this.client.sendProtocolMessage(a(e, this.client.sessionId ?? 0, {
1695
+ trackId: c,
1696
+ targetIds: [s]
1697
+ }));
1698
+ } catch {}
1699
+ }
1700
+ reportPlaybackStateIfChanged() {
1701
+ let e = this.computePlaybackState();
1702
+ if (e !== this.lastReportedPlaybackState) {
1703
+ for (let [e, t] of this.lastPlaybackByStream) this.sendPlaybackReport(e, t.epoch, t.playedSeconds, t.bufferMs);
1704
+ this.lastReportedPlaybackState = e;
1705
+ }
1706
+ }
1707
+ enqueuePendingWorkletMessage(e) {
1708
+ if (e.type === "streamBegin") {
1709
+ let t = this.pendingWorkletMessages.findIndex((t) => t.type === "streamBegin" && t.streamId === e.streamId);
1710
+ t >= 0 && this.pendingWorkletMessages.splice(t, 1);
1711
+ }
1712
+ if (this.pendingWorkletMessages.push(e), !(this.pendingWorkletMessages.length <= 512)) for (; !(this.pendingWorkletMessages.length <= 512);) {
1713
+ let e = this.pendingWorkletMessages.findIndex((e) => e.type !== "streamBegin");
1714
+ if (e < 0) break;
1715
+ this.pendingWorkletMessages.splice(e, 1);
1716
+ }
1717
+ }
1718
+ stopAudioWorklet() {
1719
+ if (this.audioWorkletNode) {
1720
+ try {
1721
+ this.audioWorkletNode.disconnect();
1722
+ } catch {}
1723
+ this.audioWorkletNode = null;
1724
+ }
1725
+ }
1726
+ stopScriptProcessor() {
1727
+ if (this.scriptProcessorNode) {
1728
+ try {
1729
+ this.scriptProcessorNode.disconnect();
1730
+ } catch {}
1731
+ this.scriptProcessorNode.onaudioprocess = null, this.scriptProcessorNode = null;
1732
+ }
1733
+ this.stopScriptProcessorDiagnostics();
1734
+ }
1735
+ stopAudioGraph() {
1736
+ if (this.stopAudioWorklet(), this.stopScriptProcessor(), this.fallbackQueues.clear(), this.audioContext &&= (this.audioContext.close().catch(() => void 0), null), this.workletModuleUrl) {
1737
+ try {
1738
+ URL.revokeObjectURL(this.workletModuleUrl);
1739
+ } catch {}
1740
+ this.workletModuleUrl = null, this.workletModuleKey = null;
1741
+ }
1742
+ this.workletReady = null, this.pendingWorkletMessages = [];
1743
+ }
1744
+ handleWorkerCrash() {
1745
+ if (this.stopWorkers(), this.workerRebuildAttempts >= n.MAX_WORKER_REBUILD_ATTEMPTS) {
1746
+ _.error("Audio worker crashed repeatedly, giving up on rebuilds; audio playback is off for the rest of the session"), this.workerGivenUp = !0, this.reportPlaybackStateIfChanged(), this.client.reportError(/* @__PURE__ */ Error("Audio playback worker crashed repeatedly; audio playback is off for the rest of the session"));
1747
+ return;
1748
+ }
1749
+ this.workerRebuildAttempts++, this.ensureWorkers().then(() => {
1750
+ let e = this.client.globalState;
1751
+ e && this.syncStreamsFromGlobalState(e);
1752
+ });
1753
+ }
1754
+ stopWorkers() {
1755
+ let e = Array.from(this.activeStreams);
1756
+ if (this.audioWorker) {
1757
+ try {
1758
+ this.audioWorker.postMessage({ type: "dispose" });
1759
+ } catch {}
1760
+ try {
1761
+ this.audioWorker.terminate();
1762
+ } catch {}
1763
+ this.audioWorker = null;
1764
+ }
1765
+ if (this.protocolPort &&= (this.protocolPort.close(), null), this.audioWorkletNode) for (let t of e) this.postToWorklet({
1766
+ type: "streamEnd",
1767
+ streamId: t
1768
+ });
1769
+ this.activeStreams.clear(), this.fallbackQueues.clear(), this.removeUnlockHandler();
1770
+ }
1771
+ ensureStateSubscription() {
1772
+ this.stateUnsubscribe ||= this.client.subscribeToState((e) => {
1773
+ if (this.enabled) {
1774
+ if (e === "connected" || e === "reconnecting") {
1775
+ this.ensureWorkers();
1776
+ return;
1777
+ }
1778
+ (e === "idle" || e === "offline" || e === "offlineError" || e === "connecting" || e === "connectingSlow") && this.stopWorkers();
1779
+ }
1780
+ });
1781
+ }
1782
+ };
1783
+ //#endregion
1784
+ //#region src/worker/video-playback-worker.ts?worker
1785
+ function S(e) {
1786
+ return new Worker(new URL("assets/video-playback-worker-v179IJ-C.js", import.meta.url).href, {
1787
+ type: "module",
1788
+ name: e?.name
1789
+ });
1790
+ }
1791
+ //#endregion
1792
+ //#region src/media/ikon-video-playback.ts
1793
+ var C = n("IkonVideoPlayback");
1794
+ function w(e) {
1795
+ return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
1796
+ }
1797
+ var T = class e {
1798
+ static MAX_WORKER_REBUILD_ATTEMPTS = 3;
1799
+ client;
1800
+ config;
1801
+ enabled = !0;
1802
+ workerRebuildAttempts = 0;
1803
+ stateUnsubscribe = null;
1804
+ worker = null;
1805
+ protocolPort = null;
1806
+ surfaces = /* @__PURE__ */ new Map();
1807
+ constructor(e, t) {
1808
+ this.client = e, this.config = t ?? {}, this.ensureStateSubscription();
1809
+ }
1810
+ async attachCanvas(e, t) {
1811
+ if (!e || typeof e != "string") throw Error("streamId is required");
1812
+ this.detachCanvas(e);
1813
+ let n = this.canUseOffscreenCanvas(t) ? "offscreen" : "bitmap", r = null, i = null;
1814
+ if (n === "bitmap" && (i = t.getContext("bitmaprenderer", { alpha: !1 }), i || (r = t.getContext("2d", {
1815
+ alpha: !1,
1816
+ desynchronized: !0
1817
+ }))), this.surfaces.set(e, {
1818
+ canvas: t,
1819
+ ctx2d: r,
1820
+ ctxBitmap: i,
1821
+ mode: n,
1822
+ attachedToWorker: !1
1823
+ }), this.ensureStateSubscription(), !this.enabled) {
1824
+ C.warn(`Canvas for stream ${e} not attached to the worker: video playback is disabled`);
1825
+ return;
1826
+ }
1827
+ await this.ensureWorker(), this.ensureProtocolPort(), this.syncStreamToWorker(e), this.surfaces.get(e)?.attachedToWorker || C.warn(`Canvas for stream ${e} did not reach the worker; it will stay blank`);
1828
+ }
1829
+ detachCanvas(e) {
1830
+ let t = this.surfaces.get(e);
1831
+ if (t && (this.surfaces.delete(e), this.worker)) try {
1832
+ this.worker.postMessage({
1833
+ type: "unwatchStream",
1834
+ streamId: e
1835
+ }), t.mode === "offscreen" && this.worker.postMessage({
1836
+ type: "detachSurface",
1837
+ streamId: e
1838
+ });
1839
+ } catch {}
1840
+ }
1841
+ async setEnabled(e) {
1842
+ if (this.enabled = e, this.ensureStateSubscription(), !e) {
1843
+ if (this.worker) try {
1844
+ this.worker.postMessage({
1845
+ type: "setEnabled",
1846
+ enabled: !1
1847
+ });
1848
+ } catch {}
1849
+ this.detachProtocolPort();
1850
+ return;
1851
+ }
1852
+ if (await this.ensureWorker(), this.ensureProtocolPort(), this.worker) {
1853
+ try {
1854
+ this.worker.postMessage({
1855
+ type: "setEnabled",
1856
+ enabled: !0
1857
+ });
1858
+ } catch {}
1859
+ this.syncAllStreamsToWorker();
1860
+ }
1861
+ }
1862
+ dispose() {
1863
+ this.enabled = !1, this.stateUnsubscribe?.(), this.stateUnsubscribe = null, this.surfaces.clear(), this.stopWorker();
1864
+ }
1865
+ canUseOffscreenCanvas(e) {
1866
+ return this.config.performance?.preferOffscreenCanvas ?? !0 ? "transferControlToOffscreen" in e && typeof e.transferControlToOffscreen == "function" : !1;
1867
+ }
1868
+ async ensureWorker() {
1869
+ if (!this.enabled || this.worker) return;
1870
+ if (typeof Worker > "u") {
1871
+ C.warn("Video decode worker unavailable (Workers not supported)");
1872
+ return;
1873
+ }
1874
+ let e;
1875
+ try {
1876
+ e = new S();
1877
+ } catch (e) {
1878
+ C.warn(`Failed to create video worker: ${w(e)}`);
1879
+ return;
1880
+ }
1881
+ this.worker = e, e.addEventListener("message", (e) => this.onWorkerMessage(e.data)), e.addEventListener("error", (e) => {
1882
+ let t = [
1883
+ e.message,
1884
+ e.filename,
1885
+ e.lineno,
1886
+ e.colno
1887
+ ].filter((e) => e != null && String(e).length > 0);
1888
+ C.warn(`Video worker failed, rebuilding: ${t.join(" ")}`.trim()), this.handleWorkerCrash();
1889
+ }), e.postMessage({
1890
+ type: "configure",
1891
+ webRtcEnabled: this.client.isWebRtcMediaActive
1892
+ }), e.postMessage({
1893
+ type: "setEnabled",
1894
+ enabled: this.enabled
1895
+ }), this.syncAllStreamsToWorker();
1896
+ let t = this.client.globalState;
1897
+ t && this.syncStreamsFromGlobalState(t);
1898
+ }
1899
+ onWorkerMessage(e) {
1900
+ if (e.type !== "streamBegin" && e.type !== "streamEnd") {
1901
+ if (e.type === "frame") {
1902
+ let t = this.surfaces.get(e.streamId);
1903
+ if (!t || t.mode !== "bitmap") {
1904
+ try {
1905
+ e.bitmap.close();
1906
+ } catch {}
1907
+ return;
1908
+ }
1909
+ if (t.ctxBitmap) {
1910
+ try {
1911
+ t.ctxBitmap.transferFromImageBitmap(e.bitmap);
1912
+ } catch {}
1913
+ return;
1914
+ }
1915
+ if (!t.ctx2d) {
1916
+ try {
1917
+ e.bitmap.close();
1918
+ } catch {}
1919
+ return;
1920
+ }
1921
+ (t.canvas.width !== e.width || t.canvas.height !== e.height) && (t.canvas.width = e.width, t.canvas.height = e.height);
1922
+ try {
1923
+ t.ctx2d.drawImage(e.bitmap, 0, 0, e.width, e.height);
1924
+ } finally {
1925
+ try {
1926
+ e.bitmap.close();
1927
+ } catch {}
1928
+ }
1929
+ return;
1930
+ }
1931
+ if (e.type === "requestIdr") {
1932
+ this.requestIdrForStream(e.senderId, e.trackId);
1933
+ return;
1934
+ }
1935
+ if (e.type === "error") {
1936
+ C.warn(`Video worker error: ${e.error.message}`);
1937
+ return;
1938
+ }
1939
+ e.type === "log" ? C.info(e.message) : e.type === "debug" && C.debug(e.message);
1940
+ }
1941
+ }
1942
+ syncAllStreamsToWorker() {
1943
+ for (let e of this.surfaces.keys()) this.syncStreamToWorker(e);
1944
+ }
1945
+ syncStreamToWorker(e) {
1946
+ if (!this.worker) return;
1947
+ let t = this.surfaces.get(e);
1948
+ if (t && !t.attachedToWorker) {
1949
+ if (t.mode === "offscreen") {
1950
+ try {
1951
+ let n = t.canvas.transferControlToOffscreen();
1952
+ this.worker.postMessage({
1953
+ type: "attachSurface",
1954
+ streamId: e,
1955
+ canvas: n
1956
+ }, [n]), t.attachedToWorker = !0;
1957
+ } catch (n) {
1958
+ C.warn(`Failed to attach offscreen canvas for stream ${e}: ${w(n)}`), t.mode = "bitmap", t.ctxBitmap = t.canvas.getContext("bitmaprenderer", { alpha: !1 }), t.ctxBitmap || (t.ctx2d = t.canvas.getContext("2d", {
1959
+ alpha: !1,
1960
+ desynchronized: !0
1961
+ }));
1962
+ try {
1963
+ this.worker.postMessage({
1964
+ type: "watchStream",
1965
+ streamId: e
1966
+ }), t.attachedToWorker = !0;
1967
+ } catch {}
1968
+ }
1969
+ return;
1970
+ }
1971
+ try {
1972
+ this.worker.postMessage({
1973
+ type: "watchStream",
1974
+ streamId: e
1975
+ }), t.attachedToWorker = !0;
1976
+ } catch {}
1977
+ }
1978
+ }
1979
+ requestIdrForStream(e, t) {
1980
+ let n = s(r(), this.client.sessionId ?? 0, {
1981
+ trackId: t,
1982
+ targetIds: [e]
1983
+ });
1984
+ this.client.sendProtocolMessage(n);
1985
+ }
1986
+ detachProtocolPort() {
1987
+ if (this.protocolPort) {
1988
+ try {
1989
+ this.protocolPort.close();
1990
+ } catch {}
1991
+ this.protocolPort = null;
1992
+ }
1993
+ }
1994
+ ensureProtocolPort() {
1995
+ if (!this.worker || this.protocolPort) return;
1996
+ let e = this.client.createProtocolMessagePort({ opcodeGroupsMask: t.GROUP_VIDEO });
1997
+ if (e) {
1998
+ this.protocolPort = e;
1999
+ try {
2000
+ this.worker.postMessage({
2001
+ type: "attachProtocolPort",
2002
+ port: e.port
2003
+ }, [e.port]);
2004
+ } catch (e) {
2005
+ C.warn(`Failed to attach video protocol port: ${w(e)}`), this.protocolPort.close(), this.protocolPort = null;
2006
+ }
2007
+ }
2008
+ }
2009
+ syncStreamsFromGlobalState(e) {
2010
+ if (!this.worker) return;
2011
+ let t = Object.values(e.VideoStreams ?? {}).map((e) => ({
2012
+ senderId: e.ClientSessionId,
2013
+ trackId: e.TrackId,
2014
+ info: e.Info
2015
+ }));
2016
+ if (t.length !== 0) try {
2017
+ this.worker.postMessage({
2018
+ type: "syncStreams",
2019
+ streams: t
2020
+ });
2021
+ } catch {}
2022
+ }
2023
+ setWebRtcActive(e) {
2024
+ if (this.worker) try {
2025
+ this.worker.postMessage({
2026
+ type: "setWebRtcEnabled",
2027
+ enabled: e
2028
+ });
2029
+ } catch {}
2030
+ }
2031
+ handleProtocolTransportLost() {
2032
+ this.worker && this.stopWorker();
2033
+ }
2034
+ handleWorkerCrash() {
2035
+ if (this.stopWorker(), this.workerRebuildAttempts >= e.MAX_WORKER_REBUILD_ATTEMPTS) {
2036
+ C.error("Video worker crashed repeatedly, giving up on rebuilds; video playback is off for the rest of the session"), this.client.reportError(/* @__PURE__ */ Error("Video playback worker crashed repeatedly; video playback is off for the rest of the session"));
2037
+ return;
2038
+ }
2039
+ this.workerRebuildAttempts++, this.ensureWorker().then(() => this.ensureProtocolPort());
2040
+ }
2041
+ stopWorker() {
2042
+ if (this.detachProtocolPort(), this.worker) {
2043
+ try {
2044
+ this.worker.postMessage({ type: "dispose" });
2045
+ } catch {}
2046
+ try {
2047
+ this.worker.terminate();
2048
+ } catch {}
2049
+ this.worker = null;
2050
+ }
2051
+ }
2052
+ ensureStateSubscription() {
2053
+ this.stateUnsubscribe ||= this.client.subscribeToState((e) => {
2054
+ if (this.enabled) {
2055
+ if (e === "connected" || e === "reconnecting") {
2056
+ this.ensureWorker().then(() => this.ensureProtocolPort());
2057
+ return;
2058
+ }
2059
+ (e === "idle" || e === "offline" || e === "offlineError" || e === "connecting" || e === "connectingSlow") && this.detachProtocolPort();
2060
+ }
2061
+ });
2062
+ }
2063
+ }, E = class {
2064
+ audio;
2065
+ video;
2066
+ constructor(e, t) {
2067
+ this.audio = new x(e, t?.audio), this.video = new T(e, t?.video), t?.mediaSession && this.audio.setMediaSession(t.mediaSession);
2068
+ }
2069
+ dispose() {
2070
+ this.audio.dispose(), this.video.dispose();
2071
+ }
2072
+ };
2073
+ //#endregion
2074
+ export { E as IkonMedia };