From 0ba7d614ee4e6a438b2ead21f61c16643fe1cb12 Mon Sep 17 00:00:00 2001 From: William Floyd Date: Mon, 15 Dec 2025 15:56:31 -0600 Subject: [PATCH 1/5] feat: Add option to automatically play on load, or when page url ends in `#autoplay --- src/components/Group.tsx | 1 + src/components/Server.tsx | 3 ++- src/components/Settings.tsx | 3 +++ src/components/SnapWeb.tsx | 8 +++++++- src/config.ts | 9 ++++++++- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/Group.tsx b/src/components/Group.tsx index 3831e3a..62b4246 100644 --- a/src/components/Group.tsx +++ b/src/components/Group.tsx @@ -19,6 +19,7 @@ type GroupProps = { group: Snapcast.Group; snapcontrol: SnapControl; showOffline: boolean; + autoPlay: boolean; }; type GroupVolumeChange = { diff --git a/src/components/Server.tsx b/src/components/Server.tsx index d1ce61d..543457b 100644 --- a/src/components/Server.tsx +++ b/src/components/Server.tsx @@ -7,13 +7,14 @@ type ServerProps = { server: Snapcast.Server; snapcontrol: SnapControl; showOffline: boolean; + autoPlay: boolean; }; export default function Server(props: ServerProps) { console.log("Render Server"); return ( - {props.server.groups.map(group => )} + {props.server.groups.map(group => )} ); } diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 305644d..cf4d83b 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -6,12 +6,14 @@ export default function SettingsDialog(props: { open: boolean, onClose: (_apply: const [serverurl, setServerurl] = useState(config.baseUrl); const [theme, setTheme] = useState(config.theme); const [showOffline, setShowOffline] = useState(config.showOffline); + const [autoPlay, setAutoPlay] = useState(config.autoPlay); function handleClose(apply: boolean) { if (apply) { config.baseUrl = serverurl; config.theme = theme; config.showOffline = showOffline; + config.autoPlay = autoPlay; } props.onClose(apply); } @@ -43,6 +45,7 @@ export default function SettingsDialog(props: { open: boolean, onClose: (_apply: , checked: boolean) => setShowOffline(checked)} />} label="Show offline clients" /> + , checked: boolean) => setAutoPlay(checked)} />} label="Automatically connect on load" /> diff --git a/src/components/SnapWeb.tsx b/src/components/SnapWeb.tsx index 73a302e..d21be45 100644 --- a/src/components/SnapWeb.tsx +++ b/src/components/SnapWeb.tsx @@ -12,6 +12,7 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import silence from '../assets/10-seconds-of-silence.mp3'; import snapcast512 from '../assets/snapcast-512.png'; +import { log } from 'node:console'; const lightTheme = createTheme({ @@ -93,6 +94,7 @@ export default function SnapWeb() { const [server, setServer] = useState(new Snapcast.Server()); const [drawerOpen, setDrawerOpen] = useState(false); const [showOffline, setShowOffline] = useState(config.showOffline); + const [autoPlay, setAutoPlay] = useState(config.autoPlay); const [theme, setTheme] = useState(config.theme); const [serverUrl, setServerUrl] = useState(config.baseUrl); const [aboutOpen, setAboutOpen] = useState(false); @@ -144,6 +146,9 @@ export default function SnapWeb() { setConnectError(error); } setConnected(connected); + if (autoPlay || (document.location.hash.match(/autoplay/) !== null)) { + setIsPlaying(true); + } }; @@ -381,7 +386,7 @@ export default function SnapWeb() { > {list()} - + {snackbar()} { setAboutOpen(false); }} /> { @@ -391,6 +396,7 @@ export default function SnapWeb() { setServerUrl(config.baseUrl); setTheme(config.theme); setShowOffline(config.showOffline); + setAutoPlay(config.autoPlay); } }} /> diff --git a/src/config.ts b/src/config.ts index f2ac686..8ac52e8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,8 @@ const host = import.meta.env.VITE_APP_SNAPSERVER_HOST || window.location.host; const keys = { snapserver_host: "snapserver.host", theme: "theme", - showoffline: "showoffline" + showoffline: "showoffline", + autoPlay: "autoPlay" } enum Theme { @@ -48,6 +49,12 @@ const config = { }, set showOffline(value: boolean) { setPersistentValue(keys.showoffline, String(value)); + }, + get autoPlay() { + return getPersistentValue(keys.autoPlay, String(false)) === String(true); + }, + set autoPlay(value: boolean) { + setPersistentValue(keys.autoPlay, String(value)); } }; From 846ad491832f7cd879901a2a78ae70f8b1002c4b Mon Sep 17 00:00:00 2001 From: William Floyd Date: Tue, 16 Dec 2025 10:17:28 -0600 Subject: [PATCH 2/5] fix: Re-add debug log --- src/components/SnapWeb.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SnapWeb.tsx b/src/components/SnapWeb.tsx index d21be45..8806598 100644 --- a/src/components/SnapWeb.tsx +++ b/src/components/SnapWeb.tsx @@ -12,7 +12,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import silence from '../assets/10-seconds-of-silence.mp3'; import snapcast512 from '../assets/snapcast-512.png'; -import { log } from 'node:console'; const lightTheme = createTheme({ @@ -147,6 +146,7 @@ export default function SnapWeb() { } setConnected(connected); if (autoPlay || (document.location.hash.match(/autoplay/) !== null)) { + console.debug("Autoplaying") setIsPlaying(true); } }; From ebfbf65b389efce393042fb88e57ec1a1f1e92b7 Mon Sep 17 00:00:00 2001 From: William Floyd Date: Fri, 26 Dec 2025 14:12:58 -0600 Subject: [PATCH 3/5] feat: Add snackbar alert for autoplay failure --- src/components/SnapWeb.tsx | 43 ++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/components/SnapWeb.tsx b/src/components/SnapWeb.tsx index 8806598..e32a6e2 100644 --- a/src/components/SnapWeb.tsx +++ b/src/components/SnapWeb.tsx @@ -94,6 +94,7 @@ export default function SnapWeb() { const [drawerOpen, setDrawerOpen] = useState(false); const [showOffline, setShowOffline] = useState(config.showOffline); const [autoPlay, setAutoPlay] = useState(config.autoPlay); + const [autoplaySuccess, setAutoplaySuccess] = useState(true); const [theme, setTheme] = useState(config.theme); const [serverUrl, setServerUrl] = useState(config.baseUrl); const [aboutOpen, setAboutOpen] = useState(false); @@ -135,6 +136,10 @@ export default function SnapWeb() { updateMediaSession(); } + function shouldAutoplay(): boolean { + return (autoPlay || (document.location.hash.match(/autoplay/) !== null)); + } + snapControlRef.current.onChange = (_control: SnapControl, server: Snapcast.Server) => handleChange(server); snapControlRef.current.onConnectionChanged = (_control: SnapControl, connected: boolean, error?: string) => { console.log("Connection state changed: " + connected + ", error: " + error); @@ -145,8 +150,8 @@ export default function SnapWeb() { setConnectError(error); } setConnected(connected); - if (autoPlay || (document.location.hash.match(/autoplay/) !== null)) { - console.debug("Autoplaying") + if (shouldAutoplay()) { + console.debug("Attempting autoplay") setIsPlaying(true); } }; @@ -282,9 +287,22 @@ export default function SnapWeb() { console.debug("isPlaying changed to true"); audioRef.current.src = silence; audioRef.current.loop = true; - audioRef.current.play().then(() => { - snapstreamRef.current = new SnapStream(config.baseUrl); - }); + audioRef.current.play().then( + () => { + setAutoplaySuccess(true) + snapstreamRef.current = new SnapStream(config.baseUrl); + }, + (error) => { + setAutoplaySuccess(false) + if (snapstreamRef.current) + snapstreamRef.current.stop(); + snapstreamRef.current = null; + audioRef.current.pause(); + audioRef.current.src = ''; + setIsPlaying(false) + console.error("Playing failed, likely due to disallowed autoplay:", error) + } + ); // updateMediaSession(); // }); } else { @@ -327,6 +345,19 @@ export default function SnapWeb() { function snackbar() { if (isConnected) { + if (shouldAutoplay() && !autoplaySuccess) { + return ( + { if (reason !== 'clickaway') { console.log("Snackbar - onClose") } }}> + { console.log("Snackbar - alert onClose") }} severity="error" sx={{ width: '100%' }}> + Autoplay failed + + + ) + } return (null); } return ( @@ -375,7 +406,7 @@ export default function SnapWeb() { sx={{ mr: 2 }} onClick={(_) => { setIsPlaying(!isPlaying); }} > - {isPlaying ? : } + {isPlaying && autoplaySuccess ? : } : } From 49629240365bb107ec4369c36b44f4668c8c4dff Mon Sep 17 00:00:00 2001 From: William Floyd Date: Fri, 26 Dec 2025 14:13:32 -0600 Subject: [PATCH 4/5] fix: Tweak setting description text for Autoplay --- src/components/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index cf4d83b..7f1c036 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -45,7 +45,7 @@ export default function SettingsDialog(props: { open: boolean, onClose: (_apply: , checked: boolean) => setShowOffline(checked)} />} label="Show offline clients" /> - , checked: boolean) => setAutoPlay(checked)} />} label="Automatically connect on load" /> + , checked: boolean) => setAutoPlay(checked)} />} label="Autoplay on connect" /> From 16f48328ab2ec19a9323f186175dc657a3395e8c Mon Sep 17 00:00:00 2001 From: William Floyd Date: Fri, 26 Dec 2025 14:32:16 -0600 Subject: [PATCH 5/5] feat: Check browser autoplay policy first before trying autoplay --- src/components/SnapWeb.tsx | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/components/SnapWeb.tsx b/src/components/SnapWeb.tsx index e32a6e2..77b22d3 100644 --- a/src/components/SnapWeb.tsx +++ b/src/components/SnapWeb.tsx @@ -140,6 +140,21 @@ export default function SnapWeb() { return (autoPlay || (document.location.hash.match(/autoplay/) !== null)); } + function isAutoplaySupported(): string { + if ("getAutoplayPolicy" in navigator) { + try { + const policy = (navigator as any).getAutoplayPolicy("mediaelement"); + return policy; + } catch (error) { + console.warn("getAutoplayPolicy failed:", error); + } + } + + return "unknown" + }; + + const autoplayPolicy = isAutoplaySupported() + snapControlRef.current.onChange = (_control: SnapControl, server: Snapcast.Server) => handleChange(server); snapControlRef.current.onConnectionChanged = (_control: SnapControl, connected: boolean, error?: string) => { console.log("Connection state changed: " + connected + ", error: " + error); @@ -151,8 +166,16 @@ export default function SnapWeb() { } setConnected(connected); if (shouldAutoplay()) { - console.debug("Attempting autoplay") - setIsPlaying(true); + if (autoplayPolicy === "allowed") { + console.debug("autoplayPolicy:", autoplayPolicy) + setIsPlaying(true); + } else if (autoplayPolicy === "unknown") { + console.warn("autoplayPolicy unknown, attempting autoplay anyway") + setIsPlaying(true); + } else { + console.warn("autoplayPolicy:", autoplayPolicy) + setAutoplaySuccess(false) + } } }; @@ -353,7 +376,7 @@ export default function SnapWeb() { key='autoplay-error' onClose={(_, reason: string) => { if (reason !== 'clickaway') { console.log("Snackbar - onClose") } }}> { console.log("Snackbar - alert onClose") }} severity="error" sx={{ width: '100%' }}> - Autoplay failed + {"Autoplay failed with policy: " + autoplayPolicy} )