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()
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()
}, (e) => console.log(e + " Failed to preload"))
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)
})
}
is_seeking = false
})
@ -35,18 +38,18 @@ euterpe.on_time_tick((time) => {
})
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()
}, (e) => alert(e + "Failed to change song"))
})
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()
}, (e) => alert(e + "Failed to change song"))
})
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", () => {
euterpe.pause()
@ -61,7 +64,7 @@ document.querySelector("#toggle-mute")?.addEventListener("click", () => {
euterpe.mute_toggle()
})
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) => {
euterpe.change_volume(e.target?.valueAsNumber)
@ -99,7 +102,7 @@ function add_library_to_dom() {
}
function library_play(e: MouseEvent) {
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(),
(e) => alert(e)
)

View file

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

View file

@ -23,28 +23,26 @@ class Euterpe extends MusicPlayer {
super(audio_context, audio_element, track, gain, volume, current_song_path)
}
/**
* Use to load song on page load.
* @throws if song with ID doesn't exist
*/
preload_song_async(id: number) {
return new Promise((resolve, reject) => {
async try_preload_song(id: number) {
const next = this.db.songs.find((song) => song!.id == id)
if (!next) reject(new Error(`Song with id ${id} doesn't exist`))
if (!next) throw new Error(`Song with id ${id} doesn't exist`)
else {
this.try_new_song_async(next.url.pathname).then((s) => {
await this.try_new_song(next.url.pathname)
this.current_song = next
resolve(s)
}, (e) => reject(e))
}
}
})
}
/**
* 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
* @throws if on last song or song fails to start
*/
try_next_song_async() {
return new Promise((resolve, reject) => {
async try_next_song() {
let new_song: Song
if (this.queue.length > 0) {
new_song = this.queue.shift()!
@ -53,30 +51,22 @@ class Euterpe extends MusicPlayer {
while (this.db.songs[--id_i].id! > this.current_song_id);
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)!
}
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) => {
await this.try_new_song(url)
await this.try_play()
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 loop back to first song if already on last song,
* If queue present, uses that, if not, relies on Song ID directly from DB
*/
next_song_async() {
return new Promise((resolve, reject) => {
async try_next_song_looping() {
let new_song: Song
if (this.queue.length > 0) {
new_song = this.queue.shift()!
@ -89,24 +79,18 @@ class Euterpe extends MusicPlayer {
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) => {
await this.try_new_song(url)
await this.try_play()
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 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
*/
next_song() {
next_song_looping() {
let new_song: Song
if (this.queue.length > 0) {
new_song = this.queue.shift()!
@ -125,11 +109,12 @@ class Euterpe extends MusicPlayer {
this.current_song = new_song
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
*/
try_next_song() {
next_song() {
let new_song: Song
if (this.queue.length > 0) {
new_song = this.queue.shift()!
@ -137,7 +122,6 @@ class Euterpe extends MusicPlayer {
let id_i = this.db.songs.length;
while (this.db.songs[--id_i].id! > this.current_song_id);
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)!
}
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
@ -147,58 +131,28 @@ class Euterpe extends MusicPlayer {
this.current_song = new_song
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
*/
try_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 {
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) {
async try_specific_song(new_song_id: number) {
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`)
else {
this.try_new_song(new_song.url.pathname)
await this.try_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 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()
@ -206,13 +160,13 @@ class Euterpe extends MusicPlayer {
this.current_song = new_song
this.current_song_id = new_song.id!
}
}
/**
* 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
* @throws if playback was unsuccessful or at first song/ can't go more previous
*/
try_previous_song_async() {
return new Promise((resolve, reject) => {
async try_previous_song() {
let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
@ -221,30 +175,22 @@ class Euterpe extends MusicPlayer {
while (this.db.songs[++id_i].id! < this.current_song_id);
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)!
}
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) => {
await this.try_new_song(url)
await this.try_play()
//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 loop back to first song if already on the last.
* If history present, uses that, if not, relies on Song ID directly from DB
*/
previous_song_async() {
return new Promise((resolve, reject) => {
async try_previous_song_looping() {
let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
@ -257,24 +203,18 @@ class Euterpe extends MusicPlayer {
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) => {
await this.try_new_song(url)
await this.try_play()
//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.
* If history present, uses that, if not, relies on Song ID directly from DB
*/
try_previous_song() {
previous_song() {
let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
@ -293,11 +233,12 @@ class Euterpe extends MusicPlayer {
this.current_song_id = new_song.id!
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.
* If queue present, uses that, if not, relies on Song ID directly from DB
*/
previous_song() {
previous_song_looping() {
let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
@ -316,6 +257,7 @@ class Euterpe extends MusicPlayer {
this.current_song_id = new_song.id!
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
* @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()
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(() => {
let is_seeking = false
document.querySelector("#play")?.addEventListener("click", () => {
//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))
})
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()
})
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) => {
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;
})
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)
})
}
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.subscribe_to_formatted_duration_time((time) => {
document.querySelector("#duration").innerHTML = time
document.querySelector("#seek").max = "" + music_player.get_current_duration()
music_player.on_duration_formatted((time) => {
document.querySelector("#duration")!.innerHTML = time
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
})
music_player.subscribe_to_formatted_current_time_tick((time) => {
document.querySelector("#current").innerHTML = time
music_player.on_time_tick_formatted((time) => {
document.querySelector("#current")!.innerHTML = time
})
music_player.subscribe_to_time_tick((time) => {
music_player.on_time_tick((time) => {
if (is_seeking) return
document.querySelector("#seek").value = "" + time
document.querySelector("#seek")!.value = "" + time
})
}, (e) => console.log(e))

View file

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

View file

@ -118,73 +118,50 @@ export class MusicPlayer {
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) {
return new Promise((resolve, reject) => {
if (this.track.context.state !== "running") {
async try_seek(new_time: number) {
if (this.audio_context.state !== "running") {
this.is_playing = false
reject(new Error("Can't seek - track not playing"))
throw new Error("Can't seek - audioContext not running, audio_context.state : " + this.audio_context.state)
}
if (this.audio_element.paused) await this.try_play()
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.
*/
seek(new_time: number) {
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
* @throws Error if playback failed
*/
try_play_toggle_async() {
return new Promise((resolve, reject) => {
if (this.track.context.state !== "running") {
this.audio_context.resume().then(undefined, (e) =>
reject(e))
async try_play_toggle() {
if (this.audio_context.state !== "running") {
await this.audio_context.resume()
}
if (this.audio_element.paused) {
this.audio_element.play().then((s) => {
try {
await this.audio_element.play()
this.is_playing = true
resolve(s)
}, (r) => {
} catch (e) {
this.is_playing = false
reject(r)
})
throw e
}
} else {
this.audio_element.pause()
this.is_playing = false
resolve(null)
}
})
}
/**
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async()
*/
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.
* Unsafe, can just fail. Use try_play_toggle unless you don't care about the result.
*/
play_toggle() {
if (this.audio_element.paused) {
@ -198,59 +175,37 @@ export class MusicPlayer {
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() {
return new Promise((resolve, reject) => {
if (this.is_playing) resolve(Error("Already playing"))
if (this.track.context.state !== "running") {
this.audio_context.resume().then(() => {
this.audio_element.play().then((s) => {
this.is_playing = true
resolve(s)
}, (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)
})
async try_play() {
if (this.is_playing) return
if (this.audio_context.state !== "running") {
await this.audio_context.resume()
}
})
if (this.audio_element.paused) {
try {
await this.audio_element.play()
this.is_playing = true
} catch (e) {
this.is_playing = false
throw e
}
}
}
/**
* 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
resolve(s)
}, (r) => {
this.is_playing = false
reject(r)
})
})
}
/**
* 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() {
if (this.is_playing) return
this.audio_element.play().catch((r) => {
this.audio_element.play().catch(() => {
this.is_playing = false
throw r
})
}
/**
* 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.is_playing = false
}
/**
* 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) {
return new Promise((resolve, reject) => {
try_new_song(path: string) {
return new Promise<void>((resolve, reject) => {
this.audio_element.src = this.current_song_path = path
//Found out today about this. Such a nice new way to mass remove event listeners!
const controller = new AbortController();
this.audio_element.addEventListener("canplay", function canplay_listener(s) {
controller.abort()
resolve(s)
}, { signal: controller.signal })
this.audio_element.addEventListener("error", function error_listener(e) {
controller.abort()
reject(e)
controller.abort("new src error")
}, { signal: controller.signal })
this.audio_element.addEventListener("stalled", function stalled_listener(e) {
controller.abort()
reject(e)
controller.abort("new src stalled")
}, { signal: controller.signal })
//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
if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason))
resolve()
})
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)
}
})
}