@gentomiyano/optimized-web-audio-player 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +189 -159
  2. package/dist/config/index.d.ts +1 -1
  3. package/dist/config/index.js +1 -0
  4. package/dist/config/index.js.map +1 -1
  5. package/dist/consumer/index.d.ts +1 -1
  6. package/dist/consumer/index.js.map +1 -1
  7. package/dist/contracts/index.d.ts +2 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/media-delivery/index.d.ts +1 -1
  12. package/dist/media-delivery/index.js.map +1 -1
  13. package/dist/{media-element-C9dis4aG.d.ts → media-element-DyMO-PhK.d.ts} +1 -1
  14. package/dist/{player-DE9Vqztu.d.ts → player-BxNFzRq4.d.ts} +53 -1
  15. package/dist/ports/index.d.ts +2 -2
  16. package/dist/react/index.d.ts +22 -3
  17. package/dist/react/index.js +721 -12
  18. package/dist/react/index.js.map +1 -1
  19. package/dist/testing/index.d.ts +1 -1
  20. package/dist/testing/index.js +1 -0
  21. package/dist/testing/index.js.map +1 -1
  22. package/dist/ui/index.d.ts +18 -2
  23. package/dist/ui/index.js +148 -55
  24. package/dist/ui/index.js.map +1 -1
  25. package/dist/ui/player-tokens.css +572 -572
  26. package/dist/ui/player-views.css +713 -654
  27. package/dist/ui/styles.css +2 -2
  28. package/package.json +14 -3
  29. package/dist/consumer-smoke/node_modules/.modules.yaml +0 -30
  30. package/dist/consumer-smoke/node_modules/.package-map.json +0 -1
  31. package/dist/consumer-smoke/node_modules/.pnpm/lock.yaml +0 -57
  32. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/LICENSE +0 -21
  33. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/README.md +0 -9
  34. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.development.js +0 -414
  35. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.production.js +0 -406
  36. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.development.js +0 -150
  37. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.production.js +0 -140
  38. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.development.js +0 -364
  39. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.development.js +0 -350
  40. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.production.js +0 -330
  41. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.production.js +0 -340
  42. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.js +0 -7
  43. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.native.js +0 -7
  44. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/package.json +0 -27
  45. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_mock.js +0 -7
  46. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_post_task.js +0 -7
  47. package/dist/consumer-smoke/node_modules/.pnpm-workspace-state-v1.json +0 -29
