@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.mjs CHANGED
@@ -17,6 +17,7 @@ function install(__dmScope) {
17
17
  var lyricList = [];
18
18
  var lyricRenderer = null;
19
19
  var boundAudio = /* @__PURE__ */ new WeakSet();
20
+ var playbackBindings = /* @__PURE__ */ new Map();
20
21
  var activeAudio = null;
21
22
  var pageClockStarted = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
22
23
  var referenceDurationSeconds = positiveNumber(rawConfig.masterDurationMilliseconds) / 1e3;
@@ -106,13 +107,16 @@ function install(__dmScope) {
106
107
  }
107
108
  function currentTick(options) {
108
109
  var settings = options || {};
109
- var audio = settings.audio || (settings.clock === "audio" ? activeAudio : null);
110
+ var audio = playbackAudio(settings.audio) || (settings.clock === "audio" ? activeAudio : null);
110
111
  if (audio && !audio.paused && !audio.ended && Number.isFinite(Number(audio.currentTime))) {
111
112
  return Number(audio.currentTime) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 60;
112
113
  }
113
114
  var now = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
114
115
  return Math.max(0, now - pageClockStarted) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 6e4;
115
116
  }
117
+ function playbackAudio(audio) {
118
+ return audio && api && api.playbackSource ? api.playbackSource(audio) : audio;
119
+ }
116
120
  function nextIntervalTick(intervalTicks, fromTick, strictlyFuture) {
117
121
  var interval = Math.max(1, roundHalfUp(intervalTicks));
118
122
  var tick = Math.max(0, Number(fromTick) || 0);
@@ -169,7 +173,7 @@ function install(__dmScope) {
169
173
  var pollAudio = function() {
170
174
  if (cancelled)
171
175
  return;
172
- var candidate = settings.audio || activeAudio;
176
+ var candidate = playbackAudio(settings.audio) || activeAudio;
173
177
  if (!candidate || candidate.paused || candidate.ended) {
174
178
  if (sourceAudio && sourceAudio.ended) {
175
179
  cancel();
@@ -731,9 +735,18 @@ function install(__dmScope) {
731
735
  return Math.abs(duration - referenceDurationSeconds) <= Math.max(0.25, duration / 1e5);
732
736
  }
733
737
  function bindAudio(audio) {
738
+ if (api.playbackSource)
739
+ audio = api.playbackSource(audio);
734
740
  if (boundAudio.has(audio) || !matchesMasterDuration(audio))
735
741
  return;
736
742
  boundAudio.add(audio);
743
+ var removers = [];
744
+ function listen(target, name, handler) {
745
+ __dmScope.listen(target, name, handler);
746
+ removers.push(function() {
747
+ __dmScope.unlisten(target, name, handler);
748
+ });
749
+ }
737
750
  __dmScope.dataset(audio, "danceMovesTiming", "master-length");
738
751
  var state = {
739
752
  next: firstCueAfter(audio.currentTime || 0),
@@ -741,7 +754,8 @@ function install(__dmScope) {
741
754
  last: audio.currentTime || 0,
742
755
  seeking: false,
743
756
  frame: 0,
744
- pendingCue: cueAtLanding(audio.currentTime || 0)
757
+ pendingCue: cueAtLanding(audio.currentTime || 0),
758
+ endedPending: false
745
759
  };
746
760
  function stopFrame() {
747
761
  if (state.frame)
@@ -750,7 +764,7 @@ function install(__dmScope) {
750
764
  }
751
765
  function tick() {
752
766
  state.frame = 0;
753
- if (audio.paused || audio.ended || state.seeking)
767
+ if (audio.paused || audio.ended || state.seeking || document2.hidden)
754
768
  return;
755
769
  var diagnosticStartedAt = diagnosticsSink ? diagnosticNow() : 0;
756
770
  var firedCount = 0;
@@ -788,8 +802,14 @@ function install(__dmScope) {
788
802
  }
789
803
  function start() {
790
804
  stopFrame();
805
+ if (document2.hidden || audio.paused || audio.ended)
806
+ return;
791
807
  activeAudio = audio;
792
808
  state.last = positiveNumber(audio.currentTime);
809
+ if (state.endedPending) {
810
+ state.pendingCue = cueAtLanding(state.last);
811
+ state.endedPending = false;
812
+ }
793
813
  state.next = firstCueAfter(state.last);
794
814
  state.lyric = -2;
795
815
  if (state.pendingCue) {
@@ -799,47 +819,70 @@ function install(__dmScope) {
799
819
  setOwnedPlaybackRate(audio.playbackRate);
800
820
  state.frame = window2.requestAnimationFrame(tick);
801
821
  }
802
- __dmScope.listen(audio, "play", start);
803
- __dmScope.listen(audio, "playing", start);
804
- __dmScope.listen(audio, "pause", function() {
822
+ listen(audio, "play", start);
823
+ listen(audio, "playing", start);
824
+ listen(audio, "pause", function() {
805
825
  stopFrame();
806
826
  if (activeAudio === audio)
807
827
  activeAudio = null;
808
828
  if (lyricRenderer)
809
829
  lyricRenderer.hide();
810
830
  });
811
- __dmScope.listen(audio, "ended", function() {
831
+ listen(audio, "ended", function() {
812
832
  stopFrame();
813
833
  if (activeAudio === audio)
814
834
  activeAudio = null;
815
835
  state.last = 0;
816
836
  state.next = 0;
817
837
  state.lyric = -2;
818
- state.pendingCue = cueAtLanding(0);
838
+ state.pendingCue = null;
839
+ state.endedPending = true;
819
840
  if (lyricRenderer)
820
841
  lyricRenderer.hide();
821
842
  });
822
- __dmScope.listen(audio, "seeking", function() {
843
+ listen(audio, "seeking", function() {
823
844
  state.seeking = true;
824
845
  state.pendingCue = null;
825
846
  stopFrame();
826
847
  });
827
- __dmScope.listen(audio, "seeked", function() {
848
+ listen(audio, "seeked", function() {
828
849
  state.seeking = false;
829
850
  state.last = positiveNumber(audio.currentTime);
830
851
  state.next = firstCueAfter(state.last);
831
852
  state.lyric = -2;
832
853
  state.pendingCue = cueAtLanding(state.last);
854
+ playbackBindings.set(audio, function() {
855
+ stopFrame();
856
+ removers.splice(0).forEach(function(remove) {
857
+ remove();
858
+ });
859
+ boundAudio.delete(audio);
860
+ if (activeAudio === audio)
861
+ activeAudio = null;
862
+ playbackBindings.delete(audio);
863
+ });
833
864
  if (!audio.paused && !audio.ended)
834
865
  start();
835
866
  });
836
- __dmScope.listen(audio, "ratechange", function() {
867
+ listen(audio, "ratechange", function() {
837
868
  setOwnedPlaybackRate(audio.playbackRate);
838
869
  });
839
- __dmScope.listen(audio, "timeupdate", function() {
870
+ listen(audio, "timeupdate", function() {
840
871
  if (!audio.paused && !state.frame && !state.seeking)
841
872
  tick();
842
873
  });
874
+ listen(document2, "visibilitychange", function() {
875
+ if (document2.hidden) {
876
+ stopFrame();
877
+ if (lyricRenderer)
878
+ lyricRenderer.hide();
879
+ } else if (!audio.paused && !audio.ended && !state.seeking) {
880
+ state.pendingCue = null;
881
+ start();
882
+ }
883
+ });
884
+ if (!audio.paused && !audio.ended)
885
+ start();
843
886
  }
844
887
  function discoverAudio() {
845
888
  var audioElements = Array.from(document2.querySelectorAll("audio"));
@@ -953,6 +996,16 @@ function install(__dmScope) {
953
996
  registerAnimationScope,
954
997
  resetRunningAnimations,
955
998
  discoverAudio,
999
+ unregisterPlaybackSource: function(source) {
1000
+ var remove = playbackBindings.get(source);
1001
+ if (remove)
1002
+ remove();
1003
+ },
1004
+ registerPlaybackSource: function(source) {
1005
+ if (!referenceDurationSeconds)
1006
+ referenceDurationSeconds = positiveNumber(source.duration);
1007
+ bindAudio(source);
1008
+ },
956
1009
  setDiagnosticsSink,
957
1010
  diagnosticsEnabled: function() {
958
1011
  return diagnosticsAllowed && Boolean(diagnosticsSink);
@@ -993,6 +1046,10 @@ function install2(__dmScope) {
993
1046
  return document2.querySelector(value);
994
1047
  return fallback || null;
995
1048
  }
1049
+ function playbackElement(value, fallback) {
1050
+ var target = element(value, fallback);
1051
+ return target && motion.playbackSource ? motion.playbackSource(target) : target;
1052
+ }
996
1053
  function media(query) {
997
1054
  return window2.matchMedia(query);
998
1055
  }
@@ -1118,7 +1175,7 @@ function install2(__dmScope) {
1118
1175
  function playbackPulse(options) {
1119
1176
  options = options || {};
1120
1177
  var state = commonState(options, "pulse");
1121
- var audio = element(options.audio, state.root.querySelector("audio"));
1178
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1122
1179
  if (!audio)
1123
1180
  throw new Error("DanceMovesEffects pulse requires an audio element");
1124
1181
  var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
@@ -1218,7 +1275,7 @@ function install2(__dmScope) {
1218
1275
  function cueTimeline(options) {
1219
1276
  options = options || {};
1220
1277
  var state = commonState(options, "cue-timeline");
1221
- var audio = element(options.audio, state.root.querySelector("audio"));
1278
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1222
1279
  if (!audio)
1223
1280
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1224
1281
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
@@ -1379,7 +1436,7 @@ function install2(__dmScope) {
1379
1436
  function spritePlayback(options) {
1380
1437
  options = options || {};
1381
1438
  var state = commonState(options, "sprite-playback");
1382
- var audio = element(options.audio, state.root.querySelector("audio"));
1439
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1383
1440
  var target = element(options.target, state.root);
1384
1441
  if (!audio)
1385
1442
  throw new Error("DanceMovesEffects spritePlayback requires an audio element");
@@ -1573,7 +1630,7 @@ function install2(__dmScope) {
1573
1630
  function lyricStage(options) {
1574
1631
  options = options || {};
1575
1632
  var state = commonState(options, "lyric-stage");
1576
- var audio = element(options.audio, state.root.querySelector("audio"));
1633
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1577
1634
  var popover = element(options.popover, document2.querySelector(".dance-moves-lyric-popover"));
1578
1635
  if (!audio)
1579
1636
  throw new Error("DanceMovesEffects lyricStage requires an audio element");
@@ -4792,9 +4849,11 @@ function install9(__dmScope) {
4792
4849
  const clock = options.clock === void 0 ? "page" : options.clock;
4793
4850
  if (!["page", "audio", "auto"].includes(clock))
4794
4851
  throw new TypeError("clock must be page, audio or auto");
4795
- const audio = clock === "page" ? null : element(options.audio, "audio");
4796
- if (audio && String(audio.tagName).toLowerCase() !== "audio")
4797
- throw new TypeError("audio must be an HTMLAudioElement");
4852
+ let audio = clock === "page" ? null : element(options.audio, "audio");
4853
+ if (audio && motion.playbackSource)
4854
+ audio = motion.playbackSource(audio);
4855
+ if (audio && !["audio", "video"].includes(String(audio.tagName).toLowerCase()))
4856
+ throw new TypeError("audio must resolve to native media or a shared playback source");
4798
4857
  const rate = options.rate === void 0 ? 1 : number(options.rate, "rate");
4799
4858
  if (rate <= 0 || rate > 64)
4800
4859
  throw new RangeError("rate must be in (0, 64]");
@@ -5042,7 +5101,7 @@ function install9(__dmScope) {
5042
5101
  }
5043
5102
  if (options.resetOnCue)
5044
5103
  disposers.push(motion.onCue(typeof options.resetOnCue === "string" ? options.resetOnCue : "*", (detail) => {
5045
- if (!detail.audio || !audio || detail.audio === audio)
5104
+ if (!detail.audio || !audio || detail.audio === audio || motion.playbackSource && motion.playbackSource(detail.audio) === motion.playbackSource(audio))
5046
5105
  reset();
5047
5106
  }, { id: id + ":cue-reset" }));
5048
5107
  if (options.offscreen !== false && window2.IntersectionObserver) {
@@ -5316,6 +5375,8 @@ function createScope(root, config, { legacyGlobals = false, nonce, orientation =
5316
5375
  }
5317
5376
  const api = {
5318
5377
  id,
5378
+ root,
5379
+ legacyGlobals,
5319
5380
  window,
5320
5381
  document,
5321
5382
  get disposed() {
@@ -5918,8 +5979,855 @@ function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples =
5918
5979
  });
5919
5980
  }
5920
5981
 
5982
+ // .build/modules/playback.mjs
5983
+ var youtubeLoaders = /* @__PURE__ */ new WeakMap();
5984
+ function validatePlaybackMapping(input, masterDuration) {
5985
+ const offset = input.offset ?? 0, duration = input.duration ?? masterDuration, playerStart = input.playerStart ?? 0;
5986
+ for (const [key, value] of Object.entries({ offset, duration, playerStart, masterDuration })) {
5987
+ 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`);
5988
+ }
5989
+ if (offset + duration > masterDuration + 1e-3) throw new RangeError("The clip extends beyond its associated master");
5990
+ return Object.freeze({ offset, duration, playerStart, masterDuration });
5991
+ }
5992
+ function loadYouTubeAPI(host) {
5993
+ if (host.YT?.Player) return Promise.resolve(host.YT);
5994
+ if (youtubeLoaders.has(host)) return youtubeLoaders.get(host);
5995
+ const pending = new Promise((resolve, reject) => {
5996
+ const previous = host.onYouTubeIframeAPIReady;
5997
+ let timeout;
5998
+ const script = host.document.createElement("script");
5999
+ function finish(error) {
6000
+ host.clearTimeout(timeout);
6001
+ if (host.onYouTubeIframeAPIReady === ready) host.onYouTubeIframeAPIReady = previous;
6002
+ script.removeEventListener("error", failed);
6003
+ if (error) {
6004
+ script.remove();
6005
+ youtubeLoaders.delete(host);
6006
+ reject(error);
6007
+ } else resolve(host.YT);
6008
+ }
6009
+ function ready() {
6010
+ try {
6011
+ previous?.();
6012
+ } finally {
6013
+ finish(host.YT?.Player ? null : new Error("YouTube API unavailable"));
6014
+ }
6015
+ }
6016
+ function failed() {
6017
+ finish(new Error("YouTube API failed to load"));
6018
+ }
6019
+ host.onYouTubeIframeAPIReady = ready;
6020
+ script.src = "https://www.youtube.com/iframe_api";
6021
+ script.addEventListener("error", failed, { once: true });
6022
+ timeout = host.setTimeout(() => finish(new Error("YouTube API timed out")), 2e4);
6023
+ host.document.head.appendChild(script);
6024
+ });
6025
+ youtubeLoaders.set(host, pending);
6026
+ return pending;
6027
+ }
6028
+ function installPlayback(scope, api, host = scope.document.defaultView) {
6029
+ const groups = /* @__PURE__ */ new Map(), transports = /* @__PURE__ */ new WeakSet(), aliases = /* @__PURE__ */ new WeakMap(), controllers = /* @__PURE__ */ new Set(), registered = /* @__PURE__ */ new WeakMap();
6030
+ let disposed = false;
6031
+ function emit(target, type) {
6032
+ target.dispatchEvent(new host.Event(type));
6033
+ }
6034
+ function groupFor(reference) {
6035
+ if (disposed) throw new Error("DanceMoves playback owner is destroyed");
6036
+ const master = reference?.element || reference, declared = reference?.element ? reference.duration : void 0;
6037
+ const isNative = !!master?.addEventListener && ["AUDIO", "VIDEO"].includes(master.tagName);
6038
+ 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");
6039
+ if (isNative && (master.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(master))) throw new TypeError("The master must belong to this runtime root");
6040
+ if (groups.has(master)) {
6041
+ const group2 = groups.get(master);
6042
+ group2.setDuration(declared);
6043
+ return group2;
6044
+ }
6045
+ const transport = new host.EventTarget();
6046
+ transports.add(transport);
6047
+ let active = null, lastPaused = true, delegate = null;
6048
+ let declaredDuration = declared || 0;
6049
+ function setDuration(value) {
6050
+ if (value === void 0) return;
6051
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new RangeError("Declared master duration must be positive and finite");
6052
+ if (declaredDuration && Math.abs(declaredDuration - value) > 1e-3) throw new RangeError("Conflicting master durations");
6053
+ 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");
6054
+ declaredDuration = value;
6055
+ }
6056
+ setDuration(declared);
6057
+ const duration = () => delegate?.duration || declaredDuration || (Number(master.duration) > 0 && Number.isFinite(Number(master.duration)) ? Number(master.duration) : Number(api.config?.masterDurationMilliseconds || 0) / 1e3);
6058
+ const target = isNative ? master : scope.root;
6059
+ 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) => {
6060
+ if (isNative) master.currentTime = time;
6061
+ }, mapping: null };
6062
+ active = native;
6063
+ function read() {
6064
+ if (delegate) return { time: delegate.currentTime, paused: delegate.paused, ended: delegate.ended, rate: delegate.playbackRate, seeking: delegate.seeking, error: delegate.error };
6065
+ const raw = active.read(), map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6066
+ const local = raw.time - map.playerStart, outside = local < -1e-3 || local > map.duration + 1e-3;
6067
+ 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 };
6068
+ }
6069
+ Object.defineProperties(transport, {
6070
+ nodeType: { value: 1 },
6071
+ tagName: { get: () => isNative ? master.tagName : "VIDEO" },
6072
+ ownerDocument: { get: () => target.ownerDocument },
6073
+ dataset: { get: () => target.dataset },
6074
+ id: { get: () => master.id },
6075
+ isConnected: { get: () => target.isConnected },
6076
+ currentTime: { get: () => read().time, set: (value) => {
6077
+ if (delegate) {
6078
+ delegate.currentTime = value;
6079
+ return;
6080
+ }
6081
+ const map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6082
+ active.seek(map.playerStart + Math.max(0, Math.min(map.duration, Number(value) - map.offset)));
6083
+ } },
6084
+ duration: { get: duration },
6085
+ paused: { get: () => read().paused },
6086
+ ended: { get: () => read().ended },
6087
+ seeking: { get: () => read().seeking || false },
6088
+ playbackRate: { get: () => read().rate },
6089
+ error: { get: () => read().error || null },
6090
+ element: { value: isNative ? master : null },
6091
+ activePlayer: { get: () => delegate ? delegate.activePlayer : active.element },
6092
+ masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id }
6093
+ });
6094
+ transport.matches = (selector) => target.matches(selector);
6095
+ transport.getAttribute = (name) => target.getAttribute(name);
6096
+ transport.setAttribute = (name, value) => target.setAttribute(name, value);
6097
+ transport.removeAttribute = (name) => target.removeAttribute(name);
6098
+ transport.play = () => delegate ? delegate.play() : active.play();
6099
+ transport.pause = () => delegate ? delegate.pause() : active.pause();
6100
+ function activate(source) {
6101
+ if (active !== source) {
6102
+ const previous = active;
6103
+ active = source;
6104
+ previous.pause();
6105
+ emit(transport, "seeking");
6106
+ emit(transport, "seeked");
6107
+ }
6108
+ }
6109
+ function forward(source, type) {
6110
+ if (type === "play" || type === "playing") activate(source);
6111
+ if (active !== source) return;
6112
+ const paused = read().paused;
6113
+ if (type === "timeupdate" && paused !== lastPaused) emit(transport, paused ? "pause" : "playing");
6114
+ lastPaused = paused;
6115
+ emit(transport, type);
6116
+ }
6117
+ const events = ["loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"];
6118
+ const removers = [];
6119
+ function bindNative(element, mapping) {
6120
+ 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) => {
6121
+ element.currentTime = time;
6122
+ } };
6123
+ source.removers = [];
6124
+ for (const type of events) {
6125
+ const listener = () => forward(source, type);
6126
+ scope.listen(element, type, listener);
6127
+ const remove = () => scope.unlisten(element, type, listener);
6128
+ removers.push(remove);
6129
+ source.removers.push(remove);
6130
+ }
6131
+ return source;
6132
+ }
6133
+ if (isNative) bindNative(master, null);
6134
+ const group = {
6135
+ transport,
6136
+ duration,
6137
+ setDuration,
6138
+ activate,
6139
+ forward,
6140
+ bindNative,
6141
+ isActive: (source) => active === source,
6142
+ aliasTo(target2) {
6143
+ removers.splice(0).forEach((remove) => remove());
6144
+ delegate = target2;
6145
+ aliases.set(transport, target2);
6146
+ for (const type of events) {
6147
+ const relay = () => emit(transport, type);
6148
+ scope.listen(target2, type, relay);
6149
+ removers.push(() => scope.unlisten(target2, type, relay));
6150
+ }
6151
+ },
6152
+ reconcile(source) {
6153
+ if (active !== source) return;
6154
+ const paused = read().paused;
6155
+ if (paused !== lastPaused) {
6156
+ lastPaused = paused;
6157
+ emit(transport, paused ? "pause" : "playing");
6158
+ }
6159
+ emit(transport, "timeupdate");
6160
+ },
6161
+ remove(source) {
6162
+ if (active === source) {
6163
+ source.pause();
6164
+ active = native;
6165
+ lastPaused = true;
6166
+ emit(transport, "pause");
6167
+ emit(transport, "seeking");
6168
+ emit(transport, "seeked");
6169
+ }
6170
+ },
6171
+ destroy() {
6172
+ removers.splice(0).forEach((remove) => remove());
6173
+ emit(transport, "pause");
6174
+ }
6175
+ };
6176
+ groups.set(master, group);
6177
+ return group;
6178
+ }
6179
+ function registerEndpoint(options, provider = false) {
6180
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6181
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6182
+ const element = options.player;
6183
+ if (!element?.addEventListener || !["AUDIO", "VIDEO"].includes(element.tagName)) throw new TypeError("player must be native audio or video");
6184
+ if (element.ownerDocument !== host.document || !provider && !scope.legacyGlobals && !scope.root.contains(element)) throw new TypeError("The player must belong to this runtime root");
6185
+ if (provider && (!element.removeEventListener || typeof element.play !== "function" || typeof element.pause !== "function")) throw new TypeError("An event-driven playback provider is required");
6186
+ if (provider && typeof host.Node === "function" && element instanceof host.Node) throw new TypeError("Use registerPlayer for native DOM players");
6187
+ if (registered.has(element)) throw new Error("Player already has a playback owner");
6188
+ if (element === options.master || element === group.transport.element) throw new TypeError("The master is already registered");
6189
+ const previousGroup = groups.get(element);
6190
+ if (previousGroup && previousGroup !== group) {
6191
+ if ([...controllers].some((owner) => owner.source === previousGroup.transport)) throw new Error("This player already owns a different master group");
6192
+ api.unregisterPlaybackSource?.(previousGroup.transport);
6193
+ previousGroup.aliasTo(group.transport);
6194
+ for (const [key, value] of groups) if (value === previousGroup) groups.set(key, group);
6195
+ }
6196
+ const source = group.bindNative(element, mapping);
6197
+ let destroyed = false;
6198
+ const controller = { source: group.transport, mapping, teardown() {
6199
+ if (destroyed) return;
6200
+ destroyed = true;
6201
+ source.removers.splice(0).forEach((remove) => remove());
6202
+ group.remove(source);
6203
+ registered.delete(element);
6204
+ controllers.delete(controller);
6205
+ }, snapshot() {
6206
+ return { kind: provider ? "provider" : "native", mapping, active: group.isActive(source), destroyed };
6207
+ } };
6208
+ registered.set(element, controller);
6209
+ if (!element.paused && !element.ended) group.forward(source, "playing");
6210
+ Promise.resolve(api.ready).then(() => {
6211
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6212
+ });
6213
+ controllers.add(controller);
6214
+ return controller;
6215
+ }
6216
+ const registerPlayer = (options) => registerEndpoint(options);
6217
+ const registerPlaybackProvider = (options) => registerEndpoint(options, true);
6218
+ function registerYouTube(options) {
6219
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6220
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6221
+ const iframe = options.iframe;
6222
+ if (iframe?.tagName !== "IFRAME") throw new TypeError("A YouTube iframe is required");
6223
+ if (iframe.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(iframe)) throw new TypeError("The iframe must belong to this runtime root");
6224
+ if (registered.has(iframe)) throw new Error("Player already has a playback owner");
6225
+ const url2 = new URL(iframe.src, host.location.href);
6226
+ if (!/^https:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[A-Za-z0-9_-]{11}(\?|$)/.test(url2.href)) throw new TypeError("Unsupported YouTube embed");
6227
+ url2.searchParams.set("enablejsapi", "1");
6228
+ url2.searchParams.set("origin", host.location.origin);
6229
+ const expectedVideoId = url2.pathname.split("/").at(-1);
6230
+ if (iframe.src !== url2.href) iframe.src = url2.href;
6231
+ let player = null, state = -1, error = null, frame = 0, destroyed = false, lastTime = null, lastWall = null, lastRate = 1, readyTimer = 0, rejectReady = null;
6232
+ const source = {
6233
+ element: iframe,
6234
+ mapping,
6235
+ read: () => ({ time: player?.getCurrentTime?.() || 0, paused: state !== 1, ended: state === 0, rate: player?.getPlaybackRate?.() || 1, error }),
6236
+ pause: () => player?.pauseVideo?.(),
6237
+ play: () => player?.playVideo?.(),
6238
+ seek: (time) => player?.seekTo?.(time, true)
6239
+ };
6240
+ function stop() {
6241
+ if (frame) scope.window.cancelAnimationFrame(frame);
6242
+ frame = 0;
6243
+ lastTime = null;
6244
+ lastWall = null;
6245
+ }
6246
+ function sample(wall) {
6247
+ frame = 0;
6248
+ if (destroyed || state !== 1 || scope.document.hidden || !group.isActive(source)) return;
6249
+ const raw = source.read();
6250
+ if (lastTime !== null && Math.abs(raw.time - lastTime - (wall - lastWall) / 1e3 * raw.rate) > 0.4) {
6251
+ group.forward(source, "seeking");
6252
+ group.forward(source, "seeked");
6253
+ }
6254
+ if (raw.rate !== lastRate) {
6255
+ lastRate = raw.rate;
6256
+ group.forward(source, "ratechange");
6257
+ }
6258
+ lastTime = raw.time;
6259
+ lastWall = wall;
6260
+ group.reconcile(source);
6261
+ frame = scope.window.requestAnimationFrame(sample);
6262
+ }
6263
+ function resume() {
6264
+ stop();
6265
+ if (!destroyed && state === 1 && !scope.document.hidden && group.isActive(source)) frame = scope.window.requestAnimationFrame(sample);
6266
+ }
6267
+ const visibility = () => {
6268
+ if (scope.document.hidden) stop();
6269
+ else {
6270
+ group.forward(source, "seeking");
6271
+ group.forward(source, "seeked");
6272
+ resume();
6273
+ }
6274
+ };
6275
+ const pageHide = () => stop(), pageShow = () => {
6276
+ if (player?.getPlayerState) state = player.getPlayerState();
6277
+ visibility();
6278
+ };
6279
+ scope.listen(scope.document, "visibilitychange", visibility);
6280
+ scope.listen(host, "pagehide", pageHide);
6281
+ scope.listen(host, "pageshow", pageShow);
6282
+ const controller = {
6283
+ source: group.transport,
6284
+ mapping,
6285
+ ready: null,
6286
+ teardown() {
6287
+ if (destroyed) return;
6288
+ destroyed = true;
6289
+ stop();
6290
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6291
+ readyTimer = 0;
6292
+ rejectReady?.(new Error("YouTube playback owner removed"));
6293
+ rejectReady = null;
6294
+ group.remove(source);
6295
+ scope.unlisten(scope.document, "visibilitychange", visibility);
6296
+ scope.unlisten(host, "pagehide", pageHide);
6297
+ scope.unlisten(host, "pageshow", pageShow);
6298
+ player?.destroy?.();
6299
+ registered.delete(iframe);
6300
+ controllers.delete(controller);
6301
+ },
6302
+ snapshot() {
6303
+ return { kind: "youtube", mapping, state, error, active: group.isActive(source), destroyed, polling: !!frame, time: group.transport.currentTime };
6304
+ }
6305
+ };
6306
+ controllers.add(controller);
6307
+ registered.set(iframe, controller);
6308
+ Promise.resolve(api.ready).then(() => {
6309
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6310
+ });
6311
+ controller.ready = loadYouTubeAPI(host).then((YT) => {
6312
+ if (destroyed || disposed) return controller;
6313
+ return new Promise((resolve, reject) => {
6314
+ rejectReady = reject;
6315
+ readyTimer = scope.window.setTimeout(() => {
6316
+ readyTimer = 0;
6317
+ error = new Error("YouTube player readiness timed out");
6318
+ rejectReady = null;
6319
+ reject(error);
6320
+ }, 15e3);
6321
+ player = new YT.Player(iframe, { events: {
6322
+ onReady() {
6323
+ if (destroyed) return;
6324
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6325
+ readyTimer = 0;
6326
+ rejectReady = null;
6327
+ const actual = Number(player.getDuration?.());
6328
+ if (actual > 0 && actual + 0.25 < mapping.playerStart + mapping.duration) {
6329
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6330
+ reject(error);
6331
+ return;
6332
+ }
6333
+ group.forward(source, "loadedmetadata");
6334
+ const initialState = player.getPlayerState?.();
6335
+ if (initialState === 1) controllerState({ data: initialState });
6336
+ resolve(controller);
6337
+ },
6338
+ onStateChange: controllerState,
6339
+ onPlaybackRateChange() {
6340
+ if (!destroyed) group.forward(source, "ratechange");
6341
+ },
6342
+ onError(event) {
6343
+ if (destroyed) return;
6344
+ error = new Error("YouTube playback error " + event.data);
6345
+ state = -1;
6346
+ stop();
6347
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6348
+ readyTimer = 0;
6349
+ rejectReady = null;
6350
+ group.forward(source, "pause");
6351
+ group.forward(source, "error");
6352
+ reject(error);
6353
+ }
6354
+ } });
6355
+ function controllerState(event) {
6356
+ if (destroyed) return;
6357
+ state = event.data;
6358
+ stop();
6359
+ if (state === 1) {
6360
+ let actualId;
6361
+ try {
6362
+ const actualUrl = new URL(player.getVideoUrl());
6363
+ actualId = actualUrl.searchParams.get("v") || actualUrl.pathname.match(/^\/(?:shorts|embed)\/([A-Za-z0-9_-]{11})\/?$/)?.[1];
6364
+ } catch {
6365
+ }
6366
+ if (actualId !== expectedVideoId) {
6367
+ error = new Error("YouTube switched away from the verified soundtrack asset");
6368
+ state = -1;
6369
+ group.forward(source, "pause");
6370
+ group.forward(source, "error");
6371
+ return;
6372
+ }
6373
+ const actualDuration = Number(player.getDuration?.());
6374
+ if (actualDuration > 0 && actualDuration + 0.25 < mapping.playerStart + mapping.duration) {
6375
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6376
+ state = -1;
6377
+ group.forward(source, "pause");
6378
+ group.forward(source, "error");
6379
+ return;
6380
+ }
6381
+ group.forward(source, "play");
6382
+ group.forward(source, "playing");
6383
+ resume();
6384
+ } else group.forward(source, state === 0 ? "ended" : state === 3 ? "waiting" : "pause");
6385
+ if (state === 3) group.forward(source, "pause");
6386
+ }
6387
+ });
6388
+ });
6389
+ controller.ready.catch(() => {
6390
+ });
6391
+ return controller;
6392
+ }
6393
+ Object.assign(api, { playbackSource: (master) => {
6394
+ let source = transports.has(master) ? master : groupFor(master).transport;
6395
+ while (aliases.has(source)) source = aliases.get(source);
6396
+ return source;
6397
+ }, registerPlayer, registerPlaybackProvider, registerYouTube });
6398
+ scope.onDispose(() => {
6399
+ disposed = true;
6400
+ for (const controller of [...controllers]) controller.teardown();
6401
+ for (const group of new Set(groups.values())) group.destroy();
6402
+ groups.clear();
6403
+ });
6404
+ }
6405
+
6406
+ // .build/modules/child-playback.mjs
6407
+ var PROTOCOL = "dancemoves.child-playback.v1";
6408
+ var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
6409
+ var MAX_SECONDS = 86400;
6410
+ var publisherOwners = /* @__PURE__ */ new WeakMap();
6411
+ var receiverOwners = /* @__PURE__ */ new WeakMap();
6412
+ function identity(value, name) {
6413
+ if (typeof value !== "string" || !value.trim() || value.length > 200) throw new TypeError(`${name} must be a nonempty string of at most 200 characters`);
6414
+ return value;
6415
+ }
6416
+ function exactOrigin(value) {
6417
+ const url2 = new URL(value);
6418
+ if (!["http:", "https:"].includes(url2.protocol) || url2.origin !== value) throw new TypeError("An exact HTTP(S) origin is required");
6419
+ return value;
6420
+ }
6421
+ function context(options) {
6422
+ const host = options.window || globalThis.window;
6423
+ if (!host?.document || !host.crypto?.randomUUID) throw new TypeError("A browser Window with crypto.randomUUID is required");
6424
+ return { host, channel: identity(options.channel, "channel"), assetId: identity(options.assetId, "assetId"), origin: exactOrigin(options.origin) };
6425
+ }
6426
+ function validState(value) {
6427
+ 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");
6428
+ }
6429
+ function packet(data, channel, assetId) {
6430
+ return data && typeof data === "object" && data.protocol === PROTOCOL && data.channel === channel && data.assetId === assetId;
6431
+ }
6432
+ function resources(host) {
6433
+ const removers = [], timers = /* @__PURE__ */ new Set();
6434
+ return {
6435
+ listen(target, type, callback) {
6436
+ target.addEventListener(type, callback);
6437
+ removers.push(() => target.removeEventListener(type, callback));
6438
+ },
6439
+ later(callback, delay) {
6440
+ const id = host.setTimeout(() => {
6441
+ timers.delete(id);
6442
+ callback();
6443
+ }, delay);
6444
+ timers.add(id);
6445
+ return id;
6446
+ },
6447
+ cancel(id) {
6448
+ if (id) {
6449
+ host.clearTimeout(id);
6450
+ timers.delete(id);
6451
+ }
6452
+ },
6453
+ dispose() {
6454
+ removers.splice(0).forEach((remove) => remove());
6455
+ for (const id of timers) host.clearTimeout(id);
6456
+ timers.clear();
6457
+ },
6458
+ snapshot() {
6459
+ return { listeners: removers.length, timers: timers.size };
6460
+ }
6461
+ };
6462
+ }
6463
+ function createPlaybackPublisher(options = {}) {
6464
+ const { host, channel, assetId, origin } = context(options), source = options.source;
6465
+ const target = options.targetWindow || host.parent;
6466
+ if (!source?.addEventListener || !source?.pause || !target?.postMessage || target === host) throw new TypeError("A playback source and distinct target Window are required");
6467
+ const owners = publisherOwners.get(host) || /* @__PURE__ */ new Map(), key = JSON.stringify([origin, channel, assetId]);
6468
+ const routes = owners.get(target) || /* @__PURE__ */ new Set();
6469
+ if (routes.has(key)) throw new Error("Child playback route already has a publisher");
6470
+ const owned = resources(host);
6471
+ let session = null, sequence = 0, commandSequence = 0, timer = 0, destroyed = false, suspended = false, lastPublished = null;
6472
+ const envelope = (type, extra = {}) => ({ protocol: PROTOCOL, channel, assetId, session, type, ...extra });
6473
+ function send(type, extra) {
6474
+ target.postMessage(envelope(type, extra), origin);
6475
+ }
6476
+ function stop() {
6477
+ owned.cancel(timer);
6478
+ timer = 0;
6479
+ }
6480
+ function sample(event = "snapshot") {
6481
+ if (event === "timeupdate" && lastPublished !== null && host.performance.now() - lastPublished < 250) return;
6482
+ stop();
6483
+ if (destroyed || suspended || !session) return;
6484
+ 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 };
6485
+ if (!validState(state)) return;
6486
+ lastPublished = host.performance.now();
6487
+ send("state", { sequence: ++sequence, event, state });
6488
+ if (!state.paused && !state.ended && !state.error && !state.buffering && !host.document.hidden) timer = owned.later(() => sample(), 250);
6489
+ }
6490
+ function disconnect(reason) {
6491
+ stop();
6492
+ source.pause();
6493
+ if (session) send("disconnect", { reason });
6494
+ session = null;
6495
+ }
6496
+ function message(event) {
6497
+ const data = event.data;
6498
+ if (destroyed || suspended || event.source !== target || event.origin !== origin || !packet(data, channel, assetId)) return;
6499
+ if (data.type === "connect" && typeof data.session === "string" && /^[0-9a-f-]{36}$/i.test(data.session)) {
6500
+ if (session !== data.session) {
6501
+ session = data.session;
6502
+ sequence = 0;
6503
+ commandSequence = 0;
6504
+ lastPublished = null;
6505
+ }
6506
+ sample();
6507
+ return;
6508
+ }
6509
+ if (data.session !== session || !session) return;
6510
+ if (data.type === "disconnect") {
6511
+ disconnect("parent-disconnected");
6512
+ return;
6513
+ }
6514
+ if (data.type !== "command" || !Number.isSafeInteger(data.sequence) || data.sequence <= commandSequence) return;
6515
+ if (!["pause", "seek", "play"].includes(data.command)) return;
6516
+ if (data.command === "seek" && (typeof data.time !== "number" || !Number.isFinite(data.time) || data.time < 0 || data.time > Number(source.duration))) return;
6517
+ commandSequence = data.sequence;
6518
+ const currentSession = session;
6519
+ Promise.resolve().then(() => {
6520
+ if (destroyed || session !== currentSession) return;
6521
+ if (data.command === "pause") source.pause();
6522
+ else if (data.command === "seek") source.currentTime = data.time;
6523
+ else if (options.allowPlayCommands === true) return source.play();
6524
+ else throw new Error("Parent play commands are disabled");
6525
+ }).then(() => {
6526
+ if (!destroyed && session === currentSession) {
6527
+ sample();
6528
+ send("ack", { sequence: data.sequence, ok: true });
6529
+ }
6530
+ }, () => {
6531
+ if (!destroyed && session === currentSession) send("ack", { sequence: data.sequence, ok: false });
6532
+ });
6533
+ }
6534
+ owned.listen(host, "message", message);
6535
+ function requestClose() {
6536
+ if (!destroyed && !suspended && session) send("dismiss", { sequence: ++sequence });
6537
+ }
6538
+ if (options.notifyEscape === true) owned.listen(host.document, "keydown", (event) => {
6539
+ if (event.key === "Escape" && !event.defaultPrevented && !host.document.fullscreenElement) requestClose();
6540
+ });
6541
+ for (const event of EVENTS) if (event !== "snapshot") owned.listen(source, event, () => sample(event));
6542
+ owned.listen(host.document, "visibilitychange", () => {
6543
+ if (host.document.hidden) {
6544
+ stop();
6545
+ if (session) {
6546
+ sample("waiting");
6547
+ stop();
6548
+ }
6549
+ } else sample();
6550
+ });
6551
+ owned.listen(host, "pagehide", () => {
6552
+ suspended = true;
6553
+ disconnect("pagehide");
6554
+ });
6555
+ owned.listen(host, "pageshow", () => {
6556
+ if (!destroyed) {
6557
+ suspended = false;
6558
+ send("available");
6559
+ }
6560
+ });
6561
+ routes.add(key);
6562
+ owners.set(target, routes);
6563
+ publisherOwners.set(host, owners);
6564
+ send("available");
6565
+ return { requestClose, teardown() {
6566
+ if (destroyed) return;
6567
+ disconnect("teardown");
6568
+ destroyed = true;
6569
+ owned.dispose();
6570
+ routes.delete(key);
6571
+ if (!routes.size) owners.delete(target);
6572
+ }, snapshot() {
6573
+ return { kind: "child-publisher", connected: !!session, destroyed, suspended, ...owned.snapshot() };
6574
+ } };
6575
+ }
6576
+ function createPlaybackReceiver(options = {}) {
6577
+ const { host, channel, assetId, origin } = context(options), iframe = options.iframe;
6578
+ if (iframe?.tagName !== "IFRAME" || !iframe.contentWindow) throw new TypeError("An attached iframe is required");
6579
+ if (receiverOwners.has(iframe)) throw new Error("The child frame already has a playback receiver");
6580
+ const staleAfter = options.staleAfterMilliseconds ?? 1500;
6581
+ if (typeof staleAfter !== "number" || !Number.isFinite(staleAfter) || staleAfter < 750 || staleAfter > 1e4) throw new RangeError("staleAfterMilliseconds must be between 750 and 10000");
6582
+ const owned = resources(host), player = new host.EventTarget(), pending = /* @__PURE__ */ new Map();
6583
+ let session = "", sequence = 0, commands = 0, lastWall = 0, timer = 0, connectTimer = 0, destroyed = false, connected = false, status = "connecting";
6584
+ let state = { time: 0, duration: 0, rate: 1, paused: true, ended: false, seeking: false, buffering: false, error: false };
6585
+ let resolveReady, rejectReady, ready, readySettled = false;
6586
+ function prepareReady() {
6587
+ readySettled = false;
6588
+ ready = new Promise((resolve, reject) => {
6589
+ resolveReady = (value) => {
6590
+ readySettled = true;
6591
+ resolve(value);
6592
+ };
6593
+ rejectReady = (error) => {
6594
+ readySettled = true;
6595
+ reject(error);
6596
+ };
6597
+ });
6598
+ ready.catch(() => {
6599
+ });
6600
+ }
6601
+ prepareReady();
6602
+ const emit = (event) => player.dispatchEvent(new host.Event(event));
6603
+ const envelope = (type, extra = {}) => ({ protocol: PROTOCOL, channel, assetId, session, type, ...extra });
6604
+ function send(type, extra) {
6605
+ iframe.contentWindow?.postMessage(envelope(type, extra), origin);
6606
+ }
6607
+ function cancelPending() {
6608
+ for (const request of pending.values()) {
6609
+ owned.cancel(request.timer);
6610
+ request.reject(new Error("Child playback disconnected"));
6611
+ }
6612
+ pending.clear();
6613
+ }
6614
+ function stop(reason) {
6615
+ owned.cancel(timer);
6616
+ owned.cancel(connectTimer);
6617
+ timer = connectTimer = 0;
6618
+ const playing = !player.paused;
6619
+ state = { ...state, time: player.currentTime, paused: true, buffering: false };
6620
+ connected = false;
6621
+ status = reason;
6622
+ cancelPending();
6623
+ if (playing) emit("pause");
6624
+ }
6625
+ function connect() {
6626
+ if (destroyed) return ready;
6627
+ if (readySettled) prepareReady();
6628
+ stop("connecting");
6629
+ session = host.crypto.randomUUID();
6630
+ sequence = 0;
6631
+ send("connect");
6632
+ const started = host.performance.now();
6633
+ const retry = () => {
6634
+ if (destroyed || connected) return;
6635
+ if (host.performance.now() - started >= 1e4) {
6636
+ send("disconnect");
6637
+ stop("unavailable");
6638
+ session = "";
6639
+ rejectReady(new Error("Child playback did not become ready"));
6640
+ return;
6641
+ }
6642
+ send("connect");
6643
+ connectTimer = owned.later(retry, 500);
6644
+ };
6645
+ connectTimer = owned.later(retry, 500);
6646
+ return ready;
6647
+ }
6648
+ function command(name, time) {
6649
+ if (destroyed || !connected) return Promise.reject(new Error("Child playback is unavailable"));
6650
+ if (pending.size >= 16) return Promise.reject(new Error("Too many pending child playback commands"));
6651
+ const id = ++commands;
6652
+ return new Promise((resolve, reject) => {
6653
+ const timeout = owned.later(() => {
6654
+ pending.delete(id);
6655
+ reject(new Error("Child playback command timed out"));
6656
+ }, 2e3);
6657
+ pending.set(id, { resolve, reject, timer: timeout });
6658
+ send("command", { sequence: id, command: name, ...name === "seek" ? { time } : {} });
6659
+ });
6660
+ }
6661
+ Object.defineProperties(player, {
6662
+ nodeType: { value: 1 },
6663
+ tagName: { value: "VIDEO" },
6664
+ ownerDocument: { value: host.document },
6665
+ isConnected: { get: () => !destroyed && iframe.isConnected },
6666
+ 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) => {
6667
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > state.duration) throw new RangeError("Seek must be inside the child duration");
6668
+ command("seek", value).catch(() => {
6669
+ });
6670
+ } },
6671
+ duration: { get: () => state.duration },
6672
+ paused: { get: () => state.paused || state.buffering || state.error || !connected },
6673
+ ended: { get: () => state.ended },
6674
+ seeking: { get: () => state.seeking },
6675
+ playbackRate: { get: () => state.rate },
6676
+ error: { get: () => state.error ? new Error("Child media error") : null }
6677
+ });
6678
+ player.play = () => command("play");
6679
+ player.pause = () => {
6680
+ command("pause").catch(() => {
6681
+ });
6682
+ };
6683
+ function message(event) {
6684
+ const data = event.data;
6685
+ if (destroyed || event.source !== iframe.contentWindow || event.origin !== origin || !packet(data, channel, assetId)) return;
6686
+ if (data.type === "available") {
6687
+ if (!host.document.hidden) {
6688
+ if (connected) send("connect");
6689
+ else connect();
6690
+ }
6691
+ return;
6692
+ }
6693
+ if (data.session !== session) return;
6694
+ if (data.type === "dismiss" && Number.isSafeInteger(data.sequence) && data.sequence > sequence) {
6695
+ sequence = data.sequence;
6696
+ emit("dismiss");
6697
+ return;
6698
+ }
6699
+ if (data.type === "disconnect") {
6700
+ stop("disconnected");
6701
+ session = "";
6702
+ return;
6703
+ }
6704
+ if (data.type === "ack" && Number.isSafeInteger(data.sequence) && typeof data.ok === "boolean") {
6705
+ const request = pending.get(data.sequence);
6706
+ if (request) {
6707
+ owned.cancel(request.timer);
6708
+ pending.delete(data.sequence);
6709
+ data.ok ? request.resolve() : request.reject(new Error("Child playback rejected the command"));
6710
+ }
6711
+ return;
6712
+ }
6713
+ if (data.type !== "state" || !Number.isSafeInteger(data.sequence) || data.sequence <= sequence || !EVENTS.has(data.event) || !validState(data.state)) return;
6714
+ const previous = state, wasConnected = connected, wasPaused = player.paused;
6715
+ sequence = data.sequence;
6716
+ state = { ...data.state };
6717
+ lastWall = host.performance.now();
6718
+ connected = true;
6719
+ status = "connected";
6720
+ owned.cancel(connectTimer);
6721
+ connectTimer = 0;
6722
+ owned.cancel(timer);
6723
+ timer = 0;
6724
+ if (!wasConnected) {
6725
+ emit("loadedmetadata");
6726
+ resolveReady(controller);
6727
+ } else if (previous.duration !== state.duration) emit("durationchange");
6728
+ if (previous.rate !== state.rate) emit("ratechange");
6729
+ if (state.seeking && !previous.seeking) emit("seeking");
6730
+ if (!state.seeking && previous.seeking) emit("seeked");
6731
+ if (!state.seeking && !previous.seeking && wasConnected && Math.abs(state.time - previous.time - (wasPaused ? 0 : (lastWall - previousWall) / 1e3 * previous.rate)) > 0.4) {
6732
+ emit("seeking");
6733
+ emit("seeked");
6734
+ }
6735
+ previousWall = lastWall;
6736
+ if (wasPaused && !player.paused) {
6737
+ emit("play");
6738
+ emit("playing");
6739
+ }
6740
+ if (!wasPaused && player.paused) emit("pause");
6741
+ if (state.ended && !previous.ended) emit("ended");
6742
+ if (data.event === "waiting") emit("waiting");
6743
+ if (state.error && !previous.error) emit("error");
6744
+ emit("timeupdate");
6745
+ if (!player.paused) timer = owned.later(() => {
6746
+ send("disconnect");
6747
+ stop("stale");
6748
+ session = "";
6749
+ emit("waiting");
6750
+ }, staleAfter);
6751
+ }
6752
+ let previousWall = 0;
6753
+ owned.listen(host, "message", message);
6754
+ owned.listen(iframe, "load", connect);
6755
+ owned.listen(host, "pagehide", () => {
6756
+ send("disconnect");
6757
+ stop("suspended");
6758
+ session = "";
6759
+ });
6760
+ owned.listen(host, "pageshow", connect);
6761
+ owned.listen(host.document, "visibilitychange", () => {
6762
+ if (host.document.hidden) {
6763
+ send("command", { sequence: ++commands, command: "pause" });
6764
+ stop("hidden");
6765
+ session = "";
6766
+ } else connect();
6767
+ });
6768
+ const observer = new host.MutationObserver(() => {
6769
+ if (!iframe.isConnected) controller.teardown();
6770
+ });
6771
+ observer.observe(host.document.documentElement, { childList: true, subtree: true });
6772
+ const controller = { player, get ready() {
6773
+ return ready;
6774
+ }, reconnect: connect, teardown() {
6775
+ if (destroyed) return;
6776
+ send("command", { sequence: ++commands, command: "pause" });
6777
+ send("disconnect");
6778
+ stop("destroyed");
6779
+ destroyed = true;
6780
+ session = "";
6781
+ rejectReady(new Error("Child playback owner removed"));
6782
+ observer.disconnect();
6783
+ owned.dispose();
6784
+ receiverOwners.delete(iframe);
6785
+ }, snapshot() {
6786
+ return { kind: "child-receiver", status, connected, destroyed, time: player.currentTime, paused: player.paused, assetId, ...owned.snapshot() };
6787
+ } };
6788
+ receiverOwners.set(iframe, controller);
6789
+ connect();
6790
+ return controller;
6791
+ }
6792
+ function installChildPlayback(scope, api, host) {
6793
+ function own(controller) {
6794
+ const teardown = controller.teardown.bind(controller);
6795
+ const remove = scope.onDispose(() => teardown());
6796
+ controller.teardown = () => {
6797
+ remove();
6798
+ teardown();
6799
+ };
6800
+ return controller;
6801
+ }
6802
+ api.publishPlayback = (options) => {
6803
+ if (api.destroyed) throw new Error("DanceMoves owner is destroyed");
6804
+ return own(createPlaybackPublisher({ ...options, window: host }));
6805
+ };
6806
+ api.receiveChildPlayback = (options) => {
6807
+ if (api.destroyed) throw new Error("DanceMoves owner is destroyed");
6808
+ if (typeof api.registerPlaybackProvider !== "function") throw new Error("The shared playback provider API is required");
6809
+ if (options?.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6810
+ const receiver = createPlaybackReceiver({ ...options, window: host });
6811
+ let registration;
6812
+ try {
6813
+ registration = api.registerPlaybackProvider({ ...options, player: receiver.player });
6814
+ } catch (error) {
6815
+ receiver.teardown();
6816
+ throw error;
6817
+ }
6818
+ return own({ source: registration.source, player: receiver.player, get ready() {
6819
+ return receiver.ready;
6820
+ }, mapping: registration.mapping, reconnect: receiver.reconnect, teardown() {
6821
+ receiver.teardown();
6822
+ registration.teardown();
6823
+ }, snapshot() {
6824
+ return { ...receiver.snapshot(), mapping: registration.mapping };
6825
+ } });
6826
+ };
6827
+ }
6828
+
5921
6829
  // .build/modules/version.mjs
5922
- var VERSION = "3.1.19";
6830
+ var VERSION = "3.1.21";
5923
6831
 
5924
6832
  // .build/modules/index.mjs
5925
6833
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -5954,6 +6862,7 @@ function createDanceMoves(options = {}) {
5954
6862
  }
5955
6863
  try {
5956
6864
  runtime_default.core(scope);
6865
+ installPlayback(scope, w.DanceMoves, root.ownerDocument.defaultView);
5957
6866
  runtime_default.effects(scope);
5958
6867
  runtime_default.orientationCore(scope);
5959
6868
  runtime_default.native(scope);
@@ -5986,6 +6895,7 @@ function createDanceMoves(options = {}) {
5986
6895
  }
5987
6896
  });
5988
6897
  Object.defineProperty(api, "destroyed", { get: () => scope.disposed });
6898
+ installChildPlayback(scope, api, ownerDocument.defaultView);
5989
6899
  api.ready = Promise.resolve(api.ready).then(() => {
5990
6900
  if (!scope.disposed) scope.document.dispatchEvent(new w.CustomEvent("dance-moves-ready", {
5991
6901
  bubbles: true,
@@ -6008,6 +6918,8 @@ export {
6008
6918
  VERSION,
6009
6919
  createDanceMoves,
6010
6920
  createMemoryPageStore,
6921
+ createPlaybackPublisher,
6922
+ createPlaybackReceiver,
6011
6923
  fromWordPressMeta,
6012
6924
  validatePageConfig
6013
6925
  };