diff --git a/server/player/index.html b/server/player/index.html
index 11c5815..91d1646 100644
--- a/server/player/index.html
+++ b/server/player/index.html
@@ -1320,6 +1320,14 @@
const container = document.getElementById('playerContainer');
container.style.display = 'block';
+ // Multi-zone: each zone pulls its own content by zone_id, independent of the
+ // rotating "current item". Render zones here (before the single-item bail) so
+ // an empty/placeholder current item can't blank the whole multi-zone screen.
+ if (layout && layout.zones && layout.zones.length > 1 && !wallConfig) {
+ renderZones(container, item);
+ return;
+ }
+
// Defense in depth: bail to waiting state on missing/malformed item rather
// than fall through every branch and leave a blank container.
const hasRenderableType = item && (
diff --git a/server/ws/deviceSocket.js b/server/ws/deviceSocket.js
index ec1900f..7ef1cff 100644
--- a/server/ws/deviceSocket.js
+++ b/server/ws/deviceSocket.js
@@ -154,6 +154,16 @@ function buildPlaylistPayload(deviceId) {
}
}
+ // Zone reset: if the device isn't in a real multi-zone layout (single zone or
+ // no layout), strip any leftover zone_id from assignments. Otherwise, after
+ // switching a device from a multi-zone layout back to single/fullscreen, the
+ // content stays bound to a now-gone left/right zone_id and never plays. With
+ // zone_id nulled, both players fall back to the default fullscreen renderer.
+ const zoneCount = layout?.zones?.length || 0;
+ if (zoneCount < 2 && Array.isArray(assignments)) {
+ assignments = assignments.map(a => (a && a.zone_id != null ? { ...a, zone_id: null } : a));
+ }
+
return { assignments, layout, orientation: device?.orientation || 'landscape', wall_config };
}