Stupid callbacks xd.Reworked to proper await/async

This commit is contained in:
Djkáťo 2023-08-09 18:04:12 +02:00
parent dd4b45103b
commit a5df675913
7 changed files with 243 additions and 293 deletions

View file

@ -7,14 +7,17 @@ const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
.build() .build()
add_library_to_dom() add_library_to_dom()
euterpe.preload_song_async(0).then(() => { euterpe.try_preload_song(0).then(() => {
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
}, (e) => console.log(e + " Failed to preload")) }, (e) => console.log(e + " Failed to preload"))
document.querySelector("#seek")?.addEventListener("mouseup", (e) => { document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
euterpe.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => { try {
euterpe.try_seek(e.target?.valueAsNumber)
console.log("seeked to " + e.target?.valueAsNumber)
} catch {
alert("Failed seeking! " + e) alert("Failed seeking! " + e)
}) }
is_seeking = false is_seeking = false
}) })
@ -35,18 +38,18 @@ euterpe.on_time_tick((time) => {
}) })
document.querySelector("#previous")?.addEventListener("click", () => { document.querySelector("#previous")?.addEventListener("click", () => {
euterpe.previous_song_async().then(() => { euterpe.try_previous_song_looping().then(() => {
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
}, (e) => alert(e + "Failed to change song")) }, (e) => alert(e + "Failed to change song"))
}) })
document.querySelector("#next")?.addEventListener("click", () => { document.querySelector("#next")?.addEventListener("click", () => {
euterpe.next_song_async().then(() => { euterpe.try_next_song_looping().then(() => {
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
}, (e) => alert(e + "Failed to change song")) }, (e) => alert(e + "Failed to change song"))
}) })
document.querySelector("#play")?.addEventListener("click", () => { document.querySelector("#play")?.addEventListener("click", () => {
euterpe.try_play_async().catch((e) => alert("Failed to play, " + e)) euterpe.try_play().catch((e) => alert("Failed to play, " + e))
}) })
document.querySelector("#pause")?.addEventListener("click", () => { document.querySelector("#pause")?.addEventListener("click", () => {
euterpe.pause() euterpe.pause()
@ -61,7 +64,7 @@ document.querySelector("#toggle-mute")?.addEventListener("click", () => {
euterpe.mute_toggle() euterpe.mute_toggle()
}) })
document.querySelector("#toggle-play")?.addEventListener("click", () => { document.querySelector("#toggle-play")?.addEventListener("click", () => {
euterpe.play_toggle_async().catch((e) => alert("failed to toggle pause/play!" + e)) euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
}) })
document.querySelector("#volume")?.addEventListener("input", (e) => { document.querySelector("#volume")?.addEventListener("input", (e) => {
euterpe.change_volume(e.target?.valueAsNumber) euterpe.change_volume(e.target?.valueAsNumber)
@ -99,7 +102,7 @@ function add_library_to_dom() {
} }
function library_play(e: MouseEvent) { function library_play(e: MouseEvent) {
const b = e.currentTarget as HTMLButtonElement const b = e.currentTarget as HTMLButtonElement
euterpe.try_specific_song_async(parseInt(b.dataset["id"]!)).then( euterpe.try_specific_song(parseInt(b.dataset["id"]!)).then(
() => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(), () => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(),
(e) => alert(e) (e) => alert(e)
) )

View file

@ -1,6 +1,6 @@
{ {
"name": "@euterpe.js/euterpe", "name": "@euterpe.js/euterpe",
"version": "1.0.15", "version": "2.0.0",
"type": "module", "type": "module",
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!", "description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
"main": "./src/index.js", "main": "./src/index.js",

View file

@ -23,90 +23,74 @@ class Euterpe extends MusicPlayer {
super(audio_context, audio_element, track, gain, volume, current_song_path) super(audio_context, audio_element, track, gain, volume, current_song_path)
} }
/** /**
* Use to load song on page load. * Use to load song on page load.
* @throws if song with ID doesn't exist
*/ */
preload_song_async(id: number) { async try_preload_song(id: number) {
return new Promise((resolve, reject) => { const next = this.db.songs.find((song) => song!.id == id)
const next = this.db.songs.find((song) => song!.id == id) if (!next) throw new Error(`Song with id ${id} doesn't exist`)
if (!next) reject(new Error(`Song with id ${id} doesn't exist`)) else {
else { await this.try_new_song(next.url.pathname)
this.try_new_song_async(next.url.pathname).then((s) => { this.current_song = next
this.current_song = next }
resolve(s)
}, (e) => reject(e))
}
})
} }
/** /**
* Won't loop back to first song if already on the last. * Won't loop back to first song if already on the last.
* If queue present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
* @throws if on last song or song fails to start
*/ */
try_next_song_async() { async try_next_song() {
return new Promise((resolve, reject) => { let new_song: Song
let new_song: Song if (this.queue.length > 0) {
if (this.queue.length > 0) { new_song = this.queue.shift()!
new_song = this.queue.shift()! } else {
} else { let id_i = this.db.songs.length;
let id_i = this.db.songs.length; while (this.db.songs[--id_i].id! > this.current_song_id);
while (this.db.songs[--id_i].id! > this.current_song_id); const next_id = ++id_i;
const next_id = ++id_i;
if (next_id == this.db.songs.length) reject(new Error("Won't go past the last song")) if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.try_new_song_async(url).then( await this.try_new_song(url)
() => { await this.try_play()
this.try_play_async().then((s) => { if (this.current_song) this.played_history.push(this.current_song)
if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song
this.current_song = new_song this.current_song_id = new_song.id!
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
})
} }
/** /**
* Will loop back to first song if already on last song, * Will loop back to first song if already on last song,
* If queue present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
next_song_async() { async try_next_song_looping() {
return new Promise((resolve, reject) => { let new_song: Song
let new_song: Song if (this.queue.length > 0) {
if (this.queue.length > 0) { new_song = this.queue.shift()!
new_song = this.queue.shift()! } else {
} else { let id_i = this.db.songs.length;
let id_i = this.db.songs.length; while (this.db.songs[--id_i].id! > this.current_song_id);
while (this.db.songs[--id_i].id! > this.current_song_id); let next_id = ++id_i
let next_id = ++id_i
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.try_new_song_async(url).then( await this.try_new_song(url)
() => { await this.try_play()
this.try_play_async().then((s) => { if (this.current_song) this.played_history.push(this.current_song)
if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song
this.current_song = new_song this.current_song_id = new_song.id!
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
})
} }
/** /**
* Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async() * Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async()
* If queue present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
next_song() { next_song_looping() {
let new_song: Song let new_song: Song
if (this.queue.length > 0) { if (this.queue.length > 0) {
new_song = this.queue.shift()! new_song = this.queue.shift()!
@ -125,11 +109,12 @@ class Euterpe extends MusicPlayer {
this.current_song = new_song this.current_song = new_song
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
} }
/** /**
* Won't tell you if the playback was successsful, won't loop back if already on last song and will throw error if attempted. Best use next_song_async() * Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song_async()
* If queue present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
try_next_song() { next_song() {
let new_song: Song let new_song: Song
if (this.queue.length > 0) { if (this.queue.length > 0) {
new_song = this.queue.shift()! new_song = this.queue.shift()!
@ -137,7 +122,6 @@ class Euterpe extends MusicPlayer {
let id_i = this.db.songs.length; let id_i = this.db.songs.length;
while (this.db.songs[--id_i].id! > this.current_song_id); while (this.db.songs[--id_i].id! > this.current_song_id);
const next_id = ++id_i; const next_id = ++id_i;
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
@ -147,134 +131,90 @@ class Euterpe extends MusicPlayer {
this.current_song = new_song this.current_song = new_song
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
} }
/** /**
* Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed * Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed
*/ */
try_specific_song_async(new_song_id: number) { async try_specific_song(new_song_id: number) {
return new Promise((resolve, reject) => {
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
if (!new_song) reject(new Error(`No song with id "${new_song_id}" found`))
else {
this.try_new_song_async(new_song.url.pathname).then(
() => {
this.try_play_async().then((s) => {
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
}
})
}
/**
* uses play_async. Will try to play even if the audio context was suspended or closed.
*/
specific_song_async(new_song_id: number) {
return new Promise((resolve, reject) => {
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
if (!new_song) reject(new Error(`No song with id "${new_song_id}" found`))
else {
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.try_new_song_async(url).then(
() => {
this.play_async().then((s) => {
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
}
})
}
/**
* Will throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song_async() or try_specific_song_async()
*/
specific_song(new_song_id: number) {
const new_song = this.db.songs.find((song) => song.id! == new_song_id) const new_song = this.db.songs.find((song) => song.id! == new_song_id)
if (!new_song) throw new Error(`No song with id "${new_song_id}" found`) if (!new_song) throw new Error(`No song with id "${new_song_id}" found`)
else { else {
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() this.try_new_song(new_song.url.pathname)
this.new_song(url) await this.try_play()
this.play()
if (this.current_song) this.played_history.push(this.current_song) if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song this.current_song = new_song
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
} }
} }
/**
* Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song_async() or try_specific_song_async()
*/
specific_song(new_song_id: number) {
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
if (!new_song) return
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.new_song(url)
this.play()
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
}
/** /**
* Won't loop back to first song if already on the last. * Won't loop back to first song if already on the last.
* If played_history is present, uses that, if not, relies on Song ID directly from DB * If played_history is present, uses that, if not, relies on Song ID directly from DB
* @throws if playback was unsuccessful or at first song/ can't go more previous
*/ */
try_previous_song_async() { async try_previous_song() {
return new Promise((resolve, reject) => { let new_song: Song
let new_song: Song if (this.played_history.length > 0) {
if (this.played_history.length > 0) { new_song = this.played_history.pop()!
new_song = this.played_history.pop()! } else {
} else { let id_i = 0;
let id_i = 0; while (this.db.songs[++id_i].id! < this.current_song_id);
while (this.db.songs[++id_i].id! < this.current_song_id); const next_id = --id_i;
const next_id = --id_i;
if (next_id == this.db.songs.length) reject(new Error("Won't roll backwards to last song")) if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song")
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.try_new_song_async(url).then( await this.try_new_song(url)
() => { await this.try_play()
this.try_play_async().then((s) => { //if (this.current_song) this.played_history.push(this.current_song)
//if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song
this.current_song = new_song this.current_song_id = new_song.id!
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
})
} }
/** /**
* Will loop back to first song if already on the last. * Will loop back to first song if already on the last.
* If history present, uses that, if not, relies on Song ID directly from DB * If history present, uses that, if not, relies on Song ID directly from DB
*/ */
previous_song_async() { async try_previous_song_looping() {
return new Promise((resolve, reject) => { let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
} else {
let id_i = -1;
while (this.db.songs[++id_i].id! < this.current_song_id);
let next_id = --id_i;
let new_song: Song if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id!
if (this.played_history.length > 0) { new_song = this.db.songs.find((song) => song.id == next_id)!
new_song = this.played_history.pop()! }
} else { const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
let id_i = -1; await this.try_new_song(url)
while (this.db.songs[++id_i].id! < this.current_song_id); await this.try_play()
let next_id = --id_i; //if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id! this.current_song_id = new_song.id!
new_song = this.db.songs.find((song) => song.id == next_id)!
}
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.try_new_song_async(url).then(
() => {
this.try_play_async().then((s) => {
//if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
resolve(s)
}, (e) => reject(e))
},
(e) => reject(e)
)
})
} }
/** /**
* won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted. * won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted.
* If history present, uses that, if not, relies on Song ID directly from DB * If history present, uses that, if not, relies on Song ID directly from DB
*/ */
try_previous_song() { previous_song() {
let new_song: Song let new_song: Song
if (this.played_history.length > 0) { if (this.played_history.length > 0) {
new_song = this.played_history.pop()! new_song = this.played_history.pop()!
@ -293,11 +233,12 @@ class Euterpe extends MusicPlayer {
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
this.current_song = new_song this.current_song = new_song
} }
/** /**
* won't tell you if the play was successful & will loop back to last song if already on the first. * won't tell you if the play was successful & will loop back to last song if already on the first.
* If queue present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
previous_song() { previous_song_looping() {
let new_song: Song let new_song: Song
if (this.played_history.length > 0) { if (this.played_history.length > 0) {
new_song = this.played_history.pop()! new_song = this.played_history.pop()!
@ -316,6 +257,7 @@ class Euterpe extends MusicPlayer {
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
this.current_song = new_song this.current_song = new_song
} }
/** /**
* Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist * Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist
* @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix) * @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)

View file

@ -4,12 +4,12 @@ const music_player_builder = new MusicPlayerBuilder(audio_el)
const music_player = music_player_builder.build() const music_player = music_player_builder.build()
music_player.change_volume(1) music_player.change_volume(1)
music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISION (EXTENDED MIX).ogg")) music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory - NVISION (EXTENDED MIX).ogg"))
.then(() => { .then(() => {
let is_seeking = false let is_seeking = false
document.querySelector("#play")?.addEventListener("click", () => { document.querySelector("#play")?.addEventListener("click", () => {
//const analyser_node = music_player_builder.add_analyser() //const analyser_node = music_player_builder.add_analyser()
music_player.play_async() music_player.try_play()
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
}) })
document.querySelector("#pause")?.addEventListener("click", () => { document.querySelector("#pause")?.addEventListener("click", () => {
@ -25,7 +25,7 @@ music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISI
music_player.mute_toggle() music_player.mute_toggle()
}) })
document.querySelector("#toggle-play")?.addEventListener("click", () => { document.querySelector("#toggle-play")?.addEventListener("click", () => {
music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) music_player.try_play_toggle().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e))
}) })
document.querySelector("#volume")?.addEventListener("input", (e) => { document.querySelector("#volume")?.addEventListener("input", (e) => {
music_player.change_volume(e.target?.valueAsNumber) music_player.change_volume(e.target?.valueAsNumber)
@ -34,22 +34,25 @@ music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISI
is_seeking = true; is_seeking = true;
}) })
document.querySelector("#seek")?.addEventListener("mouseup", (e) => { document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => { try {
music_player.try_seek(e.target?.valueAsNumber)
console.log("seeked to " + e.target?.valueAsNumber)
} catch (e) {
alert("Failed seeking! " + e) alert("Failed seeking! " + e)
}) }
is_seeking = false is_seeking = false
}) })
// Subscriptions to AudioContext changes, eg. time.. // Subscriptions to AudioContext changes, eg. time..
music_player.subscribe_to_formatted_duration_time((time) => { music_player.on_duration_formatted((time) => {
document.querySelector("#duration").innerHTML = time document.querySelector("#duration")!.innerHTML = time
document.querySelector("#seek").max = "" + music_player.get_current_duration() document.querySelector("#seek")!.max = "" + music_player.current_song_duration
}) })
music_player.subscribe_to_formatted_current_time_tick((time) => { music_player.on_time_tick_formatted((time) => {
document.querySelector("#current").innerHTML = time document.querySelector("#current")!.innerHTML = time
}) })
music_player.subscribe_to_time_tick((time) => { music_player.on_time_tick((time) => {
if (is_seeking) return if (is_seeking) return
document.querySelector("#seek").value = "" + time document.querySelector("#seek")!.value = "" + time
}) })
}, (e) => console.log(e)) }, (e) => console.log(e))

View file

@ -1,6 +1,6 @@
{ {
"name": "@euterpe.js/player", "name": "@euterpe.js/player",
"version": "1.0.10", "version": "2.0.0",
"type": "module", "type": "module",
"description": "A simple, safe AudioContext web music player", "description": "A simple, safe AudioContext web music player",
"main": "./src/index.js", "main": "./src/index.js",

View file

@ -118,73 +118,50 @@ export class MusicPlayer {
this.volume = this.gain.gain.value = volume_i this.volume = this.gain.gain.value = volume_i
} }
/** /**
* Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed * Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed.
* Will also resume playback if player is paused (by finishing the song etc)
* @throws if "Can't seek - Audiocontext is not running"
*/ */
try_seek_async(new_time: number) { async try_seek(new_time: number) {
return new Promise((resolve, reject) => { if (this.audio_context.state !== "running") {
if (this.track.context.state !== "running") { this.is_playing = false
this.is_playing = false throw new Error("Can't seek - audioContext not running, audio_context.state : " + this.audio_context.state)
reject(new Error("Can't seek - track not playing")) }
} if (this.audio_element.paused) await this.try_play()
this.audio_element.currentTime = new_time this.audio_element.currentTime = new_time
resolve(null)
})
} }
/** /**
* Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result. * Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result.
*/ */
seek(new_time: number) { seek(new_time: number) {
this.audio_element.currentTime = new_time this.audio_element.currentTime = new_time
} }
/** /**
* Safer play_toggle_async. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed * Safer play_toggle_async. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed
* @throws Error if playback failed
*/ */
try_play_toggle_async() { async try_play_toggle() {
return new Promise((resolve, reject) => { if (this.audio_context.state !== "running") {
if (this.track.context.state !== "running") { await this.audio_context.resume()
this.audio_context.resume().then(undefined, (e) => }
reject(e)) if (this.audio_element.paused) {
} try {
if (this.audio_element.paused) { await this.audio_element.play()
this.audio_element.play().then((s) => { this.is_playing = true
this.is_playing = true } catch (e) {
resolve(s)
}, (r) => {
this.is_playing = false
reject(r)
})
} else {
this.audio_element.pause()
this.is_playing = false this.is_playing = false
resolve(null) throw e
} }
}) } else {
this.audio_element.pause()
this.is_playing = false
}
} }
/** /**
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async() * Unsafe, can just fail. Use try_play_toggle unless you don't care about the result.
*/
play_toggle_async() {
return new Promise((resolve, reject) => {
if (this.track.context.state !== "running") {
this.audio_context.resume()
}
if (this.audio_element.paused) {
this.audio_element.play().then((s) => {
this.is_playing = true
resolve(s)
}, (r) => {
this.is_playing = false
reject(r)
})
} else {
this.audio_element.pause()
this.is_playing = false
resolve(null)
}
})
}
/**
* Unsafe, throws error if failed. Use play_toggle_async or try_play_toggle_async unless you don't care about the result.
*/ */
play_toggle() { play_toggle() {
if (this.audio_element.paused) { if (this.audio_element.paused) {
@ -198,59 +175,37 @@ export class MusicPlayer {
this.audio_element.pause() this.audio_element.pause()
} }
} }
/** /**
* Safer play_async. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed * Safer play. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed
* @throws Error if playback failed
*/ */
try_play_async() { async try_play() {
return new Promise((resolve, reject) => { if (this.is_playing) return
if (this.is_playing) resolve(Error("Already playing")) if (this.audio_context.state !== "running") {
if (this.track.context.state !== "running") { await this.audio_context.resume()
this.audio_context.resume().then(() => { }
this.audio_element.play().then((s) => { if (this.audio_element.paused) {
this.is_playing = true try {
resolve(s) await this.audio_element.play()
}, (r) => {
this.is_playing = false
reject(r)
})
}, (e) =>
reject(new Error("Context closed or suspended" + JSON.stringify(e))))
} else {
this.audio_element.play().then((s) => {
this.is_playing = true
resolve(s)
}, (r) => {
this.is_playing = false
reject(r)
})
}
})
}
/**
* Will try to play even if the audio context was suspended or closed. Best to use try_play_async()
*/
play_async() {
return new Promise((resolve, reject) => {
if (this.is_playing) resolve(Error("Already playing"))
this.audio_element.play().then((s) => {
this.is_playing = true this.is_playing = true
resolve(s) } catch (e) {
}, (r) => {
this.is_playing = false this.is_playing = false
reject(r) throw e
}) }
}) }
} }
/** /**
* Unsafe, throws error if failed. Use play_async or try_play_async unless you don't care about the result. * Unsafe, can just fail. Use play_async or try_play_async unless you don't care about the result.
*/ */
play() { play() {
if (this.is_playing) return if (this.is_playing) return
this.audio_element.play().catch((r) => { this.audio_element.play().catch(() => {
this.is_playing = false this.is_playing = false
throw r
}) })
} }
/** /**
* Safe technically. Even if audioContext is suspended or closed it will pretend that it paused. * Safe technically. Even if audioContext is suspended or closed it will pretend that it paused.
*/ */
@ -258,35 +213,35 @@ export class MusicPlayer {
this.audio_element.pause() this.audio_element.pause()
this.is_playing = false this.is_playing = false
} }
/** /**
* Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback * Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback
* @throws Error if adding element throwed Error or Stalled
*/ */
try_new_song_async(path: string) { try_new_song(path: string) {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.audio_element.src = this.current_song_path = path this.audio_element.src = this.current_song_path = path
//Found out today about this. Such a nice new way to mass remove event listeners! //Found out today about this. Such a nice new way to mass remove event listeners!
const controller = new AbortController(); const controller = new AbortController();
this.audio_element.addEventListener("canplay", function canplay_listener(s) { this.audio_element.addEventListener("canplay", function canplay_listener(s) {
controller.abort() controller.abort()
resolve(s)
}, { signal: controller.signal }) }, { signal: controller.signal })
this.audio_element.addEventListener("error", function error_listener(e) { this.audio_element.addEventListener("error", function error_listener(e) {
controller.abort() controller.abort("new src error")
reject(e)
}, { signal: controller.signal }) }, { signal: controller.signal })
this.audio_element.addEventListener("stalled", function stalled_listener(e) { this.audio_element.addEventListener("stalled", function stalled_listener(e) {
controller.abort() controller.abort("new src stalled")
reject(e)
}, { signal: controller.signal }) }, { signal: controller.signal })
//once aborted, try to set current_song_duration //once aborted, try to set current_song_duration
controller.signal.addEventListener("abort", () => { controller.signal.addEventListener("abort", (r) => {
this.current_song_duration = this.audio_element.duration this.current_song_duration = this.audio_element.duration
if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason))
resolve()
}) })
this.is_playing = false this.is_playing = false
}) })
} }

47
test.js Normal file
View file

@ -0,0 +1,47 @@
class AudioContexthehe {
state = "suspended"
constructor() { }
resume() {
return new Promise((resolve, reject) => {
this.state = "running"
resolve()
})
}
}
class AudioElementHehe {
constructor() { }
play() {
return new Promise((resolve, reject) => {
console.log("playing!")
resolve()
})
}
pause() {
console.log("Pausing!")
}
}
const audio_context = new AudioContexthehe
const audio_element = new AudioElementHehe
let is_playing = false
try_play_toggle_async()
function try_play_toggle_async() {
return new Promise((resolve, reject) => {
if (audio_context.state !== "running") {
audio_context.resume().catch((e) => reject(e))
}
if (audio_element.paused) {
audio_element.play().then((s) => {
is_playing = true
resolve(s)
}, (r) => {
is_playing = false
reject(r)
})
} else {
audio_element.pause()
is_playing = false
resolve(null)
}
})
}