summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-player.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js100
1 files changed, 92 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
index 7a3978c..ffe092d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
@@ -65,10 +65,36 @@ function shuffledOrder(n, keepFirst) {
return order;
}
-// Bounded: only the currently playing track plus a one-track read-ahead are
+// Bounded: only the currently playing track plus the read-ahead window are
// ever worth holding in memory. Older blob URLs are revoked, not merely
// dropped — otherwise every track played in a session leaks its object URL.
-const MAX_CACHED_BLOBS = 3;
+// Sized for the largest read-ahead prefetchDepth() can return (currently
+// playing + 5 on Wi-Fi) — a smaller run on cellular just evicts sooner.
+const MAX_CACHED_BLOBS = 6;
+
+/**
+ * How many tracks to warm the cache for, ahead of the one playing.
+ *
+ * A prefetched track needs no live connection to play — it is exactly what
+ * buys time through a screen-lock network gap (see transport.js's
+ * auto-reconnect) — so the more of a mobile-data budget it is safe to spend
+ * on tracks that might not even get listened to, the better the odds a lock
+ * of ordinary length is fully covered by tracks already sitting in
+ * blobCacheRef. Wi-Fi is effectively free and usually fast, so 5; a metered
+ * connection (or one this API cannot see at all) gets 3 — enough to matter,
+ * not so much it burns a noticeable chunk of a data plan on an album that
+ * might get abandoned after track one.
+ *
+ * `navigator.connection` is Chromium-only (Chrome, Edge, Electron) — plain
+ * `undefined` on Firefox and Safari, where this must fall through to the
+ * conservative tier exactly as it would for a cellular connection it could
+ * name. Never assume "fast" from the absence of a signal that says so.
+ */
+function prefetchDepth() {
+ const conn = navigator.connection;
+ if (conn && conn.type === 'wifi') return 5;
+ return 3;
+}
function loadVolume() {
try {
@@ -151,7 +177,7 @@ function QueuePanel({ tracks, order, pos, onSelect, onClose }) {
`;
}
-function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) {
+function MusicPlayerBar({ transportRef, gekRef, queue, onClose, userPrefs }) {
const audioRef = useRef(null);
const blobCacheRef = useRef(new Map()); // file id -> { url, order: insertion index }
const blobInsertRef = useRef(0);
@@ -201,6 +227,47 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) {
for (const { url } of blobCacheRef.current.values()) URL.revokeObjectURL(url);
}, []);
+ // Screen Wake Lock, opt-in only (Settings → music_keep_screen_on) and only
+ // while a track is actually playing — off by default because the ordinary
+ // expectation, matching Spotify/Deezer, is that the phone locks on its own
+ // idle timer while listening (docs/musicbay.md §2.2). Unlike the video
+ // player's unconditional lock, this must not fight that default for
+ // everyone who never asked for it; it exists for whoever explicitly wants
+ // to trade battery for riding out the WebRTC screen-lock reconnect gap
+ // without waiting on it at all.
+ useEffect(() => {
+ if (!playing) return;
+ if (!(userPrefs && userPrefs.music_keep_screen_on === 'true')) return;
+ if (!('wakeLock' in navigator)) return;
+ let sentinel = null;
+ let cancelled = false;
+ const acquire = async () => {
+ try {
+ const wl = await navigator.wakeLock.request('screen');
+ if (cancelled) { try { wl.release(); } catch { /* ignore */ } return; }
+ sentinel = wl;
+ wl.addEventListener('release', () => { sentinel = null; });
+ } catch (e) {
+ // Battery saver, no permission, an insecure context — playback has
+ // never depended on this, so there is nothing to fall back to.
+ console.warn('[MeshBay] Wake lock request failed:', e.message);
+ }
+ };
+ acquire();
+ // Released automatically the moment the page goes hidden (spec
+ // behaviour) — re-requested here so it holds again once foregrounded,
+ // same as the video player's handling of the same event.
+ const onVisibility = () => {
+ if (document.visibilityState === 'visible' && !sentinel) acquire();
+ };
+ document.addEventListener('visibilitychange', onVisibility);
+ return () => {
+ cancelled = true;
+ document.removeEventListener('visibilitychange', onVisibility);
+ if (sentinel) { try { sentinel.release(); } catch { /* already released */ } }
+ };
+ }, [playing, userPrefs && userPrefs.music_keep_screen_on]);
+
const evictOldBlobs = useCallback(() => {
const cache = blobCacheRef.current;
while (cache.size > MAX_CACHED_BLOBS) {
@@ -218,7 +285,14 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) {
const cached = blobCacheRef.current.get(entry.id);
if (cached) return cached.url;
const transport = transportRef.current;
- if (!transport || !transport.connected) throw new Error(t('music.err_transport'));
+ if (!transport) throw new Error(t('music.err_transport'));
+ // A track ending (or "next") right after a screen-lock reconnect started
+ // is exactly when this used to throw: `connected` was still false because
+ // the reconnect it only had to wait a few seconds for hadn't landed yet.
+ // waitForReconnect is a no-op when nothing is in flight, so this costs
+ // nothing on the ordinary path.
+ if (!transport.connected) await transport.waitForReconnect();
+ if (!transport.connected) throw new Error(t('music.err_transport'));
let downloadId = entry.id;
let downloadSize = entry.size;
@@ -248,12 +322,22 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) {
return url;
}, [transportRef, gekRef, evictOldBlobs]);
- // Silently warms the cache for the next track so pressing "next" doesn't
+ // Silently warms the cache for the next tracks so pressing "next" doesn't
// visibly wait (musicbay.md §2.2) — best-effort, never surfaces an error.
+ //
+ // More than one: a screen lock can cost the transport several minutes (see
+ // the WebRTC auto-reconnect in transport.js — this is the other half of
+ // the same fix). A track already sitting in blobCacheRef needs no
+ // connection at all to play, so whatever got fetched *before* the lock
+ // started plays through it regardless of what the connection is doing
+ // afterward — see prefetchDepth() for how far ahead that runway goes.
const prefetchNext = useCallback((fromPos) => {
- const nextEntry = tracks[order[fromPos + 1]];
- if (!nextEntry || blobCacheRef.current.has(nextEntry.id)) return;
- fetchTrackBlob(nextEntry).catch(() => {});
+ const ahead = prefetchDepth();
+ for (let i = 1; i <= ahead; i++) {
+ const nextEntry = tracks[order[fromPos + i]];
+ if (!nextEntry || blobCacheRef.current.has(nextEntry.id)) continue;
+ fetchTrackBlob(nextEntry).catch(() => {});
+ }
}, [tracks, order, fetchTrackBlob]);
// (Re)initialize the queue whenever the shell hands over a new one.