@djangocfg/ui-tools 2.1.310 → 2.1.313
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/README.md +38 -22
- package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
- package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
- package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
- package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
- package/dist/Player-M3GC3VPE.mjs +4 -0
- package/dist/Player-M3GC3VPE.mjs.map +1 -0
- package/dist/Player-ZGQKKOWI.css +65 -0
- package/dist/Player-ZGQKKOWI.css.map +1 -0
- package/dist/Player-ZL2X5LGG.cjs +13 -0
- package/dist/Player-ZL2X5LGG.cjs.map +1 -0
- package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
- package/dist/chunk-DFTVB66S.cjs.map +1 -0
- package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
- package/dist/chunk-EUADAUBQ.mjs.map +1 -0
- package/dist/chunk-FX2QFYWF.mjs +2059 -0
- package/dist/chunk-FX2QFYWF.mjs.map +1 -0
- package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
- package/dist/chunk-GBLQTHWT.mjs.map +1 -0
- package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
- package/dist/chunk-S44PW6NK.cjs.map +1 -0
- package/dist/chunk-ZLQHUZDU.cjs +2061 -0
- package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
- package/dist/components-WYEZL5TE.cjs +26 -0
- package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
- package/dist/components-ZAGG2PBO.mjs +5 -0
- package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
- package/dist/index.cjs +36 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +65 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +44 -500
- package/dist/index.d.ts +44 -500
- package/dist/index.mjs +16 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
- package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
- package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
- package/src/components/markdown/MarkdownMessage/README.md +72 -0
- package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
- package/src/components/markdown/MarkdownMessage/index.ts +6 -0
- package/src/index.ts +2 -11
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
- package/src/tools/AudioPlayer/Player.tsx +80 -0
- package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
- package/src/tools/AudioPlayer/README.md +139 -204
- package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
- package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
- package/src/tools/AudioPlayer/audio/index.ts +4 -0
- package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
- package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
- package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
- package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
- package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -15
- package/src/tools/AudioPlayer/context/selectors.ts +36 -0
- package/src/tools/AudioPlayer/hooks/index.ts +12 -39
- package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
- package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
- package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
- package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
- package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
- package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
- package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
- package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
- package/src/tools/AudioPlayer/index.ts +63 -134
- package/src/tools/AudioPlayer/lazy.tsx +8 -97
- package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
- package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
- package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
- package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
- package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
- package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
- package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
- package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
- package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
- package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
- package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
- package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
- package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
- package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
- package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
- package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
- package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
- package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
- package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
- package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
- package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
- package/src/tools/AudioPlayer/parts/index.ts +1 -0
- package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
- package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
- package/src/tools/AudioPlayer/store/index.ts +16 -0
- package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
- package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
- package/src/tools/AudioPlayer/types.ts +95 -0
- package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
- package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
- package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
- package/src/tools/AudioPlayer/utils/index.ts +4 -5
- package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
- package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
- package/src/tools/index.ts +5 -75
- package/dist/chunk-2QY3LJR6.mjs.map +0 -1
- package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
- package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
- package/dist/chunk-F2N7P5XU.cjs +0 -30
- package/dist/chunk-F2N7P5XU.cjs.map +0 -1
- package/dist/chunk-FYLR232K.cjs.map +0 -1
- package/dist/chunk-HMHIVEMS.mjs +0 -1619
- package/dist/chunk-HMHIVEMS.mjs.map +0 -1
- package/dist/chunk-JWB2EWQO.mjs +0 -5
- package/dist/chunk-JWB2EWQO.mjs.map +0 -1
- package/dist/chunk-YZX6FH3H.cjs +0 -1656
- package/dist/chunk-YZX6FH3H.cjs.map +0 -1
- package/dist/components-3RTH76CV.cjs +0 -27
- package/dist/components-5GVVL2Q6.mjs +0 -5
- package/dist/components-CPHOUQ5F.cjs +0 -46
- package/dist/components-CPHOUQ5F.cjs.map +0 -1
- package/dist/components-OTK43IMD.mjs +0 -6
- package/dist/components-OTK43IMD.mjs.map +0 -1
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
- package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
- package/src/tools/AudioPlayer/components/index.ts +0 -23
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
- package/src/tools/AudioPlayer/effects/index.ts +0 -412
- package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
- package/src/tools/AudioPlayer/types/effects.ts +0 -73
- package/src/tools/AudioPlayer/types/index.ts +0 -27
- package/src/tools/AudioPlayer/utils/debug.ts +0 -14
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Inner shell that runs inside <PlayerProvider>. Owns variant selection,
|
|
4
|
+
// keyboard shortcuts and MediaSession wiring; renders the picked layout.
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
|
7
|
+
import { TooltipProvider } from '@djangocfg/ui-core/components';
|
|
8
|
+
import { useIsPhone } from '@djangocfg/ui-core/hooks';
|
|
9
|
+
import { usePlayerAudio, usePlayerControls, usePlayerMeta } from './context/selectors';
|
|
10
|
+
import { useElementWidth } from './hooks/useResizeObserver';
|
|
11
|
+
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
|
12
|
+
import { useMediaSession } from './hooks/useMediaSession';
|
|
13
|
+
import { CompactLayout, DefaultLayout } from './parts/Layout';
|
|
14
|
+
import type { PlayerHandle, PlayerProps, PlayerVariant } from './types';
|
|
15
|
+
|
|
16
|
+
const COMPACT_BREAKPOINT = 480;
|
|
17
|
+
|
|
18
|
+
type Props = Pick<
|
|
19
|
+
PlayerProps,
|
|
20
|
+
| 'className'
|
|
21
|
+
| 'variant'
|
|
22
|
+
| 'waveform'
|
|
23
|
+
| 'reactiveCover'
|
|
24
|
+
| 'onPrev'
|
|
25
|
+
| 'onNext'
|
|
26
|
+
| 'enableKeyboardShortcuts'
|
|
27
|
+
| 'ariaLabel'
|
|
28
|
+
| 'seekStartsPlayback'
|
|
29
|
+
> & {
|
|
30
|
+
handleRef?: React.Ref<PlayerHandle>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function PlayerShell({
|
|
34
|
+
className = '',
|
|
35
|
+
variant = 'auto',
|
|
36
|
+
waveform,
|
|
37
|
+
reactiveCover = false,
|
|
38
|
+
onPrev,
|
|
39
|
+
onNext,
|
|
40
|
+
enableKeyboardShortcuts = true,
|
|
41
|
+
ariaLabel,
|
|
42
|
+
seekStartsPlayback = true,
|
|
43
|
+
handleRef,
|
|
44
|
+
}: Props) {
|
|
45
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
46
|
+
const audio = usePlayerAudio();
|
|
47
|
+
const controls = usePlayerControls();
|
|
48
|
+
const meta = usePlayerMeta();
|
|
49
|
+
const width = useElementWidth(container);
|
|
50
|
+
const isPhone = useIsPhone();
|
|
51
|
+
|
|
52
|
+
// `auto` resolves to the layout best for the available space:
|
|
53
|
+
// - on a phone viewport we always use compact (regardless of container);
|
|
54
|
+
// - otherwise compact only when the container itself is narrower than
|
|
55
|
+
// COMPACT_BREAKPOINT.
|
|
56
|
+
const resolvedVariant: PlayerVariant =
|
|
57
|
+
variant === 'auto'
|
|
58
|
+
? isPhone || (width > 0 && width < COMPACT_BREAKPOINT)
|
|
59
|
+
? 'compact'
|
|
60
|
+
: 'default'
|
|
61
|
+
: variant;
|
|
62
|
+
|
|
63
|
+
useMediaSession(audio, meta, controls, onPrev, onNext);
|
|
64
|
+
const hotkeys = useKeyboardShortcuts({
|
|
65
|
+
audio,
|
|
66
|
+
controls,
|
|
67
|
+
enabled: enableKeyboardShortcuts,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Compose the container state-ref with the hotkey scoping ref. Both want
|
|
71
|
+
// to see the same DOM node — one for layout/keyboard-focus management, the
|
|
72
|
+
// other for react-hotkeys-hook's scoped listener.
|
|
73
|
+
const setRootRef = useCallback(
|
|
74
|
+
(node: HTMLDivElement | null) => {
|
|
75
|
+
setContainer(node);
|
|
76
|
+
hotkeys.ref(node);
|
|
77
|
+
},
|
|
78
|
+
[hotkeys],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
useImperativeHandle(
|
|
82
|
+
handleRef,
|
|
83
|
+
(): PlayerHandle => ({
|
|
84
|
+
audio,
|
|
85
|
+
play: () => controls.play(),
|
|
86
|
+
pause: () => controls.pause(),
|
|
87
|
+
seek: (s: number) => controls.seek(s),
|
|
88
|
+
getCurrentTime: () => audio.currentTime,
|
|
89
|
+
getDuration: () => (Number.isFinite(audio.duration) ? audio.duration : 0),
|
|
90
|
+
}),
|
|
91
|
+
[audio, controls],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Keyboard shortcuts work only when the container can take focus.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!container || container.hasAttribute('tabindex')) return;
|
|
97
|
+
container.setAttribute('tabindex', '0');
|
|
98
|
+
}, [container]);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<TooltipProvider delayDuration={400}>
|
|
102
|
+
<div
|
|
103
|
+
ref={setRootRef}
|
|
104
|
+
role="group"
|
|
105
|
+
aria-label={ariaLabel ?? 'Audio player'}
|
|
106
|
+
className={`audioplayer @container/player rounded-lg border border-border/60 bg-card text-foreground shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ${className}`}
|
|
107
|
+
>
|
|
108
|
+
{resolvedVariant === 'compact' ? (
|
|
109
|
+
<CompactLayout waveform={waveform} seekStartsPlayback={seekStartsPlayback} />
|
|
110
|
+
) : (
|
|
111
|
+
<DefaultLayout
|
|
112
|
+
waveform={waveform}
|
|
113
|
+
reactiveCover={reactiveCover}
|
|
114
|
+
onPrev={onPrev}
|
|
115
|
+
onNext={onNext}
|
|
116
|
+
seekStartsPlayback={seekStartsPlayback}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</TooltipProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -1,243 +1,178 @@
|
|
|
1
|
-
# AudioPlayer
|
|
1
|
+
# AudioPlayer (v6)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WebView-safe React audio player. One component, two layouts, waveform-as-progress.
|
|
4
|
+
Tuned for 60 fps inside WKWebView (Wails / Electron) and Tauri WebView2.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Architecture, ADRs and the visual direction live at
|
|
7
|
+
`packages/ui-tools/@dev/@refactoring6-audioplayer/`.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
- Web Audio API for real-time frequency analysis
|
|
9
|
-
- Audio-reactive cover effects (glow, orbs, spotlight, mesh)
|
|
10
|
-
- Frequency visualization waveform
|
|
11
|
-
- Full playback controls
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
9
|
+
## Quick start
|
|
14
10
|
|
|
15
11
|
```tsx
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// Simple usage
|
|
19
|
-
<HybridSimplePlayer src="https://example.com/audio.mp3" />
|
|
20
|
-
|
|
21
|
-
// Compact single-row player (for lists, sidebars)
|
|
22
|
-
<HybridCompactPlayer src="https://example.com/audio.mp3" title="Rain & Thunder" />
|
|
23
|
-
|
|
24
|
-
// With metadata and reactive cover
|
|
25
|
-
<HybridSimplePlayer
|
|
26
|
-
src={audioUrl}
|
|
27
|
-
title="Track Title"
|
|
28
|
-
artist="Artist Name"
|
|
29
|
-
coverArt="/path/to/cover.jpg"
|
|
30
|
-
reactiveCover
|
|
31
|
-
variant="spotlight"
|
|
32
|
-
/>
|
|
12
|
+
import { LazyAudioPlayer } from '@djangocfg/ui-tools';
|
|
33
13
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
showWaveform
|
|
40
|
-
waveformMode="frequency" // 'frequency' | 'static'
|
|
41
|
-
showLoop
|
|
42
|
-
reactiveCover
|
|
43
|
-
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh' | 'none'
|
|
44
|
-
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
45
|
-
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
46
|
-
layout="horizontal" // 'vertical' | 'horizontal'
|
|
14
|
+
<LazyAudioPlayer
|
|
15
|
+
src="/audio/track.mp3"
|
|
16
|
+
title="Track"
|
|
17
|
+
artist="Artist"
|
|
18
|
+
cover="/cover.jpg"
|
|
47
19
|
/>
|
|
48
20
|
```
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| Prop | Type | Default | Description |
|
|
53
|
-
|------|------|---------|-------------|
|
|
54
|
-
| `src` | `string` | required | Audio URL |
|
|
55
|
-
| `title` | `string` | - | Track title |
|
|
56
|
-
| `artist` | `string` | - | Artist name |
|
|
57
|
-
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
58
|
-
| `coverSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cover art size |
|
|
59
|
-
| `showWaveform` | `boolean` | `true` | Show frequency visualization |
|
|
60
|
-
| `waveformMode` | `'frequency' \| 'static'` | `'frequency'` | Visualization mode |
|
|
61
|
-
| `waveformHeight` | `number` | `64` | Waveform height in pixels |
|
|
62
|
-
| `showTimer` | `boolean` | `true` | Show time display |
|
|
63
|
-
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
64
|
-
| `showLoop` | `boolean` | `true` | Show loop button |
|
|
65
|
-
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
66
|
-
| `variant` | `VisualizationVariant` | `'spotlight'` | Effect variant |
|
|
67
|
-
| `intensity` | `EffectIntensity` | `'medium'` | Effect intensity |
|
|
68
|
-
| `colorScheme` | `EffectColorScheme` | `'primary'` | Effect colors |
|
|
69
|
-
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
70
|
-
| `loop` | `boolean` | `false` | Loop playback |
|
|
71
|
-
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
72
|
-
|
|
73
|
-
## Advanced Usage
|
|
74
|
-
|
|
75
|
-
### HybridAudioProvider + HybridAudioPlayer
|
|
76
|
-
|
|
77
|
-
For custom layouts:
|
|
22
|
+
For non-lazy use (e.g. above-the-fold):
|
|
78
23
|
|
|
79
24
|
```tsx
|
|
80
|
-
import {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
AudioReactiveCover,
|
|
84
|
-
useHybridAudioContext
|
|
85
|
-
} from '@djangocfg/ui-nextjs';
|
|
86
|
-
|
|
87
|
-
function MyPlayer({ audioUrl }: { audioUrl: string }) {
|
|
88
|
-
return (
|
|
89
|
-
<HybridAudioProvider src={audioUrl}>
|
|
90
|
-
<AudioReactiveCover variant="spotlight" onClick={handleClick}>
|
|
91
|
-
<img src={coverUrl} alt="Cover" />
|
|
92
|
-
</AudioReactiveCover>
|
|
93
|
-
<HybridAudioPlayer showWaveform showControls />
|
|
94
|
-
<CustomControls />
|
|
95
|
-
</HybridAudioProvider>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function CustomControls() {
|
|
100
|
-
const { state, controls, audioLevels } = useHybridAudioContext();
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div>
|
|
104
|
-
<p>Bass level: {(audioLevels.bass * 100).toFixed(0)}%</p>
|
|
105
|
-
<button onClick={controls.togglePlay}>
|
|
106
|
-
{state.isPlaying ? 'Pause' : 'Play'}
|
|
107
|
-
</button>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
25
|
+
import { Player } from '@djangocfg/ui-tools/tools/AudioPlayer';
|
|
26
|
+
|
|
27
|
+
<Player src="/audio/track.mp3" />
|
|
111
28
|
```
|
|
112
29
|
|
|
113
|
-
##
|
|
30
|
+
## Why v6
|
|
114
31
|
|
|
115
|
-
|
|
32
|
+
The previous Hybrid* surface (now `AudioPlayer_old/`) used a per-frame
|
|
33
|
+
`AnalyserNode` redraw, multiple effect components and a single Context that
|
|
34
|
+
re-rendered every consumer at 60 Hz. v6 fixes the structural issues:
|
|
116
35
|
|
|
117
|
-
|
|
36
|
+
- Static peaks decoded **once**, painted **twice** (background + foreground),
|
|
37
|
+
playhead animated via CSS `clip-path` + a single CSS variable. Zero canvas
|
|
38
|
+
paints during steady-state playback.
|
|
39
|
+
- Three split contexts (State / Controls / Levels) — controls memoised once,
|
|
40
|
+
levels exposed as an imperative store; React doesn't re-render on level
|
|
41
|
+
updates at all.
|
|
42
|
+
- One module-level `AudioContext`, `MediaElementSourceNode` cached in a
|
|
43
|
+
`WeakMap` per `<audio>` — Safari context-quota safe and StrictMode safe.
|
|
118
44
|
|
|
119
|
-
|
|
120
|
-
const {
|
|
121
|
-
state, // { isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping }
|
|
122
|
-
controls, // { play, pause, togglePlay, seek, skip, setVolume, toggleMute, toggleLoop }
|
|
123
|
-
audioLevels, // { bass, mid, high, overall }
|
|
124
|
-
webAudio, // { context, analyser, sourceNode }
|
|
125
|
-
audioRef, // React ref to HTMLAudioElement
|
|
126
|
-
} = useHybridAudioContext();
|
|
127
|
-
```
|
|
45
|
+
## Props
|
|
128
46
|
|
|
129
|
-
|
|
47
|
+
| Prop | Type | Default | Notes |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `src` | `string` | required | |
|
|
50
|
+
| `peaks` | `Float32Array` | — | Pre-computed peaks (skips client decode). |
|
|
51
|
+
| `title` / `artist` / `album` / `cover` | `string` | — | Drive `<MediaSession>` too. |
|
|
52
|
+
| `variant` | `'auto' \| 'default' \| 'compact'` | `'auto'` | `auto` → compact on phones / narrow containers. |
|
|
53
|
+
| `waveform.mode` | `'peaks' \| 'live' \| 'bars' \| 'progress' \| 'none'` | `'peaks'` | `progress` is a plain scrubber, no animation. `peaks` falls back to `progress` on decode failure. |
|
|
54
|
+
| `waveform.height` | `number` | 40 (default) / 24 (compact) | |
|
|
55
|
+
| `reactiveCover` | `false \| 'subtle'` | `false` | Compositor-only scale pulse. |
|
|
56
|
+
| `exclusive` | `boolean` | `true` | Pauses sibling players via `activePlayerBus` (cross-tab via `BroadcastChannel`). |
|
|
57
|
+
| `seekStartsPlayback` | `boolean` | `true` | Click on the waveform also starts playback when paused. |
|
|
58
|
+
| `autoplay` / `loop` / `muted` / `initialVolume` / `preload` | — | — | Standard HTML media. |
|
|
59
|
+
| `onPrev` / `onNext` | `() => void` | — | Render skip buttons + `MediaSession` handlers. |
|
|
60
|
+
| `onPlay` / `onPause` / `onEnded` / `onError` | callbacks | — | |
|
|
61
|
+
| `enableKeyboardShortcuts` | `boolean` | `true` | Active only when focus is inside the player. |
|
|
62
|
+
| `ariaLabel` / `className` | `string` | — | |
|
|
63
|
+
|
|
64
|
+
## Controls
|
|
65
|
+
|
|
66
|
+
- **Mouse / touch** — click the waveform to seek (and start playback if
|
|
67
|
+
paused); drag to scrub. Hover shows a time tooltip (desktop only).
|
|
68
|
+
- **Keyboard** — Space/K play-pause, ←/→ seek 5 s, ↑/↓ volume, M mute, L loop.
|
|
69
|
+
- **MediaSession** — wires play / pause / next / prev / seek to OS-level Now
|
|
70
|
+
Playing controls (works in Wails through WKWebView).
|
|
71
|
+
|
|
72
|
+
## Mobile
|
|
73
|
+
|
|
74
|
+
- `variant: 'auto'` resolves to `compact` on phone viewports (< 640 px).
|
|
75
|
+
- Volume popover opens by tap on touch (no hover); hover affordances are
|
|
76
|
+
hidden via `@media (hover: none)`.
|
|
77
|
+
- iOS Safari: the volume slider is hidden (the OS controls hardware volume
|
|
78
|
+
there — `audio.volume` is read-only); mute toggle stays.
|
|
79
|
+
- Tap targets ≥ 40 px on coarse pointers.
|
|
80
|
+
|
|
81
|
+
## Persistent preferences
|
|
82
|
+
|
|
83
|
+
`volume` and `muted` are stored in `localStorage` (key
|
|
84
|
+
`djangocfg-audioplayer:prefs`) and synced across all uncontrolled players in
|
|
85
|
+
the page and across tabs (`storage` event). Pass `initialVolume` / `muted` to
|
|
86
|
+
opt a specific player out — it then ignores the store and never writes to it.
|
|
87
|
+
|
|
88
|
+
## Active-player coordination
|
|
89
|
+
|
|
90
|
+
`exclusive` (default `true`) registers each player with a small in-memory bus.
|
|
91
|
+
Only one player plays at a time per page; `BroadcastChannel` extends the same
|
|
92
|
+
rule across tabs. Hooks for the bus state:
|
|
130
93
|
|
|
131
94
|
```tsx
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Audio levels for reactive effects
|
|
139
|
-
const { bass, mid, high, overall } = useHybridAudioLevels();
|
|
140
|
-
|
|
141
|
-
// Web Audio API access
|
|
142
|
-
const { context, analyser, sourceNode } = useHybridWebAudio();
|
|
95
|
+
import {
|
|
96
|
+
useActivePlayer, // currently playing id (or null)
|
|
97
|
+
useLastActivePlayer, // most recently active id (sticky)
|
|
98
|
+
useIsActivePlayer, // boolean for a specific id
|
|
99
|
+
} from '@djangocfg/ui-tools';
|
|
143
100
|
```
|
|
144
101
|
|
|
145
|
-
##
|
|
146
|
-
|
|
147
|
-
### AudioReactiveCover
|
|
102
|
+
## Custom layouts (slot composition)
|
|
148
103
|
|
|
149
|
-
|
|
104
|
+
Drop `<PlayerProvider>` and arrange the parts you want yourself. Every part
|
|
105
|
+
reads from the player context — pass nothing, just compose.
|
|
150
106
|
|
|
151
107
|
```tsx
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
108
|
+
import {
|
|
109
|
+
PlayerProvider,
|
|
110
|
+
Cover, Title, Artist, TimeDisplay,
|
|
111
|
+
PlayButton, VolumeControl, LoopButton,
|
|
112
|
+
Waveform,
|
|
113
|
+
} from '@djangocfg/ui-tools';
|
|
114
|
+
|
|
115
|
+
<PlayerProvider src="/track.mp3" title="…" artist="…" cover="/cover.jpg">
|
|
116
|
+
<div className="grid grid-cols-[96px_1fr_auto] gap-4 rounded-lg border bg-card p-4">
|
|
117
|
+
<div className="row-span-2"><Cover size={96} /></div>
|
|
118
|
+
<div className="min-w-0"><Title /><Artist /></div>
|
|
119
|
+
<TimeDisplay />
|
|
120
|
+
<div className="col-span-2 space-y-3">
|
|
121
|
+
<Waveform height={48} />
|
|
122
|
+
<div className="flex items-center justify-between">
|
|
123
|
+
<PlayButton />
|
|
124
|
+
<div className="flex items-center gap-1">
|
|
125
|
+
<VolumeControl />
|
|
126
|
+
<LoopButton />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</PlayerProvider>
|
|
160
132
|
```
|
|
161
133
|
|
|
162
|
-
|
|
134
|
+
> **Note:** the slot-composed players use Radix `Tooltip` for control labels.
|
|
135
|
+
> The default `<Player>` ships its own `TooltipProvider`, but slot composition
|
|
136
|
+
> bypasses it — wrap your custom layout in
|
|
137
|
+
> `<TooltipProvider>` (from `@djangocfg/ui-core`) once at the app root, or
|
|
138
|
+
> tooltips will throw `"Tooltip must be used within TooltipProvider"`.
|
|
163
139
|
|
|
164
|
-
|
|
140
|
+
Re-exported parts: `Cover`, `CoverPlaceholder`, `ReactivePulse`, `Title`,
|
|
141
|
+
`Artist`, `TimeDisplay`, `PlayButton`, `SkipButton`, `VolumeControl`,
|
|
142
|
+
`LoopButton`, `ControlsRow`, `IconButton`, `Waveform`, `PeaksWaveform`,
|
|
143
|
+
`LiveWaveform`, `BarsWaveform`, `ProgressBar`, `WaveformSkeleton`,
|
|
144
|
+
`ErrorState`, `DefaultLayout`, `CompactLayout`.
|
|
165
145
|
|
|
166
|
-
|
|
167
|
-
<HybridCompactPlayer
|
|
168
|
-
src={audioUrl}
|
|
169
|
-
title="Track name" // used as tooltip
|
|
170
|
-
waveformMode="frequency" // 'frequency' | 'static'
|
|
171
|
-
showTimer={true}
|
|
172
|
-
autoPlay={false}
|
|
173
|
-
/>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
| Prop | Type | Default | Description |
|
|
177
|
-
|------|------|---------|-------------|
|
|
178
|
-
| `src` | `string` | required | Audio URL |
|
|
179
|
-
| `title` | `string` | - | Tooltip / aria-label |
|
|
180
|
-
| `waveformMode` | `'frequency' \| 'static'` | `'frequency'` | Visualization mode |
|
|
181
|
-
| `showTimer` | `boolean` | `true` | Show current/total time |
|
|
182
|
-
| `buttonSize` | `'sm' \| 'md'` | `'md'` | Play button size (`sm` = h-6 w-6, `md` = h-8 w-8) |
|
|
183
|
-
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
184
|
-
| `loop` | `boolean` | `false` | Loop playback |
|
|
185
|
-
| `initialVolume` | `number` | `1` | Initial volume 0-1 |
|
|
146
|
+
## Selector hooks
|
|
186
147
|
|
|
187
|
-
For
|
|
148
|
+
For custom toolbars — read state without re-implementing the controls UI:
|
|
188
149
|
|
|
189
150
|
```tsx
|
|
190
|
-
import {
|
|
191
|
-
|
|
192
|
-
|
|
151
|
+
import {
|
|
152
|
+
usePlayerState, // discriminated union: idle | loading | playing | paused | …
|
|
153
|
+
usePlayerControls, // play / pause / toggle / seek / setVolume / …
|
|
154
|
+
usePlayerLevels, // imperative store; for live-mode canvases
|
|
155
|
+
usePlayerMeta, // src / title / artist / cover
|
|
156
|
+
usePlayerPaused, // boolean shortcut
|
|
157
|
+
usePlayerDuration, // number shortcut
|
|
158
|
+
usePlayerPreferences, // { volume, muted } from the persistent store
|
|
159
|
+
} from '@djangocfg/ui-tools';
|
|
193
160
|
```
|
|
194
161
|
|
|
195
|
-
|
|
162
|
+
These work inside `<Player>`'s tree; if you need your own JSX around the
|
|
163
|
+
state, mount `<PlayerProvider>` directly (also exported).
|
|
196
164
|
|
|
197
|
-
|
|
165
|
+
## Stories
|
|
198
166
|
|
|
199
|
-
|
|
200
|
-
<HybridWaveform
|
|
201
|
-
mode="frequency" // 'frequency' | 'static'
|
|
202
|
-
height={64}
|
|
203
|
-
barWidth={3}
|
|
204
|
-
barGap={2}
|
|
205
|
-
/>
|
|
206
|
-
```
|
|
167
|
+
Storybook-style stories live in `AudioPlayer.story.tsx`:
|
|
207
168
|
|
|
208
|
-
|
|
169
|
+
- `Default`, `WithCover`, `Compact`, `CustomLayout`, `Bars`, `Live`,
|
|
170
|
+
`NoWaveform`, `ReactiveCover`, `ErrorState`, `Exclusive`, `Interactive`,
|
|
171
|
+
`Showcase` (one-page demo of every prop / mode / hook).
|
|
209
172
|
|
|
210
|
-
|
|
211
|
-
|---------|-------------|
|
|
212
|
-
| `spotlight` | Rotating conic gradient with bass pulse |
|
|
213
|
-
| `glow` | Multi-layered radial glows from edges |
|
|
214
|
-
| `orbs` | Floating orbs that react to frequencies |
|
|
215
|
-
| `mesh` | Large gradient blobs with movement |
|
|
216
|
-
| `none` | Effects disabled |
|
|
173
|
+
Run `pnpm playground` from `packages/ui-tools/`.
|
|
217
174
|
|
|
218
|
-
##
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
AudioPlayer/
|
|
222
|
-
├── index.ts # Public API exports
|
|
223
|
-
├── types/ # TypeScript types
|
|
224
|
-
├── hooks/
|
|
225
|
-
│ ├── useHybridAudio.ts # HTML5 audio + Web Audio hook
|
|
226
|
-
│ ├── useHybridAudioAnalysis.ts # Frequency analysis
|
|
227
|
-
│ └── useVisualization.tsx # Visualization settings
|
|
228
|
-
├── context/
|
|
229
|
-
│ └── HybridAudioProvider.tsx # Audio context provider
|
|
230
|
-
├── components/
|
|
231
|
-
│ ├── HybridAudioPlayer.tsx # Main player component
|
|
232
|
-
│ ├── HybridSimplePlayer.tsx # All-in-one wrapper (with cover, volume, effects)
|
|
233
|
-
│ ├── HybridCompactPlayer.tsx # Compact single-row player
|
|
234
|
-
│ ├── HybridWaveform.tsx # Frequency visualization
|
|
235
|
-
│ └── ReactiveCover/ # Reactive effects
|
|
236
|
-
├── effects/ # Effect calculations
|
|
237
|
-
└── utils/ # Utilities
|
|
238
|
-
```
|
|
175
|
+
## Related
|
|
239
176
|
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
- Web Audio API AnalyserNode for visualization only (not connected to output)
|
|
243
|
-
- Audio graph: `source → destination` + `source → analyser` (passive)
|
|
177
|
+
- Old (reference only): `tools/AudioPlayer_old/`.
|
|
178
|
+
- Architecture: `@dev/@refactoring6-audioplayer/`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Singleton AudioContext for the whole tab.
|
|
2
|
+
// See ADR-004. Never closed during normal operation.
|
|
3
|
+
|
|
4
|
+
let _ctx: AudioContext | null = null;
|
|
5
|
+
|
|
6
|
+
type WebkitWindow = typeof window & {
|
|
7
|
+
webkitAudioContext?: typeof AudioContext;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function getAudioContext(): AudioContext {
|
|
11
|
+
if (_ctx) return _ctx;
|
|
12
|
+
const w = window as WebkitWindow;
|
|
13
|
+
const Ctor = w.AudioContext ?? w.webkitAudioContext;
|
|
14
|
+
if (!Ctor) {
|
|
15
|
+
throw new Error('Web Audio API is not supported in this environment');
|
|
16
|
+
}
|
|
17
|
+
_ctx = new Ctor({ latencyHint: 'interactive' });
|
|
18
|
+
return _ctx;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function unlockAudioContext(): Promise<void> {
|
|
22
|
+
const ctx = getAudioContext();
|
|
23
|
+
if (ctx.state === 'suspended') {
|
|
24
|
+
try {
|
|
25
|
+
await ctx.resume();
|
|
26
|
+
} catch {
|
|
27
|
+
// Resume requires a user gesture; caller should retry from one.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isAudioContextRunning(): boolean {
|
|
33
|
+
return _ctx?.state === 'running';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Test-only: drop the singleton between test cases.
|
|
37
|
+
export function _resetAudioContextForTesting(): void {
|
|
38
|
+
_ctx = null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// fetch + OfflineAudioContext + bucketize. See ADR-002 / research 02.
|
|
2
|
+
// Uses a low-sample-rate mono context to keep memory bounded.
|
|
3
|
+
|
|
4
|
+
import { bucketize } from '../utils/bucketize';
|
|
5
|
+
|
|
6
|
+
const SAMPLE_RATE = 22_050;
|
|
7
|
+
const DEFAULT_BUCKETS = 1800;
|
|
8
|
+
|
|
9
|
+
type WebkitWindow = typeof window & {
|
|
10
|
+
webkitOfflineAudioContext?: typeof OfflineAudioContext;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getOfflineCtor(): typeof OfflineAudioContext {
|
|
14
|
+
const w = window as WebkitWindow;
|
|
15
|
+
const Ctor = w.OfflineAudioContext ?? w.webkitOfflineAudioContext;
|
|
16
|
+
if (!Ctor) throw new Error('OfflineAudioContext is not supported');
|
|
17
|
+
return Ctor;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function decodePeaks(
|
|
21
|
+
src: string,
|
|
22
|
+
buckets: number = DEFAULT_BUCKETS,
|
|
23
|
+
signal?: AbortSignal,
|
|
24
|
+
): Promise<Float32Array> {
|
|
25
|
+
const response = await fetch(src, { signal, credentials: 'same-origin' });
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Failed to fetch audio for peaks (${response.status})`);
|
|
28
|
+
}
|
|
29
|
+
const arr = await response.arrayBuffer();
|
|
30
|
+
const Ctor = getOfflineCtor();
|
|
31
|
+
// Length must be ≥ 1; the actual length doesn't matter for decodeAudioData.
|
|
32
|
+
const ctx = new Ctor(1, 1, SAMPLE_RATE);
|
|
33
|
+
const audio = await ctx.decodeAudioData(arr);
|
|
34
|
+
const channel = audio.getChannelData(0);
|
|
35
|
+
return bucketize(channel, buckets);
|
|
36
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { getAudioContext, unlockAudioContext, isAudioContextRunning } from './audioContext';
|
|
2
|
+
export { getMediaElementSource, hasMediaElementSource } from './mediaElementSourceCache';
|
|
3
|
+
export { decodePeaks } from './decodePeaks';
|
|
4
|
+
export { getPeaks, setPeaks, getPeaksFromCache, clearPeaksCache } from './peaksCache';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Per-element MediaElementSourceNode cache.
|
|
2
|
+
// See ADR-004 §B. WeakMap so entries die with the audio element.
|
|
3
|
+
|
|
4
|
+
import { getAudioContext } from './audioContext';
|
|
5
|
+
|
|
6
|
+
const cache = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>();
|
|
7
|
+
|
|
8
|
+
export function getMediaElementSource(el: HTMLAudioElement): MediaElementAudioSourceNode {
|
|
9
|
+
const hit = cache.get(el);
|
|
10
|
+
if (hit) return hit;
|
|
11
|
+
const ctx = getAudioContext();
|
|
12
|
+
const node = ctx.createMediaElementSource(el);
|
|
13
|
+
node.connect(ctx.destination);
|
|
14
|
+
cache.set(el, node);
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasMediaElementSource(el: HTMLAudioElement): boolean {
|
|
19
|
+
return cache.has(el);
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Module-level peaks cache. Dedupes in-flight decodes; caches resolved buffers.
|
|
2
|
+
// See ADR-002.
|
|
3
|
+
|
|
4
|
+
import { decodePeaks } from './decodePeaks';
|
|
5
|
+
|
|
6
|
+
const cache = new Map<string, Float32Array>();
|
|
7
|
+
const inflight = new Map<string, Promise<Float32Array>>();
|
|
8
|
+
|
|
9
|
+
export async function getPeaks(src: string, buckets?: number): Promise<Float32Array> {
|
|
10
|
+
const hit = cache.get(src);
|
|
11
|
+
if (hit) return hit;
|
|
12
|
+
const flying = inflight.get(src);
|
|
13
|
+
if (flying) return flying;
|
|
14
|
+
const promise = decodePeaks(src, buckets).then((peaks) => {
|
|
15
|
+
cache.set(src, peaks);
|
|
16
|
+
return peaks;
|
|
17
|
+
});
|
|
18
|
+
inflight.set(src, promise);
|
|
19
|
+
try {
|
|
20
|
+
return await promise;
|
|
21
|
+
} finally {
|
|
22
|
+
inflight.delete(src);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setPeaks(src: string, peaks: Float32Array): void {
|
|
27
|
+
cache.set(src, peaks);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getPeaksFromCache(src: string): Float32Array | undefined {
|
|
31
|
+
return cache.get(src);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearPeaksCache(src?: string): void {
|
|
35
|
+
if (src) cache.delete(src);
|
|
36
|
+
else cache.clear();
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext } from 'react';
|
|
4
|
+
|
|
5
|
+
// The persistent <audio> element. Components that need imperative access
|
|
6
|
+
// (TimeDisplay, Waveform playhead, click-to-seek) read this — not the React
|
|
7
|
+
// state — to avoid coupling per-frame work to renders.
|
|
8
|
+
export const AudioRefCtx = createContext<HTMLAudioElement | null>(null);
|
|
9
|
+
AudioRefCtx.displayName = 'AudioPlayerAudioRefCtx';
|