@wvdsh/sdk-js 1.3.25 → 1.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -94,69 +94,30 @@ var WavedashManager = class {
94
94
  };
95
95
 
96
96
  // src/services/audio.ts
97
- var WeakRefSet = class {
98
- constructor() {
99
- this.set = /* @__PURE__ */ new Set();
100
- }
101
- add(value) {
102
- for (const ref of this.set) {
103
- if (ref.deref() === value) return;
104
- }
105
- this.set.add(new WeakRef(value));
106
- }
107
- forEach(callback) {
108
- for (const ref of this.set) {
109
- const v = ref.deref();
110
- if (v === void 0) this.set.delete(ref);
111
- else callback(v);
112
- }
113
- }
114
- clear() {
115
- this.set.clear();
116
- }
117
- };
118
97
  var AudioManager = class extends WavedashManager {
119
98
  constructor(sdk) {
120
99
  super(sdk);
121
100
  this._isMuted = false;
122
- // Web Audio contexts and their master gain nodes
123
- this.contexts = /* @__PURE__ */ new Map();
124
- // HTML media elements we know about + their game-intended muted state
125
- this.elements = new WeakRefSet();
126
- this.intendedMuted = /* @__PURE__ */ new WeakMap();
127
- // Speech synthesis utterances + their game-intended volume
128
- this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
129
- // Originals (restored on destroy)
130
- this.originalAudioContext = null;
131
- this.originalWebKitAudioContext = null;
132
- this.originalAudio = null;
133
- this.originalMutedDescriptor = null;
134
- this.originalPlay = null;
135
- this.originalSpeak = null;
136
- this.originalUtteranceVolumeDescriptor = null;
137
- this.mutationObserver = null;
101
+ // One shim per frame we've attached to.
102
+ this.frames = /* @__PURE__ */ new Set();
103
+ // Per-iframe state so we can re-shim across navigations/swaps and tear down
104
+ // the right frame when an iframe is removed or re-navigates. Keyed on the
105
+ // Document (replaced on every navigation), not contentWindow (a stable
106
+ // WindowProxy that survives navigations and so can't reveal a realm change).
107
+ this.iframeBindings = /* @__PURE__ */ new WeakMap();
108
+ this.iframeLoadHandlers = /* @__PURE__ */ new Map();
109
+ this.boundIframes = /* @__PURE__ */ new Set();
138
110
  this.handleMute = (data) => {
139
111
  if (this._isMuted === data.isMuted) return;
140
112
  this._isMuted = data.isMuted;
141
- const target = this._isMuted ? 0 : 1;
142
- this.contexts.forEach((gain, ctx) => {
143
- const now = ctx.currentTime;
144
- gain.gain.cancelScheduledValues(now);
145
- gain.gain.setValueAtTime(gain.gain.value, now);
146
- gain.gain.linearRampToValueAtTime(target, now + 0.05);
147
- });
148
- const setMutedNative = this.originalMutedDescriptor?.set;
149
- if (setMutedNative) {
150
- this.elements.forEach((el) => {
151
- const intended = this.intendedMuted.get(el) ?? false;
152
- setMutedNative.call(el, this._isMuted ? true : intended);
153
- });
154
- }
113
+ this.frames.forEach((shim) => shim.applyMute(this._isMuted));
155
114
  this.sdk.gameEventManager.notifyGame(WavedashEvents.MUTE_CHANGED, {
156
115
  isMuted: this._isMuted
157
116
  });
158
117
  };
159
- this.installShims();
118
+ if (typeof window !== "undefined") {
119
+ this.attachWindow(window);
120
+ }
160
121
  this.sdk.iframeMessenger.addEventListener(
161
122
  IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
162
123
  this.handleMute
@@ -190,10 +151,148 @@ var AudioManager = class extends WavedashManager {
190
151
  );
191
152
  return response.success;
192
153
  }
154
+ /** Shim a window we can reach. Same-origin only (cross-origin access throws). */
155
+ attachWindow(win) {
156
+ try {
157
+ void win.document;
158
+ } catch {
159
+ return;
160
+ }
161
+ const shim = new AudioFrameShim(this, win);
162
+ this.frames.add(shim);
163
+ }
164
+ /**
165
+ * Start tracking an iframe: attach now (already-loaded frames) and on every
166
+ * `load` (about:blank → game, and later src swaps). Idempotent.
167
+ */
168
+ bindIframe(iframe) {
169
+ if (!this.boundIframes.has(iframe)) {
170
+ this.boundIframes.add(iframe);
171
+ const onLoad = () => this.attachIframe(iframe);
172
+ this.iframeLoadHandlers.set(iframe, onLoad);
173
+ iframe.addEventListener("load", onLoad);
174
+ }
175
+ this.attachIframe(iframe);
176
+ }
177
+ /** Stop tracking an iframe and tear down its frame (iframe removed from DOM). */
178
+ unbindIframe(iframe) {
179
+ const handler = this.iframeLoadHandlers.get(iframe);
180
+ if (handler) {
181
+ iframe.removeEventListener("load", handler);
182
+ this.iframeLoadHandlers.delete(iframe);
183
+ }
184
+ this.boundIframes.delete(iframe);
185
+ this.teardownFrame(iframe);
186
+ }
193
187
  /**
194
- * Track a media element and (if SDK is currently muted) silence it.
195
- * Idempotent safe to call multiple times for the same element.
188
+ * Install (or re-install) a shim for an iframe's current document. No-ops
189
+ * while not yet navigated or already shimmed; replaces the previous shim when
190
+ * the iframe navigates to a fresh document, and drops it when it goes
191
+ * cross-origin (we can no longer reach it).
196
192
  */
193
+ attachIframe(iframe) {
194
+ let win = null;
195
+ let doc = null;
196
+ try {
197
+ const cw = iframe.contentWindow;
198
+ if (cw) {
199
+ doc = cw.document;
200
+ win = cw;
201
+ }
202
+ } catch {
203
+ }
204
+ if (!win || !doc) {
205
+ this.teardownFrame(iframe);
206
+ return;
207
+ }
208
+ const existing = this.iframeBindings.get(iframe);
209
+ if (existing) {
210
+ if (existing.doc === doc) return;
211
+ this.teardownFrame(iframe);
212
+ }
213
+ const shim = new AudioFrameShim(this, win);
214
+ this.frames.add(shim);
215
+ this.iframeBindings.set(iframe, { doc, shim });
216
+ if (this._isMuted) shim.applyMute(true);
217
+ }
218
+ /** Remove and uninstall the shim bound to an iframe's (previous) document. */
219
+ teardownFrame(iframe) {
220
+ const binding = this.iframeBindings.get(iframe);
221
+ if (!binding) return;
222
+ this.frames.delete(binding.shim);
223
+ binding.shim.uninstall();
224
+ this.iframeBindings.delete(iframe);
225
+ }
226
+ destroy() {
227
+ this.sdk.iframeMessenger.removeEventListener(
228
+ IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
229
+ this.handleMute
230
+ );
231
+ this.boundIframes.forEach((iframe) => {
232
+ const handler = this.iframeLoadHandlers.get(iframe);
233
+ if (handler) iframe.removeEventListener("load", handler);
234
+ });
235
+ this.boundIframes.clear();
236
+ this.iframeLoadHandlers.clear();
237
+ this.frames.forEach((shim) => shim.uninstall());
238
+ this.frames.clear();
239
+ super.destroy();
240
+ }
241
+ };
242
+ var AudioFrameShim = class {
243
+ constructor(manager, win) {
244
+ this.contexts = /* @__PURE__ */ new Map();
245
+ // Tracked media elements + the game's intended muted value (what it last set).
246
+ this.elements = new WeakRefSet();
247
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
248
+ // Utterances + the game's intended volume.
249
+ this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
250
+ // Child iframes discovered in this frame's document. Tracked so we can
251
+ // cascade-unbind them when this frame is torn down — their own removal events
252
+ // never fire when the containing document is discarded wholesale.
253
+ this.boundChildren = /* @__PURE__ */ new Set();
254
+ // Originals, restored on uninstall.
255
+ this.originalAudioContext = null;
256
+ this.originalWebKitAudioContext = null;
257
+ this.originalAudio = null;
258
+ this.originalMutedDescriptor = null;
259
+ this.originalPlay = null;
260
+ this.originalSpeak = null;
261
+ this.originalUtteranceVolumeDescriptor = null;
262
+ this.mutationObserver = null;
263
+ this.manager = manager;
264
+ this.win = win;
265
+ this.doc = win.document ?? null;
266
+ this.installShims();
267
+ }
268
+ /** Push the current mute state onto everything this frame is tracking. */
269
+ applyMute(isMuted) {
270
+ const target = isMuted ? 0 : 1;
271
+ this.contexts.forEach((gain, ctx) => {
272
+ const now = ctx.currentTime;
273
+ gain.gain.cancelScheduledValues(now);
274
+ gain.gain.setValueAtTime(gain.gain.value, now);
275
+ gain.gain.linearRampToValueAtTime(target, now + 0.05);
276
+ });
277
+ const setMutedNative = this.originalMutedDescriptor?.set;
278
+ if (setMutedNative) {
279
+ this.elements.forEach((el) => {
280
+ const intended = this.intendedMuted.get(el) ?? false;
281
+ setMutedNative.call(el, isMuted ? true : intended);
282
+ });
283
+ }
284
+ }
285
+ /** Hand a discovered child iframe to the manager, remembering it for teardown. */
286
+ bindChild(iframe) {
287
+ this.boundChildren.add(iframe);
288
+ this.manager.bindIframe(iframe);
289
+ }
290
+ /** Stop tracking a child iframe that was removed from this document. */
291
+ unbindChild(iframe) {
292
+ this.boundChildren.delete(iframe);
293
+ this.manager.unbindIframe(iframe);
294
+ }
295
+ /** Track a media element and silence it if currently muted. Idempotent. */
197
296
  trackElement(el) {
198
297
  if (this.intendedMuted.has(el)) return;
199
298
  const getMuted = this.originalMutedDescriptor?.get;
@@ -201,139 +300,161 @@ var AudioManager = class extends WavedashManager {
201
300
  const current = getMuted ? getMuted.call(el) : el.muted;
202
301
  this.intendedMuted.set(el, current);
203
302
  this.elements.add(el);
204
- if (this._isMuted && !current && setMuted) {
303
+ if (this.manager.isMuted() && !current && setMuted) {
205
304
  setMuted.call(el, true);
206
305
  }
207
306
  }
208
307
  installShims() {
209
- if (typeof window === "undefined") return;
210
- if (window.AudioContext) {
211
- this.originalAudioContext = window.AudioContext;
212
- window.AudioContext = this.shimAudioContextClass(window.AudioContext);
213
- }
214
- const win = window;
215
- if (win.webkitAudioContext) {
216
- this.originalWebKitAudioContext = win.webkitAudioContext;
217
- win.webkitAudioContext = this.shimAudioContextClass(
218
- win.webkitAudioContext
219
- );
220
- }
221
- if (window.Audio) {
222
- const OriginalAudio = window.Audio;
308
+ const win = this.win;
309
+ const doc = this.doc;
310
+ if (win.AudioContext) {
311
+ this.originalAudioContext = win.AudioContext;
312
+ win.AudioContext = this.shimAudioContextClass(win.AudioContext);
313
+ }
314
+ const w = win;
315
+ if (w.webkitAudioContext) {
316
+ this.originalWebKitAudioContext = w.webkitAudioContext;
317
+ w.webkitAudioContext = this.shimAudioContextClass(w.webkitAudioContext);
318
+ }
319
+ if (win.Audio) {
320
+ const OriginalAudio = win.Audio;
223
321
  this.originalAudio = OriginalAudio;
224
- ((manager) => {
322
+ ((shim) => {
225
323
  const Shimmed = function(src) {
226
324
  const audio = new OriginalAudio(src);
227
- manager.trackElement(audio);
325
+ shim.trackElement(audio);
228
326
  return audio;
229
327
  };
230
328
  Shimmed.prototype = OriginalAudio.prototype;
231
- window.Audio = Shimmed;
329
+ win.Audio = Shimmed;
232
330
  })(this);
233
331
  }
234
- if (typeof document !== "undefined") {
235
- document.querySelectorAll("audio, video").forEach((el) => {
332
+ if (doc) {
333
+ const HTMLMediaElementCtor = win.HTMLMediaElement;
334
+ const HTMLIFrameElementCtor = win.HTMLIFrameElement;
335
+ const HTMLElementCtor = win.HTMLElement;
336
+ doc.querySelectorAll("audio, video").forEach((el) => {
236
337
  this.trackElement(el);
237
338
  });
238
- this.mutationObserver = new MutationObserver((mutations) => {
339
+ doc.querySelectorAll("iframe").forEach((el) => {
340
+ this.bindChild(el);
341
+ });
342
+ this.mutationObserver = new win.MutationObserver((mutations) => {
239
343
  for (const m of mutations) {
240
344
  m.addedNodes.forEach((node) => {
241
- if (node instanceof HTMLMediaElement) {
345
+ if (node instanceof HTMLMediaElementCtor) {
242
346
  this.trackElement(node);
243
- } else if (node instanceof HTMLElement) {
244
- node.querySelectorAll("audio, video").forEach((el) => {
245
- this.trackElement(el);
347
+ } else if (node instanceof HTMLIFrameElementCtor) {
348
+ this.bindChild(node);
349
+ } else if (node instanceof HTMLElementCtor) {
350
+ const el = node;
351
+ el.querySelectorAll("audio, video").forEach((m2) => {
352
+ this.trackElement(m2);
353
+ });
354
+ el.querySelectorAll("iframe").forEach((f) => {
355
+ this.bindChild(f);
356
+ });
357
+ }
358
+ });
359
+ m.removedNodes.forEach((node) => {
360
+ if (node instanceof HTMLIFrameElementCtor) {
361
+ this.unbindChild(node);
362
+ } else if (node instanceof HTMLElementCtor) {
363
+ node.querySelectorAll("iframe").forEach((f) => {
364
+ this.unbindChild(f);
246
365
  });
247
366
  }
248
367
  });
249
368
  }
250
369
  });
251
- this.mutationObserver.observe(document.documentElement, {
370
+ this.mutationObserver.observe(doc.documentElement, {
252
371
  childList: true,
253
372
  subtree: true
254
373
  });
255
374
  }
256
- this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "muted") ?? null;
375
+ this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(
376
+ win.HTMLMediaElement.prototype,
377
+ "muted"
378
+ ) ?? null;
257
379
  const original = this.originalMutedDescriptor;
258
380
  if (original?.get && original?.set) {
259
- ((manager) => {
260
- Object.defineProperty(HTMLMediaElement.prototype, "muted", {
381
+ ((shim) => {
382
+ Object.defineProperty(win.HTMLMediaElement.prototype, "muted", {
261
383
  configurable: true,
262
384
  get() {
263
- const intended = manager.intendedMuted.get(this);
385
+ const intended = shim.intendedMuted.get(this);
264
386
  return intended !== void 0 ? intended : original.get.call(this);
265
387
  },
266
388
  set(value) {
267
- manager.intendedMuted.set(this, value);
268
- manager.elements.add(this);
269
- original.set.call(this, manager._isMuted ? true : value);
389
+ shim.intendedMuted.set(this, value);
390
+ shim.elements.add(this);
391
+ original.set.call(this, shim.manager.isMuted() ? true : value);
270
392
  }
271
393
  });
272
394
  })(this);
273
395
  }
274
- const originalPlay = HTMLMediaElement.prototype.play;
396
+ const originalPlay = win.HTMLMediaElement.prototype.play;
275
397
  this.originalPlay = originalPlay;
276
- ((manager) => {
277
- HTMLMediaElement.prototype.play = function() {
278
- manager.trackElement(this);
398
+ ((shim) => {
399
+ win.HTMLMediaElement.prototype.play = function() {
400
+ shim.trackElement(this);
279
401
  return originalPlay.call(this);
280
402
  };
281
403
  })(this);
282
404
  this.shimSpeechSynthesis();
283
405
  }
284
406
  /**
285
- * Shim `window.speechSynthesis` so speech respects the SDK mute state.
286
- *
287
- * Never swallows speak(): utterances have a lifecycle the game may sequence
288
- * off (onstart/onend, synth.speaking/pending checks), so every call is
289
- * delegated and silenced via volume instead. Volume is sampled at speak()
290
- * time, so forcing the native value to 0 right before delegating silences
291
- * anything spoken while muted; in-flight speech at the mute edge is
292
- * deliberately left to finish (can't be softened mid-utterance, and
293
- * cancel() would discard the pending queue).
407
+ * Shim `speechSynthesis`. We never swallow `speak()` (games sequence off its
408
+ * lifecycle) — instead we sample volume at call time and force it to 0 while
409
+ * muted. Speech already in flight at the mute edge is left to finish.
294
410
  */
295
411
  shimSpeechSynthesis() {
296
- if (!window.speechSynthesis || typeof SpeechSynthesisUtterance === "undefined") {
412
+ const win = this.win;
413
+ if (!win.speechSynthesis || typeof win.SpeechSynthesisUtterance === "undefined") {
297
414
  return;
298
415
  }
299
416
  this.originalUtteranceVolumeDescriptor = Object.getOwnPropertyDescriptor(
300
- SpeechSynthesisUtterance.prototype,
417
+ win.SpeechSynthesisUtterance.prototype,
301
418
  "volume"
302
419
  ) ?? null;
303
420
  const volDesc = this.originalUtteranceVolumeDescriptor;
304
421
  if (volDesc?.get && volDesc?.set) {
305
- ((manager) => {
306
- Object.defineProperty(SpeechSynthesisUtterance.prototype, "volume", {
307
- configurable: true,
308
- get() {
309
- const intended = manager.intendedUtteranceVolume.get(this);
310
- return intended !== void 0 ? intended : volDesc.get.call(this);
311
- },
312
- set(value) {
313
- manager.intendedUtteranceVolume.set(this, value);
314
- volDesc.set.call(this, value);
422
+ ((shim) => {
423
+ Object.defineProperty(
424
+ win.SpeechSynthesisUtterance.prototype,
425
+ "volume",
426
+ {
427
+ configurable: true,
428
+ get() {
429
+ const intended = shim.intendedUtteranceVolume.get(this);
430
+ return intended !== void 0 ? intended : volDesc.get.call(this);
431
+ },
432
+ set(value) {
433
+ shim.intendedUtteranceVolume.set(this, value);
434
+ volDesc.set.call(this, value);
435
+ }
315
436
  }
316
- });
437
+ );
317
438
  })(this);
318
439
  }
319
- const speechSynthesis = window.speechSynthesis;
440
+ const speechSynthesis = win.speechSynthesis;
320
441
  const originalSpeak = speechSynthesis.speak;
321
442
  this.originalSpeak = originalSpeak;
322
- ((manager) => {
443
+ ((shim) => {
323
444
  speechSynthesis.speak = function(utterance) {
324
- if (manager._isMuted) {
325
- if (!manager.intendedUtteranceVolume.has(utterance)) {
445
+ if (shim.manager.isMuted()) {
446
+ if (!shim.intendedUtteranceVolume.has(utterance)) {
326
447
  const current = volDesc?.get ? volDesc.get.call(utterance) : utterance.volume;
327
- manager.intendedUtteranceVolume.set(utterance, current);
448
+ shim.intendedUtteranceVolume.set(utterance, current);
328
449
  }
329
450
  if (volDesc?.set) volDesc.set.call(utterance, 0);
330
451
  else utterance.volume = 0;
331
452
  } else {
332
- const intended = manager.intendedUtteranceVolume.get(utterance);
453
+ const intended = shim.intendedUtteranceVolume.get(utterance);
333
454
  if (intended !== void 0) {
334
455
  if (volDesc?.set) volDesc.set.call(utterance, intended);
335
456
  else utterance.volume = intended;
336
- manager.intendedUtteranceVolume.delete(utterance);
457
+ shim.intendedUtteranceVolume.delete(utterance);
337
458
  }
338
459
  }
339
460
  return originalSpeak.call(speechSynthesis, utterance);
@@ -341,13 +462,13 @@ var AudioManager = class extends WavedashManager {
341
462
  })(this);
342
463
  }
343
464
  shimAudioContextClass(Original) {
344
- return /* @__PURE__ */ ((manager) => class extends Original {
465
+ return /* @__PURE__ */ ((shim) => class extends Original {
345
466
  constructor(opts) {
346
467
  super(opts);
347
468
  const masterGain = this.createGain();
348
469
  masterGain.connect(this.destination);
349
470
  masterGain.gain.setValueAtTime(
350
- manager._isMuted ? 0 : 1,
471
+ shim.manager.isMuted() ? 0 : 1,
351
472
  this.currentTime
352
473
  );
353
474
  Object.defineProperty(this, "destination", {
@@ -356,60 +477,101 @@ var AudioManager = class extends WavedashManager {
356
477
  return masterGain;
357
478
  }
358
479
  });
359
- manager.contexts.set(this, masterGain);
480
+ shim.contexts.set(this, masterGain);
360
481
  }
361
482
  close() {
362
- manager.contexts.delete(this);
483
+ shim.contexts.delete(this);
363
484
  return super.close();
364
485
  }
365
486
  })(this);
366
487
  }
367
- destroy() {
368
- this.sdk.iframeMessenger.removeEventListener(
369
- IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
370
- this.handleMute
371
- );
372
- if (this.mutationObserver) {
373
- this.mutationObserver.disconnect();
374
- this.mutationObserver = null;
488
+ /**
489
+ * Restore the globals we patched. Best-effort per statement: a frame reached
490
+ * through an iframe may have navigated away (globals gone) before teardown.
491
+ */
492
+ uninstall() {
493
+ const win = this.win;
494
+ try {
495
+ if (this.mutationObserver) {
496
+ this.mutationObserver.disconnect();
497
+ this.mutationObserver = null;
498
+ }
499
+ } catch {
375
500
  }
376
- if (typeof window !== "undefined") {
377
- if (this.originalAudioContext) {
378
- window.AudioContext = this.originalAudioContext;
501
+ this.boundChildren.forEach((child) => this.manager.unbindIframe(child));
502
+ this.boundChildren.clear();
503
+ const restore = (fn) => {
504
+ try {
505
+ fn();
506
+ } catch {
507
+ }
508
+ };
509
+ restore(() => {
510
+ if (this.originalAudioContext)
511
+ win.AudioContext = this.originalAudioContext;
512
+ });
513
+ restore(() => {
514
+ const w = win;
515
+ if (this.originalWebKitAudioContext && w.webkitAudioContext) {
516
+ w.webkitAudioContext = this.originalWebKitAudioContext;
379
517
  }
380
- const win = window;
381
- if (this.originalWebKitAudioContext && win.webkitAudioContext) {
382
- win.webkitAudioContext = this.originalWebKitAudioContext;
518
+ });
519
+ restore(() => {
520
+ if (this.originalAudio) win.Audio = this.originalAudio;
521
+ });
522
+ restore(() => {
523
+ if (this.originalSpeak && win.speechSynthesis) {
524
+ win.speechSynthesis.speak = this.originalSpeak;
383
525
  }
384
- if (this.originalAudio) {
385
- window.Audio = this.originalAudio;
526
+ });
527
+ restore(() => {
528
+ if (this.originalUtteranceVolumeDescriptor && typeof win.SpeechSynthesisUtterance !== "undefined") {
529
+ Object.defineProperty(
530
+ win.SpeechSynthesisUtterance.prototype,
531
+ "volume",
532
+ this.originalUtteranceVolumeDescriptor
533
+ );
386
534
  }
387
- if (this.originalSpeak && window.speechSynthesis) {
388
- window.speechSynthesis.speak = this.originalSpeak;
535
+ });
536
+ restore(() => {
537
+ if (this.originalPlay) {
538
+ win.HTMLMediaElement.prototype.play = this.originalPlay;
389
539
  }
390
- }
391
- if (this.originalUtteranceVolumeDescriptor && typeof SpeechSynthesisUtterance !== "undefined") {
392
- Object.defineProperty(
393
- SpeechSynthesisUtterance.prototype,
394
- "volume",
395
- this.originalUtteranceVolumeDescriptor
396
- );
397
- }
398
- if (this.originalPlay) {
399
- HTMLMediaElement.prototype.play = this.originalPlay;
400
- }
401
- if (this.originalMutedDescriptor) {
402
- Object.defineProperty(
403
- HTMLMediaElement.prototype,
404
- "muted",
405
- this.originalMutedDescriptor
406
- );
407
- }
540
+ });
541
+ restore(() => {
542
+ if (this.originalMutedDescriptor) {
543
+ Object.defineProperty(
544
+ win.HTMLMediaElement.prototype,
545
+ "muted",
546
+ this.originalMutedDescriptor
547
+ );
548
+ }
549
+ });
408
550
  this.contexts.clear();
409
551
  this.elements.clear();
410
552
  this.intendedMuted = /* @__PURE__ */ new WeakMap();
411
553
  this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
412
- super.destroy();
554
+ }
555
+ };
556
+ var WeakRefSet = class {
557
+ constructor() {
558
+ this.set = /* @__PURE__ */ new Set();
559
+ }
560
+ add(value) {
561
+ for (const ref of this.set) {
562
+ if (ref.deref() === value) return;
563
+ }
564
+ this.set.add(new WeakRef(value));
565
+ }
566
+ forEach(callback) {
567
+ for (const ref of this.set) {
568
+ const v = ref.deref();
569
+ if (v === void 0) this.set.delete(ref);
570
+ else callback(v);
571
+ }
572
+ }
573
+ clear() {
574
+ this.set.clear();
413
575
  }
414
576
  };
415
577
 
@@ -3328,9 +3490,9 @@ var PaidContentManager = class extends WavedashManager {
3328
3490
  super(...arguments);
3329
3491
  this.paywallOpen = false;
3330
3492
  }
3331
- async isEntitled(contentId) {
3493
+ async isEntitled(contentIdentifier) {
3332
3494
  const jwt = await this.sdk.ensureGameplayJwt();
3333
- return readEntitlementsFromJwt(jwt).includes(contentId);
3495
+ return readEntitlementsFromJwt(jwt).includes(contentIdentifier);
3334
3496
  }
3335
3497
  async getEntitlements() {
3336
3498
  const jwt = await this.sdk.ensureGameplayJwt();
@@ -4954,23 +5116,23 @@ var WavedashSDK = class extends EventTarget {
4954
5116
  * doesn't actually unlock anything. Pair with triggerPaywall() to drive
4955
5117
  * in-game UI.
4956
5118
  */
4957
- async isEntitled(contentId) {
5119
+ async isEntitled(contentIdentifier) {
4958
5120
  return this.apiCall(
4959
5121
  this.paidContentManager,
4960
5122
  "isEntitled",
4961
- [["contentId", vString]],
4962
- contentId
5123
+ [["contentIdentifier", vString]],
5124
+ contentIdentifier
4963
5125
  );
4964
5126
  }
4965
5127
  // Kept for backwards compatibility
4966
- async isEntitled_EXPERIMENTAL(contentId) {
4967
- return this.isEntitled(contentId);
5128
+ async isEntitled_EXPERIMENTAL(contentIdentifier) {
5129
+ return this.isEntitled(contentIdentifier);
4968
5130
  }
4969
5131
  /**
4970
- * Returns the full list of paid-content IDs the player owns for this game.
5132
+ * Returns the full list of paid-content identifiers the player owns for this game.
4971
5133
  * Reads the `entitlements` claim from the gameplay JWT — this is a UX hint,
4972
5134
  * not a security check (see {@link isEntitled}). Useful
4973
- * for access gating multiple items at once without a call per content ID.
5135
+ * for access gating multiple items at once without a call per content identifier.
4974
5136
  */
4975
5137
  async getEntitlements() {
4976
5138
  return this.apiCall(this.paidContentManager, "getEntitlements", []);