audiotoolheadless
Version:
A modern, headless audio player with HLS support, equalizer, queue management, and media session integration
1,646 lines (1,637 loc) • 55.6 kB
JavaScript
'use strict';
// src/config/index.ts
var PLAYER_CONSTANTS = Object.freeze({
REACT: "REACT",
VANILLA: "VANILLA",
DEVELOPMENT: "development",
PRODUCTION: "production"
});
var PLAYBACK_STATES = Object.freeze({
BUFFERING: "buffering",
PLAYING: "playing",
PAUSED: "paused",
READY: "ready",
IDLE: "idle",
ENDED: "ended",
STALLED: "stalled",
ERROR: "error",
TRACK_CHANGE: "trackchanged",
DURATION_CHANGE: "durationchanged",
QUEUE_ENDED: "queueended"
});
var READY_STATES = Object.freeze({
HAVE_NOTHING: 0,
HAVE_METADATA: 1,
HAVE_CURRENT_DATA: 2,
HAVE_FUTURE_DATA: 3,
HAVE_ENOUGH_DATA: 4
});
var NETWORK_STATES = Object.freeze({
NETWORK_EMPTY: 0,
NETWORK_IDLE: 1,
NETWORK_LOADING: 2,
NETWORK_NO_SOURCE: 3
});
var ERROR_MESSAGES = Object.freeze({
MEDIA_ERR_ABORTED: "The user canceled the audio.",
MEDIA_ERR_DECODE: "An error occurred while decoding the audio.",
MEDIA_ERR_NETWORK: "A network error occurred while fetching the audio.",
MEDIA_ERR_SRC_NOT_SUPPORTED: "The audio is missing or is in a format not supported by your browser.",
DEFAULT: "An unknown error occurred."
});
var EXTERNAL_URLS = Object.freeze({
HLS_CDN: "https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.18/hls.min.js",
CAST_SDK: "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
});
var EQUALIZER_PRESETS = Object.freeze({
FLAT: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
ACOUSTIC: [-0.5, 0, 0.5, 0.8, 0.8, 0.5, 0, 0.2, 0.5, 0.8],
BASS_BOOSTER: [2.4, 1.8, 1.2, 0.5, 0, 0, 0, 0, 0, 0],
BASS_REDUCER: [-2.4, -1.8, -1.2, -0.5, 0, 0, 0, 0, 0, 0],
CLASSICAL: [0, 0, 0, 0, 0, 0, 0, -1.2, -1.2, -1.2],
DEEP: [1.2, 0.8, 1.6, 1.3, 0, 1.5, 2.4, 2.2, 1.8, 1.4],
ELECTRONIC: [2.4, 1.8, 1, 0, -0.5, 2, 1, 1.8, 2.4, 2.4],
LATIN: [0, 0, 0, 0, -1, -1, -1, 0, 1.2, 1.8],
LOUDNESS: [6.5, 4, 0, 0, -2, 0, -1, -4.5, 4, 1],
LOUNGE: [-1.5, -0.5, -0.2, 1.8, 2.4, 1, 0, 0, 1.8, 0.2],
PIANO: [1.5, 1, 0, 2.4, 3, 1.5, 3.5, 4, 3, 3.5],
POP: [-1, -0.5, 0, 0.8, 1.4, 1.4, 0.8, 0, -0.5, -1],
RNB: [3, 7, 5.5, 1.4, -2, -1.5, 2, 2.4, 2.8, 3.5],
ROCK: [3.2, 2.5, -0.6, -0.8, -0.3, 0.4, 0.9, 1.1, 1.1, 1.1],
SMALL_SPEAKERS: [2.4, 4, 3.5, 3, 1.5, 0, -1.5, -2, -2.5, -3],
SPOKEN_WORD: [-2.5, -1, 0, 0.7, 1.8, 2.5, 2.5, 2, 1, 0],
TREBLE_BOOSTER: [0, 0, 0, 0, 0, 1, 2, 3, 4, 4.5],
TREBLE_REDUCER: [0, 0, 0, 0, 0, -1, -2, -3, -4, -4.5],
VOCAL_BOOSTER: [-1.6, -3, -3, 1, 3.8, 3.8, 3, 2.4, 1.5, 0]
});
var EQUALIZER_BANDS = [
{ type: "peaking", frequency: 32, Q: 1, gain: 0 },
{ type: "peaking", frequency: 64, Q: 1, gain: 0 },
{ type: "peaking", frequency: 125, Q: 1, gain: 0 },
{ type: "peaking", frequency: 250, Q: 1, gain: 0 },
{ type: "peaking", frequency: 500, Q: 1, gain: 0 },
{ type: "peaking", frequency: 1e3, Q: 1, gain: 0 },
{ type: "peaking", frequency: 2e3, Q: 1, gain: 0 },
{ type: "peaking", frequency: 4e3, Q: 1, gain: 0 },
{ type: "peaking", frequency: 8e3, Q: 1, gain: 0 },
{ type: "peaking", frequency: 16e3, Q: 1, gain: 0 }
];
var AUDIO_EVENTS = Object.freeze({
ABORT: "abort",
TIME_UPDATE: "timeupdate",
CAN_PLAY: "canplay",
CAN_PLAY_THROUGH: "canplaythrough",
DURATION_CHANGE: "durationchange",
ENDED: "ended",
EMPTIED: "emptied",
PLAYING: "playing",
WAITING: "waiting",
SEEKING: "seeking",
SEEKED: "seeked",
LOADED_META_DATA: "loadedmetadata",
LOADED_DATA: "loadeddata",
PLAY: "play",
PAUSE: "pause",
RATE_CHANGE: "ratechange",
VOLUME_CHANGE: "volumechange",
SUSPEND: "suspend",
STALLED: "stalled",
PROGRESS: "progress",
LOAD_START: "loadstart",
ERROR: "error",
TRACK_CHANGE: "trackchange",
QUEUE_ENDED: "queueended"
});
var HLS_EVENTS = Object.freeze({
MEDIA_ATTACHING: "hlsMediaAttaching",
MEDIA_ATTACHED: "hlsMediaAttached",
MEDIA_DETACHING: "hlsMediaDetaching",
MEDIA_DETACHED: "hlsMediaDetached",
BUFFER_RESET: "hlsBufferReset",
BUFFER_CODECS: "hlsBufferCodecs",
BUFFER_CREATED: "hlsBufferCreated",
BUFFER_APPENDING: "hlsBufferAppending",
BUFFER_APPENDED: "hlsBufferAppended",
BUFFER_EOS: "hlsBufferEos",
BUFFER_FLUSHING: "hlsBufferFlushing",
BUFFER_FLUSHED: "hlsBufferFlushed",
MANIFEST_LOADING: "hlsManifestLoading",
MANIFEST_LOADED: "hlsManifestLoaded",
MANIFEST_PARSED: "hlsManifestParsed",
LEVEL_SWITCHING: "hlsLevelSwitching",
LEVEL_SWITCHED: "hlsLevelSwitched",
LEVEL_LOADING: "hlsLevelLoading",
LEVEL_LOADED: "hlsLevelLoaded",
LEVEL_UPDATED: "hlsLevelUpdated",
LEVEL_PTS_UPDATED: "hlsLevelPtsUpdated",
LEVELS_UPDATED: "hlsLevelsUpdated",
AUDIO_TRACKS_UPDATED: "hlsAudioTracksUpdated",
AUDIO_TRACK_SWITCHING: "hlsAudioTrackSwitching",
AUDIO_TRACK_SWITCHED: "hlsAudioTrackSwitched",
AUDIO_TRACK_LOADING: "hlsAudioTrackLoading",
AUDIO_TRACK_LOADED: "hlsAudioTrackLoaded",
SUBTITLE_TRACKS_UPDATED: "hlsSubtitleTracksUpdated",
SUBTITLE_TRACKS_CLEARED: "hlsSubtitleTracksCleared",
SUBTITLE_TRACK_SWITCH: "hlsSubtitleTrackSwitch",
SUBTITLE_TRACK_LOADING: "hlsSubtitleTrackLoading",
SUBTITLE_TRACK_LOADED: "hlsSubtitleTrackLoaded",
SUBTITLE_FRAG_PROCESSED: "hlsSubtitleFragProcessed",
CUES_PARSED: "hlsCuesParsed",
NON_NATIVE_TEXT_TRACKS_FOUND: "hlsNonNativeTextTracksFound",
INIT_PTS_FOUND: "hlsInitPtsFound",
FRAG_LOADING: "hlsFragLoading",
FRAG_LOAD_EMERGENCY_ABORTED: "hlsFragLoadEmergencyAborted",
FRAG_LOADED: "hlsFragLoaded",
FRAG_DECRYPTED: "hlsFragDecrypted",
FRAG_PARSING_INIT_SEGMENT: "hlsFragParsingInitSegment",
FRAG_PARSING_USERDATA: "hlsFragParsingUserdata",
FRAG_PARSING_METADATA: "hlsFragParsingMetadata",
FRAG_PARSED: "hlsFragParsed",
FRAG_BUFFERED: "hlsFragBuffered",
FRAG_CHANGED: "hlsFragChanged",
FPS_DROP: "hlsFpsDrop",
FPS_DROP_LEVEL_CAPPING: "hlsFpsDropLevelCapping",
ERROR: "hlsError",
DESTROYING: "hlsDestroying",
KEY_LOADING: "hlsKeyLoading",
KEY_LOADED: "hlsKeyLoaded",
LIVE_BACK_BUFFER_REACHED: "hlsLiveBackBufferReached",
BACK_BUFFER_REACHED: "hlsBackBufferReached"
});
var ERROR_EVENTS = Object.freeze({
MEDIA_ERR_ABORTED: "Media playback was aborted",
MEDIA_ERR_NETWORK: "Network error occurred",
MEDIA_ERR_DECODE: "Media decoding error",
MEDIA_ERR_SRC_NOT_SUPPORTED: "Media source not supported"
});
var AUDIO_X_CONSTANTS = PLAYER_CONSTANTS;
var PLAYBACK_STATE = PLAYBACK_STATES;
var READY_STATE = READY_STATES;
var ERROR_MSG_MAP = ERROR_MESSAGES;
var URLS = EXTERNAL_URLS;
// src/utils/event-emitter.ts
var EventEmitter = class {
constructor() {
this.eventMap = {};
}
/**
* Subscribe to an event
* @param eventName - Name of the event
* @param callback - Function to call when event is emitted
* @returns Unsubscribe function
*/
subscribe(eventName, callback) {
if (!this.eventMap[eventName]) {
this.eventMap[eventName] = [];
}
this.eventMap[eventName].push(callback);
return () => {
this.unsubscribe(eventName, callback);
};
}
/**
* Unsubscribe from an event
* @param eventName - Name of the event
* @param callback - Function to remove
*/
unsubscribe(eventName, callback) {
if (!this.eventMap[eventName]) return;
const index = this.eventMap[eventName].indexOf(callback);
if (index > -1) {
this.eventMap[eventName].splice(index, 1);
}
}
/**
* Emit an event to all subscribers
* @param eventName - Name of the event
* @param data - Data to pass to callbacks
*/
emit(eventName, data) {
if (!this.eventMap[eventName]) return;
for (const callback of this.eventMap[eventName]) {
try {
callback(data);
} catch (error) {
console.error(`EventEmitter: Error in callback for event '${eventName}':`, error);
}
}
}
/**
* Remove all listeners for an event
* @param eventName - Name of the event
*/
removeAllListeners(eventName) {
if (eventName) {
delete this.eventMap[eventName];
} else {
this.eventMap = {};
}
}
/**
* Get the number of listeners for an event
* @param eventName - Name of the event
* @returns Number of listeners
*/
listenerCount(eventName) {
return this.eventMap[eventName]?.length || 0;
}
/**
* Get all event names that have listeners
* @returns Array of event names
*/
eventNames() {
return Object.keys(this.eventMap);
}
};
// src/utils/validation.ts
var ValidationUtils = class _ValidationUtils {
/**
* Check if an array is valid and has elements
* @param arr - Array to validate
* @returns True if array is valid and not empty
*/
static isValidArray(arr) {
return arr && Array.isArray(arr) && arr.length > 0;
}
/**
* Check if a function is valid
* @param fn - Function to validate
* @returns True if function is valid
*/
static isValidFunction(fn) {
return fn instanceof Function && typeof fn === "function";
}
/**
* Check if an object is valid and not null
* @param obj - Object to validate
* @returns True if object is valid
*/
static isValidObject(obj) {
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
}
/**
* Check if a string is valid and not empty
* @param str - String to validate
* @returns True if string is valid
*/
static isValidString(str) {
return typeof str === "string" && str.trim().length > 0;
}
/**
* Check if a number is valid and finite
* @param num - Number to validate
* @returns True if number is valid
*/
static isValidNumber(num) {
return typeof num === "number" && Number.isFinite(num);
}
/**
* Check if a value is defined (not null or undefined)
* @param value - Value to check
* @returns True if value is defined
*/
static isDefined(value) {
return value !== null && value !== void 0;
}
/**
* Validate volume range (0-100)
* @param volume - Volume value to validate
* @returns True if volume is in valid range
*/
static isValidVolume(volume) {
return _ValidationUtils.isValidNumber(volume) && volume >= 0 && volume <= 100;
}
/**
* Validate time value (non-negative)
* @param time - Time value to validate
* @returns True if time is valid
*/
static isValidTime(time) {
return _ValidationUtils.isValidNumber(time) && time >= 0;
}
};
// src/utils/queue-manager.ts
var QueueManager = class {
constructor() {
this.queue = [];
}
/**
* Set the queue with specific playback type
* @param tracks - Array of tracks
* @param playbackType - Type of playback ordering
*/
setQueue(tracks, playbackType) {
if (!ValidationUtils.isValidArray(tracks)) {
throw new Error("QueueManager: Invalid tracks array provided");
}
const tracksCopy = [...tracks];
switch (playbackType) {
case "DEFAULT":
this.queue = tracksCopy;
break;
case "REVERSE":
this.queue = tracksCopy.reverse();
break;
case "SHUFFLE":
this.queue = this.shuffleArray(tracksCopy);
break;
default:
this.queue = tracksCopy;
break;
}
}
/**
* Get the current queue
* @returns Array of tracks
*/
getQueue() {
return [...this.queue];
}
/**
* Add tracks to the queue
* @param tracks - Single track or array of tracks
*/
addToQueue(tracks) {
const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
this.queue.push(...tracksArray);
}
/**
* Remove track from queue by ID
* @param trackId - ID of track to remove
* @returns True if track was removed
*/
removeFromQueue(trackId) {
const initialLength = this.queue.length;
this.queue = this.queue.filter((track) => track.id !== trackId);
return this.queue.length < initialLength;
}
/**
* Clear the entire queue
*/
clearQueue() {
this.queue = [];
}
/**
* Get queue length
* @returns Number of tracks in queue
*/
getQueueLength() {
return this.queue.length;
}
/**
* Get track at specific index
* @param index - Index of track
* @returns Track at index or undefined
*/
getTrackAt(index) {
return this.queue[index];
}
/**
* Find track index by ID
* @param trackId - ID of track to find
* @returns Index of track or -1 if not found
*/
findTrackIndex(trackId) {
return this.queue.findIndex((track) => track.id === trackId);
}
/**
* Move track to new position
* @param fromIndex - Current index
* @param toIndex - Target index
* @returns True if move was successful
*/
moveTrack(fromIndex, toIndex) {
if (fromIndex < 0 || fromIndex >= this.queue.length || toIndex < 0 || toIndex >= this.queue.length) {
return false;
}
const track = this.queue.splice(fromIndex, 1)[0];
if (track) {
this.queue.splice(toIndex, 0, track);
}
return true;
}
/**
* Shuffle the queue using Fisher-Yates algorithm
* @param currentTrackId - Optional current track ID to keep at current position
* @returns Shuffled queue
*/
shuffleQueue(currentTrackId) {
let currentTrack;
let currentIndex = -1;
if (currentTrackId) {
currentIndex = this.findTrackIndex(currentTrackId);
if (currentIndex > -1) {
currentTrack = this.queue[currentIndex];
}
}
const shuffled = this.shuffleArray([...this.queue]);
if (currentTrack && currentIndex > -1) {
const newIndex = shuffled.findIndex((track) => track.id === currentTrackId);
if (newIndex > -1 && newIndex !== currentIndex) {
const trackAtCurrent = shuffled[currentIndex];
const trackAtNew = shuffled[newIndex];
if (trackAtCurrent && trackAtNew) {
shuffled[currentIndex] = trackAtNew;
shuffled[newIndex] = trackAtCurrent;
}
}
}
this.queue = shuffled;
return [...this.queue];
}
/**
* Shuffle an array using Fisher-Yates algorithm
* @param array - Array to shuffle
* @returns New shuffled array
*/
shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = shuffled[i];
const other = shuffled[j];
if (temp !== void 0 && other !== void 0) {
shuffled[i] = other;
shuffled[j] = temp;
}
}
return shuffled;
}
/**
* Get next track index with loop support
* @param currentIndex - Current track index
* @param loopMode - Current loop mode
* @returns Next track index or -1 if no next track
*/
getNextTrackIndex(currentIndex, loopMode) {
switch (loopMode) {
case "SINGLE":
return currentIndex;
// Stay on same track
case "QUEUE":
return currentIndex + 1 >= this.queue.length ? 0 : currentIndex + 1;
case "OFF":
return currentIndex + 1 < this.queue.length ? currentIndex + 1 : -1;
default:
return currentIndex + 1 < this.queue.length ? currentIndex + 1 : -1;
}
}
/**
* Get previous track index
* @param currentIndex - Current track index
* @returns Previous track index or -1 if no previous track
*/
getPreviousTrackIndex(currentIndex) {
return currentIndex > 0 ? currentIndex - 1 : -1;
}
/**
* Check if queue is empty
* @returns True if queue is empty
*/
isEmpty() {
return this.queue.length === 0;
}
/**
* Get queue statistics
* @returns Object with queue statistics
*/
getQueueStats() {
const totalTracks = this.queue.length;
const totalDuration = this.queue.reduce((sum, track) => {
return sum + (track.duration || 0);
}, 0);
const averageDuration = totalTracks > 0 ? totalDuration / totalTracks : 0;
return {
totalTracks,
totalDuration,
averageDuration
};
}
};
// src/utils/playback-utils.ts
var _PlaybackUtils = class _PlaybackUtils {
/**
* Calculate actual played length for analytics
* @param audioElement - HTML audio element
* @param event - Event type that triggered calculation
*/
static calculateActualPlayedLength(audioElement, event) {
if (!audioElement) return;
const currentTime = Date.now();
audioElement.currentTime;
switch (event) {
case "PLAY":
_PlaybackUtils.playbackStartTime = currentTime;
break;
case "PAUSE":
case "ENDED":
case "TRACK_CHANGE":
if (_PlaybackUtils.playbackStartTime > 0) {
const sessionDuration = currentTime - _PlaybackUtils.playbackStartTime;
_PlaybackUtils.totalPlayTime += sessionDuration;
_PlaybackUtils.playbackStartTime = 0;
console.debug("PlaybackUtils: Session duration:", sessionDuration, "ms");
console.debug("PlaybackUtils: Total play time:", _PlaybackUtils.totalPlayTime, "ms");
}
break;
}
}
/**
* Get total playback time
* @returns Total playback time in milliseconds
*/
static getTotalPlayTime() {
return _PlaybackUtils.totalPlayTime;
}
/**
* Reset playback time tracking
*/
static resetPlayTime() {
_PlaybackUtils.playbackStartTime = 0;
_PlaybackUtils.totalPlayTime = 0;
}
/**
* Handle loop playback mode
* @param loopMode - The loop mode to apply
*/
static handleLoopPlayback(loopMode) {
console.debug("PlaybackUtils: Loop mode set to:", loopMode);
}
/**
* Handle queue playback progression
*/
static handleQueuePlayback() {
console.debug("PlaybackUtils: Queue playback handler activated");
}
/**
* Calculate playback progress percentage
* @param currentTime - Current playback time
* @param duration - Total track duration
* @returns Progress percentage (0-100)
*/
static calculateProgress(currentTime, duration) {
if (!duration || duration === 0) return 0;
return Math.min(100, currentTime / duration * 100);
}
/**
* Calculate remaining time
* @param currentTime - Current playback time
* @param duration - Total track duration
* @returns Remaining time in seconds
*/
static calculateRemainingTime(currentTime, duration) {
if (!duration || duration === 0) return 0;
return Math.max(0, duration - currentTime);
}
/**
* Format time for display
* @param seconds - Time in seconds
* @returns Formatted time string (MM:SS or HH:MM:SS)
*/
static formatTime(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return "00:00";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor(seconds % 3600 / 60);
const secs = Math.floor(seconds % 60);
const formatPart = (num) => num.toString().padStart(2, "0");
if (hours > 0) {
return `${formatPart(hours)}:${formatPart(minutes)}:${formatPart(secs)}`;
}
return `${formatPart(minutes)}:${formatPart(secs)}`;
}
/**
* Get buffered duration
* @param audioElement - HTML audio element
* @returns Buffered duration in seconds
*/
static getBufferedDuration(audioElement) {
if (!audioElement.buffered || audioElement.buffered.length === 0) {
return 0;
}
const buffered = audioElement.buffered;
const currentTime = audioElement.currentTime;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (currentTime >= start && currentTime <= end) {
return end - currentTime;
}
}
return 0;
}
/**
* Check if track can be played through without buffering
* @param audioElement - HTML audio element
* @returns True if track can play through
*/
static canPlayThrough(audioElement) {
return audioElement.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA;
}
/**
* Get playback rate adjusted duration
* @param duration - Original duration
* @param playbackRate - Current playback rate
* @returns Adjusted duration
*/
static getAdjustedDuration(duration, playbackRate) {
if (playbackRate <= 0) return duration;
return duration / playbackRate;
}
/**
* Smooth seek to prevent audio glitches
* @param audioElement - HTML audio element
* @param targetTime - Target time to seek to
* @param smooth - Whether to use smooth seeking
*/
static seekTo(audioElement, targetTime, smooth = true) {
if (!Number.isFinite(targetTime) || targetTime < 0) return;
const duration = audioElement.duration;
let adjustedTargetTime = targetTime;
if (Number.isFinite(duration)) {
adjustedTargetTime = Math.min(targetTime, duration - 0.1);
}
if (smooth && Math.abs(audioElement.currentTime - adjustedTargetTime) < 1) {
const steps = 5;
const stepSize = (adjustedTargetTime - audioElement.currentTime) / steps;
let currentStep = 0;
const smoothSeek = () => {
if (currentStep < steps) {
audioElement.currentTime += stepSize;
currentStep++;
setTimeout(smoothSeek, 20);
} else {
audioElement.currentTime = adjustedTargetTime;
}
};
smoothSeek();
} else {
audioElement.currentTime = adjustedTargetTime;
}
}
};
_PlaybackUtils.playbackStartTime = 0;
_PlaybackUtils.totalPlayTime = 0;
var PlaybackUtils = _PlaybackUtils;
// src/lib/equalizer/index.ts
var Equalizer = class _Equalizer {
constructor() {
if (_Equalizer.equalizerInstance) {
console.warn(
"Equalizer: Multiple instances detected. Returning existing **singleton** instance."
);
return _Equalizer.equalizerInstance;
}
this.initializeAudioContext();
_Equalizer.equalizerInstance = this;
}
/**
* Initialize AudioContext with browser compatibility
* @private
*/
initializeAudioContext() {
const contextOptions = {
latencyHint: "playback",
sampleRate: 44100
};
try {
if (typeof AudioContext !== "undefined") {
this.audioContext = new AudioContext(contextOptions);
} else if (typeof window.webkitAudioContext !== "undefined") {
this.audioContext = new window.webkitAudioContext(contextOptions);
} else {
throw new Error("Web Audio API is not supported in this browser");
}
this.contextStatus = "RUNNING";
if (this.audioContext.state === "suspended") {
this.addContextResumeListener();
}
} catch (error) {
console.error("Equalizer: Failed to initialize AudioContext:", error);
this.contextStatus = "CLOSED";
}
}
/**
* Add listener to resume AudioContext on user interaction
* @private
*/
addContextResumeListener() {
const resumeContext = () => {
this.audioContext.resume();
setTimeout(() => {
if (this.audioContext.state === "running") {
document.body.removeEventListener("click", resumeContext, false);
document.body.removeEventListener("touchstart", resumeContext, false);
}
}, 0);
};
document.body.addEventListener("click", resumeContext, false);
document.body.addEventListener("touchstart", resumeContext, false);
}
/**
* Initialize all audio processing nodes
* @param audioElement - The HTML audio element to connect
*/
initializeWithAudioElement(audioElement) {
try {
const audioSource = this.audioContext.createMediaElementSource(audioElement);
this.filterBands = EQUALIZER_BANDS.map((bandConfig) => {
const filter = this.audioContext.createBiquadFilter();
filter.type = bandConfig.type;
filter.frequency.value = bandConfig.frequency;
filter.gain.value = bandConfig.gain;
filter.Q.value = bandConfig.Q || 1;
return filter;
});
this.bassBoostFilter = this.audioContext.createBiquadFilter();
this.bassBoostFilter.type = "lowshelf";
this.bassBoostFilter.frequency.value = 100;
this.bassBoostFilter.gain.value = 0;
this.compressorNode = this.audioContext.createDynamicsCompressor();
this.compressorNode.threshold.value = -24;
this.compressorNode.knee.value = 30;
this.compressorNode.ratio.value = 12;
this.compressorNode.attack.value = 3e-3;
this.compressorNode.release.value = 0.25;
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = 1;
this.connectAudioChain(audioSource);
this.contextStatus = "RUNNING";
} catch (error) {
console.error("Equalizer: Failed to initialize audio nodes:", error);
this.contextStatus = "CLOSED";
}
}
/**
* Connect all audio nodes in the processing chain
* @private
*/
connectAudioChain(audioSource) {
if (this.filterBands.length === 0) return;
const firstBand = this.filterBands[0];
if (firstBand) {
audioSource.connect(firstBand);
}
for (let i = 0; i < this.filterBands.length - 1; i++) {
const currentBand = this.filterBands[i];
const nextBand = this.filterBands[i + 1];
if (currentBand && nextBand) {
currentBand.connect(nextBand);
}
}
const lastBand = this.filterBands[this.filterBands.length - 1];
if (lastBand) {
lastBand.connect(this.bassBoostFilter);
}
this.bassBoostFilter.connect(this.compressorNode);
this.compressorNode.connect(this.gainNode);
this.gainNode.connect(this.audioContext.destination);
}
/**
* Apply equalizer preset
* @param presetName - Name of the preset to apply
*/
setPreset(presetName) {
const presetGains = EQUALIZER_PRESETS[presetName];
if (!presetGains) {
throw new Error(`Equalizer: Preset '${String(presetName)}' not found`);
}
this.setCustomEQ(presetGains);
}
/**
* Set custom equalizer gains
* @param gains - Array of gain values (-12 to +12 dB)
*/
setCustomEQ(gains) {
if (!ValidationUtils.isValidArray(gains)) {
throw new Error("Equalizer: Invalid gains array provided");
}
if (gains.length !== this.filterBands.length) {
throw new Error(
`Equalizer: Expected ${this.filterBands.length} gain values, received ${gains.length}`
);
}
const currentTime = this.audioContext.currentTime;
const transitionTime = 0.05;
for (let i = 0; i < this.filterBands.length; i++) {
const band = this.filterBands[i];
const gainValue = gains[i];
if (band && gainValue !== void 0 && ValidationUtils.isValidNumber(gainValue)) {
const clampedGain = Math.max(-12, Math.min(12, gainValue));
band.gain.setTargetAtTime(clampedGain, currentTime, transitionTime);
}
}
}
/**
* Enable or disable bass boost
* @param enabled - Whether to enable bass boost
* @param boostAmount - Boost amount in dB (default: 6)
*/
setBassBoost(enabled, boostAmount = 6) {
const currentTime = this.audioContext.currentTime;
const targetGain = enabled ? Math.max(0, Math.min(12, boostAmount)) : 0;
this.bassBoostFilter.gain.setTargetAtTime(targetGain, currentTime, 0.05);
}
/**
* Set master volume
* @param volume - Volume level (0.0 to 1.0)
*/
setMasterVolume(volume) {
if (!ValidationUtils.isValidNumber(volume)) {
throw new Error("Equalizer: Invalid volume value");
}
const clampedVolume = Math.max(0, Math.min(1, volume));
const currentTime = this.audioContext.currentTime;
this.gainNode.gain.setTargetAtTime(clampedVolume, currentTime, 0.01);
}
/**
* Get current equalizer gains
* @returns Array of current gain values
*/
getCurrentGains() {
return this.filterBands.map((band) => band.gain.value);
}
/**
* Reset equalizer to flat response
*/
reset() {
const currentTime = this.audioContext.currentTime;
for (const band of this.filterBands) {
band.gain.setTargetAtTime(0, currentTime, 0.05);
}
this.bassBoostFilter.gain.setTargetAtTime(0, currentTime, 0.05);
this.gainNode.gain.setTargetAtTime(1, currentTime, 0.05);
}
/**
* Get equalizer status
* @returns Current equalizer status
*/
status() {
if (this.audioContext.state === "suspended") {
this.audioContext.resume();
}
return this.contextStatus;
}
/**
* Update compressor settings
* @param settings - Partial compressor settings
*/
setCompressorSettings(settings) {
const currentTime = this.audioContext.currentTime;
if (settings.threshold !== void 0) {
this.compressorNode.threshold.setTargetAtTime(settings.threshold, currentTime, 0.01);
}
if (settings.knee !== void 0) {
this.compressorNode.knee.setTargetAtTime(settings.knee, currentTime, 0.01);
}
if (settings.ratio !== void 0) {
this.compressorNode.ratio.setTargetAtTime(settings.ratio, currentTime, 0.01);
}
if (settings.attack !== void 0) {
this.compressorNode.attack.setTargetAtTime(settings.attack, currentTime, 0.01);
}
if (settings.release !== void 0) {
this.compressorNode.release.setTargetAtTime(settings.release, currentTime, 0.01);
}
}
/**
* Get available presets
* @returns Object containing all available presets
*/
static getPresets() {
return EQUALIZER_PRESETS;
}
/**
* Destroy equalizer and cleanup resources
*/
async destroy() {
try {
if (this.audioContext && this.audioContext.state !== "closed") {
await this.audioContext.close();
}
this.contextStatus = "CLOSED";
} catch (error) {
console.error("Equalizer: Error during cleanup:", error);
}
}
};
// src/lib/hls/index.ts
var HlsManager = class {
constructor() {
this.hlsInstance = null;
this.isHlsSupported = false;
this.isLoggingEnabled = false;
this.checkHlsSupport();
}
/**
* Initialize HLS manager
* @param enableLogging - Whether to enable HLS logging
* @param hlsConfig - HLS configuration options
*/
async initialize(enableLogging = false, hlsConfig = {}) {
this.isLoggingEnabled = enableLogging;
if (this.isHlsSupported) {
await this.loadHlsLibrary();
await this.createHlsInstance(hlsConfig);
}
}
/**
* Check if HLS is supported
* @private
*/
checkHlsSupport() {
const audio = document.createElement("audio");
const hasNativeHls = audio.canPlayType("application/vnd.apple.mpegurl") !== "";
this.isHlsSupported = hasNativeHls || typeof window !== "undefined";
}
/**
* Load hls.js library dynamically
* @private
*/
async loadHlsLibrary() {
if (typeof window === "undefined") return;
if (window.Hls) {
return;
}
try {
const { default: Hls } = await import('hls.js');
window.Hls = Hls;
} catch (_error) {
console.warn("HlsManager: hls.js not found, HLS streams will not be supported");
this.isHlsSupported = false;
}
}
/**
* Create HLS instance
* @private
*/
async createHlsInstance(config) {
const Hls = window.Hls;
if (!Hls || !Hls.isSupported()) {
this.isHlsSupported = false;
return;
}
const defaultConfig = {
debug: this.isLoggingEnabled,
enableWorker: true,
lowLatencyMode: false,
...config
};
this.hlsInstance = new Hls(defaultConfig);
if (this.isLoggingEnabled) {
this.attachHlsEventListeners();
}
}
/**
* Attach HLS event listeners for debugging
* @private
*/
attachHlsEventListeners() {
if (!this.hlsInstance) return;
this.hlsInstance.on(window.Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
console.error("HlsManager: Fatal error:", data);
} else {
console.warn("HlsManager: Non-fatal error:", data);
}
});
this.hlsInstance.on(window.Hls.Events.MANIFEST_LOADED, () => {
console.log("HlsManager: Manifest loaded successfully");
});
}
/**
* Load HLS media
* @param track - Media track with HLS source
*/
async loadMedia(track) {
if (!this.isHlsSupported || !this.hlsInstance) {
throw new Error("HlsManager: HLS not supported or not initialized");
}
const audioElement = globalThis.getAudioElement?.();
if (!audioElement) {
throw new Error("HlsManager: Audio element not available");
}
this.hlsInstance.attachMedia(audioElement);
this.hlsInstance.loadSource(track.source);
}
/**
* Get HLS instance
* @returns HLS instance or null
*/
getHlsInstance() {
return this.hlsInstance;
}
/**
* Check if HLS is supported
* @returns True if HLS is supported
*/
isSupported() {
return this.isHlsSupported;
}
/**
* Destroy HLS instance and cleanup
*/
async destroy() {
if (this.hlsInstance) {
this.hlsInstance.destroy();
this.hlsInstance = null;
}
}
};
// src/lib/media-session/index.ts
var globalAudioElement;
var MediaSessionManager = class {
constructor() {
this.isSupported = false;
this.currentTrack = null;
this.checkSupport();
}
/**
* Initialize Media Session API
* @param audioElement - The audio element to control
*/
async initialize(audioElement) {
if (!this.isSupported) {
console.warn("MediaSessionManager: Media Session API not supported");
return;
}
if (audioElement) {
globalAudioElement = audioElement;
}
this.setupActionHandlers();
}
/**
* Check if Media Session API is supported
* @private
*/
checkSupport() {
this.isSupported = "mediaSession" in navigator;
}
/**
* Update media metadata
* @param track - Current media track
*/
updateMetadata(track) {
if (!this.isSupported) return;
this.currentTrack = track;
navigator.mediaSession.metadata = new MediaMetadata({
title: track.title || "Unknown Title",
artist: track.artist || "Unknown Artist",
album: track.album || "Unknown Album",
artwork: track.artwork ? Array.isArray(track.artwork) ? track.artwork.map((art) => ({
src: art.src,
sizes: art.sizes || "512x512",
type: art.type || "image/png"
})) : [
{
src: track.artwork.src,
sizes: track.artwork.sizes || "512x512",
type: track.artwork.type || "image/png"
}
] : []
});
}
/**
* Update playback queue for media session
* @param queue - Current playback queue
*/
updateQueue(queue) {
if (!this.isSupported) return;
console.debug("MediaSessionManager: Queue updated with", queue.length, "tracks");
}
/**
* Set playback state
* @param state - Playback state ('playing', 'paused', 'none')
*/
setPlaybackState(state) {
if (!this.isSupported) return;
navigator.mediaSession.playbackState = state;
}
/**
* Set position state
* @param duration - Track duration in seconds
* @param position - Current position in seconds
* @param playbackRate - Current playback rate
*/
setPositionState(duration, position, playbackRate = 1) {
if (!this.isSupported) return;
try {
navigator.mediaSession.setPositionState({
duration: duration || 0,
position: position || 0,
playbackRate: playbackRate || 1
});
} catch (error) {
console.warn("MediaSessionManager: Failed to set position state:", error);
}
}
/**
* Setup media session action handlers
* @private
*/
setupActionHandlers() {
if (!this.isSupported) return;
navigator.mediaSession.setActionHandler("play", () => {
this.handleAction("play");
});
navigator.mediaSession.setActionHandler("pause", () => {
this.handleAction("pause");
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
this.handleAction("previoustrack");
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
this.handleAction("nexttrack");
});
navigator.mediaSession.setActionHandler("seekbackward", (details) => {
this.handleAction("seekbackward", details.seekOffset || 10);
});
navigator.mediaSession.setActionHandler("seekforward", (details) => {
this.handleAction("seekforward", details.seekOffset || 10);
});
navigator.mediaSession.setActionHandler("seekto", (details) => {
this.handleAction("seekto", details.seekTime || 0);
});
}
/**
* Handle media session actions
* @private
*/
handleAction(action, value) {
const audioElement = globalAudioElement;
if (!audioElement) return;
const playerInstance = globalThis.audioHeadlessInstance;
switch (action) {
case "play":
audioElement.play().catch(console.error);
break;
case "pause":
audioElement.pause();
break;
case "previoustrack":
playerInstance?.playPrevious?.();
break;
case "nexttrack":
playerInstance?.playNext?.();
break;
case "seekbackward":
if (value && audioElement.currentTime >= value) {
audioElement.currentTime -= value;
}
break;
case "seekforward":
if (value && audioElement.duration) {
audioElement.currentTime = Math.min(
audioElement.currentTime + value,
audioElement.duration
);
}
break;
case "seekto":
if (typeof value === "number" && audioElement.duration) {
audioElement.currentTime = Math.min(value, audioElement.duration);
}
break;
}
}
/**
* Clear media session metadata
*/
clearMetadata() {
if (!this.isSupported) return;
navigator.mediaSession.metadata = null;
this.currentTrack = null;
}
/**
* Check if Media Session is supported
* @returns True if supported
*/
isMediaSessionSupported() {
return this.isSupported;
}
/**
* Destroy media session and cleanup
*/
async destroy() {
if (!this.isSupported) return;
this.clearMetadata();
this.setPlaybackState("none");
const actions = [
"play",
"pause",
"previoustrack",
"nexttrack",
"seekbackward",
"seekforward",
"seekto"
];
for (const action of actions) {
try {
navigator.mediaSession.setActionHandler(action, null);
} catch {
}
}
}
};
// src/core/player.ts
var globalAudioElement2;
var AudioHeadless = class _AudioHeadless {
constructor() {
this.isEqualizerEnabled = false;
this.shouldShowNotifications = false;
this.currentTrackIndex = 0;
this.originalPlaybackQueue = [];
this.isShuffleEnabled = false;
this.currentLoopMode = "OFF";
// Audio processing modules
this.equalizerStatus = "IDEAL";
if (_AudioHeadless.playerInstance) {
console.warn(
"AudioHeadless: Multiple instances detected. Returning existing singleton instance."
);
return _AudioHeadless.playerInstance;
}
if (process.env.NODE_ENV !== PLAYER_CONSTANTS.DEVELOPMENT && globalAudioElement2) {
throw new Error("AudioHeadless: Cannot create multiple audio instances in production.");
}
_AudioHeadless.playerInstance = this;
this.audioElement = new Audio();
globalAudioElement2 = this.audioElement;
this.initializeCoreModules();
}
/**
* Initialize core utility modules
* @private
*/
initializeCoreModules() {
this.eventEmitter = new EventEmitter();
this.queueManager = new QueueManager();
}
/**
* Initialize the audio player with configuration
* @param config - Player configuration options
*/
async initialize(config) {
const {
preloadStrategy = "auto",
autoPlay = false,
useDefaultEventListeners = true,
customEventListeners = null,
showNotificationActions = false,
enablePlayLog = false,
enableHls = false,
enableEqualizer = false,
crossOrigin = null,
hlsConfig = {}
} = config;
this.audioElement.setAttribute("id", "audioheadless_instance");
this.audioElement.preload = preloadStrategy;
this.audioElement.autoplay = autoPlay;
this.audioElement.crossOrigin = crossOrigin;
this.isPlaybackLoggingEnabled = enablePlayLog;
this.isEqualizerEnabled = enableEqualizer;
this.shouldShowNotifications = showNotificationActions;
await this.initializeEventListeners(
useDefaultEventListeners,
customEventListeners,
enablePlayLog
);
if (showNotificationActions) {
this.mediaSessionManager = new MediaSessionManager();
await this.mediaSessionManager.initialize(this.audioElement);
}
if (enableHls) {
this.hlsManager = new HlsManager();
await this.hlsManager.initialize(enablePlayLog, hlsConfig);
}
if (enableEqualizer) {
await this.initializeEqualizer();
}
}
/**
* Initialize event listeners for the audio element
* @private
*/
async initializeEventListeners(_useDefault, _customListeners, _enableLogging) {
}
/**
* Initialize the equalizer module
* @private
*/
async initializeEqualizer() {
try {
this.equalizerInstance = new Equalizer();
this.equalizerInstance.initializeWithAudioElement(this.audioElement);
this.equalizerStatus = this.equalizerInstance.status();
} catch (error) {
console.error("AudioHeadless: Failed to initialize equalizer:", error);
this.equalizerStatus = "CLOSED";
}
}
// ==================== MEDIA LOADING METHODS ====================
/**
* Load a media track into the player
* @param track - The media track to load
* @param fetchFunction - Optional function to fetch track data
*/
async loadTrack(track, fetchFunction) {
if (!track) {
throw new Error("AudioHeadless: No media track provided");
}
if (fetchFunction && !track.source.length) {
this.mediaFetchFunction = fetchFunction;
}
this.updateCurrentTrackIndex(track);
const isHlsStream = track.source.includes(".m3u8");
if (this.isPlaybackLoggingEnabled) {
PlaybackUtils.calculateActualPlayedLength(globalAudioElement2, "TRACK_CHANGE");
}
if (isHlsStream && !this.audioElement.canPlayType("application/vnd.apple.mpegurl")) {
await this.loadHlsStream(track);
} else {
this.audioElement.src = track.source;
}
this.emitStateChange({
playbackState: PLAYBACK_STATES.TRACK_CHANGE,
currentTrackPlayTime: 0,
currentTrack: track
});
if (this.mediaSessionManager) {
this.mediaSessionManager.updateMetadata(track);
}
this.audioElement.load();
}
/**
* Load and immediately play a media track
* @param track - The media track to load and play
* @param fetchFunction - Optional function to fetch track data
*/
async loadAndPlay(track, fetchFunction) {
const targetTrack = track || (this.playbackQueue?.length > 0 ? this.playbackQueue[0] : void 0);
if (fetchFunction && ValidationUtils.isValidFunction(fetchFunction) && targetTrack?.source.length) {
this.mediaFetchFunction = fetchFunction;
await fetchFunction(targetTrack);
}
if (!targetTrack) {
throw new Error("AudioHeadless: No media track available for playback");
}
await this.loadTrack(targetTrack);
if (this.audioElement.readyState >= 4) {
setTimeout(async () => {
await this.play();
if (this.isEqualizerEnabled) {
await this.initializeEqualizer();
}
}, 950);
}
}
/**
* Load HLS stream using HLS manager
* @private
*/
async loadHlsStream(track) {
if (!this.hlsManager) {
console.warn(
"AudioHeadless: HLS stream detected but HLS support not enabled. Please enable HLS in player configuration."
);
await this.reset();
return;
}
const hlsInstance = this.hlsManager.getHlsInstance();
if (hlsInstance) {
hlsInstance.detachMedia();
await this.hlsManager.loadMedia(track);
}
}
// ==================== PLAYBACK CONTROL METHODS ====================
/**
* Start audio playback
*/
async play() {
const hasSource = this.audioElement.src !== "";
const isReady = this.audioElement.readyState >= 4;
if (this.audioElement.paused && isReady && hasSource) {
try {
await this.audioElement.play();
console.log("AudioHeadless: Playback started");
} catch (error) {
console.warn("AudioHeadless: Playback cancelled or failed:", error);
}
}
if (this.isEqualizerEnabled && this.equalizerStatus === "IDEAL") {
await this.initializeEqualizer();
}
}
/**
* Pause audio playback
*/
pause() {
if (this.audioElement && !this.audioElement.paused) {
this.audioElement.pause();
}
}
/**
* Stop audio playback and reset position
*/
stop() {
if (this.audioElement && !this.audioElement.paused) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
}
}
// ==================== VOLUME AND PLAYBACK RATE METHODS ====================
/**
* Set audio volume (0-100)
* @param volume - Volume level between 0 and 100
*/
setVolume(volume) {
if (volume < 0 || volume > 100) {
throw new Error("AudioHeadless: Volume must be between 0 and 100");
}
const normalizedVolume = volume / 100;
this.audioElement.volume = normalizedVolume;
this.emitStateChange({ volume });
}
/**
* Get current volume (0-100)
*/
getVolume() {
return Math.round(this.audioElement.volume * 100);
}
/**
* Set playback rate
* @param rate - Playback rate multiplier
*/
setPlaybackRate(rate) {
this.audioElement.playbackRate = rate;
this.emitStateChange({ playbackRate: rate });
}
/**
* Get current playback rate
*/
getPlaybackRate() {
return this.audioElement.playbackRate;
}
/**
* Seek to specific time in seconds
* @param seconds - Time position in seconds
*/
seekToTime(seconds) {
if (seconds < 0) {
throw new Error("AudioHeadless: Seek time cannot be negative");
}
this.audioElement.currentTime = seconds;
}
/**
* Seek by relative time offset
* @param seconds - Time offset in seconds (can be negative)
*/
seekByTime(seconds) {
const newTime = this.audioElement.currentTime + seconds;
this.seekToTime(Math.max(0, newTime));
}
/**
* Mute audio
*/
mute() {
this.audioElement.muted = true;
}
/**
* Unmute audio
*/
unmute() {
this.audioElement.muted = false;
}
/**
* Check if audio is muted
*/
isMuted() {
return this.audioElement.muted;
}
// ==================== QUEUE MANAGEMENT METHODS ====================
/**
* Set the playback queue
* @param tracks - Array of media tracks
* @param playbackType - Type of queue playback (default, reverse, shuffle)
*/
setQueue(tracks, playbackType = "DEFAULT") {
this.clearQueue();
if (!ValidationUtils.isValidArray(tracks)) {
console.warn("AudioHeadless: Invalid tracks array provided");
return;
}
this.queueManager.setQueue(tracks, playbackType);
this.playbackQueue = this.queueManager.getQueue();
if (playbackType === "SHUFFLE") {
this.isShuffleEnabled = true;
this.originalPlaybackQueue = [...tracks];
}
this.emitQueueChange();
}
/**
* Add tracks to the current queue
* @param tracks - Single track or array of tracks to add
*/
addToQueue(tracks) {
const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
if (this.playbackQueue) {
this.playbackQueue.push(...tracksArray);
} else {
this.playbackQueue = tracksArray;
}
}
/**
* Remove track from queue by ID
* @param trackId - ID of the track to remove
*/
removeFromQueue(trackId) {
if (this.playbackQueue) {
this.playbackQueue = this.playbackQueue.filter((track) => track.id !== trackId);
}
}
/**
* Clear the entire queue
*/
clearQueue() {
this.playbackQueue = [];
this.currentTrackIndex = 0;
this.originalPlaybackQueue = [];
this.isShuffleEnabled = false;
}
/**
* Get current queue
*/
getQueue() {
return this.playbackQueue || [];
}
/**
* Get current track index
*/
getCurrentTrackIndex() {
return this.currentTrackIndex;
}
/**
* Play next track in queue
*/
async playNext() {
const nextIndex = this.currentTrackIndex + 1;
if (this.playbackQueue?.length > nextIndex) {
const nextTrack = this.playbackQueue[nextIndex];
await this.loadAndPlay(nextTrack, this.mediaFetchFunction);
this.currentTrackIndex = nextIndex;
} else {
this.stop();
this.emitStateChange({ playbackState: PLAYBACK_STATES.QUEUE_ENDED });
}
}
/**
* Play previous track in queue
*/
async playPrevious() {
const previousIndex = this.currentTrackIndex - 1;
if (previousIndex >= 0) {
const previousTrack = this.playbackQueue[previousIndex];
await this.loadAndPlay(previousTrack, this.mediaFetchFunction);
this.currentTrackIndex = previousIndex;
} else {
console.log("AudioHeadless: Already at the beginning of the queue");
}
}
/**
* Play track at specific index
* @param index - Index of track to play
*/
async playTrackAt(index) {
if (index < 0 || index >= this.playbackQueue.length) {
throw new Error("AudioHeadless: Track index out of bounds");
}
const track = this.playbackQueue[index];
await this.loadAndPlay(track, this.mediaFetchFunction);
this.currentTrackIndex = index;
}
// ==================== SHUFFLE AND LOOP METHODS ====================
/**
* Enable or disable shuffle mode
* @param enabled - Whether to enable shuffle
*/
enableShuffle(enabled) {
if (enabled && !this.isShuffleEnabled) {
this.originalPlaybackQueue = [...this.playbackQueue];
this.queueManager.setQueue(this.playbackQueue, "SHUFFLE");
this.playbackQueue = this.queueManager.getQueue();
this.isShuffleEnabled = true;
} else if (!enabled && this.isShuffleEnabled) {
this.playbackQueue = [...this.originalPlaybackQueue];
this.isShuffleEnabled = false;
}
}
/**
* Check if shuffle is enabled
*/
isShuffleActive() {
return this.isShuffleEnabled;
}
/**
* Set loop mode
* @param mode - Loop mode (SINGLE, QUEUE, OFF)
*/
setLoopMo