mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Apply a playlist change even when the outgoing item never advances
Replacing the single item of a one-item playlist did nothing. The old promo, board or clip kept playing while the dashboard showed the new playlist published and the device perfectly healthy — only a reboot or a manual refresh cleared it. #157 defers a rotation so a live item is not yanked mid-play, and applies it "on the next natural advance". For a one-item playlist there is no such thing, by design: single-item rendering deliberately never advances. A video gets `loop = (playlist.length === 1)` and so never fires `ended`; a YouTube embed loops for the same reason and skips its safety net; a solo widget is "held" on a self-re-arming refresh that never calls nextItem, because reloading it would reset a directory board's scroll. Tizen is worse still — `single` makes every renderer skip its timer, so images freeze too. Two guards, the same pair already applied to the Android controller: - A one-item playlist is never deferred. There is nothing to protect from being cut off, since nothing was going to advance anyway. - Any deferral that does happen gets a 60-second deadline. The deferral is a bet that an advance is coming; if the bet loses, the change must still land rather than strand the screen on content the operator has already replaced. Verified in headless Chrome: a one-item playlist holding a solo widget (the "held" case that never advances), its only item replaced with a different widget — the screen followed, with no reload and no restart. Before the change it stayed on the replaced item indefinitely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
d3f6af831b
commit
f4d309a0d4
|
|
@ -404,6 +404,7 @@
|
|||
// (e.g. it just expired) in solo playback, we keep it up and rotate to deferredSuccessorId on
|
||||
// the next natural advance instead of interrupting/restarting.
|
||||
let deferredRotation = false;
|
||||
let deferredRotationDeadline = null; // a deferral must not wait forever — see the #157 block
|
||||
let deferredSuccessorId = null;
|
||||
function itemIdentity(x) { return x ? `${x.content_id || ''}|${x.widget_id || ''}|${x.remote_url || ''}|${x.filepath || ''}` : ''; }
|
||||
|
||||
|
|
@ -2025,10 +2026,30 @@
|
|||
// video onended still fires nextItem). Schedule-driven modes (wall follower / group-sync)
|
||||
// advance via their own tick, so they reconcile immediately as before.
|
||||
const scheduleDriven = isWallFollower() || !!groupSync;
|
||||
if (isPlaying && !scheduleDriven) {
|
||||
// #157 defers so a live item is not yanked mid-play — but it assumes an advance is coming,
|
||||
// and for a ONE-ITEM playlist that is never true. Single-item rendering deliberately never
|
||||
// advances: a video gets `loop = (playlist.length === 1)`, a YouTube embed the same, and a
|
||||
// solo widget is "held" on a self-re-arming refresh that never calls nextItem (reloading it
|
||||
// would reset a directory board's scroll). So replacing the single item of a one-item
|
||||
// playlist deferred forever — the old promo, board or clip kept playing while the dashboard
|
||||
// showed the new playlist published and the device healthy. Only a reboot or a refresh
|
||||
// command cleared it.
|
||||
const outgoingNeverAdvances = oldPlaylist.length <= 1;
|
||||
if (isPlaying && !scheduleDriven && !outgoingNeverAdvances) {
|
||||
deferredRotation = true;
|
||||
deferredSuccessorId = itemIdentity(playlist[nextIdx]);
|
||||
console.log('#157: current item removed but still live — deferring rotation-out');
|
||||
// Safety net for anything else that turns out not to advance: a deferral is a bet that
|
||||
// one is coming, and if it never arrives the change must still land rather than strand
|
||||
// the screen on content the operator has already replaced.
|
||||
if (deferredRotationDeadline) clearTimeout(deferredRotationDeadline);
|
||||
deferredRotationDeadline = setTimeout(() => {
|
||||
if (!deferredRotation) return;
|
||||
console.warn('#157: deferred rotation never got an advance — applying it now');
|
||||
deferredRotation = false;
|
||||
const di = deferredSuccessorId ? playlist.findIndex(x => itemIdentity(x) === deferredSuccessorId) : -1;
|
||||
startPlaybackAt(di === -1 ? 0 : di);
|
||||
}, 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2116,6 +2137,7 @@
|
|||
// into the (already-swapped) list at the preserved successor instead of interrupting.
|
||||
if (deferredRotation) {
|
||||
deferredRotation = false;
|
||||
if (deferredRotationDeadline) { clearTimeout(deferredRotationDeadline); deferredRotationDeadline = null; }
|
||||
const sid = deferredSuccessorId; deferredSuccessorId = null;
|
||||
let idx = sid ? playlist.findIndex(x => itemIdentity(x) === sid) : -1;
|
||||
if (idx < 0) idx = 0;
|
||||
|
|
|
|||
|
|
@ -198,9 +198,28 @@ PlaylistPlayer.prototype.load = function (assignments) {
|
|||
// #157: removed-but-live in solo playback -> don't interrupt; rotate out on the next advance
|
||||
// (the current item's video onended / image timer still fires advance()). Group-sync (schedule-
|
||||
// driven) and wall followers reconcile via their own tick, so play through immediately as before.
|
||||
if (this.hasContentOnScreen() && !this.wallFollower && !this.scheduleDriven) {
|
||||
// ...but only when an advance is actually coming. Single-item playback here deliberately has
|
||||
// none: `single` makes renderImage, renderVideo and renderWidget all skip their timer (a solo
|
||||
// item is meant to sit there), so replacing the one item of a one-item playlist deferred forever
|
||||
// and the old content stayed on the screen. On Tizen this strands IMAGES too, not just video and
|
||||
// widgets as on the web player, because the timer is skipped for every type.
|
||||
var outgoingNeverAdvances = !this.items || this.items.length <= 1;
|
||||
if (this.hasContentOnScreen() && !this.wallFollower && !this.scheduleDriven && !outgoingNeverAdvances) {
|
||||
this._deferredRotation = true;
|
||||
this._deferredSuccessorId = this.itemIdentity(items[nextIdx]);
|
||||
// Safety net: a deferral is a bet that an advance will arrive. If it does not, apply the
|
||||
// change anyway rather than leave the screen on content the operator has replaced.
|
||||
var self = this;
|
||||
if (this._deferredDeadline) clearTimeout(this._deferredDeadline);
|
||||
this._deferredDeadline = setTimeout(function () {
|
||||
if (!self._deferredRotation) return;
|
||||
self._deferredRotation = false;
|
||||
var di = -1;
|
||||
for (var k = 0; k < self.items.length; k++) {
|
||||
if (self.itemIdentity(self.items[k]) === self._deferredSuccessorId) { di = k; break; }
|
||||
}
|
||||
self.startPlaybackAt(di === -1 ? 0 : di);
|
||||
}, 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -250,6 +269,7 @@ PlaylistPlayer.prototype.advance = function () {
|
|||
// stashed list and continue at the preserved successor instead of interrupting/restarting.
|
||||
if (this._deferredRotation) {
|
||||
this._deferredRotation = false;
|
||||
if (this._deferredDeadline) { clearTimeout(this._deferredDeadline); this._deferredDeadline = null; }
|
||||
var sid = this._deferredSuccessorId; this._deferredSuccessorId = null;
|
||||
var to = sid ? this.indexOfIdentity(this.items, sid) : -1;
|
||||
this.startPlaybackAt(to >= 0 ? to : 0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue