gapless 4.3.0 → 4.4.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/dist/index.d.ts +8 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -12
- package/src/Queue.ts +24 -8
- package/src/Track.ts +119 -61
- package/src/machines/queue.machine.ts +10 -6
- package/src/machines/track.machine.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gapless",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Gapless audio playback javascript plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -20,16 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"private": false,
|
|
23
|
-
"scripts": {
|
|
24
|
-
"test": "vitest run",
|
|
25
|
-
"build": "tsup",
|
|
26
|
-
"build:demo": "pnpm --dir demo build",
|
|
27
|
-
"dev": "pnpm --dir demo dev",
|
|
28
|
-
"types": "tsc --noEmit",
|
|
29
|
-
"test:watch": "vitest",
|
|
30
|
-
"test:coverage": "vitest run --coverage",
|
|
31
|
-
"prepublishOnly": "pnpm run build"
|
|
32
|
-
},
|
|
33
23
|
"repository": {
|
|
34
24
|
"type": "git",
|
|
35
25
|
"url": "git+https://github.com/RelistenNet/gapless.js.git"
|
|
@@ -47,8 +37,16 @@
|
|
|
47
37
|
"typescript": "^6.0.3",
|
|
48
38
|
"vitest": "^4.1.8"
|
|
49
39
|
},
|
|
50
|
-
"packageManager": "pnpm@10.30.2",
|
|
51
40
|
"dependencies": {
|
|
52
41
|
"xstate": "^5.32.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"build:demo": "pnpm --dir demo build",
|
|
47
|
+
"dev": "pnpm --dir demo dev",
|
|
48
|
+
"types": "tsc --noEmit",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"test:coverage": "vitest run --coverage"
|
|
53
51
|
}
|
|
54
52
|
}
|
package/src/Queue.ts
CHANGED
|
@@ -198,8 +198,9 @@ export class Queue implements TrackQueueRef {
|
|
|
198
198
|
const e = event as { type: 'SEEK'; time: number };
|
|
199
199
|
this._trackAt(context.currentTrackIndex)?.seek(e.time);
|
|
200
200
|
},
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
seekCurrentToStartTime: ({ context, event }) => {
|
|
202
|
+
const e = event as { type: 'GOTO'; startTime?: number };
|
|
203
|
+
this._trackAt(context.currentTrackIndex)?.seek(e.startTime ?? 0);
|
|
203
204
|
},
|
|
204
205
|
scheduleGapless: ({ context }) => {
|
|
205
206
|
this._tryScheduleGapless(context.currentTrackIndex);
|
|
@@ -271,20 +272,19 @@ export class Queue implements TrackQueueRef {
|
|
|
271
272
|
previous(): void {
|
|
272
273
|
const ct = this._currentTrack;
|
|
273
274
|
if (ct && ct.currentTime > 8) {
|
|
274
|
-
|
|
275
|
-
ct.play();
|
|
275
|
+
this._actor.send({ type: 'SEEK', time: 0 });
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
this._actor.send({ type: 'PREVIOUS' });
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
gotoTrack(index: number, playImmediately = false): void {
|
|
282
|
+
gotoTrack(index: number, playImmediately = false, startTime?: number): void {
|
|
283
283
|
if (index < 0 || index >= this._tracks.length) return;
|
|
284
284
|
this.onDebug(
|
|
285
|
-
`gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`
|
|
285
|
+
`gotoTrack(${index}, playImmediately=${playImmediately}, startTime=${startTime}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`
|
|
286
286
|
);
|
|
287
|
-
this._actor.send({ type: 'GOTO', index, playImmediately });
|
|
287
|
+
this._actor.send({ type: 'GOTO', index, playImmediately, startTime });
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
seek(time: number): void {
|
|
@@ -530,6 +530,11 @@ export class Queue implements TrackQueueRef {
|
|
|
530
530
|
track.cancelGaplessStart();
|
|
531
531
|
this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);
|
|
532
532
|
}
|
|
533
|
+
// Cancel any HTML5 gain mute that was scheduled on the current track,
|
|
534
|
+
// but only if it's still in HTML5 state. After crossover the gain node
|
|
535
|
+
// is managed by the crossfade ramp — cancelScheduledValues would wipe it.
|
|
536
|
+
const cur = this._trackAt(this._actor.getSnapshot().context.currentTrackIndex);
|
|
537
|
+
if (cur && cur.playbackType === 'HTML5') cur.cancelHtml5Mute();
|
|
533
538
|
this._scheduledNextIndex = null;
|
|
534
539
|
}
|
|
535
540
|
|
|
@@ -564,7 +569,18 @@ export class Queue implements TrackQueueRef {
|
|
|
564
569
|
return;
|
|
565
570
|
}
|
|
566
571
|
|
|
567
|
-
next.scheduleGaplessStart(endTime)
|
|
572
|
+
if (!next.scheduleGaplessStart(endTime)) {
|
|
573
|
+
this.onDebug(
|
|
574
|
+
`_tryScheduleGapless: track ${nextIndex} rejected SCHEDULE_GAPLESS (state=${next.machineState})`
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// When scheduling from an HTML5-clock prediction, mute the HTML5 gain
|
|
579
|
+
// at endTime so a stalled/late HTML5 element is silenced rather than
|
|
580
|
+
// overlapping the next track.
|
|
581
|
+
if (current.playbackType === 'HTML5') {
|
|
582
|
+
current.scheduleHtml5Mute(endTime);
|
|
583
|
+
}
|
|
568
584
|
this.onDebug(
|
|
569
585
|
`_tryScheduleGapless: scheduled track ${nextIndex} at endTime=${endTime.toFixed(3)} (in ${(endTime - ctx.currentTime).toFixed(1)}s) curPlaybackType=${current.playbackType}`
|
|
570
586
|
);
|
package/src/Track.ts
CHANGED
|
@@ -273,6 +273,11 @@ export class Track {
|
|
|
273
273
|
// half a second of silence/garbage. _bufferStartPaddingSec is
|
|
274
274
|
// computed at decode time via _ensureBufferAlignment.
|
|
275
275
|
this.sourceNode.start(when, this._bufferStartPaddingSec);
|
|
276
|
+
const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec;
|
|
277
|
+
const schedRate = this.sourceNode.playbackRate.value || 1;
|
|
278
|
+
if (bufRemaining > 0) {
|
|
279
|
+
this.sourceNode.stop(when + bufRemaining / schedRate);
|
|
280
|
+
}
|
|
276
281
|
this._waRefCtxTime = when;
|
|
277
282
|
this._waRefTrackTime = 0;
|
|
278
283
|
this.queueRef.onDebug(
|
|
@@ -353,14 +358,33 @@ export class Track {
|
|
|
353
358
|
}
|
|
354
359
|
|
|
355
360
|
setPlaybackRate(rate: number): void {
|
|
356
|
-
// Freeze current track position at the old rate before switching
|
|
357
|
-
|
|
361
|
+
// Freeze current track position at the old rate before switching.
|
|
362
|
+
// Skip the anchor rewrite during the scheduling lead (when the source
|
|
363
|
+
// is scheduled but ctx.currentTime hasn't reached _waRefCtxTime yet) —
|
|
364
|
+
// the source hasn't started producing audio, so the anchors are still
|
|
365
|
+
// correct for the future start. Rewriting them with a negative elapsed
|
|
366
|
+
// time would corrupt playbackEndContextTime and cause overlap/gap.
|
|
367
|
+
if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying
|
|
368
|
+
&& this.ctx.currentTime >= this._waRefCtxTime) {
|
|
358
369
|
const oldRate = this.sourceNode.playbackRate.value;
|
|
359
370
|
this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;
|
|
360
371
|
this._waRefCtxTime = this.ctx.currentTime;
|
|
361
372
|
}
|
|
362
373
|
this.audio.playbackRate = rate;
|
|
363
|
-
if (this.sourceNode)
|
|
374
|
+
if (this.sourceNode) {
|
|
375
|
+
this.sourceNode.playbackRate.value = rate;
|
|
376
|
+
// Re-issue the hard stop ceiling at the new end time. The spec
|
|
377
|
+
// allows repeated stop() calls (latest wins), so this overwrites
|
|
378
|
+
// the stale stop scheduled at the old rate.
|
|
379
|
+
if (this.audioBuffer && this.ctx) {
|
|
380
|
+
const remaining = this.audioBuffer.duration - this._bufferStartPaddingSec - this._waRefTrackTime;
|
|
381
|
+
if (remaining > 0) {
|
|
382
|
+
try {
|
|
383
|
+
this.sourceNode.stop(this._waRefCtxTime + remaining / rate);
|
|
384
|
+
} catch { /* already stopped */ }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
364
388
|
}
|
|
365
389
|
|
|
366
390
|
preload(): void {
|
|
@@ -411,15 +435,34 @@ export class Track {
|
|
|
411
435
|
// Gapless scheduling (called by Queue)
|
|
412
436
|
// --------------------------------------------------------------------------
|
|
413
437
|
|
|
438
|
+
/** Schedule the HTML5 gain node to mute at `when` — safety valve so an
|
|
439
|
+
* HTML5 element that runs past the predicted end is silenced instead of
|
|
440
|
+
* overlapping the next track. No-op if the HTML5 gain path isn't active. */
|
|
441
|
+
scheduleHtml5Mute(when: number): void {
|
|
442
|
+
if (!this._html5GainNode || !this.ctx) return;
|
|
443
|
+
this._html5GainNode.gain.setValueAtTime(1, when - 0.005);
|
|
444
|
+
this._html5GainNode.gain.linearRampToValueAtTime(0, when);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Cancel any scheduled HTML5 gain mute (called when gapless is cancelled). */
|
|
448
|
+
cancelHtml5Mute(): void {
|
|
449
|
+
if (!this._html5GainNode || !this.ctx) return;
|
|
450
|
+
this._html5GainNode.gain.cancelScheduledValues(this.ctx.currentTime);
|
|
451
|
+
this._html5GainNode.gain.setValueAtTime(1, this.ctx.currentTime);
|
|
452
|
+
}
|
|
453
|
+
|
|
414
454
|
cancelGaplessStart(): void {
|
|
415
455
|
const snap = this._actor.getSnapshot();
|
|
416
456
|
if (snap.context.scheduledStartContextTime === null) return;
|
|
417
457
|
this._actor.send({ type: 'CANCEL_GAPLESS' });
|
|
418
458
|
}
|
|
419
459
|
|
|
420
|
-
scheduleGaplessStart(when: number):
|
|
421
|
-
if (!this.ctx || !this.audioBuffer || !this.gainNode) return;
|
|
460
|
+
scheduleGaplessStart(when: number): boolean {
|
|
461
|
+
if (!this.ctx || !this.audioBuffer || !this.gainNode) return false;
|
|
462
|
+
const state = this._actor.getSnapshot().value;
|
|
463
|
+
if (state !== 'idle' && state !== 'loading') return false;
|
|
422
464
|
this._actor.send({ type: 'SCHEDULE_GAPLESS', when });
|
|
465
|
+
return true;
|
|
423
466
|
}
|
|
424
467
|
|
|
425
468
|
// --------------------------------------------------------------------------
|
|
@@ -605,51 +648,46 @@ export class Track {
|
|
|
605
648
|
return;
|
|
606
649
|
}
|
|
607
650
|
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// If _html5GainNode wasn't created (createMediaElementSource fell back —
|
|
619
|
-
// e.g. CORS blocked or legacy browser), we degrade to immediate pause +
|
|
620
|
-
// WebAudio fade-in only.
|
|
621
|
-
const t0 = this.ctx.currentTime;
|
|
622
|
-
const t1 = t0 + CROSSOVER_FADE_SEC;
|
|
623
|
-
|
|
624
|
-
if (this._html5GainNode) {
|
|
625
|
-
this._html5GainNode.gain.cancelScheduledValues(t0);
|
|
626
|
-
this._html5GainNode.gain.setValueAtTime(1, t0);
|
|
627
|
-
this._html5GainNode.gain.linearRampToValueAtTime(0, t1);
|
|
628
|
-
// Pause the HTML5 element after the fade completes — it stops consuming
|
|
629
|
-
// network/decoder resources and the gain is back to silent regardless.
|
|
630
|
-
// Reset the gain to 1 afterwards so future plays through this element
|
|
631
|
-
// (post-deactivate/reactivate) start at full level.
|
|
651
|
+
// Start the WebAudio source first to learn the exact context-clock time
|
|
652
|
+
// it will begin producing audio (the "when" returned by _startSourceNode).
|
|
653
|
+
// Then align the HTML5 fade-out to that same instant so both crossfade
|
|
654
|
+
// halves overlap at constant-summed gain.
|
|
655
|
+
const when = this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);
|
|
656
|
+
|
|
657
|
+
if (when !== null && this._html5GainNode) {
|
|
658
|
+
this._html5GainNode.gain.cancelScheduledValues(when);
|
|
659
|
+
this._html5GainNode.gain.setValueAtTime(1, when);
|
|
660
|
+
this._html5GainNode.gain.linearRampToValueAtTime(0, when + CROSSOVER_FADE_SEC);
|
|
632
661
|
const ctxRef = this.ctx;
|
|
633
662
|
const html5GainRef = this._html5GainNode;
|
|
663
|
+
const delayMs = (when - this.ctx.currentTime + CROSSOVER_FADE_SEC) * 1000 + 5;
|
|
634
664
|
setTimeout(() => {
|
|
635
665
|
this.audio.pause();
|
|
636
666
|
if (ctxRef && html5GainRef) {
|
|
637
667
|
html5GainRef.gain.cancelScheduledValues(ctxRef.currentTime);
|
|
638
668
|
html5GainRef.gain.setValueAtTime(1, ctxRef.currentTime);
|
|
639
669
|
}
|
|
640
|
-
},
|
|
641
|
-
} else {
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
this.
|
|
646
|
-
|
|
670
|
+
}, delayMs);
|
|
671
|
+
} else if (!this._html5GainNode) {
|
|
672
|
+
// No MediaElementSource path (CORS blocked or legacy browser).
|
|
673
|
+
// Delay the HTML5 pause until the WebAudio source actually starts
|
|
674
|
+
// at `when` so there's no silence gap during the scheduling lead.
|
|
675
|
+
if (when !== null && this.ctx) {
|
|
676
|
+
const delayMs = (when - this.ctx.currentTime) * 1000;
|
|
677
|
+
setTimeout(() => {
|
|
678
|
+
this.audio.volume = 0;
|
|
679
|
+
this.audio.pause();
|
|
680
|
+
this.audio.volume = 1;
|
|
681
|
+
}, Math.max(0, delayMs));
|
|
682
|
+
} else {
|
|
683
|
+
this.audio.volume = 0;
|
|
684
|
+
this.audio.pause();
|
|
685
|
+
this.audio.volume = 1;
|
|
686
|
+
}
|
|
647
687
|
}
|
|
648
688
|
|
|
649
|
-
this._startSourceNode(this.pausedAtTrackTime, CROSSOVER_FADE_SEC);
|
|
650
|
-
|
|
651
689
|
this.queueRef.onDebug(
|
|
652
|
-
`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${CROSSOVER_FADE_SEC}s mediaSource=${!!this._html5GainNode}`
|
|
690
|
+
`crossoverHtml5ToWebAudio track=${this.index} offset=${this.pausedAtTrackTime.toFixed(3)} wasPlaying=true fade=${CROSSOVER_FADE_SEC}s mediaSource=${!!this._html5GainNode} when=${when?.toFixed(3) ?? 'null'}`
|
|
653
691
|
);
|
|
654
692
|
}
|
|
655
693
|
|
|
@@ -671,16 +709,13 @@ export class Track {
|
|
|
671
709
|
);
|
|
672
710
|
}
|
|
673
711
|
|
|
674
|
-
private _startSourceNode(offset: number, fadeInSec = 0):
|
|
675
|
-
if (!this.ctx || !this.audioBuffer || !this.gainNode) return;
|
|
676
|
-
// Re-check alignment in case audio.duration became available since the
|
|
677
|
-
// last computation (e.g., preloaded track whose metadata load completed).
|
|
712
|
+
private _startSourceNode(offset: number, fadeInSec = 0): number | null {
|
|
713
|
+
if (!this.ctx || !this.audioBuffer || !this.gainNode) return null;
|
|
678
714
|
this._maybeComputeBufferAlignment();
|
|
679
715
|
this._stopSourceNode();
|
|
680
716
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (this.ctx.state === 'suspended') {
|
|
717
|
+
const wasSuspended = this.ctx.state === 'suspended';
|
|
718
|
+
if (wasSuspended) {
|
|
684
719
|
this.ctx.resume();
|
|
685
720
|
}
|
|
686
721
|
|
|
@@ -688,30 +723,53 @@ export class Track {
|
|
|
688
723
|
this.sourceNode.buffer = this.audioBuffer;
|
|
689
724
|
this.sourceNode.playbackRate.value = this.queueRef.playbackRate;
|
|
690
725
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
726
|
+
// Schedule the source at an explicit future time rather than start(0).
|
|
727
|
+
// start(0) dispatches to the audio render thread asynchronously — the
|
|
728
|
+
// actual start lags the main-thread currentTime read by up to one
|
|
729
|
+
// hardware callback period (5-100 ms depending on device). That δ makes
|
|
730
|
+
// playbackEndContextTime compute an end time δ too early, causing the
|
|
731
|
+
// next gapless track to overlap briefly. Using start(when) with an
|
|
732
|
+
// explicit when that matches _waRefCtxTime eliminates the mismatch.
|
|
733
|
+
const lead = wasSuspended
|
|
734
|
+
? 0.15
|
|
735
|
+
: Math.max(0.02, 2 * ((this.ctx as unknown as { baseLatency?: number }).baseLatency || 0) + 0.01);
|
|
736
|
+
const when = this.ctx.currentTime + lead;
|
|
737
|
+
|
|
738
|
+
// Advance the buffer start position by the lead so the source begins
|
|
739
|
+
// from where playback will actually be at `when`, not where it was at
|
|
740
|
+
// the moment we read currentTime. This keeps the currentTime formula
|
|
741
|
+
// (waRefTrackTime + (ctx.currentTime - waRefCtxTime) * rate) equal to
|
|
742
|
+
// `offset` immediately after the call without accumulating drift across
|
|
743
|
+
// pause/resume cycles. During crossover it also aligns the WebAudio
|
|
744
|
+
// source to where the HTML5 element will be at the fade point.
|
|
745
|
+
// The skipped interval (≤20 ms at 1× rate) is below audible threshold.
|
|
746
|
+
const rate = this.queueRef.playbackRate;
|
|
747
|
+
const maxOffset = this.audioBuffer.duration - this._bufferStartPaddingSec;
|
|
748
|
+
const effectiveOffset = Math.min(offset + lead * rate, maxOffset);
|
|
749
|
+
|
|
696
750
|
if (fadeInSec > 0) {
|
|
697
751
|
const fadeNode = this.ctx.createGain();
|
|
698
|
-
|
|
699
|
-
fadeNode.gain.
|
|
700
|
-
fadeNode.gain.linearRampToValueAtTime(1, t0 + fadeInSec);
|
|
752
|
+
fadeNode.gain.setValueAtTime(0, when);
|
|
753
|
+
fadeNode.gain.linearRampToValueAtTime(1, when + fadeInSec);
|
|
701
754
|
this.sourceNode.connect(fadeNode);
|
|
702
755
|
fadeNode.connect(this.gainNode);
|
|
703
756
|
} else {
|
|
704
757
|
this.sourceNode.connect(this.gainNode);
|
|
705
758
|
}
|
|
706
|
-
// gainNode → destination connected once at ctx setup; do not re-connect.
|
|
707
759
|
this.sourceNode.onended = this._handleWebAudioEnded;
|
|
708
760
|
|
|
709
|
-
this._waRefCtxTime =
|
|
710
|
-
this._waRefTrackTime =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
//
|
|
714
|
-
|
|
761
|
+
this._waRefCtxTime = when;
|
|
762
|
+
this._waRefTrackTime = effectiveOffset;
|
|
763
|
+
this.sourceNode.start(when, effectiveOffset + this._bufferStartPaddingSec);
|
|
764
|
+
|
|
765
|
+
// Hard ceiling: stop the source at its computed end time so any
|
|
766
|
+
// platform-level late start (render thread couldn't honor `when`)
|
|
767
|
+
// produces an inaudible truncation instead of overlap with the next track.
|
|
768
|
+
const bufRemaining = this.audioBuffer.duration - this._bufferStartPaddingSec - effectiveOffset;
|
|
769
|
+
if (bufRemaining > 0) {
|
|
770
|
+
this.sourceNode.stop(when + bufRemaining / rate);
|
|
771
|
+
}
|
|
772
|
+
return when;
|
|
715
773
|
}
|
|
716
774
|
|
|
717
775
|
private _stopSourceNode(): void {
|
|
@@ -33,7 +33,7 @@ export type QueueEvent =
|
|
|
33
33
|
| { type: 'TOGGLE' }
|
|
34
34
|
| { type: 'NEXT' }
|
|
35
35
|
| { type: 'PREVIOUS' }
|
|
36
|
-
| { type: 'GOTO'; index: number; playImmediately?: boolean }
|
|
36
|
+
| { type: 'GOTO'; index: number; playImmediately?: boolean; startTime?: number }
|
|
37
37
|
| { type: 'SEEK'; time: number }
|
|
38
38
|
| { type: 'SET_VOLUME'; volume: number }
|
|
39
39
|
| { type: 'ADD_TRACK' }
|
|
@@ -113,7 +113,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
113
113
|
playCurrent: () => {},
|
|
114
114
|
pauseCurrent: () => {},
|
|
115
115
|
seekCurrent: () => {},
|
|
116
|
-
|
|
116
|
+
seekCurrentToStartTime: () => {},
|
|
117
117
|
scheduleGapless: () => {},
|
|
118
118
|
cancelScheduledGapless: () => {},
|
|
119
119
|
cancelAndRescheduleGapless: () => {},
|
|
@@ -151,6 +151,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
151
151
|
'deactivateCurrent',
|
|
152
152
|
'cancelAllGapless',
|
|
153
153
|
'gotoTrackIndex',
|
|
154
|
+
'seekCurrentToStartTime',
|
|
154
155
|
'activateAndPlayCurrent',
|
|
155
156
|
'notifyStartNewTrack',
|
|
156
157
|
'updateMediaSessionMetadata',
|
|
@@ -163,7 +164,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
163
164
|
'deactivateCurrent',
|
|
164
165
|
'cancelAllGapless',
|
|
165
166
|
'gotoTrackIndex',
|
|
166
|
-
'
|
|
167
|
+
'seekCurrentToStartTime',
|
|
167
168
|
'preloadAhead',
|
|
168
169
|
],
|
|
169
170
|
},
|
|
@@ -239,6 +240,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
239
240
|
'deactivateCurrent',
|
|
240
241
|
'cancelAllGapless',
|
|
241
242
|
'gotoTrackIndex',
|
|
243
|
+
'seekCurrentToStartTime',
|
|
242
244
|
'activateAndPlayCurrent',
|
|
243
245
|
'notifyStartNewTrack',
|
|
244
246
|
'updateMediaSessionMetadata',
|
|
@@ -250,7 +252,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
250
252
|
'deactivateCurrent',
|
|
251
253
|
'cancelAllGapless',
|
|
252
254
|
'gotoTrackIndex',
|
|
253
|
-
'
|
|
255
|
+
'seekCurrentToStartTime',
|
|
254
256
|
'preloadAhead',
|
|
255
257
|
],
|
|
256
258
|
},
|
|
@@ -353,6 +355,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
353
355
|
'deactivateCurrent',
|
|
354
356
|
'cancelAllGapless',
|
|
355
357
|
'gotoTrackIndex',
|
|
358
|
+
'seekCurrentToStartTime',
|
|
356
359
|
'activateAndPlayCurrent',
|
|
357
360
|
'notifyStartNewTrack',
|
|
358
361
|
'updateMediaSessionMetadata',
|
|
@@ -364,7 +367,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
364
367
|
'deactivateCurrent',
|
|
365
368
|
'cancelAllGapless',
|
|
366
369
|
'gotoTrackIndex',
|
|
367
|
-
'
|
|
370
|
+
'seekCurrentToStartTime',
|
|
368
371
|
'preloadAhead',
|
|
369
372
|
],
|
|
370
373
|
},
|
|
@@ -421,6 +424,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
421
424
|
'deactivateCurrent',
|
|
422
425
|
'cancelAllGapless',
|
|
423
426
|
'gotoTrackIndex',
|
|
427
|
+
'seekCurrentToStartTime',
|
|
424
428
|
'activateAndPlayCurrent',
|
|
425
429
|
'notifyStartNewTrack',
|
|
426
430
|
'updateMediaSessionMetadata',
|
|
@@ -433,7 +437,7 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
433
437
|
'deactivateCurrent',
|
|
434
438
|
'cancelAllGapless',
|
|
435
439
|
'gotoTrackIndex',
|
|
436
|
-
'
|
|
440
|
+
'seekCurrentToStartTime',
|
|
437
441
|
'preloadAhead',
|
|
438
442
|
],
|
|
439
443
|
},
|
|
@@ -187,6 +187,7 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
187
187
|
'resetTiming',
|
|
188
188
|
'stopProgressLoop',
|
|
189
189
|
'clearScheduleAndLookahead',
|
|
190
|
+
'clearPendingPlay',
|
|
190
191
|
],
|
|
191
192
|
},
|
|
192
193
|
ACTIVATE: {
|
|
@@ -194,6 +195,7 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
194
195
|
'resetTiming',
|
|
195
196
|
'resetHtml5Element',
|
|
196
197
|
'clearScheduleAndLookahead',
|
|
198
|
+
'clearPendingPlay',
|
|
197
199
|
],
|
|
198
200
|
},
|
|
199
201
|
PLAY: [
|
|
@@ -400,13 +402,14 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
400
402
|
target: 'idle',
|
|
401
403
|
actions: [
|
|
402
404
|
'clearPlayingAndSchedule',
|
|
405
|
+
'clearPendingPlay',
|
|
403
406
|
'resetTiming',
|
|
404
407
|
'resetHtml5Element',
|
|
405
408
|
],
|
|
406
409
|
},
|
|
407
410
|
DEACTIVATE: {
|
|
408
411
|
target: 'idle',
|
|
409
|
-
actions: ['clearIsPlaying', 'resetTiming'],
|
|
412
|
+
actions: ['clearIsPlaying', 'clearPendingPlay', 'resetTiming'],
|
|
410
413
|
},
|
|
411
414
|
URL_RESOLVED: {
|
|
412
415
|
actions: 'setResolvedUrl',
|