gapless 4.0.6 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -9
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -9
- package/src/Track.ts +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -28,14 +28,6 @@ interface GaplessOptions {
|
|
|
28
28
|
onDebug?: (msg: string) => void;
|
|
29
29
|
/** Called when autoplay is blocked by the browser. */
|
|
30
30
|
onPlayBlocked?: () => void;
|
|
31
|
-
/** Called whenever the queue state machine transitions. */
|
|
32
|
-
onQueueStateChange?: (snapshot: {
|
|
33
|
-
state: string;
|
|
34
|
-
context: {
|
|
35
|
-
currentTrackIndex: number;
|
|
36
|
-
trackCount: number;
|
|
37
|
-
};
|
|
38
|
-
}) => void;
|
|
39
31
|
/**
|
|
40
32
|
* Set true to disable Web Audio API entirely and use HTML5 audio only.
|
|
41
33
|
* Gapless playback will not be available in this mode.
|
|
@@ -168,7 +160,6 @@ declare class Queue implements TrackQueueRef {
|
|
|
168
160
|
private readonly _onStartNewTrack?;
|
|
169
161
|
private readonly _onError?;
|
|
170
162
|
private readonly _onPlayBlocked?;
|
|
171
|
-
private readonly _onQueueStateChange?;
|
|
172
163
|
private readonly _onDebug?;
|
|
173
164
|
readonly webAudioIsDisabled: boolean;
|
|
174
165
|
private _volume;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createActor as Y}from"xstate";var B=typeof window<"u",p;function $(){if(!B)return null;let r=window.AudioContext??window.webkitAudioContext;return r?new r:null}function k(){return p===void 0?null:p}function x(){p===void 0&&(p=$());let r=p;return r&&r.state==="suspended"?r.resume():Promise.resolve()}var T=typeof navigator<"u"&&"mediaSession"in navigator;function v(r){if(!T)return;let{mediaSession:e}=navigator;e.setActionHandler("play",r.onPlay),e.setActionHandler("pause",r.onPause),e.setActionHandler("nexttrack",r.onNext),e.setActionHandler("previoustrack",r.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&r.onSeek(t.seekTime)})}function P(r){if(T){if(!r){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:r.title??"",artist:r.artist??"",album:r.album??"",artwork:r.artwork??[]})}}function b(r){T&&(navigator.mediaSession.playbackState=r?"playing":"paused")}function m(r,e,t=1){if(T)try{navigator.mediaSession.setPositionState({duration:r,position:e,playbackRate:t})}catch{}}import{setup as F,assign as h}from"xstate";function D(r){return F({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>t.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):e.currentTrackIndex}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>e.currentTrackIndex+1}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:r,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["scheduleGapless","preloadAhead"]}}},paused:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as V,fromPromise as S}from"xstate";import{setup as q,assign as d}from"xstate";import{setup as W,assign as C,sendParent as y,fromPromise as E}from"xstate";var A=W({types:{context:{},input:{}},actors:{resolveUrl:E(async()=>null),fetchAudio:E(async()=>{}),decodeAudio:E(async()=>{})},guards:{shouldSkipHEAD:({context:r})=>r.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:({input:r})=>({trackUrl:r.trackUrl,resolvedUrl:r.resolvedUrl,skipHEAD:r.skipHEAD}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:r})=>({trackUrl:r.trackUrl}),onDone:{target:"fetching",actions:[C({resolvedUrl:({event:r,context:e})=>r.output??e.resolvedUrl,skipHEAD:()=>!0}),y(({event:r,context:e})=>({type:"URL_RESOLVED",url:r.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:C({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:r})=>({resolvedUrl:r.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:y({type:"BUFFER_READY"})},onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function N(r){return q({types:{context:{},events:{}},actors:{fetchDecode:A},guards:{canPlayWebAudio:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&e.fetchDecodeRef===null},actions:{playHtml5:()=>{},startSourceNode:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},disconnectGain:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},setIsPlaying:d({isPlaying:()=>!0}),clearIsPlaying:d({isPlaying:()=>!1}),setLoadingState:d({webAudioLoadingState:()=>"LOADING"}),setLoadedState:d({webAudioLoadingState:()=>"LOADED"}),setErrorState:d({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:d({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:d({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:d({notifiedLookahead:()=>!0}),setResolvedUrl:d({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:d({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:d({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"})}}).createMachine({id:"track",initial:"idle",context:r,on:{START_FETCH:{guard:"canStartFetch",actions:d({webAudioLoadingState:()=>"LOADING",fetchDecodeRef:({context:e,spawn:t})=>t("fetchDecode",{id:"fetchDecode",input:{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}})})}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{target:"idle",actions:"setLoadedState"},BUFFER_ERROR:{target:"idle",actions:"setErrorState"},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["freezePausedTime","clearIsPlaying","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 K=5,g=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_webAudioDisabled;get ctx(){if(this._webAudioDisabled)return null;let e=k();return e&&!this.gainNode&&(this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume),e}gainNode=null;sourceNode=null;audioBuffer=null;webAudioStartedAt=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var u,l;let n=(u=this.audio.error)==null?void 0:u.code;if(n===1)return;let s=((l=this.audio.error)==null?void 0:l.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${n}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._webAudioDisabled=e.queue.webAudioIsDisabled;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchDecodeRef:null},i=N(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode)},actors:{fetchDecode:A.provide({actors:{resolveUrl:S(async({signal:n})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:n});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:S(async({input:n,signal:s})=>{let{resolvedUrl:u}=n,l=await fetch(u,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${u}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:S(async()=>{let n=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!n||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(n),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:n})=>{let s=n.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this.webAudioStartedAt=s,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{this.pausedAtTrackTime=this.currentTime},stopSourceNode:()=>this._stopSourceNode(),disconnectGain:()=>this._disconnectGain(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>this.queueRef.onProgress(this.toInfo()),seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this.webAudioStartedAt=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=V(i),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!(this.audioBuffer||!this.ctx)&&this._actor.send({type:"START_FETCH"})}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this.ctx.currentTime-this.webAudioStartedAt):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load())}_startSourceNode(e){!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.webAudioStartedAt=this.ctx.currentTime-e,this.sourceNode.start(0,e))}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_disconnectGain(){if(!(!this.gainNode||!this.ctx))try{this.gainNode.disconnect(this.ctx.destination)}catch{}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,i=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(i)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=K&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function L(r,e){let t=0;return(...i)=>{let n=performance.now();n-t<e||(t=n,r(...i))}}var Q=2,f=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onQueueStateChange;_onDebug;webAudioIsDisabled;_volume;_scheduledNextIndex=null;_throttledUpdatePositionState=L((e,t)=>m(e,t),1e3);constructor(e={}){let{tracks:t=[],onProgress:i,onEnded:n,onPlayNextTrack:s,onPlayPreviousTrack:u,onStartNewTrack:l,onError:I,onPlayBlocked:R,onQueueStateChange:U,onDebug:M,webAudioIsDisabled:w=!1,trackMetadata:O=[],volume:G=1}=e;this._volume=Math.min(1,Math.max(0,G)),this.webAudioIsDisabled=w,this._onProgress=i,this._onEnded=n,this._onPlayNextTrack=s,this._onPlayPreviousTrack=u,this._onStartNewTrack=l,this._onError=I,this._onPlayBlocked=R,this._onQueueStateChange=U,this._onDebug=M,this._tracks=t.map((o,a)=>new g({trackUrl:o,index:a,queue:this,metadata:O[a]}));let H=D({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},deactivateEndedTrack:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},activateAndPlayCurrent:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(a.activate(),this._scheduledNextIndex!==a.index&&a.play())},playOrContinueGapless:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(this._scheduledNextIndex!==a.index?a.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${a.index} \u2014 sourceNode=${a.hasSourceNode} isPlaying=${a.isPlaying} machineState=${a.machineState}`),a.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onStartNewTrack)==null||c.call(this,a.toInfo()))},notifyPlayNextTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onPlayNextTrack)==null||c.call(this,a.toInfo()))},notifyPlayPreviousTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onPlayPreviousTrack)==null||c.call(this,a.toInfo()))},notifyEnded:()=>{var o;return(o=this._onEnded)==null?void 0:o.call(this)},updateMediaSessionMetadata:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&P(a.metadata)},preloadAhead:({context:o})=>{this._preloadAhead(o.currentTrackIndex)},playCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.play()},pauseCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.pause()},seekCurrent:({context:o,event:a})=>{var _;let c=a;(_=this._trackAt(o.currentTrackIndex))==null||_.seek(c.time)},seekCurrentToZero:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.seek(0)},scheduleGapless:({context:o})=>{this._tryScheduleGapless(o.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:o})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(o.currentTrackIndex)}}});this._actor=Y(H),this._actor.subscribe(o=>{var c;b(o.value==="playing");let a=this._trackAt(o.context.currentTrackIndex);a&&!isNaN(a.duration)&&m(a.duration,a.currentTime),(c=this._onQueueStateChange)==null||c.call(this,{state:o.value,context:o.context})}),this._actor.start(),v({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:o=>this.seek(o)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let i of this._tracks)i.setVolume(t)}addTrack(e,t={}){let i=this._tracks.length,n=t.metadata??{};this._tracks.push(new g({trackUrl:e,index:i,queue:this,skipHEAD:t.skipHEAD,metadata:n})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e&&(this._scheduledNextIndex=null),this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return x()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e.index!==t.context.currentTrackIndex)return;this._actor.send({type:"TRACK_ENDED"});let i=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${i.value} curIdx=${i.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;(e=this._onPlayBlocked)==null||e.call(this)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=e+Q+1;this.onDebug(`_preloadAhead(${e}) limit=${t} trackCount=${this._tracks.length}`);for(let i=e+1;i<this._tracks.length&&i<t;i++){let n=this._tracks[i];if(n.isBufferLoaded)this.onDebug(`_preloadAhead: track ${i} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${i}`),n.preload();break}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.webAudioIsDisabled)return;let i=e+1;if(i>=this._tracks.length)return;let n=this._tracks[e],s=this._tracks[i];if(!n.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===i||!n.isPlaying)return;let u=this._computeTrackEndTime(n);u!==null&&(u<t.currentTime+.01||(s.scheduleGaplessStart(u),this._scheduledNextIndex=i))}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let i=e.duration;if(isNaN(i))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+i;let n=i-e.currentTime;return n<=0?null:t.currentTime+n}};export{f as Queue,f as default};
|
|
1
|
+
import{createActor as K}from"xstate";var H=typeof window<"u",p;function B(){if(!H)return null;let r=window.AudioContext??window.webkitAudioContext;return r?new r:null}function k(){return p===void 0?null:p}function _(){p===void 0&&(p=B());let r=p;return r&&r.state==="suspended"?r.resume():Promise.resolve()}var T=typeof navigator<"u"&&"mediaSession"in navigator;function x(r){if(!T)return;let{mediaSession:e}=navigator;e.setActionHandler("play",r.onPlay),e.setActionHandler("pause",r.onPause),e.setActionHandler("nexttrack",r.onNext),e.setActionHandler("previoustrack",r.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&r.onSeek(t.seekTime)})}function v(r){if(T){if(!r){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:r.title??"",artist:r.artist??"",album:r.album??"",artwork:r.artwork??[]})}}function P(r){T&&(navigator.mediaSession.playbackState=r?"playing":"paused")}function b(r,e,t=1){if(T)try{navigator.mediaSession.setPositionState({duration:r,position:e,playbackRate:t})}catch{}}import{setup as $,assign as h}from"xstate";function D(r){return $({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>t.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):e.currentTrackIndex}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>e.currentTrackIndex+1}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:r,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["scheduleGapless","preloadAhead"]}}},paused:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as q,fromPromise as E}from"xstate";import{setup as W,assign as d}from"xstate";import{setup as F,assign as C,sendParent as y,fromPromise as m}from"xstate";var A=F({types:{context:{},input:{}},actors:{resolveUrl:m(async()=>null),fetchAudio:m(async()=>{}),decodeAudio:m(async()=>{})},guards:{shouldSkipHEAD:({context:r})=>r.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:({input:r})=>({trackUrl:r.trackUrl,resolvedUrl:r.resolvedUrl,skipHEAD:r.skipHEAD}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:r})=>({trackUrl:r.trackUrl}),onDone:{target:"fetching",actions:[C({resolvedUrl:({event:r,context:e})=>r.output??e.resolvedUrl,skipHEAD:()=>!0}),y(({event:r,context:e})=>({type:"URL_RESOLVED",url:r.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:C({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:r})=>({resolvedUrl:r.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:y({type:"BUFFER_READY"})},onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function L(r){return W({types:{context:{},events:{}},actors:{fetchDecode:A},guards:{canPlayWebAudio:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&e.fetchDecodeRef===null},actions:{playHtml5:()=>{},startSourceNode:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},disconnectGain:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},setIsPlaying:d({isPlaying:()=>!0}),clearIsPlaying:d({isPlaying:()=>!1}),setLoadingState:d({webAudioLoadingState:()=>"LOADING"}),setLoadedState:d({webAudioLoadingState:()=>"LOADED"}),setErrorState:d({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:d({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:d({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:d({notifiedLookahead:()=>!0}),setResolvedUrl:d({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:d({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:d({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"})}}).createMachine({id:"track",initial:"idle",context:r,on:{START_FETCH:{guard:"canStartFetch",actions:d({webAudioLoadingState:()=>"LOADING",fetchDecodeRef:({context:e,spawn:t})=>t("fetchDecode",{id:"fetchDecode",input:{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}})})}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{target:"idle",actions:"setLoadedState"},BUFFER_ERROR:{target:"idle",actions:"setErrorState"},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["clearIsPlaying","freezePausedTime","stopSourceNode","disconnectGain","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var V=5,g=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_webAudioDisabled;get ctx(){if(this._webAudioDisabled)return null;let e=k();return e&&!this.gainNode&&(this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume),e}gainNode=null;sourceNode=null;audioBuffer=null;webAudioStartedAt=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var c,l;let n=(c=this.audio.error)==null?void 0:c.code;if(n===1)return;let s=((l=this.audio.error)==null?void 0:l.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${n}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._webAudioDisabled=e.queue.webAudioIsDisabled;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchDecodeRef:null},i=L(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode)},actors:{fetchDecode:A.provide({actors:{resolveUrl:E(async({signal:n})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:n});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:E(async({input:n,signal:s})=>{let{resolvedUrl:c}=n,l=await fetch(c,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${c}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:E(async()=>{let n=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!n||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(n),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:n})=>{let s=n.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this.webAudioStartedAt=s,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{this.pausedAtTrackTime=this.currentTime},stopSourceNode:()=>this._stopSourceNode(),disconnectGain:()=>this._disconnectGain(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>{queueMicrotask(()=>this.queueRef.onProgress(this.toInfo()))},seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this.webAudioStartedAt=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=q(i),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!(this.audioBuffer||!this.ctx)&&this._actor.send({type:"START_FETCH"})}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this.ctx.currentTime-this.webAudioStartedAt):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load())}_startSourceNode(e){!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.webAudioStartedAt=this.ctx.currentTime-e,this.sourceNode.start(0,e))}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_disconnectGain(){if(!(!this.gainNode||!this.ctx))try{this.gainNode.disconnect(this.ctx.destination)}catch{}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,i=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(i)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=V&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function N(r,e){let t=0;return(...i)=>{let n=performance.now();n-t<e||(t=n,r(...i))}}var Y=2,f=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onDebug;webAudioIsDisabled;_volume;_scheduledNextIndex=null;_throttledUpdatePositionState=N((e,t)=>b(e,t),1e3);constructor(e={}){let{tracks:t=[],onProgress:i,onEnded:n,onPlayNextTrack:s,onPlayPreviousTrack:c,onStartNewTrack:l,onError:I,onPlayBlocked:R,onDebug:U,webAudioIsDisabled:M=!1,trackMetadata:w=[],volume:O=1}=e;this._volume=Math.min(1,Math.max(0,O)),this.webAudioIsDisabled=M,this._onProgress=i,this._onEnded=n,this._onPlayNextTrack=s,this._onPlayPreviousTrack=c,this._onStartNewTrack=l,this._onError=I,this._onPlayBlocked=R,this._onDebug=U,this._tracks=t.map((o,a)=>new g({trackUrl:o,index:a,queue:this,metadata:w[a]}));let G=D({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},deactivateEndedTrack:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},activateAndPlayCurrent:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(a.activate(),this._scheduledNextIndex!==a.index&&a.play())},playOrContinueGapless:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(this._scheduledNextIndex!==a.index?a.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${a.index} \u2014 sourceNode=${a.hasSourceNode} isPlaying=${a.isPlaying} machineState=${a.machineState}`),a.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onStartNewTrack)==null||u.call(this,a.toInfo()))},notifyPlayNextTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onPlayNextTrack)==null||u.call(this,a.toInfo()))},notifyPlayPreviousTrack:({context:o})=>{var u;let a=this._trackAt(o.currentTrackIndex);a&&((u=this._onPlayPreviousTrack)==null||u.call(this,a.toInfo()))},notifyEnded:()=>{var o;return(o=this._onEnded)==null?void 0:o.call(this)},updateMediaSessionMetadata:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&v(a.metadata)},preloadAhead:({context:o})=>{this._preloadAhead(o.currentTrackIndex)},playCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.play()},pauseCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.pause()},seekCurrent:({context:o,event:a})=>{var S;let u=a;(S=this._trackAt(o.currentTrackIndex))==null||S.seek(u.time)},seekCurrentToZero:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.seek(0)},scheduleGapless:({context:o})=>{this._tryScheduleGapless(o.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:o})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(o.currentTrackIndex)}}});this._actor=K(G),this._actor.subscribe(o=>{P(o.value==="playing")}),this._actor.start(),x({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:o=>this.seek(o)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let i of this._tracks)i.setVolume(t)}addTrack(e,t={}){let i=this._tracks.length,n=t.metadata??{};this._tracks.push(new g({trackUrl:e,index:i,queue:this,skipHEAD:t.skipHEAD,metadata:n})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e&&(this._scheduledNextIndex=null),this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return _()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e.index!==t.context.currentTrackIndex)return;this._actor.send({type:"TRACK_ENDED"});let i=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${i.value} curIdx=${i.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;(e=this._onPlayBlocked)==null||e.call(this)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=e+Y+1;this.onDebug(`_preloadAhead(${e}) limit=${t} trackCount=${this._tracks.length}`);for(let i=e+1;i<this._tracks.length&&i<t;i++){let n=this._tracks[i];if(n.isBufferLoaded)this.onDebug(`_preloadAhead: track ${i} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${i}`),n.preload();break}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.webAudioIsDisabled)return;let i=e+1;if(i>=this._tracks.length)return;let n=this._tracks[e],s=this._tracks[i];if(!n.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===i||!n.isPlaying)return;let c=this._computeTrackEndTime(n);c!==null&&(c<t.currentTime+.01||(s.scheduleGaplessStart(c),this._scheduledNextIndex=i))}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let i=e.duration;if(isNaN(i))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+i;let n=i-e.currentTime;return n<=0?null:t.currentTime+n}};export{f as Queue,f as default};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';\n\n/** Maximum number of tracks to preload ahead of the current track. */\nconst PRELOAD_AHEAD = 2;\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onQueueStateChange?: (snapshot: { state: string; context: { currentTrackIndex: number; trackCount: number } }) => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly webAudioIsDisabled: boolean;\n\n private _volume: number;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number) =>\n updateMediaSessionPositionState(duration, currentTime),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onQueueStateChange,\n onDebug,\n webAudioIsDisabled = false,\n trackMetadata = [],\n volume: initialVolume = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this.webAudioIsDisabled = webAudioIsDisabled;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onQueueStateChange = onQueueStateChange;\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 // Flush position immediately on state change to avoid stale OS scrubber\n const cur = this._trackAt(snapshot.context.currentTrackIndex);\n if (cur && !isNaN(cur.duration)) {\n updateMediaSessionPositionState(cur.duration, cur.currentTime);\n }\n this._onQueueStateChange?.({\n state: snapshot.value as string,\n context: snapshot.context,\n });\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n ct.seek(0);\n ct.play();\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n if (track.index !== snap.context.currentTrackIndex) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._onPlayBlocked?.();\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const limit = fromIndex + PRELOAD_AHEAD + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n break;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.webAudioIsDisabled) return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n next.scheduleGaplessStart(endTime);\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration;\n }\n\n const remaining = duration - track.currentTime;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n return context.currentTrackIndex;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToZero: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly webAudioIsDisabled: boolean;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _webAudioDisabled: boolean;\n\n private get ctx(): AudioContext | null {\n if (this._webAudioDisabled) return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime when the current source node was started. */\n private webAudioStartedAt = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._webAudioDisabled = opts.queue.webAudioIsDisabled;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchDecodeRef: null,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n this._resolvedUrl = res.url;\n this.audio.src = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }),\n },\n }),\n },\n actions: {\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n this.sourceNode.start(when, 0);\n this.webAudioStartedAt = when;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n this.pausedAtTrackTime = this.currentTime;\n },\n stopSourceNode: () => this._stopSourceNode(),\n disconnectGain: () => this._disconnectGain(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => this.queueRef.onProgress(this.toInfo()),\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this.webAudioStartedAt = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n this.audio.volume = vol;\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer || !this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this.ctx.currentTime - this.webAudioStartedAt);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n private _startSourceNode(offset: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this.webAudioStartedAt = this.ctx.currentTime - offset;\n this.sourceNode.start(0, offset);\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _disconnectGain(): void {\n if (!this.gainNode || !this.ctx) return;\n try {\n this.gainNode.disconnect(this.ctx.destination);\n } catch {\n /* already disconnected */\n }\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode may be in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins eventually\":\n// When a track's buffer finishes decoding (BUFFER_READY), webAudioLoadingState\n// is set to 'LOADED' regardless of what state the machine is in (html5, loading,\n// or idle). We intentionally do NOT switch mid-stream — the track stays in html5\n// until the next play(). But every state handles BUFFER_READY, so the flag is\n// never lost, and the next play() will see the buffer and use Web Audio.\n//\n// All DEACTIVATE transitions land in idle (not loading), so a deactivated track\n// with a loaded buffer is always in idle+LOADED — ready for Web Audio on re-play.\n//\n// Bug fixes in this rewrite:\n// #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)\n// #3: DEACTIVATE from webaudio → idle (was staying in webaudio)\n// #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, type AnyActorRef } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchDecodeRef: AnyActorRef | null;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && context.fetchDecodeRef === null,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n disconnectGain: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchDecodeRef: ({ context, spawn }) =>\n spawn('fetchDecode', {\n id: 'fetchDecode',\n input: {\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n },\n }),\n }),\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Bug #2 fix: BUFFER_READY in html5 stays in html5, only updates loading state.\n // The actual switchover to webaudio only happens via explicit PLAY_WEBAUDIO.\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n target: 'idle',\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n target: 'idle',\n actions: 'setErrorState',\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n PAUSE: {\n actions: [\n 'freezePausedTime',\n 'clearIsPlaying',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n // Note: xstate v5 internally calls assign() when initialising a child\n // actor with a context function, which produces a false-positive\n // \"Custom actions should not call assign()\" warning when this machine\n // is spawned from a parent. This is harmless and cannot be avoided.\n context: ({ input }) => ({\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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EAGA,aAAc,CACZ,QAAS,gBACX,EACA,aAAc,CACZ,QAAS,eACX,EACA,KAAM,CACJ,QAAS,CACP,YACA,gBACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,YAAa,CACX,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,aACA,mBACA,cACA,mBACF,CACF,EACA,aAAc,CACZ,QAAS,gBACX,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAc,oBAAqB,cAAe,kBAAkB,CAClG,CACF,CACF,EAKA,QAAS,CACP,GAAI,CACF,eAAgB,CACd,QAAS,iBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,gBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,eACX,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,eAAgB,YAAa,mBAAmB,CAC5D,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,cACA,mBACF,CACF,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAa,CAC3C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,SAAU,CACR,GAAI,CACF,MAAO,CACL,QAAS,CACP,mBACA,iBACA,iBACA,iBACA,mBACA,gBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CAAC,eAAgB,kBAAmB,mBAAmB,CAClE,EACA,CACE,QAAS,cACX,CACF,EACA,cAAe,CACb,QAAS,wBACX,EACA,KAAM,CACJ,QAAS,CACP,sBACA,eACA,gBACF,CACF,EACA,WAAY,CAAC,EACb,eAAgB,CACd,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,mBACA,aACF,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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","next","createActor","fromPromise","setup","assign","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","input","event","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawn","GAPLESS_SCHEDULE_LOOKAHEAD","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","when","createActor","time","clamped","v","vol","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","throttle","fn","ms","last","args","now","PRELOAD_AHEAD","Queue","throttle","duration","currentTime","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onQueueStateChange","onDebug","webAudioIsDisabled","trackMetadata","initialVolume","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","metadata","resumeAudioContext","snap","newSnap","info","error","msg","fromIndex","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","remaining"]}
|
|
1
|
+
{"version":3,"sources":["../src/Queue.ts","../src/utils/audioContext.ts","../src/utils/mediaSession.ts","../src/machines/queue.machine.ts","../src/Track.ts","../src/machines/track.machine.ts","../src/machines/fetchDecode.machine.ts","../src/utils/throttle.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Queue — public API class; orchestrates tracks via QueueMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor } from 'xstate';\nimport { getAudioContext, resumeAudioContext as _resumeAudioContext } from './utils/audioContext';\nimport {\n setupMediaSession,\n updateMediaSessionMetadata,\n updateMediaSessionPlaybackState,\n updateMediaSessionPositionState,\n} from './utils/mediaSession';\nimport { createQueueMachine } from './machines/queue.machine';\nimport { Track } from './Track';\nimport type { TrackQueueRef } from './Track';\nimport { throttle } from './utils/throttle';\nimport type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';\n\n/** Maximum number of tracks to preload ahead of the current track. */\nconst PRELOAD_AHEAD = 2;\n\nexport class Queue implements TrackQueueRef {\n private _tracks: Track[] = [];\n private readonly _actor;\n\n private readonly _onProgress?: (info: TrackInfo) => void;\n private readonly _onEnded?: () => void;\n private readonly _onPlayNextTrack?: (info: TrackInfo) => void;\n private readonly _onPlayPreviousTrack?: (info: TrackInfo) => void;\n private readonly _onStartNewTrack?: (info: TrackInfo) => void;\n private readonly _onError?: (error: Error) => void;\n private readonly _onPlayBlocked?: () => void;\n private readonly _onDebug?: (msg: string) => void;\n\n readonly webAudioIsDisabled: boolean;\n\n private _volume: number;\n\n /** Index of the next track with a pre-scheduled gapless start, or null. */\n private _scheduledNextIndex: number | null = null;\n\n private _throttledUpdatePositionState = throttle(\n (duration: number, currentTime: number) =>\n updateMediaSessionPositionState(duration, currentTime),\n 1000,\n );\n\n constructor(options: GaplessOptions = {}) {\n const {\n tracks = [],\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n onError,\n onPlayBlocked,\n onDebug,\n webAudioIsDisabled = false,\n trackMetadata = [],\n volume: initialVolume = 1,\n } = options;\n\n this._volume = Math.min(1, Math.max(0, initialVolume));\n this.webAudioIsDisabled = webAudioIsDisabled;\n this._onProgress = onProgress;\n this._onEnded = onEnded;\n this._onPlayNextTrack = onPlayNextTrack;\n this._onPlayPreviousTrack = onPlayPreviousTrack;\n this._onStartNewTrack = onStartNewTrack;\n this._onError = onError;\n this._onPlayBlocked = onPlayBlocked;\n this._onDebug = onDebug;\n\n this._tracks = tracks.map(\n (url, i) =>\n new Track({\n trackUrl: url,\n index: i,\n queue: this,\n metadata: trackMetadata[i],\n })\n );\n\n // -----------------------------------------------------------------------\n // Wire up the queue machine with real action implementations.\n //\n // IMPORTANT: actions receive ({ context }) which reflects the in-progress\n // context (updated by prior assign() calls within the same transition).\n // Do NOT use this._actor.getSnapshot() inside actions — that returns the\n // pre-transition snapshot and won't reflect intermediate assign() updates.\n // -----------------------------------------------------------------------\n const machine = createQueueMachine({\n currentTrackIndex: 0,\n trackCount: this._tracks.length\n }).provide({\n actions: {\n deactivateCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n deactivateEndedTrack: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.deactivate();\n },\n activateAndPlayCurrent: ({ context }) => {\n const track = this._trackAt(context.currentTrackIndex);\n if (!track) return;\n track.activate();\n if (this._scheduledNextIndex !== track.index) {\n track.play();\n }\n },\n playOrContinueGapless: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (!cur) return;\n if (this._scheduledNextIndex !== cur.index) {\n cur.play();\n } else {\n this._scheduledNextIndex = null;\n this.onDebug(\n `onTrackEnded: gapless track ${cur.index} — sourceNode=${cur.hasSourceNode} isPlaying=${cur.isPlaying} machineState=${cur.machineState}`\n );\n cur.startProgressLoop();\n }\n },\n cancelAllGapless: () => this._cancelScheduledGapless(),\n notifyStartNewTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onStartNewTrack?.(cur.toInfo());\n },\n notifyPlayNextTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayNextTrack?.(cur.toInfo());\n },\n notifyPlayPreviousTrack: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) this._onPlayPreviousTrack?.(cur.toInfo());\n },\n notifyEnded: () => this._onEnded?.(),\n updateMediaSessionMetadata: ({ context }) => {\n const cur = this._trackAt(context.currentTrackIndex);\n if (cur) updateMediaSessionMetadata(cur.metadata);\n },\n preloadAhead: ({ context }) => {\n this._preloadAhead(context.currentTrackIndex);\n },\n playCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.play();\n },\n pauseCurrent: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.pause();\n },\n seekCurrent: ({ context, event }) => {\n const e = event as { type: 'SEEK'; time: number };\n this._trackAt(context.currentTrackIndex)?.seek(e.time);\n },\n seekCurrentToZero: ({ context }) => {\n this._trackAt(context.currentTrackIndex)?.seek(0);\n },\n scheduleGapless: ({ context }) => {\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n cancelScheduledGapless: () => {\n this._cancelScheduledGapless();\n },\n cancelAndRescheduleGapless: ({ context }) => {\n this._cancelScheduledGapless();\n this._tryScheduleGapless(context.currentTrackIndex);\n },\n },\n });\n\n this._actor = createActor(machine);\n\n this._actor.subscribe((snapshot) => {\n updateMediaSessionPlaybackState(snapshot.value === 'playing');\n });\n\n this._actor.start();\n\n setupMediaSession({\n onPlay: () => this.play(),\n onPause: () => {\n if (this._actor.getSnapshot().value === 'playing') this.pause();\n },\n onNext: () => this.next(),\n onPrevious: () => this.previous(),\n onSeek: (t) => this.seek(t),\n });\n }\n\n // --------------------------------------------------------------------------\n // Public API\n // --------------------------------------------------------------------------\n\n play(): void {\n if (!this._currentTrack) return;\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n togglePlayPause(): void {\n if (this._actor.getSnapshot().value === 'playing') {\n this.pause();\n } else {\n this.play();\n }\n }\n\n next(): void {\n const snap = this._actor.getSnapshot();\n const nextIndex = snap.context.currentTrackIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n this._actor.send({ type: 'NEXT' });\n }\n\n previous(): void {\n const ct = this._currentTrack;\n if (ct && ct.currentTime > 8) {\n ct.seek(0);\n ct.play();\n return;\n }\n\n this._actor.send({ type: 'PREVIOUS' });\n }\n\n gotoTrack(index: number, playImmediately = false): void {\n if (index < 0 || index >= this._tracks.length) return;\n this.onDebug(\n `gotoTrack(${index}, playImmediately=${playImmediately}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`\n );\n this._actor.send({ type: 'GOTO', index, playImmediately });\n }\n\n seek(time: number): void {\n this._actor.send({ type: 'SEEK', time });\n }\n\n setVolume(volume: number): void {\n const clamped = Math.min(1, Math.max(0, volume));\n this._volume = clamped;\n for (const track of this._tracks) track.setVolume(clamped);\n }\n\n addTrack(url: string, options: AddTrackOptions = {}): void {\n const index = this._tracks.length;\n const metadata = options.metadata ?? ({} as TrackMetadata);\n this._tracks.push(\n new Track({\n trackUrl: url,\n index,\n queue: this,\n skipHEAD: options.skipHEAD,\n metadata,\n })\n );\n this._actor.send({ type: 'ADD_TRACK' });\n }\n\n removeTrack(index: number): void {\n if (index < 0 || index >= this._tracks.length) return;\n this._tracks[index].destroy();\n this._tracks.splice(index, 1);\n for (let i = index; i < this._tracks.length; i++) {\n (this._tracks[i] as unknown as { index: number }).index = i;\n }\n if (this._scheduledNextIndex === index) {\n this._scheduledNextIndex = null;\n }\n this._actor.send({ type: 'REMOVE_TRACK', index });\n }\n\n resumeAudioContext(): Promise<void> {\n return _resumeAudioContext();\n }\n\n destroy(): void {\n for (const track of this._tracks) track.destroy();\n this._tracks = [];\n this._actor.stop();\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTrack(): TrackInfo | undefined {\n return this._currentTrack?.toInfo();\n }\n\n get currentTrackIndex(): number {\n return this._actor.getSnapshot().context.currentTrackIndex;\n }\n\n get tracks(): readonly TrackInfo[] {\n return this._tracks.map((t) => t.toInfo());\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().value === 'playing';\n }\n\n get isPaused(): boolean {\n return this._actor.getSnapshot().value === 'paused';\n }\n\n get volume(): number {\n return this._volume;\n }\n\n /** Snapshot of the queue state machine (state name + context). For debugging. */\n get queueSnapshot(): { state: string; context: { currentTrackIndex: number; trackCount: number } } {\n const snap = this._actor.getSnapshot();\n return { state: snap.value as string, context: snap.context };\n }\n\n // --------------------------------------------------------------------------\n // TrackQueueRef — called by Track instances\n // --------------------------------------------------------------------------\n\n onTrackEnded(track: Track): void {\n const snap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded track=${track.index} queueState=${snap.value} curIdx=${snap.context.currentTrackIndex}`\n );\n if (track.index !== snap.context.currentTrackIndex) return;\n\n this._actor.send({ type: 'TRACK_ENDED' });\n const newSnap = this._actor.getSnapshot();\n this.onDebug(\n `onTrackEnded after TRACK_ENDED → queueState=${newSnap.value} curIdx=${newSnap.context.currentTrackIndex}`\n );\n }\n\n onTrackBufferReady(track: Track): void {\n this._actor.send({ type: 'TRACK_LOADED', index: track.index });\n }\n\n onProgress(info: TrackInfo): void {\n if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;\n if (!isNaN(info.duration)) {\n this._throttledUpdatePositionState(info.duration, info.currentTime);\n }\n this._onProgress?.(info);\n }\n\n onError(error: Error): void {\n this._onError?.(error);\n }\n\n onPlayBlocked(): void {\n this._onPlayBlocked?.();\n }\n\n onDebug(msg: string): void {\n this._onDebug?.(msg);\n }\n\n // --------------------------------------------------------------------------\n // Private helpers\n // --------------------------------------------------------------------------\n\n /** Look up a track by index — safe for use inside machine actions. */\n private _trackAt(index: number): Track | undefined {\n return this._tracks[index];\n }\n\n private get _currentTrack(): Track | undefined {\n return this._tracks[this._actor.getSnapshot().context.currentTrackIndex];\n }\n\n private _preloadAhead(fromIndex: number): void {\n const limit = fromIndex + PRELOAD_AHEAD + 1;\n this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);\n for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {\n const t = this._tracks[i];\n if (!t.isBufferLoaded) {\n this.onDebug(`_preloadAhead: starting preload for track ${i}`);\n t.preload();\n break;\n } else {\n this.onDebug(`_preloadAhead: track ${i} already loaded`);\n }\n }\n }\n\n private _cancelScheduledGapless(): void {\n if (this._scheduledNextIndex === null) return;\n const track = this._trackAt(this._scheduledNextIndex);\n if (track) {\n track.cancelGaplessStart();\n this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`);\n }\n this._scheduledNextIndex = null;\n }\n\n private _tryScheduleGapless(curIndex: number): void {\n const ctx = getAudioContext();\n if (!ctx || this.webAudioIsDisabled) return;\n\n const nextIndex = curIndex + 1;\n if (nextIndex >= this._tracks.length) return;\n\n const current = this._tracks[curIndex];\n const next = this._tracks[nextIndex];\n\n if (\n !current.isBufferLoaded ||\n !next.isBufferLoaded ||\n this._scheduledNextIndex === nextIndex ||\n !current.isPlaying\n )\n return;\n\n const endTime = this._computeTrackEndTime(current);\n if (endTime === null) return;\n\n if (endTime < ctx.currentTime + 0.01) return;\n\n next.scheduleGaplessStart(endTime);\n this._scheduledNextIndex = nextIndex;\n }\n\n private _computeTrackEndTime(track: Track): number | null {\n const ctx = getAudioContext();\n if (!ctx || !track.isBufferLoaded) return null;\n const duration = track.duration;\n if (isNaN(duration)) return null;\n\n if (track.scheduledStartContextTime !== null) {\n return track.scheduledStartContextTime + duration;\n }\n\n const remaining = duration - track.currentTime;\n if (remaining <= 0) return null;\n return ctx.currentTime + remaining;\n }\n}\n","// ---------------------------------------------------------------------------\n// Singleton AudioContext manager\n//\n// A single AudioContext is shared across all Track instances. This is\n// essential: all AudioBufferSourceNodes must share the same context so that\n// audioContext.currentTime is a common monotonic clock, enabling\n// sample-accurate scheduling of back-to-back tracks.\n// ---------------------------------------------------------------------------\n\nconst isBrowser = typeof window !== 'undefined';\n\nlet _audioContext: AudioContext | null | undefined = undefined;\n\nfunction createAudioContext(): AudioContext | null {\n if (!isBrowser) return null;\n const Ctor =\n (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) return null;\n return new Ctor();\n}\n\n/**\n * Return the shared AudioContext if it has already been created, otherwise null.\n *\n * Does NOT create the context — call resumeAudioContext() from a user-gesture\n * handler to create and resume it. This prevents the browser warning:\n * \"An AudioContext was prevented from starting automatically.\"\n */\nexport function getAudioContext(): AudioContext | null {\n // undefined = never initialised (return null without creating)\n // null = explicitly disabled or unavailable\n // AudioContext = ready to use\n if (_audioContext === undefined) return null;\n return _audioContext;\n}\n\n/**\n * Create (if needed) and resume the AudioContext.\n * Must be called from a user-gesture handler (click, keydown, etc.).\n * Safe to call multiple times — subsequent calls are cheap no-ops.\n */\nexport function resumeAudioContext(): Promise<void> {\n if (_audioContext === undefined) {\n _audioContext = createAudioContext();\n }\n const ctx = _audioContext;\n if (ctx && ctx.state === 'suspended') {\n return ctx.resume();\n }\n return Promise.resolve();\n}\n\n/**\n * Reset the cached AudioContext — used in tests so each test gets a fresh mock.\n * @internal\n */\nexport function _resetAudioContext(): void {\n _audioContext = undefined;\n}\n\n/**\n * Directly set the cached AudioContext — used in tests to inject a mock instance.\n * @internal\n */\nexport function _setAudioContext(ctx: AudioContext | null): void {\n _audioContext = ctx;\n}\n","// ---------------------------------------------------------------------------\n// Media Session API integration\n//\n// Provides OS-level media controls (lock screen, notification shade, keyboard\n// media keys). Gracefully no-ops when the API is unavailable.\n// ---------------------------------------------------------------------------\n\nimport type { TrackMetadata } from '../types';\n\nconst hasMediaSession =\n typeof navigator !== 'undefined' && 'mediaSession' in navigator;\n\nexport interface MediaSessionHandlers {\n onPlay: () => void;\n onPause: () => void;\n onNext: () => void;\n onPrevious: () => void;\n onSeek: (time: number) => void;\n}\n\n/** Register Media Session action handlers. Call once after construction. */\nexport function setupMediaSession(handlers: MediaSessionHandlers): void {\n if (!hasMediaSession) return;\n const { mediaSession } = navigator;\n mediaSession.setActionHandler('play', handlers.onPlay);\n mediaSession.setActionHandler('pause', handlers.onPause);\n mediaSession.setActionHandler('nexttrack', handlers.onNext);\n mediaSession.setActionHandler('previoustrack', handlers.onPrevious);\n mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime != null) handlers.onSeek(details.seekTime);\n });\n}\n\n/** Update the OS metadata display (title, artist, album, artwork). */\nexport function updateMediaSessionMetadata(metadata?: TrackMetadata): void {\n if (!hasMediaSession) return;\n if (!metadata) {\n navigator.mediaSession.metadata = null;\n return;\n }\n navigator.mediaSession.metadata = new MediaMetadata({\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n artwork: metadata.artwork ?? [],\n });\n}\n\n/** Sync the OS playback state indicator (playing / paused). */\nexport function updateMediaSessionPlaybackState(\n isPlaying: boolean,\n): void {\n if (!hasMediaSession) return;\n navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n}\n\n/** Update the OS position/duration display (lock screen scrubber). */\nexport function updateMediaSessionPositionState(\n duration: number,\n position: number,\n playbackRate = 1,\n): void {\n if (!hasMediaSession) return;\n try {\n navigator.mediaSession.setPositionState({ duration, position, playbackRate });\n } catch {\n // Some browsers throw on invalid values (e.g. NaN duration)\n }\n}\n","// ---------------------------------------------------------------------------\n// QueueMachine — top-level queue state (xstate v5)\n//\n// States:\n// idle No tracks, or not yet started.\n// playing A track is actively playing.\n// paused Explicitly paused by the user.\n// ended The last track in the queue has finished.\n//\n// Root-level `on:` eliminates the handler duplication that plagued v2.\n//\n// Named actions (no-op defaults here, real implementations via .provide()):\n// Actions before assign() see the OLD context (e.g. deactivateCurrent\n// sees the old currentTrackIndex). Actions after assign() see the NEW\n// context (e.g. activateAndPlayCurrent sees the incremented index).\n// Implementations MUST use the ({ context }) parameter, NOT getSnapshot().\n// ---------------------------------------------------------------------------\n\nimport { setup, assign } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface QueueContext {\n currentTrackIndex: number;\n trackCount: number;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type QueueEvent =\n | { type: 'PLAY' }\n | { type: 'PAUSE' }\n | { type: 'TOGGLE' }\n | { type: 'NEXT' }\n | { type: 'PREVIOUS' }\n | { type: 'GOTO'; index: number; playImmediately?: boolean }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'ADD_TRACK' }\n | { type: 'REMOVE_TRACK'; index: number }\n | { type: 'TRACK_ENDED' }\n | { type: 'TRACK_LOADED'; index: number };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createQueueMachine(initialContext: QueueContext) {\n return setup({\n types: {\n context: {} as QueueContext,\n events: {} as QueueEvent,\n },\n guards: {\n hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,\n playImmediately: ({ event }) =>\n !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,\n },\n actions: {\n // --- named assign actions (avoids mixing inline assign with custom actions) ---\n incrementTrackCount: assign({\n trackCount: ({ context }) => context.trackCount + 1,\n }),\n decrementTrackCount: assign({\n trackCount: ({ context }) => Math.max(0, context.trackCount - 1),\n currentTrackIndex: ({ context, event }) => {\n const e = event as { type: 'REMOVE_TRACK'; index: number };\n if (e.index < context.currentTrackIndex) {\n return Math.max(0, context.currentTrackIndex - 1);\n }\n return context.currentTrackIndex;\n },\n }),\n gotoTrackIndex: assign({\n currentTrackIndex: ({ event }) =>\n (event as { type: 'GOTO'; index: number }).index,\n }),\n advanceToNextTrack: assign({\n currentTrackIndex: ({ context }) => {\n const next = context.currentTrackIndex + 1;\n return next < context.trackCount ? next : context.currentTrackIndex;\n },\n }),\n goToPreviousTrack: assign({\n currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),\n }),\n advanceOnTrackEnd: assign({\n currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,\n }),\n resetToFirstTrack: assign({\n currentTrackIndex: () => 0,\n }),\n // --- side-effect actions (no-op defaults, provided by Queue.ts) ---\n deactivateCurrent: () => {},\n deactivateEndedTrack: () => {},\n activateAndPlayCurrent: () => {},\n playOrContinueGapless: () => {},\n cancelAllGapless: () => {},\n notifyStartNewTrack: () => {},\n notifyPlayNextTrack: () => {},\n notifyPlayPreviousTrack: () => {},\n notifyEnded: () => {},\n updateMediaSessionMetadata: () => {},\n preloadAhead: () => {},\n playCurrent: () => {},\n pauseCurrent: () => {},\n seekCurrent: () => {},\n seekCurrentToZero: () => {},\n scheduleGapless: () => {},\n cancelScheduledGapless: () => {},\n cancelAndRescheduleGapless: () => {},\n },\n }).createMachine({\n id: 'queue',\n initial: 'idle',\n context: initialContext,\n\n // Global handlers — shared across all states\n on: {\n ADD_TRACK: {\n actions: 'incrementTrackCount',\n },\n REMOVE_TRACK: {\n actions: 'decrementTrackCount',\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle\n // -----------------------------------------------------------------\n idle: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // playing\n // -----------------------------------------------------------------\n playing: {\n on: {\n PAUSE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n TOGGLE: {\n target: 'paused',\n actions: ['cancelScheduledGapless', 'pauseCurrent'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent', 'cancelAndRescheduleGapless'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'playing',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'playOrContinueGapless',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['scheduleGapless', 'preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // paused\n // -----------------------------------------------------------------\n paused: {\n on: {\n PLAY: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n TOGGLE: {\n target: 'playing',\n actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],\n },\n NEXT: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'advanceToNextTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayNextTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n PREVIOUS: {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'goToPreviousTrack',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'notifyPlayPreviousTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n SEEK: {\n actions: ['seekCurrent'],\n },\n TRACK_ENDED: [\n {\n guard: 'hasNextTrack',\n target: 'paused',\n actions: [\n 'deactivateEndedTrack',\n 'advanceOnTrackEnd',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n ],\n },\n {\n target: 'ended',\n actions: ['deactivateEndedTrack', 'notifyEnded'],\n },\n ],\n TRACK_LOADED: {\n actions: ['preloadAhead'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // ended\n // -----------------------------------------------------------------\n ended: {\n on: {\n PLAY: {\n target: 'playing',\n actions: [\n 'resetToFirstTrack',\n 'playCurrent',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n 'scheduleGapless',\n ],\n },\n GOTO: [\n {\n guard: 'playImmediately',\n target: 'playing',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'activateAndPlayCurrent',\n 'notifyStartNewTrack',\n 'updateMediaSessionMetadata',\n 'preloadAhead',\n ],\n },\n {\n target: 'paused',\n actions: [\n 'deactivateCurrent',\n 'cancelAllGapless',\n 'gotoTrackIndex',\n 'seekCurrentToZero',\n 'preloadAhead',\n ],\n },\n ],\n },\n },\n },\n });\n}\n\nexport type QueueMachine = ReturnType<typeof createQueueMachine>;\n","// ---------------------------------------------------------------------------\n// Track — owns one audio track's Web Audio nodes and drives TrackMachine\n// ---------------------------------------------------------------------------\n\nimport { createActor, fromPromise } from 'xstate';\nimport { getAudioContext } from './utils/audioContext';\nimport { createTrackMachine } from './machines/track.machine';\nimport { fetchDecodeMachine } from './machines/fetchDecode.machine';\nimport type { TrackContext } from './machines/track.machine';\nimport type { TrackInfo, TrackMetadata, WebAudioLoadingState, PlaybackType } from './types';\n\nexport interface TrackQueueRef {\n onTrackEnded(track: Track): void;\n onTrackBufferReady(track: Track): void;\n onProgress(info: TrackInfo): void;\n onError(error: Error): void;\n onPlayBlocked(): void;\n onDebug(msg: string): void;\n readonly volume: number;\n readonly webAudioIsDisabled: boolean;\n readonly currentTrackIndex: number;\n}\n\n/** How close to the end (in seconds) before we attempt gapless scheduling. */\nconst GAPLESS_SCHEDULE_LOOKAHEAD = 5;\n\nexport class Track {\n readonly index: number;\n readonly metadata: TrackMetadata;\n\n private _trackUrl: string;\n private _resolvedUrl: string;\n private readonly skipHEAD: boolean;\n /** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */\n private _pendingArrayBuffer: ArrayBuffer | null = null;\n\n // ---- HTML5 Audio ---------------------------------------------------------\n readonly audio: HTMLAudioElement;\n\n // ---- Web Audio nodes -----------------------------------------------------\n private readonly _webAudioDisabled: boolean;\n\n private get ctx(): AudioContext | null {\n if (this._webAudioDisabled) return null;\n const context = getAudioContext();\n if (context && !this.gainNode) {\n this.gainNode = context.createGain();\n this.gainNode.gain.value = this.audio.volume;\n }\n return context;\n }\n\n private gainNode: GainNode | null = null;\n private sourceNode: AudioBufferSourceNode | null = null;\n audioBuffer: AudioBuffer | null = null;\n /** AudioContext.currentTime when the current source node was started. */\n private webAudioStartedAt = 0;\n /** Track-time (seconds) frozen at the moment of the most recent pause. */\n private pausedAtTrackTime = 0;\n // ---- FSM -----------------------------------------------------------------\n private readonly _actor;\n\n // ---- Callbacks -----------------------------------------------------------\n private readonly queueRef: TrackQueueRef;\n private rafId: number | null = null;\n\n constructor(opts: {\n trackUrl: string;\n index: number;\n queue: TrackQueueRef;\n skipHEAD?: boolean;\n metadata?: TrackMetadata;\n }) {\n this.index = opts.index;\n this._trackUrl = opts.trackUrl;\n this._resolvedUrl = opts.trackUrl;\n this.skipHEAD = opts.skipHEAD ?? false;\n this.metadata = opts.metadata ?? {};\n this.queueRef = opts.queue;\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.preload = 'none';\n this.audio.src = this._trackUrl;\n this.audio.volume = opts.queue.volume;\n this.audio.controls = false;\n this.audio.onerror = () => {\n const code = this.audio.error?.code;\n if (code === 1) return;\n const msg = this.audio.error?.message ?? 'unknown';\n this.queueRef.onError(\n new Error(`HTML5 audio error on track ${this.index} (code ${code}): ${msg}`)\n );\n };\n this.audio.onended = () => {\n this.queueRef.onDebug(\n `audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n this._actor.send({ type: 'HTML5_ENDED' });\n };\n\n this._webAudioDisabled = opts.queue.webAudioIsDisabled;\n\n const initialContext: TrackContext = {\n trackUrl: this._trackUrl,\n resolvedUrl: this._trackUrl,\n skipHEAD: this.skipHEAD,\n playbackType: 'HTML5',\n webAudioLoadingState: 'NONE',\n isPlaying: false,\n scheduledStartContextTime: null,\n notifiedLookahead: false,\n fetchDecodeRef: null,\n };\n const machine = createTrackMachine(initialContext).provide({\n guards: {\n canPlayWebAudio: () => !!(this.ctx && this.audioBuffer && this.gainNode),\n },\n actors: {\n fetchDecode: fetchDecodeMachine.provide({\n actors: {\n resolveUrl: fromPromise(async ({ signal }) => {\n const res = await fetch(this._trackUrl, { method: 'HEAD', signal });\n if (res.redirected && res.url) {\n this._resolvedUrl = res.url;\n this.audio.src = res.url;\n return res.url;\n }\n return null;\n }),\n fetchAudio: fromPromise(async ({ input, signal }) => {\n const { resolvedUrl } = input as { resolvedUrl: string };\n const res = await fetch(resolvedUrl, { signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} for ${resolvedUrl}`);\n this._pendingArrayBuffer = await res.arrayBuffer();\n }),\n decodeAudio: fromPromise(async () => {\n const buf = this._pendingArrayBuffer;\n this._pendingArrayBuffer = null;\n if (!buf || !this.ctx) throw new Error('No ArrayBuffer or AudioContext');\n this.audioBuffer = await this.ctx.decodeAudioData(buf);\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }),\n },\n }),\n },\n actions: {\n playHtml5: () => this._playHtml5(),\n startSourceNode: () => {\n this._startSourceNode(this.pausedAtTrackTime);\n },\n startScheduledSourceNode: ({ context }: { context: TrackContext }) => {\n const when = context.scheduledStartContextTime;\n if (when === null || !this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n this.sourceNode.start(when, 0);\n this.webAudioStartedAt = when;\n this.queueRef.onDebug(\n `startScheduledSourceNode track=${this.index} when=${when.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(when - this.ctx.currentTime).toFixed(3)}s`\n );\n },\n startProgressLoop: () => this.startProgressLoop(),\n pauseHtml5: () => this.audio.pause(),\n freezePausedTime: () => {\n this.pausedAtTrackTime = this.currentTime;\n },\n stopSourceNode: () => this._stopSourceNode(),\n disconnectGain: () => this._disconnectGain(),\n stopProgressLoop: () => this._stopProgressLoop(),\n reportProgress: () => {\n queueMicrotask(() => this.queueRef.onProgress(this.toInfo()));\n },\n seekHtml5: () => this._seekHtml5(),\n seekWebAudio: () => this._seekWebAudio(),\n resetHtml5Element: () => {\n this.audio.currentTime = 0;\n },\n resetTiming: () => {\n this.webAudioStartedAt = 0;\n this.pausedAtTrackTime = 0;\n },\n notifyTrackEnded: () => {\n queueMicrotask(() => this.queueRef.onTrackEnded(this));\n },\n },\n });\n this._actor = createActor(machine);\n this._actor.start();\n }\n\n // --------------------------------------------------------------------------\n // Public playback controls\n // --------------------------------------------------------------------------\n\n play(): void {\n this.queueRef.onDebug(\n `Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`\n );\n this._actor.send({ type: 'PLAY' });\n }\n\n pause(): void {\n this._actor.send({ type: 'PAUSE' });\n }\n\n seek(time: number): void {\n const clamped = Math.max(0, isNaN(this.duration) ? time : Math.min(time, this.duration));\n this.pausedAtTrackTime = clamped;\n this._actor.send({ type: 'SEEK', time: clamped });\n }\n\n setVolume(v: number): void {\n const vol = Math.min(1, Math.max(0, v));\n this.audio.volume = vol;\n if (this.gainNode) this.gainNode.gain.value = vol;\n this._actor.send({ type: 'SET_VOLUME', volume: vol });\n }\n\n preload(): void {\n this.queueRef.onDebug(\n `preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`\n );\n if (this._actor.getSnapshot().value === 'idle') {\n this._actor.send({ type: 'PRELOAD' });\n }\n if (this.audioBuffer || !this.ctx) return;\n this._actor.send({ type: 'START_FETCH' });\n }\n\n seekToEnd(secondsFromEnd = 6): void {\n const dur = this.duration;\n if (!isNaN(dur) && dur > secondsFromEnd) {\n this.seek(dur - secondsFromEnd);\n }\n }\n\n activate(): void {\n this._actor.send({ type: 'ACTIVATE' });\n }\n\n deactivate(): void {\n this.queueRef.onDebug(\n `Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`\n );\n this._actor.send({ type: 'DEACTIVATE' });\n this.queueRef.onDebug(\n `Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`\n );\n }\n\n destroy(): void {\n this.deactivate();\n this._pendingArrayBuffer = null;\n this.audioBuffer = null;\n this.gainNode?.disconnect();\n this.gainNode = null;\n this._actor.stop(); // Stops spawned fetchDecode child actor, aborting in-flight fetches\n }\n\n // --------------------------------------------------------------------------\n // Gapless scheduling (called by Queue)\n // --------------------------------------------------------------------------\n\n cancelGaplessStart(): void {\n const snap = this._actor.getSnapshot();\n if (snap.context.scheduledStartContextTime === null) return;\n this._actor.send({ type: 'CANCEL_GAPLESS' });\n }\n\n scheduleGaplessStart(when: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._actor.send({ type: 'SCHEDULE_GAPLESS', when });\n }\n\n // --------------------------------------------------------------------------\n // Getters\n // --------------------------------------------------------------------------\n\n get currentTime(): number {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') {\n if (!snap.context.isPlaying) return this.pausedAtTrackTime;\n if (!this.ctx) return 0;\n return Math.max(0, this.ctx.currentTime - this.webAudioStartedAt);\n }\n return this.audio.currentTime;\n }\n\n get duration(): number {\n if (this.audioBuffer) return this.audioBuffer.duration;\n return this.audio.duration;\n }\n\n get isPaused(): boolean {\n const snap = this._actor.getSnapshot();\n if (snap.value === 'webaudio') return !snap.context.isPlaying;\n return this.audio.paused;\n }\n\n get isPlaying(): boolean {\n return this._actor.getSnapshot().context.isPlaying;\n }\n\n get trackUrl(): string {\n return this._resolvedUrl;\n }\n\n get playbackType(): PlaybackType {\n return this._actor.getSnapshot().context.playbackType;\n }\n\n get webAudioLoadingState(): WebAudioLoadingState {\n return this._actor.getSnapshot().context.webAudioLoadingState;\n }\n\n get hasSourceNode(): boolean {\n return this.sourceNode !== null;\n }\n\n get machineState(): string {\n return this._actor.getSnapshot().value as string;\n }\n\n get scheduledStartContextTime(): number | null {\n return this._actor.getSnapshot().context.scheduledStartContextTime;\n }\n\n get isBufferLoaded(): boolean {\n return this.audioBuffer !== null;\n }\n\n toInfo(): TrackInfo {\n return {\n index: this.index,\n currentTime: this.currentTime,\n duration: this.duration,\n isPlaying: this.isPlaying,\n isPaused: this.isPaused,\n volume: this.gainNode?.gain.value ?? this.audio.volume,\n trackUrl: this.trackUrl,\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n metadata: this.metadata,\n machineState: this.machineState,\n };\n }\n\n // --------------------------------------------------------------------------\n // Private: HTML5 helpers\n // --------------------------------------------------------------------------\n\n private _playHtml5(): void {\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n const promise = this.audio.play();\n if (promise) {\n promise.catch((err: unknown) => {\n if (err instanceof Error && err.name === 'NotAllowedError') {\n this.queueRef.onPlayBlocked();\n } else if (err instanceof Error && err.name === 'AbortError') {\n // Browser aborted — element will recover on next play()\n } else {\n this.queueRef.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n }\n\n private _seekHtml5(): void {\n const clamped = this.pausedAtTrackTime;\n if (this.audio.preload !== 'auto') this.audio.preload = 'auto';\n if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {\n this.audio.currentTime = clamped;\n } else {\n this.audio.addEventListener(\n 'loadedmetadata',\n () => {\n this.audio.currentTime = clamped;\n },\n { once: true }\n );\n this.audio.load();\n }\n }\n\n // --------------------------------------------------------------------------\n // Private: Web Audio helpers\n // --------------------------------------------------------------------------\n\n private _startSourceNode(offset: number): void {\n if (!this.ctx || !this.audioBuffer || !this.gainNode) return;\n this._stopSourceNode();\n\n // Ensure the AudioContext is running (it may have been suspended after\n // the previous track's source was disconnected).\n if (this.ctx.state === 'suspended') {\n this.ctx.resume();\n }\n\n this.sourceNode = this.ctx.createBufferSource();\n this.sourceNode.buffer = this.audioBuffer;\n this.sourceNode.connect(this.gainNode);\n this.gainNode.connect(this.ctx.destination);\n this.sourceNode.onended = this._handleWebAudioEnded;\n\n this.webAudioStartedAt = this.ctx.currentTime - offset;\n this.sourceNode.start(0, offset);\n }\n\n private _stopSourceNode(): void {\n if (!this.sourceNode) return;\n this.sourceNode.onended = null;\n try {\n this.sourceNode.stop();\n } catch {\n /* already stopped */\n }\n try {\n this.sourceNode.disconnect();\n } catch {\n /* already disconnected */\n }\n this.sourceNode = null;\n }\n\n private _disconnectGain(): void {\n if (!this.gainNode || !this.ctx) return;\n try {\n this.gainNode.disconnect(this.ctx.destination);\n } catch {\n /* already disconnected */\n }\n }\n\n private _seekWebAudio(): void {\n const snap = this._actor.getSnapshot();\n const wasPlaying = snap.context.isPlaying;\n const clamped = this.pausedAtTrackTime;\n this._stopSourceNode();\n if (wasPlaying) {\n this._startSourceNode(clamped);\n }\n }\n\n private _handleWebAudioEnded = (): void => {\n this.queueRef.onDebug(\n `_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`\n );\n if (!this.sourceNode) return;\n this._actor.send({ type: 'WEBAUDIO_ENDED' });\n };\n\n // --------------------------------------------------------------------------\n // Private: progress loop (requestAnimationFrame)\n // --------------------------------------------------------------------------\n\n startProgressLoop(): void {\n if (this.rafId !== null) return;\n const loop = () => {\n if (this.isPaused || !this.isPlaying) {\n this.rafId = null;\n return;\n }\n this.queueRef.onProgress(this.toInfo());\n\n const remaining = this.duration - this.currentTime;\n const snap = this._actor.getSnapshot();\n if (\n !snap.context.notifiedLookahead &&\n !isNaN(remaining) &&\n remaining <= GAPLESS_SCHEDULE_LOOKAHEAD\n ) {\n this._actor.send({ type: 'LOOKAHEAD_REACHED' });\n queueMicrotask(() => this.queueRef.onTrackBufferReady(this));\n }\n\n this.rafId = requestAnimationFrame(loop);\n };\n this.rafId = requestAnimationFrame(loop);\n }\n\n private _stopProgressLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n}\n","// ---------------------------------------------------------------------------\n// TrackMachine — per-track audio state (xstate v5)\n//\n// States:\n// idle Initial state. Audio nodes not yet initialised.\n// html5 HTML5 Audio is playing. Web Audio fetch+decode may be in progress.\n// loading Track is preloaded (not yet playing). Decode in progress.\n// webaudio AudioBufferSourceNode is the active output.\n//\n// Design invariant — \"Web Audio always wins eventually\":\n// When a track's buffer finishes decoding (BUFFER_READY), webAudioLoadingState\n// is set to 'LOADED' regardless of what state the machine is in (html5, loading,\n// or idle). We intentionally do NOT switch mid-stream — the track stays in html5\n// until the next play(). But every state handles BUFFER_READY, so the flag is\n// never lost, and the next play() will see the buffer and use Web Audio.\n//\n// All DEACTIVATE transitions land in idle (not loading), so a deactivated track\n// with a loaded buffer is always in idle+LOADED — ready for Web Audio on re-play.\n//\n// Bug fixes in this rewrite:\n// #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)\n// #3: DEACTIVATE from webaudio → idle (was staying in webaudio)\n// #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, type AnyActorRef } from 'xstate';\nimport { fetchDecodeMachine } from './fetchDecode.machine';\nimport type { WebAudioLoadingState, PlaybackType } from '../types';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface TrackContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n playbackType: PlaybackType;\n webAudioLoadingState: WebAudioLoadingState;\n isPlaying: boolean;\n scheduledStartContextTime: number | null;\n notifiedLookahead: boolean;\n fetchDecodeRef: AnyActorRef | null;\n}\n\n// ---- Events ----------------------------------------------------------------\n\nexport type TrackEvent =\n | { type: 'ACTIVATE' }\n | { type: 'DEACTIVATE' }\n | { type: 'PRELOAD' }\n | { type: 'BUFFER_LOADING' }\n | { type: 'PLAY' }\n | { type: 'PLAY_WEBAUDIO' }\n | { type: 'PAUSE' }\n | { type: 'SEEK'; time: number }\n | { type: 'SET_VOLUME'; volume: number }\n | { type: 'BUFFER_READY' }\n | { type: 'BUFFER_ERROR' }\n | { type: 'HTML5_ENDED' }\n | { type: 'WEBAUDIO_ENDED' }\n | { type: 'URL_RESOLVED'; url: string }\n | { type: 'START_FETCH' }\n | { type: 'SCHEDULE_GAPLESS'; when: number }\n | { type: 'CANCEL_GAPLESS' }\n | { type: 'LOOKAHEAD_REACHED' };\n\n// ---- Machine ---------------------------------------------------------------\n\nexport function createTrackMachine(initialContext: TrackContext) {\n return setup({\n types: {\n context: {} as TrackContext,\n events: {} as TrackEvent,\n },\n actors: {\n fetchDecode: fetchDecodeMachine,\n },\n guards: {\n canPlayWebAudio: () => false,\n canStartFetch: ({ context }) => context.webAudioLoadingState === 'NONE' && context.fetchDecodeRef === null,\n },\n actions: {\n playHtml5: () => {},\n startSourceNode: () => {},\n startScheduledSourceNode: () => {},\n startProgressLoop: () => {},\n pauseHtml5: () => {},\n freezePausedTime: () => {},\n stopSourceNode: () => {},\n disconnectGain: () => {},\n stopProgressLoop: () => {},\n reportProgress: () => {},\n seekHtml5: () => {},\n seekWebAudio: () => {},\n resetHtml5Element: () => {},\n resetTiming: () => {},\n notifyTrackEnded: () => {},\n setIsPlaying: assign({ isPlaying: () => true }),\n clearIsPlaying: assign({ isPlaying: () => false }),\n setLoadingState: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),\n setLoadedState: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),\n setErrorState: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),\n clearScheduleAndLookahead: assign({ scheduledStartContextTime: () => null, notifiedLookahead: () => false }),\n setPlayingWebAudio: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n setScheduledGapless: assign({\n isPlaying: () => true,\n webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n scheduledStartContextTime: ({ event }) =>\n (event as { type: 'SCHEDULE_GAPLESS'; when: number }).when,\n }),\n clearPlayingAndSchedule: assign({\n isPlaying: () => false,\n scheduledStartContextTime: () => null,\n notifiedLookahead: () => false,\n }),\n setNotifiedLookahead: assign({ notifiedLookahead: () => true }),\n setResolvedUrl: assign({\n resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,\n }),\n clearScheduledStart: assign({ scheduledStartContextTime: () => null }),\n setPlayingWebAudioType: assign({\n isPlaying: () => true,\n playbackType: () => 'WEBAUDIO' as PlaybackType,\n }),\n },\n }).createMachine({\n id: 'track',\n initial: 'idle',\n context: initialContext,\n\n on: {\n START_FETCH: {\n guard: 'canStartFetch',\n actions: assign({\n webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState,\n fetchDecodeRef: ({ context, spawn }) =>\n spawn('fetchDecode', {\n id: 'fetchDecode',\n input: {\n trackUrl: context.trackUrl,\n resolvedUrl: context.resolvedUrl,\n skipHEAD: context.skipHEAD,\n },\n }),\n }),\n },\n },\n\n states: {\n // -----------------------------------------------------------------\n // idle: constructed but not started\n // -----------------------------------------------------------------\n idle: {\n on: {\n HTML5_ENDED: {\n actions: ['notifyTrackEnded'],\n },\n DEACTIVATE: {\n actions: [\n 'resetHtml5Element',\n 'resetTiming',\n 'stopProgressLoop',\n 'clearScheduleAndLookahead',\n ],\n },\n ACTIVATE: {\n actions: [\n 'resetTiming',\n 'resetHtml5Element',\n 'clearScheduleAndLookahead',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n PRELOAD: { target: 'loading' },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // html5: HTML5 Audio is playing; WebAudio decode may be in progress\n // -----------------------------------------------------------------\n html5: {\n on: {\n PAUSE: {\n actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],\n },\n PLAY: {\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: 'setPlayingWebAudio',\n },\n // Bug #2 fix: BUFFER_READY in html5 stays in html5, only updates loading state.\n // The actual switchover to webaudio only happens via explicit PLAY_WEBAUDIO.\n BUFFER_READY: {\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n actions: 'setErrorState',\n },\n SEEK: {\n actions: [\n 'seekHtml5',\n 'reportProgress',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n HTML5_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'pauseHtml5',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'pauseHtml5', 'resetHtml5Element', 'resetTiming', 'stopProgressLoop'],\n },\n },\n },\n\n // -----------------------------------------------------------------\n // loading: preloading in background (not the active track yet)\n // -----------------------------------------------------------------\n loading: {\n on: {\n BUFFER_LOADING: {\n actions: 'setLoadingState',\n },\n BUFFER_READY: {\n target: 'idle',\n actions: 'setLoadedState',\n },\n BUFFER_ERROR: {\n target: 'idle',\n actions: 'setErrorState',\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n {\n target: 'html5',\n actions: ['setIsPlaying', 'playHtml5', 'startProgressLoop'],\n },\n ],\n PLAY_WEBAUDIO: {\n target: 'webaudio',\n actions: [\n 'setPlayingWebAudio',\n 'startSourceNode',\n 'startProgressLoop',\n ],\n },\n SCHEDULE_GAPLESS: {\n target: 'webaudio',\n actions: [\n 'setScheduledGapless',\n 'startScheduledSourceNode',\n ],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n DEACTIVATE: {\n target: 'idle',\n actions: ['clearIsPlaying', 'resetTiming'],\n },\n URL_RESOLVED: {\n actions: 'setResolvedUrl',\n },\n },\n },\n\n // -----------------------------------------------------------------\n // webaudio: AudioBufferSourceNode is driving output\n // -----------------------------------------------------------------\n webaudio: {\n on: {\n PAUSE: {\n actions: [\n 'clearIsPlaying',\n 'freezePausedTime',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'reportProgress',\n ],\n },\n PLAY: [\n {\n guard: 'canPlayWebAudio',\n actions: ['setIsPlaying', 'startSourceNode', 'startProgressLoop'],\n },\n {\n actions: 'setIsPlaying',\n },\n ],\n PLAY_WEBAUDIO: {\n actions: 'setPlayingWebAudioType',\n },\n SEEK: {\n actions: [\n 'clearScheduledStart',\n 'seekWebAudio',\n 'reportProgress',\n ],\n },\n SET_VOLUME: {},\n CANCEL_GAPLESS: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n ],\n },\n LOOKAHEAD_REACHED: {\n actions: 'setNotifiedLookahead',\n },\n WEBAUDIO_ENDED: {\n target: 'idle',\n actions: ['clearIsPlaying', 'stopProgressLoop', 'notifyTrackEnded'],\n },\n ACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'stopProgressLoop',\n 'resetTiming',\n 'resetHtml5Element',\n ],\n },\n // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)\n DEACTIVATE: {\n target: 'idle',\n actions: [\n 'clearPlayingAndSchedule',\n 'stopSourceNode',\n 'disconnectGain',\n 'resetTiming',\n 'resetHtml5Element',\n 'stopProgressLoop',\n ],\n },\n },\n },\n },\n });\n}\n\nexport type TrackMachine = ReturnType<typeof createTrackMachine>;\n","// ---------------------------------------------------------------------------\n// FetchDecodeMachine — child actor for the fetch + decode pipeline\n//\n// States: resolvingUrl → fetching → decoding → done | error\n//\n// Spawned by TrackMachine via START_FETCH. Sends BUFFER_READY, BUFFER_ERROR,\n// and URL_RESOLVED back to the parent — the same events TrackMachine already\n// handles. Promise implementations are no-op defaults, provided by Track.ts\n// via .provide().\n//\n// xstate v5 automatically passes an AbortSignal to fromPromise actors, so\n// when the parent stops (destroy), in-flight fetches are aborted for free.\n// ---------------------------------------------------------------------------\n\nimport { setup, assign, sendParent, fromPromise } from 'xstate';\n\n// ---- Context ---------------------------------------------------------------\n\nexport interface FetchDecodeContext {\n trackUrl: string;\n resolvedUrl: string;\n skipHEAD: boolean;\n}\n\n// ---- Machine ---------------------------------------------------------------\n\nexport const fetchDecodeMachine = setup({\n types: {\n context: {} as FetchDecodeContext,\n input: {} as FetchDecodeContext,\n },\n actors: {\n resolveUrl: fromPromise<string | null, { trackUrl: string }>(async () => null),\n fetchAudio: fromPromise<void, { resolvedUrl: string }>(async () => {}),\n decodeAudio: fromPromise<void, void>(async () => {}),\n },\n guards: {\n shouldSkipHEAD: ({ context }) => context.skipHEAD,\n },\n}).createMachine({\n id: 'fetchDecode',\n initial: 'resolvingUrl',\n // Note: xstate v5 internally calls assign() when initialising a child\n // actor with a context function, which produces a false-positive\n // \"Custom actions should not call assign()\" warning when this machine\n // is spawned from a parent. This is harmless and cannot be avoided.\n context: ({ input }) => ({\n trackUrl: input.trackUrl,\n resolvedUrl: input.resolvedUrl,\n skipHEAD: input.skipHEAD,\n }),\n\n states: {\n // -----------------------------------------------------------------\n // resolvingUrl: HEAD request to resolve redirects\n // -----------------------------------------------------------------\n resolvingUrl: {\n always: {\n guard: 'shouldSkipHEAD',\n target: 'fetching',\n },\n invoke: {\n id: 'resolveUrl',\n src: 'resolveUrl',\n input: ({ context }) => ({ trackUrl: context.trackUrl }),\n onDone: {\n target: 'fetching',\n actions: [\n assign({\n resolvedUrl: ({ event, context }) => event.output ?? context.resolvedUrl,\n skipHEAD: () => true,\n }),\n sendParent(({ event, context }) => {\n const url = event.output;\n return { type: 'URL_RESOLVED' as const, url: url ?? context.resolvedUrl };\n }),\n ],\n },\n onError: {\n // HEAD failed — non-fatal, fall back to original URL\n target: 'fetching',\n actions: assign({ skipHEAD: () => true }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // fetching: GET the audio data\n // -----------------------------------------------------------------\n fetching: {\n invoke: {\n id: 'fetchAudio',\n src: 'fetchAudio',\n input: ({ context }) => ({ resolvedUrl: context.resolvedUrl }),\n onDone: 'decoding',\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // decoding: decodeAudioData on the ArrayBuffer\n // -----------------------------------------------------------------\n decoding: {\n invoke: {\n id: 'decodeAudio',\n src: 'decodeAudio',\n input: () => {},\n onDone: {\n target: 'done',\n actions: sendParent({ type: 'BUFFER_READY' }),\n },\n onError: {\n target: 'error',\n actions: sendParent({ type: 'BUFFER_ERROR' }),\n },\n },\n },\n\n // -----------------------------------------------------------------\n // Terminal states\n // -----------------------------------------------------------------\n done: { type: 'final' },\n error: { type: 'final' },\n },\n});\n\nexport type FetchDecodeMachine = typeof fetchDecodeMachine;\n","/** Returns a throttled version of `fn` that fires at most once per `ms`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(\n fn: T,\n ms: number,\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = performance.now();\n if (now - last < ms) return;\n last = now;\n fn(...args);\n };\n}\n"],"mappings":"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EAGA,aAAc,CACZ,QAAS,gBACX,EACA,aAAc,CACZ,QAAS,eACX,EACA,KAAM,CACJ,QAAS,CACP,YACA,gBACF,CACF,EACA,kBAAmB,CACjB,QAAS,sBACX,EACA,YAAa,CACX,OAAQ,OACR,QAAS,CAAC,iBAAkB,mBAAoB,kBAAkB,CACpE,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,aACA,mBACA,cACA,mBACF,CACF,EACA,aAAc,CACZ,QAAS,gBACX,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAc,oBAAqB,cAAe,kBAAkB,CAClG,CACF,CACF,EAKA,QAAS,CACP,GAAI,CACF,eAAgB,CACd,QAAS,iBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,gBACX,EACA,aAAc,CACZ,OAAQ,OACR,QAAS,eACX,EACA,KAAM,CACJ,CACE,MAAO,kBACP,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,CACE,OAAQ,QACR,QAAS,CAAC,eAAgB,YAAa,mBAAmB,CAC5D,CACF,EACA,cAAe,CACb,OAAQ,WACR,QAAS,CACP,qBACA,kBACA,mBACF,CACF,EACA,iBAAkB,CAChB,OAAQ,WACR,QAAS,CACP,sBACA,0BACF,CACF,EACA,SAAU,CACR,OAAQ,OACR,QAAS,CACP,0BACA,cACA,mBACF,CACF,EACA,WAAY,CACV,OAAQ,OACR,QAAS,CAAC,iBAAkB,aAAa,CAC3C,EACA,aAAc,CACZ,QAAS,gBACX,CACF,CACF,EAKA,SAAU,CACR,GAAI,CACF,MAAO,CACL,QAAS,CACP,iBACA,mBACA,iBACA,iBACA,mBACA,gBACF,CACF,EACA,KAAM,CACJ,CACE,MAAO,kBACP,QAAS,CAAC,eAAgB,kBAAmB,mBAAmB,CAClE,EACA,CACE,QAAS,cACX,CACF,EACA,cAAe,CACb,QAAS,wBACX,EACA,KAAM,CACJ,QAAS,CACP,sBACA,eACA,gBACF,CACF,EACA,WAAY,CAAC,EACb,eAAgB,CACd,OAAQ,OACR,QAAS,CACP,0BACA,iBACA,iBACA,mBACA,aACF,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","names":["createActor","isBrowser","_audioContext","createAudioContext","Ctor","getAudioContext","resumeAudioContext","ctx","hasMediaSession","setupMediaSession","handlers","mediaSession","details","updateMediaSessionMetadata","metadata","updateMediaSessionPlaybackState","isPlaying","updateMediaSessionPositionState","duration","position","playbackRate","setup","assign","createQueueMachine","initialContext","context","event","next","createActor","fromPromise","setup","assign","setup","assign","sendParent","fromPromise","fetchDecodeMachine","context","input","event","createTrackMachine","initialContext","setup","fetchDecodeMachine","context","assign","event","spawn","GAPLESS_SCHEDULE_LOOKAHEAD","Track","context","getAudioContext","opts","_a","_b","code","msg","initialContext","machine","createTrackMachine","fetchDecodeMachine","fromPromise","signal","res","input","resolvedUrl","buf","when","createActor","time","clamped","v","vol","secondsFromEnd","dur","snap","promise","err","offset","wasPlaying","loop","remaining","throttle","fn","ms","last","args","now","PRELOAD_AHEAD","Queue","throttle","duration","currentTime","updateMediaSessionPositionState","options","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","onError","onPlayBlocked","onDebug","webAudioIsDisabled","trackMetadata","initialVolume","url","i","Track","machine","createQueueMachine","context","_a","track","cur","updateMediaSessionMetadata","event","e","createActor","snapshot","updateMediaSessionPlaybackState","setupMediaSession","t","ct","index","playImmediately","time","volume","clamped","metadata","resumeAudioContext","snap","newSnap","info","error","msg","fromIndex","limit","curIndex","ctx","getAudioContext","nextIndex","current","next","endTime","remaining"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gapless",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8",
|
|
4
4
|
"description": "Gapless audio playback javascript plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -47,14 +47,6 @@
|
|
|
47
47
|
"vitest": "^4.0.18"
|
|
48
48
|
},
|
|
49
49
|
"packageManager": "pnpm@10.30.2",
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"xstate": "^5.28.0"
|
|
52
|
-
},
|
|
53
|
-
"peerDependenciesMeta": {
|
|
54
|
-
"xstate": {
|
|
55
|
-
"optional": true
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
50
|
"dependencies": {
|
|
59
51
|
"xstate": "^5.28.0"
|
|
60
52
|
}
|
package/src/Track.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { createActor, fromPromise } from 'xstate';
|
|
6
|
-
import { getAudioContext } from './utils/audioContext';
|
|
6
|
+
import { getAudioContext, resumeAudioContext } from './utils/audioContext';
|
|
7
7
|
import { createTrackMachine } from './machines/track.machine';
|
|
8
8
|
import { fetchDecodeMachine } from './machines/fetchDecode.machine';
|
|
9
9
|
import type { TrackContext } from './machines/track.machine';
|
|
@@ -228,7 +228,9 @@ export class Track {
|
|
|
228
228
|
if (this._actor.getSnapshot().value === 'idle') {
|
|
229
229
|
this._actor.send({ type: 'PRELOAD' });
|
|
230
230
|
}
|
|
231
|
-
if (this.audioBuffer
|
|
231
|
+
if (this.audioBuffer) return;
|
|
232
|
+
resumeAudioContext();
|
|
233
|
+
if (!this.ctx) return;
|
|
232
234
|
this._actor.send({ type: 'START_FETCH' });
|
|
233
235
|
}
|
|
234
236
|
|