A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
5.1 kB · 162 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";import { untracked } from "~/common/signal.js";
/** * @import {InputElement} from "@specs/components/input/types.d.ts" * @import {OutputElement} from "@specs/components/output/types.d.ts" * @import RepeatShuffleEngine from "~/components/engine/repeat-shuffle/element.js" */
////////////////////////////////////////////// ELEMENT////////////////////////////////////////////
/** * When the active queue item changes, * coordinate the audio engine accordingly. * * Vice versa, when the audio ends, * shift the queue if needed. */class QueueAudioOrchestrator extends BroadcastableDiffuseElement { static NAME = "diffuse/orchestrator/queue-audio";
// LIFE CYCLE
/** * @override */ async connectedCallback() { // Broadcast if needed if (this.hasAttribute("group")) { this.broadcast(this.identifier, {}); }
// Super super.connectedCallback();
/** @type {import("~/components/engine/audio/element.js").CLASS} */ this.audio = query(this, "audio-engine-selector");
/** @type {InputElement} */ this.input = query(this, "input-selector");
/** @type {OutputElement} */ this.output = query(this, "output-selector");
/** @type {import("~/components/engine/queue/element.js").CLASS} */ this.queue = query(this, "queue-engine-selector");
/** @type {RepeatShuffleEngine} */ this.repeatShuffle = query(this, "repeat-shuffle-engine-selector");
// Wait until defined await customElements.whenDefined(this.audio.localName); await customElements.whenDefined(this.input.localName); await customElements.whenDefined(this.queue.localName); await customElements.whenDefined(this.repeatShuffle.localName);
// Effects this.effect(() => this.monitorActiveQueueItem()); this.effect(() => this.monitorAudioEnd()); }
// 🛠️
async monitorActiveQueueItem() { const audio = this.audio; const input = this.input; const queue = this.queue;
if (!audio) return; if (!input) return; if (!queue) return;
const activeItem = queue.now(); const tracksCol = this.output?.tracks.collection(); const tracks = tracksCol?.state === "loaded" ? tracksCol.data : undefined;
// Read synchronously so leadership changes (e.g. tab takeover) re-trigger this effect. const statusPromise = this.broadcasted ? this.broadcastingStatus() : undefined;
const activeTrack = activeItem ? tracks?.find((t) => t.id === activeItem.id) : undefined;
const status = statusPromise ? await statusPromise : undefined; if (status && !status.leader) return;
const isPlaying = untracked(audio.isPlaying);
// Fast path: the active track may already be resolved & supplied // (e.g. the preload prelude resolved the next track ahead of time). // Reusing that URL avoids an async resolve roundtrip between the // `ended` event and the play() call — on iOS that gap deactivates the // background audio session, and the next track then silently fails to // start until the page is refocused. Blob URLs are excluded: they're // bound to the context that created them, so they always need a fresh // resolve (e.g. after a tab leadership handoff). const supplied = activeTrack ? untracked(() => audio.items()).find( (i) => i.id === activeTrack.id && i.url && !i.url.startsWith("blob:"), ) : undefined;
// Resolve active URI const resolvedUri = activeTrack ? (supplied ? { url: supplied.url, mimeType: supplied.mimeType } : await input.resolve({ method: "GET", uri: activeTrack.uri })) : undefined;
// Check if we still need to render if (queue.now?.()?.id !== activeItem?.id) return;
// Supply active track immediately // TODO: Take URL expiration timestamp into account // TODO: Add support for seeking streams // (requires a lot of code, decoding audio frames, etc.) const activeAudio = activeTrack && resolvedUri ? [{ id: activeTrack.id, isPreload: false, track: activeTrack, ...resolvedUri, }] : [];
audio.supply({ audio: activeAudio, play: activeItem && isPlaying ? { audioId: activeItem.id } : undefined, }); }
async monitorAudioEnd() { if (!this.audio) return; if (!this.queue) return;
const now = this.queue.now(); const aud = now ? this.audio.state(now.id) : undefined;
if (aud?.hasEnded() && (await this.isLeader())) { if (this.repeatShuffle?.repeat() && now) { this.audio.seek({ audioId: now.id, currentTime: 0 }); this.audio.play({ audioId: now.id }); return; }
await this.queue.shift(); } }}
export default QueueAudioOrchestrator;
////////////////////////////////////////////// REGISTER////////////////////////////////////////////
export const CLASS = QueueAudioOrchestrator;export const NAME = "do-queue-audio";
defineElement(NAME, QueueAudioOrchestrator);