gapless 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Queue.ts +13 -0
- package/src/machines/track.machine.ts +1 -0
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Gapless.js
|
|
4
|
+
|
|
5
|
+
[](https://github.com/RelistenNet/gapless.js/actions/workflows/ci.yml)
|
|
6
|
+
[](https://bundlejs.com/?q=gapless&treeshake=[{+default+}])
|
|
2
7
|
|
|
3
8
|
Gapless audio player for the web. Takes an array of audio tracks and uses HTML5 audio with the Web Audio API to enable seamless, gapless transitions between tracks.
|
|
4
9
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createActor as X}from"xstate";var W=typeof window<"u",p;function $(){if(!W)return null;let o=window.AudioContext??window.webkitAudioContext;return o?new o:null}function k(){return p===void 0?null:p}function y(){p===void 0&&(p=$());let o=p;return o&&o.state==="suspended"?o.resume():Promise.resolve()}var g=typeof navigator<"u"&&"mediaSession"in navigator;function v(o){if(!g)return;let{mediaSession:e}=navigator;e.setActionHandler("play",o.onPlay),e.setActionHandler("pause",o.onPause),e.setActionHandler("nexttrack",o.onNext),e.setActionHandler("previoustrack",o.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&o.onSeek(t.seekTime)})}function P(o){if(g){if(!o){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:o.title??"",artist:o.artist??"",album:o.album??"",artwork:o.artwork??[]})}}function b(o){g&&(navigator.mediaSession.playbackState=o?"playing":"paused")}function C(o,e,t=1){if(g)try{navigator.mediaSession.setPositionState({duration:o,position:e,playbackRate:t})}catch{}}import{setup as q,assign as h}from"xstate";function N(o){return q({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,a=Math.max(0,e.trackCount-1);return n.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):a>0?Math.min(e.currentTrackIndex,a-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:o,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 Q,fromPromise as x}from"xstate";import{setup as K,assign as d,spawnChild as Y}from"xstate";import{setup as V,assign as _,sendParent as f,fromPromise as E}from"xstate";var m=V({types:{context:{},input:{}},actors:{resolveUrl:E(async()=>null),fetchAudio:E(async()=>{}),decodeAudio:E(async()=>{})},guards:{shouldSkipHEAD:({context:o})=>o.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:{trackUrl:"",resolvedUrl:"",skipHEAD:!1},entry:_(({event:o})=>{let{input:e}=o;return{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:o})=>({trackUrl:o.trackUrl}),onDone:{target:"fetching",actions:[_({resolvedUrl:({event:o,context:e})=>o.output??e.resolvedUrl,skipHEAD:()=>!0}),f(({event:o,context:e})=>({type:"URL_RESOLVED",url:o.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:_({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:o})=>({resolvedUrl:o.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:f({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:f({type:"BUFFER_READY"})},onError:{target:"error",actions:f({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function R(o){return K({types:{context:{},events:{}},actors:{fetchDecode:m},guards:{canPlayWebAudio:()=>!1,isWebAudioOnly:()=>!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:()=>{},triggerFetchForPendingPlay:()=>{},setPendingPlay:d({pendingPlay:()=>!0}),clearPendingPlay:d({pendingPlay:()=>!1}),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:o,on:{START_FETCH:{guard:"canStartFetch",actions:[d({webAudioLoadingState:()=>"LOADING",fetchStarted:()=>!0}),Y("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"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{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:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop"]},{actions:"setLoadedState"}],BUFFER_ERROR:{actions:["setErrorState","clearPendingPlay"]},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:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"idle",actions:"setLoadedState"}],BUFFER_ERROR:{target:"idle",actions:["setErrorState","clearPendingPlay"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{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 Z=5,L=15,T=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_playbackMethod;get ctx(){if(this._playbackMethod==="HTML5_ONLY")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;_waRefCtxTime=0;_waRefTrackTime=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 a=(c=this.audio.error)==null?void 0:c.code;if(a===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 ${a}): ${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._playbackMethod=e.queue.playbackMethod;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchStarted:!1,pendingPlay:!1},n=R(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode),isWebAudioOnly:()=>this._playbackMethod==="WEBAUDIO_ONLY"},actors:{fetchDecode:m.provide({actors:{resolveUrl:x(async({signal:a})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:a});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:x(async({input:a,signal:s})=>{let{resolvedUrl:c}=a,l=await fetch(c,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${c}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:x(async()=>{let a=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!a||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(a),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{triggerFetchForPendingPlay:()=>{this.preload(),y()},playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:a})=>{let s=a.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this._waRefCtxTime=s,this._waRefTrackTime=0,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:()=>{let a=this.currentTime;this.pausedAtTrackTime=isFinite(a)?a:0},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._waRefCtxTime=0,this._waRefTrackTime=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=Q(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){if(!isFinite(e))return;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})}setPlaybackRate(e){if(this.ctx&&this.sourceNode&&this._actor.getSnapshot().context.isPlaying){let t=this.sourceNode.playbackRate.value;this._waRefTrackTime=this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*t,this._waRefCtxTime=this.ctx.currentTime}this.audio.playbackRate=e,this.sourceNode&&(this.sourceNode.playbackRate.value=e)}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&&(y(),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._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*this.queueRef.playbackRate):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,playbackRate:this.queueRef.playbackRate,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.playbackRate=this.queueRef.playbackRate;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;isFinite(e)&&(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.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this._waRefCtxTime=this.ctx.currentTime,this._waRefTrackTime=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<=Z&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this)));let a=isNaN(this.duration)?L:Math.min(this.duration*.2,L);!this._notifiedPreloadThreshold&&this.currentTime>=a&&(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 D(o,e){let t=0;return(...n)=>{let a=performance.now();a-t<e||(t=a,o(...n))}}var A=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;playbackMethod;_volume;_preloadNumTracks;_playbackRate;_scheduledNextIndex=null;_throttledUpdatePositionState=D((e,t,n)=>C(e,t,n),1e3);constructor(e={}){let{tracks:t=[],onProgress:n,onEnded:a,onPlayNextTrack:s,onPlayPreviousTrack:c,onStartNewTrack:l,onError:I,onPlayBlocked:M,onDebug:U,playbackMethod:O="HYBRID",trackMetadata:w=[],volume:G=1,preloadNumTracks:H=2,playbackRate:B=1}=e;this._volume=Math.min(1,Math.max(0,G)),this._preloadNumTracks=Math.max(0,H),this._playbackRate=Math.min(4,Math.max(.25,B)),this.playbackMethod=O,this._onProgress=n,this._onEnded=a,this._onPlayNextTrack=s,this._onPlayPreviousTrack=c,this._onStartNewTrack=l,this._onError=I,this._onPlayBlocked=M,this._onDebug=U,this._tracks=t.map((i,r)=>new T({trackUrl:i,index:r,queue:this,metadata:w[r]}));let F=N({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.deactivate()},deactivateEndedTrack:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.deactivate()},activateAndPlayCurrent:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&(r.activate(),this._scheduledNextIndex!==r.index&&r.play())},playOrContinueGapless:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&(this._scheduledNextIndex!==r.index?r.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${r.index} \u2014 sourceNode=${r.hasSourceNode} isPlaying=${r.isPlaying} machineState=${r.machineState}`),r.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:i})=>{var u;let r=this._trackAt(i.currentTrackIndex);r&&((u=this._onStartNewTrack)==null||u.call(this,r.toInfo()))},notifyPlayNextTrack:({context:i})=>{var u;let r=this._trackAt(i.currentTrackIndex);r&&((u=this._onPlayNextTrack)==null||u.call(this,r.toInfo()))},notifyPlayPreviousTrack:({context:i})=>{var u;let r=this._trackAt(i.currentTrackIndex);r&&((u=this._onPlayPreviousTrack)==null||u.call(this,r.toInfo()))},notifyEnded:()=>{var i;return(i=this._onEnded)==null?void 0:i.call(this)},updateMediaSessionMetadata:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&P(r.metadata)},preloadAhead:({context:i})=>{this._preloadAhead(i.currentTrackIndex)},playCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.play()},pauseCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.pause()},seekCurrent:({context:i,event:r})=>{var S;let u=r;(S=this._trackAt(i.currentTrackIndex))==null||S.seek(u.time)},seekCurrentToZero:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.seek(0)},scheduleGapless:({context:i})=>{this._tryScheduleGapless(i.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:i})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(i.currentTrackIndex)}}});this._actor=X(F),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)}setPlaybackRate(e){var a;let t=Math.min(4,Math.max(.25,e));this._playbackRate=t,(a=this._currentTrack)==null||a.setPlaybackRate(t),this._cancelScheduledGapless();let n=this._actor.getSnapshot();n.value==="playing"&&this._tryScheduleGapless(n.context.currentTrackIndex)}addTrack(e,t={}){let n=this._tracks.length,a=t.metadata??{};this._tracks.push(new T({trackUrl:e,index:n,queue:this,skipHEAD:t.skipHEAD,metadata:a})),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 y()}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 preloadNumTracks(){return this._preloadNumTracks}set preloadNumTracks(e){this._preloadNumTracks=Math.max(0,e);let t=this._actor.getSnapshot();t.value==="playing"&&this._preloadAhead(t.context.currentTrackIndex)}get playbackRate(){return this._playbackRate}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,this._playbackRate),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;this._actor.send({type:"PAUSE"}),(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 a=isNaN(t.duration)?15:Math.min(t.duration*.2,15);if(t.currentTime<a){this.onDebug(`_preloadAhead: deferring \u2014 HTML5 track ${e} at ${t.currentTime.toFixed(1)}s (threshold=${a.toFixed(1)}s)`);return}}let n=e+this._preloadNumTracks+1;this.onDebug(`_preloadAhead(${e}) limit=${n} trackCount=${this._tracks.length}`);for(let a=e+1;a<this._tracks.length&&a<n;a++){let s=this._tracks[a];if(s.isBufferLoaded)this.onDebug(`_preloadAhead: track ${a} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${a}`),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.playbackMethod==="HTML5_ONLY")return;let n=e+1;if(n>=this._tracks.length)return;let a=this._tracks[e],s=this._tracks[n];if(!a.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===n||!a.isPlaying)return;let c=this._computeTrackEndTime(a);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/this._playbackRate;let a=(n-e.currentTime)/this._playbackRate;return a<=0?null:t.currentTime+a}};export{A as Queue,A as default};
|
|
1
|
+
import{createActor as z}from"xstate";var W=typeof window<"u",p;function q(){if(!W)return null;let o=window.AudioContext??window.webkitAudioContext;return o?new o:null}function k(){return p===void 0?null:p}function y(){p===void 0&&(p=q());let o=p;return o&&o.state==="suspended"?o.resume():Promise.resolve()}var g=typeof navigator<"u"&&"mediaSession"in navigator;function v(o){if(!g)return;let{mediaSession:e}=navigator;e.setActionHandler("play",o.onPlay),e.setActionHandler("pause",o.onPause),e.setActionHandler("nexttrack",o.onNext),e.setActionHandler("previoustrack",o.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&o.onSeek(t.seekTime)})}function P(o){if(g){if(!o){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:o.title??"",artist:o.artist??"",album:o.album??"",artwork:o.artwork??[]})}}function b(o){g&&(navigator.mediaSession.playbackState=o?"playing":"paused")}function C(o,e,t=1){if(g)try{navigator.mediaSession.setPositionState({duration:o,position:e,playbackRate:t})}catch{}}import{setup as V,assign as h}from"xstate";function N(o){return V({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,a=Math.max(0,e.trackCount-1);return n.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):a>0?Math.min(e.currentTrackIndex,a-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:o,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 Z,fromPromise as x}from"xstate";import{setup as Y,assign as d,spawnChild as Q}from"xstate";import{setup as K,assign as _,sendParent as f,fromPromise as E}from"xstate";var m=K({types:{context:{},input:{}},actors:{resolveUrl:E(async()=>null),fetchAudio:E(async()=>{}),decodeAudio:E(async()=>{})},guards:{shouldSkipHEAD:({context:o})=>o.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:{trackUrl:"",resolvedUrl:"",skipHEAD:!1},entry:_(({event:o})=>{let{input:e}=o;return{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:o})=>({trackUrl:o.trackUrl}),onDone:{target:"fetching",actions:[_({resolvedUrl:({event:o,context:e})=>o.output??e.resolvedUrl,skipHEAD:()=>!0}),f(({event:o,context:e})=>({type:"URL_RESOLVED",url:o.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:_({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:o})=>({resolvedUrl:o.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:f({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:f({type:"BUFFER_READY"})},onError:{target:"error",actions:f({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function R(o){return Y({types:{context:{},events:{}},actors:{fetchDecode:m},guards:{canPlayWebAudio:()=>!1,isWebAudioOnly:()=>!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:()=>{},triggerFetchForPendingPlay:()=>{},setPendingPlay:d({pendingPlay:()=>!0}),clearPendingPlay:d({pendingPlay:()=>!1}),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:o,on:{START_FETCH:{guard:"canStartFetch",actions:[d({webAudioLoadingState:()=>"LOADING",fetchStarted:()=>!0}),Q("fetchDecode",{id:"fetchDecode",input:({context:e})=>({trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD})})]}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{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:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop"]},{actions:"setLoadedState"}],BUFFER_ERROR:{actions:["setErrorState","clearPendingPlay"]},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:[{guard:({context:e})=>e.pendingPlay,target:"webaudio",actions:["clearPendingPlay","setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"idle",actions:"setLoadedState"}],BUFFER_ERROR:{target:"idle",actions:["setErrorState","clearPendingPlay"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{guard:"isWebAudioOnly",actions:["setPendingPlay","triggerFetchForPendingPlay"]},{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 X=5,L=15,T=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_playbackMethod;get ctx(){if(this._playbackMethod==="HTML5_ONLY")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;_waRefCtxTime=0;_waRefTrackTime=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,u;let a=(c=this.audio.error)==null?void 0:c.code;if(a===1)return;let s=((u=this.audio.error)==null?void 0:u.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${a}): ${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._playbackMethod=e.queue.playbackMethod;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchStarted:!1,pendingPlay:!1},n=R(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode),isWebAudioOnly:()=>this._playbackMethod==="WEBAUDIO_ONLY"},actors:{fetchDecode:m.provide({actors:{resolveUrl:x(async({signal:a})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:a});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:x(async({input:a,signal:s})=>{let{resolvedUrl:c}=a,u=await fetch(c,{signal:s});if(!u.ok)throw new Error(`HTTP ${u.status} for ${c}`);this._pendingArrayBuffer=await u.arrayBuffer()}),decodeAudio:x(async()=>{let a=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!a||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(a),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{triggerFetchForPendingPlay:()=>{this.preload(),y()},playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:a})=>{let s=a.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this._waRefCtxTime=s,this._waRefTrackTime=0,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:()=>{let a=this.currentTime;this.pausedAtTrackTime=isFinite(a)?a:0},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._waRefCtxTime=0,this._waRefTrackTime=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=Z(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){if(!isFinite(e))return;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})}setPlaybackRate(e){if(this.ctx&&this.sourceNode&&this._actor.getSnapshot().context.isPlaying){let t=this.sourceNode.playbackRate.value;this._waRefTrackTime=this._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*t,this._waRefCtxTime=this.ctx.currentTime}this.audio.playbackRate=e,this.sourceNode&&(this.sourceNode.playbackRate.value=e)}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&&(y(),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._waRefTrackTime+(this.ctx.currentTime-this._waRefCtxTime)*this.queueRef.playbackRate):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,playbackRate:this.queueRef.playbackRate,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.playbackRate=this.queueRef.playbackRate;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;isFinite(e)&&(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.playbackRate.value=this.queueRef.playbackRate,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this._waRefCtxTime=this.ctx.currentTime,this._waRefTrackTime=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<=X&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this)));let a=isNaN(this.duration)?L:Math.min(this.duration*.2,L);!this._notifiedPreloadThreshold&&this.currentTime>=a&&(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 D(o,e){let t=0;return(...n)=>{let a=performance.now();a-t<e||(t=a,o(...n))}}var I=5,A=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;playbackMethod;_volume;_preloadNumTracks;_playbackRate;_scheduledNextIndex=null;_throttledUpdatePositionState=D((e,t,n)=>C(e,t,n),1e3);constructor(e={}){let{tracks:t=[],onProgress:n,onEnded:a,onPlayNextTrack:s,onPlayPreviousTrack:c,onStartNewTrack:u,onError:M,onPlayBlocked:U,onDebug:O,playbackMethod:w="HYBRID",trackMetadata:G=[],volume:H=1,preloadNumTracks:B=2,playbackRate:F=1}=e;this._volume=Math.min(1,Math.max(0,H)),this._preloadNumTracks=Math.max(0,B),this._playbackRate=Math.min(4,Math.max(.25,F)),this.playbackMethod=w,this._onProgress=n,this._onEnded=a,this._onPlayNextTrack=s,this._onPlayPreviousTrack=c,this._onStartNewTrack=u,this._onError=M,this._onPlayBlocked=U,this._onDebug=O,this._tracks=t.map((i,r)=>new T({trackUrl:i,index:r,queue:this,metadata:G[r]}));let $=N({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.deactivate()},deactivateEndedTrack:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.deactivate()},activateAndPlayCurrent:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&(r.activate(),this._scheduledNextIndex!==r.index&&r.play())},playOrContinueGapless:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&(this._scheduledNextIndex!==r.index?r.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${r.index} \u2014 sourceNode=${r.hasSourceNode} isPlaying=${r.isPlaying} machineState=${r.machineState}`),r.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:i})=>{var l;let r=this._trackAt(i.currentTrackIndex);r&&((l=this._onStartNewTrack)==null||l.call(this,r.toInfo()))},notifyPlayNextTrack:({context:i})=>{var l;let r=this._trackAt(i.currentTrackIndex);r&&((l=this._onPlayNextTrack)==null||l.call(this,r.toInfo()))},notifyPlayPreviousTrack:({context:i})=>{var l;let r=this._trackAt(i.currentTrackIndex);r&&((l=this._onPlayPreviousTrack)==null||l.call(this,r.toInfo()))},notifyEnded:()=>{var i;return(i=this._onEnded)==null?void 0:i.call(this)},updateMediaSessionMetadata:({context:i})=>{let r=this._trackAt(i.currentTrackIndex);r&&P(r.metadata)},preloadAhead:({context:i})=>{this._preloadAhead(i.currentTrackIndex)},playCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.play()},pauseCurrent:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.pause()},seekCurrent:({context:i,event:r})=>{var S;let l=r;(S=this._trackAt(i.currentTrackIndex))==null||S.seek(l.time)},seekCurrentToZero:({context:i})=>{var r;(r=this._trackAt(i.currentTrackIndex))==null||r.seek(0)},scheduleGapless:({context:i})=>{this._tryScheduleGapless(i.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:i})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(i.currentTrackIndex)}}});this._actor=z($),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)}setPlaybackRate(e){var a;let t=Math.min(4,Math.max(.25,e));this._playbackRate=t,(a=this._currentTrack)==null||a.setPlaybackRate(t),this._cancelScheduledGapless();let n=this._actor.getSnapshot();n.value==="playing"&&this._tryScheduleGapless(n.context.currentTrackIndex)}addTrack(e,t={}){let n=this._tracks.length,a=t.metadata??{};this._tracks.push(new T({trackUrl:e,index:n,queue:this,skipHEAD:t.skipHEAD,metadata:a})),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 y()}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 preloadNumTracks(){return this._preloadNumTracks}set preloadNumTracks(e){this._preloadNumTracks=Math.max(0,e);let t=this._actor.getSnapshot();t.value==="playing"&&this._preloadAhead(t.context.currentTrackIndex)}get playbackRate(){return this._playbackRate}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,this._playbackRate),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;this._actor.send({type:"PAUSE"}),(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 a=isNaN(t.duration)?15:Math.min(t.duration*.2,15);if(t.currentTime<a){this.onDebug(`_preloadAhead: deferring \u2014 HTML5 track ${e} at ${t.currentTime.toFixed(1)}s (threshold=${a.toFixed(1)}s)`);return}}let n=e+this._preloadNumTracks+1;this.onDebug(`_preloadAhead(${e}) limit=${n} trackCount=${this._tracks.length}`);for(let a=e+1;a<this._tracks.length&&a<n;a++){let s=this._tracks[a];if(s.isBufferLoaded)this.onDebug(`_preloadAhead: track ${a} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${a}`),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.playbackMethod==="HTML5_ONLY")return;let n=e+1;if(n>=this._tracks.length)return;let a=this._tracks[e],s=this._tracks[n];if(!a.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===n||!a.isPlaying)return;let c=this._computeTrackEndTime(a);if(c===null||c<t.currentTime+.01)return;let u=c-t.currentTime;if(a.playbackType==="HTML5"&&u>I){this.onDebug(`_tryScheduleGapless: deferring \u2014 HTML5 track ${e} has ${u.toFixed(1)}s remaining (max lookahead=${I}s)`);return}s.scheduleGaplessStart(c),this.onDebug(`_tryScheduleGapless: scheduled track ${n} at endTime=${c.toFixed(3)} (in ${(c-t.currentTime).toFixed(1)}s) curPlaybackType=${a.playbackType}`),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/this._playbackRate;let a=(n-e.currentTime)/this._playbackRate;return a<=0?null:t.currentTime+a}};export{A as Queue,A 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, PlaybackMethod } from './types';\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 playbackMethod: PlaybackMethod;\n\n private _volume: number;\n private _preloadNumTracks: number;\n private _playbackRate: 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, playbackRate: number) =>\n updateMediaSessionPositionState(duration, currentTime, playbackRate),\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 playbackMethod = 'HYBRID',\n trackMetadata = [],\n volume: initialVolume = 1,\n preloadNumTracks = 2,\n playbackRate: initialPlaybackRate = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this._preloadNumTracks = Math.max(0, preloadNumTracks);\n this._playbackRate = Math.min(4, Math.max(0.25, initialPlaybackRate));\n this.playbackMethod = playbackMethod;\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 setPlaybackRate(rate: number): void {\n const clamped = Math.min(4, Math.max(0.25, rate));\n this._playbackRate = clamped;\n this._currentTrack?.setPlaybackRate(clamped);\n this._cancelScheduledGapless();\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._tryScheduleGapless(snap.context.currentTrackIndex);\n }\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 get preloadNumTracks(): number {\n return this._preloadNumTracks;\n }\n\n set preloadNumTracks(value: number) {\n this._preloadNumTracks = Math.max(0, value);\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n }\n\n get playbackRate(): number {\n return this._playbackRate;\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, this._playbackRate);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._actor.send({ type: 'PAUSE' });\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 + this._preloadNumTracks + 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.playbackMethod === 'HTML5_ONLY') 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 / this._playbackRate;\n }\n\n const remaining = (duration - track.currentTime) / this._playbackRate;\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, PlaybackMethod } 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 playbackMethod: PlaybackMethod;\n readonly playbackRate: number;\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 _playbackMethod: PlaybackMethod;\n\n private get ctx(): AudioContext | null {\n if (this._playbackMethod === 'HTML5_ONLY') 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 at the start of the current playback segment. */\n private _waRefCtxTime = 0;\n /** Track position (seconds) at the start of the current playback segment. */\n private _waRefTrackTime = 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._playbackMethod = opts.queue.playbackMethod;\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 pendingPlay: false,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n isWebAudioOnly: () => this._playbackMethod === 'WEBAUDIO_ONLY',\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 triggerFetchForPendingPlay: () => {\n this.preload();\n resumeAudioContext();\n },\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.playbackRate.value = this.queueRef.playbackRate;\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._waRefCtxTime = when;\n this._waRefTrackTime = 0;\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 const t = this.currentTime;\n this.pausedAtTrackTime = isFinite(t) ? t : 0;\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._waRefCtxTime = 0;\n this._waRefTrackTime = 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 if (!isFinite(time)) return;\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 setPlaybackRate(rate: number): void {\n // Freeze current track position at the old rate before switching\n if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying) {\n const oldRate = this.sourceNode.playbackRate.value;\n this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;\n this._waRefCtxTime = this.ctx.currentTime;\n }\n this.audio.playbackRate = rate;\n if (this.sourceNode) this.sourceNode.playbackRate.value = rate;\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._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * this.queueRef.playbackRate);\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 playbackRate: this.queueRef.playbackRate,\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 this.audio.playbackRate = this.queueRef.playbackRate;\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 (!isFinite(clamped)) return;\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.playbackRate.value = this.queueRef.playbackRate;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = this.ctx.currentTime;\n this._waRefTrackTime = 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 /** True when PLAY was received in webAudioOnly mode before buffer is ready. */\n pendingPlay: 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 isWebAudioOnly: () => 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 triggerFetchForPendingPlay: () => {},\n setPendingPlay: assign({ pendingPlay: () => true }),\n clearPendingPlay: assign({ pendingPlay: () => false }),\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 guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\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 {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setLoadedState',\n },\n ],\n BUFFER_ERROR: {\n actions: ['setErrorState', 'clearPendingPlay'],\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 {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],\n },\n {\n target: 'idle',\n actions: 'setLoadedState',\n },\n ],\n BUFFER_ERROR: {\n target: 'idle',\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\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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","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","resumeAudioContext","when","t","createActor","time","clamped","v","vol","rate","oldRate","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","preloadThreshold","throttle","fn","ms","last","args","now","Queue","throttle","duration","currentTime","playbackRate","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","playbackMethod","trackMetadata","initialVolume","preloadNumTracks","initialPlaybackRate","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","rate","snap","metadata","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","threshold","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, PlaybackMethod } from './types';\n\nconst MAX_SCHEDULE_LOOKAHEAD = 5;\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 playbackMethod: PlaybackMethod;\n\n private _volume: number;\n private _preloadNumTracks: number;\n private _playbackRate: 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, playbackRate: number) =>\n updateMediaSessionPositionState(duration, currentTime, playbackRate),\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 playbackMethod = 'HYBRID',\n trackMetadata = [],\n volume: initialVolume = 1,\n preloadNumTracks = 2,\n playbackRate: initialPlaybackRate = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this._preloadNumTracks = Math.max(0, preloadNumTracks);\n this._playbackRate = Math.min(4, Math.max(0.25, initialPlaybackRate));\n this.playbackMethod = playbackMethod;\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 setPlaybackRate(rate: number): void {\n const clamped = Math.min(4, Math.max(0.25, rate));\n this._playbackRate = clamped;\n this._currentTrack?.setPlaybackRate(clamped);\n this._cancelScheduledGapless();\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._tryScheduleGapless(snap.context.currentTrackIndex);\n }\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 get preloadNumTracks(): number {\n return this._preloadNumTracks;\n }\n\n set preloadNumTracks(value: number) {\n this._preloadNumTracks = Math.max(0, value);\n const snap = this._actor.getSnapshot();\n if (snap.value === 'playing') {\n this._preloadAhead(snap.context.currentTrackIndex);\n }\n }\n\n get playbackRate(): number {\n return this._playbackRate;\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, this._playbackRate);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._actor.send({ type: 'PAUSE' });\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 + this._preloadNumTracks + 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.playbackMethod === 'HTML5_ONLY') 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 const timeUntilEnd = endTime - ctx.currentTime;\n if (current.playbackType === 'HTML5' && timeUntilEnd > MAX_SCHEDULE_LOOKAHEAD) {\n this.onDebug(\n `_tryScheduleGapless: deferring — HTML5 track ${curIndex} has ${timeUntilEnd.toFixed(1)}s remaining (max lookahead=${MAX_SCHEDULE_LOOKAHEAD}s)`\n );\n return;\n }\n\n next.scheduleGaplessStart(endTime);\n this.onDebug(\n `_tryScheduleGapless: scheduled track ${nextIndex} at endTime=${endTime.toFixed(3)} (in ${(endTime - ctx.currentTime).toFixed(1)}s) curPlaybackType=${current.playbackType}`\n );\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 / this._playbackRate;\n }\n\n const remaining = (duration - track.currentTime) / this._playbackRate;\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, PlaybackMethod } 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 playbackMethod: PlaybackMethod;\n readonly playbackRate: number;\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 _playbackMethod: PlaybackMethod;\n\n private get ctx(): AudioContext | null {\n if (this._playbackMethod === 'HTML5_ONLY') 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 at the start of the current playback segment. */\n private _waRefCtxTime = 0;\n /** Track position (seconds) at the start of the current playback segment. */\n private _waRefTrackTime = 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._playbackMethod = opts.queue.playbackMethod;\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 pendingPlay: false,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n isWebAudioOnly: () => this._playbackMethod === 'WEBAUDIO_ONLY',\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 triggerFetchForPendingPlay: () => {\n this.preload();\n resumeAudioContext();\n },\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.playbackRate.value = this.queueRef.playbackRate;\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._waRefCtxTime = when;\n this._waRefTrackTime = 0;\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 const t = this.currentTime;\n this.pausedAtTrackTime = isFinite(t) ? t : 0;\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._waRefCtxTime = 0;\n this._waRefTrackTime = 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 if (!isFinite(time)) return;\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 setPlaybackRate(rate: number): void {\n // Freeze current track position at the old rate before switching\n if (this.ctx && this.sourceNode && this._actor.getSnapshot().context.isPlaying) {\n const oldRate = this.sourceNode.playbackRate.value;\n this._waRefTrackTime = this._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * oldRate;\n this._waRefCtxTime = this.ctx.currentTime;\n }\n this.audio.playbackRate = rate;\n if (this.sourceNode) this.sourceNode.playbackRate.value = rate;\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._waRefTrackTime + (this.ctx.currentTime - this._waRefCtxTime) * this.queueRef.playbackRate);\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 playbackRate: this.queueRef.playbackRate,\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 this.audio.playbackRate = this.queueRef.playbackRate;\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 (!isFinite(clamped)) return;\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.playbackRate.value = this.queueRef.playbackRate;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this._waRefCtxTime = this.ctx.currentTime;\n this._waRefTrackTime = 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 /** True when PLAY was received in webAudioOnly mode before buffer is ready. */\n pendingPlay: 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 isWebAudioOnly: () => 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 triggerFetchForPendingPlay: () => {},\n setPendingPlay: assign({ pendingPlay: () => true }),\n clearPendingPlay: assign({ pendingPlay: () => false }),\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 'pauseHtml5',\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 guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\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 {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setLoadedState',\n },\n ],\n BUFFER_ERROR: {\n actions: ['setErrorState', 'clearPendingPlay'],\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 {\n guard: ({ context }: { context: TrackContext }) => context.pendingPlay,\n target: 'webaudio',\n actions: ['clearPendingPlay', 'setPlayingWebAudio', 'startSourceNode', 'startProgressLoop'],\n },\n {\n target: 'idle',\n actions: 'setLoadedState',\n },\n ],\n BUFFER_ERROR: {\n target: 'idle',\n actions: ['setErrorState', 'clearPendingPlay'],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n guard: 'isWebAudioOnly',\n actions: ['setPendingPlay', 'triggerFetchForPendingPlay'],\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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","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","resumeAudioContext","when","t","createActor","time","clamped","v","vol","rate","oldRate","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","preloadThreshold","throttle","fn","ms","last","args","now","MAX_SCHEDULE_LOOKAHEAD","Queue","throttle","duration","currentTime","playbackRate","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","playbackMethod","trackMetadata","initialVolume","preloadNumTracks","initialPlaybackRate","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","rate","snap","metadata","resumeAudioContext","value","newSnap","info","error","msg","fromIndex","threshold","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","timeUntilEnd","remaining"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gapless",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Gapless audio playback javascript plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@switz/eslint-config": "^12.5.2",
|
|
42
42
|
"@vitest/coverage-v8": "^4.0.18",
|
|
43
43
|
"eslint": "^9.25.1",
|
|
44
|
-
"happy-dom": "^20.
|
|
44
|
+
"happy-dom": "^20.8.3",
|
|
45
45
|
"playwright": "^1.58.2",
|
|
46
46
|
"tsup": "^8.5.1",
|
|
47
47
|
"typescript": "^5.9.3",
|
package/src/Queue.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { TrackQueueRef } from './Track';
|
|
|
16
16
|
import { throttle } from './utils/throttle';
|
|
17
17
|
import type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata, PlaybackMethod } from './types';
|
|
18
18
|
|
|
19
|
+
const MAX_SCHEDULE_LOOKAHEAD = 5;
|
|
20
|
+
|
|
19
21
|
export class Queue implements TrackQueueRef {
|
|
20
22
|
private _tracks: Track[] = [];
|
|
21
23
|
private readonly _actor;
|
|
@@ -470,7 +472,18 @@ export class Queue implements TrackQueueRef {
|
|
|
470
472
|
|
|
471
473
|
if (endTime < ctx.currentTime + 0.01) return;
|
|
472
474
|
|
|
475
|
+
const timeUntilEnd = endTime - ctx.currentTime;
|
|
476
|
+
if (current.playbackType === 'HTML5' && timeUntilEnd > MAX_SCHEDULE_LOOKAHEAD) {
|
|
477
|
+
this.onDebug(
|
|
478
|
+
`_tryScheduleGapless: deferring — HTML5 track ${curIndex} has ${timeUntilEnd.toFixed(1)}s remaining (max lookahead=${MAX_SCHEDULE_LOOKAHEAD}s)`
|
|
479
|
+
);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
473
483
|
next.scheduleGaplessStart(endTime);
|
|
484
|
+
this.onDebug(
|
|
485
|
+
`_tryScheduleGapless: scheduled track ${nextIndex} at endTime=${endTime.toFixed(3)} (in ${(endTime - ctx.currentTime).toFixed(1)}s) curPlaybackType=${current.playbackType}`
|
|
486
|
+
);
|
|
474
487
|
this._scheduledNextIndex = nextIndex;
|
|
475
488
|
}
|
|
476
489
|
|