gapless.js 2.2.3 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -2
- package/README.md +16 -14
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.d.mts +131 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -39
- package/src/Queue.ts +289 -0
- package/src/Track.ts +813 -0
- package/src/index.ts +1 -0
- package/CHANGELOG.md +0 -29
- package/index.d.ts +0 -110
- package/index.html +0 -55
- package/index.js +0 -456
- package/tsconfig.json +0 -37
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2017 Daniel Saewitz
|
|
3
|
+
Copyright (c) 2017-2025 Daniel Saewitz
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
package/README.md
CHANGED
|
@@ -4,26 +4,28 @@ gapless.js is a library for gapless audio playback. It is not intended to be a f
|
|
|
4
4
|
|
|
5
5
|
In short, it takes an array of audio tracks and utilizes HTML5 audio and the web audio API to enable gapless playback of individual tracks.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
I will expand this README with more details in time. If you're interested in this library, don't hesitate to leave me a note with any questions.
|
|
8
8
|
|
|
9
9
|
You can see a sample of the library in use currently at <Relisten.live> which is the not-yet-released beta of the next version of <Relisten.net>
|
|
10
10
|
|
|
11
11
|
## Sample usage
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
import GaplessQueue from 'gapless.js';
|
|
15
|
+
|
|
16
|
+
const player = new GaplessQueue({
|
|
17
|
+
tracks: [
|
|
18
|
+
"http://phish.in/audio/000/012/321/12321.mp3",
|
|
19
|
+
"http://phish.in/audio/000/012/322/12322.mp3",
|
|
20
|
+
"http://phish.in/audio/000/012/323/12323.mp3",
|
|
21
|
+
"http://phish.in/audio/000/012/324/12324.mp3"
|
|
22
|
+
],
|
|
23
|
+
onProgress: function(track) {
|
|
24
|
+
track && console.log(track.completeState);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
player.play();
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
## Gapless.Queue Options
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";var n=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var k=(o,e)=>{for(var t in e)n(o,t,{get:e[t],enumerable:!0})},T=(o,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of g(e))!A.call(o,s)&&s!==t&&n(o,s,{get:()=>e[s],enumerable:!(i=p(e,s))||i.enumerable});return o};var y=o=>T(n({},"__esModule",{value:!0}),o);var S={};k(S,{default:()=>d});module.exports=y(S);var m=typeof window<"u",P=m&&(window.AudioContext||window.webkitAudioContext)?new(window.AudioContext||window.webkitAudioContext):null;var a=class{playbackType;webAudioLoadingState;loadedHEAD;idx;queue;trackUrl;skipHEAD;metadata;audio;audioContext;gainNode;bufferSourceNode;audioBuffer;webAudioStartedPlayingAt;webAudioPausedDuration;webAudioPausedAt;boundOnEnded;boundOnProgress;boundAudioOnError;progressFrameId=null;constructor({trackUrl:e,skipHEAD:t,queue:i,idx:s,metadata:u={}}){this.playbackType="HTML5",this.webAudioLoadingState="NONE",this.loadedHEAD=!1,this.idx=s,this.queue=i,this.trackUrl=e,this.skipHEAD=t,this.metadata=u,this.boundOnEnded=r=>this.onEnded(r),this.boundOnProgress=()=>this.onProgress(),this.boundAudioOnError=r=>this.audioOnError(r),this.audio=new Audio,this.audio.onerror=this.boundAudioOnError,this.audio.onended=()=>this.boundOnEnded("HTML5"),this.audio.controls=!1,this.audio.volume=i.state.volume,this.audio.preload="none",this.audio.src=e,this.audioContext=i.state.webAudioIsDisabled?null:P,this.gainNode=null,this.bufferSourceNode=null,this.audioBuffer=null,this.webAudioStartedPlayingAt=0,this.webAudioPausedDuration=0,this.webAudioPausedAt=0,this.audioContext&&(this.gainNode=this.audioContext.createGain(),this.gainNode.gain.value=i.state.volume)}loadHEAD(e){if(this.loadedHEAD||this.skipHEAD){e();return}let t={method:"HEAD"};fetch(this.trackUrl,t).then(i=>{i.redirected&&i.url&&(this.trackUrl=i.url,this.audio.src!==this.trackUrl&&this.audio.readyState===0&&(this.audio.src=this.trackUrl)),this.loadedHEAD=!0,e()}).catch(i=>{console.error(`HEAD request failed for track ${this.idx}:`,i),e()})}loadBuffer(e){!this.audioContext||this.webAudioLoadingState!=="NONE"||this.queue.state.webAudioIsDisabled||(this.webAudioLoadingState="LOADING",this.debug("starting download"),fetch(this.trackUrl).then(t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return t.arrayBuffer()}).then(t=>this.audioContext.decodeAudioData(t,i=>{this.debug("finished decoding track"),this.webAudioLoadingState="LOADED",this.audioBuffer=i,this.queue.loadTrack(this.idx+1),this.isActiveTrack&&this.playbackType==="HTML5"&&!this.isPaused?this.switchToWebAudio():this.isActiveTrack&&this.playbackType==="HTML5"&&this.isPaused?(this.playbackType="WEBAUDIO",this.debug("WebAudio ready for paused track")):this.isActiveTrack||(this.playbackType="WEBAUDIO",this.debug("WebAudio ready for inactive track")),e==null||e(i)},i=>{console.error(`Error decoding audio data for track ${this.idx}:`,i),this.webAudioLoadingState="NONE"})).catch(t=>{this.debug("caught fetch/decode error",t),this.webAudioLoadingState="NONE"}))}createBufferSourceNode(){if(!this.audioContext||!this.audioBuffer||!this.gainNode)return null;let e=this.audioContext.createBufferSource();return e.buffer=this.audioBuffer,e.connect(this.gainNode),e.onended=()=>this.boundOnEnded("webaudio_auto"),e}switchToWebAudio(e=!1){if(!this.audioContext||!this.audioBuffer||!this.gainNode||this.webAudioLoadingState!=="LOADED"){this.debug("Cannot switch to WebAudio: not ready.");return}if(!this.isActiveTrack&&!e){this.debug("Cannot switch to WebAudio: not active track.");return}let t=this.audio.paused,i=this.audio.currentTime;if(this.debug("Attempting switch to web audio",`currentTime: ${i}`,`wasPaused: ${t}`,`HTML5 duration: ${this.audio.duration}`,`WebAudio duration: ${this.audioBuffer.duration}`),this.audio.pause(),this.stopAndDisconnectSourceNode(),this.bufferSourceNode=this.createBufferSourceNode(),!this.bufferSourceNode){this.debug("Failed to create buffer source node for switch"),this.playbackType="HTML5";return}this.connectGainNode(),this.webAudioStartedPlayingAt=this.audioContext.currentTime-i,this.webAudioPausedDuration=0,this.webAudioPausedAt=0;try{this.bufferSourceNode.start(0,i),this.debug(`WebAudio started at context time ${this.audioContext.currentTime}, track time ${i}`)}catch(s){console.error("Error starting buffer source node:",s),this.playbackType="HTML5",this.disconnectGainNode();return}t||e?(this.pauseWebAudio(),this.debug("Switched to WebAudio (Paused)")):(this.bufferSourceNode.playbackRate.value=1,this.debug("Switched to WebAudio (Playing)")),this.playbackType="WEBAUDIO"}pause(){this.debug("pause command received"),this.isUsingWebAudio?this.pauseWebAudio():(this.audio.pause(),this.cancelProgressFrame())}pauseWebAudio(){!this.audioContext||!this.bufferSourceNode||this.isPaused||(this.webAudioPausedAt=this.audioContext.currentTime,this.disconnectGainNode(),this.debug(`WebAudio paused at ${this.webAudioPausedAt}`),this.cancelProgressFrame())}play(){this.debug("play command received"),this.audioBuffer&&this.audioContext&&!this.queue.state.webAudioIsDisabled?this.isUsingWebAudio?this.playWebAudio():this.webAudioLoadingState==="LOADED"?(this.debug("WebAudio buffer ready, switching from HTML5..."),this.switchToWebAudio(),this.requestProgressFrame()):this.webAudioLoadingState==="LOADING"?(this.debug("WebAudio loading, playing HTML5 temporarily..."),this.playHtml5Audio()):(this.debug("WebAudio not loaded, starting load and playing HTML5..."),this.preload(),this.playHtml5Audio()):this.playHtml5Audio(),this.queue.loadTrack(this.idx+1)}playWebAudio(){if(!(!this.audioContext||!this.bufferSourceNode||!this.isPaused)){if(this.webAudioPausedAt>0){let e=this.audioContext.currentTime-this.webAudioPausedAt;this.webAudioPausedDuration+=e,this.debug(`Resuming WebAudio after ${e.toFixed(2)}s pause. Total paused: ${this.webAudioPausedDuration.toFixed(2)}s`)}this.connectGainNode(),this.webAudioPausedAt=0,this.debug("WebAudio playing"),this.requestProgressFrame()}}playHtml5Audio(){if(!this.audio.paused)return;this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e!==void 0?e.then(t=>{this.debug("HTML5 playing"),this.requestProgressFrame(),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&this.preload()}).catch(t=>{console.error(`Error playing HTML5 audio for track ${this.idx}:`,t),this.boundAudioOnError(`Playback error: ${t.message}`)}):(this.debug("HTML5 playing (no promise)"),this.requestProgressFrame(),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&this.preload())}togglePlayPause(){this.isPaused?this.play():this.pause()}preload(e=!1){this.debug(`preload called, loadHTML5: ${e}`),e&&this.audio.preload!=="auto"&&this.audio.readyState<2&&(this.debug("preloading HTML5"),this.audio.preload="auto"),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&(this.debug("preloading WebAudio buffer"),this.skipHEAD?this.loadBuffer():this.loadHEAD(()=>this.loadBuffer()))}seek(e=0){let t=this.duration;if(isNaN(t)||t<=0){this.debug("Cannot seek: duration unknown or invalid.");return}let i=Math.max(0,Math.min(e,t));this.debug(`seek command to: ${i} (original: ${e})`),this.isUsingWebAudio&&this.audioContext?this.seekBufferSourceNode(i):this.audio.readyState>=this.audio.HAVE_METADATA?this.audio.currentTime=i:this.debug("Cannot seek HTML5: not ready."),this.onProgress()}seekBufferSourceNode(e){if(!this.audioContext||!this.audioBuffer||!this.gainNode){this.debug("Cannot seek WebAudio: context, buffer, or gain node missing.");return}let t=this.isPaused;if(this.debug(`Seeking WebAudio to ${e}. Was paused: ${t}`),this.stopAndDisconnectSourceNode(),this.bufferSourceNode=this.createBufferSourceNode(),!this.bufferSourceNode){this.debug("Failed to create buffer source node for seek");return}this.webAudioStartedPlayingAt=this.audioContext.currentTime-e,this.webAudioPausedDuration=0,this.webAudioPausedAt=0;try{this.bufferSourceNode.start(0,e),this.debug(`WebAudio started after seek at context time ${this.audioContext.currentTime}, track time ${e}`)}catch(i){console.error("Error starting buffer source node after seek:",i),this.disconnectGainNode();return}t?(this.pauseWebAudio(),this.debug("Re-applied paused state after seek.")):(this.connectGainNode(),this.debug("Resumed playing state after seek."),this.requestProgressFrame())}stopAndDisconnectSourceNode(){if(this.bufferSourceNode){try{this.bufferSourceNode.onended=null,this.bufferSourceNode.stop(),this.bufferSourceNode.disconnect(),this.debug("Stopped and disconnected previous source node.")}catch(e){e.name!=="InvalidStateError"&&console.error("Error stopping buffer source node:",e)}this.bufferSourceNode=null}}connectGainNode(){if(this.gainNode&&this.audioContext)try{this.gainNode.connect(this.audioContext.destination)}catch(e){console.error("Error connecting gain node:",e)}}disconnectGainNode(){if(this.gainNode&&this.audioContext)try{this.gainNode.disconnect(this.audioContext.destination)}catch{}}audioOnError=e=>{let t=e;if(typeof e!="string"&&e.target){let i=e.target.error;t=`HTML5 Audio Error: code=${i==null?void 0:i.code}, message=${i==null?void 0:i.message}`}this.debug("audioOnError",t)};onEnded(e){this.debug("onEnded triggered",`from: ${typeof e=="string"?e:"event"}`,`isActive: ${this.isActiveTrack}`),this.bufferSourceNode&&(this.bufferSourceNode.onended=null),this.audio.onended=null,this.cancelProgressFrame(),this.isActiveTrack?this.queue.playNext():(this.debug("onEnded ignored for inactive track"),this.resetStateAfterEnded())}resetStateAfterEnded(){this.webAudioStartedPlayingAt=0,this.webAudioPausedDuration=0,this.webAudioPausedAt=0,this.stopAndDisconnectSourceNode(),this.audio.readyState>0&&(this.audio.currentTime=0),this.audio.onended=()=>this.boundOnEnded("HTML5"),this.bufferSourceNode&&this.audioContext}onProgress(){if(!this.isActiveTrack||this.isPaused){this.cancelProgressFrame();return}let e=this.currentTime,t=this.duration;if(isNaN(e)||isNaN(t)||t<=0){this.requestProgressFrame();return}let i=t-e<=25,s=this.queue.nextTrack;i&&s&&!s.isLoaded&&this.queue.loadTrack(this.idx+1,!0),this.queue.onProgress(this),this.requestProgressFrame()}requestProgressFrame(){this.cancelProgressFrame(),this.progressFrameId=window.requestAnimationFrame(this.boundOnProgress)}cancelProgressFrame(){this.progressFrameId!==null&&(window.cancelAnimationFrame(this.progressFrameId),this.progressFrameId=null)}setVolume(e){let t=Math.max(0,Math.min(1,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t)}get isUsingWebAudio(){return this.playbackType==="WEBAUDIO"&&!this.queue.state.webAudioIsDisabled}get isPaused(){return this.isUsingWebAudio&&this.audioContext?this.webAudioPausedAt>0:this.audio.paused}get currentTime(){return this.isUsingWebAudio&&this.audioContext?this.webAudioPausedAt>0?this.webAudioStartedPlayingAt>0?this.webAudioPausedAt-this.webAudioStartedPlayingAt-this.webAudioPausedDuration:0:this.webAudioStartedPlayingAt>0?this.audioContext.currentTime-this.webAudioStartedPlayingAt-this.webAudioPausedDuration:0:this.audio.currentTime}get duration(){return this.isUsingWebAudio&&this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isActiveTrack(){return this.queue.currentTrack===this}get isLoaded(){return this.webAudioLoadingState==="LOADED"||this.audio.readyState>=this.audio.HAVE_FUTURE_DATA}get state(){return{playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState}}get completeState(){var e;return{playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,isPaused:this.isPaused,currentTime:this.currentTime,duration:this.duration,idx:this.idx,id:(e=this.metadata)==null?void 0:e.trackId}}debug(e,...t){console.debug(`[Track ${this.idx} | ${this.playbackType} | ${this.webAudioLoadingState}] ${e}`,...t,this.completeState)}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e?this.seek(t-e):this.debug(`Cannot seekToEnd: duration invalid or too short (${t})`)}};var w=2,v=typeof window<"u",h=v&&typeof window.AudioContext<"u"?new window.AudioContext:null,d=class{props;state;tracks;Track;constructor(e={}){let{tracks:t=[],onProgress:i=()=>{},onEnded:s=()=>{},onPlayNextTrack:u=()=>{},onPlayPreviousTrack:r=()=>{},onStartNewTrack:c=()=>{},webAudioIsDisabled:l=!1}=e;this.props={onProgress:i,onEnded:s,onPlayNextTrack:u,onPlayPreviousTrack:r,onStartNewTrack:c},this.state={volume:1,currentTrackIdx:0,webAudioIsDisabled:l},this.Track=a,this.tracks=t.map((b,f)=>new a({trackUrl:b,idx:f,queue:this,metadata:{}})),h||this.disableWebAudio()}addTrack({trackUrl:e,skipHEAD:t,metadata:i={}}){this.tracks.push(new a({trackUrl:e,skipHEAD:t,metadata:i,idx:this.tracks.length,queue:this}))}removeTrack(e){let t=this.tracks.indexOf(e);return t>-1?this.tracks.splice(t,1):[]}togglePlayPause(){this.currentTrack&&this.currentTrack.togglePlayPause()}play(){this.currentTrack&&this.currentTrack.play()}pause(){this.currentTrack&&this.currentTrack.pause()}playPrevious(){if(this.currentTrack&&this.currentTrack.currentTime>8){this.currentTrack.seek(0);return}this.resetCurrentTrack(),--this.state.currentTrackIdx<0&&(this.state.currentTrackIdx=0),this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack),this.props.onPlayPreviousTrack&&this.props.onPlayPreviousTrack(this.currentTrack)}playNext(){if(this.resetCurrentTrack(),this.state.currentTrackIdx<this.tracks.length-1)this.state.currentTrackIdx++;else{this.props.onEnded();return}this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack),this.props.onPlayNextTrack&&this.props.onPlayNextTrack(this.currentTrack)}resetCurrentTrack(){if(this.currentTrack)try{this.currentTrack.isPaused||this.currentTrack.pause(),this.currentTrack.duration>0&&!isNaN(this.currentTrack.duration)&&this.currentTrack.seek(0)}catch(e){console.error("Error resetting track:",e,this.currentTrack)}}pauseAll(){this.tracks.forEach(e=>{e.pause()})}cleanUp(){this.tracks.forEach(e=>{e.bufferSourceNode&&e.bufferSourceNode.buffer&&(e.bufferSourceNode.buffer=null),e.audioBuffer&&(e.audioBuffer=null);try{e.bufferSourceNode&&(e.bufferSourceNode.onended=null,e.bufferSourceNode.stop(),e.bufferSourceNode.disconnect()),e.gainNode&&h&&e.gainNode.disconnect(),e.audio&&(e.audio.pause(),e.audio.src="",e.audio.load(),e.audio.onended=null,e.audio.onerror=null)}catch(t){console.error("Error during track cleanup:",t,e)}})}gotoTrack(e,t=!1){if(e<0||e>=this.tracks.length){console.warn(`gotoTrack: Index ${e} out of bounds.`);return}this.pauseAll(),this.resetCurrentTrack(),this.state.currentTrackIdx=e,t&&(this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack))}loadTrack(e,t){if(e<0||e>=this.tracks.length||this.state.currentTrackIdx+w<e)return;let i=this.tracks[e];i&&i.preload(t)}setProps(e={}){this.props={...this.props,...e}}onEnded(){this.props.onEnded&&this.props.onEnded()}onProgress(e){this.props.onProgress&&this.props.onProgress(e)}get currentTrack(){return this.tracks[this.state.currentTrackIdx]}get nextTrack(){return this.tracks[this.state.currentTrackIdx+1]}disableWebAudio(){this.state.webAudioIsDisabled=!0,this.tracks.forEach(e=>{e.isUsingWebAudio&&console.warn("Web Audio disabled while track was using it. State might be inconsistent.")})}setVolume(e){let t=Math.max(0,Math.min(1,e));this.state.volume=t,this.tracks.forEach(i=>i.setVolume(t))}};
|
|
2
|
+
module.exports = module.exports.default;
|
|
3
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/Track.ts","../../src/Queue.ts"],"sourcesContent":["export { default } from './Queue';\n","import Queue from './Queue';\n\nconst isBrowser: boolean = typeof window !== 'undefined';\nconst audioContext: AudioContext | null =\n isBrowser && (window.AudioContext || (window as any).webkitAudioContext)\n ? new (window.AudioContext || (window as any).webkitAudioContext)()\n : null;\n\n// Use Enums for better type safety\nenum GaplessPlaybackType {\n HTML5 = 'HTML5',\n WEBAUDIO = 'WEBAUDIO',\n}\n\nenum GaplessPlaybackLoadingState {\n NONE = 'NONE',\n LOADING = 'LOADING',\n LOADED = 'LOADED',\n}\n\ninterface TrackProps {\n trackUrl: string;\n skipHEAD?: boolean;\n queue: Queue;\n idx: number;\n metadata?: Record<string, any>;\n}\n\ninterface TrackState {\n playbackType: GaplessPlaybackType;\n webAudioLoadingState: GaplessPlaybackLoadingState;\n}\n\ninterface TrackCompleteState extends TrackState {\n isPaused: boolean;\n currentTime: number;\n duration: number;\n idx: number;\n id?: any; // Assuming metadata.trackId can be any type\n}\n\nexport default class Track {\n // Playback state\n playbackType: GaplessPlaybackType;\n webAudioLoadingState: GaplessPlaybackLoadingState;\n loadedHEAD: boolean;\n\n // Basic info\n idx: number;\n queue: Queue;\n trackUrl: string;\n skipHEAD?: boolean;\n metadata: Record<string, any>;\n\n // HTML5 Audio\n audio: HTMLAudioElement;\n\n // WebAudio API elements (nullable)\n audioContext: AudioContext | null;\n gainNode: GainNode | null;\n bufferSourceNode: AudioBufferSourceNode | null;\n audioBuffer: AudioBuffer | null;\n\n // WebAudio timing state\n webAudioStartedPlayingAt: number; // Time from audioContext.currentTime when playback started\n webAudioPausedDuration: number; // Total duration spent paused\n webAudioPausedAt: number; // Timestamp (audioContext.currentTime) when paused\n\n // Bound methods for event listeners\n private boundOnEnded: (from?: string | Event) => void;\n private boundOnProgress: () => void;\n private boundAudioOnError: (e: Event | string) => void;\n\n private progressFrameId: number | null = null; // Store requestAnimationFrame ID\n\n constructor({ trackUrl, skipHEAD, queue, idx, metadata = {} }: TrackProps) {\n // playback type state\n this.playbackType = GaplessPlaybackType.HTML5;\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE;\n this.loadedHEAD = false;\n\n // basic inputs from Queue\n this.idx = idx;\n this.queue = queue;\n this.trackUrl = trackUrl;\n this.skipHEAD = skipHEAD;\n this.metadata = metadata;\n\n // Bind methods to ensure 'this' context\n this.boundOnEnded = (from?: string | Event) => this.onEnded(from);\n this.boundOnProgress = () => this.onProgress();\n this.boundAudioOnError = (e: Event | string) => this.audioOnError(e);\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.onerror = this.boundAudioOnError;\n this.audio.onended = () => this.boundOnEnded('HTML5'); // Use bound method\n this.audio.controls = false;\n this.audio.volume = queue.state.volume;\n this.audio.preload = 'none'; // Explicitly 'none' initially\n this.audio.src = trackUrl;\n // this.audio.onprogress = () => this.debug(this.idx, this.audio.buffered)\n\n // WebAudio Initialization (only if supported and not disabled)\n this.audioContext = queue.state.webAudioIsDisabled ? null : audioContext;\n this.gainNode = null;\n this.bufferSourceNode = null;\n this.audioBuffer = null;\n this.webAudioStartedPlayingAt = 0;\n this.webAudioPausedDuration = 0;\n this.webAudioPausedAt = 0;\n\n if (this.audioContext) {\n this.gainNode = this.audioContext.createGain();\n this.gainNode.gain.value = queue.state.volume;\n // Don't create bufferSourceNode until needed\n }\n }\n\n // private functions\n private loadHEAD(cb: () => void): void {\n if (this.loadedHEAD || this.skipHEAD) {\n cb();\n return;\n }\n\n const options: RequestInit = {\n method: 'HEAD',\n };\n\n fetch(this.trackUrl, options)\n .then((res) => {\n if (res.redirected && res.url) {\n this.trackUrl = res.url;\n // If URL changed, might need to update HTMLAudioElement src?\n // Only if not already playing/loaded via HTML5.\n if (this.audio.src !== this.trackUrl && this.audio.readyState === 0) {\n this.audio.src = this.trackUrl;\n }\n }\n this.loadedHEAD = true;\n cb();\n })\n .catch((err) => {\n console.error(`HEAD request failed for track ${this.idx}:`, err);\n // Decide how to proceed, maybe try loading directly?\n cb(); // Or maybe call an error handler\n });\n }\n\n private loadBuffer(cb?: (buffer: AudioBuffer) => void): void {\n if (\n !this.audioContext ||\n this.webAudioLoadingState !== GaplessPlaybackLoadingState.NONE ||\n this.queue.state.webAudioIsDisabled\n ) {\n return;\n }\n\n this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADING;\n this.debug('starting download');\n\n fetch(this.trackUrl)\n .then((res) => {\n if (!res.ok) {\n throw new Error(`HTTP error! status: ${res.status}`);\n }\n return res.arrayBuffer();\n })\n .then((arrayBuffer) =>\n this.audioContext!.decodeAudioData(\n // Use non-null assertion as we checked audioContext\n arrayBuffer,\n (buffer) => {\n this.debug('finished decoding track');\n\n this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADED;\n this.audioBuffer = buffer;\n\n // Create and connect the source node *now* that we have the buffer\n // Don't connect gainNode to destination until play()\n // this.bufferSourceNode = this.createBufferSourceNode(); // Moved node creation\n\n // try to preload next track (WebAudio buffer)\n this.queue.loadTrack(this.idx + 1);\n\n // if we loaded the active track, switch to web audio if it was playing HTML5\n if (\n this.isActiveTrack &&\n this.playbackType === GaplessPlaybackType.HTML5 &&\n !this.isPaused\n ) {\n this.switchToWebAudio();\n } else if (\n this.isActiveTrack &&\n this.playbackType === GaplessPlaybackType.HTML5 &&\n this.isPaused\n ) {\n // If it was paused HTML5, just mark ready for WebAudio, don't auto-play\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n this.debug('WebAudio ready for paused track');\n } else if (!this.isActiveTrack) {\n // If it's not the active track, just mark ready\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n this.debug('WebAudio ready for inactive track');\n }\n\n cb?.(buffer);\n },\n (err) => {\n console.error(`Error decoding audio data for track ${this.idx}:`, err);\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE; // Reset state on decode error\n }\n )\n )\n .catch((e) => {\n this.debug('caught fetch/decode error', e);\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE; // Reset state on fetch error\n });\n }\n\n private createBufferSourceNode(): AudioBufferSourceNode | null {\n if (!this.audioContext || !this.audioBuffer || !this.gainNode) return null;\n\n const node = this.audioContext.createBufferSource();\n node.buffer = this.audioBuffer;\n node.connect(this.gainNode); // Connect source to gain\n node.onended = () => this.boundOnEnded('webaudio_auto'); // Use bound method\n return node;\n }\n\n private switchToWebAudio(forcePause: boolean = false): void {\n // Ensure WebAudio is ready and we are the active track (unless forced)\n if (\n !this.audioContext ||\n !this.audioBuffer ||\n !this.gainNode ||\n this.webAudioLoadingState !== GaplessPlaybackLoadingState.LOADED\n ) {\n this.debug('Cannot switch to WebAudio: not ready.');\n return;\n }\n if (!this.isActiveTrack && !forcePause) {\n this.debug('Cannot switch to WebAudio: not active track.');\n return;\n }\n\n const wasPaused = this.audio.paused; // State *before* switching\n const currentTime = this.audio.currentTime; // Time *before* switching\n\n this.debug(\n 'Attempting switch to web audio',\n `currentTime: ${currentTime}`,\n `wasPaused: ${wasPaused}`,\n `HTML5 duration: ${this.audio.duration}`,\n `WebAudio duration: ${this.audioBuffer.duration}`\n );\n\n // Stop HTML5 audio\n this.audio.pause();\n\n // Disconnect previous WebAudio node if exists\n this.stopAndDisconnectSourceNode();\n\n // Create and configure the new source node\n this.bufferSourceNode = this.createBufferSourceNode();\n if (!this.bufferSourceNode) {\n this.debug('Failed to create buffer source node for switch');\n // Revert? Or stay paused?\n this.playbackType = GaplessPlaybackType.HTML5; // Revert type\n return;\n }\n\n // Connect gain to destination *before* starting\n this.connectGainNode();\n\n // Calculate start time for WebAudio context\n this.webAudioStartedPlayingAt = this.audioContext.currentTime - currentTime;\n this.webAudioPausedDuration = 0; // Reset pause duration\n this.webAudioPausedAt = 0; // Reset pause timestamp\n\n // Start the buffer source\n try {\n this.bufferSourceNode.start(0, currentTime);\n this.debug(\n `WebAudio started at context time ${this.audioContext.currentTime}, track time ${currentTime}`\n );\n } catch (e) {\n console.error('Error starting buffer source node:', e);\n this.playbackType = GaplessPlaybackType.HTML5; // Revert type on error\n this.disconnectGainNode(); // Disconnect gain if start failed\n return;\n }\n\n // Handle initial pause state\n if (wasPaused || forcePause) {\n this.pauseWebAudio(); // Use dedicated pause logic\n this.debug('Switched to WebAudio (Paused)');\n } else {\n // Ensure playback rate is 1 if it wasn't paused\n this.bufferSourceNode.playbackRate.value = 1;\n this.debug('Switched to WebAudio (Playing)');\n }\n\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n }\n\n // public-ish functions\n pause(): void {\n this.debug('pause command received');\n if (this.isUsingWebAudio) {\n this.pauseWebAudio();\n } else {\n this.audio.pause();\n this.cancelProgressFrame(); // Stop progress updates\n }\n }\n\n private pauseWebAudio(): void {\n if (!this.audioContext || !this.bufferSourceNode || this.isPaused) {\n // Already paused or not ready\n return;\n }\n this.webAudioPausedAt = this.audioContext.currentTime;\n // Instead of setting playbackRate to 0, which can cause issues,\n // we disconnect the gain node. Reconnect on play.\n this.disconnectGainNode();\n // We keep the onended listener active even when paused via disconnect.\n // Setting playbackRate to 0 might be needed for specific effects, but disconnect is safer for pausing.\n // this.bufferSourceNode.playbackRate.value = 0; // Avoid if possible\n this.debug(`WebAudio paused at ${this.webAudioPausedAt}`);\n this.cancelProgressFrame(); // Stop progress updates\n }\n\n play(): void {\n this.debug('play command received');\n\n // --- Web Audio Path ---\n if (this.audioBuffer && this.audioContext && !this.queue.state.webAudioIsDisabled) {\n // If already using WebAudio and it's ready\n if (this.isUsingWebAudio) {\n this.playWebAudio();\n }\n // If HTML5 is playing/paused but WebAudio buffer is ready, switch\n else if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADED) {\n this.debug('WebAudio buffer ready, switching from HTML5...');\n this.switchToWebAudio(); // This will handle starting playback\n this.requestProgressFrame(); // Start progress updates after switch\n }\n // If WebAudio is loading, play HTML5 for now and switch when ready\n else if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADING) {\n this.debug('WebAudio loading, playing HTML5 temporarily...');\n this.playHtml5Audio();\n }\n // If WebAudio hasn't started loading, start loading and play HTML5\n else {\n this.debug('WebAudio not loaded, starting load and playing HTML5...');\n this.preload(); // Start WebAudio load\n this.playHtml5Audio();\n }\n }\n // --- HTML5 Audio Path ---\n else {\n this.playHtml5Audio();\n }\n\n // Try to preload the next track (can be HTML5 or WebAudio)\n this.queue.loadTrack(this.idx + 1);\n }\n\n private playWebAudio(): void {\n if (!this.audioContext || !this.bufferSourceNode || !this.isPaused) {\n // Already playing or not ready\n return;\n }\n\n if (this.webAudioPausedAt > 0) {\n const pauseDuration = this.audioContext.currentTime - this.webAudioPausedAt;\n this.webAudioPausedDuration += pauseDuration;\n this.debug(\n `Resuming WebAudio after ${pauseDuration.toFixed(2)}s pause. Total paused: ${this.webAudioPausedDuration.toFixed(2)}s`\n );\n }\n\n // Reconnect the gain node to the destination to resume sound\n this.connectGainNode();\n // Ensure playback rate is 1 (might not be necessary if using disconnect method)\n // this.bufferSourceNode.playbackRate.value = 1;\n\n // Reset pause timestamp\n this.webAudioPausedAt = 0;\n\n this.debug('WebAudio playing');\n this.requestProgressFrame(); // Start progress updates\n }\n\n private playHtml5Audio(): void {\n if (!this.audio.paused) return; // Already playing\n\n // Ensure preload is 'auto' before playing\n if (this.audio.preload !== 'auto') {\n this.audio.preload = 'auto';\n }\n\n const playPromise = this.audio.play();\n if (playPromise !== undefined) {\n playPromise\n .then((_) => {\n // Playback started successfully\n this.debug('HTML5 playing');\n this.requestProgressFrame(); // Start progress updates\n // If WebAudio isn't disabled and hasn't loaded/started loading, trigger load\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.preload(); // Start WebAudio load in background\n }\n })\n .catch((error) => {\n console.error(`Error playing HTML5 audio for track ${this.idx}:`, error);\n // Handle playback error (e.g., user interaction needed)\n this.boundAudioOnError(`Playback error: ${error.message}`);\n });\n } else {\n // Fallback for older browsers where play() doesn't return a promise\n // Assume playback starts, though errors might not be catchable here.\n this.debug('HTML5 playing (no promise)');\n this.requestProgressFrame();\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.preload();\n }\n }\n }\n\n togglePlayPause(): void {\n if (this.isPaused) {\n this.play();\n } else {\n this.pause();\n }\n }\n\n preload(loadHTML5: boolean = false): void {\n this.debug(`preload called, loadHTML5: ${loadHTML5}`);\n // Preload HTML5 if requested and not already loading/loaded\n if (loadHTML5 && this.audio.preload !== 'auto' && this.audio.readyState < 2) {\n // readyState < HAVE_CURRENT_DATA\n this.debug('preloading HTML5');\n this.audio.preload = 'auto';\n // Note: 'auto' is just a hint, browser decides how much to load.\n // Calling load() might be more explicit if needed: this.audio.load();\n }\n\n // Preload WebAudio if enabled and not already loading/loaded\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.debug('preloading WebAudio buffer');\n if (this.skipHEAD) {\n this.loadBuffer();\n } else {\n // Ensure HEAD request completes before loading buffer\n this.loadHEAD(() => this.loadBuffer());\n }\n }\n }\n\n // TODO: add checks for to > duration or null or negative (duration - to)\n seek(to: number = 0): void {\n const currentDuration = this.duration;\n if (isNaN(currentDuration) || currentDuration <= 0) {\n this.debug('Cannot seek: duration unknown or invalid.');\n return;\n }\n // Clamp seek time to valid range [0, duration]\n const seekTime = Math.max(0, Math.min(to, currentDuration));\n this.debug(`seek command to: ${seekTime} (original: ${to})`);\n\n if (this.isUsingWebAudio && this.audioContext) {\n this.seekBufferSourceNode(seekTime);\n } else {\n // Check if HTML5 audio is ready to seek\n if (this.audio.readyState >= this.audio.HAVE_METADATA) {\n // HAVE_METADATA or higher\n this.audio.currentTime = seekTime;\n } else {\n this.debug('Cannot seek HTML5: not ready.');\n // Optionally, queue the seek until readyState changes\n }\n }\n\n // Update progress immediately after seek\n this.onProgress(); // Call directly to update state\n }\n\n private seekBufferSourceNode(to: number): void {\n if (!this.audioContext || !this.audioBuffer || !this.gainNode) {\n this.debug('Cannot seek WebAudio: context, buffer, or gain node missing.');\n return;\n }\n\n const wasPaused = this.isPaused; // Check state *before* stopping\n this.debug(`Seeking WebAudio to ${to}. Was paused: ${wasPaused}`);\n\n // Stop the current node\n this.stopAndDisconnectSourceNode();\n\n // Create a new source node\n this.bufferSourceNode = this.createBufferSourceNode();\n if (!this.bufferSourceNode) {\n this.debug('Failed to create buffer source node for seek');\n return;\n }\n\n // Update timing references *before* starting the new node\n this.webAudioStartedPlayingAt = this.audioContext.currentTime - to;\n this.webAudioPausedDuration = 0; // Reset pause duration on seek\n this.webAudioPausedAt = 0; // Reset pause timestamp\n\n // Start the new node at the desired offset\n try {\n this.bufferSourceNode.start(0, to);\n this.debug(\n `WebAudio started after seek at context time ${this.audioContext.currentTime}, track time ${to}`\n );\n } catch (e) {\n console.error('Error starting buffer source node after seek:', e);\n this.disconnectGainNode(); // Disconnect gain if start failed\n return;\n }\n\n // Re-apply paused state if necessary\n if (wasPaused) {\n this.pauseWebAudio(); // Use dedicated pause logic\n this.debug('Re-applied paused state after seek.');\n } else {\n // Ensure gain is connected if it wasn't paused\n this.connectGainNode();\n this.debug('Resumed playing state after seek.');\n this.requestProgressFrame(); // Ensure progress updates resume if it was playing\n }\n }\n\n private stopAndDisconnectSourceNode(): void {\n if (this.bufferSourceNode) {\n try {\n this.bufferSourceNode.onended = null; // Remove listener before stopping\n this.bufferSourceNode.stop();\n this.bufferSourceNode.disconnect(); // Disconnect from gain node\n this.debug('Stopped and disconnected previous source node.');\n } catch (e: any) {\n // Ignore errors like \"invalid state\" if node already stopped\n if (e.name !== 'InvalidStateError') {\n console.error('Error stopping buffer source node:', e);\n }\n }\n this.bufferSourceNode = null;\n }\n }\n\n private connectGainNode(): void {\n // Connects gain node to audio context destination if not already connected\n if (this.gainNode && this.audioContext) {\n try {\n // Check connection state if possible (not standard API)\n // As a simple approach, just connect. Redundant connections are usually harmless.\n this.gainNode.connect(this.audioContext.destination);\n // this.debug(\"Gain node connected to destination.\");\n } catch (e) {\n console.error('Error connecting gain node:', e);\n }\n }\n }\n\n private disconnectGainNode(): void {\n // Disconnects gain node from audio context destination\n if (this.gainNode && this.audioContext) {\n try {\n this.gainNode.disconnect(this.audioContext.destination);\n // this.debug(\"Gain node disconnected from destination.\");\n } catch (e) {\n // Ignore errors if already disconnected\n // console.error(\"Error disconnecting gain node:\", e);\n }\n }\n }\n\n // basic event handlers\n private audioOnError = (e: Event | string): void => {\n let errorDetails = e;\n if (typeof e !== 'string' && e.target) {\n const mediaError = (e.target as HTMLAudioElement).error;\n errorDetails = `HTML5 Audio Error: code=${mediaError?.code}, message=${mediaError?.message}`;\n }\n this.debug('audioOnError', errorDetails);\n // Potentially trigger a queue-level error handler\n // this.queue.handleError(this, errorDetails);\n };\n\n private onEnded(from?: string | Event): void {\n this.debug(\n 'onEnded triggered',\n `from: ${typeof from === 'string' ? from : 'event'}`,\n `isActive: ${this.isActiveTrack}`\n );\n\n // Prevent multiple triggers if event fires close together\n if (this.bufferSourceNode) {\n this.bufferSourceNode.onended = null; // Clear listener immediately\n }\n this.audio.onended = null; // Clear listener\n\n this.cancelProgressFrame(); // Stop progress updates\n\n // Only trigger next track if this track *was* the active one when it ended\n if (this.isActiveTrack) {\n this.queue.playNext(); // Let the queue handle playing the next track\n // The queue's onEnded prop should be called by playNext or the queue itself\n // this.queue.onEnded(); // Avoid calling this directly here, let queue manage its state\n } else {\n this.debug('onEnded ignored for inactive track');\n // Reset state for this inactive track if needed\n this.resetStateAfterEnded();\n }\n }\n\n private resetStateAfterEnded(): void {\n // Reset timing for WebAudio\n this.webAudioStartedPlayingAt = 0;\n this.webAudioPausedDuration = 0;\n this.webAudioPausedAt = 0;\n // Consider resetting bufferSourceNode if WebAudio was used\n this.stopAndDisconnectSourceNode();\n // Reset HTML5 time\n if (this.audio.readyState > 0) {\n this.audio.currentTime = 0;\n }\n // Re-attach listeners if needed for future plays\n this.audio.onended = () => this.boundOnEnded('HTML5');\n if (this.bufferSourceNode && this.audioContext) {\n // Re-create node only when play is called next time\n }\n }\n\n private onProgress(): void {\n // This function runs inside requestAnimationFrame, avoid heavy computation\n\n // Ensure the track is still the active one and is playing\n if (!this.isActiveTrack || this.isPaused) {\n this.cancelProgressFrame(); // Stop updates if paused or changed track\n return;\n }\n\n // --- Calculations (keep efficient) ---\n const currentTime = this.currentTime;\n const duration = this.duration;\n\n // Check for valid numbers before proceeding\n if (isNaN(currentTime) || isNaN(duration) || duration <= 0) {\n this.requestProgressFrame(); // Continue trying\n return;\n }\n\n // --- Preloading Logic ---\n const isWithinLastTwentyFiveSeconds = duration - currentTime <= 25;\n const nextTrack = this.queue.nextTrack;\n\n if (isWithinLastTwentyFiveSeconds && nextTrack && !nextTrack.isLoaded) {\n // Only preload HTML5 here for faster availability, WebAudio preload is handled elsewhere\n this.queue.loadTrack(this.idx + 1, true);\n }\n\n // --- Callbacks ---\n // Call queue's progress handler (throttle this if it causes performance issues)\n this.queue.onProgress(this);\n\n // --- Schedule Next Frame ---\n this.requestProgressFrame();\n }\n\n private requestProgressFrame(): void {\n // Ensure only one frame is scheduled\n this.cancelProgressFrame();\n this.progressFrameId = window.requestAnimationFrame(this.boundOnProgress);\n }\n\n private cancelProgressFrame(): void {\n if (this.progressFrameId !== null) {\n window.cancelAnimationFrame(this.progressFrameId);\n this.progressFrameId = null;\n }\n }\n\n setVolume(nextVolume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, nextVolume));\n this.audio.volume = clampedVolume;\n if (this.gainNode) {\n // Use setValueAtTime for smoother transitions if needed, but direct set is often fine\n this.gainNode.gain.value = clampedVolume;\n }\n }\n\n // getter helpers\n get isUsingWebAudio(): boolean {\n return (\n this.playbackType === GaplessPlaybackType.WEBAUDIO && !this.queue.state.webAudioIsDisabled\n );\n }\n\n get isPaused(): boolean {\n if (this.isUsingWebAudio && this.audioContext) {\n // Check if gain node is disconnected (our pause method) OR if pausedAt is set\n // Checking connection state directly isn't reliable across browsers.\n // Rely on our webAudioPausedAt flag.\n return this.webAudioPausedAt > 0;\n // Alternative check (less reliable if using disconnect):\n // return this.bufferSourceNode ? this.bufferSourceNode.playbackRate.value === 0 : true;\n } else {\n return this.audio.paused;\n }\n }\n\n get currentTime(): number {\n if (this.isUsingWebAudio && this.audioContext) {\n if (this.webAudioPausedAt > 0) {\n // If paused, time is frozen at the point it was paused\n return this.webAudioStartedPlayingAt > 0\n ? this.webAudioPausedAt - this.webAudioStartedPlayingAt - this.webAudioPausedDuration\n : 0;\n } else if (this.webAudioStartedPlayingAt > 0) {\n // If playing, calculate current time\n return (\n this.audioContext.currentTime -\n this.webAudioStartedPlayingAt -\n this.webAudioPausedDuration\n );\n } else {\n // If not started yet (e.g., just loaded, before play)\n return 0;\n }\n } else {\n return this.audio.currentTime;\n }\n }\n\n get duration(): number {\n if (this.isUsingWebAudio && this.audioBuffer) {\n return this.audioBuffer.duration;\n } else {\n // Return NaN if duration is not available (consistent with HTMLMediaElement)\n return this.audio.duration;\n }\n }\n\n get isActiveTrack(): boolean {\n return this.queue.currentTrack === this;\n }\n\n get isLoaded(): boolean {\n // Consider loaded if either WebAudio buffer is ready OR HTML5 has enough data\n if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADED) {\n return true;\n }\n // HTML5 readyState >= HAVE_FUTURE_DATA indicates enough data for smooth playback\n if (this.audio.readyState >= this.audio.HAVE_FUTURE_DATA) {\n return true;\n }\n return false;\n }\n\n get state(): TrackState {\n return {\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n };\n }\n\n get completeState(): TrackCompleteState {\n return {\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n isPaused: this.isPaused,\n currentTime: this.currentTime,\n duration: this.duration,\n idx: this.idx,\n id: this.metadata?.trackId, // Access safely\n };\n }\n\n // debug helper\n debug(first: string, ...args: any[]): void {\n console.debug(\n `[Track ${this.idx} | ${this.playbackType} | ${this.webAudioLoadingState}] ${first}`,\n ...args,\n this.completeState\n );\n }\n\n // just a helper to quick jump to the end of a track for testing\n seekToEnd(secondsFromEnd: number = 6): void {\n const targetDuration = this.duration;\n if (!isNaN(targetDuration) && targetDuration > secondsFromEnd) {\n this.seek(targetDuration - secondsFromEnd);\n } else {\n this.debug(`Cannot seekToEnd: duration invalid or too short (${targetDuration})`);\n }\n }\n}\n","import Track from './Track';\n\nconst PRELOAD_NUM_TRACKS = 2;\n\nconst isBrowser: boolean = typeof window !== 'undefined';\nconst audioContext: AudioContext | null =\n isBrowser && typeof window.AudioContext !== 'undefined' ? new window.AudioContext() : null;\n\n// Define interfaces for props and state\ninterface QueueProps {\n tracks?: string[];\n onProgress?: (track: Track) => void;\n onEnded?: () => void;\n onPlayNextTrack?: (track: Track | undefined) => void;\n onPlayPreviousTrack?: (track: Track | undefined) => void;\n onStartNewTrack?: (track: Track | undefined) => void;\n webAudioIsDisabled?: boolean;\n}\n\ninterface QueueState {\n volume: number;\n currentTrackIdx: number;\n webAudioIsDisabled: boolean;\n}\n\ninterface AddTrackParams {\n trackUrl: string;\n skipHEAD?: boolean;\n metadata?: Record<string, any>;\n}\n\nexport default class Queue {\n props: Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>; // Make callbacks required but omit others handled differently\n state: QueueState;\n tracks: Track[];\n // Track property is just holding the class itself, which is unusual.\n // If it's meant for instantiation elsewhere, it's fine, but often not needed.\n Track: typeof Track;\n\n constructor(props: QueueProps = {}) {\n const {\n tracks = [],\n onProgress = () => {},\n onEnded = () => {},\n onPlayNextTrack = () => {},\n onPlayPreviousTrack = () => {},\n onStartNewTrack = () => {},\n webAudioIsDisabled = false,\n } = props;\n\n this.props = {\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n };\n\n this.state = {\n volume: 1,\n currentTrackIdx: 0,\n webAudioIsDisabled,\n };\n\n this.Track = Track; // Assigning the class itself\n\n this.tracks = tracks.map(\n (trackUrl: string, idx: number) =>\n new Track({\n trackUrl,\n idx,\n queue: this,\n metadata: {}, // Provide default empty metadata\n })\n );\n\n // if the browser doesn't support web audio\n // disable it!\n if (!audioContext) {\n this.disableWebAudio();\n }\n }\n\n addTrack({ trackUrl, skipHEAD, metadata = {} }: AddTrackParams): void {\n this.tracks.push(\n new Track({\n trackUrl,\n skipHEAD,\n metadata,\n idx: this.tracks.length,\n queue: this,\n })\n );\n }\n\n removeTrack(track: Track): Track[] {\n const index = this.tracks.indexOf(track);\n if (index > -1) {\n return this.tracks.splice(index, 1);\n }\n return [];\n }\n\n togglePlayPause(): void {\n if (this.currentTrack) this.currentTrack.togglePlayPause();\n }\n\n play(): void {\n if (this.currentTrack) this.currentTrack.play();\n }\n\n pause(): void {\n if (this.currentTrack) this.currentTrack.pause();\n }\n\n playPrevious(): void {\n if (this.currentTrack && this.currentTrack.currentTime > 8) {\n this.currentTrack.seek(0);\n return;\n }\n\n this.resetCurrentTrack();\n\n if (--this.state.currentTrackIdx < 0) this.state.currentTrackIdx = 0;\n\n // No need to reset again here, play() will handle starting the new current track\n // this.resetCurrentTrack();\n\n this.play(); // This will play the new currentTrack\n\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n if (this.props.onPlayPreviousTrack) this.props.onPlayPreviousTrack(this.currentTrack);\n }\n\n playNext(): void {\n this.resetCurrentTrack(); // Pause and reset the current one\n\n // Ensure we don't go beyond the last track index\n if (this.state.currentTrackIdx < this.tracks.length - 1) {\n this.state.currentTrackIdx++;\n } else {\n // Optional: handle queue end (e.g., stop, loop, etc.)\n // For now, just stay on the last track or reset index if looping\n // this.state.currentTrackIdx = 0; // Example: loop back to start\n this.props.onEnded(); // Call the main onEnded callback\n return; // Stop execution if at the end and not looping\n }\n\n // No need to reset again here\n // this.resetCurrentTrack();\n\n this.play(); // Play the new current track\n\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n if (this.props.onPlayNextTrack) this.props.onPlayNextTrack(this.currentTrack);\n }\n\n resetCurrentTrack(): void {\n if (this.currentTrack) {\n // Check if seek and pause are necessary/safe\n try {\n if (!this.currentTrack.isPaused) {\n this.currentTrack.pause();\n }\n // Only seek if duration is valid\n if (this.currentTrack.duration > 0 && !isNaN(this.currentTrack.duration)) {\n this.currentTrack.seek(0);\n }\n } catch (error) {\n console.error('Error resetting track:', error, this.currentTrack);\n }\n }\n }\n\n pauseAll(): void {\n // Use forEach for side effects, map is for creating new arrays\n this.tracks.forEach((track: Track) => {\n track.pause();\n });\n }\n\n cleanUp(): void {\n // Correctly reference 'track' instead of 'player'\n this.tracks.forEach((track: Track) => {\n // Ensure nodes exist before trying to nullify buffer\n if (track.bufferSourceNode && track.bufferSourceNode.buffer) {\n track.bufferSourceNode.buffer = null; // Release buffer reference\n }\n if (track.audioBuffer) {\n track.audioBuffer = null; // Release internal buffer reference\n }\n // Optional: Stop and disconnect nodes if necessary\n try {\n if (track.bufferSourceNode) {\n track.bufferSourceNode.onended = null; // Remove listener\n track.bufferSourceNode.stop();\n track.bufferSourceNode.disconnect();\n }\n if (track.gainNode && audioContext) {\n track.gainNode.disconnect();\n }\n if (track.audio) {\n track.audio.pause();\n track.audio.src = ''; // Release resource\n track.audio.load();\n track.audio.onended = null;\n track.audio.onerror = null;\n }\n } catch (e) {\n console.error('Error during track cleanup:', e, track);\n }\n });\n // Consider clearing the tracks array if the queue itself is being destroyed\n // this.tracks = [];\n }\n\n gotoTrack(idx: number, playImmediately: boolean = false): void {\n if (idx < 0 || idx >= this.tracks.length) {\n console.warn(`gotoTrack: Index ${idx} out of bounds.`);\n return;\n }\n this.pauseAll(); // Pause potentially playing track\n this.resetCurrentTrack(); // Reset the state of the outgoing track\n\n this.state.currentTrackIdx = idx;\n\n // Reset the new current track before playing (if needed, though play should handle it)\n // this.resetCurrentTrack(); // Might be redundant if play() handles starting correctly\n\n if (playImmediately) {\n this.play();\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n }\n }\n\n loadTrack(idx: number, loadHTML5?: boolean): void {\n // only preload if song is within the next PRELOAD_NUM_TRACKS\n if (\n idx < 0 ||\n idx >= this.tracks.length ||\n this.state.currentTrackIdx + PRELOAD_NUM_TRACKS < idx\n )\n return;\n const track = this.tracks[idx];\n\n if (track) track.preload(loadHTML5);\n }\n\n setProps(obj: Partial<Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>> = {}): void {\n this.props = { ...this.props, ...obj };\n }\n\n // These seem redundant if the props callbacks are called directly elsewhere\n // Keep if they add logic, otherwise call props directly\n onEnded(): void {\n if (this.props.onEnded) this.props.onEnded();\n }\n\n onProgress(track: Track): void {\n if (this.props.onProgress) this.props.onProgress(track);\n }\n\n get currentTrack(): Track | undefined {\n return this.tracks[this.state.currentTrackIdx];\n }\n\n get nextTrack(): Track | undefined {\n return this.tracks[this.state.currentTrackIdx + 1];\n }\n\n disableWebAudio(): void {\n this.state.webAudioIsDisabled = true;\n // Potentially update existing tracks if needed\n this.tracks.forEach((track) => {\n if (track.isUsingWebAudio) {\n // Handle transition back to HTML5 if possible/necessary\n console.warn('Web Audio disabled while track was using it. State might be inconsistent.');\n }\n });\n }\n\n setVolume(nextVolume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, nextVolume)); // Clamp between 0 and 1\n\n this.state.volume = clampedVolume;\n\n this.tracks.forEach((track) => track.setVolume(clampedVolume));\n }\n}\n"],"mappings":"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","names":["index_exports","__export","Queue","__toCommonJS","isBrowser","audioContext","Track","trackUrl","skipHEAD","queue","idx","metadata","from","e","audioContext","cb","options","res","err","arrayBuffer","buffer","node","forcePause","wasPaused","currentTime","pauseDuration","playPromise","_","error","loadHTML5","to","currentDuration","seekTime","errorDetails","mediaError","duration","isWithinLastTwentyFiveSeconds","nextTrack","nextVolume","clampedVolume","_a","first","args","secondsFromEnd","targetDuration","PRELOAD_NUM_TRACKS","isBrowser","audioContext","Queue","props","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","webAudioIsDisabled","Track","trackUrl","idx","skipHEAD","metadata","track","index","error","e","playImmediately","loadHTML5","obj","nextVolume","clampedVolume"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
declare enum GaplessPlaybackType {
|
|
2
|
+
HTML5 = "HTML5",
|
|
3
|
+
WEBAUDIO = "WEBAUDIO"
|
|
4
|
+
}
|
|
5
|
+
declare enum GaplessPlaybackLoadingState {
|
|
6
|
+
NONE = "NONE",
|
|
7
|
+
LOADING = "LOADING",
|
|
8
|
+
LOADED = "LOADED"
|
|
9
|
+
}
|
|
10
|
+
interface TrackProps {
|
|
11
|
+
trackUrl: string;
|
|
12
|
+
skipHEAD?: boolean;
|
|
13
|
+
queue: Queue;
|
|
14
|
+
idx: number;
|
|
15
|
+
metadata?: Record<string, any>;
|
|
16
|
+
}
|
|
17
|
+
interface TrackState {
|
|
18
|
+
playbackType: GaplessPlaybackType;
|
|
19
|
+
webAudioLoadingState: GaplessPlaybackLoadingState;
|
|
20
|
+
}
|
|
21
|
+
interface TrackCompleteState extends TrackState {
|
|
22
|
+
isPaused: boolean;
|
|
23
|
+
currentTime: number;
|
|
24
|
+
duration: number;
|
|
25
|
+
idx: number;
|
|
26
|
+
id?: any;
|
|
27
|
+
}
|
|
28
|
+
declare class Track {
|
|
29
|
+
playbackType: GaplessPlaybackType;
|
|
30
|
+
webAudioLoadingState: GaplessPlaybackLoadingState;
|
|
31
|
+
loadedHEAD: boolean;
|
|
32
|
+
idx: number;
|
|
33
|
+
queue: Queue;
|
|
34
|
+
trackUrl: string;
|
|
35
|
+
skipHEAD?: boolean;
|
|
36
|
+
metadata: Record<string, any>;
|
|
37
|
+
audio: HTMLAudioElement;
|
|
38
|
+
audioContext: AudioContext | null;
|
|
39
|
+
gainNode: GainNode | null;
|
|
40
|
+
bufferSourceNode: AudioBufferSourceNode | null;
|
|
41
|
+
audioBuffer: AudioBuffer | null;
|
|
42
|
+
webAudioStartedPlayingAt: number;
|
|
43
|
+
webAudioPausedDuration: number;
|
|
44
|
+
webAudioPausedAt: number;
|
|
45
|
+
private boundOnEnded;
|
|
46
|
+
private boundOnProgress;
|
|
47
|
+
private boundAudioOnError;
|
|
48
|
+
private progressFrameId;
|
|
49
|
+
constructor({ trackUrl, skipHEAD, queue, idx, metadata }: TrackProps);
|
|
50
|
+
private loadHEAD;
|
|
51
|
+
private loadBuffer;
|
|
52
|
+
private createBufferSourceNode;
|
|
53
|
+
private switchToWebAudio;
|
|
54
|
+
pause(): void;
|
|
55
|
+
private pauseWebAudio;
|
|
56
|
+
play(): void;
|
|
57
|
+
private playWebAudio;
|
|
58
|
+
private playHtml5Audio;
|
|
59
|
+
togglePlayPause(): void;
|
|
60
|
+
preload(loadHTML5?: boolean): void;
|
|
61
|
+
seek(to?: number): void;
|
|
62
|
+
private seekBufferSourceNode;
|
|
63
|
+
private stopAndDisconnectSourceNode;
|
|
64
|
+
private connectGainNode;
|
|
65
|
+
private disconnectGainNode;
|
|
66
|
+
private audioOnError;
|
|
67
|
+
private onEnded;
|
|
68
|
+
private resetStateAfterEnded;
|
|
69
|
+
private onProgress;
|
|
70
|
+
private requestProgressFrame;
|
|
71
|
+
private cancelProgressFrame;
|
|
72
|
+
setVolume(nextVolume: number): void;
|
|
73
|
+
get isUsingWebAudio(): boolean;
|
|
74
|
+
get isPaused(): boolean;
|
|
75
|
+
get currentTime(): number;
|
|
76
|
+
get duration(): number;
|
|
77
|
+
get isActiveTrack(): boolean;
|
|
78
|
+
get isLoaded(): boolean;
|
|
79
|
+
get state(): TrackState;
|
|
80
|
+
get completeState(): TrackCompleteState;
|
|
81
|
+
debug(first: string, ...args: any[]): void;
|
|
82
|
+
seekToEnd(secondsFromEnd?: number): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface QueueProps {
|
|
86
|
+
tracks?: string[];
|
|
87
|
+
onProgress?: (track: Track) => void;
|
|
88
|
+
onEnded?: () => void;
|
|
89
|
+
onPlayNextTrack?: (track: Track | undefined) => void;
|
|
90
|
+
onPlayPreviousTrack?: (track: Track | undefined) => void;
|
|
91
|
+
onStartNewTrack?: (track: Track | undefined) => void;
|
|
92
|
+
webAudioIsDisabled?: boolean;
|
|
93
|
+
}
|
|
94
|
+
interface QueueState {
|
|
95
|
+
volume: number;
|
|
96
|
+
currentTrackIdx: number;
|
|
97
|
+
webAudioIsDisabled: boolean;
|
|
98
|
+
}
|
|
99
|
+
interface AddTrackParams {
|
|
100
|
+
trackUrl: string;
|
|
101
|
+
skipHEAD?: boolean;
|
|
102
|
+
metadata?: Record<string, any>;
|
|
103
|
+
}
|
|
104
|
+
declare class Queue {
|
|
105
|
+
props: Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>;
|
|
106
|
+
state: QueueState;
|
|
107
|
+
tracks: Track[];
|
|
108
|
+
Track: typeof Track;
|
|
109
|
+
constructor(props?: QueueProps);
|
|
110
|
+
addTrack({ trackUrl, skipHEAD, metadata }: AddTrackParams): void;
|
|
111
|
+
removeTrack(track: Track): Track[];
|
|
112
|
+
togglePlayPause(): void;
|
|
113
|
+
play(): void;
|
|
114
|
+
pause(): void;
|
|
115
|
+
playPrevious(): void;
|
|
116
|
+
playNext(): void;
|
|
117
|
+
resetCurrentTrack(): void;
|
|
118
|
+
pauseAll(): void;
|
|
119
|
+
cleanUp(): void;
|
|
120
|
+
gotoTrack(idx: number, playImmediately?: boolean): void;
|
|
121
|
+
loadTrack(idx: number, loadHTML5?: boolean): void;
|
|
122
|
+
setProps(obj?: Partial<Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>>): void;
|
|
123
|
+
onEnded(): void;
|
|
124
|
+
onProgress(track: Track): void;
|
|
125
|
+
get currentTrack(): Track | undefined;
|
|
126
|
+
get nextTrack(): Track | undefined;
|
|
127
|
+
disableWebAudio(): void;
|
|
128
|
+
setVolume(nextVolume: number): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { Queue as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var f=typeof window<"u",p=f&&(window.AudioContext||window.webkitAudioContext)?new(window.AudioContext||window.webkitAudioContext):null;var o=class{playbackType;webAudioLoadingState;loadedHEAD;idx;queue;trackUrl;skipHEAD;metadata;audio;audioContext;gainNode;bufferSourceNode;audioBuffer;webAudioStartedPlayingAt;webAudioPausedDuration;webAudioPausedAt;boundOnEnded;boundOnProgress;boundAudioOnError;progressFrameId=null;constructor({trackUrl:e,skipHEAD:t,queue:i,idx:s,metadata:d={}}){this.playbackType="HTML5",this.webAudioLoadingState="NONE",this.loadedHEAD=!1,this.idx=s,this.queue=i,this.trackUrl=e,this.skipHEAD=t,this.metadata=d,this.boundOnEnded=a=>this.onEnded(a),this.boundOnProgress=()=>this.onProgress(),this.boundAudioOnError=a=>this.audioOnError(a),this.audio=new Audio,this.audio.onerror=this.boundAudioOnError,this.audio.onended=()=>this.boundOnEnded("HTML5"),this.audio.controls=!1,this.audio.volume=i.state.volume,this.audio.preload="none",this.audio.src=e,this.audioContext=i.state.webAudioIsDisabled?null:p,this.gainNode=null,this.bufferSourceNode=null,this.audioBuffer=null,this.webAudioStartedPlayingAt=0,this.webAudioPausedDuration=0,this.webAudioPausedAt=0,this.audioContext&&(this.gainNode=this.audioContext.createGain(),this.gainNode.gain.value=i.state.volume)}loadHEAD(e){if(this.loadedHEAD||this.skipHEAD){e();return}let t={method:"HEAD"};fetch(this.trackUrl,t).then(i=>{i.redirected&&i.url&&(this.trackUrl=i.url,this.audio.src!==this.trackUrl&&this.audio.readyState===0&&(this.audio.src=this.trackUrl)),this.loadedHEAD=!0,e()}).catch(i=>{console.error(`HEAD request failed for track ${this.idx}:`,i),e()})}loadBuffer(e){!this.audioContext||this.webAudioLoadingState!=="NONE"||this.queue.state.webAudioIsDisabled||(this.webAudioLoadingState="LOADING",this.debug("starting download"),fetch(this.trackUrl).then(t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return t.arrayBuffer()}).then(t=>this.audioContext.decodeAudioData(t,i=>{this.debug("finished decoding track"),this.webAudioLoadingState="LOADED",this.audioBuffer=i,this.queue.loadTrack(this.idx+1),this.isActiveTrack&&this.playbackType==="HTML5"&&!this.isPaused?this.switchToWebAudio():this.isActiveTrack&&this.playbackType==="HTML5"&&this.isPaused?(this.playbackType="WEBAUDIO",this.debug("WebAudio ready for paused track")):this.isActiveTrack||(this.playbackType="WEBAUDIO",this.debug("WebAudio ready for inactive track")),e==null||e(i)},i=>{console.error(`Error decoding audio data for track ${this.idx}:`,i),this.webAudioLoadingState="NONE"})).catch(t=>{this.debug("caught fetch/decode error",t),this.webAudioLoadingState="NONE"}))}createBufferSourceNode(){if(!this.audioContext||!this.audioBuffer||!this.gainNode)return null;let e=this.audioContext.createBufferSource();return e.buffer=this.audioBuffer,e.connect(this.gainNode),e.onended=()=>this.boundOnEnded("webaudio_auto"),e}switchToWebAudio(e=!1){if(!this.audioContext||!this.audioBuffer||!this.gainNode||this.webAudioLoadingState!=="LOADED"){this.debug("Cannot switch to WebAudio: not ready.");return}if(!this.isActiveTrack&&!e){this.debug("Cannot switch to WebAudio: not active track.");return}let t=this.audio.paused,i=this.audio.currentTime;if(this.debug("Attempting switch to web audio",`currentTime: ${i}`,`wasPaused: ${t}`,`HTML5 duration: ${this.audio.duration}`,`WebAudio duration: ${this.audioBuffer.duration}`),this.audio.pause(),this.stopAndDisconnectSourceNode(),this.bufferSourceNode=this.createBufferSourceNode(),!this.bufferSourceNode){this.debug("Failed to create buffer source node for switch"),this.playbackType="HTML5";return}this.connectGainNode(),this.webAudioStartedPlayingAt=this.audioContext.currentTime-i,this.webAudioPausedDuration=0,this.webAudioPausedAt=0;try{this.bufferSourceNode.start(0,i),this.debug(`WebAudio started at context time ${this.audioContext.currentTime}, track time ${i}`)}catch(s){console.error("Error starting buffer source node:",s),this.playbackType="HTML5",this.disconnectGainNode();return}t||e?(this.pauseWebAudio(),this.debug("Switched to WebAudio (Paused)")):(this.bufferSourceNode.playbackRate.value=1,this.debug("Switched to WebAudio (Playing)")),this.playbackType="WEBAUDIO"}pause(){this.debug("pause command received"),this.isUsingWebAudio?this.pauseWebAudio():(this.audio.pause(),this.cancelProgressFrame())}pauseWebAudio(){!this.audioContext||!this.bufferSourceNode||this.isPaused||(this.webAudioPausedAt=this.audioContext.currentTime,this.disconnectGainNode(),this.debug(`WebAudio paused at ${this.webAudioPausedAt}`),this.cancelProgressFrame())}play(){this.debug("play command received"),this.audioBuffer&&this.audioContext&&!this.queue.state.webAudioIsDisabled?this.isUsingWebAudio?this.playWebAudio():this.webAudioLoadingState==="LOADED"?(this.debug("WebAudio buffer ready, switching from HTML5..."),this.switchToWebAudio(),this.requestProgressFrame()):this.webAudioLoadingState==="LOADING"?(this.debug("WebAudio loading, playing HTML5 temporarily..."),this.playHtml5Audio()):(this.debug("WebAudio not loaded, starting load and playing HTML5..."),this.preload(),this.playHtml5Audio()):this.playHtml5Audio(),this.queue.loadTrack(this.idx+1)}playWebAudio(){if(!(!this.audioContext||!this.bufferSourceNode||!this.isPaused)){if(this.webAudioPausedAt>0){let e=this.audioContext.currentTime-this.webAudioPausedAt;this.webAudioPausedDuration+=e,this.debug(`Resuming WebAudio after ${e.toFixed(2)}s pause. Total paused: ${this.webAudioPausedDuration.toFixed(2)}s`)}this.connectGainNode(),this.webAudioPausedAt=0,this.debug("WebAudio playing"),this.requestProgressFrame()}}playHtml5Audio(){if(!this.audio.paused)return;this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e!==void 0?e.then(t=>{this.debug("HTML5 playing"),this.requestProgressFrame(),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&this.preload()}).catch(t=>{console.error(`Error playing HTML5 audio for track ${this.idx}:`,t),this.boundAudioOnError(`Playback error: ${t.message}`)}):(this.debug("HTML5 playing (no promise)"),this.requestProgressFrame(),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&this.preload())}togglePlayPause(){this.isPaused?this.play():this.pause()}preload(e=!1){this.debug(`preload called, loadHTML5: ${e}`),e&&this.audio.preload!=="auto"&&this.audio.readyState<2&&(this.debug("preloading HTML5"),this.audio.preload="auto"),!this.queue.state.webAudioIsDisabled&&this.webAudioLoadingState==="NONE"&&(this.debug("preloading WebAudio buffer"),this.skipHEAD?this.loadBuffer():this.loadHEAD(()=>this.loadBuffer()))}seek(e=0){let t=this.duration;if(isNaN(t)||t<=0){this.debug("Cannot seek: duration unknown or invalid.");return}let i=Math.max(0,Math.min(e,t));this.debug(`seek command to: ${i} (original: ${e})`),this.isUsingWebAudio&&this.audioContext?this.seekBufferSourceNode(i):this.audio.readyState>=this.audio.HAVE_METADATA?this.audio.currentTime=i:this.debug("Cannot seek HTML5: not ready."),this.onProgress()}seekBufferSourceNode(e){if(!this.audioContext||!this.audioBuffer||!this.gainNode){this.debug("Cannot seek WebAudio: context, buffer, or gain node missing.");return}let t=this.isPaused;if(this.debug(`Seeking WebAudio to ${e}. Was paused: ${t}`),this.stopAndDisconnectSourceNode(),this.bufferSourceNode=this.createBufferSourceNode(),!this.bufferSourceNode){this.debug("Failed to create buffer source node for seek");return}this.webAudioStartedPlayingAt=this.audioContext.currentTime-e,this.webAudioPausedDuration=0,this.webAudioPausedAt=0;try{this.bufferSourceNode.start(0,e),this.debug(`WebAudio started after seek at context time ${this.audioContext.currentTime}, track time ${e}`)}catch(i){console.error("Error starting buffer source node after seek:",i),this.disconnectGainNode();return}t?(this.pauseWebAudio(),this.debug("Re-applied paused state after seek.")):(this.connectGainNode(),this.debug("Resumed playing state after seek."),this.requestProgressFrame())}stopAndDisconnectSourceNode(){if(this.bufferSourceNode){try{this.bufferSourceNode.onended=null,this.bufferSourceNode.stop(),this.bufferSourceNode.disconnect(),this.debug("Stopped and disconnected previous source node.")}catch(e){e.name!=="InvalidStateError"&&console.error("Error stopping buffer source node:",e)}this.bufferSourceNode=null}}connectGainNode(){if(this.gainNode&&this.audioContext)try{this.gainNode.connect(this.audioContext.destination)}catch(e){console.error("Error connecting gain node:",e)}}disconnectGainNode(){if(this.gainNode&&this.audioContext)try{this.gainNode.disconnect(this.audioContext.destination)}catch{}}audioOnError=e=>{let t=e;if(typeof e!="string"&&e.target){let i=e.target.error;t=`HTML5 Audio Error: code=${i==null?void 0:i.code}, message=${i==null?void 0:i.message}`}this.debug("audioOnError",t)};onEnded(e){this.debug("onEnded triggered",`from: ${typeof e=="string"?e:"event"}`,`isActive: ${this.isActiveTrack}`),this.bufferSourceNode&&(this.bufferSourceNode.onended=null),this.audio.onended=null,this.cancelProgressFrame(),this.isActiveTrack?this.queue.playNext():(this.debug("onEnded ignored for inactive track"),this.resetStateAfterEnded())}resetStateAfterEnded(){this.webAudioStartedPlayingAt=0,this.webAudioPausedDuration=0,this.webAudioPausedAt=0,this.stopAndDisconnectSourceNode(),this.audio.readyState>0&&(this.audio.currentTime=0),this.audio.onended=()=>this.boundOnEnded("HTML5"),this.bufferSourceNode&&this.audioContext}onProgress(){if(!this.isActiveTrack||this.isPaused){this.cancelProgressFrame();return}let e=this.currentTime,t=this.duration;if(isNaN(e)||isNaN(t)||t<=0){this.requestProgressFrame();return}let i=t-e<=25,s=this.queue.nextTrack;i&&s&&!s.isLoaded&&this.queue.loadTrack(this.idx+1,!0),this.queue.onProgress(this),this.requestProgressFrame()}requestProgressFrame(){this.cancelProgressFrame(),this.progressFrameId=window.requestAnimationFrame(this.boundOnProgress)}cancelProgressFrame(){this.progressFrameId!==null&&(window.cancelAnimationFrame(this.progressFrameId),this.progressFrameId=null)}setVolume(e){let t=Math.max(0,Math.min(1,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t)}get isUsingWebAudio(){return this.playbackType==="WEBAUDIO"&&!this.queue.state.webAudioIsDisabled}get isPaused(){return this.isUsingWebAudio&&this.audioContext?this.webAudioPausedAt>0:this.audio.paused}get currentTime(){return this.isUsingWebAudio&&this.audioContext?this.webAudioPausedAt>0?this.webAudioStartedPlayingAt>0?this.webAudioPausedAt-this.webAudioStartedPlayingAt-this.webAudioPausedDuration:0:this.webAudioStartedPlayingAt>0?this.audioContext.currentTime-this.webAudioStartedPlayingAt-this.webAudioPausedDuration:0:this.audio.currentTime}get duration(){return this.isUsingWebAudio&&this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isActiveTrack(){return this.queue.currentTrack===this}get isLoaded(){return this.webAudioLoadingState==="LOADED"||this.audio.readyState>=this.audio.HAVE_FUTURE_DATA}get state(){return{playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState}}get completeState(){var e;return{playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,isPaused:this.isPaused,currentTime:this.currentTime,duration:this.duration,idx:this.idx,id:(e=this.metadata)==null?void 0:e.trackId}}debug(e,...t){console.debug(`[Track ${this.idx} | ${this.playbackType} | ${this.webAudioLoadingState}] ${e}`,...t,this.completeState)}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e?this.seek(t-e):this.debug(`Cannot seekToEnd: duration invalid or too short (${t})`)}};var g=2,A=typeof window<"u",u=A&&typeof window.AudioContext<"u"?new window.AudioContext:null,r=class{props;state;tracks;Track;constructor(e={}){let{tracks:t=[],onProgress:i=()=>{},onEnded:s=()=>{},onPlayNextTrack:d=()=>{},onPlayPreviousTrack:a=()=>{},onStartNewTrack:h=()=>{},webAudioIsDisabled:c=!1}=e;this.props={onProgress:i,onEnded:s,onPlayNextTrack:d,onPlayPreviousTrack:a,onStartNewTrack:h},this.state={volume:1,currentTrackIdx:0,webAudioIsDisabled:c},this.Track=o,this.tracks=t.map((l,b)=>new o({trackUrl:l,idx:b,queue:this,metadata:{}})),u||this.disableWebAudio()}addTrack({trackUrl:e,skipHEAD:t,metadata:i={}}){this.tracks.push(new o({trackUrl:e,skipHEAD:t,metadata:i,idx:this.tracks.length,queue:this}))}removeTrack(e){let t=this.tracks.indexOf(e);return t>-1?this.tracks.splice(t,1):[]}togglePlayPause(){this.currentTrack&&this.currentTrack.togglePlayPause()}play(){this.currentTrack&&this.currentTrack.play()}pause(){this.currentTrack&&this.currentTrack.pause()}playPrevious(){if(this.currentTrack&&this.currentTrack.currentTime>8){this.currentTrack.seek(0);return}this.resetCurrentTrack(),--this.state.currentTrackIdx<0&&(this.state.currentTrackIdx=0),this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack),this.props.onPlayPreviousTrack&&this.props.onPlayPreviousTrack(this.currentTrack)}playNext(){if(this.resetCurrentTrack(),this.state.currentTrackIdx<this.tracks.length-1)this.state.currentTrackIdx++;else{this.props.onEnded();return}this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack),this.props.onPlayNextTrack&&this.props.onPlayNextTrack(this.currentTrack)}resetCurrentTrack(){if(this.currentTrack)try{this.currentTrack.isPaused||this.currentTrack.pause(),this.currentTrack.duration>0&&!isNaN(this.currentTrack.duration)&&this.currentTrack.seek(0)}catch(e){console.error("Error resetting track:",e,this.currentTrack)}}pauseAll(){this.tracks.forEach(e=>{e.pause()})}cleanUp(){this.tracks.forEach(e=>{e.bufferSourceNode&&e.bufferSourceNode.buffer&&(e.bufferSourceNode.buffer=null),e.audioBuffer&&(e.audioBuffer=null);try{e.bufferSourceNode&&(e.bufferSourceNode.onended=null,e.bufferSourceNode.stop(),e.bufferSourceNode.disconnect()),e.gainNode&&u&&e.gainNode.disconnect(),e.audio&&(e.audio.pause(),e.audio.src="",e.audio.load(),e.audio.onended=null,e.audio.onerror=null)}catch(t){console.error("Error during track cleanup:",t,e)}})}gotoTrack(e,t=!1){if(e<0||e>=this.tracks.length){console.warn(`gotoTrack: Index ${e} out of bounds.`);return}this.pauseAll(),this.resetCurrentTrack(),this.state.currentTrackIdx=e,t&&(this.play(),this.props.onStartNewTrack&&this.props.onStartNewTrack(this.currentTrack))}loadTrack(e,t){if(e<0||e>=this.tracks.length||this.state.currentTrackIdx+g<e)return;let i=this.tracks[e];i&&i.preload(t)}setProps(e={}){this.props={...this.props,...e}}onEnded(){this.props.onEnded&&this.props.onEnded()}onProgress(e){this.props.onProgress&&this.props.onProgress(e)}get currentTrack(){return this.tracks[this.state.currentTrackIdx]}get nextTrack(){return this.tracks[this.state.currentTrackIdx+1]}disableWebAudio(){this.state.webAudioIsDisabled=!0,this.tracks.forEach(e=>{e.isUsingWebAudio&&console.warn("Web Audio disabled while track was using it. State might be inconsistent.")})}setVolume(e){let t=Math.max(0,Math.min(1,e));this.state.volume=t,this.tracks.forEach(i=>i.setVolume(t))}};export{r as default};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Track.ts","../src/Queue.ts"],"sourcesContent":["import Queue from './Queue';\n\nconst isBrowser: boolean = typeof window !== 'undefined';\nconst audioContext: AudioContext | null =\n isBrowser && (window.AudioContext || (window as any).webkitAudioContext)\n ? new (window.AudioContext || (window as any).webkitAudioContext)()\n : null;\n\n// Use Enums for better type safety\nenum GaplessPlaybackType {\n HTML5 = 'HTML5',\n WEBAUDIO = 'WEBAUDIO',\n}\n\nenum GaplessPlaybackLoadingState {\n NONE = 'NONE',\n LOADING = 'LOADING',\n LOADED = 'LOADED',\n}\n\ninterface TrackProps {\n trackUrl: string;\n skipHEAD?: boolean;\n queue: Queue;\n idx: number;\n metadata?: Record<string, any>;\n}\n\ninterface TrackState {\n playbackType: GaplessPlaybackType;\n webAudioLoadingState: GaplessPlaybackLoadingState;\n}\n\ninterface TrackCompleteState extends TrackState {\n isPaused: boolean;\n currentTime: number;\n duration: number;\n idx: number;\n id?: any; // Assuming metadata.trackId can be any type\n}\n\nexport default class Track {\n // Playback state\n playbackType: GaplessPlaybackType;\n webAudioLoadingState: GaplessPlaybackLoadingState;\n loadedHEAD: boolean;\n\n // Basic info\n idx: number;\n queue: Queue;\n trackUrl: string;\n skipHEAD?: boolean;\n metadata: Record<string, any>;\n\n // HTML5 Audio\n audio: HTMLAudioElement;\n\n // WebAudio API elements (nullable)\n audioContext: AudioContext | null;\n gainNode: GainNode | null;\n bufferSourceNode: AudioBufferSourceNode | null;\n audioBuffer: AudioBuffer | null;\n\n // WebAudio timing state\n webAudioStartedPlayingAt: number; // Time from audioContext.currentTime when playback started\n webAudioPausedDuration: number; // Total duration spent paused\n webAudioPausedAt: number; // Timestamp (audioContext.currentTime) when paused\n\n // Bound methods for event listeners\n private boundOnEnded: (from?: string | Event) => void;\n private boundOnProgress: () => void;\n private boundAudioOnError: (e: Event | string) => void;\n\n private progressFrameId: number | null = null; // Store requestAnimationFrame ID\n\n constructor({ trackUrl, skipHEAD, queue, idx, metadata = {} }: TrackProps) {\n // playback type state\n this.playbackType = GaplessPlaybackType.HTML5;\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE;\n this.loadedHEAD = false;\n\n // basic inputs from Queue\n this.idx = idx;\n this.queue = queue;\n this.trackUrl = trackUrl;\n this.skipHEAD = skipHEAD;\n this.metadata = metadata;\n\n // Bind methods to ensure 'this' context\n this.boundOnEnded = (from?: string | Event) => this.onEnded(from);\n this.boundOnProgress = () => this.onProgress();\n this.boundAudioOnError = (e: Event | string) => this.audioOnError(e);\n\n // HTML5 Audio\n this.audio = new Audio();\n this.audio.onerror = this.boundAudioOnError;\n this.audio.onended = () => this.boundOnEnded('HTML5'); // Use bound method\n this.audio.controls = false;\n this.audio.volume = queue.state.volume;\n this.audio.preload = 'none'; // Explicitly 'none' initially\n this.audio.src = trackUrl;\n // this.audio.onprogress = () => this.debug(this.idx, this.audio.buffered)\n\n // WebAudio Initialization (only if supported and not disabled)\n this.audioContext = queue.state.webAudioIsDisabled ? null : audioContext;\n this.gainNode = null;\n this.bufferSourceNode = null;\n this.audioBuffer = null;\n this.webAudioStartedPlayingAt = 0;\n this.webAudioPausedDuration = 0;\n this.webAudioPausedAt = 0;\n\n if (this.audioContext) {\n this.gainNode = this.audioContext.createGain();\n this.gainNode.gain.value = queue.state.volume;\n // Don't create bufferSourceNode until needed\n }\n }\n\n // private functions\n private loadHEAD(cb: () => void): void {\n if (this.loadedHEAD || this.skipHEAD) {\n cb();\n return;\n }\n\n const options: RequestInit = {\n method: 'HEAD',\n };\n\n fetch(this.trackUrl, options)\n .then((res) => {\n if (res.redirected && res.url) {\n this.trackUrl = res.url;\n // If URL changed, might need to update HTMLAudioElement src?\n // Only if not already playing/loaded via HTML5.\n if (this.audio.src !== this.trackUrl && this.audio.readyState === 0) {\n this.audio.src = this.trackUrl;\n }\n }\n this.loadedHEAD = true;\n cb();\n })\n .catch((err) => {\n console.error(`HEAD request failed for track ${this.idx}:`, err);\n // Decide how to proceed, maybe try loading directly?\n cb(); // Or maybe call an error handler\n });\n }\n\n private loadBuffer(cb?: (buffer: AudioBuffer) => void): void {\n if (\n !this.audioContext ||\n this.webAudioLoadingState !== GaplessPlaybackLoadingState.NONE ||\n this.queue.state.webAudioIsDisabled\n ) {\n return;\n }\n\n this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADING;\n this.debug('starting download');\n\n fetch(this.trackUrl)\n .then((res) => {\n if (!res.ok) {\n throw new Error(`HTTP error! status: ${res.status}`);\n }\n return res.arrayBuffer();\n })\n .then((arrayBuffer) =>\n this.audioContext!.decodeAudioData(\n // Use non-null assertion as we checked audioContext\n arrayBuffer,\n (buffer) => {\n this.debug('finished decoding track');\n\n this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADED;\n this.audioBuffer = buffer;\n\n // Create and connect the source node *now* that we have the buffer\n // Don't connect gainNode to destination until play()\n // this.bufferSourceNode = this.createBufferSourceNode(); // Moved node creation\n\n // try to preload next track (WebAudio buffer)\n this.queue.loadTrack(this.idx + 1);\n\n // if we loaded the active track, switch to web audio if it was playing HTML5\n if (\n this.isActiveTrack &&\n this.playbackType === GaplessPlaybackType.HTML5 &&\n !this.isPaused\n ) {\n this.switchToWebAudio();\n } else if (\n this.isActiveTrack &&\n this.playbackType === GaplessPlaybackType.HTML5 &&\n this.isPaused\n ) {\n // If it was paused HTML5, just mark ready for WebAudio, don't auto-play\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n this.debug('WebAudio ready for paused track');\n } else if (!this.isActiveTrack) {\n // If it's not the active track, just mark ready\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n this.debug('WebAudio ready for inactive track');\n }\n\n cb?.(buffer);\n },\n (err) => {\n console.error(`Error decoding audio data for track ${this.idx}:`, err);\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE; // Reset state on decode error\n }\n )\n )\n .catch((e) => {\n this.debug('caught fetch/decode error', e);\n this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE; // Reset state on fetch error\n });\n }\n\n private createBufferSourceNode(): AudioBufferSourceNode | null {\n if (!this.audioContext || !this.audioBuffer || !this.gainNode) return null;\n\n const node = this.audioContext.createBufferSource();\n node.buffer = this.audioBuffer;\n node.connect(this.gainNode); // Connect source to gain\n node.onended = () => this.boundOnEnded('webaudio_auto'); // Use bound method\n return node;\n }\n\n private switchToWebAudio(forcePause: boolean = false): void {\n // Ensure WebAudio is ready and we are the active track (unless forced)\n if (\n !this.audioContext ||\n !this.audioBuffer ||\n !this.gainNode ||\n this.webAudioLoadingState !== GaplessPlaybackLoadingState.LOADED\n ) {\n this.debug('Cannot switch to WebAudio: not ready.');\n return;\n }\n if (!this.isActiveTrack && !forcePause) {\n this.debug('Cannot switch to WebAudio: not active track.');\n return;\n }\n\n const wasPaused = this.audio.paused; // State *before* switching\n const currentTime = this.audio.currentTime; // Time *before* switching\n\n this.debug(\n 'Attempting switch to web audio',\n `currentTime: ${currentTime}`,\n `wasPaused: ${wasPaused}`,\n `HTML5 duration: ${this.audio.duration}`,\n `WebAudio duration: ${this.audioBuffer.duration}`\n );\n\n // Stop HTML5 audio\n this.audio.pause();\n\n // Disconnect previous WebAudio node if exists\n this.stopAndDisconnectSourceNode();\n\n // Create and configure the new source node\n this.bufferSourceNode = this.createBufferSourceNode();\n if (!this.bufferSourceNode) {\n this.debug('Failed to create buffer source node for switch');\n // Revert? Or stay paused?\n this.playbackType = GaplessPlaybackType.HTML5; // Revert type\n return;\n }\n\n // Connect gain to destination *before* starting\n this.connectGainNode();\n\n // Calculate start time for WebAudio context\n this.webAudioStartedPlayingAt = this.audioContext.currentTime - currentTime;\n this.webAudioPausedDuration = 0; // Reset pause duration\n this.webAudioPausedAt = 0; // Reset pause timestamp\n\n // Start the buffer source\n try {\n this.bufferSourceNode.start(0, currentTime);\n this.debug(\n `WebAudio started at context time ${this.audioContext.currentTime}, track time ${currentTime}`\n );\n } catch (e) {\n console.error('Error starting buffer source node:', e);\n this.playbackType = GaplessPlaybackType.HTML5; // Revert type on error\n this.disconnectGainNode(); // Disconnect gain if start failed\n return;\n }\n\n // Handle initial pause state\n if (wasPaused || forcePause) {\n this.pauseWebAudio(); // Use dedicated pause logic\n this.debug('Switched to WebAudio (Paused)');\n } else {\n // Ensure playback rate is 1 if it wasn't paused\n this.bufferSourceNode.playbackRate.value = 1;\n this.debug('Switched to WebAudio (Playing)');\n }\n\n this.playbackType = GaplessPlaybackType.WEBAUDIO;\n }\n\n // public-ish functions\n pause(): void {\n this.debug('pause command received');\n if (this.isUsingWebAudio) {\n this.pauseWebAudio();\n } else {\n this.audio.pause();\n this.cancelProgressFrame(); // Stop progress updates\n }\n }\n\n private pauseWebAudio(): void {\n if (!this.audioContext || !this.bufferSourceNode || this.isPaused) {\n // Already paused or not ready\n return;\n }\n this.webAudioPausedAt = this.audioContext.currentTime;\n // Instead of setting playbackRate to 0, which can cause issues,\n // we disconnect the gain node. Reconnect on play.\n this.disconnectGainNode();\n // We keep the onended listener active even when paused via disconnect.\n // Setting playbackRate to 0 might be needed for specific effects, but disconnect is safer for pausing.\n // this.bufferSourceNode.playbackRate.value = 0; // Avoid if possible\n this.debug(`WebAudio paused at ${this.webAudioPausedAt}`);\n this.cancelProgressFrame(); // Stop progress updates\n }\n\n play(): void {\n this.debug('play command received');\n\n // --- Web Audio Path ---\n if (this.audioBuffer && this.audioContext && !this.queue.state.webAudioIsDisabled) {\n // If already using WebAudio and it's ready\n if (this.isUsingWebAudio) {\n this.playWebAudio();\n }\n // If HTML5 is playing/paused but WebAudio buffer is ready, switch\n else if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADED) {\n this.debug('WebAudio buffer ready, switching from HTML5...');\n this.switchToWebAudio(); // This will handle starting playback\n this.requestProgressFrame(); // Start progress updates after switch\n }\n // If WebAudio is loading, play HTML5 for now and switch when ready\n else if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADING) {\n this.debug('WebAudio loading, playing HTML5 temporarily...');\n this.playHtml5Audio();\n }\n // If WebAudio hasn't started loading, start loading and play HTML5\n else {\n this.debug('WebAudio not loaded, starting load and playing HTML5...');\n this.preload(); // Start WebAudio load\n this.playHtml5Audio();\n }\n }\n // --- HTML5 Audio Path ---\n else {\n this.playHtml5Audio();\n }\n\n // Try to preload the next track (can be HTML5 or WebAudio)\n this.queue.loadTrack(this.idx + 1);\n }\n\n private playWebAudio(): void {\n if (!this.audioContext || !this.bufferSourceNode || !this.isPaused) {\n // Already playing or not ready\n return;\n }\n\n if (this.webAudioPausedAt > 0) {\n const pauseDuration = this.audioContext.currentTime - this.webAudioPausedAt;\n this.webAudioPausedDuration += pauseDuration;\n this.debug(\n `Resuming WebAudio after ${pauseDuration.toFixed(2)}s pause. Total paused: ${this.webAudioPausedDuration.toFixed(2)}s`\n );\n }\n\n // Reconnect the gain node to the destination to resume sound\n this.connectGainNode();\n // Ensure playback rate is 1 (might not be necessary if using disconnect method)\n // this.bufferSourceNode.playbackRate.value = 1;\n\n // Reset pause timestamp\n this.webAudioPausedAt = 0;\n\n this.debug('WebAudio playing');\n this.requestProgressFrame(); // Start progress updates\n }\n\n private playHtml5Audio(): void {\n if (!this.audio.paused) return; // Already playing\n\n // Ensure preload is 'auto' before playing\n if (this.audio.preload !== 'auto') {\n this.audio.preload = 'auto';\n }\n\n const playPromise = this.audio.play();\n if (playPromise !== undefined) {\n playPromise\n .then((_) => {\n // Playback started successfully\n this.debug('HTML5 playing');\n this.requestProgressFrame(); // Start progress updates\n // If WebAudio isn't disabled and hasn't loaded/started loading, trigger load\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.preload(); // Start WebAudio load in background\n }\n })\n .catch((error) => {\n console.error(`Error playing HTML5 audio for track ${this.idx}:`, error);\n // Handle playback error (e.g., user interaction needed)\n this.boundAudioOnError(`Playback error: ${error.message}`);\n });\n } else {\n // Fallback for older browsers where play() doesn't return a promise\n // Assume playback starts, though errors might not be catchable here.\n this.debug('HTML5 playing (no promise)');\n this.requestProgressFrame();\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.preload();\n }\n }\n }\n\n togglePlayPause(): void {\n if (this.isPaused) {\n this.play();\n } else {\n this.pause();\n }\n }\n\n preload(loadHTML5: boolean = false): void {\n this.debug(`preload called, loadHTML5: ${loadHTML5}`);\n // Preload HTML5 if requested and not already loading/loaded\n if (loadHTML5 && this.audio.preload !== 'auto' && this.audio.readyState < 2) {\n // readyState < HAVE_CURRENT_DATA\n this.debug('preloading HTML5');\n this.audio.preload = 'auto';\n // Note: 'auto' is just a hint, browser decides how much to load.\n // Calling load() might be more explicit if needed: this.audio.load();\n }\n\n // Preload WebAudio if enabled and not already loading/loaded\n if (\n !this.queue.state.webAudioIsDisabled &&\n this.webAudioLoadingState === GaplessPlaybackLoadingState.NONE\n ) {\n this.debug('preloading WebAudio buffer');\n if (this.skipHEAD) {\n this.loadBuffer();\n } else {\n // Ensure HEAD request completes before loading buffer\n this.loadHEAD(() => this.loadBuffer());\n }\n }\n }\n\n // TODO: add checks for to > duration or null or negative (duration - to)\n seek(to: number = 0): void {\n const currentDuration = this.duration;\n if (isNaN(currentDuration) || currentDuration <= 0) {\n this.debug('Cannot seek: duration unknown or invalid.');\n return;\n }\n // Clamp seek time to valid range [0, duration]\n const seekTime = Math.max(0, Math.min(to, currentDuration));\n this.debug(`seek command to: ${seekTime} (original: ${to})`);\n\n if (this.isUsingWebAudio && this.audioContext) {\n this.seekBufferSourceNode(seekTime);\n } else {\n // Check if HTML5 audio is ready to seek\n if (this.audio.readyState >= this.audio.HAVE_METADATA) {\n // HAVE_METADATA or higher\n this.audio.currentTime = seekTime;\n } else {\n this.debug('Cannot seek HTML5: not ready.');\n // Optionally, queue the seek until readyState changes\n }\n }\n\n // Update progress immediately after seek\n this.onProgress(); // Call directly to update state\n }\n\n private seekBufferSourceNode(to: number): void {\n if (!this.audioContext || !this.audioBuffer || !this.gainNode) {\n this.debug('Cannot seek WebAudio: context, buffer, or gain node missing.');\n return;\n }\n\n const wasPaused = this.isPaused; // Check state *before* stopping\n this.debug(`Seeking WebAudio to ${to}. Was paused: ${wasPaused}`);\n\n // Stop the current node\n this.stopAndDisconnectSourceNode();\n\n // Create a new source node\n this.bufferSourceNode = this.createBufferSourceNode();\n if (!this.bufferSourceNode) {\n this.debug('Failed to create buffer source node for seek');\n return;\n }\n\n // Update timing references *before* starting the new node\n this.webAudioStartedPlayingAt = this.audioContext.currentTime - to;\n this.webAudioPausedDuration = 0; // Reset pause duration on seek\n this.webAudioPausedAt = 0; // Reset pause timestamp\n\n // Start the new node at the desired offset\n try {\n this.bufferSourceNode.start(0, to);\n this.debug(\n `WebAudio started after seek at context time ${this.audioContext.currentTime}, track time ${to}`\n );\n } catch (e) {\n console.error('Error starting buffer source node after seek:', e);\n this.disconnectGainNode(); // Disconnect gain if start failed\n return;\n }\n\n // Re-apply paused state if necessary\n if (wasPaused) {\n this.pauseWebAudio(); // Use dedicated pause logic\n this.debug('Re-applied paused state after seek.');\n } else {\n // Ensure gain is connected if it wasn't paused\n this.connectGainNode();\n this.debug('Resumed playing state after seek.');\n this.requestProgressFrame(); // Ensure progress updates resume if it was playing\n }\n }\n\n private stopAndDisconnectSourceNode(): void {\n if (this.bufferSourceNode) {\n try {\n this.bufferSourceNode.onended = null; // Remove listener before stopping\n this.bufferSourceNode.stop();\n this.bufferSourceNode.disconnect(); // Disconnect from gain node\n this.debug('Stopped and disconnected previous source node.');\n } catch (e: any) {\n // Ignore errors like \"invalid state\" if node already stopped\n if (e.name !== 'InvalidStateError') {\n console.error('Error stopping buffer source node:', e);\n }\n }\n this.bufferSourceNode = null;\n }\n }\n\n private connectGainNode(): void {\n // Connects gain node to audio context destination if not already connected\n if (this.gainNode && this.audioContext) {\n try {\n // Check connection state if possible (not standard API)\n // As a simple approach, just connect. Redundant connections are usually harmless.\n this.gainNode.connect(this.audioContext.destination);\n // this.debug(\"Gain node connected to destination.\");\n } catch (e) {\n console.error('Error connecting gain node:', e);\n }\n }\n }\n\n private disconnectGainNode(): void {\n // Disconnects gain node from audio context destination\n if (this.gainNode && this.audioContext) {\n try {\n this.gainNode.disconnect(this.audioContext.destination);\n // this.debug(\"Gain node disconnected from destination.\");\n } catch (e) {\n // Ignore errors if already disconnected\n // console.error(\"Error disconnecting gain node:\", e);\n }\n }\n }\n\n // basic event handlers\n private audioOnError = (e: Event | string): void => {\n let errorDetails = e;\n if (typeof e !== 'string' && e.target) {\n const mediaError = (e.target as HTMLAudioElement).error;\n errorDetails = `HTML5 Audio Error: code=${mediaError?.code}, message=${mediaError?.message}`;\n }\n this.debug('audioOnError', errorDetails);\n // Potentially trigger a queue-level error handler\n // this.queue.handleError(this, errorDetails);\n };\n\n private onEnded(from?: string | Event): void {\n this.debug(\n 'onEnded triggered',\n `from: ${typeof from === 'string' ? from : 'event'}`,\n `isActive: ${this.isActiveTrack}`\n );\n\n // Prevent multiple triggers if event fires close together\n if (this.bufferSourceNode) {\n this.bufferSourceNode.onended = null; // Clear listener immediately\n }\n this.audio.onended = null; // Clear listener\n\n this.cancelProgressFrame(); // Stop progress updates\n\n // Only trigger next track if this track *was* the active one when it ended\n if (this.isActiveTrack) {\n this.queue.playNext(); // Let the queue handle playing the next track\n // The queue's onEnded prop should be called by playNext or the queue itself\n // this.queue.onEnded(); // Avoid calling this directly here, let queue manage its state\n } else {\n this.debug('onEnded ignored for inactive track');\n // Reset state for this inactive track if needed\n this.resetStateAfterEnded();\n }\n }\n\n private resetStateAfterEnded(): void {\n // Reset timing for WebAudio\n this.webAudioStartedPlayingAt = 0;\n this.webAudioPausedDuration = 0;\n this.webAudioPausedAt = 0;\n // Consider resetting bufferSourceNode if WebAudio was used\n this.stopAndDisconnectSourceNode();\n // Reset HTML5 time\n if (this.audio.readyState > 0) {\n this.audio.currentTime = 0;\n }\n // Re-attach listeners if needed for future plays\n this.audio.onended = () => this.boundOnEnded('HTML5');\n if (this.bufferSourceNode && this.audioContext) {\n // Re-create node only when play is called next time\n }\n }\n\n private onProgress(): void {\n // This function runs inside requestAnimationFrame, avoid heavy computation\n\n // Ensure the track is still the active one and is playing\n if (!this.isActiveTrack || this.isPaused) {\n this.cancelProgressFrame(); // Stop updates if paused or changed track\n return;\n }\n\n // --- Calculations (keep efficient) ---\n const currentTime = this.currentTime;\n const duration = this.duration;\n\n // Check for valid numbers before proceeding\n if (isNaN(currentTime) || isNaN(duration) || duration <= 0) {\n this.requestProgressFrame(); // Continue trying\n return;\n }\n\n // --- Preloading Logic ---\n const isWithinLastTwentyFiveSeconds = duration - currentTime <= 25;\n const nextTrack = this.queue.nextTrack;\n\n if (isWithinLastTwentyFiveSeconds && nextTrack && !nextTrack.isLoaded) {\n // Only preload HTML5 here for faster availability, WebAudio preload is handled elsewhere\n this.queue.loadTrack(this.idx + 1, true);\n }\n\n // --- Callbacks ---\n // Call queue's progress handler (throttle this if it causes performance issues)\n this.queue.onProgress(this);\n\n // --- Schedule Next Frame ---\n this.requestProgressFrame();\n }\n\n private requestProgressFrame(): void {\n // Ensure only one frame is scheduled\n this.cancelProgressFrame();\n this.progressFrameId = window.requestAnimationFrame(this.boundOnProgress);\n }\n\n private cancelProgressFrame(): void {\n if (this.progressFrameId !== null) {\n window.cancelAnimationFrame(this.progressFrameId);\n this.progressFrameId = null;\n }\n }\n\n setVolume(nextVolume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, nextVolume));\n this.audio.volume = clampedVolume;\n if (this.gainNode) {\n // Use setValueAtTime for smoother transitions if needed, but direct set is often fine\n this.gainNode.gain.value = clampedVolume;\n }\n }\n\n // getter helpers\n get isUsingWebAudio(): boolean {\n return (\n this.playbackType === GaplessPlaybackType.WEBAUDIO && !this.queue.state.webAudioIsDisabled\n );\n }\n\n get isPaused(): boolean {\n if (this.isUsingWebAudio && this.audioContext) {\n // Check if gain node is disconnected (our pause method) OR if pausedAt is set\n // Checking connection state directly isn't reliable across browsers.\n // Rely on our webAudioPausedAt flag.\n return this.webAudioPausedAt > 0;\n // Alternative check (less reliable if using disconnect):\n // return this.bufferSourceNode ? this.bufferSourceNode.playbackRate.value === 0 : true;\n } else {\n return this.audio.paused;\n }\n }\n\n get currentTime(): number {\n if (this.isUsingWebAudio && this.audioContext) {\n if (this.webAudioPausedAt > 0) {\n // If paused, time is frozen at the point it was paused\n return this.webAudioStartedPlayingAt > 0\n ? this.webAudioPausedAt - this.webAudioStartedPlayingAt - this.webAudioPausedDuration\n : 0;\n } else if (this.webAudioStartedPlayingAt > 0) {\n // If playing, calculate current time\n return (\n this.audioContext.currentTime -\n this.webAudioStartedPlayingAt -\n this.webAudioPausedDuration\n );\n } else {\n // If not started yet (e.g., just loaded, before play)\n return 0;\n }\n } else {\n return this.audio.currentTime;\n }\n }\n\n get duration(): number {\n if (this.isUsingWebAudio && this.audioBuffer) {\n return this.audioBuffer.duration;\n } else {\n // Return NaN if duration is not available (consistent with HTMLMediaElement)\n return this.audio.duration;\n }\n }\n\n get isActiveTrack(): boolean {\n return this.queue.currentTrack === this;\n }\n\n get isLoaded(): boolean {\n // Consider loaded if either WebAudio buffer is ready OR HTML5 has enough data\n if (this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADED) {\n return true;\n }\n // HTML5 readyState >= HAVE_FUTURE_DATA indicates enough data for smooth playback\n if (this.audio.readyState >= this.audio.HAVE_FUTURE_DATA) {\n return true;\n }\n return false;\n }\n\n get state(): TrackState {\n return {\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n };\n }\n\n get completeState(): TrackCompleteState {\n return {\n playbackType: this.playbackType,\n webAudioLoadingState: this.webAudioLoadingState,\n isPaused: this.isPaused,\n currentTime: this.currentTime,\n duration: this.duration,\n idx: this.idx,\n id: this.metadata?.trackId, // Access safely\n };\n }\n\n // debug helper\n debug(first: string, ...args: any[]): void {\n console.debug(\n `[Track ${this.idx} | ${this.playbackType} | ${this.webAudioLoadingState}] ${first}`,\n ...args,\n this.completeState\n );\n }\n\n // just a helper to quick jump to the end of a track for testing\n seekToEnd(secondsFromEnd: number = 6): void {\n const targetDuration = this.duration;\n if (!isNaN(targetDuration) && targetDuration > secondsFromEnd) {\n this.seek(targetDuration - secondsFromEnd);\n } else {\n this.debug(`Cannot seekToEnd: duration invalid or too short (${targetDuration})`);\n }\n }\n}\n","import Track from './Track';\n\nconst PRELOAD_NUM_TRACKS = 2;\n\nconst isBrowser: boolean = typeof window !== 'undefined';\nconst audioContext: AudioContext | null =\n isBrowser && typeof window.AudioContext !== 'undefined' ? new window.AudioContext() : null;\n\n// Define interfaces for props and state\ninterface QueueProps {\n tracks?: string[];\n onProgress?: (track: Track) => void;\n onEnded?: () => void;\n onPlayNextTrack?: (track: Track | undefined) => void;\n onPlayPreviousTrack?: (track: Track | undefined) => void;\n onStartNewTrack?: (track: Track | undefined) => void;\n webAudioIsDisabled?: boolean;\n}\n\ninterface QueueState {\n volume: number;\n currentTrackIdx: number;\n webAudioIsDisabled: boolean;\n}\n\ninterface AddTrackParams {\n trackUrl: string;\n skipHEAD?: boolean;\n metadata?: Record<string, any>;\n}\n\nexport default class Queue {\n props: Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>; // Make callbacks required but omit others handled differently\n state: QueueState;\n tracks: Track[];\n // Track property is just holding the class itself, which is unusual.\n // If it's meant for instantiation elsewhere, it's fine, but often not needed.\n Track: typeof Track;\n\n constructor(props: QueueProps = {}) {\n const {\n tracks = [],\n onProgress = () => {},\n onEnded = () => {},\n onPlayNextTrack = () => {},\n onPlayPreviousTrack = () => {},\n onStartNewTrack = () => {},\n webAudioIsDisabled = false,\n } = props;\n\n this.props = {\n onProgress,\n onEnded,\n onPlayNextTrack,\n onPlayPreviousTrack,\n onStartNewTrack,\n };\n\n this.state = {\n volume: 1,\n currentTrackIdx: 0,\n webAudioIsDisabled,\n };\n\n this.Track = Track; // Assigning the class itself\n\n this.tracks = tracks.map(\n (trackUrl: string, idx: number) =>\n new Track({\n trackUrl,\n idx,\n queue: this,\n metadata: {}, // Provide default empty metadata\n })\n );\n\n // if the browser doesn't support web audio\n // disable it!\n if (!audioContext) {\n this.disableWebAudio();\n }\n }\n\n addTrack({ trackUrl, skipHEAD, metadata = {} }: AddTrackParams): void {\n this.tracks.push(\n new Track({\n trackUrl,\n skipHEAD,\n metadata,\n idx: this.tracks.length,\n queue: this,\n })\n );\n }\n\n removeTrack(track: Track): Track[] {\n const index = this.tracks.indexOf(track);\n if (index > -1) {\n return this.tracks.splice(index, 1);\n }\n return [];\n }\n\n togglePlayPause(): void {\n if (this.currentTrack) this.currentTrack.togglePlayPause();\n }\n\n play(): void {\n if (this.currentTrack) this.currentTrack.play();\n }\n\n pause(): void {\n if (this.currentTrack) this.currentTrack.pause();\n }\n\n playPrevious(): void {\n if (this.currentTrack && this.currentTrack.currentTime > 8) {\n this.currentTrack.seek(0);\n return;\n }\n\n this.resetCurrentTrack();\n\n if (--this.state.currentTrackIdx < 0) this.state.currentTrackIdx = 0;\n\n // No need to reset again here, play() will handle starting the new current track\n // this.resetCurrentTrack();\n\n this.play(); // This will play the new currentTrack\n\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n if (this.props.onPlayPreviousTrack) this.props.onPlayPreviousTrack(this.currentTrack);\n }\n\n playNext(): void {\n this.resetCurrentTrack(); // Pause and reset the current one\n\n // Ensure we don't go beyond the last track index\n if (this.state.currentTrackIdx < this.tracks.length - 1) {\n this.state.currentTrackIdx++;\n } else {\n // Optional: handle queue end (e.g., stop, loop, etc.)\n // For now, just stay on the last track or reset index if looping\n // this.state.currentTrackIdx = 0; // Example: loop back to start\n this.props.onEnded(); // Call the main onEnded callback\n return; // Stop execution if at the end and not looping\n }\n\n // No need to reset again here\n // this.resetCurrentTrack();\n\n this.play(); // Play the new current track\n\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n if (this.props.onPlayNextTrack) this.props.onPlayNextTrack(this.currentTrack);\n }\n\n resetCurrentTrack(): void {\n if (this.currentTrack) {\n // Check if seek and pause are necessary/safe\n try {\n if (!this.currentTrack.isPaused) {\n this.currentTrack.pause();\n }\n // Only seek if duration is valid\n if (this.currentTrack.duration > 0 && !isNaN(this.currentTrack.duration)) {\n this.currentTrack.seek(0);\n }\n } catch (error) {\n console.error('Error resetting track:', error, this.currentTrack);\n }\n }\n }\n\n pauseAll(): void {\n // Use forEach for side effects, map is for creating new arrays\n this.tracks.forEach((track: Track) => {\n track.pause();\n });\n }\n\n cleanUp(): void {\n // Correctly reference 'track' instead of 'player'\n this.tracks.forEach((track: Track) => {\n // Ensure nodes exist before trying to nullify buffer\n if (track.bufferSourceNode && track.bufferSourceNode.buffer) {\n track.bufferSourceNode.buffer = null; // Release buffer reference\n }\n if (track.audioBuffer) {\n track.audioBuffer = null; // Release internal buffer reference\n }\n // Optional: Stop and disconnect nodes if necessary\n try {\n if (track.bufferSourceNode) {\n track.bufferSourceNode.onended = null; // Remove listener\n track.bufferSourceNode.stop();\n track.bufferSourceNode.disconnect();\n }\n if (track.gainNode && audioContext) {\n track.gainNode.disconnect();\n }\n if (track.audio) {\n track.audio.pause();\n track.audio.src = ''; // Release resource\n track.audio.load();\n track.audio.onended = null;\n track.audio.onerror = null;\n }\n } catch (e) {\n console.error('Error during track cleanup:', e, track);\n }\n });\n // Consider clearing the tracks array if the queue itself is being destroyed\n // this.tracks = [];\n }\n\n gotoTrack(idx: number, playImmediately: boolean = false): void {\n if (idx < 0 || idx >= this.tracks.length) {\n console.warn(`gotoTrack: Index ${idx} out of bounds.`);\n return;\n }\n this.pauseAll(); // Pause potentially playing track\n this.resetCurrentTrack(); // Reset the state of the outgoing track\n\n this.state.currentTrackIdx = idx;\n\n // Reset the new current track before playing (if needed, though play should handle it)\n // this.resetCurrentTrack(); // Might be redundant if play() handles starting correctly\n\n if (playImmediately) {\n this.play();\n if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);\n }\n }\n\n loadTrack(idx: number, loadHTML5?: boolean): void {\n // only preload if song is within the next PRELOAD_NUM_TRACKS\n if (\n idx < 0 ||\n idx >= this.tracks.length ||\n this.state.currentTrackIdx + PRELOAD_NUM_TRACKS < idx\n )\n return;\n const track = this.tracks[idx];\n\n if (track) track.preload(loadHTML5);\n }\n\n setProps(obj: Partial<Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>> = {}): void {\n this.props = { ...this.props, ...obj };\n }\n\n // These seem redundant if the props callbacks are called directly elsewhere\n // Keep if they add logic, otherwise call props directly\n onEnded(): void {\n if (this.props.onEnded) this.props.onEnded();\n }\n\n onProgress(track: Track): void {\n if (this.props.onProgress) this.props.onProgress(track);\n }\n\n get currentTrack(): Track | undefined {\n return this.tracks[this.state.currentTrackIdx];\n }\n\n get nextTrack(): Track | undefined {\n return this.tracks[this.state.currentTrackIdx + 1];\n }\n\n disableWebAudio(): void {\n this.state.webAudioIsDisabled = true;\n // Potentially update existing tracks if needed\n this.tracks.forEach((track) => {\n if (track.isUsingWebAudio) {\n // Handle transition back to HTML5 if possible/necessary\n console.warn('Web Audio disabled while track was using it. State might be inconsistent.');\n }\n });\n }\n\n setVolume(nextVolume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, nextVolume)); // Clamp between 0 and 1\n\n this.state.volume = clampedVolume;\n\n this.tracks.forEach((track) => track.setVolume(clampedVolume));\n }\n}\n"],"mappings":"AAEA,IAAMA,EAAqB,OAAO,OAAW,IACvCC,EACJD,IAAc,OAAO,cAAiB,OAAe,oBACjD,IAAK,OAAO,cAAiB,OAAe,oBAC5C,KAmCN,IAAqBE,EAArB,KAA2B,CAEzB,aACA,qBACA,WAGA,IACA,MACA,SACA,SACA,SAGA,MAGA,aACA,SACA,iBACA,YAGA,yBACA,uBACA,iBAGQ,aACA,gBACA,kBAEA,gBAAiC,KAEzC,YAAY,CAAE,SAAAC,EAAU,SAAAC,EAAU,MAAAC,EAAO,IAAAC,EAAK,SAAAC,EAAW,CAAC,CAAE,EAAe,CAEzE,KAAK,aAAe,QACpB,KAAK,qBAAuB,OAC5B,KAAK,WAAa,GAGlB,KAAK,IAAMD,EACX,KAAK,MAAQD,EACb,KAAK,SAAWF,EAChB,KAAK,SAAWC,EAChB,KAAK,SAAWG,EAGhB,KAAK,aAAgBC,GAA0B,KAAK,QAAQA,CAAI,EAChE,KAAK,gBAAkB,IAAM,KAAK,WAAW,EAC7C,KAAK,kBAAqBC,GAAsB,KAAK,aAAaA,CAAC,EAGnE,KAAK,MAAQ,IAAI,MACjB,KAAK,MAAM,QAAU,KAAK,kBAC1B,KAAK,MAAM,QAAU,IAAM,KAAK,aAAa,OAAO,EACpD,KAAK,MAAM,SAAW,GACtB,KAAK,MAAM,OAASJ,EAAM,MAAM,OAChC,KAAK,MAAM,QAAU,OACrB,KAAK,MAAM,IAAMF,EAIjB,KAAK,aAAeE,EAAM,MAAM,mBAAqB,KAAOK,EAC5D,KAAK,SAAW,KAChB,KAAK,iBAAmB,KACxB,KAAK,YAAc,KACnB,KAAK,yBAA2B,EAChC,KAAK,uBAAyB,EAC9B,KAAK,iBAAmB,EAEpB,KAAK,eACP,KAAK,SAAW,KAAK,aAAa,WAAW,EAC7C,KAAK,SAAS,KAAK,MAAQL,EAAM,MAAM,OAG3C,CAGQ,SAASM,EAAsB,CACrC,GAAI,KAAK,YAAc,KAAK,SAAU,CACpCA,EAAG,EACH,MACF,CAEA,IAAMC,EAAuB,CAC3B,OAAQ,MACV,EAEA,MAAM,KAAK,SAAUA,CAAO,EACzB,KAAMC,GAAQ,CACTA,EAAI,YAAcA,EAAI,MACxB,KAAK,SAAWA,EAAI,IAGhB,KAAK,MAAM,MAAQ,KAAK,UAAY,KAAK,MAAM,aAAe,IAChE,KAAK,MAAM,IAAM,KAAK,WAG1B,KAAK,WAAa,GAClBF,EAAG,CACL,CAAC,EACA,MAAOG,GAAQ,CACd,QAAQ,MAAM,iCAAiC,KAAK,GAAG,IAAKA,CAAG,EAE/DH,EAAG,CACL,CAAC,CACL,CAEQ,WAAWA,EAA0C,CAEzD,CAAC,KAAK,cACN,KAAK,uBAAyB,QAC9B,KAAK,MAAM,MAAM,qBAKnB,KAAK,qBAAuB,UAC5B,KAAK,MAAM,mBAAmB,EAE9B,MAAM,KAAK,QAAQ,EAChB,KAAME,GAAQ,CACb,GAAI,CAACA,EAAI,GACP,MAAM,IAAI,MAAM,uBAAuBA,EAAI,MAAM,EAAE,EAErD,OAAOA,EAAI,YAAY,CACzB,CAAC,EACA,KAAME,GACL,KAAK,aAAc,gBAEjBA,EACCC,GAAW,CACV,KAAK,MAAM,yBAAyB,EAEpC,KAAK,qBAAuB,SAC5B,KAAK,YAAcA,EAOnB,KAAK,MAAM,UAAU,KAAK,IAAM,CAAC,EAI/B,KAAK,eACL,KAAK,eAAiB,SACtB,CAAC,KAAK,SAEN,KAAK,iBAAiB,EAEtB,KAAK,eACL,KAAK,eAAiB,SACtB,KAAK,UAGL,KAAK,aAAe,WACpB,KAAK,MAAM,iCAAiC,GAClC,KAAK,gBAEf,KAAK,aAAe,WACpB,KAAK,MAAM,mCAAmC,GAGhDL,GAAA,MAAAA,EAAKK,EACP,EACCF,GAAQ,CACP,QAAQ,MAAM,uCAAuC,KAAK,GAAG,IAAKA,CAAG,EACrE,KAAK,qBAAuB,MAC9B,CACF,CACF,EACC,MAAOL,GAAM,CACZ,KAAK,MAAM,4BAA6BA,CAAC,EACzC,KAAK,qBAAuB,MAC9B,CAAC,EACL,CAEQ,wBAAuD,CAC7D,GAAI,CAAC,KAAK,cAAgB,CAAC,KAAK,aAAe,CAAC,KAAK,SAAU,OAAO,KAEtE,IAAMQ,EAAO,KAAK,aAAa,mBAAmB,EAClD,OAAAA,EAAK,OAAS,KAAK,YACnBA,EAAK,QAAQ,KAAK,QAAQ,EAC1BA,EAAK,QAAU,IAAM,KAAK,aAAa,eAAe,EAC/CA,CACT,CAEQ,iBAAiBC,EAAsB,GAAa,CAE1D,GACE,CAAC,KAAK,cACN,CAAC,KAAK,aACN,CAAC,KAAK,UACN,KAAK,uBAAyB,SAC9B,CACA,KAAK,MAAM,uCAAuC,EAClD,MACF,CACA,GAAI,CAAC,KAAK,eAAiB,CAACA,EAAY,CACtC,KAAK,MAAM,8CAA8C,EACzD,MACF,CAEA,IAAMC,EAAY,KAAK,MAAM,OACvBC,EAAc,KAAK,MAAM,YAkB/B,GAhBA,KAAK,MACH,iCACA,gBAAgBA,CAAW,GAC3B,cAAcD,CAAS,GACvB,mBAAmB,KAAK,MAAM,QAAQ,GACtC,sBAAsB,KAAK,YAAY,QAAQ,EACjD,EAGA,KAAK,MAAM,MAAM,EAGjB,KAAK,4BAA4B,EAGjC,KAAK,iBAAmB,KAAK,uBAAuB,EAChD,CAAC,KAAK,iBAAkB,CAC1B,KAAK,MAAM,gDAAgD,EAE3D,KAAK,aAAe,QACpB,MACF,CAGA,KAAK,gBAAgB,EAGrB,KAAK,yBAA2B,KAAK,aAAa,YAAcC,EAChE,KAAK,uBAAyB,EAC9B,KAAK,iBAAmB,EAGxB,GAAI,CACF,KAAK,iBAAiB,MAAM,EAAGA,CAAW,EAC1C,KAAK,MACH,oCAAoC,KAAK,aAAa,WAAW,gBAAgBA,CAAW,EAC9F,CACF,OAASX,EAAG,CACV,QAAQ,MAAM,qCAAsCA,CAAC,EACrD,KAAK,aAAe,QACpB,KAAK,mBAAmB,EACxB,MACF,CAGIU,GAAaD,GACf,KAAK,cAAc,EACnB,KAAK,MAAM,+BAA+B,IAG1C,KAAK,iBAAiB,aAAa,MAAQ,EAC3C,KAAK,MAAM,gCAAgC,GAG7C,KAAK,aAAe,UACtB,CAGA,OAAc,CACZ,KAAK,MAAM,wBAAwB,EAC/B,KAAK,gBACP,KAAK,cAAc,GAEnB,KAAK,MAAM,MAAM,EACjB,KAAK,oBAAoB,EAE7B,CAEQ,eAAsB,CACxB,CAAC,KAAK,cAAgB,CAAC,KAAK,kBAAoB,KAAK,WAIzD,KAAK,iBAAmB,KAAK,aAAa,YAG1C,KAAK,mBAAmB,EAIxB,KAAK,MAAM,sBAAsB,KAAK,gBAAgB,EAAE,EACxD,KAAK,oBAAoB,EAC3B,CAEA,MAAa,CACX,KAAK,MAAM,uBAAuB,EAG9B,KAAK,aAAe,KAAK,cAAgB,CAAC,KAAK,MAAM,MAAM,mBAEzD,KAAK,gBACP,KAAK,aAAa,EAGX,KAAK,uBAAyB,UACrC,KAAK,MAAM,gDAAgD,EAC3D,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAGnB,KAAK,uBAAyB,WACrC,KAAK,MAAM,gDAAgD,EAC3D,KAAK,eAAe,IAIpB,KAAK,MAAM,yDAAyD,EACpE,KAAK,QAAQ,EACb,KAAK,eAAe,GAKtB,KAAK,eAAe,EAItB,KAAK,MAAM,UAAU,KAAK,IAAM,CAAC,CACnC,CAEQ,cAAqB,CAC3B,GAAI,GAAC,KAAK,cAAgB,CAAC,KAAK,kBAAoB,CAAC,KAAK,UAK1D,IAAI,KAAK,iBAAmB,EAAG,CAC7B,IAAMG,EAAgB,KAAK,aAAa,YAAc,KAAK,iBAC3D,KAAK,wBAA0BA,EAC/B,KAAK,MACH,2BAA2BA,EAAc,QAAQ,CAAC,CAAC,0BAA0B,KAAK,uBAAuB,QAAQ,CAAC,CAAC,GACrH,CACF,CAGA,KAAK,gBAAgB,EAKrB,KAAK,iBAAmB,EAExB,KAAK,MAAM,kBAAkB,EAC7B,KAAK,qBAAqB,EAC5B,CAEQ,gBAAuB,CAC7B,GAAI,CAAC,KAAK,MAAM,OAAQ,OAGpB,KAAK,MAAM,UAAY,SACzB,KAAK,MAAM,QAAU,QAGvB,IAAMC,EAAc,KAAK,MAAM,KAAK,EAChCA,IAAgB,OAClBA,EACG,KAAMC,GAAM,CAEX,KAAK,MAAM,eAAe,EAC1B,KAAK,qBAAqB,EAGxB,CAAC,KAAK,MAAM,MAAM,oBAClB,KAAK,uBAAyB,QAE9B,KAAK,QAAQ,CAEjB,CAAC,EACA,MAAOC,GAAU,CAChB,QAAQ,MAAM,uCAAuC,KAAK,GAAG,IAAKA,CAAK,EAEvE,KAAK,kBAAkB,mBAAmBA,EAAM,OAAO,EAAE,CAC3D,CAAC,GAIH,KAAK,MAAM,4BAA4B,EACvC,KAAK,qBAAqB,EAExB,CAAC,KAAK,MAAM,MAAM,oBAClB,KAAK,uBAAyB,QAE9B,KAAK,QAAQ,EAGnB,CAEA,iBAAwB,CAClB,KAAK,SACP,KAAK,KAAK,EAEV,KAAK,MAAM,CAEf,CAEA,QAAQC,EAAqB,GAAa,CACxC,KAAK,MAAM,8BAA8BA,CAAS,EAAE,EAEhDA,GAAa,KAAK,MAAM,UAAY,QAAU,KAAK,MAAM,WAAa,IAExE,KAAK,MAAM,kBAAkB,EAC7B,KAAK,MAAM,QAAU,QAOrB,CAAC,KAAK,MAAM,MAAM,oBAClB,KAAK,uBAAyB,SAE9B,KAAK,MAAM,4BAA4B,EACnC,KAAK,SACP,KAAK,WAAW,EAGhB,KAAK,SAAS,IAAM,KAAK,WAAW,CAAC,EAG3C,CAGA,KAAKC,EAAa,EAAS,CACzB,IAAMC,EAAkB,KAAK,SAC7B,GAAI,MAAMA,CAAe,GAAKA,GAAmB,EAAG,CAClD,KAAK,MAAM,2CAA2C,EACtD,MACF,CAEA,IAAMC,EAAW,KAAK,IAAI,EAAG,KAAK,IAAIF,EAAIC,CAAe,CAAC,EAC1D,KAAK,MAAM,oBAAoBC,CAAQ,eAAeF,CAAE,GAAG,EAEvD,KAAK,iBAAmB,KAAK,aAC/B,KAAK,qBAAqBE,CAAQ,EAG9B,KAAK,MAAM,YAAc,KAAK,MAAM,cAEtC,KAAK,MAAM,YAAcA,EAEzB,KAAK,MAAM,+BAA+B,EAM9C,KAAK,WAAW,CAClB,CAEQ,qBAAqBF,EAAkB,CAC7C,GAAI,CAAC,KAAK,cAAgB,CAAC,KAAK,aAAe,CAAC,KAAK,SAAU,CAC7D,KAAK,MAAM,8DAA8D,EACzE,MACF,CAEA,IAAMP,EAAY,KAAK,SAQvB,GAPA,KAAK,MAAM,uBAAuBO,CAAE,iBAAiBP,CAAS,EAAE,EAGhE,KAAK,4BAA4B,EAGjC,KAAK,iBAAmB,KAAK,uBAAuB,EAChD,CAAC,KAAK,iBAAkB,CAC1B,KAAK,MAAM,8CAA8C,EACzD,MACF,CAGA,KAAK,yBAA2B,KAAK,aAAa,YAAcO,EAChE,KAAK,uBAAyB,EAC9B,KAAK,iBAAmB,EAGxB,GAAI,CACF,KAAK,iBAAiB,MAAM,EAAGA,CAAE,EACjC,KAAK,MACH,+CAA+C,KAAK,aAAa,WAAW,gBAAgBA,CAAE,EAChG,CACF,OAASjB,EAAG,CACV,QAAQ,MAAM,gDAAiDA,CAAC,EAChE,KAAK,mBAAmB,EACxB,MACF,CAGIU,GACF,KAAK,cAAc,EACnB,KAAK,MAAM,qCAAqC,IAGhD,KAAK,gBAAgB,EACrB,KAAK,MAAM,mCAAmC,EAC9C,KAAK,qBAAqB,EAE9B,CAEQ,6BAAoC,CAC1C,GAAI,KAAK,iBAAkB,CACzB,GAAI,CACF,KAAK,iBAAiB,QAAU,KAChC,KAAK,iBAAiB,KAAK,EAC3B,KAAK,iBAAiB,WAAW,EACjC,KAAK,MAAM,gDAAgD,CAC7D,OAAS,EAAQ,CAEX,EAAE,OAAS,qBACb,QAAQ,MAAM,qCAAsC,CAAC,CAEzD,CACA,KAAK,iBAAmB,IAC1B,CACF,CAEQ,iBAAwB,CAE9B,GAAI,KAAK,UAAY,KAAK,aACxB,GAAI,CAGF,KAAK,SAAS,QAAQ,KAAK,aAAa,WAAW,CAErD,OAAS,EAAG,CACV,QAAQ,MAAM,8BAA+B,CAAC,CAChD,CAEJ,CAEQ,oBAA2B,CAEjC,GAAI,KAAK,UAAY,KAAK,aACxB,GAAI,CACF,KAAK,SAAS,WAAW,KAAK,aAAa,WAAW,CAExD,MAAY,CAGZ,CAEJ,CAGQ,aAAgB,GAA4B,CAClD,IAAIU,EAAe,EACnB,GAAI,OAAO,GAAM,UAAY,EAAE,OAAQ,CACrC,IAAMC,EAAc,EAAE,OAA4B,MAClDD,EAAe,2BAA2BC,GAAA,YAAAA,EAAY,IAAI,aAAaA,GAAA,YAAAA,EAAY,OAAO,EAC5F,CACA,KAAK,MAAM,eAAgBD,CAAY,CAGzC,EAEQ,QAAQrB,EAA6B,CAC3C,KAAK,MACH,oBACA,SAAS,OAAOA,GAAS,SAAWA,EAAO,OAAO,GAClD,aAAa,KAAK,aAAa,EACjC,EAGI,KAAK,mBACP,KAAK,iBAAiB,QAAU,MAElC,KAAK,MAAM,QAAU,KAErB,KAAK,oBAAoB,EAGrB,KAAK,cACP,KAAK,MAAM,SAAS,GAIpB,KAAK,MAAM,oCAAoC,EAE/C,KAAK,qBAAqB,EAE9B,CAEQ,sBAA6B,CAEnC,KAAK,yBAA2B,EAChC,KAAK,uBAAyB,EAC9B,KAAK,iBAAmB,EAExB,KAAK,4BAA4B,EAE7B,KAAK,MAAM,WAAa,IAC1B,KAAK,MAAM,YAAc,GAG3B,KAAK,MAAM,QAAU,IAAM,KAAK,aAAa,OAAO,EAChD,KAAK,kBAAoB,KAAK,YAGpC,CAEQ,YAAmB,CAIzB,GAAI,CAAC,KAAK,eAAiB,KAAK,SAAU,CACxC,KAAK,oBAAoB,EACzB,MACF,CAGA,IAAMY,EAAc,KAAK,YACnBW,EAAW,KAAK,SAGtB,GAAI,MAAMX,CAAW,GAAK,MAAMW,CAAQ,GAAKA,GAAY,EAAG,CAC1D,KAAK,qBAAqB,EAC1B,MACF,CAGA,IAAMC,EAAgCD,EAAWX,GAAe,GAC1Da,EAAY,KAAK,MAAM,UAEzBD,GAAiCC,GAAa,CAACA,EAAU,UAE3D,KAAK,MAAM,UAAU,KAAK,IAAM,EAAG,EAAI,EAKzC,KAAK,MAAM,WAAW,IAAI,EAG1B,KAAK,qBAAqB,CAC5B,CAEQ,sBAA6B,CAEnC,KAAK,oBAAoB,EACzB,KAAK,gBAAkB,OAAO,sBAAsB,KAAK,eAAe,CAC1E,CAEQ,qBAA4B,CAC9B,KAAK,kBAAoB,OAC3B,OAAO,qBAAqB,KAAK,eAAe,EAChD,KAAK,gBAAkB,KAE3B,CAEA,UAAUC,EAA0B,CAClC,IAAMC,EAAgB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGD,CAAU,CAAC,EACzD,KAAK,MAAM,OAASC,EAChB,KAAK,WAEP,KAAK,SAAS,KAAK,MAAQA,EAE/B,CAGA,IAAI,iBAA2B,CAC7B,OACE,KAAK,eAAiB,YAAgC,CAAC,KAAK,MAAM,MAAM,kBAE5E,CAEA,IAAI,UAAoB,CACtB,OAAI,KAAK,iBAAmB,KAAK,aAIxB,KAAK,iBAAmB,EAIxB,KAAK,MAAM,MAEtB,CAEA,IAAI,aAAsB,CACxB,OAAI,KAAK,iBAAmB,KAAK,aAC3B,KAAK,iBAAmB,EAEnB,KAAK,yBAA2B,EACnC,KAAK,iBAAmB,KAAK,yBAA2B,KAAK,uBAC7D,EACK,KAAK,yBAA2B,EAGvC,KAAK,aAAa,YAClB,KAAK,yBACL,KAAK,uBAIA,EAGF,KAAK,MAAM,WAEtB,CAEA,IAAI,UAAmB,CACrB,OAAI,KAAK,iBAAmB,KAAK,YACxB,KAAK,YAAY,SAGjB,KAAK,MAAM,QAEtB,CAEA,IAAI,eAAyB,CAC3B,OAAO,KAAK,MAAM,eAAiB,IACrC,CAEA,IAAI,UAAoB,CAMtB,OAJI,KAAK,uBAAyB,UAI9B,KAAK,MAAM,YAAc,KAAK,MAAM,gBAI1C,CAEA,IAAI,OAAoB,CACtB,MAAO,CACL,aAAc,KAAK,aACnB,qBAAsB,KAAK,oBAC7B,CACF,CAEA,IAAI,eAAoC,CA9wB1C,IAAAC,EA+wBI,MAAO,CACL,aAAc,KAAK,aACnB,qBAAsB,KAAK,qBAC3B,SAAU,KAAK,SACf,YAAa,KAAK,YAClB,SAAU,KAAK,SACf,IAAK,KAAK,IACV,IAAIA,EAAA,KAAK,WAAL,YAAAA,EAAe,OACrB,CACF,CAGA,MAAMC,KAAkBC,EAAmB,CACzC,QAAQ,MACN,UAAU,KAAK,GAAG,MAAM,KAAK,YAAY,MAAM,KAAK,oBAAoB,KAAKD,CAAK,GAClF,GAAGC,EACH,KAAK,aACP,CACF,CAGA,UAAUC,EAAyB,EAAS,CAC1C,IAAMC,EAAiB,KAAK,SACxB,CAAC,MAAMA,CAAc,GAAKA,EAAiBD,EAC7C,KAAK,KAAKC,EAAiBD,CAAc,EAEzC,KAAK,MAAM,oDAAoDC,CAAc,GAAG,CAEpF,CACF,EC1yBA,IAAMC,EAAqB,EAErBC,EAAqB,OAAO,OAAW,IACvCC,EACJD,GAAa,OAAO,OAAO,aAAiB,IAAc,IAAI,OAAO,aAAiB,KAyBnEE,EAArB,KAA2B,CACzB,MACA,MACA,OAGA,MAEA,YAAYC,EAAoB,CAAC,EAAG,CAClC,GAAM,CACJ,OAAAC,EAAS,CAAC,EACV,WAAAC,EAAa,IAAM,CAAC,EACpB,QAAAC,EAAU,IAAM,CAAC,EACjB,gBAAAC,EAAkB,IAAM,CAAC,EACzB,oBAAAC,EAAsB,IAAM,CAAC,EAC7B,gBAAAC,EAAkB,IAAM,CAAC,EACzB,mBAAAC,EAAqB,EACvB,EAAIP,EAEJ,KAAK,MAAQ,CACX,WAAAE,EACA,QAAAC,EACA,gBAAAC,EACA,oBAAAC,EACA,gBAAAC,CACF,EAEA,KAAK,MAAQ,CACX,OAAQ,EACR,gBAAiB,EACjB,mBAAAC,CACF,EAEA,KAAK,MAAQC,EAEb,KAAK,OAASP,EAAO,IACnB,CAACQ,EAAkBC,IACjB,IAAIF,EAAM,CACR,SAAAC,EACA,IAAAC,EACA,MAAO,KACP,SAAU,CAAC,CACb,CAAC,CACL,EAIKZ,GACH,KAAK,gBAAgB,CAEzB,CAEA,SAAS,CAAE,SAAAW,EAAU,SAAAE,EAAU,SAAAC,EAAW,CAAC,CAAE,EAAyB,CACpE,KAAK,OAAO,KACV,IAAIJ,EAAM,CACR,SAAAC,EACA,SAAAE,EACA,SAAAC,EACA,IAAK,KAAK,OAAO,OACjB,MAAO,IACT,CAAC,CACH,CACF,CAEA,YAAYC,EAAuB,CACjC,IAAMC,EAAQ,KAAK,OAAO,QAAQD,CAAK,EACvC,OAAIC,EAAQ,GACH,KAAK,OAAO,OAAOA,EAAO,CAAC,EAE7B,CAAC,CACV,CAEA,iBAAwB,CAClB,KAAK,cAAc,KAAK,aAAa,gBAAgB,CAC3D,CAEA,MAAa,CACP,KAAK,cAAc,KAAK,aAAa,KAAK,CAChD,CAEA,OAAc,CACR,KAAK,cAAc,KAAK,aAAa,MAAM,CACjD,CAEA,cAAqB,CACnB,GAAI,KAAK,cAAgB,KAAK,aAAa,YAAc,EAAG,CAC1D,KAAK,aAAa,KAAK,CAAC,EACxB,MACF,CAEA,KAAK,kBAAkB,EAEnB,EAAE,KAAK,MAAM,gBAAkB,IAAG,KAAK,MAAM,gBAAkB,GAKnE,KAAK,KAAK,EAEN,KAAK,MAAM,iBAAiB,KAAK,MAAM,gBAAgB,KAAK,YAAY,EACxE,KAAK,MAAM,qBAAqB,KAAK,MAAM,oBAAoB,KAAK,YAAY,CACtF,CAEA,UAAiB,CAIf,GAHA,KAAK,kBAAkB,EAGnB,KAAK,MAAM,gBAAkB,KAAK,OAAO,OAAS,EACpD,KAAK,MAAM,sBACN,CAIL,KAAK,MAAM,QAAQ,EACnB,MACF,CAKA,KAAK,KAAK,EAEN,KAAK,MAAM,iBAAiB,KAAK,MAAM,gBAAgB,KAAK,YAAY,EACxE,KAAK,MAAM,iBAAiB,KAAK,MAAM,gBAAgB,KAAK,YAAY,CAC9E,CAEA,mBAA0B,CACxB,GAAI,KAAK,aAEP,GAAI,CACG,KAAK,aAAa,UACrB,KAAK,aAAa,MAAM,EAGtB,KAAK,aAAa,SAAW,GAAK,CAAC,MAAM,KAAK,aAAa,QAAQ,GACrE,KAAK,aAAa,KAAK,CAAC,CAE5B,OAASC,EAAO,CACd,QAAQ,MAAM,yBAA0BA,EAAO,KAAK,YAAY,CAClE,CAEJ,CAEA,UAAiB,CAEf,KAAK,OAAO,QAASF,GAAiB,CACpCA,EAAM,MAAM,CACd,CAAC,CACH,CAEA,SAAgB,CAEd,KAAK,OAAO,QAASA,GAAiB,CAEhCA,EAAM,kBAAoBA,EAAM,iBAAiB,SACnDA,EAAM,iBAAiB,OAAS,MAE9BA,EAAM,cACRA,EAAM,YAAc,MAGtB,GAAI,CACEA,EAAM,mBACRA,EAAM,iBAAiB,QAAU,KACjCA,EAAM,iBAAiB,KAAK,EAC5BA,EAAM,iBAAiB,WAAW,GAEhCA,EAAM,UAAYf,GACpBe,EAAM,SAAS,WAAW,EAExBA,EAAM,QACRA,EAAM,MAAM,MAAM,EAClBA,EAAM,MAAM,IAAM,GAClBA,EAAM,MAAM,KAAK,EACjBA,EAAM,MAAM,QAAU,KACtBA,EAAM,MAAM,QAAU,KAE1B,OAASG,EAAG,CACV,QAAQ,MAAM,8BAA+BA,EAAGH,CAAK,CACvD,CACF,CAAC,CAGH,CAEA,UAAUH,EAAaO,EAA2B,GAAa,CAC7D,GAAIP,EAAM,GAAKA,GAAO,KAAK,OAAO,OAAQ,CACxC,QAAQ,KAAK,oBAAoBA,CAAG,iBAAiB,EACrD,MACF,CACA,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,KAAK,MAAM,gBAAkBA,EAKzBO,IACF,KAAK,KAAK,EACN,KAAK,MAAM,iBAAiB,KAAK,MAAM,gBAAgB,KAAK,YAAY,EAEhF,CAEA,UAAUP,EAAaQ,EAA2B,CAEhD,GACER,EAAM,GACNA,GAAO,KAAK,OAAO,QACnB,KAAK,MAAM,gBAAkBd,EAAqBc,EAElD,OACF,IAAMG,EAAQ,KAAK,OAAOH,CAAG,EAEzBG,GAAOA,EAAM,QAAQK,CAAS,CACpC,CAEA,SAASC,EAA4E,CAAC,EAAS,CAC7F,KAAK,MAAQ,CAAE,GAAG,KAAK,MAAO,GAAGA,CAAI,CACvC,CAIA,SAAgB,CACV,KAAK,MAAM,SAAS,KAAK,MAAM,QAAQ,CAC7C,CAEA,WAAWN,EAAoB,CACzB,KAAK,MAAM,YAAY,KAAK,MAAM,WAAWA,CAAK,CACxD,CAEA,IAAI,cAAkC,CACpC,OAAO,KAAK,OAAO,KAAK,MAAM,eAAe,CAC/C,CAEA,IAAI,WAA+B,CACjC,OAAO,KAAK,OAAO,KAAK,MAAM,gBAAkB,CAAC,CACnD,CAEA,iBAAwB,CACtB,KAAK,MAAM,mBAAqB,GAEhC,KAAK,OAAO,QAASA,GAAU,CACzBA,EAAM,iBAER,QAAQ,KAAK,2EAA2E,CAE5F,CAAC,CACH,CAEA,UAAUO,EAA0B,CAClC,IAAMC,EAAgB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGD,CAAU,CAAC,EAEzD,KAAK,MAAM,OAASC,EAEpB,KAAK,OAAO,QAASR,GAAUA,EAAM,UAAUQ,CAAa,CAAC,CAC/D,CACF","names":["isBrowser","audioContext","Track","trackUrl","skipHEAD","queue","idx","metadata","from","e","audioContext","cb","options","res","err","arrayBuffer","buffer","node","forcePause","wasPaused","currentTime","pauseDuration","playPromise","_","error","loadHTML5","to","currentDuration","seekTime","errorDetails","mediaError","duration","isWithinLastTwentyFiveSeconds","nextTrack","nextVolume","clampedVolume","_a","first","args","secondsFromEnd","targetDuration","PRELOAD_NUM_TRACKS","isBrowser","audioContext","Queue","props","tracks","onProgress","onEnded","onPlayNextTrack","onPlayPreviousTrack","onStartNewTrack","webAudioIsDisabled","Track","trackUrl","idx","skipHEAD","metadata","track","index","error","e","playImmediately","loadHTML5","obj","nextVolume","clampedVolume"]}
|
package/package.json
CHANGED
|
@@ -1,50 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gapless.js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Gapless audio playback javascript plugin",
|
|
5
|
-
"main": "index.
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
5
|
+
"main": "dist/cjs/index.cjs",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.mts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./dist/cjs/index.d.cts",
|
|
22
|
+
"default": "./dist/cjs/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
},
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
]
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"private": false,
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"types": "tsc --noEmit"
|
|
23
32
|
},
|
|
24
33
|
"repository": {
|
|
25
34
|
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/
|
|
35
|
+
"url": "git+https://github.com/RelistenNet/gapless.js.git"
|
|
27
36
|
},
|
|
28
|
-
"author": "Daniel Saewitz
|
|
37
|
+
"author": "Daniel Saewitz",
|
|
29
38
|
"license": "MIT",
|
|
30
39
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"eslint-config-airbnb-typescript": "^12.0.0",
|
|
36
|
-
"eslint-config-prettier": "^6.15.0",
|
|
37
|
-
"eslint-plugin-import": "^2.22.1",
|
|
38
|
-
"eslint-plugin-jsdoc": "^30.7.8",
|
|
39
|
-
"eslint-plugin-mocha": "^8.0.0",
|
|
40
|
-
"eslint-plugin-prettier": "^3.1.4",
|
|
41
|
-
"eslint-plugin-promise": "^4.2.1",
|
|
42
|
-
"eslint-plugin-security": "^1.4.0",
|
|
43
|
-
"husky": "^5.0.4",
|
|
44
|
-
"lint-staged": "^10.5.2",
|
|
45
|
-
"pinst": "^2.1.1",
|
|
46
|
-
"prettier": "^2.2.1",
|
|
47
|
-
"rimraf": "^3.0.2",
|
|
48
|
-
"typescript": "^4.1.2"
|
|
40
|
+
"@switz/eslint-config": "^12.3.2",
|
|
41
|
+
"eslint": "^9.25.1",
|
|
42
|
+
"tsup": "^8.4.0",
|
|
43
|
+
"typescript": "^5.8.3"
|
|
49
44
|
}
|
|
50
|
-
}
|
|
45
|
+
}
|