forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
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);