@gentomiyano/optimized-web-audio-player 0.2.0 → 0.2.2

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.
@@ -1,4 +1,4 @@
1
- import { createContext, useRef, useState, useEffect, useSyncExternalStore, useMemo, useContext } from 'react';
1
+ import { createContext, useRef, useState, useEffect, useMemo, useSyncExternalStore, useContext, useId } from 'react';
2
2
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/player-provider.tsx
@@ -2077,6 +2077,147 @@ function createPlayerStore(options) {
2077
2077
  return new CorePlayerStore(options);
2078
2078
  }
2079
2079
 
2080
+ // src/react/player-runtime-bridge.ts
2081
+ var RUNTIME_PENDING_ERROR = Object.freeze({
2082
+ code: "unknown",
2083
+ recoverable: true,
2084
+ messageKey: "player.error.runtime-pending"
2085
+ });
2086
+ var RUNTIME_PENDING_RESULT = Object.freeze({
2087
+ ok: false,
2088
+ error: RUNTIME_PENDING_ERROR
2089
+ });
2090
+ function notify(listeners) {
2091
+ for (const listener of listeners) {
2092
+ listener();
2093
+ }
2094
+ }
2095
+ function createPlayerRuntimeBridge(experienceConfig) {
2096
+ let pendingState = createInitialPlayerState(experienceConfig);
2097
+ let activeStore = null;
2098
+ let activeCommands = null;
2099
+ let unsubscribeStore = null;
2100
+ let unsubscribeExperience = null;
2101
+ const listeners = /* @__PURE__ */ new Set();
2102
+ const experienceListeners = /* @__PURE__ */ new Set();
2103
+ const pendingResult = () => Promise.resolve(RUNTIME_PENDING_RESULT);
2104
+ const commandsImplementation = {
2105
+ load: (item) => activeCommands?.load(item) ?? pendingResult(),
2106
+ setQueue: (queue, options) => activeCommands?.setQueue(queue, options) ?? pendingResult(),
2107
+ activateContext: (options) => activeCommands?.activateContext(options) ?? pendingResult(),
2108
+ appendToManualHistory: (contextId, item) => {
2109
+ activeCommands?.appendToManualHistory(contextId, item);
2110
+ },
2111
+ play: () => activeCommands?.play() ?? pendingResult(),
2112
+ pause: (options) => activeCommands?.pause(options) ?? Promise.resolve(),
2113
+ stop: (options) => activeCommands?.stop(options) ?? Promise.resolve(),
2114
+ togglePlayback: () => activeCommands?.togglePlayback() ?? pendingResult(),
2115
+ seekTo: (seconds) => {
2116
+ activeCommands?.seekTo(seconds);
2117
+ },
2118
+ seekBy: (deltaSeconds) => {
2119
+ activeCommands?.seekBy(deltaSeconds);
2120
+ },
2121
+ setVolume: (volume) => {
2122
+ activeCommands?.setVolume(volume);
2123
+ },
2124
+ toggleMute: () => {
2125
+ activeCommands?.toggleMute();
2126
+ },
2127
+ setShuffle: (enabled) => {
2128
+ activeCommands?.setShuffle(enabled);
2129
+ },
2130
+ setRepeatMode: (mode) => {
2131
+ activeCommands?.setRepeatMode(mode);
2132
+ },
2133
+ next: () => activeCommands?.next() ?? pendingResult(),
2134
+ previous: () => activeCommands?.previous() ?? pendingResult(),
2135
+ retry: () => activeCommands?.retry() ?? pendingResult(),
2136
+ clear: () => activeCommands?.clear() ?? Promise.resolve()
2137
+ };
2138
+ const commands = Object.freeze(commandsImplementation);
2139
+ const experienceImplementation = {
2140
+ getConfig: () => activeStore?.experience.getConfig() ?? pendingState.experienceConfig,
2141
+ updateConfig: (patch) => {
2142
+ if (activeStore !== null) {
2143
+ activeStore.experience.updateConfig(patch);
2144
+ return;
2145
+ }
2146
+ pendingState = createInitialPlayerState({
2147
+ ...pendingState.experienceConfig,
2148
+ ...patch
2149
+ });
2150
+ notify(listeners);
2151
+ notify(experienceListeners);
2152
+ },
2153
+ resetConfig: () => {
2154
+ if (activeStore !== null) {
2155
+ activeStore.experience.resetConfig();
2156
+ return;
2157
+ }
2158
+ pendingState = createInitialPlayerState(
2159
+ PREVIEW_PLAYER_EXPERIENCE_CONFIG
2160
+ );
2161
+ notify(listeners);
2162
+ notify(experienceListeners);
2163
+ },
2164
+ subscribe: (listener) => {
2165
+ experienceListeners.add(listener);
2166
+ return () => {
2167
+ experienceListeners.delete(listener);
2168
+ };
2169
+ }
2170
+ };
2171
+ const experience = Object.freeze(experienceImplementation);
2172
+ const storeImplementation = {
2173
+ getState: () => activeStore?.getState() ?? pendingState,
2174
+ subscribe: (listener) => {
2175
+ listeners.add(listener);
2176
+ return () => {
2177
+ listeners.delete(listener);
2178
+ };
2179
+ },
2180
+ commands,
2181
+ experience,
2182
+ synchronizeFromMedia: (reason) => activeStore?.synchronizeFromMedia(reason) ?? Promise.resolve(),
2183
+ destroy: () => activeStore?.destroy() ?? Promise.resolve()
2184
+ };
2185
+ const store = Object.freeze(storeImplementation);
2186
+ const detachActiveStore = () => {
2187
+ unsubscribeStore?.();
2188
+ unsubscribeExperience?.();
2189
+ unsubscribeStore = null;
2190
+ unsubscribeExperience = null;
2191
+ activeStore = null;
2192
+ activeCommands = null;
2193
+ };
2194
+ const bridge = {
2195
+ store,
2196
+ attach(nextStore, nextCommands) {
2197
+ detachActiveStore();
2198
+ activeStore = nextStore;
2199
+ activeCommands = nextCommands;
2200
+ unsubscribeStore = nextStore.subscribe(() => {
2201
+ notify(listeners);
2202
+ });
2203
+ unsubscribeExperience = nextStore.experience.subscribe(() => {
2204
+ notify(experienceListeners);
2205
+ });
2206
+ notify(listeners);
2207
+ notify(experienceListeners);
2208
+ return () => {
2209
+ if (activeStore !== nextStore) {
2210
+ return;
2211
+ }
2212
+ detachActiveStore();
2213
+ notify(listeners);
2214
+ notify(experienceListeners);
2215
+ };
2216
+ }
2217
+ };
2218
+ return Object.freeze(bridge);
2219
+ }
2220
+
2080
2221
  // src/react/runtime-commands.ts
