@kieransimkin/dancemoves 3.1.19 → 3.1.21

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/lib/index.cjs CHANGED
@@ -24,6 +24,8 @@ __export(index_exports, {
24
24
  VERSION: () => VERSION,
25
25
  createDanceMoves: () => createDanceMoves,
26
26
  createMemoryPageStore: () => createMemoryPageStore,
27
+ createPlaybackPublisher: () => createPlaybackPublisher,
28
+ createPlaybackReceiver: () => createPlaybackReceiver,
27
29
  fromWordPressMeta: () => fromWordPressMeta,
28
30
  validatePageConfig: () => validatePageConfig
29
31
  });
@@ -48,6 +50,7 @@ function install(__dmScope) {
48
50
  var lyricList = [];
49
51
  var lyricRenderer = null;
50
52
  var boundAudio = /* @__PURE__ */ new WeakSet();
53
+ var playbackBindings = /* @__PURE__ */ new Map();
51
54
  var activeAudio = null;
52
55
  var pageClockStarted = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
53
56
  var referenceDurationSeconds = positiveNumber(rawConfig.masterDurationMilliseconds) / 1e3;
@@ -137,13 +140,16 @@ function install(__dmScope) {
137
140
  }
138
141
  function currentTick(options) {
139
142
  var settings = options || {};
140
- var audio = settings.audio || (settings.clock === "audio" ? activeAudio : null);
143
+ var audio = playbackAudio(settings.audio) || (settings.clock === "audio" ? activeAudio : null);
141
144
  if (audio && !audio.paused && !audio.ended && Number.isFinite(Number(audio.currentTime))) {
142
145
  return Number(audio.currentTime) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 60;
143
146
  }
144
147
  var now = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
145
148
  return Math.max(0, now - pageClockStarted) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 6e4;
146
149
  }
150
+ function playbackAudio(audio) {
151
+ return audio && api && api.playbackSource ? api.playbackSource(audio) : audio;
152
+ }
147
153
  function nextIntervalTick(intervalTicks, fromTick, strictlyFuture) {
148
154
  var interval = Math.max(1, roundHalfUp(intervalTicks));
149
155
  var tick = Math.max(0, Number(fromTick) || 0);
@@ -200,7 +206,7 @@ function install(__dmScope) {
200
206
  var pollAudio = function() {
201
207
  if (cancelled)
202
208
  return;
203
- var candidate = settings.audio || activeAudio;
209
+ var candidate = playbackAudio(settings.audio) || activeAudio;
204
210
  if (!candidate || candidate.paused || candidate.ended) {
205
211
  if (sourceAudio && sourceAudio.ended) {
206
212
  cancel();
@@ -762,9 +768,18 @@ function install(__dmScope) {
762
768
  return Math.abs(duration - referenceDurationSeconds) <= Math.max(0.25, duration / 1e5);
763
769
  }
764
770
  function bindAudio(audio) {
771
+ if (api.playbackSource)
772
+ audio = api.playbackSource(audio);
765
773
  if (boundAudio.has(audio) || !matchesMasterDuration(audio))
766
774
  return;
767
775
  boundAudio.add(audio);
776
+ var removers = [];
777
+ function listen(target, name, handler) {
778
+ __dmScope.listen(target, name, handler);
779
+ removers.push(function() {
780
+ __dmScope.unlisten(target, name, handler);
781
+ });
782
+ }
768
783
  __dmScope.dataset(audio, "danceMovesTiming", "master-length");
769
784
  var state = {
770
785
  next: firstCueAfter(audio.currentTime || 0),
@@ -772,7 +787,8 @@ function install(__dmScope) {
772
787
  last: audio.currentTime || 0,
773
788
  seeking: false,
774
789
  frame: 0,
775
- pendingCue: cueAtLanding(audio.currentTime || 0)
790
+ pendingCue: cueAtLanding(audio.currentTime || 0),
791
+ endedPending: false
776
792
  };
777
793
  function stopFrame() {
778
794
  if (state.frame)
@@ -781,7 +797,7 @@ function install(__dmScope) {
781
797
  }
782
798
  function tick() {
783
799
  state.frame = 0;
784
- if (audio.paused || audio.ended || state.seeking)
800
+ if (audio.paused || audio.ended || state.seeking || document2.hidden)
785
801
  return;
786
802
  var diagnosticStartedAt = diagnosticsSink ? diagnosticNow() : 0;
787
803
  var firedCount = 0;
@@ -819,8 +835,14 @@ function install(__dmScope) {
819
835
  }
820
836
  function start() {
821
837
  stopFrame();
838
+ if (document2.hidden || audio.paused || audio.ended)
839
+ return;
822
840
  activeAudio = audio;
823
841
  state.last = positiveNumber(audio.currentTime);
842
+ if (state.endedPending) {
843
+ state.pendingCue = cueAtLanding(state.last);
844
+ state.endedPending = false;
845
+ }
824
846
  state.next = firstCueAfter(state.last);
825
847
  state.lyric = -2;
826
848
  if (state.pendingCue) {
@@ -830,47 +852,70 @@ function install(__dmScope) {
830
852
  setOwnedPlaybackRate(audio.playbackRate);
831
853
  state.frame = window2.requestAnimationFrame(tick);
832
854
  }
833
- __dmScope.listen(audio, "play", start);
834
- __dmScope.listen(audio, "playing", start);
835
- __dmScope.listen(audio, "pause", function() {
855
+ listen(audio, "play", start);
856
+ listen(audio, "playing", start);
857
+ listen(audio, "pause", function() {
836
858
  stopFrame();
837
859
  if (activeAudio === audio)
838
860
  activeAudio = null;
839
861
  if (lyricRenderer)
840
862
  lyricRenderer.hide();
841
863
  });
842
- __dmScope.listen(audio, "ended", function() {
864
+ listen(audio, "ended", function() {
843
865
  stopFrame();
844
866
  if (activeAudio === audio)
845
867
  activeAudio = null;
846
868
  state.last = 0;
847
869
  state.next = 0;
848
870
  state.lyric = -2;
849
- state.pendingCue = cueAtLanding(0);
871
+ state.pendingCue = null;
872
+ state.endedPending = true;
850
873
  if (lyricRenderer)
851
874
  lyricRenderer.hide();
852
875
  });
853
- __dmScope.listen(audio, "seeking", function() {
876
+ listen(audio, "seeking", function() {
854
877
  state.seeking = true;
855
878
  state.pendingCue = null;
856
879
  stopFrame();
857
880
  });
858
- __dmScope.listen(audio, "seeked", function() {
881
+ listen(audio, "seeked", function() {
859
882
  state.seeking = false;
860
883
  state.last = positiveNumber(audio.currentTime);
861
884
  state.next = firstCueAfter(state.last);
862
885
  state.lyric = -2;
863
886
  state.pendingCue = cueAtLanding(state.last);
887
+ playbackBindings.set(audio, function() {
888
+ stopFrame();
889
+ removers.splice(0).forEach(function(remove) {
890
+ remove();
891
+ });
892
+ boundAudio.delete(audio);
893
+ if (activeAudio === audio)
894
+ activeAudio = null;
895
+ playbackBindings.delete(audio);
896
+ });
864
897
  if (!audio.paused && !audio.ended)
865
898
  start();
866
899
  });
867
- __dmScope.listen(audio, "ratechange", function() {
900
+ listen(audio, "ratechange", function() {
868
901
  setOwnedPlaybackRate(audio.playbackRate);
869
902
  });
870
- __dmScope.listen(audio, "timeupdate", function() {
903
+ listen(audio, "timeupdate", function() {
871
904
  if (!audio.paused && !state.frame && !state.seeking)
872
905
  tick();
873
906
  });
907
+ listen(document2, "visibilitychange", function() {
908
+ if (document2.hidden) {
909
+ stopFrame();
910
+ if (lyricRenderer)
911
+ lyricRenderer.hide();
912
+ } else if (!audio.paused && !audio.ended && !state.seeking) {
913
+ state.pendingCue = null;
914
+ start();
915
+ }
916
+ });
917
+ if (!audio.paused && !audio.ended)
918
+ start();
874
919
  }
875
920
  function discoverAudio() {
876
921
  var audioElements = Array.from(document2.querySelectorAll("audio"));
@@ -984,6 +1029,16 @@ function install(__dmScope) {
984
1029
  registerAnimationScope,
985
1030
  resetRunningAnimations,
986
1031
  discoverAudio,
1032
+ unregisterPlaybackSource: function(source) {
1033
+ var remove = playbackBindings.get(source);
1034
+ if (remove)
1035
+ remove();
1036
+ },
1037
+ registerPlaybackSource: function(source) {
1038
+ if (!referenceDurationSeconds)
1039
+ referenceDurationSeconds = positiveNumber(source.duration);
1040
+ bindAudio(source);
1041
+ },
987
1042
  setDiagnosticsSink,
988
1043
  diagnosticsEnabled: function() {
989
1044
  return diagnosticsAllowed && Boolean(diagnosticsSink);
@@ -1024,6 +1079,10 @@ function install2(__dmScope) {
1024
1079
  return document2.querySelector(value);
1025
1080
  return fallback || null;
1026
1081
  }
1082
+ function playbackElement(value, fallback) {
1083
+ var target = element(value, fallback);
1084
+ return target && motion.playbackSource ? motion.playbackSource(target) : target;
1085
+ }
1027
1086
  function media(query) {
1028
1087
  return window2.matchMedia(query);
1029
1088
  }
@@ -1149,7 +1208,7 @@ function install2(__dmScope) {
1149
1208
  function playbackPulse(options) {
1150
1209
  options = options || {};
1151
1210
  var state = commonState(options, "pulse");
1152
- var audio = element(options.audio, state.root.querySelector("audio"));
1211
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1153
1212
  if (!audio)
1154
1213
  throw new Error("DanceMovesEffects pulse requires an audio element");
1155
1214
  var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
@@ -1249,7 +1308,7 @@ function install2(__dmScope) {
1249
1308
  function cueTimeline(options) {
1250
1309
  options = options || {};
1251
1310
  var state = commonState(options, "cue-timeline");
1252
- var audio = element(options.audio, state.root.querySelector("audio"));
1311
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1253
1312
  if (!audio)
1254
1313
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1255
1314
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
@@ -1410,7 +1469,7 @@ function install2(__dmScope) {
1410
1469
  function spritePlayback(options) {
1411
1470
  options = options || {};
1412
1471
  var state = commonState(options, "sprite-playback");
1413
- var audio = element(options.audio, state.root.querySelector("audio"));
1472
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1414
1473
  var target = element(options.target, state.root);
1415
1474
  if (!audio)
1416
1475
  throw new Error("DanceMovesEffects spritePlayback requires an audio element");
@@ -1604,7 +1663,7 @@ function install2(__dmScope) {
1604
1663
  function lyricStage(options) {
1605
1664
  options = options || {};
1606
1665
  var state = commonState(options, "lyric-stage");
1607
- var audio = element(options.audio, state.root.querySelector("audio"));
1666
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1608
1667
  var popover = element(options.popover, document2.querySelector(".dance-moves-lyric-popover"));
1609
1668
  if (!audio)
1610
1669
  throw new Error("DanceMovesEffects lyricStage requires an audio element");
@@ -4823,9 +4882,11 @@ function install9(__dmScope) {
4823
4882
  const clock = options.clock === void 0 ? "page" : options.clock;
4824
4883
  if (!["page", "audio", "auto"].includes(clock))
4825
4884
  throw new TypeError("clock must be page, audio or auto");
4826
- const audio = clock === "page" ? null : element(options.audio, "audio");
4827
- if (audio && String(audio.tagName).toLowerCase() !== "audio")
4828
- throw new TypeError("audio must be an HTMLAudioElement");
4885
+ let audio = clock === "page" ? null : element(options.audio, "audio");
4886
+ if (audio && motion.playbackSource)
4887
+ audio = motion.playbackSource(audio);
4888
+ if (audio && !["audio", "video"].includes(String(audio.tagName).toLowerCase()))
4889
+ throw new TypeError("audio must resolve to native media or a shared playback source");
4829
4890
  const rate = options.rate === void 0 ? 1 : number(options.rate, "rate");
4830
4891
  if (rate <= 0 || rate > 64)
4831
4892
  throw new RangeError("rate must be in (0, 64]");
@@ -5073,7 +5134,7 @@ function install9(__dmScope) {
5073
5134
  }
5074
5135
  if (options.resetOnCue)
5075
5136
  disposers.push(motion.onCue(typeof options.resetOnCue === "string" ? options.resetOnCue : "*", (detail) => {
5076
- if (!detail.audio || !audio || detail.audio === audio)
5137
+ if (!detail.audio || !audio || detail.audio === audio || motion.playbackSource && motion.playbackSource(detail.audio) === motion.playbackSource(audio))
5077
5138
  reset();
5078
5139
  }, { id: id + ":cue-reset" }));
5079
5140
  if (options.offscreen !== false && window2.IntersectionObserver) {
@@ -5347,6 +5408,8 @@ function createScope(root, config, { legacyGlobals = false, nonce, orientation =
5347
5408
  }
5348
5409
  const api = {
5349
5410
  id,
5411
+ root,
5412
+ legacyGlobals,
5350
5413
  window,
5351
5414
  document,
5352
5415
  get disposed() {
@@ -5949,8 +6012,855 @@ function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples =
5949
6012
  });
5950
6013
  }
5951
6014
 
6015
+ // .build/modules/playback.mjs
6016
+ var youtubeLoaders = /* @__PURE__ */ new WeakMap();
6017
+ function validatePlaybackMapping(input, masterDuration) {
6018
+ const offset = input.offset ?? 0, duration = input.duration ?? masterDuration, playerStart = input.playerStart ?? 0;
6019
+ for (const [key, value] of Object.entries({ offset, duration, playerStart, masterDuration })) {
6020
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (key === "duration" || key === "masterDuration") && value === 0) throw new RangeError(`${key} must be a finite positive duration or nonnegative offset`);
6021
+ }
6022
+ if (offset + duration > masterDuration + 1e-3) throw new RangeError("The clip extends beyond its associated master");
6023
+ return Object.freeze({ offset, duration, playerStart, masterDuration });
6024
+ }
6025
+ function loadYouTubeAPI(host) {
6026
+ if (host.YT?.Player) return Promise.resolve(host.YT);
6027
+ if (youtubeLoaders.has(host)) return youtubeLoaders.get(host);
6028
+ const pending = new Promise((resolve, reject) => {
6029
+ const previous = host.onYouTubeIframeAPIReady;
6030
+ let timeout;
6031
+ const script = host.document.createElement("script");
6032
+ function finish(error) {
6033
+ host.clearTimeout(timeout);
6034
+ if (host.onYouTubeIframeAPIReady === ready) host.onYouTubeIframeAPIReady = previous;
6035
+ script.removeEventListener("error", failed);
6036
+ if (error) {
6037
+ script.remove();
6038
+ youtubeLoaders.delete(host);
6039
+ reject(error);
6040
+ } else resolve(host.YT);
6041
+ }
6042
+ function ready() {
6043
+ try {
6044
+ previous?.();
6045
+ } finally {
6046
+ finish(host.YT?.Player ? null : new Error("YouTube API unavailable"));
6047
+ }
6048
+ }
6049
+ function failed() {
6050
+ finish(new Error("YouTube API failed to load"));
6051
+ }
6052
+ host.onYouTubeIframeAPIReady = ready;
6053
+ script.src = "https://www.youtube.com/iframe_api";
6054
+ script.addEventListener("error", failed, { once: true });
6055
+ timeout = host.setTimeout(() => finish(new Error("YouTube API timed out")), 2e4);
6056
+ host.document.head.appendChild(script);
6057
+ });
6058
+ youtubeLoaders.set(host, pending);
6059
+ return pending;
6060
+ }
6061
+ function installPlayback(scope, api, host = scope.document.defaultView) {
6062
+ const groups = /* @__PURE__ */ new Map(), transports = /* @__PURE__ */ new WeakSet(), aliases = /* @__PURE__ */ new WeakMap(), controllers = /* @__PURE__ */ new Set(), registered = /* @__PURE__ */ new WeakMap();
6063
+ let disposed = false;
6064
+ function emit(target, type) {
6065
+ target.dispatchEvent(new host.Event(type));
6066
+ }
6067
+ function groupFor(reference) {
6068
+ if (disposed) throw new Error("DanceMoves playback owner is destroyed");
6069
+ const master = reference?.element || reference, declared = reference?.element ? reference.duration : void 0;
6070
+ const isNative = !!master?.addEventListener && ["AUDIO", "VIDEO"].includes(master.tagName);
6071
+ if (!isNative && (!master || typeof master.id !== "string" || !master.id.trim() || master.id.length > 160 || typeof master.duration !== "number" || !Number.isFinite(master.duration) || master.duration <= 0)) throw new TypeError("A native master or identified master duration is required");
6072
+ if (isNative && (master.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(master))) throw new TypeError("The master must belong to this runtime root");
6073
+ if (groups.has(master)) {
6074
+ const group2 = groups.get(master);
6075
+ group2.setDuration(declared);
6076
+ return group2;
6077
+ }
6078
+ const transport = new host.EventTarget();
6079
+ transports.add(transport);
6080
+ let active = null, lastPaused = true, delegate = null;
6081
+ let declaredDuration = declared || 0;
6082
+ function setDuration(value) {
6083
+ if (value === void 0) return;
6084
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new RangeError("Declared master duration must be positive and finite");
6085
+ if (declaredDuration && Math.abs(declaredDuration - value) > 1e-3) throw new RangeError("Conflicting master durations");
6086
+ if (isNative && Number(master.duration) > 0 && Math.abs(Number(master.duration) - value) > 0.25) throw new RangeError("Declared master duration does not match its native reference");
6087
+ declaredDuration = value;
6088
+ }
6089
+ setDuration(declared);
6090
+ const duration = () => delegate?.duration || declaredDuration || (Number(master.duration) > 0 && Number.isFinite(Number(master.duration)) ? Number(master.duration) : Number(api.config?.masterDurationMilliseconds || 0) / 1e3);
6091
+ const target = isNative ? master : scope.root;
6092
+ const native = { element: isNative ? master : null, read: () => ({ time: Number(master.currentTime) || 0, paused: isNative ? master.paused : true, ended: master.ended || false, rate: master.playbackRate || 1, seeking: master.seeking || false, error: master.error }), pause: () => master.pause?.(), play: () => master.play?.(), seek: (time) => {
6093
+ if (isNative) master.currentTime = time;
6094
+ }, mapping: null };
6095
+ active = native;
6096
+ function read() {
6097
+ if (delegate) return { time: delegate.currentTime, paused: delegate.paused, ended: delegate.ended, rate: delegate.playbackRate, seeking: delegate.seeking, error: delegate.error };
6098
+ const raw = active.read(), map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6099
+ const local = raw.time - map.playerStart, outside = local < -1e-3 || local > map.duration + 1e-3;
6100
+ return { ...raw, time: map.offset + Math.max(0, Math.min(map.duration, local)), paused: raw.paused || outside, ended: raw.ended || local > map.duration + 1e-3 };
6101
+ }
6102
+ Object.defineProperties(transport, {
6103
+ nodeType: { value: 1 },
6104
+ tagName: { get: () => isNative ? master.tagName : "VIDEO" },
6105
+ ownerDocument: { get: () => target.ownerDocument },
6106
+ dataset: { get: () => target.dataset },
6107
+ id: { get: () => master.id },
6108
+ isConnected: { get: () => target.isConnected },
6109
+ currentTime: { get: () => read().time, set: (value) => {
6110
+ if (delegate) {
6111
+ delegate.currentTime = value;
6112
+ return;
6113
+ }
6114
+ const map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6115
+ active.seek(map.playerStart + Math.max(0, Math.min(map.duration, Number(value) - map.offset)));
6116
+ } },
6117
+ duration: { get: duration },
6118
+ paused: { get: () => read().paused },
6119
+ ended: { get: () => read().ended },
6120
+ seeking: { get: () => read().seeking || false },
6121
+ playbackRate: { get: () => read().rate },
6122
+ error: { get: () => read().error || null },
6123
+ element: { value: isNative ? master : null },
6124
+ activePlayer: { get: () => delegate ? delegate.activePlayer : active.element },
6125
+ masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id }
6126
+ });
6127
+ transport.matches = (selector) => target.matches(selector);
6128
+ transport.getAttribute = (name) => target.getAttribute(name);
6129
+ transport.setAttribute = (name, value) => target.setAttribute(name, value);
6130
+ transport.removeAttribute = (name) => target.removeAttribute(name);
6131
+ transport.play = () => delegate ? delegate.play() : active.play();
6132
+ transport.pause = () => delegate ? delegate.pause() : active.pause();
6133
+ function activate(source) {
6134
+ if (active !== source) {
6135
+ const previous = active;
6136
+ active = source;
6137
+ previous.pause();
6138
+ emit(transport, "seeking");
6139
+ emit(transport, "seeked");
6140
+ }
6141
+ }
6142
+ function forward(source, type) {
6143
+ if (type === "play" || type === "playing") activate(source);
6144
+ if (active !== source) return;
6145
+ const paused = read().paused;
6146
+ if (type === "timeupdate" && paused !== lastPaused) emit(transport, paused ? "pause" : "playing");
6147
+ lastPaused = paused;
6148
+ emit(transport, type);
6149
+ }
6150
+ const events = ["loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"];
6151
+ const removers = [];
6152
+ function bindNative(element, mapping) {
6153
+ const source = element === master ? native : { element, mapping, read: () => ({ time: Number(element.currentTime) || 0, paused: element.paused, ended: element.ended, rate: element.playbackRate || 1, seeking: element.seeking, error: element.error }), pause: () => element.pause(), play: () => element.play(), seek: (time) => {
6154
+ element.currentTime = time;
6155
+ } };
6156
+ source.removers = [];
6157
+ for (const type of events) {
6158
+ const listener = () => forward(source, type);
6159
+ scope.listen(element, type, listener);
6160
+ const remove = () => scope.unlisten(element, type, listener);
6161
+ removers.push(remove);
6162
+ source.removers.push(remove);
6163
+ }
6164
+ return source;
6165
+ }
6166
+ if (isNative) bindNative(master, null);
6167
+ const group = {
6168
+ transport,
6169
+ duration,
6170
+ setDuration,
6171
+ activate,
6172
+ forward,
6173
+ bindNative,
6174
+ isActive: (source) => active === source,
6175
+ aliasTo(target2) {
6176
+ removers.splice(0).forEach((remove) => remove());
6177
+ delegate = target2;
6178
+ aliases.set(transport, target2);
6179
+ for (const type of events) {
6180
+ const relay = () => emit(transport, type);
6181
+ scope.listen(target2, type, relay);
6182
+ removers.push(() => scope.unlisten(target2, type, relay));
6183
+ }
6184
+ },
6185
+ reconcile(source) {
6186
+ if (active !== source) return;
6187
+ const paused = read().paused;
6188
+ if (paused !== lastPaused) {
6189
+ lastPaused = paused;
6190
+ emit(transport, paused ? "pause" : "playing");
6191
+ }
6192
+ emit(transport, "timeupdate");
6193
+ },
6194
+ remove(source) {
6195
+ if (active === source) {
6196
+ source.pause();
6197
+ active = native;
6198
+ lastPaused = true;
6199
+ emit(transport, "pause");
6200
+ emit(transport, "seeking");
6201
+ emit(transport, "seeked");
6202
+ }
6203
+ },
6204
+ destroy() {
6205
+ removers.splice(0).forEach((remove) => remove());
6206
+ emit(transport, "pause");
6207
+ }
6208
+ };
6209
+ groups.set(master, group);
6210
+ return group;
6211
+ }
6212
+ function registerEndpoint(options, provider = false) {
6213
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6214
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6215
+ const element = options.player;
6216
+ if (!element?.addEventListener || !["AUDIO", "VIDEO"].includes(element.tagName)) throw new TypeError("player must be native audio or video");
6217
+ if (element.ownerDocument !== host.document || !provider && !scope.legacyGlobals && !scope.root.contains(element)) throw new TypeError("The player must belong to this runtime root");
6218
+ if (provider && (!element.removeEventListener || typeof element.play !== "function" || typeof element.pause !== "function")) throw new TypeError("An event-driven playback provider is required");
6219
+ if (provider && typeof host.Node === "function" && element instanceof host.Node) throw new TypeError("Use registerPlayer for native DOM players");
6220
+ if (registered.has(element)) throw new Error("Player already has a playback owner");
6221
+ if (element === options.master || element === group.transport.element) throw new TypeError("The master is already registered");
6222
+ const previousGroup = groups.get(element);
6223
+ if (previousGroup && previousGroup !== group) {
6224
+ if ([...controllers].some((owner) => owner.source === previousGroup.transport)) throw new Error("This player already owns a different master group");
6225
+ api.unregisterPlaybackSource?.(previousGroup.transport);
6226
+ previousGroup.aliasTo(group.transport);
6227
+ for (const [key, value] of groups) if (value === previousGroup) groups.set(key, group);
6228
+ }
6229
+ const source = group.bindNative(element, mapping);
6230
+ let destroyed = false;
6231
+ const controller = { source: group.transport, mapping, teardown() {
6232
+ if (destroyed) return;
6233
+ destroyed = true;
6234
+ source.removers.splice(0).forEach((remove) => remove());
6235
+ group.remove(source);
6236
+ registered.delete(element);
6237
+ controllers.delete(controller);
6238
+ }, snapshot() {
6239
+ return { kind: provider ? "provider" : "native", mapping, active: group.isActive(source), destroyed };
6240
+ } };
6241
+ registered.set(element, controller);
6242
+ if (!element.paused && !element.ended) group.forward(source, "playing");
6243
+ Promise.resolve(api.ready).then(() => {
6244
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6245
+ });
6246
+ controllers.add(controller);
6247
+ return controller;
6248
+ }
6249
+ const registerPlayer = (options) => registerEndpoint(options);
6250
+ const registerPlaybackProvider = (options) => registerEndpoint(options, true);
6251
+ function registerYouTube(options) {
6252
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6253
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6254
+ const iframe = options.iframe;
6255
+ if (iframe?.tagName !== "IFRAME") throw new TypeError("A YouTube iframe is required");
6256
+ if (iframe.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(iframe)) throw new TypeError("The iframe must belong to this runtime root");
6257
+ if (registered.has(iframe)) throw new Error("Player already has a playback owner");
6258
+ const url2 = new URL(iframe.src, host.location.href);
6259
+ if (!/^https:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[A-Za-z0-9_-]{11}(\?|$)/.test(url2.href)) throw new TypeError("Unsupported YouTube embed");
6260
+ url2.searchParams.set("enablejsapi", "1");
6261
+ url2.searchParams.set("origin", host.location.origin);
6262
+ const expectedVideoId = url2.pathname.split("/").at(-1);
6263
+ if (iframe.src !== url2.href) iframe.src = url2.href;
6264
+ let player = null, state = -1, error = null, frame = 0, destroyed = false, lastTime = null, lastWall = null, lastRate = 1, readyTimer = 0, rejectReady = null;
6265
+ const source = {
6266
+ element: iframe,
6267
+ mapping,
6268
+ read: () => ({ time: player?.getCurrentTime?.() || 0, paused: state !== 1, ended: state === 0, rate: player?.getPlaybackRate?.() || 1, error }),
6269
+ pause: () => player?.pauseVideo?.(),
6270
+ play: () => player?.playVideo?.(),
6271
+ seek: (time) => player?.seekTo?.(time, true)
6272
+ };
6273
+ function stop() {
6274
+ if (frame) scope.window.cancelAnimationFrame(frame);
6275
+ frame = 0;
6276
+ lastTime = null;
6277
+ lastWall = null;
6278
+ }
6279
+ function sample(wall) {
6280
+ frame = 0;
6281
+ if (destroyed || state !== 1 || scope.document.hidden || !group.isActive(source)) return;
6282
+ const raw = source.read();
6283
+ if (lastTime !== null && Math.abs(raw.time - lastTime - (wall - lastWall) / 1e3 * raw.rate) > 0.4) {
6284
+ group.forward(source, "seeking");
6285
+ group.forward(source, "seeked");
6286
+ }
6287
+ if (raw.rate !== lastRate) {
6288
+ lastRate = raw.rate;
6289
+ group.forward(source, "ratechange");
6290
+ }
6291
+ lastTime = raw.time;
6292
+ lastWall = wall;
6293
+ group.reconcile(source);
6294
+ frame = scope.window.requestAnimationFrame(sample);
6295
+ }
6296
+ function resume() {
6297
+ stop();
6298
+ if (!destroyed && state === 1 && !scope.document.hidden && group.isActive(source)) frame = scope.window.requestAnimationFrame(sample);
6299
+ }
6300
+ const visibility = () => {
6301
+ if (scope.document.hidden) stop();
6302
+ else {
6303
+ group.forward(source, "seeking");
6304
+ group.forward(source, "seeked");
6305
+ resume();
6306
+ }
6307
+ };
6308
+ const pageHide = () => stop(), pageShow = () => {
6309
+ if (player?.getPlayerState) state = player.getPlayerState();
6310
+ visibility();
6311
+ };
6312
+ scope.listen(scope.document, "visibilitychange", visibility);
6313
+ scope.listen(host, "pagehide", pageHide);
6314
+ scope.listen(host, "pageshow", pageShow);
6315
+ const controller = {
6316
+ source: group.transport,
6317
+ mapping,
6318
+ ready: null,
6319
+ teardown() {
6320
+ if (destroyed) return;
6321
+ destroyed = true;
6322
+ stop();
6323
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6324
+ readyTimer = 0;
6325
+ rejectReady?.(new Error("YouTube playback owner removed"));
6326
+ rejectReady = null;
6327
+ group.remove(source);
6328
+ scope.unlisten(scope.document, "visibilitychange", visibility);
6329
+ scope.unlisten(host, "pagehide", pageHide);
6330
+ scope.unlisten(host, "pageshow", pageShow);
6331
+ player?.destroy?.();
6332
+ registered.delete(iframe);
6333
+ controllers.delete(controller);
6334
+ },
6335
+ snapshot() {
6336
+ return { kind: "youtube", mapping, state, error, active: group.isActive(source), destroyed, polling: !!frame, time: group.transport.currentTime };
6337
+ }
6338
+ };
6339
+ controllers.add(controller);
6340
+ registered.set(iframe, controller);
6341
+ Promise.resolve(api.ready).then(() => {
6342
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6343
+ });
6344
+ controller.ready = loadYouTubeAPI(host).then((YT) => {
6345
+ if (destroyed || disposed) return controller;
6346
+ return new Promise((resolve, reject) => {
6347
+ rejectReady = reject;
6348
+ readyTimer = scope.window.setTimeout(() => {
6349
+ readyTimer = 0;
6350
+ error = new Error("YouTube player readiness timed out");
6351
+ rejectReady = null;
6352
+ reject(error);
6353
+ }, 15e3);
6354
+ player = new YT.Player(iframe, { events: {
6355
+ onReady() {
6356
+ if (destroyed) return;
6357
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6358
+ readyTimer = 0;
6359
+ rejectReady = null;
6360
+ const actual = Number(player.getDuration?.());
6361
+ if (actual > 0 && actual + 0.25 < mapping.playerStart + mapping.duration) {
6362
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6363
+ reject(error);
6364
+ return;
6365
+ }
6366
+ group.forward(source, "loadedmetadata");
6367
+ const initialState = player.getPlayerState?.();
6368
+ if (initialState === 1) controllerState({ data: initialState });
6369
+ resolve(controller);
6370
+ },
6371
+ onStateChange: controllerState,
6372
+ onPlaybackRateChange() {
6373
+ if (!destroyed) group.forward(source, "ratechange");
6374
+ },
6375
+ onError(event) {
6376
+ if (destroyed) return;
6377
+ error = new Error("YouTube playback error " + event.data);
6378
+ state = -1;
6379
+ stop();
6380
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6381
+ readyTimer = 0;
6382
+ rejectReady = null;
6383
+ group.forward(source, "pause");
6384
+ group.forward(source, "error");
6385
+ reject(error);
6386
+ }
6387
+ } });
6388
+ function controllerState(event) {
6389
+ if (destroyed) return;
6390
+ state = event.data;
6391
+ stop();
6392
+ if (state === 1) {
6393
+ let actualId;
6394
+ try {
6395
+ const actualUrl = new URL(player.getVideoUrl());
6396
+ actualId = actualUrl.searchParams.get("v") || actualUrl.pathname.match(/^\/(?:shorts|embed)\/([A-Za-z0-9_-]{11})\/?$/)?.[1];
6397
+ } catch {
6398
+ }
6399
+ if (actualId !== expectedVideoId) {
6400
+ error = new Error("YouTube switched away from the verified soundtrack asset");
6401
+ state = -1;
6402
+ group.forward(source, "pause");
6403
+ group.forward(source, "error");
6404
+ return;
6405
+ }
6406
+ const actualDuration = Number(player.getDuration?.());
6407
+ if (actualDuration > 0 && actualDuration + 0.25 < mapping.playerStart + mapping.duration) {
6408
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6409
+ state = -1;
6410
+ group.forward(source, "pause");
6411
+ group.forward(source, "error");
6412
+ return;
6413
+ }
6414
+ group.forward(source, "play");
6415
+ group.forward(source, "playing");
6416
+ resume();
6417
+ } else group.forward(source, state === 0 ? "ended" : state === 3 ? "waiting" : "pause");
6418
+ if (state === 3) group.forward(source, "pause");
6419
+ }
6420
+ });
6421
+ });
6422
+ controller.ready.catch(() => {
6423
+ });
6424
+ return controller;
6425
+ }
6426
+ Object.assign(api, { playbackSource: (master) => {
6427
+ let source = transports.has(master) ? master : groupFor(master).transport;
6428
+ while (aliases.has(source)) source = aliases.get(source);
6429
+ return source;
6430
+ }, registerPlayer, registerPlaybackProvider, registerYouTube });
6431
+ scope.onDispose(() => {
6432
+ disposed = true;
6433
+ for (const controller of [...controllers]) controller.teardown();
6434
+ for (const group of new Set(groups.values())) group.destroy();
6435
+ groups.clear();
6436
+ });
6437
+ }
6438
+
6439
+ // .build/modules/child-playback.mjs
6440
+ var PROTOCOL = "dancemoves.child-playback.v1";
6441
+ var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
6442
+ var MAX_SECONDS = 86400;
6443
+ var publisherOwners = /* @__PURE__ */ new WeakMap();
6444
+ var receiverOwners = /* @__PURE__ */ new WeakMap();
6445
+ function identity(value, name) {
6446
+ if (typeof value !== "string" || !value.trim() || value.length > 200) throw new TypeError(`${name} must be a nonempty string of at most 200 characters`);
6447
+ return value;
6448
+ }
6449
+ function exactOrigin(value) {
6450
+ const url2 = new URL(value);
6451
+ if (!["http:", "https:"].includes(url2.protocol) || url2.origin !== value) throw new TypeError("An exact HTTP(S) origin is required");
6452
+ return value;
6453
+ }
6454
+ function context(options) {
6455
+ const host = options.window || globalThis.window;
6456
+ if (!host?.document || !host.crypto?.randomUUID) throw new TypeError("A browser Window with crypto.randomUUID is required");
6457
+ return { host, channel: identity(options.channel, "channel"), assetId: identity(options.assetId, "assetId"), origin: exactOrigin(options.origin) };
6458
+ }
6459
+ function validState(value) {
6460
+ return value && typeof value === "object" && typeof value.time === "number" && Number.isFinite(value.time) && value.time >= 0 && typeof value.duration === "number" && Number.isFinite(value.duration) && value.duration > 0 && value.duration <= MAX_SECONDS && value.time <= value.duration + 0.25 && typeof value.rate === "number" && Number.isFinite(value.rate) && value.rate >= 0.0625 && value.rate <= 16 && ["paused", "ended", "seeking", "buffering", "error"].every((key) => typeof value[key] === "boolean");
6461
+ }
6462
+ function packet(data, channel, assetId) {
6463
+ return data && typeof data === "object" && data.protocol === PROTOCOL && data.channel === channel && data.assetId === assetId;
6464
+ }
6465
+ function resources(host) {
6466
+ const removers = [], timers = /* @__PURE__ */ new Set();
6467
+ return {
6468
+ listen(target, type, callback) {
6469
+ target.addEventListener(type, callback);
6470
+ removers.push(() => target.removeEventListener(type, callback));
6471
+ },
6472
+ later(callback, delay) {
6473
+ const id = host.setTimeout(() => {
6474
+ timers.delete(id);
6475
+ callback();
6476
+ }, delay);
6477
+ timers.add(id);
6478
+ return id;
6479
+ },
6480
+ cancel(id) {
6481
+ if (id) {
6482
+ host.clearTimeout(id);
6483
+ timers.delete(id);
6484
+ }
6485
+ },
6486
+ dispose() {
6487
+ removers.splice(0).forEach((remove) => remove());
6488
+ for (const id of timers) host.clearTimeout(id);
6489
+ timers.clear();
6490
+ },
6491
+ snapshot() {
6492
+ return { listeners: removers.length, timers: timers.size };
6493
+ }
6494
+ };
6495
+ }
6496
+ function createPlaybackPublisher(options = {}) {
6497
+ const { host, channel, assetId, origin } = context(options), source = options.source;
6498
+ const target = options.targetWindow || host.parent;
6499
+ if (!source?.addEventListener || !source?.pause || !target?.postMessage || target === host) throw new TypeError("A playback source and distinct target Window are required");
6500
+ const owners = publisherOwners.get(host) || /* @__PURE__ */ new Map(), key = JSON.stringify([origin, channel, assetId]);
6501
+ const routes = owners.get(target) || /* @__PURE__ */ new Set();
6502
+ if (routes.has(key)) throw new Error("Child playback route already has a publisher");
6503
+ const owned = resources(host);
6504
+ let session = null, sequence = 0, commandSequence = 0, timer = 0, destroyed = false, suspended = false, lastPublished = null;
6505
+ const envelope = (type, extra = {}) => ({ protocol: PROTOCOL, channel, assetId, session, type, ...extra });
6506
+ function send(type, extra) {
6507
+ target.postMessage(envelope(type, extra), origin);
6508
+ }
6509
+ function stop() {
6510
+ owned.cancel(timer);
6511
+ timer = 0;
6512
+ }
6513
+ function sample(event = "snapshot") {
6514
+ if (event === "timeupdate" && lastPublished !== null && host.performance.now() - lastPublished < 250) return;
6515
+ stop();
6516
+ if (destroyed || suspended || !session) return;
6517
+ const state = { time: Number(source.currentTime), duration: Number(source.duration), rate: Number(source.playbackRate), paused: !!source.paused, ended: !!source.ended, seeking: !!source.seeking, buffering: event === "waiting", error: !!source.error };
6518
+ if (!validState(state)) return;
6519
+ lastPublished = host.performance.now();
6520
+ send("state", { sequence: ++sequence, event, state });
6521
+ if (!state.paused && !state.ended && !state.error && !state.buffering && !host.document.hidden) timer = owned.later(() => sample(), 250);
6522
+ }
6523
+ function disconnect(reason) {
6524
+ stop();
6525
+ source.pause();
6526
+ if (session) send("disconnect", { reason });
6527
+ session = null;
6528
+ }
6529
+ function message(event) {
6530
+ const data = event.data;
6531
+ if (destroyed || suspended || event.source !== target || event.origin !== origin || !packet(data, channel, assetId)) return;
6532
+ if (data.type === "connect" && typeof data.session === "string" && /^[0-9a-f-]{36}$/i.test(data.session)) {
6533
+ if (session !== data.session) {
6534
+ session = data.session;
6535
+ sequence = 0;
6536
+ commandSequence = 0;
6537
+ lastPublished = null;
6538
+ }
6539
+ sample();
6540
+ return;
6541
+ }
6542
+ if (data.session !== session || !session) return;
6543
+ if (data.type === "disconnect") {
6544
+ disconnect("parent-disconnected");
6545
+ return;
6546
+ }
6547
+ if (data.type !== "command" || !Number.isSafeInteger(data.sequence) || data.sequence <= commandSequence) return;
6548
+ if (!["pause", "seek", "play"].includes(data.command)) return;
6549
+ if (data.command === "seek" && (typeof data.time !== "number" || !Number.isFinite(data.time) || data.time < 0 || data.time > Number(source.duration))) return;
6550
+ commandSequence = data.sequence;
6551
+ const currentSession = session;
6552
+ Promise.resolve().then(() => {
6553
+ if (destroyed || session !== currentSession) return;
6554
+ if (data.command === "pause") source.pause();
6555
+ else if (data.command === "seek") source.currentTime = data.time;
6556
+ else if (options.allowPlayCommands === true) return source.play();
6557
+ else throw new Error("Parent play commands are disabled");
6558
+ }).then(() => {
6559
+ if (!destroyed && session === currentSession) {
6560
+ sample();
6561
+ send("ack", { sequence: data.sequence, ok: true });
6562
+ }
6563
+ }, () => {
6564
+ if (!destroyed && session === currentSession) send("ack", { sequence: data.sequence, ok: false });
6565
+ });
6566
+ }
6567
+ owned.listen(host, "message", message);
6568
+ function requestClose() {
6569
+ if (!destroyed && !suspended && session) send("dismiss", { sequence: ++sequence });
6570
+ }
6571
+ if (options.notifyEscape === true) owned.listen(host.document, "keydown", (event) => {
6572
+ if (event.key === "Escape" && !event.defaultPrevented && !host.document.fullscreenElement) requestClose();
6573
+ });
6574
+ for (const event of EVENTS) if (event !== "snapshot") owned.listen(source, event, () => sample(event));
6575
+ owned.listen(host.document, "visibilitychange", () => {
6576
+ if (host.document.hidden) {
6577
+ stop();
6578
+ if (session) {
6579
+ sample("waiting");
6580
+ stop();
6581
+ }
6582
+ } else sample();
6583
+ });
6584
+ owned.listen(host, "pagehide", () => {
6585
+ suspended = true;
6586
+ disconnect("pagehide");
6587
+ });
6588
+ owned.listen(host, "pageshow", () => {
6589
+ if (!destroyed) {
6590
+ suspended = false;
6591
+ send("available");
6592
+ }
6593
+ });
6594
+ routes.add(key);
6595
+ owners.set(target, routes);
6596
+ publisherOwners.set(host, owners);
6597
+ send("available");
6598
+ return { requestClose, teardown() {
6599
+ if (destroyed) return;
6600
+ disconnect("teardown");
6601
+ destroyed = true;
6602
+ owned.dispose();
6603
+ routes.delete(key);
6604
+ if (!routes.size) owners.delete(target);
6605
+ }, snapshot() {
6606
+ return { kind: "child-publisher", connected: !!session, destroyed, suspended, ...owned.snapshot() };
6607
+ } };
6608
+ }
6609
+ function createPlaybackReceiver(options = {}) {
6610
+ const { host, channel, assetId, origin } = context(options), iframe = options.iframe;
6611
+ if (iframe?.tagName !== "IFRAME" || !iframe.contentWindow) throw new TypeError("An attached iframe is required");
6612
+ if (receiverOwners.has(iframe)) throw new Error("The child frame already has a playback receiver");
6613
+ const staleAfter = options.staleAfterMilliseconds ?? 1500;
6614
+ if (typeof staleAfter !== "number" || !Number.isFinite(staleAfter) || staleAfter < 750 || staleAfter > 1e4) throw new RangeError("staleAfterMilliseconds must be between 750 and 10000");
6615
+ const owned = resources(host), player = new host.EventTarget(), pending = /* @__PURE__ */ new Map();
6616
+ let session = "", sequence = 0, commands = 0, lastWall = 0, timer = 0, connectTimer = 0, destroyed = false, connected = false, status = "connecting";
6617
+ let state = { time: 0, duration: 0, rate: 1, paused: true, ended: false, seeking: false, buffering: false, error: false };
6618
+ let resolveReady, rejectReady, ready, readySettled = false;
6619
+ function prepareReady() {
6620
+ readySettled = false;
6621
+ ready = new Promise((resolve, reject) => {
6622
+ resolveReady = (value) => {
6623
+ readySettled = true;
6624
+ resolve(value);
6625
+ };
6626
+ rejectReady = (error) => {
6627
+ readySettled = true;
6628
+ reject(error);
6629
+ };
6630
+ });
6631
+ ready.catch(() => {
6632
+ });
6633
+ }
6634
+ prepareReady();
6635
+ const emit = (event) => player.dispatchEvent(new host.Event(event));
6636
+ const envelope = (type, extra = {}) => ({ protocol: PROTOCOL, channel, assetId, session, type, ...extra });
6637
+ function send(type, extra) {
6638
+ iframe.contentWindow?.postMessage(envelope(type, extra), origin);
6639
+ }
6640
+ function cancelPending() {
6641
+ for (const request of pending.values()) {
6642
+ owned.cancel(request.timer);
6643
+ request.reject(new Error("Child playback disconnected"));
6644
+ }
6645
+ pending.clear();
6646
+ }
6647
+ function stop(reason) {
6648
+ owned.cancel(timer);
6649
+ owned.cancel(connectTimer);
6650
+ timer = connectTimer = 0;
6651
+ const playing = !player.paused;
6652
+ state = { ...state, time: player.currentTime, paused: true, buffering: false };
6653
+ connected = false;
6654
+ status = reason;
6655
+ cancelPending();
6656
+ if (playing) emit("pause");
6657
+ }
6658
+ function connect() {
6659
+ if (destroyed) return ready;
6660
+ if (readySettled) prepareReady();
6661
+ stop("connecting");
6662
+ session = host.crypto.randomUUID();
6663
+ sequence = 0;
6664
+ send("connect");
6665
+ const started = host.performance.now();
6666
+ const retry = () => {
6667
+ if (destroyed || connected) return;
6668
+ if (host.performance.now() - started >= 1e4) {
6669
+ send("disconnect");
6670
+ stop("unavailable");
6671
+ session = "";
6672
+ rejectReady(new Error("Child playback did not become ready"));
6673
+ return;
6674
+ }
6675
+ send("connect");
6676
+ connectTimer = owned.later(retry, 500);
6677
+ };
6678
+ connectTimer = owned.later(retry, 500);
6679
+ return ready;
6680
+ }
6681
+ function command(name, time) {
6682
+ if (destroyed || !connected) return Promise.reject(new Error("Child playback is unavailable"));
6683
+ if (pending.size >= 16) return Promise.reject(new Error("Too many pending child playback commands"));
6684
+ const id = ++commands;
6685
+ return new Promise((resolve, reject) => {
6686
+ const timeout = owned.later(() => {
6687
+ pending.delete(id);
6688
+ reject(new Error("Child playback command timed out"));
6689
+ }, 2e3);
6690
+ pending.set(id, { resolve, reject, timer: timeout });
6691
+ send("command", { sequence: id, command: name, ...name === "seek" ? { time } : {} });
6692
+ });
6693
+ }
6694
+ Object.defineProperties(player, {
6695
+ nodeType: { value: 1 },
6696
+ tagName: { value: "VIDEO" },
6697
+ ownerDocument: { value: host.document },
6698
+ isConnected: { get: () => !destroyed && iframe.isConnected },
6699
+ currentTime: { get: () => Math.min(state.duration || MAX_SECONDS, state.time + (!player.paused && !state.seeking ? Math.min(500, Math.max(0, host.performance.now() - lastWall)) / 1e3 * state.rate : 0)), set: (value) => {
6700
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > state.duration) throw new RangeError("Seek must be inside the child duration");
6701
+ command("seek", value).catch(() => {
6702
+ });
6703
+ } },
6704
+ duration: { get: () => state.duration },
6705
+ paused: { get: () => state.paused || state.buffering || state.error || !connected },
6706
+ ended: { get: () => state.ended },
6707
+ seeking: { get: () => state.seeking },
6708
+ playbackRate: { get: () => state.rate },
6709
+ error: { get: () => state.error ? new Error("Child media error") : null }
6710
+ });
6711
+ player.play = () => command("play");
6712
+ player.pause = () => {
6713
+ command("pause").catch(() => {
6714
+ });
6715
+ };
6716
+ function message(event) {
6717
+ const data = event.data;
6718
+ if (destroyed || event.source !== iframe.contentWindow || event.origin !== origin || !packet(data, channel, assetId)) return;
6719
+ if (data.type === "available") {
6720
+ if (!host.document.hidden) {
6721
+ if (connected) send("connect");
6722
+ else connect();
6723
+ }
6724
+ return;
6725
+ }
6726
+ if (data.session !== session) return;
6727
+ if (data.type === "dismiss" && Number.isSafeInteger(data.sequence) && data.sequence > sequence) {
6728
+ sequence = data.sequence;
6729
+ emit("dismiss");
6730
+ return;
6731
+ }
6732
+ if (data.type === "disconnect") {
6733
+ stop("disconnected");
6734
+ session = "";
6735
+ return;
6736
+ }
6737
+ if (data.type === "ack" && Number.isSafeInteger(data.sequence) && typeof data.ok === "boolean") {
6738
+ const request = pending.get(data.sequence);
6739
+ if (request) {
6740
+ owned.cancel(request.timer);
6741
+ pending.delete(data.sequence);
6742
+ data.ok ? request.resolve() : request.reject(new Error("Child playback rejected the command"));
6743
+ }
6744
+ return;
6745
+ }
6746
+ if (data.type !== "state" || !Number.isSafeInteger(data.sequence) || data.sequence <= sequence || !EVENTS.has(data.event) || !validState(data.state)) return;
6747
+ const previous = state, wasConnected = connected, wasPaused = player.paused;
6748
+ sequence = data.sequence;
6749
+ state = { ...data.state };
6750
+ lastWall = host.performance.now();
6751
+ connected = true;
6752
+ status = "connected";
6753
+ owned.cancel(connectTimer);
6754
+ connectTimer = 0;
6755
+ owned.cancel(timer);
6756
+ timer = 0;
6757
+ if (!wasConnected) {
6758
+ emit("loadedmetadata");
6759
+ resolveReady(controller);
6760
+ } else if (previous.duration !== state.duration) emit("durationchange");
6761
+ if (previous.rate !== state.rate) emit("ratechange");
6762
+ if (state.seeking && !previous.seeking) emit("seeking");
6763
+ if (!state.seeking && previous.seeking) emit("seeked");
6764
+ if (!state.seeking && !previous.seeking && wasConnected && Math.abs(state.time - previous.time - (wasPaused ? 0 : (lastWall - previousWall) / 1e3 * previous.rate)) > 0.4) {
6765
+ emit("seeking");
6766
+ emit("seeked");
6767
+ }
6768
+ previousWall = lastWall;
6769
+ if (wasPaused && !player.paused) {
6770
+ emit("play");
6771
+ emit("playing");
6772
+ }
6773
+ if (!wasPaused && player.paused) emit("pause");
6774
+ if (state.ended && !previous.ended) emit("ended");
6775
+ if (data.event === "waiting") emit("waiting");
6776
+ if (state.error && !previous.error) emit("error");
6777
+ emit("timeupdate");
6778
+ if (!player.paused) timer = owned.later(() => {
6779
+ send("disconnect");
6780
+ stop("stale");
6781
+ session = "";
6782
+ emit("waiting");
6783
+ }, staleAfter);
6784
+ }
6785
+ let previousWall = 0;
6786
+ owned.listen(host, "message", message);
6787
+ owned.listen(iframe, "load", connect);
6788
+ owned.listen(host, "pagehide", () => {
6789
+ send("disconnect");
6790
+ stop("suspended");
6791
+ session = "";
6792
+ });
6793
+ owned.listen(host, "pageshow", connect);
6794
+ owned.listen(host.document, "visibilitychange", () => {
6795
+ if (host.document.hidden) {
6796
+ send("command", { sequence: ++commands, command: "pause" });
6797
+ stop("hidden");
6798
+ session = "";
6799
+ } else connect();
6800
+ });
6801
+ const observer = new host.MutationObserver(() => {
6802
+ if (!iframe.isConnected) controller.teardown();
6803
+ });
6804
+ observer.observe(host.document.documentElement, { childList: true, subtree: true });
6805
+ const controller = { player, get ready() {
6806
+ return ready;
6807
+ }, reconnect: connect, teardown() {
6808
+ if (destroyed) return;
6809
+ send("command", { sequence: ++commands, command: "pause" });
6810
+ send("disconnect");
6811
+ stop("destroyed");
6812
+ destroyed = true;
6813
+ session = "";
6814
+ rejectReady(new Error("Child playback owner removed"));
6815
+ observer.disconnect();
6816
+ owned.dispose();
6817
+ receiverOwners.delete(iframe);
6818
+ }, snapshot() {
6819
+ return { kind: "child-receiver", status, connected, destroyed, time: player.currentTime, paused: player.paused, assetId, ...owned.snapshot() };
6820
+ } };
6821
+ receiverOwners.set(iframe, controller);
6822
+ connect();
6823
+ return controller;
6824
+ }
6825
+ function installChildPlayback(scope, api, host) {
6826
+ function own(controller) {
6827
+ const teardown = controller.teardown.bind(controller);
6828
+ const remove = scope.onDispose(() => teardown());
6829
+ controller.teardown = () => {
6830
+ remove();
6831
+ teardown();
6832
+ };
6833
+ return controller;
6834
+ }
6835
+ api.publishPlayback = (options) => {
6836
+ if (api.destroyed) throw new Error("DanceMoves owner is destroyed");
6837
+ return own(createPlaybackPublisher({ ...options, window: host }));
6838
+ };
6839
+ api.receiveChildPlayback = (options) => {
6840
+ if (api.destroyed) throw new Error("DanceMoves owner is destroyed");
6841
+ if (typeof api.registerPlaybackProvider !== "function") throw new Error("The shared playback provider API is required");
6842
+ if (options?.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6843
+ const receiver = createPlaybackReceiver({ ...options, window: host });
6844
+ let registration;
6845
+ try {
6846
+ registration = api.registerPlaybackProvider({ ...options, player: receiver.player });
6847
+ } catch (error) {
6848
+ receiver.teardown();
6849
+ throw error;
6850
+ }
6851
+ return own({ source: registration.source, player: receiver.player, get ready() {
6852
+ return receiver.ready;
6853
+ }, mapping: registration.mapping, reconnect: receiver.reconnect, teardown() {
6854
+ receiver.teardown();
6855
+ registration.teardown();
6856
+ }, snapshot() {
6857
+ return { ...receiver.snapshot(), mapping: registration.mapping };
6858
+ } });
6859
+ };
6860
+ }
6861
+
5952
6862
  // .build/modules/version.mjs
5953
- var VERSION = "3.1.19";
6863
+ var VERSION = "3.1.21";
5954
6864
 
5955
6865
  // .build/modules/index.mjs
5956
6866
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -5985,6 +6895,7 @@ function createDanceMoves(options = {}) {
5985
6895
  }
5986
6896
  try {
5987
6897
  runtime_default.core(scope);
6898
+ installPlayback(scope, w.DanceMoves, root.ownerDocument.defaultView);
5988
6899
  runtime_default.effects(scope);
5989
6900
  runtime_default.orientationCore(scope);
5990
6901
  runtime_default.native(scope);
@@ -6017,6 +6928,7 @@ function createDanceMoves(options = {}) {
6017
6928
  }
6018
6929
  });
6019
6930
  Object.defineProperty(api, "destroyed", { get: () => scope.disposed });
6931
+ installChildPlayback(scope, api, ownerDocument.defaultView);
6020
6932
  api.ready = Promise.resolve(api.ready).then(() => {
6021
6933
  if (!scope.disposed) scope.document.dispatchEvent(new w.CustomEvent("dance-moves-ready", {
6022
6934
  bubbles: true,