Rewild Your Web
18
fork

Configure Feed

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

at main 115 lines 3.7 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3/** 4 * P2P media session broadcast — shared between desktop and media center. 5 * Uses BroadcastChannel("beaver-media-control") to sync media state 6 * with paired peers and handle remote control actions. 7 * 8 * Protocol messages: 9 * { type: "state", sessionId, deviceName, title, artist, album, playbackState, duration, position } 10 * { type: "action", sessionId, action } 11 * { type: "hello" } 12 */ 13 14export class MediaBroadcaster { 15 #channel = new BroadcastChannel("beaver-media-control"); 16 #sessionId = null; 17 #deviceName = "Unknown device"; 18 #state = { 19 title: "", 20 artist: "", 21 album: "", 22 playbackState: "none", 23 duration: 0, 24 position: 0, 25 }; 26 #onRemoteAction; // (action: string) => void 27 #onRemoteState; // (data: object) => void — optional, for showing remote media 28 29 /** 30 * @param {object} opts 31 * @param {string} opts.deviceName — display name for this device 32 * @param {(action: string) => void} opts.onRemoteAction — called when a peer sends a control action 33 * @param {(data: object) => void} [opts.onRemoteState] — called when a peer broadcasts its state 34 */ 35 constructor({ deviceName, onRemoteAction, onRemoteState }) { 36 this.#deviceName = deviceName || "Unknown device"; 37 this.#onRemoteAction = onRemoteAction; 38 this.#onRemoteState = onRemoteState; 39 40 this.#channel.onmessage = (e) => this.#handleMessage(e.data); 41 } 42 43 /** Resolve the device name lazily from the pairing API. */ 44 async resolveDeviceName() { 45 try { 46 const info = await navigator.embedder.pairing.local(); 47 this.#deviceName = info.displayName || this.#deviceName; 48 } catch {} 49 } 50 51 /** Update local media state and broadcast to peers. */ 52 updateState(detail) { 53 // Generate session ID on first media event 54 if (!this.#sessionId) { 55 this.#sessionId = `ms-${Date.now()}-${Math.random().toString(36).slice(2)}`; 56 } 57 58 if (detail.title) this.#state.title = detail.title; 59 if (detail.artist) this.#state.artist = detail.artist; 60 if (detail.album) this.#state.album = detail.album; 61 if (detail.playbackState) this.#state.playbackState = detail.playbackState; 62 if (detail.duration) this.#state.duration = detail.duration; 63 if (detail.position) this.#state.position = detail.position; 64 65 this.#broadcast(); 66 67 // Clear session on playback stop 68 if (detail.playbackState === "none") { 69 this.#sessionId = null; 70 this.#state = { 71 title: "", artist: "", album: "", 72 playbackState: "none", duration: 0, position: 0, 73 }; 74 } 75 } 76 77 /** Send a hello to discover active sessions on peers. */ 78 sendHello() { 79 this.#channel.postMessage({ type: "hello" }); 80 } 81 82 /** Send a control action to a remote session. */ 83 sendAction(sessionId, action) { 84 this.#channel.postMessage({ type: "action", sessionId, action }); 85 } 86 87 #broadcast() { 88 if (!this.#sessionId) return; 89 this.#channel.postMessage({ 90 type: "state", 91 sessionId: this.#sessionId, 92 deviceName: this.#deviceName, 93 ...this.#state, 94 }); 95 } 96 97 #handleMessage(data) { 98 if (data.type === "hello") { 99 // Peer joined — respond with our state if active 100 if (this.#sessionId && this.#state.playbackState !== "none") { 101 this.#broadcast(); 102 } 103 } else if (data.type === "action") { 104 // Peer wants to control our media 105 if (data.sessionId === this.#sessionId && this.#onRemoteAction) { 106 this.#onRemoteAction(data.action); 107 } 108 } else if (data.type === "state") { 109 // Peer is broadcasting its state — ignore our own 110 if (data.sessionId !== this.#sessionId && this.#onRemoteState) { 111 this.#onRemoteState(data); 112 } 113 } 114 } 115}