2081
2222
  function activate(gain, options) {
2082
2223
  options.onUserActivation?.();
@@ -2216,7 +2357,10 @@ function PlayerProvider({
2216
2357
  const initialQueueItemIdRef = useRef(initialQueueItemId);
2217
2358
  const initialExperienceConfigRef = useRef(experienceConfig);
2218
2359
  const diagnosticListenerRef = useRef(onDiagnosticEvent);
2219
- const [runtime, setRuntime] = useState(null);
2360
+ const [runtimeBridge] = useState(
2361
+ () => createPlayerRuntimeBridge(experienceConfig)
2362
+ );
2363
+ const [runtimeStatus, setRuntimeStatus] = useState("pending");
2220
2364
  const [audioContextState, setAudioContextState] = useState("uninitialized");
2221
2365
  const [resolvedAudioMode, setResolvedAudioMode] = useState("web-audio");
2222
2366
  const [mediaSessionSupported, setMediaSessionSupported] = useState(false);
@@ -2295,6 +2439,7 @@ function PlayerProvider({
2295
2439
  });
2296
2440
  }
2297
2441
  });
2442
+ const detachRuntime = runtimeBridge.attach(store, commands);
2298
2443
  mediaSessionController = createBrowserMediaSessionController({
2299
2444
  commands,
2300
2445
  getState: () => store.getState(),
@@ -2391,16 +2536,7 @@ function PlayerProvider({
2391
2536
  );
2392
2537
  window.addEventListener("pageshow", handlePageShow);
2393
2538
  window.addEventListener("focus", handleFocus);
2394
- const contextValue2 = {
2395
- store,
2396
- commands,
2397
- experience: store.experience,
2398
- audioContextState: "uninitialized",
2399
- audioMode: nextAudioMode,
2400
- mediaSessionSupported: activeMediaSessionController.supported,
2401
- mediaSessionActionsEnabled: false
2402
- };
2403
- setRuntime(contextValue2);
2539
+ setRuntimeStatus("ready");
2404
2540
  void store.commands.setQueue(initialQueueRef.current, {
2405
2541
  ...initialQueueItemIdRef.current === void 0 ? {} : { startQueueItemId: initialQueueItemIdRef.current },
2406
2542
  autoplay: false
@@ -2419,21 +2555,35 @@ function PlayerProvider({
2419
2555
  }
2420
2556
  unsubscribeMediaSession();
2421
2557
  activeMediaSessionController.destroy();
2422
- setRuntime(
2423
- (current) => current?.store === store ? null : current
2424
- );
2558
+ setRuntimeStatus("pending");
2559
+ detachRuntime();
2425
2560
  void store.destroy();
2426
2561
  };
2427
2562
  }, [
2428
- audioMode
2563
+ audioMode,
2564
+ runtimeBridge
2429
2565
  ]);
2430
- const contextValue = runtime === null ? null : {
2431
- ...runtime,
2432
- audioContextState,
2433
- audioMode: resolvedAudioMode,
2434
- mediaSessionSupported,
2435
- mediaSessionActionsEnabled
2436
- };
2566
+ const contextValue = useMemo(
2567
+ () => ({
2568
+ runtimeStatus,
2569
+ store: runtimeBridge.store,
2570
+ commands: runtimeBridge.store.commands,
2571
+ experience: runtimeBridge.store.experience,
2572
+ audioContextState,
2573
+ audioMode: resolvedAudioMode,
2574
+ mediaSessionSupported,
2575
+ mediaSessionActionsEnabled
2576
+ }),
2577
+ [
2578
+ audioContextState,
2579
+ mediaSessionActionsEnabled,
2580
+ mediaSessionSupported,
2581
+ resolvedAudioMode,
2582
+ runtimeBridge,
2583
+ runtimeStatus
2584
+ ]
2585
+ );
2586
+ const consumerTree = children ?? (runtimeStatus === "pending" ? loadingFallback : null);
2437
2587
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2438
2588
  /* @__PURE__ */ jsx(
2439
2589
  "audio",
@@ -2444,11 +2594,12 @@ function PlayerProvider({
2444
2594
  "data-media-session-actions": mediaSessionActionsEnabled ? "enabled" : "inactive",
2445
2595
  "data-media-session-supported": mediaSessionSupported ? "true" : "false",
2446
2596
  "data-player-audio": "true",
2597
+ "data-player-runtime-status": runtimeStatus,
2447
2598
  preload: "metadata",
2448
2599
  ref: audioRef
2449
2600
  }
2450
2601
  ),
2451
- contextValue === null ? loadingFallback : /* @__PURE__ */ jsx(PlayerRuntimeContext.Provider, { value: contextValue, children })
2602
+ /* @__PURE__ */ jsx(PlayerRuntimeContext.Provider, { value: contextValue, children: consumerTree })
2452
2603
  ] });
2453
2604
  }
