A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ec1b8627fd237b40bf93f5fdf2a78b05413d6783 146 lines 4.2 kB view raw
1import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js"; 2import { untracked } from "~/common/signal.js"; 3 4/** 5 * @import {InputElement} from "~/components/input/types.d.ts" 6 * @import {OutputElement} from "~/components/output/types.d.ts" 7 * @import RepeatShuffleEngine from "~/components/engine/repeat-shuffle/element.js" 8 */ 9 10//////////////////////////////////////////// 11// ELEMENT 12//////////////////////////////////////////// 13 14/** 15 * When the active queue item changes, 16 * coordinate the audio engine accordingly. 17 * 18 * Vice versa, when the audio ends, 19 * shift the queue if needed. 20 */ 21class QueueAudioOrchestrator extends BroadcastableDiffuseElement { 22 static NAME = "diffuse/orchestrator/queue-audio"; 23 24 // LIFE CYCLE 25 26 /** 27 * @override 28 */ 29 async connectedCallback() { 30 // Broadcast if needed 31 if (this.hasAttribute("group")) { 32 this.broadcast(this.identifier, {}); 33 } 34 35 // Super 36 super.connectedCallback(); 37 38 /** @type {import("~/components/engine/audio/element.js").CLASS} */ 39 this.audio = query(this, "audio-engine-selector"); 40 41 /** @type {InputElement} */ 42 this.input = query(this, "input-selector"); 43 44 /** @type {OutputElement} */ 45 this.output = query(this, "output-selector"); 46 47 /** @type {import("~/components/engine/queue/element.js").CLASS} */ 48 this.queue = query(this, "queue-engine-selector"); 49 50 /** @type {RepeatShuffleEngine} */ 51 this.repeatShuffle = query(this, "repeat-shuffle-engine-selector"); 52 53 // Wait until defined 54 await customElements.whenDefined(this.audio.localName); 55 await customElements.whenDefined(this.input.localName); 56 await customElements.whenDefined(this.queue.localName); 57 await customElements.whenDefined(this.repeatShuffle.localName); 58 59 // Effects 60 this.effect(() => this.monitorActiveQueueItem()); 61 this.effect(() => this.monitorAudioEnd()); 62 } 63 64 // 🛠️ 65 66 async monitorActiveQueueItem() { 67 const audio = this.audio; 68 const input = this.input; 69 const queue = this.queue; 70 71 if (!audio) return; 72 if (!input) return; 73 if (!queue) return; 74 75 const activeItem = queue.now(); 76 const tracksCol = this.output?.tracks.collection(); 77 const tracks = tracksCol?.state === "loaded" ? tracksCol.data : undefined; 78 79 // Read synchronously so leadership changes (e.g. tab takeover) re-trigger this effect. 80 const statusPromise = this.broadcasted ? this.broadcastingStatus() : undefined; 81 82 const activeTrack = activeItem 83 ? tracks?.find((t) => t.id === activeItem.id) 84 : undefined; 85 86 const status = statusPromise ? await statusPromise : undefined; 87 if (status && !status.leader) return; 88 89 const isPlaying = untracked(audio.isPlaying); 90 91 // Resolve active URI 92 const resolvedUri = activeTrack 93 ? await input.resolve({ method: "GET", uri: activeTrack.uri }) 94 : undefined; 95 96 // Check if we still need to render 97 if (queue.now?.()?.id !== activeItem?.id) return; 98 99 // Supply active track immediately 100 // TODO: Take URL expiration timestamp into account 101 // TODO: Add support for seeking streams 102 // (requires a lot of code, decoding audio frames, etc.) 103 const activeAudio = activeTrack && resolvedUri 104 ? [{ 105 id: activeTrack.id, 106 isPreload: false, 107 track: activeTrack, 108 ...resolvedUri, 109 }] 110 : []; 111 112 audio.supply({ 113 audio: activeAudio, 114 play: activeItem && isPlaying ? { audioId: activeItem.id } : undefined, 115 }); 116 } 117 118 async monitorAudioEnd() { 119 if (!this.audio) return; 120 if (!this.queue) return; 121 122 const now = this.queue.now(); 123 const aud = now ? this.audio.state(now.id) : undefined; 124 125 if (aud?.hasEnded() && (await this.isLeader())) { 126 if (this.repeatShuffle?.repeat() && now) { 127 this.audio.seek({ audioId: now.id, currentTime: 0 }); 128 this.audio.play({ audioId: now.id }); 129 return; 130 } 131 132 await this.queue.shift(); 133 } 134 } 135} 136 137export default QueueAudioOrchestrator; 138 139//////////////////////////////////////////// 140// REGISTER 141//////////////////////////////////////////// 142 143export const CLASS = QueueAudioOrchestrator; 144export const NAME = "do-queue-audio"; 145 146defineElement(NAME, QueueAudioOrchestrator);