gapless 4.0.9 → 4.0.11
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 +3 -0
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/Queue.ts +19 -1
- package/src/Track.ts +13 -1
- package/src/machines/fetchDecode.machine.ts +12 -8
- package/src/machines/queue.machine.ts +51 -2
- package/src/machines/track.machine.ts +16 -14
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ interface TrackInfo {
|
|
|
80
80
|
interface TrackQueueRef {
|
|
81
81
|
onTrackEnded(track: Track): void;
|
|
82
82
|
onTrackBufferReady(track: Track): void;
|
|
83
|
+
onPreloadReady(track: Track): void;
|
|
83
84
|
onProgress(info: TrackInfo): void;
|
|
84
85
|
onError(error: Error): void;
|
|
85
86
|
onPlayBlocked(): void;
|
|
@@ -109,6 +110,7 @@ declare class Track {
|
|
|
109
110
|
private readonly _actor;
|
|
110
111
|
private readonly queueRef;
|
|
111
112
|
private rafId;
|
|
113
|
+
private _notifiedPreloadThreshold;
|
|
112
114
|
constructor(opts: {
|
|
113
115
|
trackUrl: string;
|
|
114
116
|
index: number;
|
|
@@ -198,6 +200,7 @@ declare class Queue implements TrackQueueRef {
|
|
|
198
200
|
onProgress(info: TrackInfo): void;
|
|
199
201
|
onError(error: Error): void;
|
|
200
202
|
onPlayBlocked(): void;
|
|
203
|
+
onPreloadReady(track: Track): void;
|
|
201
204
|
onDebug(msg: string): void;
|
|
202
205
|
/** Look up a track by index — safe for use inside machine actions. */
|
|
203
206
|
private _trackAt;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createActor as K}from"xstate";var H=typeof window<"u",p;function B(){if(!H)return null;let r=window.AudioContext??window.webkitAudioContext;return r?new r:null}function k(){return p===void 0?null:p}function T(){p===void 0&&(p=B());let r=p;return r&&r.state==="suspended"?r.resume():Promise.resolve()}var y=typeof navigator<"u"&&"mediaSession"in navigator;function x(r){if(!y)return;let{mediaSession:e}=navigator;e.setActionHandler("play",r.onPlay),e.setActionHandler("pause",r.onPause),e.setActionHandler("nexttrack",r.onNext),e.setActionHandler("previoustrack",r.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&r.onSeek(t.seekTime)})}function v(r){if(y){if(!r){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:r.title??"",artist:r.artist??"",album:r.album??"",artwork:r.artwork??[]})}}function P(r){y&&(navigator.mediaSession.playbackState=r?"playing":"paused")}function b(r,e,t=1){if(y)try{navigator.mediaSession.setPositionState({duration:r,position:e,playbackRate:t})}catch{}}import{setup as $,assign as h}from"xstate";function D(r){return $({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>t.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):e.currentTrackIndex}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>e.currentTrackIndex+1}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:r,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["scheduleGapless","preloadAhead"]}}},paused:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as q,fromPromise as S}from"xstate";import{setup as W,assign as d}from"xstate";import{setup as F,assign as C,sendParent as A,fromPromise as E}from"xstate";var f=F({types:{context:{},input:{}},actors:{resolveUrl:E(async()=>null),fetchAudio:E(async()=>{}),decodeAudio:E(async()=>{})},guards:{shouldSkipHEAD:({context:r})=>r.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:({input:r})=>({trackUrl:r.trackUrl,resolvedUrl:r.resolvedUrl,skipHEAD:r.skipHEAD}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:r})=>({trackUrl:r.trackUrl}),onDone:{target:"fetching",actions:[C({resolvedUrl:({event:r,context:e})=>r.output??e.resolvedUrl,skipHEAD:()=>!0}),A(({event:r,context:e})=>({type:"URL_RESOLVED",url:r.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:C({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:r})=>({resolvedUrl:r.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:A({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:A({type:"BUFFER_READY"})},onError:{target:"error",actions:A({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function L(r){return W({types:{context:{},events:{}},actors:{fetchDecode:f},guards:{canPlayWebAudio:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&e.fetchDecodeRef===null},actions:{playHtml5:()=>{},startSourceNode:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},disconnectGain:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},setIsPlaying:d({isPlaying:()=>!0}),clearIsPlaying:d({isPlaying:()=>!1}),setLoadingState:d({webAudioLoadingState:()=>"LOADING"}),setLoadedState:d({webAudioLoadingState:()=>"LOADED"}),setErrorState:d({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:d({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:d({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:d({notifiedLookahead:()=>!0}),setResolvedUrl:d({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:d({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:d({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"})}}).createMachine({id:"track",initial:"idle",context:r,on:{START_FETCH:{guard:"canStartFetch",actions:d({webAudioLoadingState:()=>"LOADING",fetchDecodeRef:({context:e,spawn:t})=>t("fetchDecode",{id:"fetchDecode",input:{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}})})}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{target:"idle",actions:"setLoadedState"},BUFFER_ERROR:{target:"idle",actions:"setErrorState"},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["clearIsPlaying","freezePausedTime","stopSourceNode","disconnectGain","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var V=5,g=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_webAudioDisabled;get ctx(){if(this._webAudioDisabled)return null;let e=k();return e&&!this.gainNode&&(this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume),e}gainNode=null;sourceNode=null;audioBuffer=null;webAudioStartedAt=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var c,l;let n=(c=this.audio.error)==null?void 0:c.code;if(n===1)return;let s=((l=this.audio.error)==null?void 0:l.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${n}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._webAudioDisabled=e.queue.webAudioIsDisabled;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchDecodeRef:null},i=L(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode)},actors:{fetchDecode:f.provide({actors:{resolveUrl:S(async({signal:n})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:n});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:S(async({input:n,signal:s})=>{let{resolvedUrl:c}=n,l=await fetch(c,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${c}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:S(async()=>{let n=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!n||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(n),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:n})=>{let s=n.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this.webAudioStartedAt=s,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{this.pausedAtTrackTime=this.currentTime},stopSourceNode:()=>this._stopSourceNode(),disconnectGain:()=>this._disconnectGain(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>{queueMicrotask(()=>this.queueRef.onProgress(this.toInfo()))},seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this.webAudioStartedAt=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=q(i),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!this.audioBuffer&&(T(),this.ctx&&this._actor.send({type:"START_FETCH"}))}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this.ctx.currentTime-this.webAudioStartedAt):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load())}_startSourceNode(e){!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.webAudioStartedAt=this.ctx.currentTime-e,this.sourceNode.start(0,e))}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_disconnectGain(){if(!(!this.gainNode||!this.ctx))try{this.gainNode.disconnect(this.ctx.destination)}catch{}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,i=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(i)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=V&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function N(r,e){let t=0;return(...i)=>{let n=performance.now();n-t<e||(t=n,r(...i))}}var Y=2,m=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;webAudioIsDisabled;_volume;_scheduledNextIndex=null;_throttledUpdatePositionState=N((e,t)=>b(e,t),1e3);constructor(e={}){let{tracks:t=[],onProgress:i,onEnded:n,onPlayNextTrack:s,onPlayPreviousTrack:c,onStartNewTrack:l,onError:I,onPlayBlocked:R,onDebug:U,webAudioIsDisabled:M=!1,trackMetadata:w=[],volume:O=1}=e;this._volume=Math.min(1,Math.max(0,O)),this.webAudioIsDisabled=M,this._onProgress=i,this._onEnded=n,this._onPlayNextTrack=s,this._onPlayPreviousTrack=c,this._onStartNewTrack=l,this._onError=I,this._onPlayBlocked=R,this._onDebug=U,this._tracks=t.map((o,a)=>new g({trackUrl:o,index:a,queue:this,metadata:w[a]}));let G=D({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},deactivateEndedTrack:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},activateAndPlayCurrent:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(a.activate(),this._scheduledNextIndex!==a.index&&a.play())},playOrContinueGapless:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(this._scheduledNextIndex!==a.index?a.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${a.index} \u2014 sourceNode=${a.hasSourceNode} isPlaying=${a.isPlaying} machineState=${a.machineState}`),a.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onStartNewTrack)==null||u.call(this,a.toInfo()))},notifyPlayNextTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onPlayNextTrack)==null||u.call(this,a.toInfo()))},notifyPlayPreviousTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onPlayPreviousTrack)==null||u.call(this,a.toInfo()))},notifyEnded:()=>{var o;return(o=this._onEnded)==null?void 0:o.call(this)},updateMediaSessionMetadata:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&v(a.metadata)},preloadAhead:({context:o})=>{this._preloadAhead(o.currentTrackIndex)},playCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.play()},pauseCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.pause()},seekCurrent:({context:o,event:a})=>{var _;let u=a;(_=this._trackAt(o.currentTrackIndex))==null||_.seek(u.time)},seekCurrentToZero:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.seek(0)},scheduleGapless:({context:o})=>{this._tryScheduleGapless(o.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:o})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(o.currentTrackIndex)}}});this._actor=K(G),this._actor.subscribe(o=>{P(o.value==="playing")}),this._actor.start(),x({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:o=>this.seek(o)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let i of this._tracks)i.setVolume(t)}addTrack(e,t={}){let i=this._tracks.length,n=t.metadata??{};this._tracks.push(new g({trackUrl:e,index:i,queue:this,skipHEAD:t.skipHEAD,metadata:n})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e&&(this._scheduledNextIndex=null),this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return T()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e.index!==t.context.currentTrackIndex)return;this._actor.send({type:"TRACK_ENDED"});let i=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${i.value} curIdx=${i.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;(e=this._onPlayBlocked)==null||e.call(this)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=e+Y+1;this.onDebug(`_preloadAhead(${e}) limit=${t} trackCount=${this._tracks.length}`);for(let i=e+1;i<this._tracks.length&&i<t;i++){let n=this._tracks[i];if(n.isBufferLoaded)this.onDebug(`_preloadAhead: track ${i} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${i}`),n.preload();break}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.webAudioIsDisabled)return;let i=e+1;if(i>=this._tracks.length)return;let n=this._tracks[e],s=this._tracks[i];if(!n.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===i||!n.isPlaying)return;let c=this._computeTrackEndTime(n);c!==null&&(c<t.currentTime+.01||(s.scheduleGaplessStart(c),this._scheduledNextIndex=i))}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let i=e.duration;if(isNaN(i))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+i;let n=i-e.currentTime;return n<=0?null:t.currentTime+n}};export{m as Queue,m as default};
|
|
1
|
+
import{createActor as Q}from"xstate";var B=typeof window<"u",p;function $(){if(!B)return null;let r=window.AudioContext??window.webkitAudioContext;return r?new r:null}function k(){return p===void 0?null:p}function g(){p===void 0&&(p=$());let r=p;return r&&r.state==="suspended"?r.resume():Promise.resolve()}var A=typeof navigator<"u"&&"mediaSession"in navigator;function v(r){if(!A)return;let{mediaSession:e}=navigator;e.setActionHandler("play",r.onPlay),e.setActionHandler("pause",r.onPause),e.setActionHandler("nexttrack",r.onNext),e.setActionHandler("previoustrack",r.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&r.onSeek(t.seekTime)})}function P(r){if(A){if(!r){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:r.title??"",artist:r.artist??"",album:r.album??"",artwork:r.artwork??[]})}}function b(r){A&&(navigator.mediaSession.playbackState=r?"playing":"paused")}function C(r,e,t=1){if(A)try{navigator.mediaSession.setPositionState({duration:r,position:e,playbackRate:t})}catch{}}import{setup as F,assign as h}from"xstate";function D(r){return F({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately,willBeEmpty:({context:e})=>e.trackCount<=1,isRemovingCurrent:({context:e,event:t})=>t.index===e.currentTrackIndex},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>{let n=t,o=Math.max(0,e.trackCount-1);return n.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):o>0?Math.min(e.currentTrackIndex,o-1):0}}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>Math.min(e.currentTrackIndex+1,e.trackCount-1)}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:r,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["decrementTrackCount","cancelAndRescheduleGapless","preloadAhead"]}],PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["scheduleGapless","preloadAhead"]}}},paused:{on:{REMOVE_TRACK:[{guard:"willBeEmpty",target:"idle",actions:["cancelAllGapless","decrementTrackCount"]},{guard:"isRemovingCurrent",actions:["cancelAllGapless","decrementTrackCount","preloadAhead"]},{actions:["decrementTrackCount","preloadAhead"]}],PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{ADD_TRACK:{target:"paused",actions:"incrementTrackCount"},PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as K,fromPromise as _}from"xstate";import{setup as q,assign as d,spawnChild as V}from"xstate";import{setup as W,assign as E,sendParent as y,fromPromise as S}from"xstate";var f=W({types:{context:{},input:{}},actors:{resolveUrl:S(async()=>null),fetchAudio:S(async()=>{}),decodeAudio:S(async()=>{})},guards:{shouldSkipHEAD:({context:r})=>r.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:{trackUrl:"",resolvedUrl:"",skipHEAD:!1},entry:E(({event:r})=>{let{input:e}=r;return{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:r})=>({trackUrl:r.trackUrl}),onDone:{target:"fetching",actions:[E({resolvedUrl:({event:r,context:e})=>r.output??e.resolvedUrl,skipHEAD:()=>!0}),y(({event:r,context:e})=>({type:"URL_RESOLVED",url:r.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:E({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:r})=>({resolvedUrl:r.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:y({type:"BUFFER_READY"})},onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function N(r){return q({types:{context:{},events:{}},actors:{fetchDecode:f},guards:{canPlayWebAudio:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&!e.fetchStarted},actions:{playHtml5:()=>{},startSourceNode:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},disconnectGain:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},setIsPlaying:d({isPlaying:()=>!0}),clearIsPlaying:d({isPlaying:()=>!1}),setLoadingState:d({webAudioLoadingState:()=>"LOADING"}),setLoadedState:d({webAudioLoadingState:()=>"LOADED"}),setErrorState:d({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:d({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:d({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:d({notifiedLookahead:()=>!0}),setResolvedUrl:d({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:d({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:d({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"})}}).createMachine({id:"track",initial:"idle",context:r,on:{START_FETCH:{guard:"canStartFetch",actions:[d({webAudioLoadingState:()=>"LOADING",fetchStarted:()=>!0}),V("fetchDecode",{id:"fetchDecode",input:({context:e})=>({trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD})})]}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{target:"idle",actions:"setLoadedState"},BUFFER_ERROR:{target:"idle",actions:"setErrorState"},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["clearIsPlaying","freezePausedTime","stopSourceNode","disconnectGain","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var Y=5,L=15,T=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_webAudioDisabled;get ctx(){if(this._webAudioDisabled)return null;let e=k();return e&&!this.gainNode&&(this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume),e}gainNode=null;sourceNode=null;audioBuffer=null;webAudioStartedAt=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;_notifiedPreloadThreshold=!1;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var c,l;let o=(c=this.audio.error)==null?void 0:c.code;if(o===1)return;let s=((l=this.audio.error)==null?void 0:l.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${o}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._webAudioDisabled=e.queue.webAudioIsDisabled;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchStarted:!1},n=N(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode)},actors:{fetchDecode:f.provide({actors:{resolveUrl:_(async({signal:o})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:o});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:_(async({input:o,signal:s})=>{let{resolvedUrl:c}=o,l=await fetch(c,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${c}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:_(async()=>{let o=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!o||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(o),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:o})=>{let s=o.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this.webAudioStartedAt=s,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{this.pausedAtTrackTime=this.currentTime},stopSourceNode:()=>this._stopSourceNode(),disconnectGain:()=>this._disconnectGain(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>{queueMicrotask(()=>this.queueRef.onProgress(this.toInfo()))},seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this.webAudioStartedAt=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=K(n),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!this.audioBuffer&&(g(),this.ctx&&this._actor.send({type:"START_FETCH"}))}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._notifiedPreloadThreshold=!1,this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this.ctx.currentTime-this.webAudioStartedAt):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load())}_startSourceNode(e){!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.webAudioStartedAt=this.ctx.currentTime-e,this.sourceNode.start(0,e))}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_disconnectGain(){if(!(!this.gainNode||!this.ctx))try{this.gainNode.disconnect(this.ctx.destination)}catch{}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,n=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(n)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=Y&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this)));let o=isNaN(this.duration)?L:Math.min(this.duration*.2,L);!this._notifiedPreloadThreshold&&this.currentTime>=o&&(this._notifiedPreloadThreshold=!0,queueMicrotask(()=>this.queueRef.onPreloadReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function I(r,e){let t=0;return(...n)=>{let o=performance.now();o-t<e||(t=o,r(...n))}}var Z=2,m=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;webAudioIsDisabled;_volume;_scheduledNextIndex=null;_throttledUpdatePositionState=I((e,t)=>C(e,t),1e3);constructor(e={}){let{tracks:t=[],onProgress:n,onEnded:o,onPlayNextTrack:s,onPlayPreviousTrack:c,onStartNewTrack:l,onError:R,onPlayBlocked:M,onDebug:U,webAudioIsDisabled:w=!1,trackMetadata:O=[],volume:G=1}=e;this._volume=Math.min(1,Math.max(0,G)),this.webAudioIsDisabled=w,this._onProgress=n,this._onEnded=o,this._onPlayNextTrack=s,this._onPlayPreviousTrack=c,this._onStartNewTrack=l,this._onError=R,this._onPlayBlocked=M,this._onDebug=U,this._tracks=t.map((i,a)=>new T({trackUrl:i,index:a,queue:this,metadata:O[a]}));let H=D({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:i})=>{var a;(a=this._trackAt(i.currentTrackIndex))==null||a.deactivate()},deactivateEndedTrack:({context:i})=>{var a;(a=this._trackAt(i.currentTrackIndex))==null||a.deactivate()},activateAndPlayCurrent:({context:i})=>{let a=this._trackAt(i.currentTrackIndex);a&&(a.activate(),this._scheduledNextIndex!==a.index&&a.play())},playOrContinueGapless:({context:i})=>{let a=this._trackAt(i.currentTrackIndex);a&&(this._scheduledNextIndex!==a.index?a.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${a.index} \u2014 sourceNode=${a.hasSourceNode} isPlaying=${a.isPlaying} machineState=${a.machineState}`),a.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:i})=>{var u;let a=this._trackAt(i.currentTrackIndex);a&&((u=this._onStartNewTrack)==null||u.call(this,a.toInfo()))},notifyPlayNextTrack:({context:i})=>{var u;let a=this._trackAt(i.currentTrackIndex);a&&((u=this._onPlayNextTrack)==null||u.call(this,a.toInfo()))},notifyPlayPreviousTrack:({context:i})=>{var u;let a=this._trackAt(i.currentTrackIndex);a&&((u=this._onPlayPreviousTrack)==null||u.call(this,a.toInfo()))},notifyEnded:()=>{var i;return(i=this._onEnded)==null?void 0:i.call(this)},updateMediaSessionMetadata:({context:i})=>{let a=this._trackAt(i.currentTrackIndex);a&&P(a.metadata)},preloadAhead:({context:i})=>{this._preloadAhead(i.currentTrackIndex)},playCurrent:({context:i})=>{var a;(a=this._trackAt(i.currentTrackIndex))==null||a.play()},pauseCurrent:({context:i})=>{var a;(a=this._trackAt(i.currentTrackIndex))==null||a.pause()},seekCurrent:({context:i,event:a})=>{var x;let u=a;(x=this._trackAt(i.currentTrackIndex))==null||x.seek(u.time)},seekCurrentToZero:({context:i})=>{var a;(a=this._trackAt(i.currentTrackIndex))==null||a.seek(0)},scheduleGapless:({context:i})=>{this._tryScheduleGapless(i.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:i})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(i.currentTrackIndex)}}});this._actor=Q(H),this._actor.subscribe(i=>{b(i.value==="playing")}),this._actor.start(),v({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:i=>this.seek(i)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let n of this._tracks)n.setVolume(t)}addTrack(e,t={}){let n=this._tracks.length,o=t.metadata??{};this._tracks.push(new T({trackUrl:e,index:n,queue:this,skipHEAD:t.skipHEAD,metadata:o})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e?this._scheduledNextIndex=null:this._scheduledNextIndex!==null&&this._scheduledNextIndex>e&&this._scheduledNextIndex--,this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return g()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e!==this._trackAt(t.context.currentTrackIndex))return;this._actor.send({type:"TRACK_ENDED"});let n=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${n.value} curIdx=${n.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;(e=this._onPlayBlocked)==null||e.call(this)}onPreloadReady(e){let t=this._actor.getSnapshot();e.index===t.context.currentTrackIndex&&this._preloadAhead(t.context.currentTrackIndex)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=this._trackAt(e);if(t&&t.playbackType==="HTML5"&&t.isPlaying){let o=isNaN(t.duration)?15:Math.min(t.duration*.2,15);if(t.currentTime<o){this.onDebug(`_preloadAhead: deferring \u2014 HTML5 track ${e} at ${t.currentTime.toFixed(1)}s (threshold=${o.toFixed(1)}s)`);return}}let n=e+Z+1;this.onDebug(`_preloadAhead(${e}) limit=${n} trackCount=${this._tracks.length}`);for(let o=e+1;o<this._tracks.length&&o<n;o++){let s=this._tracks[o];if(s.isBufferLoaded)this.onDebug(`_preloadAhead: track ${o} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${o}`),s.preload();break}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.webAudioIsDisabled)return;let n=e+1;if(n>=this._tracks.length)return;let o=this._tracks[e],s=this._tracks[n];if(!o.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===n||!o.isPlaying)return;let c=this._computeTrackEndTime(o);c!==null&&(c<t.currentTime+.01||(s.scheduleGaplessStart(c),this._scheduledNextIndex=n))}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let n=e.duration;if(isNaN(n))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+n;let o=n-e.currentTime;return o<=0?null:t.currentTime+o}};export{m as Queue,m as default};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';\n\n/** Maximum number of tracks to preload ahead of the current track. */\nconst PRELOAD_AHEAD = 2;\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly webAudioIsDisabled: boolean;\n\n private _volume: number;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number) =>\n updateMediaSessionPositionState(duration, currentTime),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onDebug,\n webAudioIsDisabled = false,\n trackMetadata = [],\n volume: initialVolume = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this.webAudioIsDisabled = webAudioIsDisabled;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onDebug = onDebug;\n\n this._tracks = tracks.map(\n (url, i) =>\n new Track({\n trackUrl: url,\n index: i,\n queue: this,\n metadata: trackMetadata[i],\n })\n );\n\n // -----------------------------------------------------------------------\n // Wire up the queue machine with real action implementations.\n //\n // IMPORTANT: actions receive ({ context }) which reflects the in-progress\n // context (updated by prior assign() calls within the same transition).\n // Do NOT use this._actor.getSnapshot() inside actions — that returns the\n // pre-transition snapshot and won't reflect intermediate assign() updates.\n // -----------------------------------------------------------------------\n const machine = createQueueMachine({\n currentTrackIndex: 0,\n trackCount: this._tracks.length\n }).provide({\n actions: {\n deactivateCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n deactivateEndedTrack: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n activateAndPlayCurrent: ({ context }) => {\n const track = this._trackAt(context.currentTrackIndex);\n if (!track) return;\n track.activate();\n if (this._scheduledNextIndex !== track.index) {\n track.play();\n }\n },\n playOrContinueGapless: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (!cur) return;\n if (this._scheduledNextIndex !== cur.index) {\n cur.play();\n } else {\n this._scheduledNextIndex = null;\n this.onDebug(\n `onTrackEnded: gapless track ${cur.index} — sourceNode=${cur.hasSourceNode} isPlaying=${cur.isPlaying} machineState=${cur.machineState}`\n );\n cur.startProgressLoop();\n }\n },\n cancelAllGapless: () => this._cancelScheduledGapless(),\n notifyStartNewTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onStartNewTrack?.(cur.toInfo());\n },\n notifyPlayNextTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayNextTrack?.(cur.toInfo());\n },\n notifyPlayPreviousTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayPreviousTrack?.(cur.toInfo());\n },\n notifyEnded: () => this._onEnded?.(),\n updateMediaSessionMetadata: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) updateMediaSessionMetadata(cur.metadata);\n },\n preloadAhead: ({ context }) => {\n this._preloadAhead(context.currentTrackIndex);\n },\n playCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.play();\n },\n pauseCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.pause();\n },\n seekCurrent: ({ context, event }) => {\n const e = event as { type: 'SEEK'; time: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.time);\n },\n seekCurrentToZero: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.seek(0);\n },\n scheduleGapless: ({ context }) => {\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n cancelScheduledGapless: () => {\n this._cancelScheduledGapless();\n },\n cancelAndRescheduleGapless: ({ context }) => {\n this._cancelScheduledGapless();\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n },\n });\n\n this._actor = createActor(machine);\n\n this._actor.subscribe((snapshot) => {\n updateMediaSessionPlaybackState(snapshot.value === 'playing');\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n ct.seek(0);\n ct.play();\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n if (track.index !== snap.context.currentTrackIndex) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._onPlayBlocked?.();\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const limit = fromIndex + PRELOAD_AHEAD + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n break;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.webAudioIsDisabled) return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n next.scheduleGaplessStart(endTime);\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration;\n }\n\n const remaining = duration - track.currentTime;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n return context.currentTrackIndex;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToZero: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext, resumeAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly webAudioIsDisabled: boolean;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _webAudioDisabled: boolean;\n\n private get ctx(): AudioContext | null {\n if (this._webAudioDisabled) return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime when the current source node was started. */\n private webAudioStartedAt = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._webAudioDisabled = opts.queue.webAudioIsDisabled;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchDecodeRef: null,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n this._resolvedUrl = res.url;\n this.audio.src = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }),\n },\n }),\n },\n actions: {\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n this.sourceNode.start(when, 0);\n this.webAudioStartedAt = when;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n this.pausedAtTrackTime = this.currentTime;\n },\n stopSourceNode: () => this._stopSourceNode(),\n disconnectGain: () => this._disconnectGain(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => {\n queueMicrotask(() => this.queueRef.onProgress(this.toInfo()));\n },\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this.webAudioStartedAt = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n this.audio.volume = vol;\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer) return;\n resumeAudioContext();\n if (!this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this.ctx.currentTime - this.webAudioStartedAt);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n private _startSourceNode(offset: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this.webAudioStartedAt = this.ctx.currentTime - offset;\n this.sourceNode.start(0, offset);\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _disconnectGain(): void {\n if (!this.gainNode || !this.ctx) return;\n try {\n this.gainNode.disconnect(this.ctx.destination);\n } catch {\n /* already disconnected */\n }\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode may be in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins eventually\":\n// When a track's buffer finishes decoding (BUFFER_READY), webAudioLoadingState\n// is set to 'LOADED' regardless of what state the machine is in (html5, loading,\n// or idle). We intentionally do NOT switch mid-stream — the track stays in html5\n// until the next play(). But every state handles BUFFER_READY, so the flag is\n// never lost, and the next play() will see the buffer and use Web Audio.\n//\n// All DEACTIVATE transitions land in idle (not loading), so a deactivated track\n// with a loaded buffer is always in idle+LOADED — ready for Web Audio on re-play.\n//\n// Bug fixes in this rewrite:\n// #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)\n// #3: DEACTIVATE from webaudio → idle (was staying in webaudio)\n// #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, type AnyActorRef } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchDecodeRef: AnyActorRef | null;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && context.fetchDecodeRef === null,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n disconnectGain: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchDecodeRef: ({ context, spawn }) =>\n spawn('fetchDecode', {\n id: 'fetchDecode',\n input: {\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n },\n }),\n }),\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Bug #2 fix: BUFFER_READY in html5 stays in html5, only updates loading state.\n // The actual switchover to webaudio only happens via explicit PLAY_WEBAUDIO.\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n target: 'idle',\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n target: 'idle',\n actions: 'setErrorState',\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n // Note: xstate v5 internally calls assign() when initialising a child\n // actor with a context function, which produces a false-positive\n // \"Custom actions should not call assign()\" warning when this machine\n // is spawned from a parent. This is harmless and cannot be avoided.\n context: ({ input }) => ({\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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EAGA,aAAc,CACZ,QAAS,gBACX,EACA,aAAc,CACZ,QAAS,eACX,EACA,KAAM,CACJ,QAAS,CACP,YACA,gBACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,YAAa,CACX,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,aACA,mBACA,cACA,mBACF,CACF,EACA,aAAc,CACZ,QAAS,gBACX,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAc,oBAAqB,cAAe,kBAAkB,CAClG,CACF,CACF,EAKA,QAAS,CACP,GAAI,CACF,eAAgB,CACd,QAAS,iBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,gBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,eACX,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,eAAgB,YAAa,mBAAmB,CAC5D,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,cACA,mBACF,CACF,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAa,CAC3C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,SAAU,CACR,GAAI,CACF,MAAO,CACL,QAAS,CACP,iBACA,mBACA,iBACA,iBACA,mBACA,gBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CAAC,eAAgB,kBAAmB,mBAAmB,CAClE,EACA,CACE,QAAS,cACX,CACF,EACA,cAAe,CACb,QAAS,wBACX,EACA,KAAM,CACJ,QAAS,CACP,sBACA,eACA,gBACF,CACF,EACA,WAAY,CAAC,EACb,eAAgB,CACd,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,mBACA,aACF,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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","next","createActor","fromPromise","setup","assign","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","input","event","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawn","GAPLESS_SCHEDULE_LOOKAHEAD","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","when","createActor","time","clamped","v","vol","resumeAudioContext","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","throttle","fn","ms","last","args","now","PRELOAD_AHEAD","Queue","throttle","duration","currentTime","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","webAudioIsDisabled","trackMetadata","initialVolume","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","metadata","resumeAudioContext","snap","newSnap","info","error","msg","fromIndex","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","remaining"]}
|
|
1
|
+
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';\n\n/** Maximum number of tracks to preload ahead of the current track. */\nconst PRELOAD_AHEAD = 2;\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly webAudioIsDisabled: boolean;\n\n private _volume: number;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number) =>\n updateMediaSessionPositionState(duration, currentTime),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onDebug,\n webAudioIsDisabled = false,\n trackMetadata = [],\n volume: initialVolume = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this.webAudioIsDisabled = webAudioIsDisabled;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onDebug = onDebug;\n\n this._tracks = tracks.map(\n (url, i) =>\n new Track({\n trackUrl: url,\n index: i,\n queue: this,\n metadata: trackMetadata[i],\n })\n );\n\n // -----------------------------------------------------------------------\n // Wire up the queue machine with real action implementations.\n //\n // IMPORTANT: actions receive ({ context }) which reflects the in-progress\n // context (updated by prior assign() calls within the same transition).\n // Do NOT use this._actor.getSnapshot() inside actions — that returns the\n // pre-transition snapshot and won't reflect intermediate assign() updates.\n // -----------------------------------------------------------------------\n const machine = createQueueMachine({\n currentTrackIndex: 0,\n trackCount: this._tracks.length\n }).provide({\n actions: {\n deactivateCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n deactivateEndedTrack: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n activateAndPlayCurrent: ({ context }) => {\n const track = this._trackAt(context.currentTrackIndex);\n if (!track) return;\n track.activate();\n if (this._scheduledNextIndex !== track.index) {\n track.play();\n }\n },\n playOrContinueGapless: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (!cur) return;\n if (this._scheduledNextIndex !== cur.index) {\n cur.play();\n } else {\n this._scheduledNextIndex = null;\n this.onDebug(\n `onTrackEnded: gapless track ${cur.index} — sourceNode=${cur.hasSourceNode} isPlaying=${cur.isPlaying} machineState=${cur.machineState}`\n );\n cur.startProgressLoop();\n }\n },\n cancelAllGapless: () => this._cancelScheduledGapless(),\n notifyStartNewTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onStartNewTrack?.(cur.toInfo());\n },\n notifyPlayNextTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayNextTrack?.(cur.toInfo());\n },\n notifyPlayPreviousTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayPreviousTrack?.(cur.toInfo());\n },\n notifyEnded: () => this._onEnded?.(),\n updateMediaSessionMetadata: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) updateMediaSessionMetadata(cur.metadata);\n },\n preloadAhead: ({ context }) => {\n this._preloadAhead(context.currentTrackIndex);\n },\n playCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.play();\n },\n pauseCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.pause();\n },\n seekCurrent: ({ context, event }) => {\n const e = event as { type: 'SEEK'; time: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.time);\n },\n seekCurrentToZero: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.seek(0);\n },\n scheduleGapless: ({ context }) => {\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n cancelScheduledGapless: () => {\n this._cancelScheduledGapless();\n },\n cancelAndRescheduleGapless: ({ context }) => {\n this._cancelScheduledGapless();\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n },\n });\n\n this._actor = createActor(machine);\n\n this._actor.subscribe((snapshot) => {\n updateMediaSessionPlaybackState(snapshot.value === 'playing');\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n ct.seek(0);\n ct.play();\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n } else if (this._scheduledNextIndex !== null && this._scheduledNextIndex > index) {\n this._scheduledNextIndex--;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n // Compare object identity, not just index — after removeTrack(), a\n // different track may occupy the same index as the ended track.\n if (track !== this._trackAt(snap.context.currentTrackIndex)) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._onPlayBlocked?.();\n }\n\n onPreloadReady(track: Track): void {\n const snap = this._actor.getSnapshot();\n if (track.index !== snap.context.currentTrackIndex) return;\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const cur = this._trackAt(fromIndex);\n if (cur && cur.playbackType === 'HTML5' && cur.isPlaying) {\n const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);\n if (cur.currentTime < threshold) {\n this.onDebug(`_preloadAhead: deferring — HTML5 track ${fromIndex} at ${cur.currentTime.toFixed(1)}s (threshold=${threshold.toFixed(1)}s)`);\n return;\n }\n }\n const limit = fromIndex + PRELOAD_AHEAD + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n break;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.webAudioIsDisabled) return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n next.scheduleGaplessStart(endTime);\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration;\n }\n\n const remaining = duration - track.currentTime;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n willBeEmpty: ({ context }) => context.trackCount <= 1,\n isRemovingCurrent: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n return e.index === context.currentTrackIndex;\n },\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n const newCount = Math.max(0, context.trackCount - 1);\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n // Clamp to last valid index when removing at or after current\n return newCount > 0\n ? Math.min(context.currentTrackIndex, newCount - 1)\n : 0;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) =>\n Math.min(context.currentTrackIndex + 1, context.trackCount - 1),\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToZero: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: [\n 'cancelAllGapless',\n 'decrementTrackCount',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: ['decrementTrackCount', 'cancelAndRescheduleGapless', 'preloadAhead'],\n },\n ],\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n REMOVE_TRACK: [\n {\n guard: 'willBeEmpty',\n target: 'idle',\n actions: ['cancelAllGapless', 'decrementTrackCount'],\n },\n {\n guard: 'isRemovingCurrent',\n actions: ['cancelAllGapless', 'decrementTrackCount', 'preloadAhead'],\n },\n {\n actions: ['decrementTrackCount', 'preloadAhead'],\n },\n ],\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n ADD_TRACK: {\n target: 'paused',\n actions: 'incrementTrackCount',\n },\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext, resumeAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onPreloadReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly webAudioIsDisabled: boolean;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\n/** How many seconds into HTML5 playback before we preload the next track. */\nconst PRELOAD_DELAY = 15;\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _webAudioDisabled: boolean;\n\n private get ctx(): AudioContext | null {\n if (this._webAudioDisabled) return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime when the current source node was started. */\n private webAudioStartedAt = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n private _notifiedPreloadThreshold = false;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._webAudioDisabled = opts.queue.webAudioIsDisabled;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchStarted: false,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n this._resolvedUrl = res.url;\n this.audio.src = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }),\n },\n }),\n },\n actions: {\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n this.sourceNode.start(when, 0);\n this.webAudioStartedAt = when;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n this.pausedAtTrackTime = this.currentTime;\n },\n stopSourceNode: () => this._stopSourceNode(),\n disconnectGain: () => this._disconnectGain(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => {\n queueMicrotask(() => this.queueRef.onProgress(this.toInfo()));\n },\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this.webAudioStartedAt = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n this.audio.volume = vol;\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer) return;\n resumeAudioContext();\n if (!this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._notifiedPreloadThreshold = false;\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this.ctx.currentTime - this.webAudioStartedAt);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n private _startSourceNode(offset: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this.webAudioStartedAt = this.ctx.currentTime - offset;\n this.sourceNode.start(0, offset);\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _disconnectGain(): void {\n if (!this.gainNode || !this.ctx) return;\n try {\n this.gainNode.disconnect(this.ctx.destination);\n } catch {\n /* already disconnected */\n }\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n const preloadThreshold = isNaN(this.duration) ? PRELOAD_DELAY : Math.min(this.duration * 0.2, PRELOAD_DELAY);\n if (!this._notifiedPreloadThreshold && this.currentTime >= preloadThreshold) {\n this._notifiedPreloadThreshold = true;\n queueMicrotask(() => this.queueRef.onPreloadReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode may be in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins eventually\":\n// When a track's buffer finishes decoding (BUFFER_READY), webAudioLoadingState\n// is set to 'LOADED' regardless of what state the machine is in (html5, loading,\n// or idle). We intentionally do NOT switch mid-stream — the track stays in html5\n// until the next play(). But every state handles BUFFER_READY, so the flag is\n// never lost, and the next play() will see the buffer and use Web Audio.\n//\n// All DEACTIVATE transitions land in idle (not loading), so a deactivated track\n// with a loaded buffer is always in idle+LOADED — ready for Web Audio on re-play.\n//\n// Bug fixes in this rewrite:\n// #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)\n// #3: DEACTIVATE from webaudio → idle (was staying in webaudio)\n// #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, spawnChild } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchStarted: boolean;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && !context.fetchStarted,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n disconnectGain: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: [\n assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchStarted: () => true,\n }),\n spawnChild('fetchDecode', {\n id: 'fetchDecode',\n input: ({ context }) => ({\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n }),\n }),\n ],\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Bug #2 fix: BUFFER_READY in html5 stays in html5, only updates loading state.\n // The actual switchover to webaudio only happens via explicit PLAY_WEBAUDIO.\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n target: 'idle',\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n target: 'idle',\n actions: 'setErrorState',\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n context: {\n trackUrl: '',\n resolvedUrl: '',\n skipHEAD: false,\n },\n entry: assign(({ event }) => {\n const { input } = event as { type: string; input: FetchDecodeContext };\n return {\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n };\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,QAAS,CACP,oBACA,mBACA,iBACA,oBACA,cACF,CACF,CACF,EACA,KAAM,CACJ,QAAS,CAAC,cAAe,4BAA4B,CACvD,EACA,YAAa,CACX,CACE,MAAO,eACP,OAAQ,UACR,QAAS,CACP,uBACA,oBACA,wBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,uBAAwB,aAAa,CACjD,CACF,EACA,aAAc,CACZ,QAAS,CAAC,kBAAmB,cAAc,CAC7C,CACF,CACF,EAKA,OAAQ,CACN,GAAI,CACF,aAAc,CACZ,CACE,MAAO,cACP,OAAQ,OACR,QAAS,CAAC,mBAAoB,qBAAqB,CACrD,EACA,CACE,MAAO,oBACP,QAAS,CAAC,mBAAoB,sBAAuB,cAAc,CACrE,EACA,CACE,QAAS,CAAC,sBAAuB,cAAc,CACjD,CACF,EACA,KAAM,CACJ,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,OAAQ,CACN,OAAQ,UACR,QAAS,CAAC,cAAe,6BAA8B,eAAgB,iBAAiB,CAC1F,EACA,KAAM,CACJ,QAAS,CACP,oBACA,mBACA,qBACA,yBACA,sBACA,sBACA,6BACA,cACF,CACF,EACA,SAAU,CACR,QAAS,CACP,oBACA,mBACA,oBACA,yBACA,sBACA,0BACA,6BACA,cACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,UACR,QAAS,CACP,oBACA,mBACA,iBACA,yBACA,sBACA,6BACA,cACF,CACF,EACA,CACE,QAAS,CACP,oBACA,mBACA,iBACA,oBACA,cACF,CACF,CACF,EACA,KAAM,CACJ,QAAS,CAAC,aAAa,CACzB,EACA,YAAa,CACX,CAC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c,CACZ,OAAQ,OACR,QAAS,gBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,eACX,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,eAAgB,YAAa,mBAAmB,CAC5D,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,cACA,mBACF,CACF,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAa,CAC3C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,SAAU,CACR,GAAI,CACF,MAAO,CACL,QAAS,CACP,iBACA,mBACA,iBACA,iBACA,mBACA,gBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CAAC,eAAgB,kBAAmB,mBAAmB,CAClE,EACA,CACE,QAAS,cACX,CACF,EACA,cAAe,CACb,QAAS,wBACX,EACA,KAAM,CACJ,QAAS,CACP,sBACA,eACA,gBACF,CACF,EACA,WAAY,CAAC,EACb,eAAgB,CACd,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,mBACA,aACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,eAAgB,CACd,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,mBACA,cACA,mBACF,CACF,EAEA,WAAY,CACV,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,cACA,oBACA,kBACF,CACF,CACF,CACF,CACF,CACF,CAAC,CACH,CDjZA,IAAMI,EAA6B,EAG7BC,EAAgB,GAETC,EAAN,KAAY,CACR,MACA,SAED,UACA,aACS,SAET,oBAA0C,KAGzC,MAGQ,kBAEjB,IAAY,KAA2B,CACrC,GAAI,KAAK,kBAAmB,O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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","e","newCount","next","createActor","fromPromise","setup","assign","spawnChild","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","event","input","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawnChild","GAPLESS_SCHEDULE_LOOKAHEAD","PRELOAD_DELAY","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","when","createActor","time","clamped","v","vol","resumeAudioContext","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","preloadThreshold","throttle","fn","ms","last","args","now","PRELOAD_AHEAD","Queue","throttle","duration","currentTime","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","webAudioIsDisabled","trackMetadata","initialVolume","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","metadata","resumeAudioContext","snap","newSnap","info","error","msg","fromIndex","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","remaining"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gapless",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
4
4
|
"description": "Gapless audio playback javascript plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dev": "pnpm --dir demo dev",
|
|
28
28
|
"types": "tsc --noEmit",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
|
-
"test:coverage": "vitest run --coverage"
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"prepublishOnly": "pnpm run build"
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
package/src/Queue.ts
CHANGED
|
@@ -270,6 +270,8 @@ export class Queue implements TrackQueueRef {
|
|
|
270
270
|
}
|
|
271
271
|
if (this._scheduledNextIndex === index) {
|
|
272
272
|
this._scheduledNextIndex = null;
|
|
273
|
+
} else if (this._scheduledNextIndex !== null && this._scheduledNextIndex > index) {
|
|
274
|
+
this._scheduledNextIndex--;
|
|
273
275
|
}
|
|
274
276
|
this._actor.send({ type: 'REMOVE_TRACK', index });
|
|
275
277
|
}
|
|
@@ -327,7 +329,9 @@ export class Queue implements TrackQueueRef {
|
|
|
327
329
|
this.onDebug(
|
|
328
330
|
`onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`
|
|
329
331
|
);
|
|
330
|
-
|
|
332
|
+
// Compare object identity, not just index — after removeTrack(), a
|
|
333
|
+
// different track may occupy the same index as the ended track.
|
|
334
|
+
if (track !== this._trackAt(snap.context.currentTrackIndex)) return;
|
|
331
335
|
|
|
332
336
|
this._actor.send({ type: 'TRACK_ENDED' });
|
|
333
337
|
const newSnap = this._actor.getSnapshot();
|
|
@@ -356,6 +360,12 @@ export class Queue implements TrackQueueRef {
|
|
|
356
360
|
this._onPlayBlocked?.();
|
|
357
361
|
}
|
|
358
362
|
|
|
363
|
+
onPreloadReady(track: Track): void {
|
|
364
|
+
const snap = this._actor.getSnapshot();
|
|
365
|
+
if (track.index !== snap.context.currentTrackIndex) return;
|
|
366
|
+
this._preloadAhead(snap.context.currentTrackIndex);
|
|
367
|
+
}
|
|
368
|
+
|
|
359
369
|
onDebug(msg: string): void {
|
|
360
370
|
this._onDebug?.(msg);
|
|
361
371
|
}
|
|
@@ -374,6 +384,14 @@ export class Queue implements TrackQueueRef {
|
|
|
374
384
|
}
|
|
375
385
|
|
|
376
386
|
private _preloadAhead(fromIndex: number): void {
|
|
387
|
+
const cur = this._trackAt(fromIndex);
|
|
388
|
+
if (cur && cur.playbackType === 'HTML5' && cur.isPlaying) {
|
|
389
|
+
const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);
|
|
390
|
+
if (cur.currentTime < threshold) {
|
|
391
|
+
this.onDebug(`_preloadAhead: deferring — HTML5 track ${fromIndex} at ${cur.currentTime.toFixed(1)}s (threshold=${threshold.toFixed(1)}s)`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
377
395
|
const limit = fromIndex + PRELOAD_AHEAD + 1;
|
|
378
396
|
this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);
|
|
379
397
|
for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {
|
package/src/Track.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } fro
|
|
|
12
12
|
export interface TrackQueueRef {
|
|
13
13
|
onTrackEnded(track: Track): void;
|
|
14
14
|
onTrackBufferReady(track: Track): void;
|
|
15
|
+
onPreloadReady(track: Track): void;
|
|
15
16
|
onProgress(info: TrackInfo): void;
|
|
16
17
|
onError(error: Error): void;
|
|
17
18
|
onPlayBlocked(): void;
|
|
@@ -24,6 +25,9 @@ export interface TrackQueueRef {
|
|
|
24
25
|
/** How close to the end (in seconds) before we attempt gapless scheduling. */
|
|
25
26
|
const GAPLESS_SCHEDULE_LOOKAHEAD = 5;
|
|
26
27
|
|
|
28
|
+
/** How many seconds into HTML5 playback before we preload the next track. */
|
|
29
|
+
const PRELOAD_DELAY = 15;
|
|
30
|
+
|
|
27
31
|
export class Track {
|
|
28
32
|
readonly index: number;
|
|
29
33
|
readonly metadata: TrackMetadata;
|
|
@@ -63,6 +67,7 @@ export class Track {
|
|
|
63
67
|
// ---- Callbacks -----------------------------------------------------------
|
|
64
68
|
private readonly queueRef: TrackQueueRef;
|
|
65
69
|
private rafId: number | null = null;
|
|
70
|
+
private _notifiedPreloadThreshold = false;
|
|
66
71
|
|
|
67
72
|
constructor(opts: {
|
|
68
73
|
trackUrl: string;
|
|
@@ -110,7 +115,7 @@ export class Track {
|
|
|
110
115
|
isPlaying: false,
|
|
111
116
|
scheduledStartContextTime: null,
|
|
112
117
|
notifiedLookahead: false,
|
|
113
|
-
|
|
118
|
+
fetchStarted: false,
|
|
114
119
|
};
|
|
115
120
|
const machine = createTrackMachine(initialContext).provide({
|
|
116
121
|
guards: {
|
|
@@ -249,6 +254,7 @@ export class Track {
|
|
|
249
254
|
this.queueRef.onDebug(
|
|
250
255
|
`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`
|
|
251
256
|
);
|
|
257
|
+
this._notifiedPreloadThreshold = false;
|
|
252
258
|
this._actor.send({ type: 'DEACTIVATE' });
|
|
253
259
|
this.queueRef.onDebug(
|
|
254
260
|
`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`
|
|
@@ -480,6 +486,12 @@ export class Track {
|
|
|
480
486
|
queueMicrotask(() => this.queueRef.onTrackBufferReady(this));
|
|
481
487
|
}
|
|
482
488
|
|
|
489
|
+
const preloadThreshold = isNaN(this.duration) ? PRELOAD_DELAY : Math.min(this.duration * 0.2, PRELOAD_DELAY);
|
|
490
|
+
if (!this._notifiedPreloadThreshold && this.currentTime >= preloadThreshold) {
|
|
491
|
+
this._notifiedPreloadThreshold = true;
|
|
492
|
+
queueMicrotask(() => this.queueRef.onPreloadReady(this));
|
|
493
|
+
}
|
|
494
|
+
|
|
483
495
|
this.rafId = requestAnimationFrame(loop);
|
|
484
496
|
};
|
|
485
497
|
this.rafId = requestAnimationFrame(loop);
|
|
@@ -40,14 +40,18 @@ export const fetchDecodeMachine = setup({
|
|
|
40
40
|
}).createMachine({
|
|
41
41
|
id: 'fetchDecode',
|
|
42
42
|
initial: 'resolvingUrl',
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
context: {
|
|
44
|
+
trackUrl: '',
|
|
45
|
+
resolvedUrl: '',
|
|
46
|
+
skipHEAD: false,
|
|
47
|
+
},
|
|
48
|
+
entry: assign(({ event }) => {
|
|
49
|
+
const { input } = event as { type: string; input: FetchDecodeContext };
|
|
50
|
+
return {
|
|
51
|
+
trackUrl: input.trackUrl,
|
|
52
|
+
resolvedUrl: input.resolvedUrl,
|
|
53
|
+
skipHEAD: input.skipHEAD,
|
|
54
|
+
};
|
|
51
55
|
}),
|
|
52
56
|
|
|
53
57
|
states: {
|
|
@@ -53,6 +53,11 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
53
53
|
hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,
|
|
54
54
|
playImmediately: ({ event }) =>
|
|
55
55
|
!!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,
|
|
56
|
+
willBeEmpty: ({ context }) => context.trackCount <= 1,
|
|
57
|
+
isRemovingCurrent: ({ context, event }) => {
|
|
58
|
+
const e = event as { type: 'REMOVE_TRACK'; index: number };
|
|
59
|
+
return e.index === context.currentTrackIndex;
|
|
60
|
+
},
|
|
56
61
|
},
|
|
57
62
|
actions: {
|
|
58
63
|
// --- named assign actions (avoids mixing inline assign with custom actions) ---
|
|
@@ -63,10 +68,14 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
63
68
|
trackCount: ({ context }) => Math.max(0, context.trackCount - 1),
|
|
64
69
|
currentTrackIndex: ({ context, event }) => {
|
|
65
70
|
const e = event as { type: 'REMOVE_TRACK'; index: number };
|
|
71
|
+
const newCount = Math.max(0, context.trackCount - 1);
|
|
66
72
|
if (e.index < context.currentTrackIndex) {
|
|
67
73
|
return Math.max(0, context.currentTrackIndex - 1);
|
|
68
74
|
}
|
|
69
|
-
|
|
75
|
+
// Clamp to last valid index when removing at or after current
|
|
76
|
+
return newCount > 0
|
|
77
|
+
? Math.min(context.currentTrackIndex, newCount - 1)
|
|
78
|
+
: 0;
|
|
70
79
|
},
|
|
71
80
|
}),
|
|
72
81
|
gotoTrackIndex: assign({
|
|
@@ -83,7 +92,8 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
83
92
|
currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
|
|
84
93
|
}),
|
|
85
94
|
advanceOnTrackEnd: assign({
|
|
86
|
-
currentTrackIndex: ({ context }) =>
|
|
95
|
+
currentTrackIndex: ({ context }) =>
|
|
96
|
+
Math.min(context.currentTrackIndex + 1, context.trackCount - 1),
|
|
87
97
|
}),
|
|
88
98
|
resetToFirstTrack: assign({
|
|
89
99
|
currentTrackIndex: () => 0,
|
|
@@ -169,6 +179,27 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
169
179
|
// -----------------------------------------------------------------
|
|
170
180
|
playing: {
|
|
171
181
|
on: {
|
|
182
|
+
REMOVE_TRACK: [
|
|
183
|
+
{
|
|
184
|
+
guard: 'willBeEmpty',
|
|
185
|
+
target: 'idle',
|
|
186
|
+
actions: ['cancelAllGapless', 'decrementTrackCount'],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
guard: 'isRemovingCurrent',
|
|
190
|
+
actions: [
|
|
191
|
+
'cancelAllGapless',
|
|
192
|
+
'decrementTrackCount',
|
|
193
|
+
'activateAndPlayCurrent',
|
|
194
|
+
'notifyStartNewTrack',
|
|
195
|
+
'updateMediaSessionMetadata',
|
|
196
|
+
'preloadAhead',
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
actions: ['decrementTrackCount', 'cancelAndRescheduleGapless', 'preloadAhead'],
|
|
201
|
+
},
|
|
202
|
+
],
|
|
172
203
|
PAUSE: {
|
|
173
204
|
target: 'paused',
|
|
174
205
|
actions: ['cancelScheduledGapless', 'pauseCurrent'],
|
|
@@ -257,6 +288,20 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
257
288
|
// -----------------------------------------------------------------
|
|
258
289
|
paused: {
|
|
259
290
|
on: {
|
|
291
|
+
REMOVE_TRACK: [
|
|
292
|
+
{
|
|
293
|
+
guard: 'willBeEmpty',
|
|
294
|
+
target: 'idle',
|
|
295
|
+
actions: ['cancelAllGapless', 'decrementTrackCount'],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
guard: 'isRemovingCurrent',
|
|
299
|
+
actions: ['cancelAllGapless', 'decrementTrackCount', 'preloadAhead'],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
actions: ['decrementTrackCount', 'preloadAhead'],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
260
305
|
PLAY: {
|
|
261
306
|
target: 'playing',
|
|
262
307
|
actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],
|
|
@@ -343,6 +388,10 @@ export function createQueueMachine(initialContext: QueueContext) {
|
|
|
343
388
|
// -----------------------------------------------------------------
|
|
344
389
|
ended: {
|
|
345
390
|
on: {
|
|
391
|
+
ADD_TRACK: {
|
|
392
|
+
target: 'paused',
|
|
393
|
+
actions: 'incrementTrackCount',
|
|
394
|
+
},
|
|
346
395
|
PLAY: {
|
|
347
396
|
target: 'playing',
|
|
348
397
|
actions: [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
import { setup, assign,
|
|
26
|
+
import { setup, assign, spawnChild } from 'xstate';
|
|
27
27
|
import { fetchDecodeMachine } from './fetchDecode.machine';
|
|
28
28
|
import type { WebAudioLoadingState, PlaybackType } from '../types';
|
|
29
29
|
|
|
@@ -38,7 +38,7 @@ export interface TrackContext {
|
|
|
38
38
|
isPlaying: boolean;
|
|
39
39
|
scheduledStartContextTime: number | null;
|
|
40
40
|
notifiedLookahead: boolean;
|
|
41
|
-
|
|
41
|
+
fetchStarted: boolean;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// ---- Events ----------------------------------------------------------------
|
|
@@ -76,7 +76,7 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
76
76
|
},
|
|
77
77
|
guards: {
|
|
78
78
|
canPlayWebAudio: () => false,
|
|
79
|
-
canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && context.
|
|
79
|
+
canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && !context.fetchStarted,
|
|
80
80
|
},
|
|
81
81
|
actions: {
|
|
82
82
|
playHtml5: () => {},
|
|
@@ -135,18 +135,20 @@ export function createTrackMachine(initialContext: TrackContext) {
|
|
|
135
135
|
on: {
|
|
136
136
|
START_FETCH: {
|
|
137
137
|
guard: 'canStartFetch',
|
|
138
|
-
actions:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
actions: [
|
|
139
|
+
assign({
|
|
140
|
+
webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,
|
|
141
|
+
fetchStarted: () => true,
|
|
142
|
+
}),
|
|
143
|
+
spawnChild('fetchDecode', {
|
|
144
|
+
id: 'fetchDecode',
|
|
145
|
+
input: ({ context }) => ({
|
|
146
|
+
trackUrl: context.trackUrl,
|
|
147
|
+
resolvedUrl: context.resolvedUrl,
|
|
148
|
+
skipHEAD: context.skipHEAD,
|
|
148
149
|
}),
|
|
149
|
-
|
|
150
|
+
}),
|
|
151
|
+
],
|
|
150
152
|
},
|
|
151
153
|
},
|
|
152
154
|
|