@arraypress/waveform-player-astro 0.1.0
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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/package.json +79 -0
- package/src/WaveformPlayer.astro +337 -0
- package/src/astro-shim.d.ts +20 -0
- package/src/globals.d.ts +82 -0
- package/src/index.ts +46 -0
- package/src/types.ts +442 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@arraypress/waveform-player-astro` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — Unreleased
|
|
9
|
+
|
|
10
|
+
Initial release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `<WaveformPlayer>` Astro component wrapping every option exposed by
|
|
15
|
+
`@arraypress/waveform-player` 1.6.x as a typed prop:
|
|
16
|
+
- Audio source props (`url`, `audioMode`, `preload`)
|
|
17
|
+
- Waveform visualisation props (`waveformStyle`, `height`, `samples`,
|
|
18
|
+
`barWidth`, `barSpacing`, `waveform`)
|
|
19
|
+
- Colour props (`colorPreset`, `waveformColor`, `progressColor`,
|
|
20
|
+
`buttonColor`, `buttonHoverColor`, `textColor`,
|
|
21
|
+
`textSecondaryColor`, `backgroundColor`, `borderColor`)
|
|
22
|
+
- Playback control props (`playbackRate`, `showPlaybackSpeed`,
|
|
23
|
+
`playbackRates`)
|
|
24
|
+
- UI toggle props (`showControls`, `showInfo`, `showTime`,
|
|
25
|
+
`showHoverTime`, `showBPM`, `buttonAlign`)
|
|
26
|
+
- Marker props (`markers`, `showMarkers`)
|
|
27
|
+
- Content metadata props (`title`, `subtitle`, `artwork`, `album`)
|
|
28
|
+
- Behaviour flags (`autoplay`, `singlePlay`, `playOnSeek`,
|
|
29
|
+
`enableMediaSession`)
|
|
30
|
+
- Icon props (`playIcon`, `pauseIcon`)
|
|
31
|
+
- Astro-specific `lazy` prop that switches the init attribute to
|
|
32
|
+
`data-waveform-player-lazy` and ships a single deduplicated
|
|
33
|
+
`IntersectionObserver` for grids of many previews.
|
|
34
|
+
- Astro-specific `id`, `class`, and `style` pass-throughs.
|
|
35
|
+
- Public TypeScript types: `WaveformPlayerProps`, `WaveformStyle`,
|
|
36
|
+
`WaveformMarker`, `WaveformPeaks`, `ColorPreset`, `AudioMode`,
|
|
37
|
+
`AudioPreload`, `ButtonAlign`.
|
|
38
|
+
- Ambient declaration for `window.WaveformPlayer` to type consumer
|
|
39
|
+
scripts that reach for the global.
|
|
40
|
+
- Vitest suite (29 tests) covering attribute mapping, omission
|
|
41
|
+
semantics, JSON serialisation for array props, lazy-mount script
|
|
42
|
+
presence, and pass-through props.
|
|
43
|
+
- Documentation: full prop reference, setup guide, seven usage
|
|
44
|
+
examples (`examples/basic.astro`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ArrayPress
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @arraypress/waveform-player-astro
|
|
2
|
+
|
|
3
|
+
Astro component wrapper around [`@arraypress/waveform-player`](https://github.com/arraypress/waveform-player). Exposes every library option as a typed prop, handles `data-*` attribute serialisation for you, and ships an optional lazy-mount `IntersectionObserver` for grids of many previews.
|
|
4
|
+
|
|
5
|
+
The core library stays a zero-dependency vanilla-JS package that works anywhere a `<script>` tag does (WordPress, Shopify, raw HTML). This package adds the framework-native ergonomics Astro users expect — without touching the runtime.
|
|
6
|
+
|
|
7
|
+
```astro
|
|
8
|
+
---
|
|
9
|
+
import WaveformPlayer from '@arraypress/waveform-player-astro';
|
|
10
|
+
import '@arraypress/waveform-player/dist/waveform-player.css';
|
|
11
|
+
import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';
|
|
12
|
+
---
|
|
13
|
+
<script src={wfpJsUrl} is:inline></script>
|
|
14
|
+
|
|
15
|
+
<WaveformPlayer
|
|
16
|
+
url="/audio/track.mp3"
|
|
17
|
+
title="My Track"
|
|
18
|
+
waveformStyle="bars"
|
|
19
|
+
showBPM
|
|
20
|
+
/>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @arraypress/waveform-player-astro @arraypress/waveform-player
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`@arraypress/waveform-player` is a peer dependency — you bring it explicitly so you control the version.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
The wrapper does **not** load the core library's JS or CSS for you. This is deliberate: you might want a CDN, a self-hosted asset, or the bundled npm path. Load both once in your root layout:
|
|
34
|
+
|
|
35
|
+
```astro
|
|
36
|
+
---
|
|
37
|
+
// src/layouts/Layout.astro
|
|
38
|
+
import '@arraypress/waveform-player/dist/waveform-player.css';
|
|
39
|
+
import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';
|
|
40
|
+
---
|
|
41
|
+
<html>
|
|
42
|
+
<head>
|
|
43
|
+
<script src={wfpJsUrl} is:inline></script>
|
|
44
|
+
</head>
|
|
45
|
+
<body><slot /></body>
|
|
46
|
+
</html>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then `<WaveformPlayer>` works on any page.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Minimal
|
|
54
|
+
|
|
55
|
+
```astro
|
|
56
|
+
<WaveformPlayer url="/audio/track.mp3" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### With metadata + a chosen style
|
|
60
|
+
|
|
61
|
+
```astro
|
|
62
|
+
<WaveformPlayer
|
|
63
|
+
url="/audio/track.mp3"
|
|
64
|
+
title="Midnight Dreams"
|
|
65
|
+
subtitle="The Wavelength"
|
|
66
|
+
artwork="/img/cover.jpg"
|
|
67
|
+
waveformStyle="bars"
|
|
68
|
+
barWidth={3}
|
|
69
|
+
barSpacing={1}
|
|
70
|
+
height={80}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Pre-computed peaks (recommended for catalogues)
|
|
75
|
+
|
|
76
|
+
```astro
|
|
77
|
+
<WaveformPlayer
|
|
78
|
+
url="/audio/track.mp3"
|
|
79
|
+
waveform="/peaks/track.json"
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Generate the JSON at build time with [`@arraypress/waveform-gen`](https://github.com/arraypress/waveform-gen). Removes a full Web Audio decode from the runtime render path.
|
|
84
|
+
|
|
85
|
+
### Chapter markers
|
|
86
|
+
|
|
87
|
+
```astro
|
|
88
|
+
<WaveformPlayer
|
|
89
|
+
url="/audio/podcast.mp3"
|
|
90
|
+
markers={[
|
|
91
|
+
{ time: 0, label: 'Intro' },
|
|
92
|
+
{ time: 60, label: 'Main topic', color: '#a855f7' },
|
|
93
|
+
{ time: 600, label: 'Q&A' },
|
|
94
|
+
]}
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Lazy mounting (large grids)
|
|
99
|
+
|
|
100
|
+
Pass `lazy` and the wrapper switches the init attribute to `data-waveform-player-lazy`, then installs a single shared `IntersectionObserver` that promotes each mount when it crosses the viewport (with 200 px of buffer so audio is decoded before the user actually sees the player).
|
|
101
|
+
|
|
102
|
+
```astro
|
|
103
|
+
{previews.map((p) => (
|
|
104
|
+
<WaveformPlayer url={p.url} title={p.title} lazy />
|
|
105
|
+
))}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The observer is installed at most once per page (`window.__wfpLazyMountBound` flag), and re-fires on Astro's `astro:page-load` event so cross-page navigations pick up new mounts.
|
|
109
|
+
|
|
110
|
+
### External audio mode
|
|
111
|
+
|
|
112
|
+
For setups where one audio element (e.g. [`@arraypress/waveform-bar`](https://github.com/arraypress/waveform-bar)) drives many visual surfaces:
|
|
113
|
+
|
|
114
|
+
```astro
|
|
115
|
+
<WaveformPlayer
|
|
116
|
+
url={track.url}
|
|
117
|
+
audioMode="external"
|
|
118
|
+
waveformStyle="seekbar"
|
|
119
|
+
showInfo={false}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The player dispatches `waveformplayer:request-play | request-pause | request-seek` custom events instead of touching audio itself. Drive the visualisation from your controller via the player instance's `setProgress(currentTime, duration)` and `setPlayingState(playing)` methods.
|
|
124
|
+
|
|
125
|
+
## Props
|
|
126
|
+
|
|
127
|
+
Every prop maps 1:1 to an option on the core library. Omitting a prop emits no `data-*` attribute and lets the library apply its own default.
|
|
128
|
+
|
|
129
|
+
### Audio source
|
|
130
|
+
|
|
131
|
+
| Prop | Type | Default | Description |
|
|
132
|
+
| ----------- | ------------------------------------- | ------------ | ----------- |
|
|
133
|
+
| `url` | `string` *(required)* | — | Audio file URL. |
|
|
134
|
+
| `audioMode` | `'self' \| 'external'` | `'self'` | `'external'` renders waveform only and emits request events. |
|
|
135
|
+
| `preload` | `'auto' \| 'metadata' \| 'none'` | `'metadata'` | Browser preload hint. |
|
|
136
|
+
|
|
137
|
+
### Waveform visualisation
|
|
138
|
+
|
|
139
|
+
| Prop | Type | Default | Description |
|
|
140
|
+
| --------------- | ------------------------------------------------------------- | ---------- | ----------- |
|
|
141
|
+
| `waveformStyle` | `'bars' \| 'mirror' \| 'line' \| 'blocks' \| 'dots' \| 'seekbar'` | `'mirror'` | Visual style. |
|
|
142
|
+
| `height` | `number` | `60` | Canvas height in pixels. |
|
|
143
|
+
| `samples` | `number` | `200` | Number of waveform peaks to render. |
|
|
144
|
+
| `barWidth` | `number` | style-dependent | Width of each bar/block. |
|
|
145
|
+
| `barSpacing` | `number` | style-dependent | Gap between bars. |
|
|
146
|
+
| `waveform` | `number[] \| string` | — | Pre-computed peaks (array, JSON URL, or inline JSON). |
|
|
147
|
+
|
|
148
|
+
### Colours
|
|
149
|
+
|
|
150
|
+
All optional. `colorPreset` controls the auto theme, and any individual colour wins over the preset.
|
|
151
|
+
|
|
152
|
+
| Prop | Type | Description |
|
|
153
|
+
| --------------------- | ------------------------------- | ----------- |
|
|
154
|
+
| `colorPreset` | `'dark' \| 'light' \| null` | `null` (default) auto-detects. |
|
|
155
|
+
| `waveformColor` | `string` | Unplayed peak colour. |
|
|
156
|
+
| `progressColor` | `string` | Played-through peak colour. |
|
|
157
|
+
| `buttonColor` | `string` | Play-button colour. |
|
|
158
|
+
| `buttonHoverColor` | `string` | Play-button hover colour. |
|
|
159
|
+
| `textColor` | `string` | Primary text colour. |
|
|
160
|
+
| `textSecondaryColor` | `string` | Secondary text colour. |
|
|
161
|
+
| `backgroundColor` | `string` | Reserved. |
|
|
162
|
+
| `borderColor` | `string` | Reserved. |
|
|
163
|
+
|
|
164
|
+
### Playback controls
|
|
165
|
+
|
|
166
|
+
| Prop | Type | Default | Description |
|
|
167
|
+
| ------------------- | ----------- | ------------------------------------ | ----------- |
|
|
168
|
+
| `playbackRate` | `number` | `1` | Initial speed. |
|
|
169
|
+
| `showPlaybackSpeed` | `boolean` | `false` | Show the speed menu. |
|
|
170
|
+
| `playbackRates` | `number[]` | `[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]` | Speeds offered in the menu. |
|
|
171
|
+
|
|
172
|
+
### UI toggles
|
|
173
|
+
|
|
174
|
+
| Prop | Type | Default | Description |
|
|
175
|
+
| --------------- | ------------------------------------------ | -------- | ----------- |
|
|
176
|
+
| `showControls` | `boolean` | `true` | Show the play/pause button. |
|
|
177
|
+
| `showInfo` | `boolean` | `true` | Show the info bar (title, time, etc.). |
|
|
178
|
+
| `showTime` | `boolean` | `true` | Show current / total time. |
|
|
179
|
+
| `showHoverTime` | `boolean` | `false` | Reserved. |
|
|
180
|
+
| `showBPM` | `boolean` | `false` | Detect and display BPM. |
|
|
181
|
+
| `buttonAlign` | `'auto' \| 'top' \| 'center' \| 'bottom'` | `'auto'` | Play-button vertical alignment. |
|
|
182
|
+
|
|
183
|
+
### Markers
|
|
184
|
+
|
|
185
|
+
| Prop | Type | Default | Description |
|
|
186
|
+
| ------------- | ----------------------------------------------------------------- | ------- | ----------- |
|
|
187
|
+
| `markers` | `Array<{ time: number; label: string; color?: string }>` | — | Clickable seek-point markers. |
|
|
188
|
+
| `showMarkers` | `boolean` | `true` | Render markers (false to hide without losing data). |
|
|
189
|
+
|
|
190
|
+
### Content metadata
|
|
191
|
+
|
|
192
|
+
| Prop | Type | Description |
|
|
193
|
+
| ---------- | -------- | ----------- |
|
|
194
|
+
| `title` | `string` | Track title (defaults to a prettified filename). |
|
|
195
|
+
| `subtitle` | `string` | Subtitle / artist. |
|
|
196
|
+
| `artwork` | `string` | Thumbnail image URL. |
|
|
197
|
+
| `album` | `string` | Album name (Media Session API). |
|
|
198
|
+
|
|
199
|
+
### Behaviour
|
|
200
|
+
|
|
201
|
+
| Prop | Type | Default | Description |
|
|
202
|
+
| -------------------- | --------- | ------- | ----------- |
|
|
203
|
+
| `autoplay` | `boolean` | `false` | Start as soon as metadata loads. |
|
|
204
|
+
| `singlePlay` | `boolean` | `true` | Pause other players on the page when this one starts. |
|
|
205
|
+
| `playOnSeek` | `boolean` | `true` | Resume on seek if paused. |
|
|
206
|
+
| `enableMediaSession` | `boolean` | `true` | Wire up the Media Session API. |
|
|
207
|
+
|
|
208
|
+
### Icons
|
|
209
|
+
|
|
210
|
+
| Prop | Type | Description |
|
|
211
|
+
| ----------- | -------- | ----------- |
|
|
212
|
+
| `playIcon` | `string` | Inline HTML / SVG for the play button (injected raw — trust your source). |
|
|
213
|
+
| `pauseIcon` | `string` | Inline HTML / SVG for the pause button. |
|
|
214
|
+
|
|
215
|
+
### Astro-specific
|
|
216
|
+
|
|
217
|
+
| Prop | Type | Default | Description |
|
|
218
|
+
| ------- | --------- | ------- | ----------- |
|
|
219
|
+
| `lazy` | `boolean` | `false` | Defer mount until viewport entry. |
|
|
220
|
+
| `id` | `string` | — | Forwarded to the container element. |
|
|
221
|
+
| `class` | `string` | — | Appended to the container's classes (`wfp-host` is always present). |
|
|
222
|
+
| `style` | `string` | — | Inline style on the container. |
|
|
223
|
+
|
|
224
|
+
## TypeScript
|
|
225
|
+
|
|
226
|
+
Importing the types directly:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import type {
|
|
230
|
+
WaveformPlayerProps,
|
|
231
|
+
WaveformStyle,
|
|
232
|
+
WaveformMarker,
|
|
233
|
+
WaveformPeaks,
|
|
234
|
+
ColorPreset,
|
|
235
|
+
AudioMode,
|
|
236
|
+
AudioPreload,
|
|
237
|
+
ButtonAlign,
|
|
238
|
+
} from '@arraypress/waveform-player-astro';
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The package also ships an ambient declaration for `window.WaveformPlayer` so consumers reaching for it from their own scripts get autocomplete.
|
|
242
|
+
|
|
243
|
+
## Testing
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npm test # one-shot
|
|
247
|
+
npm run test:watch
|
|
248
|
+
npm run typecheck
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Tests use Astro's official `experimental_AstroContainer` API to render the component to a string and assert on the resulting HTML. No browser required.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT © ArrayPress
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arraypress/waveform-player-astro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Astro component wrapper for @arraypress/waveform-player — typed props for every option, lazy-mount support, framework-idiomatic ergonomics.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CHANGELOG.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"default": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"./WaveformPlayer.astro": "./src/WaveformPlayer.astro",
|
|
18
|
+
"./types": {
|
|
19
|
+
"types": "./src/types.ts",
|
|
20
|
+
"default": "./src/types.ts"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"astro",
|
|
26
|
+
"astro-component",
|
|
27
|
+
"audio",
|
|
28
|
+
"player",
|
|
29
|
+
"waveform",
|
|
30
|
+
"visualization",
|
|
31
|
+
"music",
|
|
32
|
+
"sound",
|
|
33
|
+
"html5",
|
|
34
|
+
"web-audio",
|
|
35
|
+
"media-session",
|
|
36
|
+
"audio-player",
|
|
37
|
+
"waveform-visualization",
|
|
38
|
+
"podcast-player",
|
|
39
|
+
"audio-visualization",
|
|
40
|
+
"bpm-detection",
|
|
41
|
+
"withastro"
|
|
42
|
+
],
|
|
43
|
+
"author": "ArrayPress",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/arraypress/waveform-player-astro.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/arraypress/waveform-player-astro/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://waveformplayer.com",
|
|
53
|
+
"funding": {
|
|
54
|
+
"type": "github",
|
|
55
|
+
"url": "https://github.com/sponsors/arraypress"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public",
|
|
62
|
+
"registry": "https://registry.npmjs.org/"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@arraypress/waveform-player": "^1.6.0",
|
|
66
|
+
"astro": "^6.0.0"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"typecheck": "tsc --noEmit"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@arraypress/waveform-player": "^1.6.0",
|
|
75
|
+
"astro": "^6.3.7",
|
|
76
|
+
"typescript": "^6.0.3",
|
|
77
|
+
"vitest": "^4.1.7"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* WaveformPlayer.astro
|
|
4
|
+
* --------------------
|
|
5
|
+
*
|
|
6
|
+
* Astro component wrapper around `@arraypress/waveform-player`. Exposes
|
|
7
|
+
* every library option as a typed prop and emits the equivalent
|
|
8
|
+
* `data-*` attribute on the mount container. The library's own
|
|
9
|
+
* auto-initialiser (registered globally when its script loads) picks up
|
|
10
|
+
* the container and renders the player into it.
|
|
11
|
+
*
|
|
12
|
+
* ## Why a wrapper?
|
|
13
|
+
*
|
|
14
|
+
* The core library is intentionally framework-agnostic vanilla JS. That
|
|
15
|
+
* works great everywhere (WordPress, Shopify, plain HTML) but means
|
|
16
|
+
* Astro users have to hand-stringify every option as a `data-*`
|
|
17
|
+
* attribute:
|
|
18
|
+
*
|
|
19
|
+
* ```astro
|
|
20
|
+
* <div data-waveform-player
|
|
21
|
+
* data-url={url}
|
|
22
|
+
* data-waveform-style={style}
|
|
23
|
+
* data-bar-width={String(2)}
|
|
24
|
+
* data-markers={JSON.stringify(markers)}
|
|
25
|
+
* data-show-bpm="true">
|
|
26
|
+
* </div>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* This wrapper turns that into:
|
|
30
|
+
*
|
|
31
|
+
* ```astro
|
|
32
|
+
* <WaveformPlayer
|
|
33
|
+
* url={url}
|
|
34
|
+
* waveformStyle={style}
|
|
35
|
+
* barWidth={2}
|
|
36
|
+
* markers={markers}
|
|
37
|
+
* showBPM
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Typed props, correct stringification, omission semantics that let the
|
|
42
|
+
* library's defaults shine through.
|
|
43
|
+
*
|
|
44
|
+
* ## Setup
|
|
45
|
+
*
|
|
46
|
+
* Load the core library's JS + CSS once in your layout (the wrapper
|
|
47
|
+
* intentionally does NOT do this for you — you may want a CDN, a
|
|
48
|
+
* self-hosted asset, or the bundled npm path):
|
|
49
|
+
*
|
|
50
|
+
* ```astro
|
|
51
|
+
* ---
|
|
52
|
+
* import '@arraypress/waveform-player/dist/waveform-player.css';
|
|
53
|
+
* import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';
|
|
54
|
+
* ---
|
|
55
|
+
* <script src={wfpJsUrl} is:inline></script>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Then drop `<WaveformPlayer>` anywhere.
|
|
59
|
+
*
|
|
60
|
+
* ## Lazy mounting
|
|
61
|
+
*
|
|
62
|
+
* Passing `lazy` switches the init attribute from `data-waveform-player`
|
|
63
|
+
* to `data-waveform-player-lazy` and ships a one-time
|
|
64
|
+
* `IntersectionObserver` script that promotes lazy mounts when they
|
|
65
|
+
* approach the viewport. The script self-deduplicates via
|
|
66
|
+
* `window.__wfpLazyMountBound` so multiple `<WaveformPlayer lazy>`
|
|
67
|
+
* instances on the same page reuse a single observer.
|
|
68
|
+
*
|
|
69
|
+
* @example Basic
|
|
70
|
+
* <WaveformPlayer url="/audio/track.mp3" title="My Track" />
|
|
71
|
+
*
|
|
72
|
+
* @example Pre-computed peaks (recommended for catalogues)
|
|
73
|
+
* <WaveformPlayer
|
|
74
|
+
* url="/audio/track.mp3"
|
|
75
|
+
* waveform="/peaks/track.json"
|
|
76
|
+
* waveformStyle="bars"
|
|
77
|
+
* barWidth={3}
|
|
78
|
+
* showBPM
|
|
79
|
+
* />
|
|
80
|
+
*
|
|
81
|
+
* @example External mode (paired with `@arraypress/waveform-bar`)
|
|
82
|
+
* <WaveformPlayer
|
|
83
|
+
* url={track.url}
|
|
84
|
+
* audioMode="external"
|
|
85
|
+
* showInfo={false}
|
|
86
|
+
* waveformStyle="seekbar"
|
|
87
|
+
* />
|
|
88
|
+
*
|
|
89
|
+
* @example Lazy-mount grid item
|
|
90
|
+
* <WaveformPlayer url={preview.url} lazy />
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
import type { WaveformPlayerProps } from './types';
|
|
94
|
+
|
|
95
|
+
const {
|
|
96
|
+
// Audio
|
|
97
|
+
url,
|
|
98
|
+
audioMode,
|
|
99
|
+
preload,
|
|
100
|
+
// Waveform visualisation
|
|
101
|
+
waveformStyle,
|
|
102
|
+
height,
|
|
103
|
+
samples,
|
|
104
|
+
barWidth,
|
|
105
|
+
barSpacing,
|
|
106
|
+
waveform,
|
|
107
|
+
// Colours
|
|
108
|
+
colorPreset,
|
|
109
|
+
waveformColor,
|
|
110
|
+
progressColor,
|
|
111
|
+
buttonColor,
|
|
112
|
+
buttonHoverColor,
|
|
113
|
+
textColor,
|
|
114
|
+
textSecondaryColor,
|
|
115
|
+
backgroundColor,
|
|
116
|
+
borderColor,
|
|
117
|
+
// Playback controls
|
|
118
|
+
playbackRate,
|
|
119
|
+
showPlaybackSpeed,
|
|
120
|
+
playbackRates,
|
|
121
|
+
// UI toggles
|
|
122
|
+
showControls,
|
|
123
|
+
showInfo,
|
|
124
|
+
showTime,
|
|
125
|
+
showHoverTime,
|
|
126
|
+
showBPM,
|
|
127
|
+
buttonAlign,
|
|
128
|
+
// Markers
|
|
129
|
+
markers,
|
|
130
|
+
showMarkers,
|
|
131
|
+
// Metadata
|
|
132
|
+
title,
|
|
133
|
+
subtitle,
|
|
134
|
+
artwork,
|
|
135
|
+
album,
|
|
136
|
+
// Behaviour
|
|
137
|
+
autoplay,
|
|
138
|
+
singlePlay,
|
|
139
|
+
playOnSeek,
|
|
140
|
+
enableMediaSession,
|
|
141
|
+
// Icons
|
|
142
|
+
playIcon,
|
|
143
|
+
pauseIcon,
|
|
144
|
+
// Astro extras
|
|
145
|
+
lazy = false,
|
|
146
|
+
id,
|
|
147
|
+
class: className,
|
|
148
|
+
style,
|
|
149
|
+
} = Astro.props as WaveformPlayerProps;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the data-attribute map programmatically so an omitted prop
|
|
153
|
+
* never emits an attribute. This is the contract that lets the library
|
|
154
|
+
* apply its own defaults when a prop is left out, and keeps the
|
|
155
|
+
* resulting DOM tidy.
|
|
156
|
+
*
|
|
157
|
+
* @type {Record<string, string>}
|
|
158
|
+
*/
|
|
159
|
+
const data: Record<string, string> = {};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Helper — set a string attr only when the value is non-null/non-undefined.
|
|
163
|
+
* Empty strings are preserved (the library treats them as "set but blank").
|
|
164
|
+
*/
|
|
165
|
+
const setStr = (key: string, value: string | undefined | null): void => {
|
|
166
|
+
if (value === undefined || value === null) return;
|
|
167
|
+
data[key] = value;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Helper — set a numeric attr only when the value is a finite number.
|
|
172
|
+
*/
|
|
173
|
+
const setNum = (key: string, value: number | undefined | null): void => {
|
|
174
|
+
if (value === undefined || value === null) return;
|
|
175
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return;
|
|
176
|
+
data[key] = String(value);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper — set a boolean attr as the string `'true'` / `'false'`.
|
|
181
|
+
* Required because the library compares against `=== 'true'`.
|
|
182
|
+
*/
|
|
183
|
+
const setBool = (key: string, value: boolean | undefined | null): void => {
|
|
184
|
+
if (value === undefined || value === null) return;
|
|
185
|
+
data[key] = value ? 'true' : 'false';
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ─── Audio source ────────────────────────────────────────────────────────
|
|
189
|
+
setStr('data-url', url);
|
|
190
|
+
setStr('data-audio-mode', audioMode ?? null);
|
|
191
|
+
setStr('data-preload', preload ?? null);
|
|
192
|
+
|
|
193
|
+
// ─── Waveform visualisation ──────────────────────────────────────────────
|
|
194
|
+
setStr('data-waveform-style', waveformStyle ?? null);
|
|
195
|
+
setNum('data-height', height);
|
|
196
|
+
setNum('data-samples', samples);
|
|
197
|
+
setNum('data-bar-width', barWidth);
|
|
198
|
+
setNum('data-bar-spacing', barSpacing);
|
|
199
|
+
|
|
200
|
+
// `waveform` accepts an array (JSON-stringify it) or a string (URL/inline
|
|
201
|
+
// JSON — pass through). The library handles both forms.
|
|
202
|
+
if (Array.isArray(waveform)) {
|
|
203
|
+
data['data-waveform'] = JSON.stringify(waveform);
|
|
204
|
+
} else if (typeof waveform === 'string') {
|
|
205
|
+
data['data-waveform'] = waveform;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── Colours ─────────────────────────────────────────────────────────────
|
|
209
|
+
// `colorPreset` is `'dark' | 'light' | null` — only emit when non-null.
|
|
210
|
+
if (colorPreset === 'dark' || colorPreset === 'light') {
|
|
211
|
+
data['data-color-preset'] = colorPreset;
|
|
212
|
+
}
|
|
213
|
+
setStr('data-waveform-color', waveformColor ?? null);
|
|
214
|
+
setStr('data-progress-color', progressColor ?? null);
|
|
215
|
+
setStr('data-button-color', buttonColor ?? null);
|
|
216
|
+
setStr('data-button-hover-color', buttonHoverColor ?? null);
|
|
217
|
+
setStr('data-text-color', textColor ?? null);
|
|
218
|
+
setStr('data-text-secondary-color', textSecondaryColor ?? null);
|
|
219
|
+
setStr('data-background-color', backgroundColor ?? null);
|
|
220
|
+
setStr('data-border-color', borderColor ?? null);
|
|
221
|
+
|
|
222
|
+
// ─── Playback controls ───────────────────────────────────────────────────
|
|
223
|
+
setNum('data-playback-rate', playbackRate);
|
|
224
|
+
setBool('data-show-playback-speed', showPlaybackSpeed);
|
|
225
|
+
if (Array.isArray(playbackRates) && playbackRates.length > 0) {
|
|
226
|
+
data['data-playback-rates'] = JSON.stringify(playbackRates);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── UI toggles ──────────────────────────────────────────────────────────
|
|
230
|
+
setBool('data-show-controls', showControls);
|
|
231
|
+
setBool('data-show-info', showInfo);
|
|
232
|
+
setBool('data-show-time', showTime);
|
|
233
|
+
setBool('data-show-hover-time', showHoverTime);
|
|
234
|
+
// The library's data-attribute parser reads `data-show-bpm` (lowercased
|
|
235
|
+
// `bpm`), NOT `data-show-b-p-m`. Get this wrong and BPM never displays.
|
|
236
|
+
setBool('data-show-bpm', showBPM);
|
|
237
|
+
setStr('data-button-align', buttonAlign ?? null);
|
|
238
|
+
|
|
239
|
+
// ─── Markers ─────────────────────────────────────────────────────────────
|
|
240
|
+
if (Array.isArray(markers) && markers.length > 0) {
|
|
241
|
+
data['data-markers'] = JSON.stringify(markers);
|
|
242
|
+
}
|
|
243
|
+
setBool('data-show-markers', showMarkers);
|
|
244
|
+
|
|
245
|
+
// ─── Metadata ────────────────────────────────────────────────────────────
|
|
246
|
+
setStr('data-title', title ?? null);
|
|
247
|
+
setStr('data-subtitle', subtitle ?? null);
|
|
248
|
+
setStr('data-artwork', artwork ?? null);
|
|
249
|
+
setStr('data-album', album ?? null);
|
|
250
|
+
|
|
251
|
+
// ─── Behaviour ───────────────────────────────────────────────────────────
|
|
252
|
+
setBool('data-autoplay', autoplay);
|
|
253
|
+
setBool('data-single-play', singlePlay);
|
|
254
|
+
setBool('data-play-on-seek', playOnSeek);
|
|
255
|
+
setBool('data-enable-media-session', enableMediaSession);
|
|
256
|
+
|
|
257
|
+
// ─── Icons ───────────────────────────────────────────────────────────────
|
|
258
|
+
// Pass icon markup verbatim. Consumers are responsible for ensuring
|
|
259
|
+
// the HTML they hand us is safe to inject.
|
|
260
|
+
setStr('data-play-icon', playIcon ?? null);
|
|
261
|
+
setStr('data-pause-icon', pauseIcon ?? null);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The init attribute — the library auto-init scans for
|
|
265
|
+
* `[data-waveform-player]`. For lazy mounting we emit
|
|
266
|
+
* `[data-waveform-player-lazy]` instead and let our companion script
|
|
267
|
+
* promote it on viewport entry.
|
|
268
|
+
*/
|
|
269
|
+
const initAttr = lazy ? 'data-waveform-player-lazy' : 'data-waveform-player';
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
<div
|
|
273
|
+
{...{ [initAttr]: '' }}
|
|
274
|
+
{...data}
|
|
275
|
+
id={id}
|
|
276
|
+
class:list={['wfp-host', className]}
|
|
277
|
+
style={style}
|
|
278
|
+
></div>
|
|
279
|
+
|
|
280
|
+
{lazy && (
|
|
281
|
+
<script is:inline>
|
|
282
|
+
/**
|
|
283
|
+
* Lazy-mount watcher for `<WaveformPlayer lazy>` instances.
|
|
284
|
+
*
|
|
285
|
+
* Installs a single `IntersectionObserver` (deduplicated via a
|
|
286
|
+
* `window` flag) that promotes `data-waveform-player-lazy` to
|
|
287
|
+
* `data-waveform-player` once an element approaches the viewport,
|
|
288
|
+
* then triggers `WaveformPlayer.init()` to mount it.
|
|
289
|
+
*
|
|
290
|
+
* The 200 px `rootMargin` gives the browser headroom to fetch and
|
|
291
|
+
* decode the audio so the player is ready by the time the user
|
|
292
|
+
* scrolls it into actual view — eliminates the "Unable to load
|
|
293
|
+
* audio" stutter you get when 15+ players try to decode at once
|
|
294
|
+
* on page load.
|
|
295
|
+
*
|
|
296
|
+
* Re-runs on Astro `astro:page-load` so cross-page navigations
|
|
297
|
+
* pick up newly-rendered lazy mounts.
|
|
298
|
+
*/
|
|
299
|
+
(function () {
|
|
300
|
+
if (window.__wfpLazyMountBound) return;
|
|
301
|
+
window.__wfpLazyMountBound = true;
|
|
302
|
+
|
|
303
|
+
function mountLazy() {
|
|
304
|
+
if (!window.WaveformPlayer) return;
|
|
305
|
+
const targets = document.querySelectorAll('[data-waveform-player-lazy]');
|
|
306
|
+
if (!targets.length) return;
|
|
307
|
+
|
|
308
|
+
const io = new IntersectionObserver(
|
|
309
|
+
(entries) => {
|
|
310
|
+
entries.forEach((entry) => {
|
|
311
|
+
if (!entry.isIntersecting) return;
|
|
312
|
+
const el = entry.target;
|
|
313
|
+
el.removeAttribute('data-waveform-player-lazy');
|
|
314
|
+
el.setAttribute('data-waveform-player', '');
|
|
315
|
+
io.unobserve(el);
|
|
316
|
+
try {
|
|
317
|
+
window.WaveformPlayer.init();
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error('[waveform-player-astro] init failed:', err);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
{ rootMargin: '200px 0px 200px 0px', threshold: 0.01 }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
targets.forEach((target) => io.observe(target));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
document.addEventListener('astro:page-load', mountLazy);
|
|
330
|
+
if (document.readyState !== 'loading') {
|
|
331
|
+
mountLazy();
|
|
332
|
+
} else {
|
|
333
|
+
document.addEventListener('DOMContentLoaded', mountLazy);
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
</script>
|
|
337
|
+
)}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module astro-shim
|
|
3
|
+
* @description
|
|
4
|
+
* Ambient declaration for `*.astro` modules so plain `tsc --noEmit`
|
|
5
|
+
* can typecheck `index.ts` and the test suite without choking on
|
|
6
|
+
* `.astro` extensions.
|
|
7
|
+
*
|
|
8
|
+
* At runtime, Astro's Vite plugin resolves these imports to real
|
|
9
|
+
* component factories. For type-check-only contexts (like our
|
|
10
|
+
* `typecheck` npm script) we model them as opaque components that
|
|
11
|
+
* accept any props.
|
|
12
|
+
*
|
|
13
|
+
* In a consumer's Astro project, this shim is harmless — Astro's own
|
|
14
|
+
* `astro/client` types win because they're loaded via the project's
|
|
15
|
+
* `tsconfig` extends chain.
|
|
16
|
+
*/
|
|
17
|
+
declare module '*.astro' {
|
|
18
|
+
const component: (_props: Record<string, unknown>) => unknown;
|
|
19
|
+
export default component;
|
|
20
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module globals
|
|
3
|
+
* @description
|
|
4
|
+
* Ambient declarations for the globals the core `@arraypress/waveform-player`
|
|
5
|
+
* library installs on `window` when its IIFE script runs.
|
|
6
|
+
*
|
|
7
|
+
* Consumers don't need to import this file — TypeScript picks it up
|
|
8
|
+
* automatically via the package's exported types. The shim lets you do
|
|
9
|
+
* things like:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* window.WaveformPlayer?.init();
|
|
13
|
+
* const player = window.WaveformPlayer?.getInstance('my-player-id');
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* without TypeScript complaining about `window.WaveformPlayer` being
|
|
17
|
+
* `any` / undefined.
|
|
18
|
+
*
|
|
19
|
+
* The shape is intentionally minimal — just the static surface a typical
|
|
20
|
+
* consumer touches. The full instance API lives in the core library's
|
|
21
|
+
* own types (when those ship) or via `// @ts-expect-error` for now.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Static / global surface of `window.WaveformPlayer`.
|
|
26
|
+
*
|
|
27
|
+
* The core library exposes its class constructor directly on `window`;
|
|
28
|
+
* static helpers like `init()`, `getInstance()`, `getAllInstances()`,
|
|
29
|
+
* and `destroyAll()` hang off the same object.
|
|
30
|
+
*/
|
|
31
|
+
interface WaveformPlayerGlobal {
|
|
32
|
+
/**
|
|
33
|
+
* Manually scan the document for any `[data-waveform-player]`
|
|
34
|
+
* elements and mount players into them. Called automatically on
|
|
35
|
+
* `DOMContentLoaded`; call again after dynamically inserting
|
|
36
|
+
* markup.
|
|
37
|
+
*/
|
|
38
|
+
init(): void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Retrieve a player instance by element id, DOM element, or
|
|
42
|
+
* internally-generated player id.
|
|
43
|
+
*/
|
|
44
|
+
getInstance(idOrElement: string | HTMLElement): unknown | undefined;
|
|
45
|
+
|
|
46
|
+
/** Return every active player instance. */
|
|
47
|
+
getAllInstances(): unknown[];
|
|
48
|
+
|
|
49
|
+
/** Destroy every active player instance on the page. */
|
|
50
|
+
destroyAll(): void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pre-compute peaks from an audio URL using the Web Audio API.
|
|
54
|
+
* Useful for ad-hoc generation when you don't have a build-time
|
|
55
|
+
* pipeline.
|
|
56
|
+
*/
|
|
57
|
+
generateWaveformData(url: string, samples?: number): Promise<number[]>;
|
|
58
|
+
|
|
59
|
+
/** Allow `new WaveformPlayer(...)` from user code. */
|
|
60
|
+
new (container: string | HTMLElement, options?: Record<string, unknown>): unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare global {
|
|
64
|
+
interface Window {
|
|
65
|
+
/**
|
|
66
|
+
* Installed by `@arraypress/waveform-player`'s IIFE script.
|
|
67
|
+
* `undefined` until that script loads.
|
|
68
|
+
*/
|
|
69
|
+
WaveformPlayer?: WaveformPlayerGlobal;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Internal — set by `<WaveformPlayer lazy>`'s inline mount
|
|
73
|
+
* script to deduplicate the `IntersectionObserver`. Do not
|
|
74
|
+
* rely on this; it is an implementation detail.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
__wfpLazyMountBound?: boolean;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arraypress/waveform-player-astro
|
|
3
|
+
* @description
|
|
4
|
+
* Public entry point for the Astro wrapper around
|
|
5
|
+
* `@arraypress/waveform-player`.
|
|
6
|
+
*
|
|
7
|
+
* ## Importing the component
|
|
8
|
+
*
|
|
9
|
+
* ```astro
|
|
10
|
+
* ---
|
|
11
|
+
* import WaveformPlayer from '@arraypress/waveform-player-astro';
|
|
12
|
+
* // or, if you prefer the explicit path:
|
|
13
|
+
* import WaveformPlayer from '@arraypress/waveform-player-astro/WaveformPlayer.astro';
|
|
14
|
+
* ---
|
|
15
|
+
* <WaveformPlayer url="/audio/track.mp3" />
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Importing the types
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import type {
|
|
22
|
+
* WaveformPlayerProps,
|
|
23
|
+
* WaveformStyle,
|
|
24
|
+
* WaveformMarker,
|
|
25
|
+
* } from '@arraypress/waveform-player-astro';
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see {@link ./WaveformPlayer.astro} for the component implementation
|
|
29
|
+
* @see {@link ./types.ts} for the full prop interface
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import WaveformPlayer from './WaveformPlayer.astro';
|
|
33
|
+
|
|
34
|
+
export { WaveformPlayer };
|
|
35
|
+
export default WaveformPlayer;
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
WaveformPlayerProps,
|
|
39
|
+
WaveformStyle,
|
|
40
|
+
WaveformMarker,
|
|
41
|
+
WaveformPeaks,
|
|
42
|
+
ColorPreset,
|
|
43
|
+
AudioMode,
|
|
44
|
+
AudioPreload,
|
|
45
|
+
ButtonAlign,
|
|
46
|
+
} from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types
|
|
3
|
+
* @description
|
|
4
|
+
* Public TypeScript types for `@arraypress/waveform-player-astro`.
|
|
5
|
+
*
|
|
6
|
+
* Every option exposed by the core `@arraypress/waveform-player` library is
|
|
7
|
+
* surfaced here as a typed prop. The prop names match the library option
|
|
8
|
+
* names 1:1 (camelCase) — the Astro component handles the conversion to the
|
|
9
|
+
* library's `data-*` attribute contract (kebab-case) under the hood.
|
|
10
|
+
*
|
|
11
|
+
* Props are intentionally all optional (except `url`, which the library
|
|
12
|
+
* requires to load audio). When a prop is omitted, the wrapper emits no
|
|
13
|
+
* corresponding `data-*` attribute, letting the core library apply its
|
|
14
|
+
* own internal defaults.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link https://github.com/arraypress/waveform-player} — core library
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Visual style of the waveform rendering.
|
|
21
|
+
*
|
|
22
|
+
* - `bars` — classic vertical bars from the baseline up
|
|
23
|
+
* - `mirror` — symmetrical bars mirrored around the centre line
|
|
24
|
+
* - `line` — connected line graph
|
|
25
|
+
* - `blocks` — chunky square blocks
|
|
26
|
+
* - `dots` — dotted plot
|
|
27
|
+
* - `seekbar` — minimal seek bar with no peak detail
|
|
28
|
+
*/
|
|
29
|
+
export type WaveformStyle =
|
|
30
|
+
| 'bars'
|
|
31
|
+
| 'mirror'
|
|
32
|
+
| 'line'
|
|
33
|
+
| 'blocks'
|
|
34
|
+
| 'dots'
|
|
35
|
+
| 'seekbar';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Forced colour scheme. `null` (the default) auto-detects from the page
|
|
39
|
+
* theme and `prefers-color-scheme`.
|
|
40
|
+
*/
|
|
41
|
+
export type ColorPreset = 'dark' | 'light' | null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How the player handles audio.
|
|
45
|
+
*
|
|
46
|
+
* - `self` — (default) the player owns an `<audio>` element and plays
|
|
47
|
+
* the URL itself. Use this for standalone players.
|
|
48
|
+
* - `external` — the player renders the waveform visualisation only and
|
|
49
|
+
* dispatches `waveformplayer:request-play|pause|seek` events for an
|
|
50
|
+
* external controller (e.g. `@arraypress/waveform-bar`) to handle.
|
|
51
|
+
* Drive the visualisation from the controller by calling
|
|
52
|
+
* `setProgress()` and `setPlayingState()` on the player instance.
|
|
53
|
+
*/
|
|
54
|
+
export type AudioMode = 'self' | 'external';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Browser preload hint passed through to the underlying `<audio>` element.
|
|
58
|
+
*
|
|
59
|
+
* - `auto` — fetch the entire file ahead of time
|
|
60
|
+
* - `metadata` — (default) fetch only enough to determine duration
|
|
61
|
+
* - `none` — fetch nothing until play is requested
|
|
62
|
+
*/
|
|
63
|
+
export type AudioPreload = 'auto' | 'metadata' | 'none';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Vertical alignment of the play button relative to the waveform.
|
|
67
|
+
*
|
|
68
|
+
* `auto` picks `bottom` for the `bars` style and `center` for everything
|
|
69
|
+
* else.
|
|
70
|
+
*/
|
|
71
|
+
export type ButtonAlign = 'auto' | 'top' | 'center' | 'bottom';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A clickable chapter marker rendered on top of the waveform.
|
|
75
|
+
*
|
|
76
|
+
* Clicking a marker seeks the player to its `time`. Markers also display
|
|
77
|
+
* their `label` as a tooltip on hover.
|
|
78
|
+
*/
|
|
79
|
+
export interface WaveformMarker {
|
|
80
|
+
/** Time in seconds at which the marker appears. */
|
|
81
|
+
time: number;
|
|
82
|
+
/** Short label shown as a tooltip. */
|
|
83
|
+
label: string;
|
|
84
|
+
/** Optional override colour (CSS colour string). Falls back to the
|
|
85
|
+
* player's progress colour. */
|
|
86
|
+
color?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pre-computed waveform peaks, OR a string pointer to them.
|
|
91
|
+
*
|
|
92
|
+
* - `number[]` — inline array of peak amplitudes (0..1)
|
|
93
|
+
* - `string` (.json URL) — JSON file URL the library will `fetch()`
|
|
94
|
+
* - `string` (JSON array) — inline JSON string the library will parse
|
|
95
|
+
* - `null` / omitted — the library decodes the audio file with the
|
|
96
|
+
* Web Audio API at load time (slowest path)
|
|
97
|
+
*
|
|
98
|
+
* Pre-computing peaks with `@arraypress/waveform-gen` at build time is
|
|
99
|
+
* strongly recommended for any catalogue of more than a handful of
|
|
100
|
+
* tracks; it removes a full Web Audio decode from the render path.
|
|
101
|
+
*/
|
|
102
|
+
export type WaveformPeaks = number[] | string | null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Props accepted by the `<WaveformPlayer>` Astro component.
|
|
106
|
+
*
|
|
107
|
+
* Grouped to mirror the README sections:
|
|
108
|
+
* 1. Audio source
|
|
109
|
+
* 2. Waveform visualisation
|
|
110
|
+
* 3. Colours
|
|
111
|
+
* 4. Playback controls
|
|
112
|
+
* 5. UI toggles
|
|
113
|
+
* 6. Markers
|
|
114
|
+
* 7. Content metadata
|
|
115
|
+
* 8. Behaviour flags
|
|
116
|
+
* 9. Icons
|
|
117
|
+
* 10. Astro-specific extras
|
|
118
|
+
*/
|
|
119
|
+
export interface WaveformPlayerProps {
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
121
|
+
// 1. Audio source
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Audio file URL. Required for the player to do anything useful.
|
|
126
|
+
*
|
|
127
|
+
* Supported formats are whatever the user's browser supports — MP3,
|
|
128
|
+
* WAV, OGG, AAC, etc. CORS headers must allow cross-origin loads if
|
|
129
|
+
* hosted on a different domain.
|
|
130
|
+
*/
|
|
131
|
+
url: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether the player owns its `<audio>` element (`self`) or only
|
|
135
|
+
* renders visualisation and emits request events (`external`).
|
|
136
|
+
*
|
|
137
|
+
* Use `external` together with `@arraypress/waveform-bar` for
|
|
138
|
+
* persistent-bar setups where one audio element drives many visual
|
|
139
|
+
* surfaces across the page.
|
|
140
|
+
*
|
|
141
|
+
* @default 'self'
|
|
142
|
+
*/
|
|
143
|
+
audioMode?: AudioMode;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Browser preload hint for the underlying `<audio>` element.
|
|
147
|
+
*
|
|
148
|
+
* @default 'metadata'
|
|
149
|
+
*/
|
|
150
|
+
preload?: AudioPreload;
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
153
|
+
// 2. Waveform visualisation
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Which waveform render style to use.
|
|
158
|
+
*
|
|
159
|
+
* @default 'mirror'
|
|
160
|
+
*/
|
|
161
|
+
waveformStyle?: WaveformStyle;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Canvas height in pixels.
|
|
165
|
+
*
|
|
166
|
+
* @default 60
|
|
167
|
+
*/
|
|
168
|
+
height?: number;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Number of peak samples to render across the waveform. Higher values
|
|
172
|
+
* give finer detail at the cost of slightly higher CPU and a busier
|
|
173
|
+
* visual. 100–300 is the sweet spot for most player widths.
|
|
174
|
+
*
|
|
175
|
+
* @default 200
|
|
176
|
+
*/
|
|
177
|
+
samples?: number;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Width of each bar/block in pixels (where applicable). Defaults
|
|
181
|
+
* depend on `waveformStyle` (e.g. `2` for `mirror`, `3` for `bars`).
|
|
182
|
+
*/
|
|
183
|
+
barWidth?: number;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Gap between adjacent bars/blocks in pixels. Defaults depend on
|
|
187
|
+
* `waveformStyle`.
|
|
188
|
+
*/
|
|
189
|
+
barSpacing?: number;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pre-computed peaks data. See {@link WaveformPeaks}.
|
|
193
|
+
*
|
|
194
|
+
* - `number[]` is JSON-stringified into the emitted attribute.
|
|
195
|
+
* - `string` (URL or inline JSON) is emitted verbatim — the library
|
|
196
|
+
* parses it.
|
|
197
|
+
*/
|
|
198
|
+
waveform?: WaveformPeaks;
|
|
199
|
+
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
201
|
+
// 3. Colours
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Forced colour preset. `null` auto-detects from the page theme.
|
|
206
|
+
*
|
|
207
|
+
* Individual `*Color` props always win over the preset, letting you
|
|
208
|
+
* keep the auto-detection but tweak one or two colours.
|
|
209
|
+
*
|
|
210
|
+
* @default null
|
|
211
|
+
*/
|
|
212
|
+
colorPreset?: ColorPreset;
|
|
213
|
+
|
|
214
|
+
/** Colour of the unplayed waveform peaks (CSS colour string). */
|
|
215
|
+
waveformColor?: string;
|
|
216
|
+
/** Colour of the played-through portion of the waveform. */
|
|
217
|
+
progressColor?: string;
|
|
218
|
+
/** Border/text colour of the play button. */
|
|
219
|
+
buttonColor?: string;
|
|
220
|
+
/** Play button hover colour. */
|
|
221
|
+
buttonHoverColor?: string;
|
|
222
|
+
/** Primary text colour (title). */
|
|
223
|
+
textColor?: string;
|
|
224
|
+
/** Secondary text colour (subtitle, time). */
|
|
225
|
+
textSecondaryColor?: string;
|
|
226
|
+
/** Reserved for future use; currently no visible effect. */
|
|
227
|
+
backgroundColor?: string;
|
|
228
|
+
/** Reserved for future use; currently no visible effect. */
|
|
229
|
+
borderColor?: string;
|
|
230
|
+
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
232
|
+
// 4. Playback controls
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Initial playback rate (1 = normal speed; 0.5..2 supported).
|
|
237
|
+
*
|
|
238
|
+
* @default 1
|
|
239
|
+
*/
|
|
240
|
+
playbackRate?: number;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Whether to render the playback-speed control menu.
|
|
244
|
+
*
|
|
245
|
+
* @default false
|
|
246
|
+
*/
|
|
247
|
+
showPlaybackSpeed?: boolean;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* List of speeds to offer in the speed menu.
|
|
251
|
+
*
|
|
252
|
+
* @default [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
|
253
|
+
*/
|
|
254
|
+
playbackRates?: number[];
|
|
255
|
+
|
|
256
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
257
|
+
// 5. UI toggles
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Show the play/pause button.
|
|
262
|
+
*
|
|
263
|
+
* @default true
|
|
264
|
+
*/
|
|
265
|
+
showControls?: boolean;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Show the info bar (title, subtitle, time, BPM, speed).
|
|
269
|
+
*
|
|
270
|
+
* @default true
|
|
271
|
+
*/
|
|
272
|
+
showInfo?: boolean;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Show the current-time / total-time readout.
|
|
276
|
+
*
|
|
277
|
+
* @default true
|
|
278
|
+
*/
|
|
279
|
+
showTime?: boolean;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Show a hover-time indicator on the waveform (reserved — not all
|
|
283
|
+
* library versions implement this yet).
|
|
284
|
+
*
|
|
285
|
+
* @default false
|
|
286
|
+
*/
|
|
287
|
+
showHoverTime?: boolean;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Detect and display the track's BPM in the info bar.
|
|
291
|
+
*
|
|
292
|
+
* Adds a ~50–200 ms onset-detection pass on first load.
|
|
293
|
+
*
|
|
294
|
+
* @default false
|
|
295
|
+
*/
|
|
296
|
+
showBPM?: boolean;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Where to align the play button vertically relative to the waveform.
|
|
300
|
+
*
|
|
301
|
+
* @default 'auto'
|
|
302
|
+
*/
|
|
303
|
+
buttonAlign?: ButtonAlign;
|
|
304
|
+
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
306
|
+
// 6. Markers
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Chapter markers to render on the waveform. Each becomes a clickable
|
|
311
|
+
* seek point.
|
|
312
|
+
*
|
|
313
|
+
* Emitted as JSON-stringified `data-markers`. The library parses it.
|
|
314
|
+
*/
|
|
315
|
+
markers?: WaveformMarker[];
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Whether to render markers at all. Useful for hiding markers in
|
|
319
|
+
* compact layouts while still passing them in for future use.
|
|
320
|
+
*
|
|
321
|
+
* @default true
|
|
322
|
+
*/
|
|
323
|
+
showMarkers?: boolean;
|
|
324
|
+
|
|
325
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
326
|
+
// 7. Content metadata
|
|
327
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Track title shown in the info bar. Falls back to a prettified
|
|
331
|
+
* filename extracted from the URL.
|
|
332
|
+
*/
|
|
333
|
+
title?: string;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Subtitle shown under the title — typically the artist name.
|
|
337
|
+
*/
|
|
338
|
+
subtitle?: string;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Album / collection cover image URL displayed as a thumbnail.
|
|
342
|
+
*/
|
|
343
|
+
artwork?: string;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Album name. Passed through to the Media Session API for lockscreen
|
|
347
|
+
* and OS-level media displays.
|
|
348
|
+
*/
|
|
349
|
+
album?: string;
|
|
350
|
+
|
|
351
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
352
|
+
// 8. Behaviour flags
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Start playback as soon as audio metadata loads.
|
|
357
|
+
*
|
|
358
|
+
* Note that most browsers block autoplay with audible audio unless
|
|
359
|
+
* the user has interacted with the page first.
|
|
360
|
+
*
|
|
361
|
+
* @default false
|
|
362
|
+
*/
|
|
363
|
+
autoplay?: boolean;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Pause any other `WaveformPlayer` on the page when this one starts.
|
|
367
|
+
*
|
|
368
|
+
* @default true
|
|
369
|
+
*/
|
|
370
|
+
singlePlay?: boolean;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resume playback automatically when the user seeks on a paused
|
|
374
|
+
* waveform.
|
|
375
|
+
*
|
|
376
|
+
* @default true
|
|
377
|
+
*/
|
|
378
|
+
playOnSeek?: boolean;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Wire up the browser's Media Session API so OS media keys, lock
|
|
382
|
+
* screens, and bluetooth controls drive the player.
|
|
383
|
+
*
|
|
384
|
+
* @default true
|
|
385
|
+
*/
|
|
386
|
+
enableMediaSession?: boolean;
|
|
387
|
+
|
|
388
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
389
|
+
// 9. Icons
|
|
390
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Inline HTML/SVG to use for the play button. Anything you pass is
|
|
394
|
+
* injected raw — use trusted markup only.
|
|
395
|
+
*/
|
|
396
|
+
playIcon?: string;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Inline HTML/SVG to use for the pause button. Anything you pass is
|
|
400
|
+
* injected raw — use trusted markup only.
|
|
401
|
+
*/
|
|
402
|
+
pauseIcon?: string;
|
|
403
|
+
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
405
|
+
// 10. Astro-specific extras
|
|
406
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Defer initialisation until the player scrolls into view.
|
|
410
|
+
*
|
|
411
|
+
* When `true`, the component emits `data-waveform-player-lazy` and
|
|
412
|
+
* the bundled `IntersectionObserver` script promotes it to
|
|
413
|
+
* `data-waveform-player` once it crosses the viewport (with a 200px
|
|
414
|
+
* buffer so audio is decoded ahead of time).
|
|
415
|
+
*
|
|
416
|
+
* Use this on grids of many previews to avoid spawning N concurrent
|
|
417
|
+
* `fetch()` + `decodeAudioData` jobs on page load.
|
|
418
|
+
*
|
|
419
|
+
* @default false
|
|
420
|
+
*/
|
|
421
|
+
lazy?: boolean;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* DOM id forwarded to the player container. Useful for targeting
|
|
425
|
+
* the player from external scripts via `WaveformPlayer.getInstance(id)`.
|
|
426
|
+
*/
|
|
427
|
+
id?: string;
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Extra class names appended to the container's `class` attribute.
|
|
431
|
+
*
|
|
432
|
+
* The base class `wfp-host` is always applied so the package's CSS
|
|
433
|
+
* (or your own overrides) can target the wrapper.
|
|
434
|
+
*/
|
|
435
|
+
class?: string;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Inline style passed through to the container. Useful for setting
|
|
439
|
+
* `min-height` to reserve layout space before the waveform draws.
|
|
440
|
+
*/
|
|
441
|
+
style?: string;
|
|
442
|
+
}
|