2454
2605
  function MiniPlayerHost({
@@ -2511,6 +2662,7 @@ function usePlayer() {
2511
2662
  () => context.store.getState()
2512
2663
  );
2513
2664
  return {
2665
+ runtimeStatus: context.runtimeStatus,
2514
2666
  state,
2515
2667
  commands: context.commands,
2516
2668
  experience: context.experience,
@@ -2568,7 +2720,538 @@ function useBoundPlayer({
2568
2720
  }, [context, player.commands, player.state, queueItem]);
2569
2721
  return { state, commands };
2570
2722
  }
2723
+ function IconFrame({
2724
+ children,
2725
+ size = 20,
2726
+ ...props
2727
+ }) {
2728
+ return /* @__PURE__ */ jsx(
2729
+ "svg",
2730
+ {
2731
+ "aria-hidden": "true",
2732
+ focusable: "false",
2733
+ viewBox: "0 0 24 24",
2734
+ width: size,
2735
+ height: size,
2736
+ fill: "currentColor",
2737
+ ...props,
2738
+ children
2739
+ }
2740
+ );
2741
+ }
2742
+ function PlayIcon(props) {
2743
+ 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" }) });
2744
+ }
2745
+ function PauseIcon(props) {
2746
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
2747
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "4", width: "5", height: "16", rx: "0.8" }),
2748
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "5", height: "16", rx: "0.8" })
2749
+ ] });
2750
+ }
2751
+
2752
+ // src/ui/player-utils.ts
2753
+ function clamp(value, min, max) {
2754
+ return Math.min(Math.max(value, min), max);
2755
+ }
2756
+ function formatTime(seconds) {
2757
+ if (seconds === null || !Number.isFinite(seconds) || seconds < 0) {
2758
+ return "--:--";
2759
+ }
2760
+ const wholeSeconds = Math.floor(seconds);
2761
+ const hours = Math.floor(wholeSeconds / 3600);
2762
+ const minutes = Math.floor(wholeSeconds % 3600 / 60);
2763
+ const remainder = wholeSeconds % 60;
2764
+ if (hours > 0) {
2765
+ return `${String(hours)}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
2766
+ }
2767
+ return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
2768
+ }
2769
+ function formatTimeAria(seconds) {
2770
+ const safeSeconds = Math.max(0, Math.floor(seconds));
2771
+ const hours = Math.floor(safeSeconds / 3600);
2772
+ const minutes = Math.floor(safeSeconds % 3600 / 60);
2773
+ const remainder = safeSeconds % 60;
2774
+ const parts = [
2775
+ hours > 0 ? `${String(hours)} hour${hours === 1 ? "" : "s"}` : "",
2776
+ minutes > 0 ? `${String(minutes)} minute${minutes === 1 ? "" : "s"}` : "",
2777
+ `${String(remainder)} second${remainder === 1 ? "" : "s"}`
2778
+ ].filter(Boolean);
2779
+ return parts.join(" ");
2780
+ }
2781
+ function PlayerSurface({
2782
+ appearance,
2783
+ children,
2784
+ className,
2785
+ label
2786
+ }) {
2787
+ return /* @__PURE__ */ jsx(
2788
+ "section",
2789
+ {
2790
+ "aria-label": label,
2791
+ className: ["player-surface", className].filter(Boolean).join(" "),
2792
+ "data-appearance": appearance,
2793
+ children
2794
+ }
2795
+ );
2796
+ }
2797
+ function Artwork({ className, item }) {
2798
+ const classes = ["player-artwork", className].filter(Boolean).join(" ");
2799
+ if (item?.artwork !== void 0) {
2800
+ return /* @__PURE__ */ jsx("span", { className: `${classes} player-artwork--image`, children: /* @__PURE__ */ jsx(
2801
+ "img",
2802
+ {
2803
+ alt: item.artwork.alt,
2804
+ className: "player-artwork__image",
2805
+ decoding: "async",
2806
+ src: item.artwork.url
2807
+ }
2808
+ ) });
2809
+ }
2810
+ return /* @__PURE__ */ jsx(
2811
+ "div",
2812
+ {
2813
+ "aria-label": "Artwork unavailable",
2814
+ className: `${classes} player-artwork--fallback`,
2815
+ role: "img",
2816
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true" })
2817
+ }
2818
+ );
2819
+ }
2820
+ function ScrollingTitle({
2821
+ enabled,
2822
+ title
2823
+ }) {
2824
+ const containsJapanese = /[\u3040-\u30ff\u3400-\u9fff]/u.test(title);
2825
+ const viewportRef = useRef(null);
2826
+ const contentRef = useRef(null);
2827
+ const [marquee, setMarquee] = useState({
2828
+ distance: 0,
2829
+ duration: 10,
2830
+ overflow: false
2831
+ });
2832
+ useEffect(() => {
2833
+ const viewport = viewportRef.current;
2834
+ const content = contentRef.current;
2835
+ if (viewport === null || content === null) {
2836
+ return;
2837
+ }
2838
+ const viewportElement = viewport;
2839
+ const contentElement = content;
2840
+ function measure() {
2841
+ const viewportWidth = Math.max(
2842
+ viewportElement.getBoundingClientRect().width,
2843
+ viewportElement.clientWidth
2844
+ );
2845
+ const contentWidth = Math.max(
2846
+ contentElement.getBoundingClientRect().width,
2847
+ contentElement.scrollWidth
2848
+ );
2849
+ const overflow = contentWidth - viewportWidth > 1;
2850
+ const distance = enabled ? contentWidth + 12 : 0;
2851
+ setMarquee({
2852
+ distance,
2853
+ duration: Math.max(10, distance / 22 + 4),
2854
+ overflow
2855
+ });
2856
+ }
2857
+ measure();
2858
+ if (typeof ResizeObserver === "undefined") {
2859
+ window.addEventListener("resize", measure);
2860
+ return () => {
2861
+ window.removeEventListener("resize", measure);
2862
+ };
2863
+ }
2864
+ const observer = new ResizeObserver(measure);
2865
+ observer.observe(viewportElement);
2866
+ observer.observe(contentElement);
2867
+ return () => {
2868
+ observer.disconnect();
2869
+ };
2870
+ }, [enabled, title]);
2871
+ const style = {
2872
+ "--player-title-scroll-offset": `${String(-marquee.distance)}px`,
2873
+ "--player-title-scroll-duration": `${String(marquee.duration)}s`
2874
+ };
2875
+ return /* @__PURE__ */ jsx(
2876
+ "strong",
2877
+ {
2878
+ className: "player-title",
2879
+ "data-overflow": marquee.overflow ? "true" : "false",
2880
+ "data-scroll": enabled ? "true" : "false",
2881
+ "data-script": containsJapanese ? "japanese" : "latin",
2882
+ ref: viewportRef,
2883
+ style,
2884
+ children: /* @__PURE__ */ jsx("span", { className: "player-title__text", ref: contentRef, children: title })
2885
+ }
2886
+ );
2887
+ }
2888
+ function TrackMetadata({
2889
+ className,
2890
+ item,
2891
+ scrollTitle = false,
2892
+ status
2893
+ }) {
2894
+ return /* @__PURE__ */ jsxs(
2895
+ "div",
2896
+ {
2897
+ className: ["player-metadata", className].filter(Boolean).join(" "),
2898
+ children: [
2899
+ status === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-kicker", children: status }),
2900
+ /* @__PURE__ */ jsx(
2901
+ ScrollingTitle,
2902
+ {
2903
+ enabled: item !== null && scrollTitle,
2904
+ title: item?.title ?? "No media selected"
2905
+ },
2906
+ item?.title ?? "No media selected"
2907
+ ),
2908
+ /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : item.creatorName ?? "Artist unavailable" }),
2909
+ item?.collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: item.collectionTitle })
2910
+ ]
2911
+ }
2912
+ );
2913
+ }
2914
+ function TimeReadout({
2915
+ className,
2916
+ currentTimeSec,
2917
+ durationSec
2918
+ }) {
2919
+ return /* @__PURE__ */ jsxs(
2920
+ "div",
2921
+ {
2922
+ "aria-label": `${formatTime(currentTimeSec)} of ${formatTime(durationSec)}`,
2923
+ className: ["player-time", className].filter(Boolean).join(" "),
2924
+ children: [
2925
+ /* @__PURE__ */ jsx("time", { children: formatTime(currentTimeSec) }),
2926
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" }),
2927
+ /* @__PURE__ */ jsx("time", { children: formatTime(durationSec) })
2928
+ ]
2929
+ }
2930
+ );
2931
+ }
2932
+ function IconButton({
2933
+ children,
2934
+ className,
2935
+ disabled = false,
2936
+ label,
2937
+ onClick,
2938
+ pressed
2939
+ }) {
2940
+ return /* @__PURE__ */ jsx(
2941
+ "button",
2942
+ {
2943
+ "aria-label": label,
2944
+ ...pressed === void 0 ? {} : { "aria-pressed": pressed },
2945
+ className: ["player-icon-button", className].filter(Boolean).join(" "),
2946
+ disabled,
2947
+ onClick,
2948
+ title: label,
2949
+ type: "button",
2950
+ children
2951
+ }
2952
+ );
2953
+ }
2954
+ function SeekBar({
2955
+ canSeek,
2956
+ className,
2957
+ commitMode,
2958
+ currentTimeSec,
2959
+ durationSec,
2960
+ label = "Seek",
2961
+ onSeek,
2962
+ status
2963
+ }) {
2964
+ const duration = durationSec !== null && Number.isFinite(durationSec) && durationSec > 0 ? durationSec : 0;
2965
+ const disabled = !canSeek || duration === 0 || status === "loading";
2966
+ const safeCurrentTime = clamp(currentTimeSec, 0, duration);
2967
+ const [previewTime, setPreviewTime] = useState(safeCurrentTime);
2968
+ const previewProgress = duration === 0 ? 0 : clamp(previewTime / duration, 0, 1) * 100;
2969
+ const isAdjusting = useRef(false);
2970
+ const lastCommitted = useRef(null);
2971
+ useEffect(() => {
2972
+ if (!isAdjusting.current) {
2973
+ setPreviewTime(safeCurrentTime);
2974
+ }
2975
+ }, [safeCurrentTime]);
2976
+ function updatePreview(value) {
2977
+ const nextValue = clamp(value, 0, duration);
2978
+ isAdjusting.current = true;
2979
+ setPreviewTime(nextValue);
2980
+ if (commitMode === "live") {
2981
+ onSeek(nextValue);
2982
+ lastCommitted.current = nextValue;
2983
+ }
2984
+ }
2985
+ function commitPreview(value = previewTime) {
2986
+ const nextValue = clamp(value, 0, duration);
2987
+ if (disabled || !isAdjusting.current || commitMode === "live" && lastCommitted.current === nextValue) {
2988
+ isAdjusting.current = false;
2989
+ return;
2990
+ }
2991
+ onSeek(nextValue);
2992
+ lastCommitted.current = nextValue;
2993
+ isAdjusting.current = false;
2994
+ }
2995
+ function cancelPreview() {
2996
+ isAdjusting.current = false;
2997
+ lastCommitted.current = null;
2998
+ setPreviewTime(safeCurrentTime);
2999
+ }
3000
+ return /* @__PURE__ */ jsxs(
3001
+ "label",
3002
+ {
3003
+ className: ["player-seek", className].filter(Boolean).join(" "),
3004
+ "data-disabled": disabled ? "true" : "false",
3005
+ children: [
3006
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
3007
+ /* @__PURE__ */ jsx(
3008
+ "span",
3009
+ {
3010
+ "aria-hidden": "true",
3011
+ className: "player-seek__progress",
3012
+ style: { width: `${String(disabled ? 0 : previewProgress)}%` }
3013
+ }
3014
+ ),
3015
+ /* @__PURE__ */ jsx(
3016
+ "input",
3017
+ {
3018
+ "aria-label": label,
3019
+ "aria-valuetext": formatTimeAria(previewTime),
3020
+ disabled,
3021
+ max: duration || 1,
3022
+ min: 0,
3023
+ onBlur: (event) => {
3024
+ commitPreview(Number(event.currentTarget.value));
3025
+ },
3026
+ onChange: (event) => {
3027
+ updatePreview(Number(event.currentTarget.value));
3028
+ },
3029
+ onKeyUp: (event) => {
3030
+ commitPreview(Number(event.currentTarget.value));
3031
+ },
3032
+ onPointerDown: () => {
3033
+ isAdjusting.current = true;
3034
+ },
3035
+ onPointerCancel: cancelPreview,
3036
+ onPointerUp: (event) => {
3037
+ commitPreview(Number(event.currentTarget.value));
3038
+ },
3039
+ step: 0.1,
3040
+ type: "range",
3041
+ value: disabled ? 0 : previewTime
3042
+ }
3043
+ )
3044
+ ]
3045
+ }
3046
+ );
3047
+ }
3048
+ var FALLBACK_PEAKS = [
3049
+ 0.16,
3050
+ 0.24,
3051
+ 0.42,
3052
+ 0.3,
3053
+ 0.58,
3054
+ 0.76,
3055
+ 0.48,
3056
+ 0.34,
3057
+ 0.66,
3058
+ 0.88,
3059
+ 0.62,
3060
+ 0.4,
3061
+ 0.52,
3062
+ 0.72,
3063
+ 0.44,
3064
+ 0.28,
3065
+ 0.56,
3066
+ 0.82,
3067
+ 0.68,
3068
+ 0.38,
3069
+ 0.24,
3070
+ 0.46,
3071
+ 0.32,
3072
+ 0.18
3073
+ ];
3074
+ var BAR_COUNT = 512;
3075
+ var VIEWBOX_WIDTH = BAR_COUNT;
3076
+ function createWaveformBars(peaks) {
3077
+ const source = peaks.length > 1 ? peaks : FALLBACK_PEAKS;
3078
+ return Array.from({ length: BAR_COUNT }, (_, index) => {
3079
+ const sourcePosition = index / Math.max(BAR_COUNT - 1, 1) * (source.length - 1);
3080
+ const leftIndex = Math.floor(sourcePosition);
3081
+ const rightIndex = Math.min(leftIndex + 1, source.length - 1);
3082
+ const mix = sourcePosition - leftIndex;
3083
+ const leftPeak = Math.abs(source[leftIndex] ?? 0);
3084
+ const rightPeak = Math.abs(source[rightIndex] ?? leftPeak);
3085
+ const interpolated = leftPeak + (rightPeak - leftPeak) * mix;
3086
+ if (source.length >= BAR_COUNT) {
3087
+ return clamp(Math.pow(interpolated, 1.18), 0.04, 1);
3088
+ }
3089
+ const noiseSeed = Math.sin((index + 1) * 12.9898 + source.length * 78.233) * 43758.5453;
3090
+ const noise = noiseSeed - Math.floor(noiseSeed);
3091
+ const fineDetail = 0.76 + noise * 0.34;
3092
+ const transient = noise > 0.975 ? 3.2 : noise > 0.91 ? 1.75 : 1;
3093
+ return clamp(
3094
+ Math.pow(interpolated, 1.22) * fineDetail * transient,
3095
+ 0.04,
3096
+ 1
3097
+ );
3098
+ });
3099
+ }
3100
+ function Waveform({
3101
+ className,
3102
+ opacity = 1,
3103
+ playedStateEnabled = true,
3104
+ progress = 0,
3105
+ waveform
3106
+ }) {
3107
+ const clipId = useId().replaceAll(":", "");
3108
+ const bars = createWaveformBars(waveform?.peaks ?? FALLBACK_PEAKS);
3109
+ const progressWidth = clamp(progress, 0, 1) * VIEWBOX_WIDTH;
3110
+ const classes = ["player-waveform", className].filter(Boolean).join(" ");
3111
+ const barPath = bars.map((peak, index) => {
3112
+ const halfHeight = peak * 47;
3113
+ const x = index + 0.5;
3114
+ return `M ${String(x)} ${String(50 - halfHeight)} V ${String(
3115
+ 50 + halfHeight
3116
+ )}`;
3117
+ }).join(" ");
3118
+ return /* @__PURE__ */ jsxs(
3119
+ "svg",
3120
+ {
3121
+ "aria-hidden": "true",
3122
+ className: classes,
3123
+ focusable: "false",
3124
+ preserveAspectRatio: "none",
3125
+ style: { opacity: clamp(opacity, 0, 1), pointerEvents: "none" },
3126
+ viewBox: `0 0 ${String(VIEWBOX_WIDTH)} 100`,
3127
+ children: [
3128
+ /* @__PURE__ */ jsx("path", { className: "player-waveform__base", d: barPath }),
3129
+ playedStateEnabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
3130
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { width: progressWidth, height: "100" }) }),
3131
+ /* @__PURE__ */ jsx(
3132
+ "path",
3133
+ {
3134
+ className: "player-waveform__played",
3135
+ clipPath: `url(#${clipId})`,
3136
+ d: barPath
3137
+ }
3138
+ )
3139
+ ] }) : null
3140
+ ]
3141
+ }
3142
+ );
3143
+ }
3144
+ function BoundCompactPlayerView({
3145
+ appearance,
3146
+ className,
3147
+ commands,
3148
+ contextId,
3149
+ contextMode,
3150
+ options,
3151
+ runtimeStatus,
3152
+ state
3153
+ }) {
3154
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
3155
+ return /* @__PURE__ */ jsx(
3156
+ PlayerSurface,
3157
+ {
3158
+ appearance,
3159
+ className: ["player-bound-compact", className].filter(Boolean).join(" "),
3160
+ label: `Inline player: ${state.item.title}`,
3161
+ children: /* @__PURE__ */ jsxs(
3162
+ "div",
3163
+ {
3164
+ className: "player-bound-compact__main",
3165
+ "data-active": state.isActive ? "true" : "false",
3166
+ "data-playback-context-id": contextId,
3167
+ "data-playback-context-mode": contextMode,
3168
+ "data-player-runtime-status": runtimeStatus,
3169
+ "data-queue-item-id": state.queueItemId,
3170
+ children: [
3171
+ /* @__PURE__ */ jsx(
3172
+ IconButton,
3173
+ {
3174
+ className: "player-play-button",
3175
+ label: state.isPlaying ? "Pause" : "Play",
3176
+ onClick: () => {
3177
+ void commands.togglePlayback();
3178
+ },
3179
+ children: state.isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
3180
+ }
3181
+ ),
3182
+ /* @__PURE__ */ jsx(
3183
+ Artwork,
3184
+ {
3185
+ className: "player-bound-compact__artwork",
3186
+ item: state.item
3187
+ }
3188
+ ),
3189
+ /* @__PURE__ */ jsx(TrackMetadata, { item: state.item, scrollTitle: state.isPlaying }),
3190
+ /* @__PURE__ */ jsxs("div", { className: "player-bound-compact__signal", children: [
3191
+ /* @__PURE__ */ jsx(
3192
+ Waveform,
3193
+ {
3194
+ opacity: options.waveformOpacity,
3195
+ playedStateEnabled: options.waveformPlayedStateEnabled,
3196
+ progress,
3197
+ waveform: state.item.waveform
3198
+ }
3199
+ ),
3200
+ /* @__PURE__ */ jsx(
3201
+ TimeReadout,
3202
+ {
3203
+ currentTimeSec: state.currentTimeSec,
3204
+ durationSec: state.durationSec
3205
+ }
3206
+ )
3207
+ ] }),
3208
+ /* @__PURE__ */ jsx(
3209
+ SeekBar,
3210
+ {
3211
+ canSeek: state.canSeek,
3212
+ commitMode: options.seekCommitMode,
3213
+ currentTimeSec: state.currentTimeSec,
3214
+ durationSec: state.durationSec,
3215
+ label: `Seek in ${state.item.title}`,
3216
+ onSeek: (seconds) => {
3217
+ commands.seekTo(seconds);
3218
+ },
3219
+ status: state.status
3220
+ }
3221
+ )
3222
+ ]
3223
+ }
3224
+ )
3225
+ }
3226
+ );
3227
+ }
3228
+ function BoundCompactPlayer({
3229
+ appearance,
3230
+ className,
3231
+ context,
3232
+ queueItem
3233
+ }) {
3234
+ const bound = useBoundPlayer({ context, queueItem });
3235
+ const player = usePlayer();
3236
+ return /* @__PURE__ */ jsx(
3237
+ BoundCompactPlayerView,
3238
+ {
3239
+ appearance,
3240
+ ...className === void 0 ? {} : { className },
3241
+ commands: bound.commands,
3242
+ contextId: context.id,
3243
+ contextMode: context.mode,
3244
+ runtimeStatus: player.runtimeStatus,
3245
+ options: {
3246
+ seekCommitMode: player.state.experienceConfig.seekCommitMode,
3247
+ waveformOpacity: player.state.experienceConfig.waveformOpacity,
3248
+ waveformPlayedStateEnabled: player.state.experienceConfig.waveformPlayedStateEnabled
3249
+ },
3250
+ state: bound.state
3251
+ }
3252
+ );
3253
+ }
2571
3254
 
2572
- export { MiniPlayerHost, PlayerProvider, deriveBoundPlayerViewState, useBoundPlayer, usePlayer };
3255
+ export { BoundCompactPlayer, MiniPlayerHost, PlayerProvider, deriveBoundPlayerViewState, useBoundPlayer, usePlayer };
2573
3256
  //# sourceMappingURL=index.js.map
2574
3257
  //# sourceMappingURL=index.js.map