diff --git a/web/js/components/preact/LiveSlideshowControl.jsx b/web/js/components/preact/LiveSlideshowControl.jsx
new file mode 100644
index 00000000..337470b6
--- /dev/null
+++ b/web/js/components/preact/LiveSlideshowControl.jsx
@@ -0,0 +1,48 @@
+import { useI18n } from '../../i18n.js';
+import { LIVE_SLIDESHOW_INTERVALS } from './useLiveSlideshow.js';
+
+/**
+ * Slideshow toggle for the live view toolbar, with an interval picker shown
+ * while it is on. In a 1x1 layout it cycles cameras; otherwise grid pages.
+ */
+export function LiveSlideshowControl({ enabled, interval, isSingleStream, onToggle, onIntervalChange }) {
+ const { t } = useI18n();
+ const label = enabled
+ ? t('live.slideshowStop')
+ : t(isSingleStream ? 'live.slideshowStartCameras' : 'live.slideshowStartPages');
+ return (
+
+
+ {enabled && (
+
+ )}
+
+ );
+}
+
+export default LiveSlideshowControl;
diff --git a/web/js/components/preact/LiveView.jsx b/web/js/components/preact/LiveView.jsx
index 8bef20d7..c0dc24dc 100644
--- a/web/js/components/preact/LiveView.jsx
+++ b/web/js/components/preact/LiveView.jsx
@@ -20,6 +20,8 @@ import { AlwaysFullscreenToggle } from './AlwaysFullscreenToggle.jsx';
import { useAlwaysFullscreenOnTap } from './useAlwaysFullscreenOnTap.js';
import { LiveDisplayModeToggle } from './LiveDisplayModeToggle.jsx';
import { useLiveDisplayMode } from './useLiveDisplayMode.js';
+import { LiveSlideshowControl } from './LiveSlideshowControl.jsx';
+import { useLiveSlideshow } from './useLiveSlideshow.js';
import { usePullToRefresh } from './usePullToRefresh.js';
import { shouldShowGestureTip } from './mobileLiveGestures.js';
import { fetchAllStreamSummaries } from '../../utils/stream-summaries.js';
@@ -522,6 +524,20 @@ export function LiveView({audioDisabled = false, isAutoDisabled = false, isWebRT
});
}, [applyOperatorLayout, setOperatorSurface]);
+ // Auto-advance cameras (1x1) or grid pages on a timer. Paused while the
+ // operator is rearranging tiles, has one tile in native fullscreen, or is
+ // looking at the building plan instead of the grid.
+ const slideshow = useLiveSlideshow({
+ paused: reorderMode || !!fullscreenCellStream || operatorSurface !== 'grid',
+ isSingleStream,
+ orderedStreams,
+ selectedStream,
+ setSelectedStream,
+ currentPage,
+ setCurrentPage,
+ maxStreams,
+ });
+
// Ensure current page is valid when orderedStreams or layout changes
useEffect(() => {
if (orderedStreams.length === 0) return;
@@ -824,6 +840,16 @@ export function LiveView({audioDisabled = false, isAutoDisabled = false, isWebRT
/>
)}
+ {slideshow.canCycle && (
+
+ )}
+
{orderedStreams.length > 1 && (