openaudio-suite 2.5.5 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -109
- package/CONTRIBUTING.md +39 -324
- package/OpenAudio.js +314 -159
- package/OpenAudio_r.js +211 -164
- package/OpenAudio_s.js +420 -204
- package/README.md +142 -122
- package/docs/OPENAUDIO_R.md +135 -143
- package/docs/OPENAUDIO_S.md +162 -377
- package/package.json +2 -2
package/docs/OPENAUDIO_S.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# OpenAudio_s.js API Reference
|
|
2
2
|
|
|
3
|
-
**Version:** 1.
|
|
4
|
-
**Class:** `SequentialAudio`
|
|
3
|
+
**Version:** 1.3.0
|
|
4
|
+
**Class:** `SequentialAudio`
|
|
5
5
|
**Use Case:** Sequential/playlist playback with manual or auto-advance
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -9,26 +9,21 @@
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
11
|
```javascript
|
|
12
|
-
// Create a player
|
|
13
12
|
const player = new SequentialAudio([
|
|
14
|
-
{ src: 'intro.mp3',
|
|
13
|
+
{ src: 'intro.mp3', label: 'Introduction' },
|
|
15
14
|
{ src: 'chapter1.mp3', label: 'Chapter 1' },
|
|
16
15
|
{ src: 'chapter2.mp3', label: 'Chapter 2' }
|
|
17
16
|
], {
|
|
18
|
-
autoAdvance: false,
|
|
19
|
-
onPlay:
|
|
20
|
-
onComplete:
|
|
17
|
+
autoAdvance: false,
|
|
18
|
+
onPlay: (clip) => console.log(`Playing: ${clip.label}`),
|
|
19
|
+
onComplete: () => console.log('All clips finished!')
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
// Start
|
|
24
|
-
document.getElementById('play-btn').addEventListener('click', () =>
|
|
25
|
-
player.play();
|
|
26
|
-
});
|
|
22
|
+
// Start — must be inside a user gesture
|
|
23
|
+
document.getElementById('play-btn').addEventListener('click', () => player.play());
|
|
27
24
|
|
|
28
25
|
// Advance to next clip
|
|
29
|
-
document.getElementById('next-btn').addEventListener('click', () =>
|
|
30
|
-
player.next();
|
|
31
|
-
});
|
|
26
|
+
document.getElementById('next-btn').addEventListener('click', () => player.next());
|
|
32
27
|
```
|
|
33
28
|
|
|
34
29
|
---
|
|
@@ -54,63 +49,35 @@ Array of clip objects in playback order.
|
|
|
54
49
|
|
|
55
50
|
Each clip object:
|
|
56
51
|
- **`src`** (string, required) — Path or data URI to audio file
|
|
57
|
-
- **`label`** (string) — Display name for console/callbacks
|
|
52
|
+
- **`label`** (string, optional) — Display name for console/callbacks
|
|
58
53
|
|
|
59
54
|
#### `options` (object, optional)
|
|
60
55
|
|
|
61
56
|
```javascript
|
|
62
57
|
{
|
|
63
|
-
autoAdvance:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
autoAdvance: false, // Boolean: auto-play next clip after current ends
|
|
59
|
+
advanceDelay: 0.5, // Number: seconds to wait before auto-advance
|
|
60
|
+
loop: false, // Boolean: loop sequence when complete
|
|
61
|
+
volume: 1.0, // Number: volume 0.0–1.0
|
|
62
|
+
onPlay: (clip) => {},// Function: called when clip starts
|
|
63
|
+
onEnd: (clip) => {},// Function: called when clip ends
|
|
64
|
+
onComplete: () => {} // Function: called when sequence finishes
|
|
68
65
|
}
|
|
69
66
|
```
|
|
70
67
|
|
|
71
68
|
**All options are optional.**
|
|
72
69
|
|
|
73
|
-
### Constructor Examples
|
|
74
|
-
|
|
75
|
-
```javascript
|
|
76
|
-
// Minimal
|
|
77
|
-
const player = new SequentialAudio([
|
|
78
|
-
{ src: 'clip1.mp3' },
|
|
79
|
-
{ src: 'clip2.mp3' }
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
// With manual advance (require click between clips)
|
|
83
|
-
const player = new SequentialAudio(clips, {
|
|
84
|
-
autoAdvance: false,
|
|
85
|
-
onPlay: (clip) => updateUI(clip.label)
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// With auto-advance (play next automatically)
|
|
89
|
-
const player = new SequentialAudio(clips, {
|
|
90
|
-
autoAdvance: true,
|
|
91
|
-
onComplete: () => showFinishScreen()
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// With looping
|
|
95
|
-
const player = new SequentialAudio(clips, {
|
|
96
|
-
loop: true, // Restart sequence when complete
|
|
97
|
-
onComplete: () => console.log('Looping...')
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
70
|
### Constructor Errors
|
|
102
71
|
|
|
103
72
|
```javascript
|
|
104
|
-
// Throws TypeError
|
|
105
|
-
new SequentialAudio();
|
|
106
|
-
new SequentialAudio([]);
|
|
107
|
-
new SequentialAudio([{ src: '' }]);
|
|
73
|
+
// Throws TypeError
|
|
74
|
+
new SequentialAudio(); // ❌ Missing clips
|
|
75
|
+
new SequentialAudio([]); // ❌ Empty array
|
|
76
|
+
new SequentialAudio([{ src: '' }]); // ❌ Empty src
|
|
108
77
|
new SequentialAudio([{ label: 'No src' }]); // ❌ Missing src
|
|
109
78
|
|
|
110
79
|
// Valid
|
|
111
|
-
new SequentialAudio([
|
|
112
|
-
{ src: 'clip.mp3', label: 'Clip' }
|
|
113
|
-
]); // ✅
|
|
80
|
+
new SequentialAudio([{ src: 'clip.mp3', label: 'Clip' }]); // ✅
|
|
114
81
|
```
|
|
115
82
|
|
|
116
83
|
---
|
|
@@ -119,205 +86,135 @@ new SequentialAudio([
|
|
|
119
86
|
|
|
120
87
|
### `play()`
|
|
121
88
|
|
|
122
|
-
Start
|
|
123
|
-
|
|
124
|
-
**Must be called inside a user gesture on first use.**
|
|
89
|
+
Start the sequence. Must be called inside a user gesture on first use.
|
|
125
90
|
|
|
126
91
|
```javascript
|
|
127
|
-
|
|
128
|
-
document.addEventListener('click', () => {
|
|
129
|
-
player.play(); // ✅ Starts from clip 0
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Subsequent calls
|
|
133
|
-
player.play(); // Safe to call anytime
|
|
92
|
+
document.addEventListener('click', () => player.play(), { once: true });
|
|
134
93
|
```
|
|
135
94
|
|
|
136
95
|
**Behavior:**
|
|
137
|
-
- If already playing: ignored (safe)
|
|
138
|
-
- If
|
|
139
|
-
- If finished: starts from beginning
|
|
96
|
+
- If already started or playing: ignored (safe)
|
|
97
|
+
- If sequence has not started: unlocks Audio element and plays first clip
|
|
140
98
|
|
|
141
99
|
---
|
|
142
100
|
|
|
143
101
|
### `next()`
|
|
144
102
|
|
|
145
|
-
Advance to the next clip and play it.
|
|
103
|
+
Advance to the next clip and play it. No-op if `play()` has not been called yet.
|
|
146
104
|
|
|
147
105
|
```javascript
|
|
148
|
-
player.next();
|
|
106
|
+
player.next();
|
|
149
107
|
```
|
|
150
108
|
|
|
151
109
|
**Behavior:**
|
|
152
110
|
- Increments clip index by 1
|
|
153
|
-
- Plays the new clip
|
|
154
111
|
- At end of sequence:
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
```javascript
|
|
159
|
-
const player = new SequentialAudio([clip1, clip2, clip3], {
|
|
160
|
-
loop: false
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
player.goto(0); // Clip 0
|
|
164
|
-
player.next(); // Clip 1
|
|
165
|
-
player.next(); // Clip 2
|
|
166
|
-
player.next(); // Stops, fires onComplete
|
|
167
|
-
```
|
|
112
|
+
- `loop: true` → wraps to clip 0
|
|
113
|
+
- `loop: false` → fires `onComplete`, stops
|
|
168
114
|
|
|
169
115
|
---
|
|
170
116
|
|
|
171
117
|
### `goto(index)`
|
|
172
118
|
|
|
173
|
-
Jump to a
|
|
119
|
+
Jump to a clip by zero-based index and play it.
|
|
174
120
|
|
|
175
121
|
```javascript
|
|
176
|
-
player.goto(0); //
|
|
177
|
-
player.goto(2); //
|
|
122
|
+
player.goto(0); // First clip
|
|
123
|
+
player.goto(2); // Third clip
|
|
124
|
+
player.goto(10); // ❌ Warning if out of range — no-op
|
|
178
125
|
```
|
|
179
126
|
|
|
180
127
|
**Parameters:**
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
**Behavior:**
|
|
184
|
-
- Jumps to the specified clip and plays it
|
|
185
|
-
- Validates index (warns if out of range, no-op)
|
|
186
|
-
|
|
187
|
-
```javascript
|
|
188
|
-
const player = new SequentialAudio([c1, c2, c3, c4]);
|
|
189
|
-
|
|
190
|
-
player.goto(1); // ✅ Play clip 2
|
|
191
|
-
player.goto(10); // ❌ Warning: out of range
|
|
192
|
-
```
|
|
128
|
+
- `index` (number) — 0-based clip index
|
|
193
129
|
|
|
194
130
|
---
|
|
195
131
|
|
|
196
132
|
### `gotoLabel(label)`
|
|
197
133
|
|
|
198
|
-
Jump to a clip by
|
|
199
|
-
|
|
200
|
-
```javascript
|
|
201
|
-
player.gotoLabel('Chapter 5');
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Parameters:**
|
|
205
|
-
- **`label`** (string) — Exact label match
|
|
206
|
-
|
|
207
|
-
**Behavior:**
|
|
208
|
-
- Searches clips for matching label
|
|
209
|
-
- Jumps to first match
|
|
210
|
-
- Warns if label not found
|
|
134
|
+
Jump to a clip by label (exact string match) and play it.
|
|
211
135
|
|
|
212
136
|
```javascript
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{ src: 'ch1.mp3', label: 'Chapter 1' },
|
|
216
|
-
{ src: 'ch2.mp3', label: 'Chapter 2' }
|
|
217
|
-
]);
|
|
218
|
-
|
|
219
|
-
player.gotoLabel('Chapter 1'); // ✅ Jumps to index 1
|
|
220
|
-
player.gotoLabel('Chapter 99'); // ❌ Warning: not found
|
|
137
|
+
player.gotoLabel('Chapter 1'); // ✅ Jumps to matching clip
|
|
138
|
+
player.gotoLabel('Chapter 99'); // ❌ Warning if not found — no-op
|
|
221
139
|
```
|
|
222
140
|
|
|
223
141
|
---
|
|
224
142
|
|
|
225
143
|
### `pause()`
|
|
226
144
|
|
|
227
|
-
Pause
|
|
228
|
-
|
|
229
|
-
```javascript
|
|
230
|
-
player.pause();
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Behavior:**
|
|
234
|
-
- Pauses audio at current position
|
|
235
|
-
- Sets `isPlaying = false`
|
|
236
|
-
- Call `resume()` to continue from same position
|
|
145
|
+
Pause at current playback position.
|
|
237
146
|
|
|
238
147
|
```javascript
|
|
239
|
-
player.play();
|
|
240
|
-
player.pause();
|
|
241
|
-
player.resume();
|
|
148
|
+
player.play(); // Playing at 2:30
|
|
149
|
+
player.pause(); // Paused at 2:30
|
|
150
|
+
player.resume(); // Resumes from 2:30
|
|
242
151
|
```
|
|
243
152
|
|
|
244
153
|
---
|
|
245
154
|
|
|
246
155
|
### `resume()`
|
|
247
156
|
|
|
248
|
-
Resume
|
|
249
|
-
|
|
250
|
-
```javascript
|
|
251
|
-
player.resume();
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
**Behavior:**
|
|
255
|
-
- If paused: resumes audio
|
|
256
|
-
- If already playing: no-op (safe)
|
|
157
|
+
Resume from paused position. No-op if not paused.
|
|
257
158
|
|
|
258
159
|
```javascript
|
|
259
|
-
player.
|
|
260
|
-
player.
|
|
261
|
-
player.resume(); // Resume from pause point
|
|
262
|
-
player.resume(); // No-op (already playing)
|
|
160
|
+
player.resume(); // Resumes from pause point
|
|
161
|
+
player.resume(); // No-op if already playing
|
|
263
162
|
```
|
|
264
163
|
|
|
265
164
|
---
|
|
266
165
|
|
|
267
166
|
### `stop()`
|
|
268
167
|
|
|
269
|
-
|
|
168
|
+
Pause, rewind current clip to 0:00, and cancel any pending auto-advance timer.
|
|
270
169
|
|
|
271
170
|
```javascript
|
|
272
171
|
player.stop();
|
|
273
172
|
```
|
|
274
173
|
|
|
275
|
-
|
|
276
|
-
- Pauses audio
|
|
277
|
-
- Rewinds to 0:00 of current clip
|
|
278
|
-
- Sets `isPlaying = false`
|
|
279
|
-
- Calling `play()` after will restart from beginning
|
|
174
|
+
From v1.2.0: `stop()` cancels any pending auto-advance `setTimeout`, so calling `stop()` during the inter-clip delay window correctly prevents the next clip from starting.
|
|
280
175
|
|
|
281
|
-
|
|
282
|
-
player.play(); // Playing clip 1, 2:30 elapsed
|
|
283
|
-
player.stop(); // Stopped, rewound to 0:00
|
|
284
|
-
player.play(); // Restart from 0:00
|
|
285
|
-
```
|
|
176
|
+
From v1.3.0: `stop()` also sets a `#playCancelled` flag. If `play()` is called and `stop()` (or `reset()`) is called before the silent-MP3 unlock resolves (~50–200 ms), the clip will not start when the unlock completes. Previously this race was unguarded — the clip would play despite the explicit stop.
|
|
286
177
|
|
|
287
178
|
---
|
|
288
179
|
|
|
289
180
|
### `reset()`
|
|
290
181
|
|
|
291
|
-
|
|
182
|
+
Stop and reset sequence to clip 0. Clears `isStarted` — next `play()` re-runs the unlock.
|
|
292
183
|
|
|
293
184
|
```javascript
|
|
294
|
-
player.
|
|
185
|
+
player.goto(3); // At clip 4
|
|
186
|
+
player.reset(); // Back to clip 0, stopped
|
|
187
|
+
player.play(); // Starts fresh from clip 0
|
|
295
188
|
```
|
|
296
189
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
-
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### `destroy()`
|
|
193
|
+
|
|
194
|
+
Release the Audio element, remove event listeners, and cancel any pending auto-advance timer. All subsequent method calls are safe no-ops.
|
|
302
195
|
|
|
303
196
|
```javascript
|
|
304
|
-
player.
|
|
305
|
-
|
|
306
|
-
|
|
197
|
+
player.destroy();
|
|
198
|
+
|
|
199
|
+
// React example
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const player = new SequentialAudio(clips);
|
|
202
|
+
return () => player.destroy();
|
|
203
|
+
}, []);
|
|
307
204
|
```
|
|
308
205
|
|
|
309
206
|
---
|
|
310
207
|
|
|
311
208
|
### `getCurrentClip()`
|
|
312
209
|
|
|
313
|
-
Get the current clip object.
|
|
210
|
+
Get a **copy** of the current clip object. Returns a copy to prevent mutation of the internal playlist.
|
|
314
211
|
|
|
315
212
|
```javascript
|
|
316
213
|
const clip = player.getCurrentClip();
|
|
317
214
|
console.log(clip.label, clip.src);
|
|
318
215
|
```
|
|
319
216
|
|
|
320
|
-
**Returns:**
|
|
217
|
+
**Returns:** `{ src: string, label: string }` (copy)
|
|
321
218
|
|
|
322
219
|
---
|
|
323
220
|
|
|
@@ -327,60 +224,30 @@ Get the current clip index (0-based).
|
|
|
327
224
|
|
|
328
225
|
```javascript
|
|
329
226
|
const index = player.getCurrentIndex();
|
|
330
|
-
console.log(`
|
|
227
|
+
console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
|
|
331
228
|
```
|
|
332
229
|
|
|
333
|
-
**Returns:** Number (0 to clipCount - 1)
|
|
334
|
-
|
|
335
230
|
---
|
|
336
231
|
|
|
337
232
|
### `getClipCount()`
|
|
338
233
|
|
|
339
|
-
Get total number of clips
|
|
234
|
+
Get total number of clips.
|
|
340
235
|
|
|
341
236
|
```javascript
|
|
342
237
|
const total = player.getClipCount();
|
|
343
|
-
console.log(`${total} clips in sequence`);
|
|
344
238
|
```
|
|
345
239
|
|
|
346
|
-
**Returns:** Number
|
|
347
|
-
|
|
348
240
|
---
|
|
349
241
|
|
|
350
242
|
### `getClips()`
|
|
351
243
|
|
|
352
|
-
Get a copy of all
|
|
244
|
+
Get a **deep copy** of all clip objects. Inner objects are copied to prevent callers from mutating the internal playlist.
|
|
353
245
|
|
|
354
246
|
```javascript
|
|
355
247
|
const allClips = player.getClips();
|
|
356
|
-
console.log(`Sequence has ${allClips.length} clips`);
|
|
357
248
|
```
|
|
358
249
|
|
|
359
|
-
**Returns:** Array
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
### `destroy()`
|
|
364
|
-
|
|
365
|
-
Clean up and remove all references. Call on SPA unmount.
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
player.destroy();
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
**Behavior:**
|
|
372
|
-
- Stops playback
|
|
373
|
-
- Clears audio element
|
|
374
|
-
- Nullifies internal references
|
|
375
|
-
- After this, don't call any other methods
|
|
376
|
-
|
|
377
|
-
```javascript
|
|
378
|
-
// React example
|
|
379
|
-
useEffect(() => {
|
|
380
|
-
const player = new SequentialAudio(clips);
|
|
381
|
-
return () => player.destroy();
|
|
382
|
-
}, []);
|
|
383
|
-
```
|
|
250
|
+
**Returns:** `Array<{ src: string, label: string }>` (deep copy)
|
|
384
251
|
|
|
385
252
|
---
|
|
386
253
|
|
|
@@ -389,23 +256,17 @@ useEffect(() => {
|
|
|
389
256
|
Check if browser supports an audio format.
|
|
390
257
|
|
|
391
258
|
```javascript
|
|
392
|
-
SequentialAudio.canPlay('audio/mpeg') → boolean
|
|
259
|
+
SequentialAudio.canPlay('audio/mpeg') // → boolean
|
|
260
|
+
SequentialAudio.canPlay('audio/ogg') // → boolean
|
|
261
|
+
SequentialAudio.canPlay('audio/wav') // → boolean
|
|
262
|
+
SequentialAudio.canPlay('audio/webm') // → boolean
|
|
393
263
|
```
|
|
394
264
|
|
|
395
|
-
**
|
|
396
|
-
- **`type`** (string) — MIME type to check
|
|
397
|
-
- `'audio/mpeg'` — MP3
|
|
398
|
-
- `'audio/ogg'` — OGG Vorbis
|
|
399
|
-
- `'audio/wav'` — WAV
|
|
400
|
-
- `'audio/webm'` — WebM
|
|
401
|
-
|
|
402
|
-
**Returns:** `true` if supported, `false` otherwise
|
|
265
|
+
**Returns:** `true` if supported, `false` if unsupported or input is not a string.
|
|
403
266
|
|
|
404
267
|
```javascript
|
|
405
|
-
if (SequentialAudio.canPlay('audio/ogg')) {
|
|
406
|
-
|
|
407
|
-
} else {
|
|
408
|
-
// Fall back to MP3
|
|
268
|
+
if (!SequentialAudio.canPlay('audio/ogg')) {
|
|
269
|
+
console.warn('OGG not supported — use MP3 sources instead');
|
|
409
270
|
}
|
|
410
271
|
```
|
|
411
272
|
|
|
@@ -415,17 +276,13 @@ if (SequentialAudio.canPlay('audio/ogg')) {
|
|
|
415
276
|
|
|
416
277
|
### `isPlaying`
|
|
417
278
|
|
|
418
|
-
**Type:** `boolean` (read-only)
|
|
279
|
+
**Type:** `boolean` (read-only getter)
|
|
419
280
|
|
|
420
|
-
`true` if a clip is actively playing, `false` if paused or
|
|
281
|
+
`true` if a clip is actively playing, `false` if paused, stopped, or between clips.
|
|
421
282
|
|
|
422
283
|
```javascript
|
|
423
|
-
player.play();
|
|
424
|
-
|
|
425
284
|
if (player.isPlaying) {
|
|
426
|
-
|
|
427
|
-
} else {
|
|
428
|
-
console.log('Clip is paused or stopped');
|
|
285
|
+
player.pause();
|
|
429
286
|
}
|
|
430
287
|
```
|
|
431
288
|
|
|
@@ -433,18 +290,16 @@ if (player.isPlaying) {
|
|
|
433
290
|
|
|
434
291
|
### `isStarted`
|
|
435
292
|
|
|
436
|
-
**Type:** `boolean` (read-only)
|
|
293
|
+
**Type:** `boolean` (read-only getter)
|
|
437
294
|
|
|
438
|
-
`true` after
|
|
295
|
+
`true` after `play()` has been called and the sequence has been unlocked. `false` before first `play()` or after `reset()`.
|
|
439
296
|
|
|
440
297
|
```javascript
|
|
441
|
-
const player = new SequentialAudio(clips);
|
|
442
|
-
|
|
443
298
|
console.log(player.isStarted); // false
|
|
444
|
-
|
|
445
299
|
player.play();
|
|
446
|
-
|
|
447
300
|
console.log(player.isStarted); // true
|
|
301
|
+
player.reset();
|
|
302
|
+
console.log(player.isStarted); // false
|
|
448
303
|
```
|
|
449
304
|
|
|
450
305
|
---
|
|
@@ -458,76 +313,52 @@ Called when a clip **starts playing**.
|
|
|
458
313
|
```javascript
|
|
459
314
|
const player = new SequentialAudio(clips, {
|
|
460
315
|
onPlay: (clip) => {
|
|
461
|
-
console.log(`Now playing: ${clip.label}`);
|
|
462
316
|
updateProgressBar(clip.label);
|
|
463
317
|
}
|
|
464
318
|
});
|
|
465
319
|
```
|
|
466
320
|
|
|
467
|
-
**Parameter:**
|
|
468
|
-
- **`clip`** — The clip object being played `{ src, label }`
|
|
321
|
+
**Parameter:** `clip` — a copy of the clip object `{ src, label }`.
|
|
469
322
|
|
|
470
|
-
**
|
|
471
|
-
|
|
472
|
-
```javascript
|
|
473
|
-
const player = new SequentialAudio(clips, {
|
|
474
|
-
onPlay: (clip) => {
|
|
475
|
-
throw new Error('Oops!');
|
|
476
|
-
// Error logged, playback unaffected
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
```
|
|
323
|
+
**Error handling:** Errors are caught and logged. A throwing callback will not stall playback.
|
|
480
324
|
|
|
481
325
|
---
|
|
482
326
|
|
|
483
327
|
### `onEnd(clip)`
|
|
484
328
|
|
|
485
|
-
Called when a clip **finishes
|
|
486
|
-
|
|
487
|
-
Does NOT fire if you call `stop()` or `pause()` before the clip ends.
|
|
329
|
+
Called when a clip **finishes naturally**. Does not fire if `stop()` or `pause()` interrupts the clip.
|
|
488
330
|
|
|
489
331
|
```javascript
|
|
490
332
|
const player = new SequentialAudio(clips, {
|
|
491
333
|
onEnd: (clip) => {
|
|
492
334
|
console.log(`Finished: ${clip.label}`);
|
|
493
|
-
// Auto-advance is handled separately
|
|
494
335
|
}
|
|
495
336
|
});
|
|
496
337
|
```
|
|
497
338
|
|
|
498
|
-
**Parameter:**
|
|
499
|
-
- **`clip`** — The clip object that ended
|
|
500
|
-
|
|
501
339
|
**After `onEnd`:**
|
|
502
|
-
-
|
|
503
|
-
-
|
|
340
|
+
- `autoAdvance: true` → next clip starts after `advanceDelay` seconds
|
|
341
|
+
- `autoAdvance: false` → sequence waits for `next()` call
|
|
504
342
|
|
|
505
343
|
---
|
|
506
344
|
|
|
507
345
|
### `onComplete()`
|
|
508
346
|
|
|
509
|
-
Called when the entire sequence finishes (no more clips).
|
|
510
|
-
|
|
511
|
-
Only fires if `autoAdvance: true` OR user explicitly calls `next()` on the last clip.
|
|
347
|
+
Called when the entire sequence finishes (no more clips and `loop: false`).
|
|
512
348
|
|
|
513
349
|
```javascript
|
|
514
350
|
const player = new SequentialAudio(clips, {
|
|
515
351
|
onComplete: () => {
|
|
516
|
-
console.log('Sequence complete!');
|
|
517
352
|
showCongratulationsScreen();
|
|
518
353
|
}
|
|
519
354
|
});
|
|
520
355
|
```
|
|
521
356
|
|
|
522
|
-
**After `onComplete`:**
|
|
523
|
-
- If `loop: true` → Sequence wraps to beginning, ready for replay
|
|
524
|
-
- If `loop: false` → Sequence stops
|
|
525
|
-
|
|
526
357
|
---
|
|
527
358
|
|
|
528
359
|
## Usage Patterns
|
|
529
360
|
|
|
530
|
-
###
|
|
361
|
+
### Narrated Story (Manual Advance)
|
|
531
362
|
|
|
532
363
|
```javascript
|
|
533
364
|
const story = new SequentialAudio([
|
|
@@ -535,9 +366,9 @@ const story = new SequentialAudio([
|
|
|
535
366
|
{ src: 'chapter2.mp3', label: 'Chapter 2' },
|
|
536
367
|
{ src: 'chapter3.mp3', label: 'Chapter 3' }
|
|
537
368
|
], {
|
|
538
|
-
autoAdvance: false,
|
|
539
|
-
onPlay:
|
|
540
|
-
onComplete:
|
|
369
|
+
autoAdvance: false,
|
|
370
|
+
onPlay: (clip) => updateUI(`Reading: ${clip.label}`),
|
|
371
|
+
onComplete: () => showTheEnd()
|
|
541
372
|
});
|
|
542
373
|
|
|
543
374
|
document.addEventListener('click', () => story.play(), { once: true });
|
|
@@ -549,106 +380,64 @@ document.getElementById('prev-btn').addEventListener('click', () => {
|
|
|
549
380
|
|
|
550
381
|
---
|
|
551
382
|
|
|
552
|
-
###
|
|
383
|
+
### Tutorial with Auto-Advance
|
|
553
384
|
|
|
554
385
|
```javascript
|
|
555
386
|
const tutorial = new SequentialAudio([
|
|
556
|
-
{ src: 'intro.mp3',
|
|
557
|
-
{ src: 'step1.mp3',
|
|
558
|
-
{ src: 'step2.mp3',
|
|
559
|
-
{ src: 'conclusion.mp3',
|
|
387
|
+
{ src: 'intro.mp3', label: 'Introduction' },
|
|
388
|
+
{ src: 'step1.mp3', label: 'Step 1' },
|
|
389
|
+
{ src: 'step2.mp3', label: 'Step 2' },
|
|
390
|
+
{ src: 'conclusion.mp3', label: 'Conclusion' }
|
|
560
391
|
], {
|
|
561
|
-
autoAdvance:
|
|
562
|
-
|
|
563
|
-
|
|
392
|
+
autoAdvance: true,
|
|
393
|
+
advanceDelay: 0.5,
|
|
394
|
+
onPlay: (clip) => highlightStep(clip.label),
|
|
395
|
+
onComplete: () => showCompletionCertificate()
|
|
564
396
|
});
|
|
565
397
|
|
|
566
|
-
document.getElementById('start-tutorial').addEventListener('click', () =>
|
|
567
|
-
tutorial.play();
|
|
568
|
-
});
|
|
398
|
+
document.getElementById('start-tutorial').addEventListener('click', () => tutorial.play());
|
|
569
399
|
```
|
|
570
400
|
|
|
571
401
|
---
|
|
572
402
|
|
|
573
|
-
###
|
|
403
|
+
### Interactive Quiz
|
|
574
404
|
|
|
575
405
|
```javascript
|
|
576
406
|
const quiz = new SequentialAudio([
|
|
577
407
|
{ src: 'question1.mp3', label: 'Question 1' },
|
|
578
408
|
{ src: 'question2.mp3', label: 'Question 2' },
|
|
579
|
-
{ src: '
|
|
580
|
-
{ src: 'results.mp3', label: 'Results' }
|
|
409
|
+
{ src: 'results.mp3', label: 'Results' }
|
|
581
410
|
], {
|
|
582
411
|
autoAdvance: false,
|
|
583
|
-
onPlay:
|
|
584
|
-
|
|
585
|
-
showQuestion(clip.label);
|
|
586
|
-
},
|
|
587
|
-
onComplete: () => {
|
|
588
|
-
// Show final score
|
|
589
|
-
showResults();
|
|
590
|
-
}
|
|
412
|
+
onPlay: (clip) => showQuestion(clip.label),
|
|
413
|
+
onComplete: () => showResults()
|
|
591
414
|
});
|
|
592
415
|
|
|
593
|
-
// User answers question, then advances
|
|
594
416
|
function submitAnswer(answer) {
|
|
595
417
|
recordAnswer(answer);
|
|
596
|
-
quiz.next();
|
|
418
|
+
quiz.next();
|
|
597
419
|
}
|
|
598
420
|
```
|
|
599
421
|
|
|
600
422
|
---
|
|
601
423
|
|
|
602
|
-
###
|
|
603
|
-
|
|
604
|
-
```javascript
|
|
605
|
-
const loopingNarration = new SequentialAudio([
|
|
606
|
-
{ src: 'intro.mp3', label: 'Intro' },
|
|
607
|
-
{ src: 'main.mp3', label: 'Main Message' },
|
|
608
|
-
{ src: 'outro.mp3', label: 'Outro' }
|
|
609
|
-
], {
|
|
610
|
-
autoAdvance: true,
|
|
611
|
-
loop: true, // Restart after outro
|
|
612
|
-
onPlay: (clip) => console.log(`[${clip.label}]`)
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
document.addEventListener('click', () => loopingNarration.play(), { once: true });
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
---
|
|
619
|
-
|
|
620
|
-
### Pattern 5: Guided Tour with Skip/Rewind
|
|
424
|
+
### Guided Tour with Skip/Rewind
|
|
621
425
|
|
|
622
426
|
```javascript
|
|
623
427
|
const tour = new SequentialAudio([
|
|
624
|
-
{ src: 'welcome.mp3',
|
|
428
|
+
{ src: 'welcome.mp3', label: 'Welcome' },
|
|
625
429
|
{ src: 'feature1.mp3', label: 'Feature 1: Dashboard' },
|
|
626
430
|
{ src: 'feature2.mp3', label: 'Feature 2: Settings' },
|
|
627
|
-
{ src: '
|
|
628
|
-
{ src: 'thanks.mp3', label: 'Thanks' }
|
|
431
|
+
{ src: 'thanks.mp3', label: 'Thanks' }
|
|
629
432
|
], {
|
|
630
|
-
onPlay:
|
|
631
|
-
onComplete: ()
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
// Play from start
|
|
635
|
-
document.getElementById('start-tour').addEventListener('click', () => tour.play());
|
|
636
|
-
|
|
637
|
-
// Jump to specific section
|
|
638
|
-
document.getElementById('skip-to-settings').addEventListener('click', () => {
|
|
639
|
-
tour.gotoLabel('Feature 2: Settings');
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// Rewind one step
|
|
643
|
-
document.getElementById('previous').addEventListener('click', () => {
|
|
644
|
-
const idx = Math.max(0, tour.getCurrentIndex() - 1);
|
|
645
|
-
tour.goto(idx);
|
|
433
|
+
onPlay: (clip) => highlightFeature(clip.label),
|
|
434
|
+
onComplete: () => completeTour()
|
|
646
435
|
});
|
|
647
436
|
|
|
648
|
-
|
|
649
|
-
document.getElementById('
|
|
650
|
-
|
|
651
|
-
|
|
437
|
+
document.getElementById('start-tour').addEventListener('click', () => tour.play());
|
|
438
|
+
document.getElementById('skip-to-settings').addEventListener('click', () => tour.gotoLabel('Feature 2: Settings'));
|
|
439
|
+
document.getElementById('previous').addEventListener('click', () => tour.goto(Math.max(0, tour.getCurrentIndex() - 1)));
|
|
440
|
+
document.getElementById('next').addEventListener('click', () => tour.next());
|
|
652
441
|
```
|
|
653
442
|
|
|
654
443
|
---
|
|
@@ -657,9 +446,6 @@ document.getElementById('next').addEventListener('click', () => {
|
|
|
657
446
|
|
|
658
447
|
### Audio Won't Play on First Click
|
|
659
448
|
|
|
660
|
-
**Problem:** Calling `play()` outside a user gesture.
|
|
661
|
-
|
|
662
|
-
**Solution:**
|
|
663
449
|
```javascript
|
|
664
450
|
// ✅ Correct
|
|
665
451
|
document.addEventListener('click', () => player.play());
|
|
@@ -668,32 +454,17 @@ document.addEventListener('click', () => player.play());
|
|
|
668
454
|
setTimeout(() => player.play(), 1000);
|
|
669
455
|
```
|
|
670
456
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
### Can't Advance to Next Clip
|
|
674
|
-
|
|
675
|
-
**Problem:** Calling `next()` but nothing happens.
|
|
457
|
+
### Stop Doesn't Prevent Auto-Advance (v1.1.0 and earlier)
|
|
676
458
|
|
|
677
|
-
|
|
459
|
+
This was fixed in v1.2.0. `stop()` now calls `clearTimeout` on the pending advance timer. Upgrade to v1.2.0 to resolve.
|
|
678
460
|
|
|
679
|
-
|
|
680
|
-
```javascript
|
|
681
|
-
if (player.getCurrentIndex() < player.getClipCount() - 1) {
|
|
682
|
-
player.next();
|
|
683
|
-
} else if (loop) {
|
|
684
|
-
// Already handled by library
|
|
685
|
-
player.next();
|
|
686
|
-
}
|
|
687
|
-
```
|
|
461
|
+
### Stop During Unlock Still Plays Clip (v1.2.0 and earlier)
|
|
688
462
|
|
|
689
|
-
|
|
463
|
+
This was fixed in v1.3.0. `stop()` and `reset()` now set a `#playCancelled` flag that the unlock `.then()` checks before starting playback. Previously, calling `stop()` or `reset()` in the ~50–200 ms window between `play()` being called and the silent-MP3 unlock resolving was silently ignored — the clip would start anyway. Upgrade to v1.3.0 to resolve.
|
|
690
464
|
|
|
691
465
|
### Progress Bar Shows Wrong Index
|
|
692
466
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
**Solution:** `getCurrentIndex()` is 0-based, so display `index + 1`:
|
|
696
|
-
|
|
467
|
+
`getCurrentIndex()` is 0-based:
|
|
697
468
|
```javascript
|
|
698
469
|
const index = player.getCurrentIndex();
|
|
699
470
|
console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
|
|
@@ -701,34 +472,15 @@ console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
|
|
|
701
472
|
|
|
702
473
|
---
|
|
703
474
|
|
|
704
|
-
### onComplete Fires Too Early
|
|
705
|
-
|
|
706
|
-
**Problem:** Callback fires before last clip actually ends.
|
|
707
|
-
|
|
708
|
-
**Cause:** `autoAdvance: true` fires `next()` → triggers `onComplete` before last clip fully plays.
|
|
709
|
-
|
|
710
|
-
**Solution:** Use manual advance or check if you really want `onComplete` at this time:
|
|
711
|
-
|
|
712
|
-
```javascript
|
|
713
|
-
const player = new SequentialAudio(clips, {
|
|
714
|
-
autoAdvance: false, // User controls advancement
|
|
715
|
-
onComplete: () => {
|
|
716
|
-
// Now fires only when user explicitly ends sequence
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
---
|
|
722
|
-
|
|
723
475
|
## Browser Compatibility
|
|
724
476
|
|
|
725
477
|
| Browser | Support | Notes |
|
|
726
478
|
|---------|---------|-------|
|
|
727
|
-
| Chrome 70+ | ✅ Full |
|
|
479
|
+
| Chrome 70+ | ✅ Full | Gesture required first time |
|
|
728
480
|
| Firefox 65+ | ✅ Full | Same as Chrome |
|
|
729
|
-
| Safari 12+ | ✅ Full | iOS: gesture required
|
|
481
|
+
| Safari 12+ | ✅ Full | iOS: gesture required |
|
|
730
482
|
| Edge 79+ | ✅ Full | Same as Chrome |
|
|
731
|
-
| iOS Safari 12+ | ✅ Full |
|
|
483
|
+
| iOS Safari 12+ | ✅ Full | Extensively tested |
|
|
732
484
|
| Chrome Android | ✅ Full | Gesture required |
|
|
733
485
|
|
|
734
486
|
---
|
|
@@ -738,13 +490,46 @@ const player = new SequentialAudio(clips, {
|
|
|
738
490
|
- **File Size:** ~5 KB (minified)
|
|
739
491
|
- **Gzipped:** ~2 KB
|
|
740
492
|
- **Runtime Memory:** < 150 KB per player
|
|
741
|
-
- **CPU:** Negligible
|
|
493
|
+
- **CPU:** Negligible
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## Changelog
|
|
498
|
+
|
|
499
|
+
### v1.3.0 (March 2026)
|
|
500
|
+
- **`#playCancelled` flag** — `stop()` and `reset()` now plant a cancellation token that the in-flight silent-MP3 unlock `.then()` checks before calling `#playClip()`. Previously, calling `stop()` or `reset()` during the ~50–200 ms unlock window was silently ignored — the clip would start regardless.
|
|
501
|
+
- **`stop()` doc updated** — Behaviour note now covers both the auto-advance timer cancellation (v1.2.0) and the new unlock cancellation (v1.3.0).
|
|
502
|
+
- **Usage example fix** — `goto(getCurrentIndex() - 1)` in the guided-tour example now uses `Math.max(0, …)`, preventing a spurious out-of-range `console.warn` when the user is already on the first clip.
|
|
503
|
+
|
|
504
|
+
### v1.2.0 (March 2026)
|
|
505
|
+
- `isPlaying` and `isStarted` are now read-only getters backed by private fields
|
|
506
|
+
- `#isPlaying` and `#isStarted` set synchronously before `#audio.play()` in `#playClip()`, closing double-play race window
|
|
507
|
+
- `#advanceTimer` stored and cleared by `stop()`, `reset()`, and `destroy()`
|
|
508
|
+
- `resume()` race fixed — `#isPlaying` set synchronously, reverted in `.catch()`
|
|
509
|
+
- `destroy()` uses `removeAttribute('src')` + `load()` per WHATWG spec
|
|
510
|
+
- `getCurrentClip()` returns shallow copy; `getClips()` deep-copies inner objects
|
|
511
|
+
|
|
512
|
+
### v1.1.0 (March 2026)
|
|
513
|
+
- Unlock now plays on the shared `#audio` element (fixes `NotAllowedError` on iOS Safari)
|
|
514
|
+
- `play()` guard extended to check `isStarted`
|
|
515
|
+
- `next()` / `goto()` / `gotoLabel()` guard against uninitialised player
|
|
516
|
+
- `pause()` / `resume()` guard on `isStarted`
|
|
517
|
+
- `destroy()` removes `ended` listener via stored `#endedHandler` reference
|
|
518
|
+
- `#isDestroyed` flag — all public methods safe no-ops after `destroy()`
|
|
519
|
+
- `advanceDelay` option added (default `0.5s`)
|
|
520
|
+
|
|
521
|
+
### v1.0.0 (March 2026)
|
|
522
|
+
- Initial release: sequential playlist player
|
|
523
|
+
- Click-to-advance playback control
|
|
524
|
+
- Jump to clip by index (`goto()`) or label (`gotoLabel()`)
|
|
525
|
+
- Pause/resume support
|
|
526
|
+
- Progress tracking (`getCurrentClip()`, `getCurrentIndex()`, `getClipCount()`)
|
|
742
527
|
|
|
743
528
|
---
|
|
744
529
|
|
|
745
530
|
## License
|
|
746
531
|
|
|
747
|
-
|
|
532
|
+
Apache License 2.0. See [LICENSE](../LICENSE).
|
|
748
533
|
|
|
749
534
|
---
|
|
750
535
|
|
|
@@ -757,4 +542,4 @@ GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
|
|
|
757
542
|
|
|
758
543
|
---
|
|
759
544
|
|
|
760
|
-
*Last updated: March
|
|
545
|
+
*Last updated: March 2026*
|