@kieransimkin/dancemoves 3.1.18 → 3.1.20

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
@@ -48,6 +48,7 @@ function install(__dmScope) {
48
48
  var lyricList = [];
49
49
  var lyricRenderer = null;
50
50
  var boundAudio = /* @__PURE__ */ new WeakSet();
51
+ var playbackBindings = /* @__PURE__ */ new Map();
51
52
  var activeAudio = null;
52
53
  var pageClockStarted = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
53
54
  var referenceDurationSeconds = positiveNumber(rawConfig.masterDurationMilliseconds) / 1e3;
@@ -137,13 +138,16 @@ function install(__dmScope) {
137
138
  }
138
139
  function currentTick(options) {
139
140
  var settings = options || {};
140
- var audio = settings.audio || (settings.clock === "audio" ? activeAudio : null);
141
+ var audio = playbackAudio(settings.audio) || (settings.clock === "audio" ? activeAudio : null);
141
142
  if (audio && !audio.paused && !audio.ended && Number.isFinite(Number(audio.currentTime))) {
142
143
  return Number(audio.currentTime) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 60;
143
144
  }
144
145
  var now = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
145
146
  return Math.max(0, now - pageClockStarted) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 6e4;
146
147
  }
148
+ function playbackAudio(audio) {
149
+ return audio && api && api.playbackSource ? api.playbackSource(audio) : audio;
150
+ }
147
151
  function nextIntervalTick(intervalTicks, fromTick, strictlyFuture) {
148
152
  var interval = Math.max(1, roundHalfUp(intervalTicks));
149
153
  var tick = Math.max(0, Number(fromTick) || 0);
@@ -200,7 +204,7 @@ function install(__dmScope) {
200
204
  var pollAudio = function() {
201
205
  if (cancelled)
202
206
  return;
203
- var candidate = settings.audio || activeAudio;
207
+ var candidate = playbackAudio(settings.audio) || activeAudio;
204
208
  if (!candidate || candidate.paused || candidate.ended) {
205
209
  if (sourceAudio && sourceAudio.ended) {
206
210
  cancel();
@@ -762,9 +766,18 @@ function install(__dmScope) {
762
766
  return Math.abs(duration - referenceDurationSeconds) <= Math.max(0.25, duration / 1e5);
763
767
  }
764
768
  function bindAudio(audio) {
769
+ if (api.playbackSource)
770
+ audio = api.playbackSource(audio);
765
771
  if (boundAudio.has(audio) || !matchesMasterDuration(audio))
766
772
  return;
767
773
  boundAudio.add(audio);
774
+ var removers = [];
775
+ function listen(target, name, handler) {
776
+ __dmScope.listen(target, name, handler);
777
+ removers.push(function() {
778
+ __dmScope.unlisten(target, name, handler);
779
+ });
780
+ }
768
781
  __dmScope.dataset(audio, "danceMovesTiming", "master-length");
769
782
  var state = {
770
783
  next: firstCueAfter(audio.currentTime || 0),
@@ -772,7 +785,8 @@ function install(__dmScope) {
772
785
  last: audio.currentTime || 0,
773
786
  seeking: false,
774
787
  frame: 0,
775
- pendingCue: cueAtLanding(audio.currentTime || 0)
788
+ pendingCue: cueAtLanding(audio.currentTime || 0),
789
+ endedPending: false
776
790
  };
777
791
  function stopFrame() {
778
792
  if (state.frame)
@@ -781,7 +795,7 @@ function install(__dmScope) {
781
795
  }
782
796
  function tick() {
783
797
  state.frame = 0;
784
- if (audio.paused || audio.ended || state.seeking)
798
+ if (audio.paused || audio.ended || state.seeking || document2.hidden)
785
799
  return;
786
800
  var diagnosticStartedAt = diagnosticsSink ? diagnosticNow() : 0;
787
801
  var firedCount = 0;
@@ -819,8 +833,14 @@ function install(__dmScope) {
819
833
  }
820
834
  function start() {
821
835
  stopFrame();
836
+ if (document2.hidden || audio.paused || audio.ended)
837
+ return;
822
838
  activeAudio = audio;
823
839
  state.last = positiveNumber(audio.currentTime);
840
+ if (state.endedPending) {
841
+ state.pendingCue = cueAtLanding(state.last);
842
+ state.endedPending = false;
843
+ }
824
844
  state.next = firstCueAfter(state.last);
825
845
  state.lyric = -2;
826
846
  if (state.pendingCue) {
@@ -830,47 +850,70 @@ function install(__dmScope) {
830
850
  setOwnedPlaybackRate(audio.playbackRate);
831
851
  state.frame = window2.requestAnimationFrame(tick);
832
852
  }
833
- __dmScope.listen(audio, "play", start);
834
- __dmScope.listen(audio, "playing", start);
835
- __dmScope.listen(audio, "pause", function() {
853
+ listen(audio, "play", start);
854
+ listen(audio, "playing", start);
855
+ listen(audio, "pause", function() {
836
856
  stopFrame();
837
857
  if (activeAudio === audio)
838
858
  activeAudio = null;
839
859
  if (lyricRenderer)
840
860
  lyricRenderer.hide();
841
861
  });
842
- __dmScope.listen(audio, "ended", function() {
862
+ listen(audio, "ended", function() {
843
863
  stopFrame();
844
864
  if (activeAudio === audio)
845
865
  activeAudio = null;
846
866
  state.last = 0;
847
867
  state.next = 0;
848
868
  state.lyric = -2;
849
- state.pendingCue = cueAtLanding(0);
869
+ state.pendingCue = null;
870
+ state.endedPending = true;
850
871
  if (lyricRenderer)
851
872
  lyricRenderer.hide();
852
873
  });
853
- __dmScope.listen(audio, "seeking", function() {
874
+ listen(audio, "seeking", function() {
854
875
  state.seeking = true;
855
876
  state.pendingCue = null;
856
877
  stopFrame();
857
878
  });
858
- __dmScope.listen(audio, "seeked", function() {
879
+ listen(audio, "seeked", function() {
859
880
  state.seeking = false;
860
881
  state.last = positiveNumber(audio.currentTime);
861
882
  state.next = firstCueAfter(state.last);
862
883
  state.lyric = -2;
863
884
  state.pendingCue = cueAtLanding(state.last);
885
+ playbackBindings.set(audio, function() {
886
+ stopFrame();
887
+ removers.splice(0).forEach(function(remove) {
888
+ remove();
889
+ });
890
+ boundAudio.delete(audio);
891
+ if (activeAudio === audio)
892
+ activeAudio = null;
893
+ playbackBindings.delete(audio);
894
+ });
864
895
  if (!audio.paused && !audio.ended)
865
896
  start();
866
897
  });
867
- __dmScope.listen(audio, "ratechange", function() {
898
+ listen(audio, "ratechange", function() {
868
899
  setOwnedPlaybackRate(audio.playbackRate);
869
900
  });
870
- __dmScope.listen(audio, "timeupdate", function() {
901
+ listen(audio, "timeupdate", function() {
871
902
  if (!audio.paused && !state.frame && !state.seeking)
872
903
  tick();
873
904
  });
905
+ listen(document2, "visibilitychange", function() {
906
+ if (document2.hidden) {
907
+ stopFrame();
908
+ if (lyricRenderer)
909
+ lyricRenderer.hide();
910
+ } else if (!audio.paused && !audio.ended && !state.seeking) {
911
+ state.pendingCue = null;
912
+ start();
913
+ }
914
+ });
915
+ if (!audio.paused && !audio.ended)
916
+ start();
874
917
  }
875
918
  function discoverAudio() {
876
919
  var audioElements = Array.from(document2.querySelectorAll("audio"));
@@ -984,6 +1027,16 @@ function install(__dmScope) {
984
1027
  registerAnimationScope,
985
1028
  resetRunningAnimations,
986
1029
  discoverAudio,
1030
+ unregisterPlaybackSource: function(source) {
1031
+ var remove = playbackBindings.get(source);
1032
+ if (remove)
1033
+ remove();
1034
+ },
1035
+ registerPlaybackSource: function(source) {
1036
+ if (!referenceDurationSeconds)
1037
+ referenceDurationSeconds = positiveNumber(source.duration);
1038
+ bindAudio(source);
1039
+ },
987
1040
  setDiagnosticsSink,
988
1041
  diagnosticsEnabled: function() {
989
1042
  return diagnosticsAllowed && Boolean(diagnosticsSink);
@@ -1024,6 +1077,10 @@ function install2(__dmScope) {
1024
1077
  return document2.querySelector(value);
1025
1078
  return fallback || null;
1026
1079
  }
1080
+ function playbackElement(value, fallback) {
1081
+ var target = element(value, fallback);
1082
+ return target && motion.playbackSource ? motion.playbackSource(target) : target;
1083
+ }
1027
1084
  function media(query) {
1028
1085
  return window2.matchMedia(query);
1029
1086
  }
@@ -1149,7 +1206,7 @@ function install2(__dmScope) {
1149
1206
  function playbackPulse(options) {
1150
1207
  options = options || {};
1151
1208
  var state = commonState(options, "pulse");
1152
- var audio = element(options.audio, state.root.querySelector("audio"));
1209
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1153
1210
  if (!audio)
1154
1211
  throw new Error("DanceMovesEffects pulse requires an audio element");
1155
1212
  var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
@@ -1249,7 +1306,7 @@ function install2(__dmScope) {
1249
1306
  function cueTimeline(options) {
1250
1307
  options = options || {};
1251
1308
  var state = commonState(options, "cue-timeline");
1252
- var audio = element(options.audio, state.root.querySelector("audio"));
1309
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1253
1310
  if (!audio)
1254
1311
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1255
1312
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
@@ -1410,7 +1467,7 @@ function install2(__dmScope) {
1410
1467
  function spritePlayback(options) {
1411
1468
  options = options || {};
1412
1469
  var state = commonState(options, "sprite-playback");
1413
- var audio = element(options.audio, state.root.querySelector("audio"));
1470
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1414
1471
  var target = element(options.target, state.root);
1415
1472
  if (!audio)
1416
1473
  throw new Error("DanceMovesEffects spritePlayback requires an audio element");
@@ -1604,7 +1661,7 @@ function install2(__dmScope) {
1604
1661
  function lyricStage(options) {
1605
1662
  options = options || {};
1606
1663
  var state = commonState(options, "lyric-stage");
1607
- var audio = element(options.audio, state.root.querySelector("audio"));
1664
+ var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1608
1665
  var popover = element(options.popover, document2.querySelector(".dance-moves-lyric-popover"));
1609
1666
  if (!audio)
1610
1667
  throw new Error("DanceMovesEffects lyricStage requires an audio element");
@@ -4823,9 +4880,11 @@ function install9(__dmScope) {
4823
4880
  const clock = options.clock === void 0 ? "page" : options.clock;
4824
4881
  if (!["page", "audio", "auto"].includes(clock))
4825
4882
  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");
4883
+ let audio = clock === "page" ? null : element(options.audio, "audio");
4884
+ if (audio && motion.playbackSource)
4885
+ audio = motion.playbackSource(audio);
4886
+ if (audio && !["audio", "video"].includes(String(audio.tagName).toLowerCase()))
4887
+ throw new TypeError("audio must resolve to native media or a shared playback source");
4829
4888
  const rate = options.rate === void 0 ? 1 : number(options.rate, "rate");
4830
4889
  if (rate <= 0 || rate > 64)
4831
4890
  throw new RangeError("rate must be in (0, 64]");
@@ -5073,7 +5132,7 @@ function install9(__dmScope) {
5073
5132
  }
5074
5133
  if (options.resetOnCue)
5075
5134
  disposers.push(motion.onCue(typeof options.resetOnCue === "string" ? options.resetOnCue : "*", (detail) => {
5076
- if (!detail.audio || !audio || detail.audio === audio)
5135
+ if (!detail.audio || !audio || detail.audio === audio || motion.playbackSource && motion.playbackSource(detail.audio) === motion.playbackSource(audio))
5077
5136
  reset();
5078
5137
  }, { id: id + ":cue-reset" }));
5079
5138
  if (options.offscreen !== false && window2.IntersectionObserver) {
@@ -5347,6 +5406,8 @@ function createScope(root, config, { legacyGlobals = false, nonce, orientation =
5347
5406
  }
5348
5407
  const api = {
5349
5408
  id,
5409
+ root,
5410
+ legacyGlobals,
5350
5411
  window,
5351
5412
  document,
5352
5413
  get disposed() {
@@ -5949,8 +6010,428 @@ function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples =
5949
6010
  });
5950
6011
  }
5951
6012
 
6013
+ // .build/modules/playback.mjs
6014
+ var youtubeLoaders = /* @__PURE__ */ new WeakMap();
6015
+ function validatePlaybackMapping(input, masterDuration) {
6016
+ const offset = input.offset ?? 0, duration = input.duration ?? masterDuration, playerStart = input.playerStart ?? 0;
6017
+ for (const [key, value] of Object.entries({ offset, duration, playerStart, masterDuration })) {
6018
+ 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`);
6019
+ }
6020
+ if (offset + duration > masterDuration + 1e-3) throw new RangeError("The clip extends beyond its associated master");
6021
+ return Object.freeze({ offset, duration, playerStart, masterDuration });
6022
+ }
6023
+ function loadYouTubeAPI(host) {
6024
+ if (host.YT?.Player) return Promise.resolve(host.YT);
6025
+ if (youtubeLoaders.has(host)) return youtubeLoaders.get(host);
6026
+ const pending = new Promise((resolve, reject) => {
6027
+ const previous = host.onYouTubeIframeAPIReady;
6028
+ let timeout;
6029
+ const script = host.document.createElement("script");
6030
+ function finish(error) {
6031
+ host.clearTimeout(timeout);
6032
+ if (host.onYouTubeIframeAPIReady === ready) host.onYouTubeIframeAPIReady = previous;
6033
+ script.removeEventListener("error", failed);
6034
+ if (error) {
6035
+ script.remove();
6036
+ youtubeLoaders.delete(host);
6037
+ reject(error);
6038
+ } else resolve(host.YT);
6039
+ }
6040
+ function ready() {
6041
+ try {
6042
+ previous?.();
6043
+ } finally {
6044
+ finish(host.YT?.Player ? null : new Error("YouTube API unavailable"));
6045
+ }
6046
+ }
6047
+ function failed() {
6048
+ finish(new Error("YouTube API failed to load"));
6049
+ }
6050
+ host.onYouTubeIframeAPIReady = ready;
6051
+ script.src = "https://www.youtube.com/iframe_api";
6052
+ script.addEventListener("error", failed, { once: true });
6053
+ timeout = host.setTimeout(() => finish(new Error("YouTube API timed out")), 2e4);
6054
+ host.document.head.appendChild(script);
6055
+ });
6056
+ youtubeLoaders.set(host, pending);
6057
+ return pending;
6058
+ }
6059
+ function installPlayback(scope, api, host = scope.document.defaultView) {
6060
+ const groups = /* @__PURE__ */ new Map(), transports = /* @__PURE__ */ new WeakSet(), aliases = /* @__PURE__ */ new WeakMap(), controllers = /* @__PURE__ */ new Set(), registered = /* @__PURE__ */ new WeakMap();
6061
+ let disposed = false;
6062
+ function emit(target, type) {
6063
+ target.dispatchEvent(new host.Event(type));
6064
+ }
6065
+ function groupFor(reference) {
6066
+ if (disposed) throw new Error("DanceMoves playback owner is destroyed");
6067
+ const master = reference?.element || reference, declared = reference?.element ? reference.duration : void 0;
6068
+ const isNative = !!master?.addEventListener && ["AUDIO", "VIDEO"].includes(master.tagName);
6069
+ 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");
6070
+ if (isNative && (master.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(master))) throw new TypeError("The master must belong to this runtime root");
6071
+ if (groups.has(master)) {
6072
+ const group2 = groups.get(master);
6073
+ group2.setDuration(declared);
6074
+ return group2;
6075
+ }
6076
+ const transport = new host.EventTarget();
6077
+ transports.add(transport);
6078
+ let active = null, lastPaused = true, delegate = null;
6079
+ let declaredDuration = declared || 0;
6080
+ function setDuration(value) {
6081
+ if (value === void 0) return;
6082
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new RangeError("Declared master duration must be positive and finite");
6083
+ if (declaredDuration && Math.abs(declaredDuration - value) > 1e-3) throw new RangeError("Conflicting master durations");
6084
+ 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");
6085
+ declaredDuration = value;
6086
+ }
6087
+ setDuration(declared);
6088
+ const duration = () => delegate?.duration || declaredDuration || (Number(master.duration) > 0 && Number.isFinite(Number(master.duration)) ? Number(master.duration) : Number(api.config?.masterDurationMilliseconds || 0) / 1e3);
6089
+ const target = isNative ? master : scope.root;
6090
+ 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) => {
6091
+ if (isNative) master.currentTime = time;
6092
+ }, mapping: null };
6093
+ active = native;
6094
+ function read() {
6095
+ if (delegate) return { time: delegate.currentTime, paused: delegate.paused, ended: delegate.ended, rate: delegate.playbackRate, seeking: delegate.seeking, error: delegate.error };
6096
+ const raw = active.read(), map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6097
+ const local = raw.time - map.playerStart, outside = local < -1e-3 || local > map.duration + 1e-3;
6098
+ 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 };
6099
+ }
6100
+ Object.defineProperties(transport, {
6101
+ nodeType: { value: 1 },
6102
+ tagName: { get: () => isNative ? master.tagName : "VIDEO" },
6103
+ ownerDocument: { get: () => target.ownerDocument },
6104
+ dataset: { get: () => target.dataset },
6105
+ id: { get: () => master.id },
6106
+ isConnected: { get: () => target.isConnected },
6107
+ currentTime: { get: () => read().time, set: (value) => {
6108
+ if (delegate) {
6109
+ delegate.currentTime = value;
6110
+ return;
6111
+ }
6112
+ const map = active.mapping || { offset: 0, playerStart: 0, duration: duration() };
6113
+ active.seek(map.playerStart + Math.max(0, Math.min(map.duration, Number(value) - map.offset)));
6114
+ } },
6115
+ duration: { get: duration },
6116
+ paused: { get: () => read().paused },
6117
+ ended: { get: () => read().ended },
6118
+ seeking: { get: () => read().seeking || false },
6119
+ playbackRate: { get: () => read().rate },
6120
+ error: { get: () => read().error || null },
6121
+ element: { value: isNative ? master : null },
6122
+ activePlayer: { get: () => delegate ? delegate.activePlayer : active.element },
6123
+ masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id }
6124
+ });
6125
+ transport.matches = (selector) => target.matches(selector);
6126
+ transport.getAttribute = (name) => target.getAttribute(name);
6127
+ transport.setAttribute = (name, value) => target.setAttribute(name, value);
6128
+ transport.removeAttribute = (name) => target.removeAttribute(name);
6129
+ transport.play = () => delegate ? delegate.play() : active.play();
6130
+ transport.pause = () => delegate ? delegate.pause() : active.pause();
6131
+ function activate(source) {
6132
+ if (active !== source) {
6133
+ const previous = active;
6134
+ active = source;
6135
+ previous.pause();
6136
+ emit(transport, "seeking");
6137
+ emit(transport, "seeked");
6138
+ }
6139
+ }
6140
+ function forward(source, type) {
6141
+ if (type === "play" || type === "playing") activate(source);
6142
+ if (active !== source) return;
6143
+ const paused = read().paused;
6144
+ if (type === "timeupdate" && paused !== lastPaused) emit(transport, paused ? "pause" : "playing");
6145
+ lastPaused = paused;
6146
+ emit(transport, type);
6147
+ }
6148
+ const events = ["loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"];
6149
+ const removers = [];
6150
+ function bindNative(element, mapping) {
6151
+ 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) => {
6152
+ element.currentTime = time;
6153
+ } };
6154
+ source.removers = [];
6155
+ for (const type of events) {
6156
+ const listener = () => forward(source, type);
6157
+ scope.listen(element, type, listener);
6158
+ const remove = () => scope.unlisten(element, type, listener);
6159
+ removers.push(remove);
6160
+ source.removers.push(remove);
6161
+ }
6162
+ return source;
6163
+ }
6164
+ if (isNative) bindNative(master, null);
6165
+ const group = {
6166
+ transport,
6167
+ duration,
6168
+ setDuration,
6169
+ activate,
6170
+ forward,
6171
+ bindNative,
6172
+ isActive: (source) => active === source,
6173
+ aliasTo(target2) {
6174
+ removers.splice(0).forEach((remove) => remove());
6175
+ delegate = target2;
6176
+ aliases.set(transport, target2);
6177
+ for (const type of events) {
6178
+ const relay = () => emit(transport, type);
6179
+ scope.listen(target2, type, relay);
6180
+ removers.push(() => scope.unlisten(target2, type, relay));
6181
+ }
6182
+ },
6183
+ reconcile(source) {
6184
+ if (active !== source) return;
6185
+ const paused = read().paused;
6186
+ if (paused !== lastPaused) {
6187
+ lastPaused = paused;
6188
+ emit(transport, paused ? "pause" : "playing");
6189
+ }
6190
+ emit(transport, "timeupdate");
6191
+ },
6192
+ remove(source) {
6193
+ if (active === source) {
6194
+ source.pause();
6195
+ active = native;
6196
+ lastPaused = true;
6197
+ emit(transport, "pause");
6198
+ emit(transport, "seeking");
6199
+ emit(transport, "seeked");
6200
+ }
6201
+ },
6202
+ destroy() {
6203
+ removers.splice(0).forEach((remove) => remove());
6204
+ emit(transport, "pause");
6205
+ }
6206
+ };
6207
+ groups.set(master, group);
6208
+ return group;
6209
+ }
6210
+ function registerPlayer(options) {
6211
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6212
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6213
+ const element = options.player;
6214
+ if (!element?.addEventListener || !["AUDIO", "VIDEO"].includes(element.tagName)) throw new TypeError("player must be native audio or video");
6215
+ if (element.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(element)) throw new TypeError("The player must belong to this runtime root");
6216
+ if (registered.has(element)) throw new Error("Player already has a playback owner");
6217
+ if (element === options.master || element === group.transport.element) throw new TypeError("The master is already registered");
6218
+ const previousGroup = groups.get(element);
6219
+ if (previousGroup && previousGroup !== group) {
6220
+ if ([...controllers].some((owner) => owner.source === previousGroup.transport)) throw new Error("This player already owns a different master group");
6221
+ api.unregisterPlaybackSource?.(previousGroup.transport);
6222
+ previousGroup.aliasTo(group.transport);
6223
+ for (const [key, value] of groups) if (value === previousGroup) groups.set(key, group);
6224
+ }
6225
+ const source = group.bindNative(element, mapping);
6226
+ let destroyed = false;
6227
+ const controller = { source: group.transport, mapping, teardown() {
6228
+ if (destroyed) return;
6229
+ destroyed = true;
6230
+ source.removers.splice(0).forEach((remove) => remove());
6231
+ group.remove(source);
6232
+ registered.delete(element);
6233
+ controllers.delete(controller);
6234
+ }, snapshot() {
6235
+ return { kind: "native", mapping, active: group.isActive(source), destroyed };
6236
+ } };
6237
+ registered.set(element, controller);
6238
+ if (!element.paused && !element.ended) group.forward(source, "playing");
6239
+ Promise.resolve(api.ready).then(() => {
6240
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6241
+ });
6242
+ controllers.add(controller);
6243
+ return controller;
6244
+ }
6245
+ function registerYouTube(options) {
6246
+ if (!options || options.masterVerified !== true) throw new TypeError("An explicitly verified master association is required");
6247
+ const group = groupFor(options.master), mapping = validatePlaybackMapping(options, group.duration());
6248
+ const iframe = options.iframe;
6249
+ if (iframe?.tagName !== "IFRAME") throw new TypeError("A YouTube iframe is required");
6250
+ if (iframe.ownerDocument !== host.document || !scope.legacyGlobals && !scope.root.contains(iframe)) throw new TypeError("The iframe must belong to this runtime root");
6251
+ if (registered.has(iframe)) throw new Error("Player already has a playback owner");
6252
+ const url2 = new URL(iframe.src, host.location.href);
6253
+ if (!/^https:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[A-Za-z0-9_-]{11}(\?|$)/.test(url2.href)) throw new TypeError("Unsupported YouTube embed");
6254
+ url2.searchParams.set("enablejsapi", "1");
6255
+ url2.searchParams.set("origin", host.location.origin);
6256
+ const expectedVideoId = url2.pathname.split("/").at(-1);
6257
+ if (iframe.src !== url2.href) iframe.src = url2.href;
6258
+ let player = null, state = -1, error = null, frame = 0, destroyed = false, lastTime = null, lastWall = null, lastRate = 1, readyTimer = 0, rejectReady = null;
6259
+ const source = {
6260
+ element: iframe,
6261
+ mapping,
6262
+ read: () => ({ time: player?.getCurrentTime?.() || 0, paused: state !== 1, ended: state === 0, rate: player?.getPlaybackRate?.() || 1, error }),
6263
+ pause: () => player?.pauseVideo?.(),
6264
+ play: () => player?.playVideo?.(),
6265
+ seek: (time) => player?.seekTo?.(time, true)
6266
+ };
6267
+ function stop() {
6268
+ if (frame) scope.window.cancelAnimationFrame(frame);
6269
+ frame = 0;
6270
+ lastTime = null;
6271
+ lastWall = null;
6272
+ }
6273
+ function sample(wall) {
6274
+ frame = 0;
6275
+ if (destroyed || state !== 1 || scope.document.hidden || !group.isActive(source)) return;
6276
+ const raw = source.read();
6277
+ if (lastTime !== null && Math.abs(raw.time - lastTime - (wall - lastWall) / 1e3 * raw.rate) > 0.4) {
6278
+ group.forward(source, "seeking");
6279
+ group.forward(source, "seeked");
6280
+ }
6281
+ if (raw.rate !== lastRate) {
6282
+ lastRate = raw.rate;
6283
+ group.forward(source, "ratechange");
6284
+ }
6285
+ lastTime = raw.time;
6286
+ lastWall = wall;
6287
+ group.reconcile(source);
6288
+ frame = scope.window.requestAnimationFrame(sample);
6289
+ }
6290
+ function resume() {
6291
+ stop();
6292
+ if (!destroyed && state === 1 && !scope.document.hidden && group.isActive(source)) frame = scope.window.requestAnimationFrame(sample);
6293
+ }
6294
+ const visibility = () => {
6295
+ if (scope.document.hidden) stop();
6296
+ else {
6297
+ group.forward(source, "seeking");
6298
+ group.forward(source, "seeked");
6299
+ resume();
6300
+ }
6301
+ };
6302
+ const pageHide = () => stop(), pageShow = () => {
6303
+ if (player?.getPlayerState) state = player.getPlayerState();
6304
+ visibility();
6305
+ };
6306
+ scope.listen(scope.document, "visibilitychange", visibility);
6307
+ scope.listen(host, "pagehide", pageHide);
6308
+ scope.listen(host, "pageshow", pageShow);
6309
+ const controller = {
6310
+ source: group.transport,
6311
+ mapping,
6312
+ ready: null,
6313
+ teardown() {
6314
+ if (destroyed) return;
6315
+ destroyed = true;
6316
+ stop();
6317
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6318
+ readyTimer = 0;
6319
+ rejectReady?.(new Error("YouTube playback owner removed"));
6320
+ rejectReady = null;
6321
+ group.remove(source);
6322
+ scope.unlisten(scope.document, "visibilitychange", visibility);
6323
+ scope.unlisten(host, "pagehide", pageHide);
6324
+ scope.unlisten(host, "pageshow", pageShow);
6325
+ player?.destroy?.();
6326
+ registered.delete(iframe);
6327
+ controllers.delete(controller);
6328
+ },
6329
+ snapshot() {
6330
+ return { kind: "youtube", mapping, state, error, active: group.isActive(source), destroyed, polling: !!frame, time: group.transport.currentTime };
6331
+ }
6332
+ };
6333
+ controllers.add(controller);
6334
+ registered.set(iframe, controller);
6335
+ Promise.resolve(api.ready).then(() => {
6336
+ if (!destroyed && !disposed) api.registerPlaybackSource?.(group.transport);
6337
+ });
6338
+ controller.ready = loadYouTubeAPI(host).then((YT) => {
6339
+ if (destroyed || disposed) return controller;
6340
+ return new Promise((resolve, reject) => {
6341
+ rejectReady = reject;
6342
+ readyTimer = scope.window.setTimeout(() => {
6343
+ readyTimer = 0;
6344
+ error = new Error("YouTube player readiness timed out");
6345
+ rejectReady = null;
6346
+ reject(error);
6347
+ }, 15e3);
6348
+ player = new YT.Player(iframe, { events: {
6349
+ onReady() {
6350
+ if (destroyed) return;
6351
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6352
+ readyTimer = 0;
6353
+ rejectReady = null;
6354
+ const actual = Number(player.getDuration?.());
6355
+ if (actual > 0 && actual + 0.25 < mapping.playerStart + mapping.duration) {
6356
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6357
+ reject(error);
6358
+ return;
6359
+ }
6360
+ group.forward(source, "loadedmetadata");
6361
+ const initialState = player.getPlayerState?.();
6362
+ if (initialState === 1) controllerState({ data: initialState });
6363
+ resolve(controller);
6364
+ },
6365
+ onStateChange: controllerState,
6366
+ onPlaybackRateChange() {
6367
+ if (!destroyed) group.forward(source, "ratechange");
6368
+ },
6369
+ onError(event) {
6370
+ if (destroyed) return;
6371
+ error = new Error("YouTube playback error " + event.data);
6372
+ state = -1;
6373
+ stop();
6374
+ if (readyTimer) scope.window.clearTimeout(readyTimer);
6375
+ readyTimer = 0;
6376
+ rejectReady = null;
6377
+ group.forward(source, "pause");
6378
+ group.forward(source, "error");
6379
+ reject(error);
6380
+ }
6381
+ } });
6382
+ function controllerState(event) {
6383
+ if (destroyed) return;
6384
+ state = event.data;
6385
+ stop();
6386
+ if (state === 1) {
6387
+ let actualId;
6388
+ try {
6389
+ const actualUrl = new URL(player.getVideoUrl());
6390
+ actualId = actualUrl.searchParams.get("v") || actualUrl.pathname.match(/^\/(?:shorts|embed)\/([A-Za-z0-9_-]{11})\/?$/)?.[1];
6391
+ } catch {
6392
+ }
6393
+ if (actualId !== expectedVideoId) {
6394
+ error = new Error("YouTube switched away from the verified soundtrack asset");
6395
+ state = -1;
6396
+ group.forward(source, "pause");
6397
+ group.forward(source, "error");
6398
+ return;
6399
+ }
6400
+ const actualDuration = Number(player.getDuration?.());
6401
+ if (actualDuration > 0 && actualDuration + 0.25 < mapping.playerStart + mapping.duration) {
6402
+ error = new Error("The mapped soundtrack window exceeds the video duration");
6403
+ state = -1;
6404
+ group.forward(source, "pause");
6405
+ group.forward(source, "error");
6406
+ return;
6407
+ }
6408
+ group.forward(source, "play");
6409
+ group.forward(source, "playing");
6410
+ resume();
6411
+ } else group.forward(source, state === 0 ? "ended" : state === 3 ? "waiting" : "pause");
6412
+ if (state === 3) group.forward(source, "pause");
6413
+ }
6414
+ });
6415
+ });
6416
+ controller.ready.catch(() => {
6417
+ });
6418
+ return controller;
6419
+ }
6420
+ Object.assign(api, { playbackSource: (master) => {
6421
+ let source = transports.has(master) ? master : groupFor(master).transport;
6422
+ while (aliases.has(source)) source = aliases.get(source);
6423
+ return source;
6424
+ }, registerPlayer, registerYouTube });
6425
+ scope.onDispose(() => {
6426
+ disposed = true;
6427
+ for (const controller of [...controllers]) controller.teardown();
6428
+ for (const group of new Set(groups.values())) group.destroy();
6429
+ groups.clear();
6430
+ });
6431
+ }
6432
+
5952
6433
  // .build/modules/version.mjs
5953
- var VERSION = "3.1.18";
6434
+ var VERSION = "3.1.20";
5954
6435
 
5955
6436
  // .build/modules/index.mjs
5956
6437
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -5985,6 +6466,7 @@ function createDanceMoves(options = {}) {
5985
6466
  }
5986
6467
  try {
5987
6468
  runtime_default.core(scope);
6469
+ installPlayback(scope, w.DanceMoves, root.ownerDocument.defaultView);
5988
6470
  runtime_default.effects(scope);
5989
6471
  runtime_default.orientationCore(scope);
5990
6472
  runtime_default.native(scope);