@@ -1,4 +1,4 @@
1
- import { createContext, useRef, useState, useEffect, useSyncExternalStore, useContext } from 'react';
1
+ import { createContext, useRef, useState, useEffect, useSyncExternalStore, useMemo, useContext, useId } from 'react';
2
2
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/player-provider.tsx
@@ -845,6 +845,7 @@ function createInitialPlayerState(experienceConfig) {
845
845
  shuffleEnabled: resolved.value.initialShuffleEnabled,
846
846
  repeatMode: resolved.value.initialRepeatMode,
847
847
  queue: Object.freeze([]),
848
+ activeContext: null,
848
849
  currentQueueIndex: null,
849
850
  canSeek: false,
850
851
  canGoPrevious: false,
@@ -995,16 +996,32 @@ function deriveCurrentIndex(queue, queueItemId) {
995
996
  const index = queue.findIndex((entry) => entry.queueItemId === queueItemId);
996
997
  return index < 0 ? null : index;
997
998
  }
999
+ function synchronizeActiveContext(context, queue, currentQueueItemId) {
1000
+ if (context === null) {
1001
+ return null;
1002
+ }
1003
+ return Object.freeze({
1004
+ ...context,
1005
+ queue,
1006
+ currentQueueItemId
1007
+ });
1008
+ }
998
1009
  function createPlayerSnapshot(previous, patch) {
999
1010
  const queue = patch.queue === void 0 ? previous.queue : freezeQueue(patch.queue);
1000
1011
  const currentQueueItemId = patch.currentQueueItemId === void 0 ? previous.currentQueueItemId : patch.currentQueueItemId;
1001
1012
  const currentQueueIndex = deriveCurrentIndex(queue, currentQueueItemId);
1002
1013
  const repeatMode = patch.repeatMode ?? previous.repeatMode;
1003
1014
  const currentItem = patch.currentItem === void 0 ? previous.currentItem : patch.currentItem;
1015
+ const activeContext = synchronizeActiveContext(
1016
+ patch.activeContext === void 0 ? previous.activeContext : patch.activeContext,
1017
+ queue,
1018
+ currentQueueItemId
1019
+ );
1004
1020
  return Object.freeze({
1005
1021
  ...previous,
1006
1022
  ...patch,
1007
1023
  queue,
1024
+ activeContext,
1008
1025
  currentQueueItemId,
1009
1026
  currentQueueIndex,
1010
1027
  canGoPrevious: patch.canGoPrevious ?? currentItem !== null,
@@ -1065,6 +1082,9 @@ function clampVolume2(value) {
1065
1082
  }
1066
1083
  return Math.min(Math.max(value, 0), 1);
1067
1084
  }
1085
+ function sourcesMatch(left, right) {
1086
+ return left.url === right.url && left.mimeType === right.mimeType;
1087
+ }
1068
1088
  var CorePlayerStore = class {
1069
1089
  commands;
1070
1090
  experience;
@@ -1117,6 +1137,10 @@ var CorePlayerStore = class {
1117
1137
  const commands = {
1118
1138
  load: (item) => this.#load(item),
1119
1139
  setQueue: (queue, commandOptions) => this.#setQueue(queue, commandOptions),
1140
+ activateContext: (commandOptions) => this.#activateContext(commandOptions),
1141
+ appendToManualHistory: (contextId, item) => {
1142
+ this.#appendToManualHistory(contextId, item);
1143
+ },
1120
1144
  play: () => this.#play(),
1121
1145
  pause: (commandOptions) => this.#pause(commandOptions),
1122
1146
  stop: (commandOptions) => this.#stop(commandOptions),
@@ -1279,7 +1303,7 @@ var CorePlayerStore = class {
1279
1303
  async #load(item) {
1280
1304
  return this.#setQueue([createLoadedQueueItem(item)]);
1281
1305
  }
1282
- async #setQueue(queue, options = {}) {
1306
+ async #setQueue(queue, options = {}, activeContext) {
1283
1307
  if (this.#destroyed) {
1284
1308
  return failed(DESTROYED_ERROR);
1285
1309
  }
@@ -1291,15 +1315,23 @@ var CorePlayerStore = class {
1291
1315
  return OK_RESULT;
1292
1316
  }
1293
1317
  const normalizedQueue = freezeQueue(queue);
1294
- const startQueueItemId = options.startQueueItemId ?? normalizedQueue[0]?.queueItemId;
1318
+ const currentQueueItemId = this.#state.currentQueueItemId;
1319
+ const currentStillExists = currentQueueItemId !== null && findQueueIndex(normalizedQueue, currentQueueItemId) !== null;
1320
+ const startQueueItemId = options.startQueueItemId ?? (currentStillExists ? currentQueueItemId : normalizedQueue[0]?.queueItemId);
1295
1321
  const startIndex = startQueueItemId === void 0 ? null : findQueueIndex(normalizedQueue, startQueueItemId);
1296
1322
  if (startIndex === null) {
1297
1323
  return failed(INVALID_QUEUE_ERROR);
1298
1324
  }
1299
- const version = this.#beginCommand(
1300
- options.autoplay === true ? "playing" : "paused"
1301
- );
1302
- const playbackQueue = this.#state.shuffleEnabled && startQueueItemId !== void 0 ? shuffleQueueAroundCurrent(
1325
+ const selected = normalizedQueue[startIndex];
1326
+ if (selected === void 0) {
1327
+ return failed(INVALID_QUEUE_ERROR);
1328
+ }
1329
+ const preservePlayback = options.reload !== true && currentQueueItemId === selected.queueItemId && this.#state.currentItem !== null && sourcesMatch(this.#state.currentItem.source, selected.item.source);
1330
+ const targetContext = activeContext ?? this.#state.activeContext;
1331
+ const manualHistory = targetContext?.mode === "manual-history";
1332
+ const shuffleEnabled = manualHistory ? false : this.#state.shuffleEnabled;
1333
+ const repeatMode = manualHistory && this.#state.repeatMode === "all" ? "off" : this.#state.repeatMode;
1334
+ const playbackQueue = shuffleEnabled && startQueueItemId !== void 0 ? shuffleQueueAroundCurrent(
1303
1335
  normalizedQueue,
1304
1336
  startQueueItemId,
1305
1337
  this.#random
@@ -1307,18 +1339,97 @@ var CorePlayerStore = class {
1307
1339
  this.#playbackOrderQueueItemIds = Object.freeze(
1308
1340
  playbackQueue.map((entry) => entry.queueItemId)
1309
1341
  );
1310
- this.#playbackOrderCursor = this.#state.shuffleEnabled ? 0 : startIndex;
1342
+ this.#playbackOrderCursor = shuffleEnabled ? 0 : startIndex;
1311
1343
  this.#setState({
1312
1344
  queue: normalizedQueue,
1313
- repeatMode: this.#state.repeatMode,
1345
+ ...activeContext === void 0 ? {} : { activeContext },
1346
+ currentItem: preservePlayback ? selected.item : this.#state.currentItem,
1347
+ currentItemId: preservePlayback ? selected.item.id : this.#state.currentItemId,
1348
+ shuffleEnabled,
1349
+ repeatMode,
1314
1350
  error: null
1315
1351
  });
1352
+ if (preservePlayback) {
1353
+ if (options.autoplay === true && this.#state.status !== "playing") {
1354
+ return this.#play();
1355
+ }
1356
+ return OK_RESULT;
1357
+ }
1358
+ const version = this.#beginCommand(
1359
+ options.autoplay === true ? "playing" : "paused"
1360
+ );
1316
1361
  return this.#selectQueueIndex(
1317
1362
  version,
1318
1363
  startIndex,
1319
1364
  options.autoplay === true
1320
1365
  );
1321
1366
  }
1367
+ async #activateContext(options) {
1368
+ if (this.#destroyed) {
1369
+ return failed(DESTROYED_ERROR);
1370
+ }
1371
+ if (options.contextId.trim().length === 0) {
1372
+ return failed(INVALID_QUEUE_ERROR);
1373
+ }
1374
+ const suppliedQueue = options.queue ?? (this.#state.activeContext?.id === options.contextId ? this.#state.queue : void 0);
1375
+ if (suppliedQueue === void 0 || !hasUniqueQueueItemIds(suppliedQueue)) {
1376
+ return failed(INVALID_QUEUE_ERROR);
1377
+ }
1378
+ const startIndex = findQueueIndex(
1379
+ suppliedQueue,
1380
+ options.startQueueItemId
1381
+ );
1382
+ if (startIndex === null) {
1383
+ return failed(INVALID_QUEUE_ERROR);
1384
+ }
1385
+ const startItem = suppliedQueue[startIndex];
1386
+ if (startItem === void 0) {
1387
+ return failed(INVALID_QUEUE_ERROR);
1388
+ }
1389
+ let queue;
1390
+ if (options.mode === "manual-history") {
1391
+ const sameManualContext = this.#state.activeContext?.id === options.contextId && this.#state.activeContext.mode === "manual-history";
1392
+ const history = sameManualContext ? [...this.#state.queue] : [];
1393
+ const existingIndex = findQueueIndex(history, startItem.queueItemId);
1394
+ if (existingIndex === null) {
1395
+ history.push(startItem);
1396
+ } else {
1397
+ history[existingIndex] = startItem;
1398
+ }
1399
+ queue = freezeQueue(history);
1400
+ } else {
1401
+ queue = freezeQueue(suppliedQueue);
1402
+ }
1403
+ const previousContext = this.#state.activeContext;
1404
+ const context = Object.freeze({
1405
+ id: options.contextId,
1406
+ mode: options.mode,
1407
+ ...options.label === void 0 && previousContext?.id === options.contextId ? previousContext.label === void 0 ? {} : { label: previousContext.label } : options.label === void 0 ? {} : { label: options.label },
1408
+ queue,
1409
+ currentQueueItemId: options.startQueueItemId
1410
+ });
1411
+ return this.#setQueue(
1412
+ queue,
1413
+ {
1414
+ startQueueItemId: options.startQueueItemId,
1415
+ ...options.autoplay === void 0 ? {} : { autoplay: options.autoplay },
1416
+ ...options.reload === void 0 ? {} : { reload: options.reload }
1417
+ },
1418
+ context
1419
+ );
1420
+ }
1421
+ #appendToManualHistory(contextId, item) {
1422
+ const context = this.#state.activeContext;
1423
+ if (this.#destroyed || context?.id !== contextId || context.mode !== "manual-history" || findQueueIndex(this.#state.queue, item.queueItemId) !== null) {
1424
+ return;
1425
+ }
1426
+ const queue = freezeQueue([...this.#state.queue, item]);
1427
+ this.#playbackOrderQueueItemIds = Object.freeze(
1428
+ queue.map((entry) => entry.queueItemId)
1429
+ );
1430
+ this.#playbackOrderCursor = this.#state.currentQueueItemId === null ? -1 : findQueueIndex(queue, this.#state.currentQueueItemId) ?? -1;
1431
+ this.#setState({ queue });
1432
+ }
1322
1433
  async #selectQueueIndex(version, index, autoplay) {
1323
1434
  const selected = this.#state.queue[index];
1324
1435
  if (selected === void 0) {
@@ -1591,7 +1702,7 @@ var CorePlayerStore = class {
1591
1702
  this.#setState({ muted });
1592
1703
  }
1593
1704
  #setShuffle(enabled) {
1594
- if (this.#destroyed || enabled === this.#state.shuffleEnabled) {
1705
+ if (this.#destroyed || enabled && this.#state.activeContext?.mode === "manual-history" || enabled === this.#state.shuffleEnabled) {
1595
1706
  return;
1596
1707
  }
1597
1708
  const currentQueueItemId = this.#state.currentQueueItemId;
@@ -1616,7 +1727,9 @@ var CorePlayerStore = class {
1616
1727
  if (this.#destroyed) {
1617
1728
  return;
1618
1729
  }
1619
- this.#setState({ repeatMode: mode });
1730
+ this.#setState({
1731
+ repeatMode: this.#state.activeContext?.mode === "manual-history" && mode === "all" ? "off" : mode
1732
+ });
1620
1733
  }
1621
1734
  async #next() {
1622
1735
  if (this.#destroyed) {
@@ -1715,6 +1828,7 @@ var CorePlayerStore = class {
1715
1828
  currentTimeSec: 0,
1716
1829
  durationSec: null,
1717
1830
  queue: Object.freeze([]),
1831
+ activeContext: null,
1718
1832
  canSeek: false,
1719
1833
  needsUserGesture: false,
1720
1834
  error: null
@@ -1884,6 +1998,14 @@ var CorePlayerStore = class {
1884
1998
  );
1885
1999
  return;
1886
2000
  }
2001
+ if (this.#state.activeContext?.mode === "manual-history") {
2002
+ this.#desiredPlayback = "paused";
2003
+ this.#setState({
2004
+ status: "ended",
2005
+ currentTimeSec: readFiniteDuration(this.#media) ?? readFiniteCurrentTime(this.#media)
2006
+ });
2007
+ return;
2008
+ }
1887
2009
  const nextPlaybackIndex = this.#getNextPlaybackIndex(
1888
2010
  currentQueueItemId
1889
2011
  );
