improve providers managament & supports remote events

This commit is contained in:
SrGooglo 2025-02-05 02:43:36 +00:00
parent 1cd6429666
commit de17716109
3 changed files with 499 additions and 416 deletions

View File

@ -1,30 +1,9 @@
import MusicModel from "comty.js/models/music" import ComtyMusicServiceInterface from "../providers/comtymusic"
class ComtyMusicService {
static id = "default"
resolve = async (track_id) => {
return await MusicModel.getTrackData(track_id)
}
resolveMany = async (track_ids, options) => {
const response = await MusicModel.getTrackData(track_ids, options)
if (response.list) {
return response
}
return [response]
}
toggleTrackLike = async (manifest, to) => {
return await MusicModel.toggleTrackLike(manifest, to)
}
}
export default class ServiceProviders { export default class ServiceProviders {
providers = [ providers = [
new ComtyMusicService() // add by default here
new ComtyMusicServiceInterface()
] ]
findProvider(providerId) { findProvider(providerId) {
@ -35,7 +14,28 @@ export default class ServiceProviders {
this.providers.push(provider) this.providers.push(provider)
} }
// operations has(providerId) {
return this.providers.some((provider) => provider.constructor.id === providerId)
}
operation = async (operationName, providerId, manifest, args) => {
const provider = await this.findProvider(providerId)
if (!provider) {
console.error(`Failed to resolve manifest, provider [${providerId}] not registered`)
return manifest
}
const operationFn = provider[operationName]
if (typeof operationFn !== "function") {
console.error(`Failed to resolve manifest, provider [${providerId}] operation [${operationName}] not found`)
return manifest
}
return await operationFn(manifest, args)
}
resolve = async (providerId, manifest) => { resolve = async (providerId, manifest) => {
const provider = await this.findProvider(providerId) const provider = await this.findProvider(providerId)

View File

@ -1,7 +1,8 @@
import { Core } from "vessel" import { Core } from "@ragestudio/vessel"
import TrackInstance from "@classes/TrackInstance" import RemoteEvent from "@classes/RemoteEvent"
import QueueManager from "@classes/QueueManager" import QueueManager from "@classes/QueueManager"
import TrackInstance from "./classes/TrackInstance"
import MediaSession from "./classes/MediaSession" import MediaSession from "./classes/MediaSession"
import ServiceProviders from "./classes/Services" import ServiceProviders from "./classes/Services"
import PlayerState from "./classes/PlayerState" import PlayerState from "./classes/PlayerState"
@ -13,400 +14,440 @@ import setSampleRate from "./helpers/setSampleRate"
import AudioPlayerStorage from "./player.storage" import AudioPlayerStorage from "./player.storage"
export default class Player extends Core { export default class Player extends Core {
// core config // core config
static dependencies = [ static dependencies = ["api", "settings"]
"api", static namespace = "player"
"settings" static bgColor = "aquamarine"
] static textColor = "black"
static namespace = "player"
static bgColor = "aquamarine" // player config
static textColor = "black" static defaultSampleRate = 48000
static gradualFadeMs = 150
// player config static maxManifestPrecompute = 3
static defaultSampleRate = 48000
static gradualFadeMs = 150 state = new PlayerState(this)
static maxManifestPrecompute = 3 ui = new PlayerUI(this)
serviceProviders = new ServiceProviders()
state = new PlayerState(this) nativeControls = new MediaSession()
ui = new PlayerUI(this) audioContext = new AudioContext({
serviceProviders = new ServiceProviders() sampleRate:
nativeControls = new MediaSession() AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
audioContext = new AudioContext({ latencyHint: "playback",
sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate, })
latencyHint: "playback"
}) audioProcessors = new PlayerProcessors(this)
audioProcessors = new PlayerProcessors(this) queue = new QueueManager({
loadFunction: this.createInstance,
queue = new QueueManager({ })
loadFunction: this.createInstance
}) currentTrackInstance = null
currentTrackInstance = null public = {
start: this.start,
public = { close: this.close,
start: this.start, queue: this.bindableReadOnlyProxy({
close: this.close, items: () => {
playback: this.bindableReadOnlyProxy({ return this.queue.nextItems
toggle: this.togglePlayback, },
play: this.resumePlayback, add: this.addToQueue,
pause: this.pausePlayback, }),
stop: this.stopPlayback, playback: this.bindableReadOnlyProxy({
previous: this.previous, toggle: this.togglePlayback,
next: this.next, play: this.resumePlayback,
mode: this.playbackMode, pause: this.pausePlayback,
}), stop: this.stopPlayback,
controls: this.bindableReadOnlyProxy({ previous: this.previous,
duration: this.duration, next: this.next,
volume: this.volume, mode: this.playbackMode,
mute: this.mute, }),
seek: this.seek, controls: this.bindableReadOnlyProxy({
setSampleRate: setSampleRate, duration: this.duration,
}), volume: this.volume,
track: () => { mute: this.mute,
return this.queue.currentItem seek: this.seek,
}, setSampleRate: setSampleRate,
eventBus: () => { }),
return this.eventBus track: () => {
}, return this.queue.currentItem
state: this.state, },
ui: this.ui.public, eventBus: () => {
audioContext: this.audioContext, return this.eventBus
gradualFadeMs: Player.gradualFadeMs, },
} state: this.state,
ui: this.ui.public,
async afterInitialize() { audioContext: this.audioContext,
if (app.isMobile) { gradualFadeMs: Player.gradualFadeMs,
this.state.volume = 1 }
}
async afterInitialize() {
await this.nativeControls.initialize() if (app.isMobile) {
await this.audioProcessors.initialize() this.state.volume = 1
} }
// await this.nativeControls.initialize()
// Instance managing methods await this.audioProcessors.initialize()
// }
async abortPreloads() {
for await (const instance of this.queue.nextItems) { //
if (instance.abortController?.abort) { // Instance managing methods
instance.abortController.abort() //
} async abortPreloads() {
} for await (const instance of this.queue.nextItems) {
} if (instance.abortController?.abort) {
instance.abortController.abort()
async createInstance(manifest) { }
return new TrackInstance(this, manifest) }
} }
// async createInstance(manifest) {
// Playback methods return new TrackInstance(this, manifest)
// }
async play(instance, params = {}) {
if (!instance) { //
throw new Error("Audio instance is required") // Playback methods
} //
async play(instance, params = {}) {
// resume audio context if needed if (!instance) {
if (this.audioContext.state === "suspended") { throw new Error("Audio instance is required")
this.audioContext.resume() }
}
this.console.log("Initializing instance", instance)
// initialize instance if is not
if (this.queue.currentItem._initialized === false) { // resume audio context if needed
this.queue.currentItem = await instance.initialize() if (this.audioContext.state === "suspended") {
} this.audioContext.resume()
}
// update manifest
this.state.track_manifest = this.queue.currentItem.manifest // initialize instance if is not
if (this.queue.currentItem._initialized === false) {
// attach processors this.queue.currentItem = await instance.initialize()
this.queue.currentItem = await this.audioProcessors.attachProcessorsToInstance(this.queue.currentItem) }
// reconstruct audio src if is not set this.console.log("Instance", this.queue.currentItem)
if (this.queue.currentItem.audio.src !== this.queue.currentItem.manifest.source) {
this.queue.currentItem.audio.src = this.queue.currentItem.manifest.source // update manifest
} this.state.track_manifest = this.queue.currentItem.manifest
// set audio properties // attach processors
this.queue.currentItem.audio.currentTime = params.time ?? 0 this.queue.currentItem =
this.queue.currentItem.audio.muted = this.state.muted await this.audioProcessors.attachProcessorsToInstance(
this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat" this.queue.currentItem,
this.queue.currentItem.gainNode.gain.value = this.state.volume )
// play // set audio properties
await this.queue.currentItem.audio.play() this.queue.currentItem.audio.currentTime = params.time ?? 0
this.queue.currentItem.audio.muted = this.state.muted
this.console.debug(`Playing track >`, this.queue.currentItem) this.queue.currentItem.audio.loop =
this.state.playback_mode === "repeat"
// update native controls this.queue.currentItem.gainNode.gain.value = this.state.volume
this.nativeControls.update(this.queue.currentItem.manifest)
// play
return this.queue.currentItem await this.queue.currentItem.audio.play()
}
this.console.log(`Playing track >`, this.queue.currentItem)
async start(manifest, { time, startIndex = 0 } = {}) {
this.ui.attachPlayerComponent() // update native controls
this.nativeControls.update(this.queue.currentItem.manifest)
if (this.queue.currentItem) {
await this.queue.currentItem.stop() return this.queue.currentItem
} }
await this.abortPreloads() async start(manifest, { time, startIndex = 0 } = {}) {
await this.queue.flush() this.ui.attachPlayerComponent()
this.state.loading = true if (this.queue.currentItem) {
await this.queue.currentItem.stop()
let playlist = Array.isArray(manifest) ? manifest : [manifest] }
if (playlist.length === 0) { await this.abortPreloads()
this.console.warn(`Playlist is empty, aborting...`) await this.queue.flush()
return false
} this.state.loading = true
if (playlist.some((item) => typeof item === "string")) { let playlist = Array.isArray(manifest) ? manifest : [manifest]
playlist = await this.serviceProviders.resolveMany(playlist)
} if (playlist.length === 0) {
this.console.warn(`Playlist is empty, aborting...`)
for await (const [index, _manifest] of playlist.entries()) { return false
let instance = await this.createInstance(_manifest) }
this.queue.add(instance) if (playlist.some((item) => typeof item === "string")) {
} playlist = await this.serviceProviders.resolveMany(playlist)
}
const item = this.queue.set(startIndex)
for await (const [index, _manifest] of playlist.entries()) {
this.play(item, { let instance = await this.createInstance(_manifest)
time: time ?? 0
}) this.queue.add(instance)
}
return manifest
} const item = this.queue.set(startIndex)
next() { this.play(item, {
if (this.queue.currentItem) { time: time ?? 0,
this.queue.currentItem.stop() })
}
// send the event to the server
//const isRandom = this.state.playback_mode === "shuffle" if (item.manifest._id && item.manifest.service === "default") {
const item = this.queue.next() new RemoteEvent("player.play", {
identifier: "unique", // this must be unique to prevent duplicate events and ensure only have unique track events
if (!item) { track_id: item.manifest._id,
return this.stopPlayback() service: item.manifest.service,
} })
}
return manifest
}
// similar to player.start, but add to the queue
// if next is true, it will add to the queue to the top of the queue
async addToQueue(manifest, { next = false }) {
if (typeof manifest === "string") {
manifest = await this.serviceProviders.resolve(manifest)
}
let instance = await this.createInstance(manifest)
this.queue.add(instance, next === true ? "start" : "end")
console.log("Added to queue", {
manifest,
queue: this.queue,
})
}
next() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
//const isRandom = this.state.playback_mode === "shuffle"
const item = this.queue.next()
if (!item) {
return this.stopPlayback()
}
return this.play(item)
}
previous() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
return this.play(item) const item = this.queue.previous()
}
previous() { return this.play(item)
if (this.queue.currentItem) { }
this.queue.currentItem.stop()
}
const item = this.queue.previous() //
// Playback Control
//
async togglePlayback() {
if (this.state.playback_status === "paused") {
await this.resumePlayback()
} else {
await this.pausePlayback()
}
}
async pausePlayback() {
if (!this.state.playback_status === "paused") {
return true
}
return await new Promise((resolve, reject) => {
if (!this.queue.currentItem) {
this.console.error("No audio instance")
return null
}
// set gain exponentially
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
0.0001,
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
)
setTimeout(() => {
this.queue.currentItem.audio.pause()
resolve()
}, Player.gradualFadeMs)
this.nativeControls.updateIsPlaying(false)
})
}
async resumePlayback() {
if (!this.state.playback_status === "playing") {
return true
}
return await new Promise((resolve, reject) => {
if (!this.queue.currentItem) {
this.console.error("No audio instance")
return null
}
// ensure audio elemeto starts from 0 volume
this.queue.currentItem.gainNode.gain.value = 0.0001
this.queue.currentItem.audio.play().then(() => {
resolve()
})
// set gain exponentially
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
this.state.volume,
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
)
this.nativeControls.updateIsPlaying(true)
})
}
playbackMode(mode) {
if (typeof mode !== "string") {
return this.state.playback_mode
}
this.state.playback_mode = mode
if (this.queue.currentItem) {
this.queue.currentItem.audio.loop =
this.state.playback_mode === "repeat"
}
AudioPlayerStorage.set("mode", mode)
return mode
}
stopPlayback() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
this.queue.flush()
this.abortPreloads()
this.state.playback_status = "stopped"
this.state.track_manifest = null
this.queue.currentItem = null
this.track_next_instances = []
this.track_prev_instances = []
this.nativeControls.destroy()
}
//
// Audio Control
//
mute(to) {
if (app.isMobile && typeof to !== "boolean") {
this.console.warn("Cannot mute on mobile")
return false
}
if (to === "toggle") {
to = !this.state.muted
}
if (typeof to === "boolean") {
this.state.muted = to
this.queue.currentItem.audio.muted = to
}
return this.state.muted
}
volume(volume) {
if (typeof volume !== "number") {
return this.state.volume
}
if (app.isMobile) {
this.console.warn("Cannot change volume on mobile")
return false
}
if (volume > 1) {
if (!app.cores.settings.get("player.allowVolumeOver100")) {
volume = 1
}
}
if (volume < 0) {
volume = 0
}
this.state.volume = volume
AudioPlayerStorage.set("volume", volume)
if (this.queue.currentItem) {
if (this.queue.currentItem.gainNode) {
this.queue.currentItem.gainNode.gain.value = this.state.volume
}
}
return this.state.volume
}
return this.play(item) seek(time) {
} if (!this.queue.currentItem || !this.queue.currentItem.audio) {
return false
}
// // if time not provided, return current time
// Playback Control if (typeof time === "undefined") {
// return this.queue.currentItem.audio.currentTime
async togglePlayback() { }
if (this.state.playback_status === "paused") {
await this.resumePlayback()
} else {
await this.pausePlayback()
}
}
async pausePlayback() { // if time is provided, seek to that time
if (!this.state.playback_status === "paused") { if (typeof time === "number") {
return true this.console.log(
} `Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`,
)
return await new Promise((resolve, reject) => { this.queue.currentItem.audio.currentTime = time
if (!this.queue.currentItem) {
this.console.error("No audio instance") return time
return null }
} }
// set gain exponentially duration() {
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime( if (!this.queue.currentItem || !this.queue.currentItem.audio) {
0.0001, return false
this.audioContext.currentTime + (Player.gradualFadeMs / 1000) }
)
return this.queue.currentItem.audio.duration
setTimeout(() => { }
this.queue.currentItem.audio.pause()
resolve() loop(to) {
}, Player.gradualFadeMs) if (typeof to !== "boolean") {
this.console.warn("Loop must be a boolean")
this.nativeControls.updateIsPlaying(false) return false
}) }
}
this.state.loop = to ?? !this.state.loop
async resumePlayback() {
if (!this.state.playback_status === "playing") { if (this.queue.currentItem.audio) {
return true this.queue.currentItem.audio.loop = this.state.loop
} }
return await new Promise((resolve, reject) => { return this.state.loop
if (!this.queue.currentItem) { }
this.console.error("No audio instance")
return null close() {
} this.stopPlayback()
this.ui.detachPlayerComponent()
// ensure audio elemeto starts from 0 volume }
this.queue.currentItem.gainNode.gain.value = 0.0001
registerService(serviceInteface) {
this.queue.currentItem.audio.play().then(() => { this.serviceProviders.register(serviceInteface)
resolve() }
}) }
// set gain exponentially
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
this.state.volume,
this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
)
this.nativeControls.updateIsPlaying(true)
})
}
playbackMode(mode) {
if (typeof mode !== "string") {
return this.state.playback_mode
}
this.state.playback_mode = mode
if (this.queue.currentItem) {
this.queue.currentItem.audio.loop = this.state.playback_mode === "repeat"
}
AudioPlayerStorage.set("mode", mode)
return mode
}
stopPlayback() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
this.queue.flush()
this.abortPreloads()
this.state.playback_status = "stopped"
this.state.track_manifest = null
this.queue.currentItem = null
this.track_next_instances = []
this.track_prev_instances = []
this.nativeControls.destroy()
}
//
// Audio Control
//
mute(to) {
if (app.isMobile && typeof to !== "boolean") {
this.console.warn("Cannot mute on mobile")
return false
}
if (to === "toggle") {
to = !this.state.muted
}
if (typeof to === "boolean") {
this.state.muted = to
this.queue.currentItem.audio.muted = to
}
return this.state.muted
}
volume(volume) {
if (typeof volume !== "number") {
return this.state.volume
}
if (app.isMobile) {
this.console.warn("Cannot change volume on mobile")
return false
}
if (volume > 1) {
if (!app.cores.settings.get("player.allowVolumeOver100")) {
volume = 1
}
}
if (volume < 0) {
volume = 0
}
this.state.volume = volume
AudioPlayerStorage.set("volume", volume)
if (this.queue.currentItem) {
if (this.queue.currentItem.gainNode) {
this.queue.currentItem.gainNode.gain.value = this.state.volume
}
}
return this.state.volume
}
seek(time) {
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
return false
}
// if time not provided, return current time
if (typeof time === "undefined") {
return this.queue.currentItem.audio.currentTime
}
// if time is provided, seek to that time
if (typeof time === "number") {
this.console.log(`Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`)
this.queue.currentItem.audio.currentTime = time
return time
}
}
duration() {
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
return false
}
return this.queue.currentItem.audio.duration
}
loop(to) {
if (typeof to !== "boolean") {
this.console.warn("Loop must be a boolean")
return false
}
this.state.loop = to ?? !this.state.loop
if (this.queue.currentItem.audio) {
this.queue.currentItem.audio.loop = this.state.loop
}
return this.state.loop
}
close() {
this.stopPlayback()
this.ui.detachPlayerComponent()
}
}

View File

@ -0,0 +1,42 @@
import MusicModel from "comty.js/models/music"
export default class ComtyMusicServiceInterface {
static id = "default"
resolve = async (manifest) => {
if (typeof manifest === "string" && manifest.startsWith("https://")) {
return {
source: manifest.source,
service: "default",
}
}
if (typeof manifest === "string") {
manifest = {
_id: manifest,
service: ComtyMusicServiceInterface.id,
}
}
const track = await MusicModel.getTrackData(manifest._id)
return track
}
resolveLyrics = async (manifest, options) => {
return await MusicModel.getTrackLyrics(manifest._id, options)
}
resolveOverride = async (manifest) => {
// not supported yet for comty music service
return {}
}
isItemFavourited = async (manifest, itemType) => {
return await MusicModel.isItemFavourited(itemType, manifest._id)
}
toggleItemFavourite = async (manifest, itemType, to) => {
return await MusicModel.toggleItemFavourite(itemType, manifest._id, to)
}
}