tangled
alpha
login
or
join now
tokono.ma
/
diffuse
5
fork
atom
A music player that connects to your cloud/distributed storage.
5
fork
atom
overview
issues
4
pulls
pipelines
feat: broadcasted audio
Steven Vandevelde
3 months ago
5a8a355e
de03e4cd
+119
-21
2 changed files
expand all
collapse all
unified
split
src
common
element.js
components
engine
audio
element.js
+8
-3
src/common/element.js
···
7
7
import { BrowserPostMessageIo } from "./worker/rpc.js";
8
8
9
9
/**
10
10
-
* @import {BroadcastingStatus, ProvisionedWorker, ProvisionedWorkers, WorkerOpts} from "./element.d.ts"
10
10
+
* @import {BroadcastingStatus, WorkerOpts} from "./element.d.ts"
11
11
* @import {ProxiedActions, Tunnel} from "./worker.d.ts";
12
12
* @import {Signal} from "./signal.d.ts"
13
13
*/
14
14
15
15
+
export { nothing } from "lit-html";
15
16
export const DEFAULT_GROUP = "default";
16
17
17
18
/**
···
22
23
#connected = Promise.withResolvers();
23
24
#disposables = /** @type {Array<() => void>} */ ([]);
24
25
26
26
+
/** */
25
27
constructor() {
26
28
super();
27
27
-
28
28
-
this.group = this.getAttribute("group") ?? DEFAULT_GROUP;
29
29
30
30
this.worker = this.worker.bind(this);
31
31
this.workerLink = this.workerLink.bind(this);
···
53
53
/** */
54
54
forceRender() {
55
55
return this.#render();
56
56
+
}
57
57
+
58
58
+
/** */
59
59
+
get group() {
60
60
+
return this.getAttribute("group") ?? DEFAULT_GROUP;
56
61
}
57
62
58
63
/** */
+111
-18
src/components/engine/audio/element.js
···
1
1
import { keyed } from "lit-html/directives/keyed.js";
2
2
3
3
-
import { BroadcastableDiffuseElement } from "@common/element.js";
4
4
-
import { computed, signal } from "@common/signal.js";
3
3
+
import { BroadcastableDiffuseElement, nothing } from "@common/element.js";
4
4
+
import { computed, signal, untracked } from "@common/signal.js";
5
5
6
6
/**
7
7
* @import {Actions, Audio, AudioState, AudioStateReadOnly, LoadingState} from "./types.d.ts"
···
48
48
* @override
49
49
*/
50
50
connectedCallback() {
51
51
-
// Setup leader election if shared
51
51
+
// Setup broadcasting if part of group
52
52
if (this.hasAttribute("group")) {
53
53
const actions = this.broadcast(
54
54
this.nameWithGroup(),
55
55
{
56
56
-
adjustVolume: { strategy: "leaderOnly", fn: this.adjustVolume },
56
56
+
adjustVolume: { strategy: "replicate", fn: this.adjustVolume },
57
57
pause: { strategy: "leaderOnly", fn: this.pause },
58
58
play: { strategy: "leaderOnly", fn: this.play },
59
59
seek: { strategy: "leaderOnly", fn: this.seek },
···
86
86
if (this.broadcasted) {
87
87
this.effect(async () => {
88
88
const status = await this.broadcastingStatus();
89
89
-
if (status.leader && status.initialLeader === false) {
90
90
-
console.log("🧙 Leadership acquired (no actions performed)");
91
91
-
}
89
89
+
untracked(() => {
90
90
+
if (!(status.leader && status.initialLeader === false)) return;
91
91
+
92
92
+
console.log("🧙 Leadership acquired");
93
93
+
this.items().forEach((item) => {
94
94
+
const el = this.#itemElement(item.id);
95
95
+
if (!el) return;
96
96
+
97
97
+
el.removeAttribute("initial-progress");
98
98
+
99
99
+
if (!el.audio) return;
100
100
+
101
101
+
const progress = el.$state.progress.value;
102
102
+
const canPlay = () => {
103
103
+
this.seek({
104
104
+
audioId: item.id,
105
105
+
percentage: progress,
106
106
+
});
107
107
+
108
108
+
if (el.$state.isPlaying.value) this.play({ audioId: item.id });
109
109
+
};
110
110
+
111
111
+
el.audio.addEventListener("canplay", canPlay, { once: true });
112
112
+
113
113
+
if (el.audio.readyState === 0) el.audio.load();
114
114
+
else canPlay();
115
115
+
});
116
116
+
});
92
117
});
93
118
}
94
119
···
211
236
if (source) source.src = SILENT_MP3;
212
237
});
213
238
239
239
+
const group = this.group;
214
240
const nodes = this.items().map((audio) => {
215
241
const ip = audio.progress === undefined
216
242
? "0"
···
220
246
audio.id,
221
247
html`
222
248
<de-audio-item
249
249
+
group="${this.broadcasted ? `${group}/${audio.id}` : nothing}"
223
250
id="${audio.id}"
224
251
initial-progress="${ip}"
252
252
+
mime-type="${audio.mimeType ? audio.mimeType : nothing}"
253
253
+
preload="${audio.isPreload ? `preload` : nothing}"
225
254
url="${audio.url}"
226
226
-
${audio.isPreload ? "preload" : ""}
227
227
-
${audio.mimeType ? 'mime-type="' + audio.mimeType + '"' : ""}
228
255
>
229
256
<audio
230
257
crossorigin="anonymous"
···
327
354
// ITEM ELEMENT
328
355
////////////////////////////////////////////
329
356
330
330
-
class AudioEngineItem extends HTMLElement {
357
357
+
class AudioEngineItem extends BroadcastableDiffuseElement {
358
358
+
static NAME = "diffuse/engine/audio/item";
359
359
+
331
360
constructor() {
332
361
super();
333
362
···
344
373
loadingState: signal(/** @type {LoadingState} */ ("loading")),
345
374
progress: signal(ip ? parseFloat(ip) : 0),
346
375
};
376
376
+
}
347
377
378
378
+
// LIFECYCLE
379
379
+
380
380
+
/**
381
381
+
* @override
382
382
+
*/
383
383
+
connectedCallback() {
348
384
const audio = this.audio;
349
385
350
386
audio.addEventListener("canplay", this.canplayEvent);
···
356
392
audio.addEventListener("suspend", this.suspendEvent);
357
393
audio.addEventListener("timeupdate", this.timeupdateEvent);
358
394
audio.addEventListener("waiting", this.waitingEvent);
359
359
-
}
395
395
+
396
396
+
// Setup broadcasting if part of group
397
397
+
if (this.hasAttribute("group")) {
398
398
+
const actions = this.broadcast(
399
399
+
this.nameWithGroup(),
400
400
+
{
401
401
+
getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get },
402
402
+
getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get },
403
403
+
getIsPlaying: {
404
404
+
strategy: "leaderOnly",
405
405
+
fn: this.$state.isPlaying.get,
406
406
+
},
407
407
+
getIsPreload: {
408
408
+
strategy: "leaderOnly",
409
409
+
fn: this.$state.isPreload.get,
410
410
+
},
411
411
+
getLoadingState: {
412
412
+
strategy: "leaderOnly",
413
413
+
fn: this.$state.loadingState.get,
414
414
+
},
415
415
+
getProgress: { strategy: "leaderOnly", fn: this.$state.progress.get },
360
416
361
361
-
// LIFECYCLE
417
417
+
// SET
418
418
+
setDuration: { strategy: "replicate", fn: this.$state.duration.set },
419
419
+
setHasEnded: { strategy: "replicate", fn: this.$state.hasEnded.set },
420
420
+
setIsPlaying: {
421
421
+
strategy: "replicate",
422
422
+
fn: this.$state.isPlaying.set,
423
423
+
},
424
424
+
setIsPreload: {
425
425
+
strategy: "replicate",
426
426
+
fn: this.$state.isPreload.set,
427
427
+
},
428
428
+
setLoadingState: {
429
429
+
strategy: "replicate",
430
430
+
fn: this.$state.loadingState.set,
431
431
+
},
432
432
+
setProgress: { strategy: "replicate", fn: this.$state.progress.set },
433
433
+
},
434
434
+
);
362
435
363
363
-
connectedCallback() {
436
436
+
if (actions) {
437
437
+
this.$state.duration.set = actions.setDuration;
438
438
+
this.$state.hasEnded.set = actions.setHasEnded;
439
439
+
this.$state.isPlaying.set = actions.setIsPlaying;
440
440
+
this.$state.isPreload.set = actions.setIsPreload;
441
441
+
this.$state.loadingState.set = actions.setLoadingState;
442
442
+
this.$state.progress.set = actions.setProgress;
443
443
+
444
444
+
untracked(async () => {
445
445
+
this.$state.duration.value = await actions.getDuration();
446
446
+
this.$state.hasEnded.value = await actions.getHasEnded();
447
447
+
this.$state.isPlaying.value = await actions.getIsPlaying();
448
448
+
this.$state.isPreload.value = await actions.getIsPreload();
449
449
+
this.$state.loadingState.value = await actions.getLoadingState();
450
450
+
this.$state.progress.value = await actions.getProgress();
451
451
+
});
452
452
+
}
453
453
+
}
454
454
+
455
455
+
// Super
456
456
+
super.connectedCallback();
364
457
}
365
458
366
459
// STATE
···
488
581
*/
489
582
timeupdateEvent(event) {
490
583
const audio = /** @type {HTMLAudioElement} */ (event.target);
584
584
+
if (isNaN(audio.duration) || audio.duration === 0) return;
491
585
492
492
-
engineItem(audio)?.$state.progress.set(
493
493
-
isNaN(audio.duration) || audio.duration === 0
494
494
-
? 0
495
495
-
: audio.currentTime / audio.duration,
496
496
-
);
586
586
+
const progress = audio.currentTime / audio.duration;
587
587
+
if (progress === 0) return;
588
588
+
589
589
+
engineItem(audio)?.$state.progress.set(progress);
497
590
}
498
591
499
592
/**