diff options
Diffstat (limited to 'packages')
14 files changed, 133 insertions, 14 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index a50a2d7..b1489ec 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -536,6 +536,7 @@ async def update_profile( ALLOWED_PREF_KEYS = frozenset([ "notifications_disabled", "default_tab", + "music_keep_screen_on", ]) def _valid_pref_key(key: str) -> bool: diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 5ec1ac8..446360c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1593,6 +1593,12 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) { () => Object.fromEntries((groups || []).map(g => [g.id, !!g.muted]))); const [globalMute, setGlobalMute] = useState(false); const [defaultTab, setDefaultTab] = useState('chat'); + // Off by default (musicbay.md §2.2): the ordinary expectation, matching + // Spotify/Deezer, is that the phone locks on its own idle timer while + // listening. This is for whoever would rather trade battery for it — + // e.g. to ride out the WebRTC screen-lock reconnect gap without waiting + // on the automatic recovery at all. + const [keepScreenOnAudio, setKeepScreenOnAudio] = useState(false); const onLocaleChange = useCallback((e) => { const code = e.target.value; @@ -1610,10 +1616,29 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) { .then(prefs => { if (prefs.notifications_disabled === 'true') setGlobalMute(true); if (prefs.default_tab) setDefaultTab(prefs.default_tab); + if (prefs.music_keep_screen_on === 'true') setKeepScreenOnAudio(true); }) .catch(() => {}); }, [user.token]); + const toggleKeepScreenOnAudio = useCallback(async () => { + const next = !keepScreenOnAudio; + setKeepScreenOnAudio(next); + try { + await hubFetch('/v1/users/me/preferences/music_keep_screen_on', { + method: 'PUT', token: user.token, + body: { value: next ? 'true' : 'false' }, + }); + // A string, matching what a fresh page load reads from the hub + // (prefs.music_keep_screen_on === 'true' above) — music-player.js + // compares against that same string, and userPrefs is one shared bag + // fed from both this immediate update and that load. + if (onPrefsChange) onPrefsChange({ music_keep_screen_on: next ? 'true' : 'false' }); + } catch (err) { + setKeepScreenOnAudio(!next); + } + }, [keepScreenOnAudio, user.token, onPrefsChange]); + const toggleGlobalMute = useCallback(async () => { const next = !globalMute; setGlobalMute(next); @@ -1802,6 +1827,14 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) { </select> </div> <p class="settings-hint">${t('settings.default_tab_hint')}</p> + <div class="settings-row"> + <span class="settings-label">${t('settings.music_keep_screen_on')}</span> + <label class="settings-value" style="cursor:pointer"> + <input type="checkbox" checked=${keepScreenOnAudio} + onChange=${toggleKeepScreenOnAudio} /> + </label> + </div> + <p class="settings-hint">${t('settings.music_keep_screen_on_hint')}</p> </div> ${platform.isNative && html` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 8683a70..470f169 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -652,7 +652,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, bar's own unmount cleanup is what actually stops playback. */ musicQueue && html` <${MusicPlayerBar} transportRef=${transportRef} gekRef=${gekRef} queue=${musicQueue} - onClose=${() => setMusicQueue(null)} /> + userPrefs=${userPrefs} onClose=${() => setMusicQueue(null)} /> `} </div> `; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 1e367fb..b12f3f4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -259,6 +259,8 @@ export default { 'settings.defaults': 'Standardwerte', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Node-Identitäten', 'settings.node_pins_hint': 'Der Identitätsschlüssel jedes Nodes wird bei der ersten ' + 'Verbindung gemerkt. Ändert er sich, wird die Verbindung abgelehnt — das ist nur ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 5aedd7a..54f47e9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -256,6 +256,8 @@ export default { 'settings.defaults': 'Defaults', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Node identities', 'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.", 'settings.node_pins_count': { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 46a4638..03d1d62 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -257,6 +257,8 @@ export default { 'settings.defaults': 'Valores predeterminados', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Identidades de los nodes', 'settings.node_pins_hint': 'La clave de identidad de cada node se memoriza la ' + 'primera vez que se conecta. Si cambia, la conexión se rechaza — algo esperable ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 5cd8894..48289d6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -258,6 +258,8 @@ export default { 'settings.defaults': 'Valeurs par défaut', 'settings.default_tab': 'Onglet par défaut', 'settings.default_tab_hint': 'L\'onglet qui s\'ouvre en premier quand vous entrez dans un groupe.', + 'settings.music_keep_screen_on': 'Garder l\'écran allumé pendant l\'écoute', + 'settings.music_keep_screen_on_hint': 'Empêche le téléphone de se verrouiller automatiquement pendant qu\'un morceau joue. Désactivé par défaut — la plupart des gens préfèrent que leur téléphone se verrouille normalement pendant l\'écoute, comme sur Spotify ou Deezer.', 'settings.node_pins': 'Identités des nodes', 'settings.node_pins_hint': "La clé d'identité de chaque node est mémorisée lors de " + 'la première connexion. Si elle change, la connexion est refusée — ce qui n’est ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 26e4e8f..f73da13 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -258,6 +258,8 @@ export default { 'settings.defaults': 'Valori predefiniti', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Identità dei node', 'settings.node_pins_hint': "La chiave d'identità di ogni node viene memorizzata alla " + 'prima connessione. Se cambia, la connessione viene rifiutata — cosa che ci si ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 903a902..0b04cca 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -255,6 +255,8 @@ export default { 'settings.defaults': 'デフォルト', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'node の識別情報', 'settings.node_pins_hint': '各 node の識別鍵は、最初に接続したときに記憶されます。' + 'それが変わった場合、接続は拒否されます。これが起こるのは、運営者が node を' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 217aead..ef8a60b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -259,6 +259,8 @@ export default { 'settings.defaults': 'Standaardwaarden', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Node-identiteiten', 'settings.node_pins_hint': 'De identiteitssleutel van elke node wordt bij de eerste ' + 'verbinding onthouden. Verandert die, dan wordt de verbinding geweigerd — wat ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 25d67f9..63ef1f7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -270,6 +270,8 @@ export default { 'settings.defaults': 'Wartości domyślne', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Tożsamości nodes', 'settings.node_pins_hint': 'Klucz tożsamości każdego node jest zapamiętywany przy ' + 'pierwszym połączeniu. Jeśli się zmieni, połączenie zostanie odrzucone — czego ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index c32ff63..71f2829 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -259,6 +259,8 @@ export default { 'settings.defaults': 'Padrões', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'Identidades dos nodes', 'settings.node_pins_hint': 'A chave de identidade de cada node é memorizada na ' + 'primeira conexão. Se ela mudar, a conexão é recusada — o que só é esperado ' diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index ee0efdf..d88817f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -251,6 +251,8 @@ export default { 'settings.defaults': '默认值', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', + 'settings.music_keep_screen_on': 'Keep screen on during music playback', + 'settings.music_keep_screen_on_hint': 'Prevents the phone from locking on its own while a track is playing. Off by default — most people want their phone to lock normally while listening, like Spotify or Deezer.', 'settings.node_pins': 'node 身份', 'settings.node_pins_hint': '每个 node 的身份密钥都会在您首次连接时被记住。' + '如果它发生变化,连接会被拒绝——只有当运营者重装 node 时才应如此。' 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 7fdc74e..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) { @@ -258,18 +325,16 @@ function MusicPlayerBar({ transportRef, gekRef, queue, onClose }) { // 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. // - // Two ahead, not 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 + // 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 — - // it just buys more of that runway than fetching only one track ahead did. - // MAX_CACHED_BLOBS is sized for exactly this: the one playing plus these two. - const PREFETCH_AHEAD = 2; - + // 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) => { - for (let ahead = 1; ahead <= PREFETCH_AHEAD; ahead++) { - const nextEntry = tracks[order[fromPos + ahead]]; + 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(() => {}); } |