@@ -1985,6 +2107,17 @@ function createGestureAwareCommands(commands, gain, activationOptions = {}) {
1985
2107
  }
1986
2108
  return commands.setQueue(queue, commandOptions);
1987
2109
  },
2110
+ activateContext(commandOptions) {
2111
+ reportCommand(activationOptions, "activateContext");
2112
+ if (commandOptions.autoplay === true) {
2113
+ activate(gain, activationOptions);
2114
+ }
2115
+ return commands.activateContext(commandOptions);
2116
+ },
2117
+ appendToManualHistory(contextId, item) {
2118
+ reportCommand(activationOptions, "appendToManualHistory");
2119
+ commands.appendToManualHistory(contextId, item);
2120
+ },
1988
2121
  play() {
1989
2122
  reportCommand(activationOptions, "play");
1990
2123
  activate(gain, activationOptions);
@@ -2387,7 +2520,583 @@ function usePlayer() {
2387
2520
  mediaSessionActionsEnabled: context.mediaSessionActionsEnabled
2388
2521
  };
2389
2522
  }
2523
+ function deriveBoundPlayerViewState(playerState, contextId, queueItem) {
2524
+ const isActive = playerState.activeContext?.id === contextId && playerState.currentQueueItemId === queueItem.queueItemId;
2525
+ return Object.freeze({
2526
+ queueItemId: queueItem.queueItemId,
2527
+ item: isActive ? playerState.currentItem ?? queueItem.item : queueItem.item,
2528
+ isActive,
2529
+ status: isActive ? playerState.status : "paused",
2530
+ currentTimeSec: isActive ? playerState.currentTimeSec : 0,
2531
+ durationSec: isActive ? playerState.durationSec : queueItem.item.durationHintSec ?? null,
2532
+ canSeek: isActive && playerState.canSeek,
2533
+ isPlaying: isActive && playerState.status === "playing"
2534
+ });
2535
+ }
2536
+ function useBoundPlayer({
2537
+ context,
2538
+ queueItem
2539
+ }) {
2540
+ const player = usePlayer();
2541
+ const state = useMemo(
2542
+ () => deriveBoundPlayerViewState(player.state, context.id, queueItem),
2543
+ [context.id, player.state, queueItem]
2544
+ );
2545
+ const commands = useMemo(() => {
2546
+ const play = () => player.commands.activateContext({
2547
+ contextId: context.id,
2548
+ mode: context.mode,
2549
+ ...context.label === void 0 ? {} : { label: context.label },
2550
+ queue: context.mode === "manual-history" ? [queueItem] : context.queue,
2551
+ startQueueItemId: queueItem.queueItemId,
2552
+ autoplay: true
2553
+ });
2554
+ return Object.freeze({
2555
+ play,
2556
+ togglePlayback() {
2557
+ if (player.state.activeContext?.id === context.id && player.state.currentQueueItemId === queueItem.queueItemId) {
2558
+ return player.commands.togglePlayback();
2559
+ }
2560
+ return play();
2561
+ },
2562
+ seekTo(seconds) {
2563
+ if (player.state.activeContext?.id === context.id && player.state.currentQueueItemId === queueItem.queueItemId && player.state.canSeek) {
2564
+ player.commands.seekTo(seconds);
2565
+ }
2566
+ }
2567
+ });
2568
+ }, [context, player.commands, player.state, queueItem]);
2569
+ return { state, commands };
2570
+ }
2571
+ function IconFrame({
2572
+ children,
2573
+ size = 20,
2574
+ ...props
2575
+ }) {
2576
+ return /* @__PURE__ */ jsx(
2577
+ "svg",
2578
+ {
2579
+ "aria-hidden": "true",
2580
+ focusable: "false",
2581
+ viewBox: "0 0 24 24",
2582
+ width: size,
2583
+ height: size,
2584
+ fill: "currentColor",
2585
+ ...props,
2586
+ children
2587
+ }
2588
+ );
2589
+ }
2590
+ function PlayIcon(props) {
2591
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M7 4.8c0-1.05 1.16-1.68 2.04-1.1l11 7.2a1.3 1.3 0 0 1 0 2.2l-11 7.2A1.31 1.31 0 0 1 7 19.2V4.8Z" }) });
2592
+ }
2593
+ function PauseIcon(props) {
2594
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
2595
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "4", width: "5", height: "16", rx: "0.8" }),
2596
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "5", height: "16", rx: "0.8" })
2597
+ ] });
2598
+ }
2599
+
2600
+ // src/ui/player-utils.ts
2601
+ function clamp(value, min, max) {
2602
+ return Math.min(Math.max(value, min), max);
2603
+ }
2604
+ function formatTime(seconds) {
2605
+ if (seconds === null || !Number.isFinite(seconds) || seconds < 0) {
2606
+ return "--:--";
2607
+ }
2608
+ const wholeSeconds = Math.floor(seconds);
2609
+ const hours = Math.floor(wholeSeconds / 3600);
2610
+ const minutes = Math.floor(wholeSeconds % 3600 / 60);
2611
+ const remainder = wholeSeconds % 60;
2612
+ if (hours > 0) {
2613
+ return `${String(hours)}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
2614
+ }
2615
+ return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
2616
+ }
2617
+ function formatTimeAria(seconds) {
2618
+ const safeSeconds = Math.max(0, Math.floor(seconds));
2619
+ const hours = Math.floor(safeSeconds / 3600);
2620
+ const minutes = Math.floor(safeSeconds % 3600 / 60);
2621
+ const remainder = safeSeconds % 60;
2622
+ const parts = [
2623
+ hours > 0 ? `${String(hours)} hour${hours === 1 ? "" : "s"}` : "",
2624
+ minutes > 0 ? `${String(minutes)} minute${minutes === 1 ? "" : "s"}` : "",
2625
+ `${String(remainder)} second${remainder === 1 ? "" : "s"}`
2626
+ ].filter(Boolean);
2627
+ return parts.join(" ");
2628
+ }
2629
+ function PlayerSurface({
2630
+ appearance,
2631
+ children,
2632
+ className,
2633
+ label
2634
+ }) {
2635
+ return /* @__PURE__ */ jsx(
2636
+ "section",
2637
+ {
2638
+ "aria-label": label,
2639
+ className: ["player-surface", className].filter(Boolean).join(" "),
2640
+ "data-appearance": appearance,
2641
+ children
2642
+ }
2643
+ );
2644
+ }
2645
+ function Artwork({ className, item }) {
2646
+ const classes = ["player-artwork", className].filter(Boolean).join(" ");
2647
+ if (item?.artwork !== void 0) {
2648
+ return /* @__PURE__ */ jsx("span", { className: `${classes} player-artwork--image`, children: /* @__PURE__ */ jsx(
2649
+ "img",
2650
+ {
2651
+ alt: item.artwork.alt,
2652
+ className: "player-artwork__image",
2653
+ decoding: "async",
2654
+ src: item.artwork.url
2655
+ }
2656
+ ) });
2657
+ }
2658
+ return /* @__PURE__ */ jsx(
2659
+ "div",
2660
+ {
2661
+ "aria-label": "Artwork unavailable",
2662
+ className: `${classes} player-artwork--fallback`,
2663
+ role: "img",
2664
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true" })
2665
+ }
2666
+ );
2667
+ }
2668
+ function ScrollingTitle({
2669
+ enabled,
2670
+ title
2671
+ }) {
2672
+ const containsJapanese = /[\u3040-\u30ff\u3400-\u9fff]/u.test(title);
2673
+ const viewportRef = useRef(null);
2674
+ const contentRef = useRef(null);
2675
+ const [marquee, setMarquee] = useState({
2676
+ distance: 0,
2677
+ duration: 10,
2678
+ overflow: false
2679
+ });
2680
+ useEffect(() => {
2681
+ const viewport = viewportRef.current;
2682
+ const content = contentRef.current;
2683
+ if (viewport === null || content === null) {
2684
+ return;
2685
+ }
2686
+ const viewportElement = viewport;
2687
+ const contentElement = content;
2688
+ function measure() {
2689
+ const viewportWidth = Math.max(
2690
+ viewportElement.getBoundingClientRect().width,
2691
+ viewportElement.clientWidth
2692
+ );
2693
+ const contentWidth = Math.max(
2694
+ contentElement.getBoundingClientRect().width,
2695
+ contentElement.scrollWidth
2696
+ );
2697
+ const overflow = contentWidth - viewportWidth > 1;
2698
+ const distance = enabled ? contentWidth + 12 : 0;
2699
+ setMarquee({
2700
+ distance,
2701
+ duration: Math.max(10, distance / 22 + 4),
2702
+ overflow
2703
+ });
2704
+ }
2705
+ measure();
2706
+ if (typeof ResizeObserver === "undefined") {
2707
+ window.addEventListener("resize", measure);
2708
+ return () => {
2709
+ window.removeEventListener("resize", measure);
2710
+ };
2711
+ }
2712
+ const observer = new ResizeObserver(measure);
2713
+ observer.observe(viewportElement);
2714
+ observer.observe(contentElement);
2715
+ return () => {
2716
+ observer.disconnect();
2717
+ };
2718
+ }, [enabled, title]);
2719
+ const style = {
2720
+ "--player-title-scroll-offset": `${String(-marquee.distance)}px`,
2721
+ "--player-title-scroll-duration": `${String(marquee.duration)}s`
2722
+ };
2723
+ return /* @__PURE__ */ jsx(
2724
+ "strong",
2725
+ {
2726
+ className: "player-title",
2727
+ "data-overflow": marquee.overflow ? "true" : "false",
2728
+ "data-scroll": enabled ? "true" : "false",
2729
+ "data-script": containsJapanese ? "japanese" : "latin",
2730
+ ref: viewportRef,
2731
+ style,
2732
+ children: /* @__PURE__ */ jsx("span", { className: "player-title__text", ref: contentRef, children: title })
2733
+ }
2734
+ );
2735
+ }
2736
+ function TrackMetadata({
2737
+ className,
2738
+ item,
2739
+ scrollTitle = false,
2740
+ status
2741
+ }) {
2742
+ return /* @__PURE__ */ jsxs(
2743
+ "div",
2744
+ {
2745
+ className: ["player-metadata", className].filter(Boolean).join(" "),
2746
+ children: [
2747
+ status === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-kicker", children: status }),
2748
+ /* @__PURE__ */ jsx(
2749
+ ScrollingTitle,
2750
+ {
2751
+ enabled: item !== null && scrollTitle,
2752
+ title: item?.title ?? "No media selected"
2753
+ },
2754
+ item?.title ?? "No media selected"
2755
+ ),
2756
+ /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : item.creatorName ?? "Artist unavailable" }),
2757
+ item?.collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: item.collectionTitle })
2758
+ ]
2759
+ }
2760
+ );
2761
+ }
2762
+ function TimeReadout({
2763
+ className,
2764
+ currentTimeSec,
2765
+ durationSec
2766
+ }) {
2767
+ return /* @__PURE__ */ jsxs(
2768
+ "div",
2769
+ {
2770
+ "aria-label": `${formatTime(currentTimeSec)} of ${formatTime(durationSec)}`,
2771
+ className: ["player-time", className].filter(Boolean).join(" "),
2772
+ children: [
2773
+ /* @__PURE__ */ jsx("time", { children: formatTime(currentTimeSec) }),
2774
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" }),
2775
+ /* @__PURE__ */ jsx("time", { children: formatTime(durationSec) })
2776
+ ]
2777
+ }
2778
+ );
2779
+ }
2780
+ function IconButton({
2781
+ children,
2782
+ className,
2783
+ disabled = false,
2784
+ label,
2785
+ onClick,
2786
+ pressed
2787
+ }) {
2788
+ return /* @__PURE__ */ jsx(
2789
+ "button",
2790
+ {
2791
+ "aria-label": label,
2792
+ ...pressed === void 0 ? {} : { "aria-pressed": pressed },
2793
+ className: ["player-icon-button", className].filter(Boolean).join(" "),
2794
+ disabled,
2795
+ onClick,
2796
+ title: label,
2797
+ type: "button",
2798
+ children
2799
+ }
2800
+ );
2801
+ }
2802
+ function SeekBar({
2803
+ canSeek,
2804
+ className,
2805
+ commitMode,
2806
+ currentTimeSec,
2807
+ durationSec,
2808
+ label = "Seek",
2809
+ onSeek,
2810
+ status
2811
+ }) {
2812
+ const duration = durationSec !== null && Number.isFinite(durationSec) && durationSec > 0 ? durationSec : 0;
2813
+ const disabled = !canSeek || duration === 0 || status === "loading";
2814
+ const safeCurrentTime = clamp(currentTimeSec, 0, duration);
2815
+ const [previewTime, setPreviewTime] = useState(safeCurrentTime);
2816
+ const previewProgress = duration === 0 ? 0 : clamp(previewTime / duration, 0, 1) * 100;
2817
+ const isAdjusting = useRef(false);
2818
+ const lastCommitted = useRef(null);
2819
+ useEffect(() => {
2820
+ if (!isAdjusting.current) {
2821
+ setPreviewTime(safeCurrentTime);
2822
+ }
2823
+ }, [safeCurrentTime]);
2824
+ function updatePreview(value) {
2825
+ const nextValue = clamp(value, 0, duration);
2826
+ isAdjusting.current = true;
2827
+ setPreviewTime(nextValue);
2828
+ if (commitMode === "live") {
2829
+ onSeek(nextValue);
2830
+ lastCommitted.current = nextValue;
2831
+ }
2832
+ }
2833
+ function commitPreview(value = previewTime) {
2834
+ const nextValue = clamp(value, 0, duration);
2835
+ if (disabled || !isAdjusting.current || commitMode === "live" && lastCommitted.current === nextValue) {
2836
+ isAdjusting.current = false;
2837
+ return;
2838
+ }
2839
+ onSeek(nextValue);
2840
+ lastCommitted.current = nextValue;
2841
+ isAdjusting.current = false;
2842
+ }
2843
+ function cancelPreview() {
2844
+ isAdjusting.current = false;
2845
+ lastCommitted.current = null;
2846
+ setPreviewTime(safeCurrentTime);
2847
+ }
2848
+ return /* @__PURE__ */ jsxs(
2849
+ "label",
2850
+ {
2851
+ className: ["player-seek", className].filter(Boolean).join(" "),
2852
+ "data-disabled": disabled ? "true" : "false",
2853
+ children: [
2854
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
2855
+ /* @__PURE__ */ jsx(
2856
+ "span",
2857
+ {
2858
+ "aria-hidden": "true",
2859
+ className: "player-seek__progress",
2860
+ style: { width: `${String(disabled ? 0 : previewProgress)}%` }
2861
+ }
2862
+ ),
2863
+ /* @__PURE__ */ jsx(
2864
+ "input",
2865
+ {
2866
+ "aria-label": label,
2867
+ "aria-valuetext": formatTimeAria(previewTime),
2868
+ disabled,
2869
+ max: duration || 1,
2870
+ min: 0,
2871
+ onBlur: (event) => {
2872
+ commitPreview(Number(event.currentTarget.value));
2873
+ },
2874
+ onChange: (event) => {
2875
+ updatePreview(Number(event.currentTarget.value));
2876
+ },
2877
+ onKeyUp: (event) => {
2878
+ commitPreview(Number(event.currentTarget.value));
2879
+ },
2880
+ onPointerDown: () => {
2881
+ isAdjusting.current = true;
2882
+ },
2883
+ onPointerCancel: cancelPreview,
2884
+ onPointerUp: (event) => {
2885
+ commitPreview(Number(event.currentTarget.value));
2886
+ },
2887
+ step: 0.1,
2888
+ type: "range",
2889
+ value: disabled ? 0 : previewTime
2890
+ }
2891
+ )
2892
+ ]
2893
+ }
2894
+ );
2895
+ }
2896
+ var FALLBACK_PEAKS = [
2897
+ 0.16,
2898
+ 0.24,
2899
+ 0.42,
2900
+ 0.3,
2901
+ 0.58,
2902
+ 0.76,
2903
+ 0.48,
2904
+ 0.34,
2905
+ 0.66,
2906
+ 0.88,
2907
+ 0.62,
2908
+ 0.4,
2909
+ 0.52,
2910
+ 0.72,
2911
+ 0.44,
2912
+ 0.28,
2913
+ 0.56,
2914
+ 0.82,
2915
+ 0.68,
2916
+ 0.38,
2917
+ 0.24,
2918
+ 0.46,
2919
+ 0.32,
2920
+ 0.18
2921
+ ];
2922
+ var BAR_COUNT = 512;
2923
+ var VIEWBOX_WIDTH = BAR_COUNT;
2924
+ function createWaveformBars(peaks) {
2925
+ const source = peaks.length > 1 ? peaks : FALLBACK_PEAKS;
2926
+ return Array.from({ length: BAR_COUNT }, (_, index) => {
2927
+ const sourcePosition = index / Math.max(BAR_COUNT - 1, 1) * (source.length - 1);
2928
+ const leftIndex = Math.floor(sourcePosition);
2929
+ const rightIndex = Math.min(leftIndex + 1, source.length - 1);
2930
+ const mix = sourcePosition - leftIndex;
2931
+ const leftPeak = Math.abs(source[leftIndex] ?? 0);
2932
+ const rightPeak = Math.abs(source[rightIndex] ?? leftPeak);
2933
+ const interpolated = leftPeak + (rightPeak - leftPeak) * mix;
2934
+ if (source.length >= BAR_COUNT) {
2935
+ return clamp(Math.pow(interpolated, 1.18), 0.04, 1);
2936
+ }
2937
+ const noiseSeed = Math.sin((index + 1) * 12.9898 + source.length * 78.233) * 43758.5453;
2938
+ const noise = noiseSeed - Math.floor(noiseSeed);
2939
+ const fineDetail = 0.76 + noise * 0.34;
2940
+ const transient = noise > 0.975 ? 3.2 : noise > 0.91 ? 1.75 : 1;
2941
+ return clamp(
2942
+ Math.pow(interpolated, 1.22) * fineDetail * transient,
2943
+ 0.04,
2944
+ 1
2945
+ );
2946
+ });
2947
+ }
2948
+ function Waveform({
2949
+ className,
2950
+ opacity = 1,
2951
+ playedStateEnabled = true,
2952
+ progress = 0,
2953
+ waveform
2954
+ }) {
2955
+ const clipId = useId().replaceAll(":", "");
2956
+ const bars = createWaveformBars(waveform?.peaks ?? FALLBACK_PEAKS);
2957
+ const progressWidth = clamp(progress, 0, 1) * VIEWBOX_WIDTH;
2958
+ const classes = ["player-waveform", className].filter(Boolean).join(" ");
2959
+ const barPath = bars.map((peak, index) => {
2960
+ const halfHeight = peak * 47;
2961
+ const x = index + 0.5;
2962
+ return `M ${String(x)} ${String(50 - halfHeight)} V ${String(
2963
+ 50 + halfHeight
2964
+ )}`;
2965
+ }).join(" ");
2966
+ return /* @__PURE__ */ jsxs(
2967
+ "svg",
2968
+ {
2969
+ "aria-hidden": "true",
2970
+ className: classes,
2971
+ focusable: "false",
2972
+ preserveAspectRatio: "none",
2973
+ style: { opacity: clamp(opacity, 0, 1), pointerEvents: "none" },
2974
+ viewBox: `0 0 ${String(VIEWBOX_WIDTH)} 100`,
2975
+ children: [
2976
+ /* @__PURE__ */ jsx("path", { className: "player-waveform__base", d: barPath }),
2977
+ playedStateEnabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
2978
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { width: progressWidth, height: "100" }) }),
2979
+ /* @__PURE__ */ jsx(
2980
+ "path",
2981
+ {
2982
+ className: "player-waveform__played",
2983
+ clipPath: `url(#${clipId})`,
2984
+ d: barPath
2985
+ }
2986
+ )
2987
+ ] }) : null
2988
+ ]
2989
+ }
2990
+ );
2991
+ }
2992
+ function BoundCompactPlayerView({
2993
+ appearance,
2994
+ className,
2995
+ commands,
2996
+ contextId,
2997
+ contextMode,
2998
+ options,
2999
+ state
3000
+ }) {
3001
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
3002
+ return /* @__PURE__ */ jsx(
3003
+ PlayerSurface,
3004
+ {
3005
+ appearance,
3006
+ className: ["player-bound-compact", className].filter(Boolean).join(" "),
3007
+ label: `Inline player: ${state.item.title}`,
3008
+ children: /* @__PURE__ */ jsxs(
3009
+ "div",
3010
+ {
3011
+ className: "player-bound-compact__main",
3012
+ "data-active": state.isActive ? "true" : "false",
3013
+ "data-playback-context-id": contextId,
3014
+ "data-playback-context-mode": contextMode,
3015
+ "data-queue-item-id": state.queueItemId,
3016
+ children: [
3017
+ /* @__PURE__ */ jsx(
3018
+ IconButton,
3019
+ {
3020
+ className: "player-play-button",
3021
+ label: state.isPlaying ? "Pause" : "Play",
3022
+ onClick: () => {
3023
+ void commands.togglePlayback();
3024
+ },
3025
+ children: state.isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
3026
+ }
3027
+ ),
3028
+ /* @__PURE__ */ jsx(
3029
+ Artwork,
3030
+ {
3031
+ className: "player-bound-compact__artwork",
3032
+ item: state.item
3033
+ }
3034
+ ),
3035
+ /* @__PURE__ */ jsx(TrackMetadata, { item: state.item, scrollTitle: state.isPlaying }),
3036
+ /* @__PURE__ */ jsxs("div", { className: "player-bound-compact__signal", children: [
3037
+ /* @__PURE__ */ jsx(
3038
+ Waveform,
3039
+ {
3040
+ opacity: options.waveformOpacity,
3041
+ playedStateEnabled: options.waveformPlayedStateEnabled,
3042
+ progress,
3043
+ waveform: state.item.waveform
3044
+ }
3045
+ ),
3046
+ /* @__PURE__ */ jsx(
3047
+ TimeReadout,
3048
+ {
3049
+ currentTimeSec: state.currentTimeSec,
3050
+ durationSec: state.durationSec
3051
+ }
3052
+ )
3053
+ ] }),
3054
+ /* @__PURE__ */ jsx(
3055
+ SeekBar,
3056
+ {
3057
+ canSeek: state.canSeek,
3058
+ commitMode: options.seekCommitMode,
3059
+ currentTimeSec: state.currentTimeSec,
3060
+ durationSec: state.durationSec,
3061
+ label: `Seek in ${state.item.title}`,
3062
+ onSeek: (seconds) => {
3063
+ commands.seekTo(seconds);
3064
+ },
3065
+ status: state.status
3066
+ }
3067
+ )
3068
+ ]
3069
+ }
3070
+ )
3071
+ }
3072
+ );
3073
+ }
3074
+ function BoundCompactPlayer({
3075
+ appearance,
3076
+ className,
3077
+ context,
3078
+ queueItem
3079
+ }) {
3080
+ const bound = useBoundPlayer({ context, queueItem });
3081
+ const player = usePlayer();
3082
+ return /* @__PURE__ */ jsx(
3083
+ BoundCompactPlayerView,
3084
+ {
3085
+ appearance,
3086
+ ...className === void 0 ? {} : { className },
3087
+ commands: bound.commands,
3088
+ contextId: context.id,
3089
+ contextMode: context.mode,
3090
+ options: {
3091
+ seekCommitMode: player.state.experienceConfig.seekCommitMode,
3092
+ waveformOpacity: player.state.experienceConfig.waveformOpacity,
3093
+ waveformPlayedStateEnabled: player.state.experienceConfig.waveformPlayedStateEnabled
3094
+ },
3095
+ state: bound.state
3096
+ }
3097
+ );
3098
+ }
2390
3099
 
2391
- export { MiniPlayerHost, PlayerProvider, usePlayer };
3100
+ export { BoundCompactPlayer, MiniPlayerHost, PlayerProvider, deriveBoundPlayerViewState, useBoundPlayer, usePlayer };
2392
3101
  //# sourceMappingURL=index.js.map
2393
3102
  //# sourceMappingURL=index.js.map