{
  "latest": "1.7.67",
  "versions": [
    {
      "version": "1.7.67",
      "releasedAt": "2026-09-07T18:59:09Z",
      "filename": "com.lg.app.signage_1.7.67_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.67_all.ipk",
      "size": 529344,
      "sha256": "efe55c0cfa52a6fa3d2cefa471bfa569e42760593b8778c1334f705dadaff4e1",
      "title": "The flash at the cut to the last frame was the clip wrapping first; slots now end on the file's last frame from the media position, and the hold is shorter.",
      "notes": [
        "Build 94 counted two loop wraps per hand-off on a playlist whose every slot equals its file length. The slot timer starts at the reveal, when the clip is already ~160 ms in, so the clip reached its end before the timer did, wrapped to frame 0 (a pipeline restart: a freeze, then the first frames) and only then did the cover show the last frame. That was the flash. A slot that ends within a second of a pass boundary now ends ON that boundary, judged by the clip's own position (polled every 50 ms, media time accumulated across wraps), 150 ms short of the file's end so the wrap can never fire. The wall timer stays as the net.",
        "The same rule on the CMS for the exit still now covers any number of passes (a slot of two passes plus a fraction ends on the last frame of pass two).",
        "Warm-up floor 1200 ms / 0.8 s → 1000 ms / 0.6 s: the reveal is motion-gated since 1.7.64, so the floor's only remaining job is to have the plane up before the seek. Per-screen `video_warm_floor_ms` still overrides it. About 0.35 s off every clip-to-clip hold.",
        "Trace: `slotEnd` (media | timer)."
      ]
    },
    {
      "version": "1.7.66",
      "releasedAt": "2026-09-07T15:12:12Z",
      "filename": "com.lg.app.signage_1.7.66_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.66_all.ipk",
      "size": 529140,
      "sha256": "13a7a50b13f424b048fc02b95337ea0440b1194b5a183e9bf581abbe483ef01f",
      "title": "The last-frame hold is shorter and no longer reads as a freeze, the black flash at the cut to it is gone, and a panel never streams a clip it already has.",
      "notes": [
        "The long last-frame hold and the black flash had one root: from 13:22 on the test panel was streaming every clip from the CMS. The source policy still allowed \"local stalls\" to send a panel to the stream, and a looping clip's wrap (reported by the pipeline as a stall) plus two mid-clip ones were enough. Streamed clips open in 1.7-3 s and seek in ~1 s, so every change-over became 4-7 s of stills. Local playback is now never abandoned for the stream; loop wraps are counted apart (`wraps`); stalls are recorded and decide nothing.",
        "Even on local files the clock takes 0.7-1.3 s to resume after the seek back to the first frame, and the 600 ms motion cap was shorter than that - so the still came away \"no motion\" onto a frozen first frame. The cap is now at least twice the panel's own measured rewind-to-motion.",
        "The black flash at the cut from a clip to its last frame: the outgoing plane was released 10-40 ms after the cover went up, and pausing/clearing a visible plane blanks it to black first. The plane is now hidden one paint before its decoder is freed, and released only after the cover has been on the glass for 120 ms.",
        "The held last frame at a clip-to-clip change-over now drifts - a slow 3.5 % push-in over 3 s - so the two seconds the panel needs to open and steady the next clip read as a held frame rather than a freeze. Only under a fade or no exit effect; a transform-based exit would snap it.",
        "New per-screen knob `screens.video_rewind_mode`: 'seek' (default) or 'pause' - pause, seek, play on seeked. On panels that keep the plane up through a pause this should cut ~0.7 s per change-over (the clock resumes within ~50 ms of play()). Try it on one panel; if the wall shows black at the cut, it does not."
      ]
    },
    {
      "version": "1.7.65",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.65_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.65_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "The download card counts playlist items, a clip is downloaded once however many times it is placed, old clips are cleared, and a re-encoded clip is fetched again.",
      "notes": [
        "The corner card said \"83 files\" for a 33-item playlist. It was counting every file it fetches - each video item is its clip plus two stills, an image is one file, a widget its logos - and left out anything already on the panel, so the number never matched the playlist. It now reads \"N / 33 items ready\": an item is ready when everything it needs is on the panel, and the percentage is the average of the items' own completion, so it moves smoothly and never disagrees with the count.",
        "Local clips are stored ONE per file. Names used to come from the playlist item, so the same ad placed twice was downloaded and stored twice, and a clip re-added after a publish was downloaded again. The first name a file is seen under is kept and reused by every item that points at it; existing downloads stay valid.",
        "Clips a playlist no longer needs are removed from the panel on the next directory listing - only once a complete list is known, never from an empty one, and never a file that is downloading.",
        "A clip re-encoded on the CMS (same address, new bytes - the 25 fps normalisation does this) was never refreshed. The CMS now sends each file's size and the panel re-downloads on a mismatch with its own listing.",
        "Checked: every video item's exit still (the frame it ends on) and entry still (the frame it opens on) is captured on the CMS, downloaded into the panel's cache with the playlist, localised at play time and used for the change-over; the file's own first/last frames are the fallbacks only."
      ]
    },
    {
      "version": "1.7.64",
      "releasedAt": "2026-09-07T12:58:27Z",
      "filename": "com.lg.app.signage_1.7.64_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.64_all.ipk",
      "size": 527860,
      "sha256": "dabf3602b3c3489cddb68a1ad36b29b896c46393b150d1b5dbe7419d41c909e8",
      "title": "No more freeze at the start of a clip, and no more wrap at the end of one whose slot is its rounded length.",
      "notes": [
        "The \"slight freeze on some videos\": a clip's stored length is a rounded integer and the editor's default slot is that number, so a 12.93 s clip in a 13 s slot reached its end 70 ms before the slot did, wrapped to the start (a seek, with a freeze) and showed the first frames again before its exit still. A slot that overruns the file by less than a second is rounding, not a request to loop: the player now ends it on the file's last frame. The CMS applies the same rule to the exit still and Android holds its last frame for the same overrun - one rule on all three surfaces.",
        "The still now comes away when the clip is seen to MOVE, not on a timer after the seek. After a rewind lands the pipeline takes a while to resume (the clock read 0 on every trace row 150 ms after `seeked`), so a still swapped for a not-yet-moving picture was a freeze, however short. The rewind-to-motion time is what the panel now learns and times the next rewind by, so motion begins as the change-over ends.",
        "Clip-to-clip: the cross to the incoming first frame is timed to the panel's own measured warm-up (plus a margin) instead of the floor - the real warm-up ran ~150 ms past the floor on every clip, and the first frame stood for that difference after every cross.",
        "On panels new enough to have requestVideoFrameCallback, the first presented frame could reveal the clip before its rewind - the hidden-playback gap the design exists to avoid. That path now only measures.",
        "Trace: `tMove` / `moveMs` (rewind to motion), `warmEst`, `slotTrim`."
      ]
    },
    {
      "version": "1.7.63",
      "releasedAt": "2026-09-07T10:55:06Z",
      "filename": "com.lg.app.signage_1.7.63_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.63_all.ipk",
      "size": 527542,
      "sha256": "272f382768f10527a8e875533e8a24275a2873430533dfda928cf543c4d3ff19",
      "title": "The first-frame still no longer lingers: the clip takes over the instant the transition ends, on every panel.",
      "notes": [
        "The rewind to the first frame is now timed by the panel's OWN measured seek time (kept per source in localStorage, `seekEst`/`seekMs` in the trace) plus the settle, so it lands exactly as the change-over on the cover finishes. There is no hold after the cross any more.",
        "When a panel's seek takes longer than the transition, the pre-warm rewinds the clip UNDER the picture before the slot even begins, timed to land as the transition ends; the slot path then only waits for it (`rewind = pre`). Panels that seek in 100 ms never need this and are unaffected.",
        "Source choice is LOCAL FIRST. The old policy alternated local and remote to measure them, and on one 55\" panel the 1.7.61 shutter had condemned local playback (\"local stalls\"), so it streamed everything from the CMS at 2.3 s opens and 1-1.7 s seeks - the 5 s clip-to-clip change-overs and 2 s first-frame holds in its traces. The statistics start fresh (`srcstats2`); local is only abandoned for a stream on genuine, repeated stutter, and re-tried periodically.",
        "The buffered-ahead check judged the play head against 3 s of run-ahead even when less than that was left in the file, so every looping pre-warmed clip failed it and fell to the ceiling; each position is now judged against what is left of the file from there.",
        "Clip-to-clip: the cross to the incoming first frame is scheduled to end when the clip can first be on its first frame (warm-up floor + this panel's seek + settle), so it overlaps the warm-up exactly instead of finishing early and holding.",
        "Trace: `tWarm` is on the slot's timeline like every other t* (it was measured from play(), which for an adopted clip was seconds before the slot); `warmMs` is the length of the warm-up; new `tRewind`, `seekMs`, `seekEst`, `rewind`."
      ]
    },
    {
      "version": "1.7.62",
      "releasedAt": "2026-09-07T07:32:44Z",
      "filename": "com.lg.app.signage_1.7.62_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.62_all.ipk",
      "size": 526770,
      "sha256": "a4eb01774503f93706c2cb41d63a8fbfc4728b85f842fccddaf55f7fffdfe9d7",
      "title": "The next clip now warms up out of sight under the picture before it, the black \"shutter\" at the reveal is gone, clip-to-image change-overs no longer flash, and Facebook / Instagram posts written in decorative Unicode lettering are readable.",
      "notes": [
        "Image / widget / document -> video: the clip is opened, decoded and run silently UNDERNEATH the picture that is on screen, from about 3.5 s before that picture's slot ends. The video stage is dropped below the content layers for the warm-up, so nothing of it reaches the wall. At the boundary it has been running for seconds: it is rewound to its first frame as the change-over finishes and shown at once. The still time the viewer sees is the configured transition, not a warm-up. (This is not the two-element pre-load 1.7.55 tried - a clip is never opened while another clip is playing, because only one can hold the panel's video hardware.)",
        "The black \"shutter\" between the first-frame still and the moving clip: 1.7.61 ended the warm-up after 0.5 s of clock and rewound then, but on the UH5Q the clock moves before the pipeline is steady - 31 % of its reveals landed on a pipeline that immediately reported `waiting` (11 % with the old 3 s hold). Three fixes: the warm-up floor is 1.2 s / 0.8 s of real playback; where the panel reports buffered ranges, the rewind waits (briefly) until enough of the file is in from both the play head and the first frame; and after the rewind the still only comes away once the pipeline reports it can play on again (`canplay` / `playing`), not on `seeked` alone. The warm-up ceiling default is 2.5 s - it is only the give-up point.",
        "Clip -> clip: the change-over to the incoming first frame used to wait for the warm-up and then run AFTER it. It is started early now, timed to finish as the warm-up floor is reached, so the two overlap; the rewind is started ~250 ms before the cross ends so it lands as the incoming still is fully up. The exit still is still held for as long as the panel needs to open and steady the next clip (~2 s on a 43\" UH5Q) - that part is the panel's floor, not a wait of ours.",
        "Clip -> image: the outgoing layer after a clip was EMPTY (the clip lives in the video stage, not the layer), so every enter transition after a clip started from black - the flash. The frame the clip ended on is now placed into that layer and painted before the cover comes down, so the transition runs over the last frame and the cover clears onto the same picture.",
        "A clip now plays its WHOLE duration: its timer starts when it is shown, not at the top of its slot. Until now every second the change-over took came out of the clip - shown late, cut short - and the CMS exit still (captured at `duration`) was that much AHEAD of the frame the clip actually froze on, which was a visible jump at every clip-to-clip change-over. A clip that never shows a picture still ends its slot 4 s past its duration, so the rotation cannot wedge.",
        "Source choice: a stall within 1.5 s of the reveal is the change-over, not the source, and no longer counts against local playback (it could have pushed a panel onto 2.4 s remote opens). It is still traced (`stallAt`). New per-screen knob `screens.video_warm_floor_ms` for the warm-up floor; `video_warmup_ms` remains the ceiling.",
        "A per-item end time no longer leaks its listener onto later clips (the shared element kept every clip's end-time handler).",
        "Facebook / Instagram widgets: post text written in decorative Unicode lettering (𝗔𝘆𝗼𝗯𝗮 - the Mathematical Alphanumeric Symbols block, circled / fullwidth / small-cap look-alikes) has no glyphs in the panel's fonts and rendered as boxes. The widget bundle folds it to plain letters before drawing; emoji and other scripts are untouched.",
        "Trace: new fields prewarm (adopted | cold | missed), preMs (how long the adopted clip had been running when its slot began), bufWarm (seconds buffered when the warm-up ended) and tSteady."
      ]
    },
    {
      "version": "1.7.61",
      "releasedAt": "2026-09-04T15:26:12Z",
      "filename": "com.lg.app.signage_1.7.61_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.61_all.ipk",
      "size": 523996,
      "sha256": "18a8c00aa97657b9b634b5403ea304208ab28e45959eb9c6eb0596aeb5009ffa",
      "title": "Clips no longer sit on their first frame for three seconds, clip-to-clip change-overs no longer flash, and the panel finally reports its memory.",
      "notes": [
        "The still held over a starting clip lasted three seconds on the 43\" panels, whatever the clip was doing. Those panels keep a decoded-frame counter that never moves - it reads zero for ever - and the player took that as \"no frames yet\" and waited out the whole 3 s ceiling on every single clip, with the clip running underneath from the first second. A counter that does not move is now ignored and half a second of real playback ends the warm-up instead. The still comes away about a second after the slot begins.",
        "Clip-to-clip change-overs were a fixed piece of theatre since 1.7.57 - 1.5 s on the last frame, then 2 s on the first - which on a panel that opens a clip in under a second was three seconds of stills nobody asked for. The holds are event-driven now: the last frame is held for exactly as long as the panel takes to get the next clip running, the change-over then plays to the first frame, and the clip takes over from that frame a quarter second later.",
        "The flash at a clip-to-clip change-over was the outgoing clip's plane being released in the same instant the cover was raised. A still assigned to the cover is not on screen until the panel has decoded and composited it, a frame or two later, and a plane released in that gap shows the empty layer under both - which is black. The stills are now loaded into the cover ahead of time, and the plane is not released until the cover is confirmed painted. The same rule covers a clip followed by an image.",
        "The dashboard showed a dash for memory on every LG screen. The player was asking the webOS memory manager, which these panels do not carry, and caching that blank answer for the life of the app. It now asks the commercial-display API LG's own reference player uses (SCAP getSystemUsageInfo), then the memory manager, then the WebView, then its own bundled service - cheapest first - and retries a blank answer on a widening schedule. The heartbeat carries whatever is known and never waits on the probe.",
        "Every hand-off row now records when and why its warm-up ended, and for a clip-to-clip change-over when the cover was confirmed painted and when the change-over ran - the CMS had been discarding those fields.",
        "On-screen build bumped to 89, app-shell cache to v89."
      ]
    },
    {
      "version": "1.7.60",
      "releasedAt": "2026-09-04T13:39:44Z",
      "filename": "com.lg.app.signage_1.7.60_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.60_all.ipk",
      "size": 523012,
      "sha256": "e95f45092326e43c8419c59cb44d6cf056cca1552a2598a3aac8917b0103a634",
      "title": "The panel now reports the version it is actually running, and a News widget no longer fades its first headline in on top of the playlist transition.",
      "notes": [
        "Since 1.7.56 the player has been announcing itself to the dashboard as 1.7.55 (83) whatever it was really running - the package version moved on every release but the version the heartbeat sends did not. A panel on 1.7.56, 1.7.57, 1.7.58 or 1.7.59 therefore shows 1.7.55 on its screen card and in the hardware tab. This build sends its real version, so an updated panel is finally distinguishable from one that was left behind.",
        "The News widget played its slide-to-slide transition on its very first slide as well - from nothing - so on a player the widget arrived as a bare background and the headline faded in half a second later, on top of the playlist transition that had just brought the widget on. The first slide is now simply there; the transition belongs between slides and runs only from the second slide on.",
        "On-screen build bumped to 88, app-shell cache to v88."
      ]
    },
    {
      "version": "1.7.59",
      "releasedAt": "2026-09-03T13:21:28Z",
      "filename": "com.lg.app.signage_1.7.59_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.59_all.ipk",
      "size": 522568,
      "sha256": "be0849ca482edd0004fe07d8a951e4dca05bed7aa0ed83cc3d41982e2a81e45f",
      "title": "The panel now reports how much memory it has, so the screen's hardware tab shows it beside the resolution.",
      "notes": [
        "Until now the only way to learn what a screen actually had was to read a crash report from it - which meant waiting for it to fail. The player reports its memory alongside its model and resolution, and the dashboard shows it under Resolution on the screen's Hardware tab.",
        "A panel whose firmware does not carry the memory manager simply reports nothing and the field shows a dash. Unknown is not zero.",
        "On-screen build bumped to 87, app-shell cache to v87."
      ]
    },
    {
      "version": "1.7.58",
      "releasedAt": "2026-09-03T06:27:43Z",
      "filename": "com.lg.app.signage_1.7.58_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.58_all.ipk",
      "size": 522552,
      "sha256": "36ac5c3cd3866017a268cc9f934bc9ea8c8f7b6a263980ce719ddb7b331165fb",
      "title": "A panel is only held back when it is genuinely small: the Website widget still publishes a 2 GB requirement, but nothing is restricted above 1.5 GB - and a panel that will not report its memory is never restricted at all.",
      "notes": [
        "The published requirement and the enforced one are now two different numbers, on purpose. 2 GB is what the Website widget wants in order to be comfortable, and it is the honest figure to show a customer choosing widgets. 1.5 GB is where a panel is actually in trouble. A screen with 1.8 GB is neither warned nor restricted, because it does not need to be - and a restriction applied where it is not needed is how people learn to ignore them.",
        "The player now asks the panel how much memory it has, once, through the webOS memory manager. Only a panel that answers with 1.5 GB or less has heavy widgets released between items.",
        "A panel that does not answer - not every firmware carries that service - is treated as fine and restricted in no way. We only ever hold something back from hardware we know is small.",
        "On-screen build bumped to 86, app-shell cache to v86."
      ]
    },
    {
      "version": "1.7.57",
      "releasedAt": "2026-09-03T05:37:24Z",
      "filename": "com.lg.app.signage_1.7.57_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.57_all.ipk",
      "size": 522366,
      "sha256": "4ebc1cb1ce0b1ca6f9a243fd8b46bbe65808b5c352f5d4de9f53190942eea04e",
      "title": "Clip-to-clip change-overs are now a fixed piece of theatre - the outgoing last frame, then the incoming first frame, with the next clip loading and warming underneath both.",
      "notes": [
        "A clip cannot start until the previous one has given the video hardware back, and taking it again is not instant on these panels. Rather than race that and hope, a video following a video now runs to a schedule: the outgoing clip's last frame is held for 1.5 seconds, cross-fades to the incoming clip's first frame, and that is held for a further 2 seconds before the clip takes the screen and plays from its opening frame. The viewer sees two photographs and then motion.",
        "The next clip starts loading the instant the first still goes up, so it has the whole three and a half seconds to open, decode and warm - and none of that wait is visible, because a real picture is on screen throughout.",
        "The clip's warm-up and the change-over now run TOGETHER. They used to run one after the other: the clip waited out the whole hand-over before it was even played, so the panel began warming up after the covering stills had finished their work and the viewer waited for it. The clip now warms and rewinds behind the stills and is ready at the exact moment the picture is meant to change.",
        "A clip following an IMAGE gets none of this, deliberately. An image already holds the screen and there is no hardware to hand back, so the clip loads underneath it and is shown the moment it is genuinely running - no hold, no theatre, no delay.",
        "Nothing is lost from any clip: it is still wound back to its first frame before it is shown, so what plays is the same frame you were just looking at, now moving.",
        "Widgets that declare a memory requirement - in practice the Website widget, which is a whole third-party page - are now shut down the moment a clip needs the panel, instead of being left running where they compete with the video decoder.",
        "On-screen build bumped to 85, app-shell cache to v85."
      ]
    },
    {
      "version": "1.7.56",
      "releasedAt": "2026-09-01T10:30:28Z",
      "filename": "com.lg.app.signage_1.7.56_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.56_all.ipk",
      "size": 522102,
      "sha256": "09c0959d875d62bc71bc0832d6c2cb11ad20d3566c68e94bf92806be5c9649cf",
      "title": "The change-over never hands the screen to an empty video plane again, and the first-frame still comes away the moment the clip is genuinely running instead of waiting out a fixed timer.",
      "notes": [
        "1.7.55 loaded the next clip into a second video element while the first was still playing. On these panels that element gets no decoder while the other one holds it, so the clip it was supposed to have prepared had nothing in it: across 74 recorded change-overs the prepared clip had produced zero frames, and 97% of them went black. That approach is gone; one video element again, which is what the panel can actually do.",
        "The black itself was an ordering fault, and it is now impossible rather than unlikely. When a clip could not be prepared, the player used to take the held picture away and start playing afterwards - so the screen was handed to a video plane that had nothing on it yet. Every path now starts the clip underneath the picture that is already up, and hands the screen over only once the decoder has genuinely put frames out. A plane with no frame on it is never revealed at all: the previous picture is held instead.",
        "The still picture no longer outstays its welcome. The warm-up was a fixed wait - and with a per-screen override it could be three and a half seconds - held over a clip that was usually ready within a fraction of that. It now watches the panel's own decoded-frame counter and ends as soon as real frames appear, so the still lasts as long as it has a job and not a moment longer. The old timer remains only as a ceiling.",
        "No clip loses its opening seconds: it is still wound back to its first frame before the still comes away, so what you see is the first frame as a picture and then that same frame moving.",
        "Every change-over now records how long the warm-up took and why it ended, so this is measurable rather than a matter of opinion.",
        "On-screen build bumped to 84, app-shell cache to v84."
      ]
    },
    {
      "version": "1.7.55",
      "releasedAt": "2026-09-01T09:38:45Z",
      "filename": "com.lg.app.signage_1.7.55_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.55_all.ipk",
      "size": 522454,
      "sha256": "f511484c0bed9bdc9ce919e7fc40c0a2c0fb057382fca8d102e53a750ab660bb",
      "title": "The next video is now loaded underneath the one still playing - so the still picture only lasts as long as the transition, and no clip loses its opening seconds.",
      "notes": [
        "Opening a video and filling its buffer takes several seconds on these panels. Until now that happened at the change-over, so the wait had to come out of the screen: the still picture was held for about four seconds, and the clip that followed then played four seconds short because the wait had eaten the start of its turn.",
        "The player now uses two video elements in turn. While one clip is playing, the next is quietly opened and buffered in the other - nothing on screen changes while it happens, because the clip on screen is never disturbed to make room. When its turn comes it is simply played, and the screen changes hands in one step. The clip that finished is released a moment after, never before.",
        "The result: the still picture is on screen for the transition and nothing more, every clip plays its full turn, and there is no black - because at no point is there nothing playing.",
        "This is how LG's own reference player works, which is the one that has never shown this fault.",
        "A video following an image gets the same head start, since an image holds the screen just as well as a clip does.",
        "The prepared clip is only loaded, never played, so it still begins at its first frame - nothing of it is lost.",
        "On-screen build bumped to 83, app-shell cache to v83."
      ]
    },
    {
      "version": "1.7.54",
      "releasedAt": "2026-09-01T07:29:49Z",
      "filename": "com.lg.app.signage_1.7.54_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.54_all.ipk",
      "size": 521918,
      "sha256": "c95dd49062f4ebe712e953b60bd01612e1d90e7398d8cb25329b9036062b08b2",
      "title": "The Countdown and World Clock widgets are simpler to set up - and a countdown now simply stops on zeros.",
      "notes": [
        "A countdown that reaches its moment now freezes on a row of zeros and stays there. It used to offer four different endings - a finished message, a clock that carried on counting upwards, frozen zeros, or hiding the numbers - which was a whole tab of settings for a state most boards are taken down before they ever reach. Any countdown currently set to show a finished message will show zeros instead from this build. A repeating event is unaffected: it never reaches an end, it rolls straight on to the next occurrence.",
        "Both widgets are far quicker to set up. Thirty-two settings have gone from the customise screen - eleven separate pixel sizes on the countdown and nine on the clock board among them. Nothing has been taken away from the design: the whole type scale is now worked out from the sizes anyone actually has an opinion about - the title and the numbers on a countdown, the clock size on a board - so raising one raises everything with it and the design stays in proportion. Every existing board renders exactly as it did.",
        "Adding a city to a clock board is now a search box. Type a city, a country, a zone name or an offset - \"tokyo\", \"south africa\", \"SAST\", \"+5:30\" - and every match shows you what time it is there right now, with a sun or moon for whether they are awake and a note when they are on tomorrow's date. The old seventy-entry dropdown could not be typed into.",
        "Both widgets now open on a gallery of ready-made designs, each painted in its own colours, with the one you are on marked. Picking one never touches your dates, your wording or your cities.",
        "The clock board's blinking colon has gone, the hour is always padded so the board cannot twitch as it rolls under ten, and city names print as written. All three matched what every existing board was already set to, so nothing on a wall moves.",
        "On-screen build bumped to 82, app-shell cache to v82."
      ]
    },
    {
      "version": "1.7.53",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.53_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.53_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "New: the World Clock widget - the time in several cities at once.",
      "notes": [
        "Put a board of clocks on any screen: offices, markets, family. Choose how many cities to show, pick each one from the time-zone list, and call it whatever your audience calls it.",
        "Every screen shows the same instant, wherever it is standing and however its own clock happens to be set - and each city follows its own daylight-saving changes automatically, including while the screen is offline. The changeover dates travel with the playlist, so a screen that has not seen the network for months still turns the clocks over on the right night.",
        "Five shapes: rows with the date alongside, tiles, equal columns, one big home clock with the rest beneath it, or analog faces with real sweeping hands.",
        "Switch between a 12-hour and a 24-hour clock, add seconds, a blinking colon, and a sun or moon marker showing at a glance who is awake.",
        "A header dated to whichever city you choose - which matters on a board spanning the date line, where two cities are genuinely on different days.",
        "Six ready-made looks, full control of every colour, a background colour, gradient or image, and your own logo in a corner or above the board.",
        "On-screen build bumped to 81, app-shell cache to v81."
      ]
    },
    {
      "version": "1.7.52",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.52_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.52_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "New: the Countdown widget - count down to the moment that matters.",
      "notes": [
        "Put a live countdown on any screen: launches, openings, kick-off, New Year. Set the date, the time and the time zone it is written in, and every screen counts down to the same instant - no matter where it is standing or how its own clock happens to be set.",
        "Choose which units to show: weeks, days, hours, minutes, seconds. Anything you hide is rolled into the next unit up, so a two-day wait with days turned off reads as 51 hours rather than 3. Nothing is ever quietly lost.",
        "Seven shapes to pick from, including split-flap flip cards like a departure board, rings that empty as they run down, and a stacked layout built for portrait totems. Six ready-made looks get you to a finished board in one click, and leave your wording and dates exactly as you set them.",
        "Set it to repeat daily, weekly, monthly or yearly for a recurring event. Repeats hold the same time of day across daylight-saving changes, and a monthly event on the 31st lands on the last day of the shorter months.",
        "Decide what happens the moment it lands: a finished message, a clock that carries on counting upwards since it started, a frozen row of zeros, or nothing at all.",
        "It keeps working with no network. The instants it counts to travel with the playlist, so a screen that has been offline for months still counts down correctly, and still rolls over to the next occurrence of a repeating event.",
        "On-screen build bumped to 80, app-shell cache to v80."
      ]
    },
    {
      "version": "1.7.51",
      "releasedAt": "2026-08-28T18:58:16Z",
      "filename": "com.lg.app.signage_1.7.51_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.51_all.ipk",
      "size": 506266,
      "sha256": "ded8ae16d20bba45ea35c5b532e175fde6a0bcde0db19cb8522f6772558d152f",
      "title": "The Website widget no longer leaves a white strip down the edge of the page.",
      "notes": [
        "Some sites keep a scrollbar-wide strip permanently reserved down their right-hand edge, even when nothing is scrolling. Nothing paints into it - not even the page’s own background - so it reached the screen as a bare white stripe. The page is now laid out one strip wider than the screen and that strip is kept out of shot, so the page reaches the edge. Where a site is loaded through our own server we simply switch the reservation off before it ever gets to the screen.",
        "Screens that draw scrollbars over the top of content, rather than beside it, never reserved anything and are unaffected - the allowance is measured on the panel itself and comes out at zero there.",
        "On-screen build bumped to 79, app-shell cache to v79."
      ]
    },
    {
      "version": "1.7.50",
      "releasedAt": "2026-08-28T13:56:29Z",
      "filename": "com.lg.app.signage_1.7.50_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.50_all.ipk",
      "size": 505848,
      "sha256": "636a135e91e09707c07d83559f1be0c060ffc4dc923cb83f7e8aaea575656fcb",
      "title": "The Website widget now has the page ready before the screen switches to it, and fills the screen exactly.",
      "notes": [
        "The page is ready BEFORE the change-over. The widget used to report itself ready the moment it had drawn its own frame - which is a blank page and a \"Loading page…\" message, because the website itself had not arrived yet. It now holds the previous item on screen until the page has actually painted, so the change-over lands on a finished page. A site that never answers is capped, so a dead address can never hold up the rotation.",
        "The page fills the screen exactly. The browser window the page is laid out in now takes the shape of the screen it plays on, so it maps on with nothing left over - no strip of background down the edge or along the top, at any panel resolution or orientation. Zoom keeps the screen filled too: it shows more or less of the page rather than shrinking it away from the edges. A fixed window size and a fit setting are still available for anyone who wants the page laid out identically on every screen.",
        "Refreshing no longer blinks. When the page reloads on its schedule the new copy is loaded behind the one on screen and only swapped in once it has drawn.",
        "On-screen build bumped to 78, app-shell cache to v78."
      ]
    },
    {
      "version": "1.7.49",
      "releasedAt": "2026-08-28T13:09:40Z",
      "filename": "com.lg.app.signage_1.7.49_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.49_all.ipk",
      "size": 505456,
      "sha256": "e9caaa803c8dd81f1d2820f2f40c877fd705f0a9fb8dca967d9810b02b449ea8",
      "title": "The clip is now visible from the moment it is opened - it was being warmed up hidden, which warms nothing.",
      "notes": [
        "This was a fault introduced two builds ago, and it is the black that has been seen on the portrait screen since. Moving every clip into one shared video element (1.7.47) brought back something that had been deliberately removed earlier: the element was hidden between clips and only made visible at the very end of the change-over.",
        "On these panels a hidden video has its display hardware switched off. So the whole warm-up - three quarters of a second of playing the clip behind the still picture, specifically so the panel could get its picture up - achieved nothing at all. The panel only began bringing the picture up at the moment the element was made visible, which is the same moment the still came away. What reached the wall was the panel starting up, in full view. That is the black.",
        "The clip is now visible from the moment it is opened, and the still picture covers it: the still sits above the whole video area, so a clip warming up underneath cannot be seen. By the time the still lifts, the panel has had the entire warm-up to get its picture up instead of starting then.",
        "The test suite now fails the build if a clip is ever started while its element is hidden - checked by looking at the actual state of the screen at the moment playback begins, not at the code. Verified by reintroducing the fault and watching the tests catch it.",
        "Also in this build: frame rates. All four LG screens now play playlists that are uniformly 25 fps, and Publish keeps any playlist that mixes rates in line automatically. That was worth doing on its own - it was not, in the end, the cause of this black.",
        "On-screen build bumped to 77, app-shell cache to v77."
      ]
    },
    {
      "version": "1.7.48",
      "releasedAt": "2026-08-28T07:42:13Z",
      "filename": "com.lg.app.signage_1.7.48_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.48_all.ipk",
      "size": 505460,
      "sha256": "b0787c92c8fb074b0057e00510c6319128a27190a6e5391850621d391495561f",
      "title": "One frame rate for every clip in the library, and the change-over timing can now be tuned per screen from the dashboard.",
      "notes": [
        "LG panels reconfigure their decoder when the next clip's frame rate differs from the one before it, and on several webOS versions that reconfigure is a black screen at the change-over. Measured on two live screens: the landscape one runs 25/25/25/23.98 and shows no black; the portrait one ran 25, 29.97 and 30 mixed - changing at six of its eight change-overs - and does.",
        "Uploaded video is now brought onto one frame rate automatically. The upload itself is not held up for it: changing frame rate means re-encoding (about seven seconds for an eleven-second clip), so an upload records what it received and a background worker brings it onto the common profile afterwards. Panels pick up the new file on their next check.",
        "The whole library can be brought into line in one pass, and the eight clips on the portrait test screen already have been - four were re-encoded, four were already correct.",
        "The picture and sound are untouched apart from the frame rate: same resolution, same H.264 profile, and the file is never allowed to come out bigger than it went in.",
        "The change-over timing - how long the still is held while the panel gets a picture up - can now be set per screen from the dashboard. Panels differ, and none of them will say when they have a picture, so this is the only way to tune one without a new build and an install. A screen that is never tuned behaves exactly as before, and a nonsense value falls back to the default.",
        "Fixed a measurement that was lying: the player counted its own rewind as the clip stalling, so every single clip looked like a stall (81 out of 81 on one panel). Only a real interruption, after the clip is on screen, counts now.",
        "On-screen build bumped to 76, app-shell cache to v76."
      ]
    },
    {
      "version": "1.7.47",
      "releasedAt": "2026-08-27T19:52:47Z",
      "filename": "com.lg.app.signage_1.7.47_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.47_all.ipk",
      "size": 503360,
      "sha256": "bcc16931ff397d774e46ed5b90df7e4f0c2e65aad85fc1fc8ec5ee64033f3c64",
      "title": "Every clip now plays in ONE video element, taking turns - because the panel only ever serves one clip's worth of video hardware at a time.",
      "notes": [
        "This is the cause, and it has been there since the first release. LG state plainly that two video elements are not supported together on webOS - only one can hold the display's video hardware. Every build until now created a NEW video element for each clip AND kept the outgoing clip playing while the new one loaded, so at every single change-over there were two, both asking for hardware that serves one. The new one could not get a picture. That is the black.",
        "Clips now take turns in a single element that exists for the life of the app: the clip that was playing is released, the hardware goes quiet for a moment, and the next clip is loaded into the same element. The still image covers all of it, exactly as before.",
        "The release is done the way the platform actually requires - the source is REMOVED and the element reloaded. Simply blanking the source leaves the decoder held, and setting it to an empty string is worse: it points the element at the app's own page.",
        "An image or a widget following a clip also gives the hardware back now. Before, a clip could keep it - and a frozen picture over the top of it - for the whole of the next item's turn.",
        "Because the element now outlives every clip, everything each clip attaches to it is removed again when its turn ends, so the tenth clip cannot run the first clip's handlers.",
        "Everything from 1.7.46 is unchanged: the change-over is still a transition between the outgoing clip's last frame and the incoming clip's first frame, and the clip is still warmed up silently behind that picture and wound back to its first frame before it is shown.",
        "On-screen build bumped to 75, app-shell cache to v75."
      ]
    },
    {
      "version": "1.7.46",
      "releasedAt": "2026-08-27T19:31:28Z",
      "filename": "com.lg.app.signage_1.7.46_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.46_all.ipk",
      "size": 502726,
      "sha256": "b8a9309fc4dc864107ce2b57a18e796a6f412d77d7a4b0e50560c70779a926de",
      "title": "First frame as a picture, then that same frame playing - no black, no pause, and nothing missing from the start of the clip.",
      "notes": [
        "The change-over into a video now does exactly what it should: the transition lands on the clip's first frame, and that picture turns into the clip playing from that same frame. Nothing stops, nothing goes black, and nothing is skipped.",
        "How: the panel needs the better part of a second between being told to play and having a picture on screen, and it never says when it gets there - so the clip is started UNDERNEATH the still picture and given that time where there is nothing to see. It plays silently while it warms up, is then wound back to its first frame, and only then does the still come away. Whatever ran during the warm-up is rewound before anybody sees it.",
        "The sound comes back at the same moment the picture does, so a soundtrack is never heard playing under a still.",
        "The player no longer shows a clip because its playback position started moving or its frame counter ticked. Both of those report the decoder, not the screen - the previous build measured a flawless change-over while the wall showed a black pause - so neither is allowed to put anything on screen any more.",
        "Every change-over reports when the rewind landed and what the panel's own frame count was at the hand-over.",
        "On-screen build bumped to 74, app-shell cache to v74."
      ]
    },
    {
      "version": "1.7.45",
      "releasedAt": "2026-08-27T18:53:58Z",
      "filename": "com.lg.app.signage_1.7.45_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.45_all.ipk",
      "size": 502724,
      "sha256": "c527ba1757396f6aff75eace91fa4fed79496b15f5704d4b8788495477b04048",
      "title": "The clip is nudged into putting its first frame on the panel while it is still paused - so nothing is played behind the picture any more.",
      "notes": [
        "This is the black that was reported between the incoming clip's first-frame image and the clip itself. The panel says it has frames long before it shows any: on the last build the player measured 35 milliseconds from starting the clip to 'it has frames', and only 18 milliseconds into the file when it handed the screen over - while the wall showed a black pause and then a clip that had visibly run on. Every signal available to the app (playback position, frame counters) reports the decoder, not the screen.",
        "So the player no longer trusts any of them. A clip is now nudged forward by a single frame while it is still PAUSED, which is what makes the panel actually display a frame rather than merely decode one, and is then left paused for a quarter of a second to get that frame up. Paused time costs nothing - the clip is not running, so none of it is lost.",
        "Only then does the held picture come away, and only after that does playback start. Nothing is played underneath the cover any more, under any circumstances - the previous build allowed it when the cover was showing the clip's own first frame, and that is exactly what was seen going black.",
        "Each change-over now also reports what the panel had on screen while the clip was paused. If that is ever zero, this hardware does not display a paused clip at all, and it will say so plainly rather than needing anyone to watch a wall.",
        "On-screen build bumped to 73, app-shell cache to v73."
      ]
    },
    {
      "version": "1.7.44",
      "releasedAt": "2026-08-27T15:25:33Z",
      "filename": "com.lg.app.signage_1.7.44_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.44_all.ipk",
      "size": 502638,
      "sha256": "6b6595d4c2fd3d5bd51b24b1671e3f52d845eeffc0183eb132b6f9d2d43d4c20",
      "title": "The change-over into a video is now played between two pictures - the frame the last item ended on and the frame the next clip opens on - while the clip opens behind them.",
      "notes": [
        "Every change into a video is now a transition between two still images: the picture the outgoing item finished on, and the exact frame the incoming clip starts at. Both are downloaded with the clip and kept on the panel, so they are ready before they are needed. The transition the playlist asks for - fade, slide, zoom, any of the twelve - is played on those images.",
        "While that is happening, the clip itself is opening behind them, and it is started under its own opening frame. When the panel is really putting frames on screen the images are taken away, and what is underneath is the clip sitting on the very frame that was showing. There is no picture change at that instant: no black, and no cut.",
        "A clip may only be started out of sight when the picture covering it is its OWN first frame. Where there is no such picture - an older clip, or one still being processed - the clip is shown first and started second, exactly as in 1.7.43. The build fails its tests if that rule is ever broken.",
        "Because the transition now runs on ordinary images, it is finally visible on video items. The panel's video hardware ignores fades and slides applied to a playing clip, which is why transitions into and out of videos never looked like the dashboard preview; on the stills they behave exactly as previewed.",
        "For a trimmed clip the CMS now captures the frame at its start point too, so the picture the transition lands on is the frame that clip actually opens on, not the start of the file.",
        "The last frame is no longer held twice - once by the cover and again by a copy in the video area for the fade to animate. One picture, one transition.",
        "On-screen build bumped to 72, app-shell cache to v72."
      ]
    },
    {
      "version": "1.7.43",
      "releasedAt": "2026-08-27T15:07:52Z",
      "filename": "com.lg.app.signage_1.7.43_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.43_all.ipk",
      "size": 501928,
      "sha256": "c4c0b535ce33ba87a7fbbf379ebdaf1e885d9e24ca4449eba281fbed40701643",
      "title": "Nothing is played where it cannot be seen. The next clip is put on screen first and started second - so it starts at the start of the file.",
      "notes": [
        "This is the thing that was noticed on the wall: the next clip was being played while the previous clip's last frame was still covering the screen. The panel starts its clock the moment playback begins, so everything it played under that cover was not queued up - it was gone. That is why the picture, when it finally appeared, was already into the file rather than at its first frame.",
        "The order is now reversed. The moment the clip can play through, the held picture comes off and the clip - already decoded, sitting on its first frame - becomes the picture on screen. Playback starts on the next paint, with the clip already visible. Nothing runs behind the cover, and the file plays from its first frame.",
        "This also means the panel may now show the clip's own first frame during the last moments of opening, instead of a held still followed by black.",
        "The clip's first-frame watcher no longer decides when to show it - it only records when real frames started. Deciding on the first frame was itself the instruction to play out of sight and reveal afterwards.",
        "Includes 1.7.42 (the panel times both ways of loading a clip and keeps the faster one) and 1.7.41 (library video indexes moved to the front of the file).",
        "On-screen build bumped to 71, app-shell cache to v71."
      ]
    },
    {
      "version": "1.7.42",
      "releasedAt": "2026-08-27T14:54:34Z",
      "filename": "com.lg.app.signage_1.7.42_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.42_all.ipk",
      "size": 501870,
      "sha256": "217b2abb37e0574ea40e17c98601ab81135ad019e5c73c5fe3da06e3314b0672",
      "title": "The player now times both ways of loading a clip on the panel itself and keeps whichever one opens faster without stuttering.",
      "notes": [
        "Opening a clip is the black. Measured across three panels: 526-1441 ms, averaging 0.6-0.9 s, every clip, and nothing the app draws covers that window - a fully loaded, full-screen picture held outside the video area was on screen for the whole of it and the wall was still black.",
        "There are two ways to feed a clip to the panel: from the display's own file server, using the copy downloaded to its internal storage, or streamed from the CMS. Which one opens faster depends on the panel and its network, and nobody can reason it out from the outside - so the player now times both on the screen it is actually running on, counts stalls, and keeps the winner. It re-checks the loser every so often in case the network changes.",
        "Safety rules always win over the measurement: with no internet, or before a clip has finished downloading, or if a source starts stuttering, the choice is made for it. A clip that keeps re-buffering is worse than one that opens slowly.",
        "Each change-over records which source was used and why, so the decision the panel made can be read back from the dashboard.",
        "Includes 1.7.41: clips appear within a frame or two of starting, and every video in the library has had its index moved to the front of the file.",
        "On-screen build bumped to 70, app-shell cache to v70."
      ]
    },
    {
      "version": "1.7.41",
      "releasedAt": "2026-08-27T14:27:24Z",
      "filename": "com.lg.app.signage_1.7.41_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.41_all.ipk",
      "size": 501062,
      "sha256": "6b51f38d6e68ca999a3bf4abe4690bdefe3d3587d3acadc63ae2d00d241fa488",
      "title": "Clips now appear within a frame or two of starting instead of a sixth of a second in, and every video in the library has had its index moved to the front.",
      "notes": [
        "A clip is shown as soon as it is really running, rather than when the panel next reports its position. Those position reports only arrive about four times a second, so the player was waiting an average of 160 ms after playback had already begun - which is both extra black and the first moments of the clip going unseen. It now checks every animation frame, so a clip appears within a frame or two of its first picture.",
        "Every video already in the library has been rewritten so its index sits at the front of the file. An MP4 with its index at the end cannot be decoded until the player has read all the way to the end - measured on one real 17 MB clip, reaching the first frame took 4 requests and 51.9 MB of reading with the index at the end, against 1 request and 6.8 MB with it at the front. 27 files were rewritten; the video and audio are untouched (no re-encoding, identical picture), only the index moved.",
        "New uploads are checked and fixed automatically, so this cannot come back.",
        "Every change-over now also reports what the panel's own decoder did - frames presented, frames dropped, and how many frames the outgoing clip managed to show while the next one was opening. That is the panel answering directly, rather than anything being inferred from the outside.",
        "On-screen build bumped to 69, app-shell cache to v69."
      ]
    },
    {
      "version": "1.7.40",
      "releasedAt": "2026-08-27T12:25:41Z",
      "filename": "com.lg.app.signage_1.7.40_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.40_all.ipk",
      "size": 500870,
      "sha256": "5c6064c6e0e06da4b57adb6642b8037a83f85dd4d82a3415ca49ea07190f1cdc",
      "title": "The dashboard finally knows what each LG panel actually is - model, webOS version, firmware, MAC and IP - and identifies it by its serial number.",
      "notes": [
        "Every LG screen in the dashboard has been showing a blank model, blank webOS version, blank MAC and blank IP since the first release. The player was asking the panel for all of this through an interface the app does not include, so every one of those questions silently returned nothing. It now asks over the same channel it already uses to download and play media - which has worked all along - using the signage APIs from LG's own reference player.",
        "The webOS version is now reported as the signage platform version together with the firmware, e.g. \"4.1.0 (fw 04.20.30)\". LG numbers commercial signage displays separately from consumer TVs, and it is the signage platform number that determines what the panel's browser and video hardware can do.",
        "A panel is now identified by its serial number instead of a random id generated on first run. The old id lived in browser storage, so reinstalling the app made the same physical screen look like a brand-new device.",
        "None of these questions can hold up the player any more: each one gives up after four seconds, so a panel that does not answer carries on exactly as before.",
        "Includes everything from 1.7.39: clips are never played out of sight (so they no longer start a second into the file), and the held frame plays the exit transition instead of the last frame being shown twice.",
        "On-screen build bumped to 68, app-shell cache to v68."
      ]
    },
    {
      "version": "1.7.39",
      "releasedAt": "2026-08-27T12:05:58Z",
      "filename": "com.lg.app.signage_1.7.39_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.39_all.ipk",
      "size": 500480,
      "sha256": "728c7b7a67c50fa68ab6b4f744086d3990cdc1b06536d775ed03d7a0dcab2fbb",
      "title": "Clips are no longer played where they cannot be seen - which is why they were starting a second in - and the held frame now plays the exit transition instead of being shown twice.",
      "notes": [
        "The clip you see is no longer the middle of the file. Every build until now created the next clip hidden, started it playing, and only made it visible once frames were flowing. The panel starts its clock at the moment playback begins, so everything it did not display in that first second was not queued up - it was skipped. That is exactly what was noticed: after the black, the video was already about a second in, and the amount it had skipped matched the length of the black. Clips are now visible from the moment they are created and are never played out of sight, so playback starts where the file starts.",
        "The picture that covers a change-over is held outside the video area, as in the previous build, and it now also plays the outgoing item's exit transition. Before, the last frame was held twice over - once by the cover while the next clip opened, and then a second time by a copy frozen into the video area so the fade had something to animate. That is the 'shows the last frame for a while' half of the report; there is now one hold, and it ends in the transition the playlist asks for.",
        "As a side effect, exit transitions finally work on video items. CSS has no authority over the panel's video hardware, so a fade or slide applied to a playing clip was never visible; applied to the held frame, all twelve transitions behave exactly as they do in the dashboard preview.",
        "Playback now starts the moment the clip can play through, with no added pause - the pause existed to protect a mask that no longer needs protecting.",
        "Each change-over now reports how far into the clip the picture appeared and whether the held frame was a real picture, so the two things that could never be seen from the dashboard are now measured on every single change.",
        "On-screen build bumped to 67, app-shell cache to v67."
      ]
    },
    {
      "version": "1.7.38",
      "releasedAt": "2026-08-27T10:13:55Z",
      "filename": "com.lg.app.signage_1.7.38_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.38_all.ipk",
      "size": 501458,
      "sha256": "d4e34d754d8e9a49e09fd458663840f5347bed36b65382854bf235f49e23a942",
      "title": "Undoes the extra wait 1.7.35 added before a clip starts - it was more than doubling the black, and buying nothing.",
      "notes": [
        "On this panel the screen is blanked from the moment the incoming clip's decoder becomes active until its first picture arrives. Every fraction of a second spent preparing between those two points is therefore black on the wall, not hidden behind anything.",
        "1.7.35 held a clip back until the panel reported it had enough of it to play straight through, on the theory that a better-prepared clip would show its first picture sooner. Measured over 89 change-overs, it did not: the hand-over itself stayed at 170 milliseconds either way, while the black grew from 236 to 573 milliseconds. That wait is gone - a clip starts as soon as it can, exactly as it did up to 1.7.34.",
        "Kept from 1.7.37: the clip that is leaving is still replaced by its own last frame before the next one opens, so a change from clip to clip costs the same as a change from a picture to a clip.",
        "On-screen build bumped to 66, app-shell cache to v66."
      ]
    },
    {
      "version": "1.7.37",
      "releasedAt": "2026-08-27T09:43:50Z",
      "filename": "com.lg.app.signage_1.7.37_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.37_all.ipk",
      "size": 501516,
      "sha256": "e8c4d92271dd0a8c43c23895c54f247afab26d61220d65dfd947535247780398",
      "title": "Video to video no longer goes black for the whole change-over: the clip that is leaving is replaced by its own last frame before the next one opens.",
      "notes": [
        "Measured on the 43-inch panel: changing from a picture to a clip went black for about a sixth of a second, but changing from a clip to a clip went black for well over a second - the whole time the new clip was opening. The difference is the clip that is leaving. While it is still playing it owns the panel's single video output, and the incoming clip takes that output the moment it starts opening - long before it has a picture of its own. So the outgoing clip was not covering the change-over at all; it was the thing being blacked out, and it took the screen with it.",
        "The clip that is leaving is now replaced by its own last frame - the still already prepared and decoded when that clip started - and its decoder is handed back BEFORE the next clip opens. A change from clip to clip is then exactly a change from a picture to a clip, which is the short one.",
        "The swap only happens when that last frame is ready to draw in the same instant. If it is not, the clip keeps playing as before - an empty layer would be worse than the old behaviour, which is what went wrong when this was tried in 1.7.26.",
        "On-screen build bumped to 65, app-shell cache to v65."
      ]
    },
    {
      "version": "1.7.36",
      "releasedAt": "2026-08-27T09:34:11Z",
      "filename": "com.lg.app.signage_1.7.36_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.36_all.ipk",
      "size": 501404,
      "sha256": "733177bd857dca3895e336a95a42caa36449a6b6762de368e90eb93c91e9a625",
      "title": "The panel was still running the previous release's code while reporting the new one - which is why the last two fixes changed nothing on screen. It now notices and corrects itself.",
      "notes": [
        "A panel keeps its own copy of the app's code. After an update the new code is on the panel, but if that copy is used instead, the panel goes on running the OLD version while reporting the new one - a fix ships, the dashboard says it is installed, and the screen behaves exactly as before. That is what has been happening here: the change-over timings coming back from the 43-inch panel were the previous release's, to the millisecond.",
        "At start-up the app now compares the version built into its own code with the version of the installed package. If they disagree it clears its copy of the app's code and restarts once, so the installed release is the one actually running. Downloaded media is left alone.",
        "Every change-over also reports which build produced it, so this is visible from the dashboard instead of being mistaken for a fix that did not work.",
        "On-screen build bumped to 64, app-shell cache to v64."
      ]
    },
    {
      "version": "1.7.35",
      "releasedAt": "2026-08-27T08:35:28Z",
      "filename": "com.lg.app.signage_1.7.35_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.35_all.ipk",
      "size": 501114,
      "sha256": "db86ecfe2aac37560c402e9a186b42219758404fb5aeed12bafc5ad385edc262",
      "title": "The black at the start of a clip is the panel's video hardware, not the app - so the clip is now fully prepared before it is allowed to take the screen.",
      "notes": [
        "This panel has ONE video output and it sits in front of everything the app draws. The moment a clip is told to play it takes that output - and until its first picture arrives, the output is black: over the previous clip, over the covering picture, over everything. That is why no covering picture ever helped, however early it was raised or however long it was held. Nothing the app draws is in front of it.",
        "The only thing that shortens that black is leaving the panel less to do when the clip takes the screen. A clip used to be started a sixteenth of a second after it first said it could begin, and the panel then took about a fifth of a second to show a picture. It is now started only once the panel reports it has enough of the clip to play straight through, plus a short settle - so the picture arrives almost as the clip takes the screen. LG's own reference player gets there by simply waiting three seconds before starting anything; this waits for the panel to say it is ready instead.",
        "Every change-over reports whether that wait was satisfied or timed out, so what is left of the gap can be measured from the dashboard instead of watched for.",
        "On-screen build bumped to 63, app-shell cache to v63."
      ]
    },
    {
      "version": "1.7.34",
      "releasedAt": "2026-08-27T08:04:26Z",
      "filename": "com.lg.app.signage_1.7.34_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.34_all.ipk",
      "size": 501046,
      "sha256": "ae932346ed624361fd1ea13096de8a190982cdc72c5d48aafd5deed53f83de46",
      "title": "Two fixes for the black flash when a clip starts, and the panel now says in its own report which of the two it was.",
      "notes": [
        "The picture that covers a change-over is loaded well ahead of time, but on this firmware 'loaded' is not 'decoded': the browser was free to keep it as compressed bytes and turn it into a picture only when it was first put on screen - which is the exact moment the change-over needed it. A full-screen photo decoded at that moment is a tenth of a second of the cover's own black backdrop, right as the old picture goes off and the clip starts. It is now decoded the moment it is loaded, while the outgoing item is still comfortably on screen.",
        "The picture covering a change-over was also taken away about a thirtieth of a second after the new clip was switched on. On a TV, 'switched on' and 'actually showing a picture' are not the same instant - the panel's video pipeline runs a few frames behind - so the cover could be pulled while the screen was still black. It is now held a fifth of a second longer, which nobody can see, instead of being pulled early, which everybody can.",
        "The trick that keeps a clip's warm-up out of sight - opening it in a four-pixel box until it has a picture - switched itself off permanently the first time two clips were slow to open, which at a cold start (media still downloading) is close to guaranteed. From then on every clip warmed up at full screen for the rest of the day, with nothing bounding the black. It now only counts slow clips that happen back to back, allows more time before calling one slow, and tries the box again every ten clips instead of writing the panel off.",
        "With the technician overlay switched on, the covering picture's backdrop turns magenta: a magenta flash means the cover was up without its picture, a black flash means something took the screen while the cover was up. Each change-over also reports whether that picture had finished loading and how long the cover was held, so a panel that still flashes is diagnosed from the dashboard instead of in front of it.",
        "On-screen build bumped to 62, app-shell cache to v62."
      ]
    },
    {
      "version": "1.7.33",
      "releasedAt": "2026-08-27T10:19:05Z",
      "filename": "com.lg.app.signage_1.7.33_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.33_all.ipk",
      "size": 500518,
      "sha256": "c9bac3750e900538466e83157cce8a4532a7b914c624f7c2af3b4aebd848e290",
      "title": "The picture that covers a change-over is now held outside the video area entirely - the one place on these panels that does not go black while a clip opens.",
      "notes": [
        "The black is the panel, not the app: while a clip is being opened, everything in the video area goes black - including the clip that is still playing in it - until the new clip has a picture. That is why none of the previous attempts worked. A still image, a photograph of the last frame, a picture stacked on top, the incoming clip shrunk to four pixels: all of them were inside the video area, so all of them were blacked out along with everything else, while the measurements showed them doing their job perfectly.",
        "The frame the viewer is looking at is now raised OUT of the video area before the next clip is opened, and comes down once that clip is actually painting. It is held in the same place as the loading screen shown when the player starts, which is the one thing that has always stayed visible through a clip opening - the only thing special about it is that it was never in the video area.",
        "The picture is loaded and decoded a few seconds before it is needed, so raising it is instant rather than a decode with nothing on screen behind it.",
        "The change from 1.7.32 that opened the next clip a second and a half early has been reverted. On paper it cut the change-over from 0.9 s to 0.3 s; on the wall it made things worse, because opening a clip early blanks the video area early - the black simply started before the previous clip had finished. Clips are opened when their turn comes, under the held picture.",
        "An image followed by a video is covered the same way, by the image itself.",
        "On-screen build bumped to 61, app-shell cache to v61."
      ]
    },
    {
      "version": "1.7.32",
      "releasedAt": "2026-08-26T14:18:11Z",
      "filename": "com.lg.app.signage_1.7.32_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.32_all.ipk",
      "size": 500076,
      "sha256": "a4b590def77e3281009af2fbc7dd0bbc22789426451a7db4aeedd8362744a57c",
      "title": "The clip on screen now keeps playing until the next one has a picture - and the next one is opened a second and a half before its turn.",
      "notes": [
        "This is the black at a change into a video, fixed at the level it was actually happening. Since 1.7.26 the player took the clip that was on screen, froze it to a photograph and handed the panel's video hardware back a quarter of a second BEFORE the slot ended - and from that moment until the next clip painted there was no video running on the panel at all. Nothing drawn by the app covers that: a still, a photographed frame, a picture stacked on top, the next clip shrunk to four pixels - all of it was measured doing its job on the panel while the wall was black anyway. The clip that is on screen is now left alone. It keeps playing, and nothing is frozen, paused or released until its replacement is actually showing frames.",
        "The next clip is opened while the current one is still playing, which is the half of LG's own reference player this one never copied. Opening a clip costs about six tenths of a second on these panels, and doing it at the moment the slot changes meant that time had to come out of the screen. It is now spent while the previous clip is still up, so the change-over is a frame or two instead of most of a second.",
        "A clip's exit transition still works: the outgoing clip is turned into a still at the moment the new one appears - late, where releasing the hardware is safe - and it is the still that fades or slides away. Before, that happened before the change-over, which is what made the change-over black.",
        "The outgoing picture no longer sits on top of the new clip for three quarters of a second after the change. It is held only for as long as the transition that is actually running needs.",
        "Every change-over now reports whether the clip had been opened early or from cold, so a slow one can be told from a stalled one from the dashboard.",
        "On-screen build bumped to 60, app-shell cache to v60."
      ]
    },
    {
      "version": "1.7.31",
      "releasedAt": "2026-08-26T13:51:46Z",
      "filename": "com.lg.app.signage_1.7.31_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.31_all.ipk",
      "size": 499714,
      "sha256": "afc9be6cbf8d71369ad906db7a0309f530a72ab4bcbc8aaa35aaa6b0ddbae444",
      "title": "A screen can now be put into diagnostic mode from the dashboard - and while it is, it alternates between the two change-over methods so the wall itself shows which one goes black.",
      "notes": [
        "The black at a change into a video has now been chased through two different explanations, each of which was fixed and each of which came back. The two candidate methods cannot both be wrong for the same reason, so this build alternates them - one change-over each - on a screen that has been put into diagnostic mode, and labels each one on screen. Whichever label is on the screen when it goes black is the answer. Nothing alternates on any other screen: every other panel behaves exactly like 1.7.30.",
        "Method A holds a still picture of the outgoing clip and gives the video hardware back before the next clip opens (what 1.7.26 to 1.7.30 do). Method B leaves the outgoing clip running and touches nothing until the next clip has real frames (what LG's own reference player does, and what this player did up to 1.7.25).",
        "Diagnostic mode also draws an unmissable frame around the screen for exactly as long as a change-over lasts. If that frame is visible while the screen is black, the page is being painted and the black is coming from inside it; if the screen goes black and the frame disappears with it, the panel's video hardware is covering the page and no picture drawn by the app can ever cover a change-over - which is a different fix entirely. It is the question this bug has never been able to answer.",
        "Diagnostic mode is switched on per screen from the dashboard's database, so a panel can be diagnosed without anyone standing in front of it, and it turns itself off if left on by mistake.",
        "On-screen build bumped to 59, app-shell cache to v59."
      ]
    },
    {
      "version": "1.7.30",
      "releasedAt": "2026-08-26T13:09:29Z",
      "filename": "com.lg.app.signage_1.7.30_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.30_all.ipk",
      "size": 499210,
      "sha256": "302564d0e3ffb59ad11bd08d1e6c10532bacca23fd3095b29f41dfff38945d90",
      "title": "The picture that covers a change-over is now painted OVER the next clip instead of under it - which is why none of the previous fixes removed the black.",
      "notes": [
        "The black between two videos is fixed at its actual cause. On these panels a video takes ownership of its rectangle on screen the moment it is given a file - before it has decoded a single frame, and whether or not it has been hidden. Everything the page draws underneath that rectangle stops being visible, so the still image the player puts up to cover the change-over was being covered itself, by a decoder with no picture in it. That is the black. Every mask this player has tried - holding the outgoing clip live, the dashboard's still, a photograph of the last frame - was placed underneath the incoming clip, which is why each one measured as working and none of them changed what was on the wall. The covering picture is now stacked ABOVE the clip that is opening, stays there for the whole opening, and is only taken away one frame after the new clip is on screen and painting.",
        "A clip also now opens inside a four-pixel box in the corner of the screen, and is only given the full screen back once it has real frames to put there - so even if a panel insists on showing a decoder that has nothing to show, there is nothing to see. If a panel refuses to open a clip that way the player notices and turns the trick off by itself, on that panel, for the rest of the session.",
        "Clips playing from the TV's own storage start about a fifth of a second sooner: the extra pause before playback existed to let the first packets of a stream arrive, which a file on the panel does not need.",
        "The measurements the panel sends to the dashboard now say which picture covered each change-over even when the clip was frozen a moment before its slot ended - until now that was recorded as 'ordinary HTML' for every video-to-video change-over, which hid the one field that explains a black one. Each change-over now also reports when the plane went up and when the covering picture came down.",
        "On-screen build bumped to 58, app-shell cache to v58."
      ]
    },
    {
      "version": "1.7.29",
      "releasedAt": "2026-08-26T11:40:35Z",
      "filename": "com.lg.app.signage_1.7.29_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.29_all.ipk",
      "size": 498590,
      "sha256": "0191f9aacfbf20c6534c5b5c70c2b27264e21ce86c050d99f4473299ad94413b",
      "title": "The change into a video is covered by a picture again - and the panel now measures every change-over and tells the dashboard how long it took.",
      "notes": [
        "When one clip hands over to the next, the picture on screen has to be a still image, because the panel can only give its video hardware to one clip at a time - if the outgoing clip is still running when the next one starts, the screen goes black for as long as the new clip takes to open. The player takes that still itself when the dashboard has not supplied one, and on these panels that photograph was being thrown away every single time: the clip is served by the TV's own media server, which counts as a different source, so reading the picture back is not allowed. The player treated 'not allowed to read it' as 'the photograph failed' and fell back to leaving the old clip running - which is exactly the black. It now keeps the photograph.",
        "Every change into a video is timed on the panel itself - when the clip was opened, when it could play, when the first real frame arrived, when it went on screen - and which picture covered the gap. Those timings are sent to the dashboard. Until now nothing could see this problem: the player's own play history is written from timers, so it reported a three-millisecond change-over while the wall was showing a second of black.",
        "Also carried over from 1.7.28: widgets keep their content up to date on a screen nobody is editing, Instagram and Facebook pictures are kept on the panel so they survive a dropped connection, and a panel waking from standby asks for fresh widget content straight away.",
        "On-screen build bumped to 57, app-shell cache to v57."
      ]
    },
    {
      "version": "1.7.28",
      "releasedAt": "2026-08-25T09:21:22Z",
      "filename": "com.lg.app.signage_1.7.28_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.28_all.ipk",
      "size": 497526,
      "sha256": "e055f3622c40fb06263364a974d636e81d88108242195cfa8502d6de21e1889f",
      "title": "Widgets now keep their content up to date on a screen nobody is editing - including an Instagram account connected after the playlist was published.",
      "notes": [
        "A widget's feed - Instagram posts, news headlines, the weather - was only ever fetched when the playlist itself changed. On a screen that is simply left running, which is what a working sign is, that meant the content it showed was frozen at whatever it was when the playlist was last published.",
        "That is why an Instagram widget could keep saying \"Connect your Instagram account\" long after the account had been connected: connecting an account does not change anything in the playlist, so the player never asked again. It now refreshes any widget whose data is more than five minutes old, which matches how often the dashboard refreshes its own copy (and what the Android player has always done).",
        "The refresh only fetches the widgets that are actually stale, and a failed fetch keeps the last good content - a screen offline for an hour still shows its last headlines rather than going blank.",
        "A panel waking from standby asks straight away instead of waiting out the next check, so a screen switched on in the morning is current within seconds.",
        "On-screen build bumped to 56, app-shell cache to v56."
      ]
    },
    {
      "version": "1.7.27",
      "releasedAt": "2026-08-24T18:57:42Z",
      "filename": "com.lg.app.signage_1.7.27_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.27_all.ipk",
      "size": 497254,
      "sha256": "5e12f5b7510a1f8858eac962063e0b6f2e1765914fe940c94cbf364ec0124cb5",
      "title": "The change from a video to its still, and from that still to the next video, is now one continuous picture.",
      "notes": [
        "No more flash where a clip hands over to its own last frame. Every clip is set to loop (a video that ENDS blanks the screen on these panels), so a clip playing for as long as it lasts had already jumped back to its first frame at the moment the player swapped in the end-of-clip still - you saw the end, the beginning, then the end again. The swap now happens a quarter of a second earlier, while the picture is still the one the still shows, so the two are the same image.",
        "That early hand-over also gives the clip's decoder back before the NEXT clip asks for it, so the next video opens against a free decoder instead of waiting for one.",
        "The still is now prepared while the clip is playing, seconds before it is needed, instead of being fetched and decoded at the exact moment the picture is handed over. A still that had not been cached yet used to leave the panel with nothing to show during that fetch - which is where the long black came from.",
        "The next clip no longer takes the screen until it can play straight through. It used to start as soon as one frame existed, which hands the panel's single video plane over while the decoder is still filling - black until frames flow. Now the previous still stays up for that time instead: a held picture rather than a black screen, exactly the trade an operator would pick.",
        "Same release on Android (1.4.31), where a clip that fills its slot now holds its final frame rather than looping back a frame before the hand-over.",
        "On-screen build bumped to 55, app-shell cache to v55."
      ]
    },
    {
      "version": "1.7.26",
      "releasedAt": "2026-08-21T19:08:37Z",
      "filename": "com.lg.app.signage_1.7.26_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.26_all.ipk",
      "size": 497026,
      "sha256": "b94acb6c2bc4d33a1e66766ab32653f36bace4f866a8db31df7112f1587fbe63",
      "title": "No more black flash when one video hands over to the next.",
      "notes": [
        "The still that covers the change from one clip to the next is now guaranteed to be ON SCREEN before the outgoing clip is taken down. The player was putting the still up and switching the video off in the same instant, so the panel showed the gap for as long as that picture took to appear - which on these panels is exactly the black flash you see between two videos.",
        "When no still can be produced at all, the outgoing clip keeps playing as the cover and the incoming one now waits until it can play straight through before it starts. These panels have a single video plane and starting the next clip takes it away from the one doing the covering, so this keeps that hand-over as short as the file allows.",
        "The dashboard now has an end-of-slot still for every video already in your library - previously only playlists published after the feature shipped had them, and everything older fell back to no cover at all. That backfill also means an LG panel finally has the same cover an Android one has.",
        "Same release on Android (1.4.30), where the cover image was being loaded while it was already on screen - so the first frames of every hand-over were a black rectangle. It is now decoded during the previous clip and drawn instantly.",
        "On-screen build bumped to 54, app-shell cache to v54."
      ]
    },
    {
      "version": "1.7.25",
      "releasedAt": "2026-08-21T16:35:11Z",
      "filename": "com.lg.app.signage_1.7.25_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.25_all.ipk",
      "size": 496770,
      "sha256": "d3984b9e18afa9955d6e4dfcb7c38f3b0b4c90140d6d8eb843cbbc50d95542b5",
      "title": "Deleting a screen in the dashboard now really lets go of the panel: it returns to the pairing codes and stops playing.",
      "notes": [
        "When you delete a screen on the CMS the panel goes back to the pairing screen, as before - but it now actually stops. The old session was left running behind the QR codes: the clip that was on screen carried on playing, sound and all, and the player kept polling and downloading for a screen that no longer exists.",
        "One of those leftover timers could bring the whole thing back to life. During a cold start the player checks every second whether enough content has arrived to begin, and that check starts playback - so a player shut down inside that window restarted itself a second later, playing a deleted screen's content over the pairing screen.",
        "Same release on Android (1.4.29), where deleting a screen left the panel stuck on \"Screen Connected!\" - it bounced between that and the player every second and a half and never showed its pairing codes again. It now returns to the codes and waits to be claimed.",
        "On-screen build bumped to 53, app-shell cache to v53."
      ]
    },
    {
      "version": "1.7.24",
      "releasedAt": "2026-08-20T18:51:07Z",
      "filename": "com.lg.app.signage_1.7.24_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.24_all.ipk",
      "size": 496658,
      "sha256": "1544c05cafd15bdbb3f9e80ff58069d3230a6cfd5c039443d3802e53d06c7f93",
      "title": "The still shown between two videos is now the exact frame the clip ended on, prepared by the CMS when you publish.",
      "notes": [
        "When you publish a playlist the CMS now captures, for every video, the precise frame it will be showing when its slot ends - and the player holds that up while the next clip starts. Previously it used the end of the FILE, so a 60-second video set to show for 10 seconds jumped to a completely different picture at the changeover.",
        "The capture understands the whole play window: trimmed clips, clips that start part-way in, and clips that loop. A 60-second video shown for 90 seconds ends 30 seconds into its second pass, and that is the frame that gets captured.",
        "The player prefers the CMS still and falls back to photographing the frame itself, so panels that have not received the new image yet - or content published before this release - still change over cleanly.",
        "Same release on Android (1.4.25), which also fixes a one-frame black flash at the end of every item: the outgoing still was being put up one frame late, which on a slower panel read as a small shutter.",
        "On-screen build bumped to 52, app-shell cache to v52."
      ]
    },
    {
      "version": "1.7.23",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.23_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.23_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "No more dimming between items, and no more long black before a video starts. Transitions at the end of a clip now actually play.",
      "notes": [
        "The screen no longer dims through every change of item. Both halves of a cross-fade were being faded at once, which leaves the picture at about three-quarters brightness in the middle and recovers - on bright video that read as a black shutter. Only one side moves now, so the picture stays complete the whole way through.",
        "A video that cannot start no longer blacks the screen out. The player used to switch the picture on regardless and drop the item that was masking for it, leaving black until a retry succeeded seconds later. It now holds the previous item on screen and moves on at the normal time.",
        "Videos start much sooner after another video. The outgoing clip is frozen to a still frame and its decoder handed over immediately, instead of being held for over a second - these panels only have one or two decoders, so the next clip was being starved and failing its way through retries.",
        "A transition at the END of a video is now visible. Exits were being run on the live video picture, which the panel draws outside the web page and ignores styling on, so nothing happened; they now run on the frozen still and animate properly.",
        "A clip that never manages to show a frame releases its decoder at the end of its turn rather than holding one for the rest of the loop and taking the following clips down with it.",
        "Same release on Android (1.4.24).",
        "On-screen build bumped to 51, app-shell cache to v51."
      ]
    },
    {
      "version": "1.7.22",
      "releasedAt": "2026-08-20T07:30:54Z",
      "filename": "com.lg.app.signage_1.7.22_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.22_all.ipk",
      "size": 495962,
      "sha256": "78f2a6c9f0925a4895a42425ac2b28b22b9592671d84fe49fdfb7f6a2dd74766",
      "title": "Transitions now play properly at both ends of every item - including around videos and widgets, where they were being cut short or skipped entirely.",
      "notes": [
        "An item's EXIT animation now uses its own duration. It was taking the duration from the item arriving next, so an exit only ran when the next item happened to have an entrance - and whenever that next item was set to 'None', the exit was given zero milliseconds and never played at all.",
        "The item leaving is now drawn ON TOP while its exit plays. An exit is the old item moving away to reveal the new one, which cannot read correctly from underneath - previously the arriving item covered it immediately and you saw at most a fraction of the effect.",
        "Anything followed by a VIDEO now plays its exit. The video path held the previous image or widget on screen as a mask and then simply deleted it, so that hand-off had no transition of any kind.",
        "A video's own exit is no longer cut off. The clip's picture was being switched off the moment the next item landed, part-way through its fade or slide; it now stays until the animation has finished.",
        "Same release on Android (1.4.22), where a video's ENTRANCE was starting when the clip was handed to the decoder rather than when it had a picture - so on a slower panel the animation was over before there was anything to see, and the clip appeared to cut in.",
        "On-screen build bumped to 50, app-shell cache to v50."
      ]
    },
    {
      "version": "1.7.21",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.21_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.21_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "No more black screens around videos and widgets. Every hand-off in the rotation now keeps a real picture on screen from one item to the next.",
      "notes": [
        "Widgets no longer flash a black rectangle while they load. The player now waits for the widget to report that it has actually drawn something before swapping it in, so the item before it stays on screen through the widget's start-up instead of the wall going black.",
        "The black frame at the end of a video is gone. The outgoing clip's picture is switched off only once the next item is already covering the screen, and the clip itself is torn down a beat later instead of exactly on the seam.",
        "Videos later in a long playlist no longer come up black. Each clip's hardware decoder is now explicitly released when its layer rotates out - an LG panel only allows one or two at a time, and the old clean-up left them held.",
        "Transitions now play over the previous item instead of behind it. Fades, slides, scales and the rest were animating underneath the outgoing content, so they were invisible and the next item simply popped in; they now run on top, exactly as the dashboard preview shows them.",
        "A very short item can no longer be wiped to black by the previous item's clean-up timer.",
        "Same release on Android (1.4.20).",
        "On-screen build bumped to 49, app-shell cache to v49."
      ]
    },
    {
      "version": "1.7.20",
      "releasedAt": "2026-08-18T18:36:33Z",
      "filename": "com.lg.app.signage_1.7.20_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.20_all.ipk",
      "size": 564288,
      "sha256": "87a2c0f73e25e587eab8eb46852f0c1ee7d53121b36cd93a201ee79fac0942ef",
      "title": "The ticker band can now be see-through. A new Band opacity slider lets your content show through the crawl instead of being covered by a solid strip.",
      "notes": [
        "Settings for both the Ticker widget and the Ticker scheduler now have a Band opacity control (0-100%). At 100% nothing changes - the band is solid, exactly as before - and anything below lets the picture behind the ticker show through.",
        "Only the band's fill fades. The headline text, your leading label and any logo stay fully opaque, so a see-through band is still readable. This is the difference between a usable overlay and an unreadable one.",
        "It works with every band fill: a solid colour, a gradient (both ends fade together), and a band background image.",
        "Especially useful for the ticker SCHEDULER, which overlays a crawl on top of whatever is already playing - you can now keep the content visible underneath instead of losing a strip of it.",
        "The live preview in the dashboard shows the real thing, so what you set is what the wall shows.",
        "On-screen build bumped to 48, app-shell cache to v48."
      ]
    },
    {
      "version": "1.7.19",
      "releasedAt": "2026-08-18T10:44:30Z",
      "filename": "com.lg.app.signage_1.7.19_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.19_all.ipk",
      "size": 562904,
      "sha256": "8098e69a98cb559ac7d51427e114a3820cd509af6cbd4818d6344ce5874e11d6",
      "title": "Set how big your splash image is. A new size slider lets the artwork fill the whole panel, or sit centred at any size down to 10% - and the per-screen panel now opens the same full preview the settings page has.",
      "notes": [
        "Settings > Splash Screen has an Image size slider (10-100% of the screen) alongside the existing fill/fit choice. The two work together: size decides how much of the panel the artwork covers, fill/fit decides how the image fills that box. Anything outside it is black, exactly as the preview shows.",
        "This is what you want for a logo, a portrait poster on a landscape screen, or artwork that should not be stretched edge to edge. 100% is the default and is identical to how the splash behaved before, so nothing changes until you move the slider.",
        "Quick presets at 25 / 50 / 75 / 100% sit under the slider, because dragging back to exactly full-screen was a pixel hunt.",
        "Screens > any screen > Splash screen gets the same size slider for a per-screen override, plus a new Full preview button. That opens the identical preview stage the settings page uses - aspect-ratio switcher included - so you can check a portrait totem or an ultrawide before you save, instead of judging it from a small thumbnail.",
        "A screen set to Workspace default previews the workspace size too, so switching between the two options shows you the real difference.",
        "The player draws the sized artwork the same way the dashboard preview does, and a new build check compares the size bounds and the centring maths across the backend, both players and the preview - so what you see stays what goes on the wall.",
        "On-screen build bumped to 47, app-shell cache to v47."
      ]
    },
    {
      "version": "1.7.18",
      "releasedAt": "2026-08-18T10:11:55Z",
      "filename": "com.lg.app.signage_1.7.18_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.18_all.ipk",
      "size": 560548,
      "sha256": "011fb906edab6c0d78ef1f6f0a6083e735ea97f1d4c341763b69a3e6b2d03cee",
      "title": "The player wakes up with the display. When the panel comes back from standby it goes straight back to your content and reports itself online, instead of sitting on a frozen frame while the dashboard shows it offline.",
      "notes": [
        "When an LG panel goes into standby webOS freezes the app: every timer stops, the live push connection to the CMS is dropped for good, and the player has already told the dashboard it went offline. Nothing recovered from that on its own - the screen came back showing whatever frame it was on, the dashboard kept it marked offline for up to two minutes, and any playlist change made while the display was off was not picked up until the next poll happened to land.",
        "Waking now rebuilds the whole live layer: an immediate heartbeat (so the dashboard flips back to online in seconds, not minutes), fresh polling timers, a new push connection, and a re-sync that catches up on anything you changed while the display was off.",
        "Playback that survived the standby is left alone - a panel that woke up mid-clip keeps playing it rather than jumping back to the top of the playlist. A rotation that actually died is detected and restarted from scratch.",
        "'Stop' from the dashboard still outranks a wake-up: a screen you deliberately stopped stays blank until you press Continue.",
        "Same release on Android (1.4.16), where the player also now relaunches itself after a power cut and when the screen switches on - see the in-app 'Automatic start-up' setup.",
        "On-screen build bumped to 46, app-shell cache to v46."
      ]
    },
    {
      "version": "1.7.17",
      "releasedAt": "2026-08-18T08:13:27Z",
      "filename": "com.lg.app.signage_1.7.17_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.17_all.ipk",
      "size": 557788,
      "sha256": "db6df95e61e9ebb6c47de0e7fbfb70175616740c3023da30d0a4f2054ec8b2e5",
      "title": "Widgets fill the whole screen again - the thin white strip down the right-hand edge of every widget is gone.",
      "notes": [
        "Every widget (Instagram, Facebook, news, weather, ticker) rendered with a scrollbar-width bar of empty space pinned to the right edge of the panel, which showed up white. The widget engine is built from the dashboard's own stylesheet, and that stylesheet permanently reserves room for a scrollbar so the dashboard doesn't jump sideways when a dialog opens. On a signage panel there is no scrollbar to put there - it just carved roughly 10px off the right of the picture that nothing ever painted into. The widget engine now opts out of that rule, so the artwork runs edge to edge.",
        "The same stylesheet was also painting the widget page on the dashboard's light background. Anywhere the widget itself didn't cover, that light colour showed through; it is black now, so nothing can flash light behind a widget again.",
        "The widget frame no longer sits on a white backing. It was white, which is why the reserved strip read as a bright white bar rather than blending in, and it also hid the 'Loading widget...' spinner behind it and flashed white for a frame or two before a widget painted. The frame is transparent now.",
        "The fix ships in the widget engine itself, so the Android player gets it from the same artifact - the strip was dead space there too, just against a dark backing rather than a white one.",
        "On-screen build bumped to 45, app-shell cache to v45."
      ]
    },
    {
      "version": "1.7.16",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.16_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.16_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "The loading screen is now identical on LG and Android panels, and the dashboard preview matches both exactly - what you see when you pick a splash image is what goes on the wall.",
      "notes": [
        "The Android player's loading and 'nothing to play' screens were a different design from the LG player's: a cloud icon instead of the logo, different text sizes, a green progress bar instead of the brand one, and different wording ('Downloading 3/8: clip.mp4' against 'Loading your content · 3 of 8 ready'). Both now render from one shared contract, so a mixed estate looks like one product.",
        "The dashboard preview is the same contract again, so the 'Splash screen' panel in Screens and Settings shows the real thing - the actual logo file the panel ships, the real type scale, the real brand progress bar - rather than an approximation.",
        "Sizes are laid out on the player's 1920x1080 canvas and scaled to the real panel, so the screen holds its proportions on a 4K wall, a 720p display and a portrait totem alike.",
        "With a splash image set, the status drops to a translucent strip along the bottom on both players and in the preview, at matching size and position, so your artwork is what the public sees while an installer can still read the progress.",
        "A new build check (scripts/check-splash-parity.mjs) compares all 18 measurements, 5 colours and 6 pieces of copy across the LG stylesheet, the Android screen and the dashboard preview, and fails the build if any of them drift apart again.",
        "On-screen build bumped to 44, app-shell cache to v44."
      ]
    },
    {
      "version": "1.7.15",
      "releasedAt": "",
      "filename": "com.imagesignage.player_1.7.15_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.7.15_all.ipk",
      "size": 0,
      "sha256": "",
      "title": "Your own splash screen while the panel loads. Pick an image in Screens - per screen, or one for all of them - and the player shows it full-screen instead of the ImageSignage loading card.",
      "notes": [
        "Screens › any screen › Splash screen lets you choose an image from your media library. Three options per screen: follow the workspace default, use a different image for this screen only, or turn the splash off and keep the built-in loading card. 'Apply to all screens' sets the default and pulls every screen back onto it in one click.",
        "The panel downloads the image as soon as it connects, or the moment you change the setting - it does not wait for the next content download. It is cached alongside your media, so it still paints when the panel boots with no network, which is exactly when a loading screen matters.",
        "The image is shown while media downloads, during the cold-start 'preparing content' wait, and whenever the panel has nothing to play. Download progress stays readable in a small bar along the bottom instead of a full-screen card, so an installer can still see what the panel is doing while the public sees your artwork.",
        "'Fill screen' crops the image to cover the panel; 'Fit inside' letterboxes it so nothing is cut off. Both match the preview shown in the dashboard exactly.",
        "On-screen build bumped to 43, app-shell cache to v43."
      ]
    },
    {
      "version": "1.7.14",
      "releasedAt": "2026-08-17T07:10:59Z",
      "filename": "com.lg.app.signage_1.7.14_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.14_all.ipk",
      "size": 552788,
      "sha256": "155edaab0518b8f8e4755248cfce528ba8e6905368b46a141c9ff655094ba0cf",
      "title": "The panel now renders each item EXACTLY like the playlist editor's preview - same backdrop, same scaling, same logo geometry, same transitions. Verified pixel-for-pixel.",
      "notes": [
        "Your artwork no longer sits on the ImageSignage backsplash. The media layer was transparent, so the branded logo + blue gradient showed through anywhere the content didn't cover: the letterbox bars of a 'contain' item, and every transparent area of a PNG with an alpha channel. The editor preview shows black there. The layer is black now, and the backsplash still backs the boot / between-item gap, which is all it was ever needed for.",
        "'Contain' now scales small images UP to fit, like the preview does. The player sized contained media with max-width/max-height, which only ever scales DOWN - so anything smaller than the screen rendered at its natural pixel size, stranded in the middle of a 1080p panel, while the editor showed it filling the screen.",
        "Slide Right, Slide Up and Swipe were INVISIBLE. Those three keyframes animated position only, with no opacity, and the layer they run on sits fully transparent until the animation paints it - so the item just popped in when the animation finished. All twelve transitions are now the dashboard's own keyframes.",
        "Swipe ran backwards (in from the right, out to the left - the preview does the opposite), and Zoom and Scale inverted on exit. Glitch was a clip-path slice on the panel against the preview's RGB-split. Fixed by using one shared definition.",
        "Every transition now uses its own easing curve. The player hard-coded 'ease' for all twelve, so Bounce, Scale and Snap landed flat instead of overshooting, and Glitch lost its stepped stutter.",
        "Branding logos are placed and sized like the preview. Size was a percentage of the layer - so a '10%' logo came out 192x108 on a 16:9 screen instead of a 108x108 box fitted to the logo's own shape - corner insets were 4% of two different axes instead of 3% of the short edge, a legacy '%' radius produced elliptical corners, and px mode needed both width and height. All four now follow the dashboard's formula exactly.",
        "A logo could also be cropped by the item's fit mode: a 'cover' item forced object-fit:cover onto the logo image too. The logo now keeps its own geometry regardless of the item's fit.",
        "Backed by two new checks in the repo: scripts/webos-preview-parity.mjs drives the real player against a stub CMS and compares its stage with the dashboard's contract pixel-by-pixel (11 cases, all identical), and scripts/check-webos-transitions.mjs fails the build if a keyframe or easing value ever drifts again.",
        "On-screen build bumped to 42, app-shell cache to v42."
      ]
    },
    {
      "version": "1.7.13",
      "releasedAt": "2026-08-17T06:30:39Z",
      "filename": "com.lg.app.signage_1.7.13_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.7.13_all.ipk",
      "size": 549124,
      "sha256": "6d7164f0e8bb6859007f12f0e223c76448bcb5792c2d31ca78cbee0b512882b2",
      "title": "Runs correctly on EVERY webOS generation - webOS 4.0 through 24. Fixes layouts collapsing and widgets rendering black on older panels, and stops a new install from silently continuing to run the previous build's code.",
      "notes": [
        "Older panels no longer lose their layout. The stylesheet used the CSS `inset` shorthand in 23 places (screens, video wraps, widget wraps, L-shape and ticker overlays). `inset` is Chromium 87+, so on webOS 4.x/5.x/6.0 the browser threw those away and the affected boxes collapsed to nothing. All of them are now written as top/right/bottom/left, which every firmware understands.",
        "Widgets no longer render as a black rectangle on webOS 4.x/5.x/6.0. The offline widget bundle was compiled to ES2020, so `?.` and `??` made the whole bundle a syntax error on anything below Chromium 80 - it never executed and left the slot black with nothing in the log. The bundle is now compiled for Chromium 53 and ships shims for the eight newer built-ins it needs (Object.fromEntries/entries/values, Array.flatMap, String.padStart, globalThis, queueMicrotask, ResizeObserver). Verified by rendering all six widget engines with those built-ins removed.",
        "The Instagram widget's hashtag matcher was rewritten without Unicode property escapes (`\\p{L}`, Chromium 64+), which were a parse-time error on webOS 4.x. Arabic, CJK, Cyrillic captions and emoji still split exactly as before.",
        "Installing a new IPK now actually runs the new code. The service worker served the player's own JS/CSS cache-first and its cache key had been left at v38 while the player shipped build 40, so panels kept executing the previous release. The app shell is network-first now, so a stale cache can never mask a fresh install again.",
        "The panel reports its real version to the dashboard. The heartbeat had drifted to '1.7.5 (33)' while the package was 1.7.12 - every screen row in the CMS was showing a version that had not been installed for several releases.",
        "The green VIDEO DIAG overlay is off by default. It had been left opt-OUT since the video debugging work, so it was burnt into customer screens. Technicians still get it by tapping the top-left corner 5x within 3 seconds.",
        "Spacing, the weather card and the branded backsplash survive old firmwares too: flexbox `gap` (Chromium 84+), CSS Grid (57+) and `min()` (79+) all have fallbacks now, chosen automatically at boot.",
        "On-screen build bumped to 41, app-shell cache to v41."
      ]
    },
    {
      "version": "1.6.7",
      "releasedAt": "2026-06-22T10:08:04Z",
      "filename": "com.lg.app.signage_1.6.7_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.7_all.ipk",
      "size": 546560,
      "sha256": "846f9c0ac8ce956f94a21ca681c17a37d8a4a88e43ca9018c786b05096a5749f",
      "title": "Plays media the LG reference way — downloads to the panel via the webOS SCAP adapter, then plays each clip from the platform's own local server (http://127.0.0.1:9080). Local file + real http URL = no black, no streaming stutter, no skips.",
      "notes": [
        "Root cause of the inconsistency (black / freeze / re-loop / skip): we had two bad options and kept swapping between them. A downloaded 'filesystem:' video SEIZES the hardware plane and shows it BLACK while decoding; remote streaming has no black but stutters/freezes/skips on a slow link. The LG reference player avoids BOTH by downloading each file to the panel's internal storage and then playing it from the platform's OWN local web server at http://127.0.0.1:9080 — a LOCAL file (no streaming) served over a REAL http URL (decodes off the plane, so no black). This build copies that method exactly.",
        "Downloads now go through the same webOS API the reference uses — the SCAP adapter (luna://com.webos.service.commercial.scapadapter, fs/copyFile) — straight from the CMS URL into file://internal/media. We list what's already on disk on boot, so persistent storage survives restarts and nothing re-downloads.",
        "Playback now PREFERS the local 127.0.0.1:9080 copy for every clip. A clip that hasn't finished downloading streams remote for one loop and flips to local the instant its copy lands. On boot we hold the branded loading screen until the FIRST clip is local, so the very first frame is a smooth local file — not a stream.",
        "Added the security.management permission the SCAP adapter requires (the reference declares the same).",
        "On-screen build bumped to 26, app-shell cache to v38 (diagnostic header shows 'build 26')."
      ]
    },
    {
      "version": "1.6.6",
      "releasedAt": "2026-06-22T06:21:16Z",
      "filename": "com.lg.app.signage_1.6.6_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.6_all.ipk",
      "size": 545826,
      "sha256": "d85889f047c295d7d807b8a6802e46fefe833d127330b7532f67d3ec2d630413",
      "title": "THE black-before-video fix — videos now play from the real http URL (like the reference player), not a filesystem: URL that blanked the plane black",
      "notes": [
        "You confirmed the screen is PURE BLACK during the gap (no backsplash visible) — which means a black VIDEO plane is covering everything, not an HTML gap. The cause: since 1.6.0 we played downloaded videos from a 'filesystem:' URL, and on webOS a filesystem:/blob: video SEIZES the hardware video plane and shows it BLACK while it decodes (the documented webOS behaviour). A real http(s) URL decodes OFF the plane, so the current clip keeps masking and there is NO black — exactly how the reference player behaves (it serves over http://127.0.0.1:9080).",
        "Videos now play from the real http(s) URL (native streaming; the service worker does not proxy video). The downloaded local copy is kept only as an OFFLINE fallback. On boot it now starts immediately and streams (no waiting on downloads), with the branded loading screen covering only the first clip's buffering.",
        "If the network drops, it falls back to the local downloaded copy automatically (which plays, with the brief warm-up black that only the offline path now has).",
        "On-screen build bumped to 25, app-shell cache to v37 (diagnostic header shows 'build 25')."
      ]
    },
    {
      "version": "1.6.5",
      "releasedAt": "2026-06-22T06:01:41Z",
      "filename": "com.lg.app.signage_1.6.5_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.5_all.ipk",
      "size": 544962,
      "sha256": "83e6ee7d9ba97814a98faf263c2b2bceff68100fd89922efe3e7c9ecdb9a5866",
      "title": "The ~1s black at the start of every clip — the plane was being shown while still black; now revealed only once real frames are flowing",
      "notes": [
        "webOS shows a BLACK hardware video plane for roughly the first second after a clip starts, and that plane sits ABOVE the page (so the backsplash can't cover it). 1.6.4 revealed the clip as soon as play() resolved — i.e. while the plane was still black — which is the ~1s black you saw at the start of every video.",
        "Now the new clip is kept paused while it decodes, then played, and only made visible once its currentTime has actually ADVANCED past the first few frames (≈0.12s of real playback) — so the plane is only shown once it's painting real (non-black) frames. The outgoing clip keeps masking until then.",
        "This also skips any black 'first frame' from the local file source, and the last-resort timer no longer force-shows a clip that isn't actually playing.",
        "On-screen build bumped to 24, app-shell cache to v36 (diagnostic header shows 'build 24')."
      ]
    },
    {
      "version": "1.6.4",
      "releasedAt": "2026-06-19T09:06:52Z",
      "filename": "com.lg.app.signage_1.6.4_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.4_all.ipk",
      "size": 544130,
      "sha256": "1c2ca0f367e51ab5152517b9a60eea8d4b5bc574e5e05aafea2739ca10ebf356",
      "title": "Black-before-video eliminated by copying the LG reference player exactly — branded backsplash + settle-then-play timing",
      "notes": [
        "The reference player is never pure black because its background is a loading image / backsplash; ours painted the stage and layers solid black, so every decode warm-up showed through as black. The stage now shows a branded backdrop (logo on the brand gradient) behind the video, and the video layers are transparent — so any warm-up or transition gap shows that backdrop, never black (a playing clip covers it).",
        "Reveal timing now matches the reference precisely: the next clip is loaded HIDDEN, given a short settle so it is FULLY decoded while paused, then played and revealed the instant playback starts — so it pops in off an already-decoded frame with no black plane flash.",
        "The outgoing clip is now held on screen well past the new clip's reveal (a generous overlap, like the reference's multi-second hold) so the old plane is never blanked before the new one is solid — no black seam at the hand-off.",
        "On-screen build bumped to 23, app-shell cache to v35 (diagnostic header shows 'build 23')."
      ]
    },
    {
      "version": "1.6.3",
      "releasedAt": "2026-06-18T13:15:07Z",
      "filename": "com.lg.app.signage_1.6.3_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.3_all.ipk",
      "size": 543126,
      "sha256": "ef4295bf1d58962218e242a8db42f833408cb160f0ad168a144af689cbf97bcd",
      "title": "No more ~1-minute black screen at boot — a branded loading screen now covers the cold-start warm-up until the first frame paints",
      "notes": [
        "The boot black was two things: the player blocked on 'Preparing' for up to a minute waiting for downloads, and even after starting, the loading screen was hidden BEFORE the first video painted — so the decode warm-up showed the black stage. Now the player starts within seconds (as soon as the first clip is local, or a short cap) and KEEPS a branded 'Loading your content' screen up until the first frame actually paints. A hidden video's hardware plane is off, so the loading screen shows through — the screen is never black at boot.",
        "Works for video, image and widget first items, with a safety timeout so the loading screen can never stick.",
        "The boot warm-up net no longer force-reveals a clip that has no frame yet (which would flash black) — it keeps the loading screen up and reveals only on a real frame; the rotation still advances on its timer so nothing wedges.",
        "On-screen build bumped to 22, app-shell cache to v34 (diagnostic header shows 'build 22')."
      ]
    },
    {
      "version": "1.6.2",
      "releasedAt": "2026-06-17T06:56:47Z",
      "filename": "com.lg.app.signage_1.6.2_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.2_all.ipk",
      "size": 542394,
      "sha256": "d7c16452c3df1b6bbe1c87bd1489e0632df203ed5076ff51d9689ba86781567f",
      "title": "Fixes 'stuck on Preparing / 0 of 12' — downloads no longer time out on a slow link, and the screen starts playing without waiting for everything",
      "notes": [
        "Root cause of the stuck 'Downloading 0/12': each video download had a fixed 2-minute total timeout, so a large clip that was downloading fine but slowly (weak wifi) got killed and retried forever, never finishing. Replaced it with a STALL watchdog — a download is only aborted if NO data arrives for 75s, so a big file on a slow link keeps going to completion as long as it's making progress.",
        "Downloads now run ONE at a time so each file gets the full bandwidth (finishes fastest) and only one clip is held in memory at once on the 1 GB panel.",
        "The screen no longer blocks for up to 10 minutes on 'Preparing content'. It now starts playing as soon as the first clip is downloaded (and after ~12s), or after a 60s cap at the latest — the remaining clips stream and finish downloading in the background, upgrading to smooth local playback loop by loop.",
        "On-screen build bumped to 21, app-shell cache to v33 (diagnostic header shows 'build 21')."
      ]
    },
    {
      "version": "1.6.1",
      "releasedAt": "2026-06-15T08:01:25Z",
      "filename": "com.lg.app.signage_1.6.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.1_all.ipk",
      "size": 541476,
      "sha256": "a32827a6953319f5a2591eca024140b73a471d7140a24823d0995a54d343e7db",
      "title": "Fixes the black flash when moving to the next video — the new clip no longer steals the screen during its warm-up",
      "notes": [
        "Now that videos play locally (1.6.0), the long-standing black-at-transition was exposed. Root cause: webOS Signage has effectively ONE hardware video plane, and we were calling play() on the next (hidden) clip immediately — which made it seize that plane during its decode warm-up and BLANK the still-playing current clip to black. This is exactly the 'black when moving to the next video file'.",
        "Fix (the LG reference player's exact behaviour): the next clip is now loaded but NOT played until its first frame is decoded (loadeddata). The current clip keeps the plane and stays on screen the whole warm-up, then we play + reveal the new clip instantly off its already-decoded frame — so the hand-off has no black gap. Cold start (the very first clip after boot) still shows a brief warm-up, as before.",
        "Kept: frame-accurate reveal, decoder release (src=\"\") so every clip in the loop gets a decoder, and the remote fallback if a clip ever fails to play locally.",
        "On-screen build bumped to 20, app-shell cache to v32 (diagnostic header shows 'build 20')."
      ]
    },
    {
      "version": "1.6.0",
      "releasedAt": "2026-06-12T19:27:58Z",
      "filename": "com.lg.app.signage_1.6.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.6.0_all.ipk",
      "size": 540528,
      "sha256": "6bcf372c1b5ef7406e79e4b35575d893cc68b46d7919804b9a2b3322e37e44b9",
      "title": "Local video now plays the LG reference way — from the WebView FileSystem, no localhost server (fixes 404 / unreachable for good)",
      "notes": [
        "Re-architected local playback to match LG's reference signage player exactly: instead of our own localhost HTTP media server (port 9088), each video is downloaded as a blob into the WebView's PERSISTENT FileSystem (webkitRequestFileSystem) and played via fileEntry.toURL() — a 'filesystem:' URL the panel decodes IN-PROCESS. This removes the whole class of failures we kept hitting (the local server returning 404, or the WebView being unable to reach 127.0.0.1) because there is no separate server or container boundary anymore.",
        "Downloads happen in the player itself (blob via XHR) straight into persistent storage, so they survive reboots and play offline. A bad cached file is detected on play, dropped and re-downloaded automatically.",
        "If a clip isn't downloaded yet (or the FileSystem API is unavailable on an old firmware) it streams from the network as before, then upgrades to local on the next loop. Decoder release (src=\"\") and frame-based reveal from the previous build are kept.",
        "The old 9088 service is no longer used for playback. Diagnostic header now shows 'media-fs: OPEN' and the FileSystem path. On-screen build bumped to 19, app-shell cache to v31."
      ]
    },
    {
      "version": "1.5.6",
      "releasedAt": "2026-06-12T18:43:50Z",
      "filename": "com.lg.app.signage_1.5.6_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.6_all.ipk",
      "size": 538866,
      "sha256": "c534fe6837991c5e549073b4bde8bdaf606a4ea456f8f720251bd684fff33c9e",
      "title": "Fixes the cascade of bogus 'fallback to remote' + 'interrupted by pause()' and black local frames from 1.5.5",
      "notes": [
        "1.5.5 revealed a clip as soon as play() resolved and used a short blanket timer. On these panels play() settles late, so clips revealed on the timer; then revealing the next clip paused the previous one, whose still-pending play() rejected with 'interrupted by a call to pause()'. That rejection then ran the retry/remote-fallback logic on a clip that had ALREADY rotated out — producing the cascade of bogus 'FALLBACK remote' lines. Fixed: playback now reveals on a REAL decoded frame (the only trustworthy 'it's playing' signal), the decoder is still released with src=\"\", and a rejection on an already-torn-down clip is ignored (so is the harmless pause abort).",
        "A stalled local clip (no frame and no error within 5s) now switches cleanly to the remote URL instead of revealing a black 'local' video that never loaded.",
        "Self-heal for 'downloaded 12/12 but the file 404s': when the local server is reachable but reports the file isn't on disk (its cache folder moved/rebuilt), the player now drops that file from its local set, re-syncs from disk and re-queues the download — instead of trusting a stale 'ready' list forever.",
        "On-screen build number bumped to 18 and app-shell cache to v30 (diagnostic header shows 'build 18')."
      ]
    },
    {
      "version": "1.5.5",
      "releasedAt": "2026-06-11T07:37:15Z",
      "filename": "com.lg.app.signage_1.5.5_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.5_all.ipk",
      "size": 537722,
      "sha256": "d0f083d58eaf73f76e25b418bafaba468b9dd0215bb527269cae72cf1ddc8d14",
      "title": "FIX: 'only 2 of 12 videos play, the rest skip' — videos now release their decoder the way the reference player does",
      "notes": [
        "Root cause: webOS panels allow only about one or two hardware video decoders at a time. When a clip rotated out we cleared it with removeAttribute('src'), which on webOS does NOT free the decoder — so after the first couple of clips the decoders were exhausted and every later clip failed with 'no supported source' and was skipped. The fix matches LG's reference player exactly: the outgoing video's source is set to empty string (src = '') on handoff, which actually releases the decoder, so all clips in the playlist get one and play in turn.",
        "Reveal timing now matches the reference too: a clip is shown the instant playback starts (not after waiting for a decoded frame that never arrives when decoders are scarce), and the previous clip keeps masking briefly so there's no black flash at the hand-off.",
        "Removed a 30s idle-socket reaper added in 1.5.4 that could cut a still-needed local connection.",
        "On-screen build number bumped to 17 and app-shell cache to v29 so you can confirm the fix is live (diagnostic header shows 'build 17')."
      ]
    },
    {
      "version": "1.5.4",
      "releasedAt": "2026-06-10T16:03:39Z",
      "filename": "com.lg.app.signage_1.5.4_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.4_all.ipk",
      "size": 537038,
      "sha256": "03bd34f98853a9ebb3844405a940f7033d95df80e0ac836f7207114fbdc87867",
      "title": "THE 'works ~10 min then goes black forever' fix — local media server no longer leaks file handles",
      "notes": [
        "Root cause of 'plays well for ~10 minutes then turns black and never recovers': the on-device media server served video over localhost but did NOT release a file handle when the TV's video element aborted a byte-range request — and a looping <video> aborts those constantly. The open handles piled up until the panel hit its file-descriptor limit, after which EVERY local read failed with 'no supported source' and playback went black until the app was restarted. The server now reliably closes each file stream the instant the client disconnects, errors, or finishes, so handles never accumulate.",
        "Idle localhost connections are also reaped after 30s so sockets can't slowly pile up over a long run.",
        "Carries forward 1.5.3's safety net: if a local clip ever still fails, the player retries local then falls back to the remote URL (so the screen keeps playing, never black) and logs an on-screen LOCAL-PROBE line showing whether the localhost server was reachable — so any remaining issue is diagnosable at a glance.",
        "On-screen build number bumped to 16 and app-shell cache to v28 so you can confirm the fix is live (diagnostic header shows 'build 16')."
      ]
    },
    {
      "version": "1.5.3",
      "releasedAt": "2026-06-10T15:19:52Z",
      "filename": "com.lg.app.signage_1.5.3_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.3_all.ipk",
      "size": 535528,
      "sha256": "156dda3c79e6338a7a63cb3f915c464313d9375ec86eef34c82dcc34b85f93b8",
      "title": "Files download & are recognized local, but local playback was failing 'no supported source' — now falls back to remote so the screen plays, and reports WHY local failed",
      "notes": [
        "Diagnostic finding: all videos download and are stored on persistent storage and the player correctly picks the LOCAL file, but every clip then fails with 'Failed to load because no supported source was found' (code 4) — i.e. the panel's video element won't play the localhost (127.0.0.1) file. The screen went black because the player never fell back to the network on a local error.",
        "Playback now ESCALATES instead of going black: local → retry local once → fall back to the REMOTE url so the screen keeps playing. On panels where local works this never triggers; on this panel it keeps content on screen while we fix local playback.",
        "Added an on-screen LOCAL-PROBE: when a local clip errors, the player fetches the local file directly and logs the result — 'http 206 ct=video/mp4 bytes=…' means the localhost server IS reachable and serving (so the cached file/codec is the issue), while 'UNREACHABLE …' means the WebView cannot reach the media server's HTTP port. This pinpoints the exact cause.",
        "On-screen build number bumped to 15 and app-shell cache to v27 so you can confirm the new code is live (diagnostic header shows 'build 15')."
      ]
    },
    {
      "version": "1.5.2",
      "releasedAt": "2026-06-10T14:04:13Z",
      "filename": "com.lg.app.signage_1.5.2_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.2_all.ipk",
      "size": 534688,
      "sha256": "3733334b556173cd3835850524360f488bfe5667b4105a3b68d0ce3c9b06e124",
      "title": "FIX: 'always downloading / always remote' — the player now finds a storage folder that REALLY works and stays local after download",
      "notes": [
        "Root cause of 'downloaded all but every clip still streams and re-downloads each loop': the chosen storage folder passed a tiny 1-byte writability test but SILENTLY rejected real video files (a read-mostly / quota'd / special mount), so every download reported 'not on disk', the player streamed from the network, and the service re-downloaded forever. The folder check now writes, reads back and verifies a real 256 KB file before trusting a folder, AND every finished download is verified to actually be readable on disk afterwards.",
        "Self-healing storage: if downloads keep failing on the selected folder, the service automatically falls back to the next folder that genuinely works (ending at /tmp, which always works for the session) and moves whatever already downloaded with it — so playback becomes LOCAL instead of streaming endlessly. The diagnostic's 'dir:' line shows the folder it settled on.",
        "The player now keeps a downloaded clip marked LOCAL once it's confirmed on disk and will NOT flip it back to streaming on a momentary status hiccup — so after the first download it plays from local storage and stays there until the content actually changes in the CMS.",
        "App-shell cache bumped to v26 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.5.1",
      "releasedAt": "2026-06-10T09:57:19Z",
      "filename": "com.lg.app.signage_1.5.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.1_all.ipk",
      "size": 532188,
      "sha256": "746ef5429de77e77e3d1d70acc3dd1f77f403ae8c1f8b0e1a16b95cc1a6e3d5e",
      "title": "Goes straight to persistent storage at boot — no /tmp detour (default build, for panels WITH write access at start)",
      "notes": [
        "The media service now waits (briefly, up to 10s, polling every 0.5s) for persistent storage to be writable at startup before settling on a folder. On a panel that has write access at boot the first probe succeeds instantly, so downloads land directly in permanent storage from the very first byte — there is no /tmp detour, no folder switch, and therefore no migration and no re-download, ever.",
        "Includes everything from 1.5.0 (cache migration + fast reprobe) as a safety net: if storage is genuinely not ready within the startup window it still falls back to /tmp and recovers by migrating once it becomes available.",
        "This is the recommended default. Use 1.5.0 instead only on panels that do NOT have write access to persistent storage at boot.",
        "App-shell cache bumped to v25 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.5.0",
      "releasedAt": "2026-06-10T09:56:10Z",
      "filename": "com.lg.app.signage_1.5.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.5.0_all.ipk",
      "size": 530850,
      "sha256": "f6718419b577349d565b8c4e5d274166881446f499e7c2625549f97cce94dd62",
      "title": "No more re-download/black after the first loop — cache survives the /tmp→persistent storage switch (build for devices WITHOUT write access at boot)",
      "notes": [
        "FIX: on a device whose persistent storage is not writable at the instant the media service starts, downloads landed in /tmp; when persistent storage later became writable the service switched folders but ORPHANED the already-downloaded files — so right after the first loop every clip re-downloaded and streamed (black on weak wifi) until it landed again. The service now MIGRATES the downloaded files into the persistent folder BEFORE switching, so nothing is lost, nothing re-downloads, and playback stays local across every loop.",
        "The upgrade-to-persistent check now runs every 5s (was 30s), so a device that starts on /tmp reaches permanent storage quickly — your downloads survive the nightly power-off sooner.",
        "Use this build for panels that do NOT have write access to persistent storage at boot (storage mounts a few seconds after the app starts). The diagnostic's 'dir:' line will briefly show [TEMP!] then flip to [PERSISTENT] once migration completes.",
        "App-shell cache bumped to v24 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.9",
      "releasedAt": "2026-06-10T09:31:09Z",
      "filename": "com.lg.app.signage_1.4.9_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.9_all.ipk",
      "size": 530396,
      "sha256": "c7575285b944247ddc062f6a7952a1552552afca9914965c60209332bb5ba41c",
      "title": "Download-first (no black streaming) + verified, parallel downloads — fixes 'black even after download'",
      "notes": [
        "Likely cause of 'downloaded but still black': on weak wifi a download could drop mid-file and a TRUNCATED .mp4 was saved as 'done' — a partial mp4 plays black. The service now checks the downloaded size against the server's Content-Length (and rejects suspiciously tiny files), so a truncated/partial download is discarded and retried instead of saved. Only fully-intact files are served.",
        "webOS cannot reliably stream from the CMS (it shows black — that's the whole reason we download), so the player no longer streams during the first download. Instead it shows a 'Preparing content' progress screen ('Downloading videos to this screen · N of M ready') and starts playing once the videos are LOCAL. Because storage is now persistent, this wait happens only ONCE — every later boot starts instantly from local files.",
        "Downloads now run 3 at a time (streamed to disk, low memory) so the first-time download finishes much faster, and a stalled connection is aborted after 60s and retried instead of hanging for minutes.",
        "App-shell cache bumped to v23 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.8",
      "releasedAt": "2026-06-10T08:04:51Z",
      "filename": "com.lg.app.signage_1.4.8_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.8_all.ipk",
      "size": 527136,
      "sha256": "541fc57c74ff483efe2e73eba84552a9c80cb36bdb3124e96cfd37d3daba1e7b",
      "title": "Downloads now persist across reboot (was using /tmp, wiped nightly) + clean stream-while-downloading",
      "notes": [
        "THE real cause of 'worked yesterday, black today': the downloaded videos were being stored in /tmp, which the TV ERASES on every power-off/reboot — so each morning the device had 0 local files and fell back to streaming (black on weak wifi). The media server now stores downloads in a PERSISTENT location and only uses /tmp as an absolute last resort; if no persistent folder is writable at boot it keeps retrying so a later download still lands somewhere permanent. It also tries several known LG-Signage storage paths and reports which it used.",
        "The diagnostic panel now shows the storage folder and whether it is PERSISTENT or TEMP (lost on reboot), plus, if temp, exactly which folders it tried and why each failed — so storage problems are obvious at a glance.",
        "Playback now picks its source ONCE, up front: a downloaded clip plays from the local file; a clip still downloading streams from the network during that one-time window and plays locally on the next loop. It no longer sets the local source then swaps to the network on error, which webOS was rejecting with 'no supported source' (a cause of black during the download window).",
        "App-shell cache bumped to v22 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.7",
      "releasedAt": "2026-06-10T07:22:41Z",
      "filename": "com.lg.app.signage_1.4.7_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.7_all.ipk",
      "size": 525798,
      "sha256": "b80dbf16d608d8fb0863cc21ee9941fb2191811552bb5c1aa6062e4e12c03923",
      "title": "Download diagnostics — surface WHY videos aren't downloading to the device",
      "notes": [
        "The diagnostic clearly showed the media server is reachable (UP) but downloads 0 of 11 videos, so every clip falls back to streaming and goes black on weak/no wifi. The on-device media server runs its own limited network stack (separate from the player's), and its downloads are failing silently. This build makes the service REPORT its download state — attempts, completed, in-flight, and the exact last error (DNS / TLS / HTTP code / timeout) — and shows it live on the diagnostic panel (second status line: 'dl[q:.. busy:.. done:.. try:..]' and 'dlERR: ..').",
        "Added a browser-style User-Agent + Accept header to the service's downloads in case the CDN was rejecting header-less requests.",
        "App-shell cache bumped to v21 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.6",
      "releasedAt": "2026-06-09T14:24:26Z",
      "filename": "com.lg.app.signage_1.4.6_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.6_all.ipk",
      "size": 524540,
      "sha256": "cc7e93c2ccf13bb40ea47a0de8655dcc93c59aa93b2b4ace5c534ac25c0a4c60",
      "title": "FIX: main-playlist videos now stay downloaded & play locally (no more streaming/black on weak wifi)",
      "notes": [
        "Root-cause fix for the black screens on wifi: the routine that tells the on-device media server which videos to keep was reading the wrong field, so it only ever listed SCHEDULED videos — never the main playlist's. Because the media server deletes any file not on the list it's given, it pruned the main playlist videos right after downloading them, and the player fell back to STREAMING every clip from the internet (hence the black/stutter on weak wifi). It now reads the correct list, and both download paths use the exact same list so they can't delete each other's files.",
        "After a clip is downloaded the player now NEVER streams it from the network — even on a transient hiccup it retries the local file instead. The internet is only used to (a) play a clip that hasn't finished downloading yet and (b) detect playlist changes. Once everything is local, playback is fully offline-capable and unaffected by bad wifi.",
        "The on-screen video diagnostic is now shown by default on this test build (tap the top-left corner 5× to hide). It now also shows a live status line: media-server UP/down and how many of the current playlist's videos are local (e.g. 'videos local: 11/11'), so it's obvious at a glance whether playback is local or streaming.",
        "App-shell cache bumped to v20 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.5",
      "releasedAt": "2026-06-09T14:11:06Z",
      "filename": "com.lg.app.signage_1.4.5_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.5_all.ipk",
      "size": 523378,
      "sha256": "ac73dc17e8cd24832dcd4bdf1cbe4f7d239e40c1dcde91e0ff9eadb0dc2b950f",
      "title": "Video hand-off black-flash fix + opt-in video diagnostic",
      "notes": [
        "Fixed a small black flash at video transitions: the outgoing video is now DETACHED from the page before its source is cleared. Clearing a video's source blanks the TV's video plane to black, and that plane draws above the page — so doing it before removing the element could flash black over the next clip. Removing the element first releases the plane cleanly, so the blank never reaches the screen.",
        "Added an opt-in on-screen video diagnostic to pinpoint any remaining black: it is HIDDEN by default (never shown on customer screens) and is toggled by tapping/clicking the TOP-LEFT corner 5 times within 3 seconds. When on, it shows a timestamped log of each clip, its source (LOCAL vs streaming), and any play/error/reveal events — so the exact cause of a black moment can be read straight off the panel. Tap 5 times again to hide.",
        "App-shell cache bumped to v19 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.4",
      "releasedAt": "2026-06-09T13:03:35Z",
      "filename": "com.lg.app.signage_1.4.4_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.4_all.ipk",
      "size": 521898,
      "sha256": "29728a3c77d6e867ff8050dfd3e70a9e48a951faf7c3412f7760bf5654bff464",
      "title": "L-shape squeeze now scales the whole frame (matches the dashboard preview & Android)",
      "notes": [
        "Rebuilt the L-shape squeeze so it scales the ENTIRE content frame uniformly into the corner — exactly like the dashboard preview and the Android player — using a smooth GPU transform. Previously it resized the content box, which re-fitted (re-letterboxed) the content frame-by-frame as the box's shape changed, so the squeeze-IN looked janky even though the squeeze-OUT was fine. Both directions now glide identically with the same 600ms broadcast ease.",
        "The backdrop image still fades in on the same curve, so the image and the squeeze move in lockstep."
      ]
    },
    {
      "version": "1.4.3",
      "releasedAt": "2026-06-09T12:13:42Z",
      "filename": "com.lg.app.signage_1.4.3_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.3_all.ipk",
      "size": 520848,
      "sha256": "4d4da30df6af351c12ce467f72400fc5331e7b69902a6b032ebd4df65111e9a0",
      "title": "Idle after download (only re-downloads on real changes) + smooth L-shape squeeze",
      "notes": [
        "The player now stays completely idle once everything is downloaded and only re-applies/re-downloads when the content actually changes on the dashboard (you publish/edit a playlist or add/change a schedule). Each 30s poll compares a stable signature of the playlist + schedules + L-shape; if nothing changed it keeps playing untouched — no rotation restart, no re-processing — which removes the occasional black blip and makes playback as steady and consistent as the Android player.",
        "When something DOES change, only the new/changed media is downloaded (files already on the device are never re-fetched) and the clip currently on screen is not interrupted — the rotation only restarts if the item list itself genuinely changed.",
        "Removed a brief black flash that could appear when a video handed off to the next item: the outgoing video is no longer paused before removal (a paused video blanks the TV's video plane to black); it's now removed directly, which clears the plane cleanly.",
        "Fixed the L-shape transition: the content now squeezes INTO the corner with the same smooth 600ms broadcast-style ease it already used coming back out. Previously the squeeze-in snapped instantly while only the squeeze-out animated — because the stage's size was 'auto' (which CSS can't animate) at the start of the move. The player now pins an explicit start size and animates from it, so both directions glide identically.",
        "The L-shape backdrop image now fades in on the exact same 600ms curve as the squeeze, so the image and the corner move in lockstep; switching directly between two L-shape layouts eases between them instead of bouncing through full-screen first.",
        "App-shell cache bumped to v17 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.2",
      "releasedAt": "2026-06-09T08:36:17Z",
      "filename": "com.lg.app.signage_1.4.2_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.2_all.ipk",
      "size": 519548,
      "sha256": "61f1fb0e1622dae3595a558a6fd6a64b66697ea865cd691ba9b537db1b566c21",
      "title": "Fix clips turning black at transitions (regression from 1.4.1)",
      "notes": [
        "Fixed a regression where the screen would turn black at clip changes even after everything was downloaded. When an outgoing video was being removed, clearing its source fired an internal error that the new offline-fallback logic misread as 'local file missing' and re-loaded that finished clip from the remote server — which composited a stale (and, offline, black) video plane over the new content. Outgoing clips are now marked as retired before teardown so this can never happen.",
        "The offline fallback now only triggers for a genuinely missing local file on the clip that's actually starting, never during cleanup of the previous clip.",
        "App-shell cache bumped to v16 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.1",
      "releasedAt": "2026-06-09T07:30:14Z",
      "filename": "com.lg.app.signage_1.4.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.1_all.ipk",
      "size": 518324,
      "sha256": "3cb875d4ce4c168d288f597a6742cedfea3e44de7336df6e75613569c295e202",
      "title": "Reliable offline playback; play instantly with a professional download card; diagnostics overlay removed",
      "notes": [
        "Fixed offline playback: once everything is downloaded, the player now plays its local files reliably with no internet — no more intermittent black clips. The list of on-device files is remembered between restarts and applied instantly, so the very first clip after a (possibly offline) reboot plays from the local copy instead of briefly trying the unreachable server and going black. Videos always play from the local file first; only if a file isn't on the device yet does it stream (and only when online).",
        "The player now starts playing the moment it's paired/synced instead of waiting on a full-screen download screen. Media downloads to the TV in the background and each file silently upgrades to smooth local playback as it lands. During this first window a clip may stream (and can briefly stutter) — that's expected and far nicer than staring at a download screen.",
        "Added a polished progress card in the bottom-left corner shown only while content is downloading: brand logo, version, files downloaded (e.g. 3 / 12 files), percentage, a progress bar, and an estimated time remaining. It eases in, updates live, and disappears automatically the instant everything is local.",
        "Removed the green diagnostics/test overlay entirely — screens now show only the content and, when relevant, the download card.",
        "App-shell cache bumped to v15 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.4.0",
      "releasedAt": "2026-06-08T15:09:06Z",
      "filename": "com.lg.app.signage_1.4.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.4.0_all.ipk",
      "size": 515776,
      "sha256": "dd81dc70c1281443c4ccbd2ec0a305acc44e619a8d71e153e46b27a6642fd849",
      "title": "Download-everything-before-play boot (like Android), smooth L-shape squeeze, fewer black frames",
      "notes": [
        "After pairing/boot the player now downloads ALL content first — images AND videos (videos to the device's local store) — shows a single progress bar with a video count, and only starts playing once everything is local. Nothing streams at play time, which removes the start/mid-clip freezes, stutter, and most black frames.",
        "If the local media service is unavailable, or a file just won't download within a generous window, playback still starts (that clip streams as a fallback) so the screen never gets stuck on the download screen.",
        "Videos download one at a time so a 1 GB panel isn't overwhelmed by many large concurrent downloads; failed downloads are retried automatically during the boot wait.",
        "L-shape schedules now animate professionally: the content smoothly squeezes into the corner and the L-shape image fades in (and reverses on exit), matching the Android player, instead of snapping.",
        "L-shape backdrop images are now downloaded up-front (at boot, alongside the rest of the content) and pre-decoded as soon as the player learns of a schedule — including a schedule you just created — so the L area no longer shows black for a moment when the schedule starts. Activation now reveals the squeeze and the image together, and only once the image is ready.",
        "App-shell cache bumped to v14 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.3.2",
      "releasedAt": "2026-06-08T10:14:28Z",
      "filename": "com.lg.app.signage_1.3.2_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.3.2_all.ipk",
      "size": 511678,
      "sha256": "5d8a75eb38eb61ec2635757fe3a50227a908c16cca2d97d32c4bbf39135b61c4",
      "title": "Transitions on videos, L-shape fix (main content now shows), local-video polish",
      "notes": [
        "Videos now honour the item's transition (fade / slide / scale / etc.) on entry, like images do — previously a clip involving video cut in instantly with no animation. The transition is masked by the outgoing clip so it stays black-free, and on panels whose video hardware ignores CSS it cleanly falls back to an instant cut.",
        "Fixed L-shape schedules: the screen was showing ONLY the L-shape image and hiding the playlist. The full-screen L-shape backdrop was stacking above the content; the shrunk content now sits on top of the backdrop, so you see your playlist in the corner with the L-shape image filling the rest, as intended.",
        "The local media cache now prunes itself: files from old playlists are removed so storage doesn't grow over time (only the current playlist's videos are kept).",
        "Diagnostics overlay now reports how many of the CURRENT playlist's videos are local-ready (e.g. 11/11) instead of the whole accumulated cache, and only shows an error string when the media server is actually down."
      ]
    },
    {
      "version": "1.3.1",
      "releasedAt": "2026-06-08T06:54:32Z",
      "filename": "com.lg.app.signage_1.3.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.3.1_all.ipk",
      "size": 508772,
      "sha256": "b137444d01e36e88344428c4b563e37edd8db1a4a8c2720fe871e275b75e549e",
      "title": "Local media server now reachable on the panel (fixes 'media-svc: DOWN')",
      "notes": [
        "1.3.0 couldn't reach the bundled media server because the app called it through window.webOS.service, which isn't present on the panel. It now calls the local service via the low-level PalmServiceBridge that webOS injects into the app — so videos can actually be downloaded and played from the local server.",
        "The on-screen overlay now shows build 3 / v1.3.1 so you can confirm the new code is running after a clean reinstall; watch 'media-svc' flip to 'up :9088' and 'videos local-ready' climb."
      ]
    },
    {
      "version": "1.3.0",
      "releasedAt": "2026-06-06T11:56:44Z",
      "filename": "com.lg.app.signage_1.3.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.3.0_all.ipk",
      "size": 507824,
      "sha256": "b99e2499f065b2155e76e9b1f26feca5a29da1d7c1558b31bbc2f4b7338b561c",
      "title": "Smooth video — media now downloads to the TV and plays from a local server (no streaming freeze/stutter/white-screen)",
      "notes": [
        "Adds a bundled local media server (a webOS JS service): playlist videos are downloaded to the TV's storage once, then played from http://127.0.0.1 — the native pipeline plays a LOCAL file, exactly like LG's reference player. This removes the start-up freeze, mid-clip stutter, white screen at loop end, and inconsistent/missing playback that came from streaming each clip over the network.",
        "Until a video has finished downloading it falls back to streaming the remote URL, so playback is never blocked — and converges to all-local within a loop or two.",
        "Videos are downloaded ignoring TLS-cert quirks the panel's certificate store can reject, then served locally over plain http, so cert/CORS issues can no longer break playback.",
        "Added a small on-screen diagnostics overlay (bottom-left) showing build, media-server status, how many videos are local-ready, and whether the current clip is playing LOCAL or streaming — to confirm the fix on the panel.",
        "App-shell cache bumped to v11 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.2.3",
      "releasedAt": "2026-06-05T10:31:52Z",
      "filename": "com.lg.app.signage_1.2.3_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.2.3_all.ipk",
      "size": 502400,
      "sha256": "1bdc834acb759eb9a50412b0aededaf81a23fa6068adeda6b3d4bc136859dd1e",
      "title": "Video black-screen — real root cause fixed: the player no longer routes video through the service worker or blob URLs",
      "notes": [
        "Root cause found: two things made webOS render video black. (1) The service worker was intercepting video requests — webOS <video> rejects a service-worker-provided response (MEDIA_ERR_SRC_NOT_SUPPORTED), so it loaded zero bytes and stayed black. (2) Cached videos were played from a blob: URL, and webOS WebKit renders a black first frame from blob: video sources.",
        "Fix: video requests now bypass the service worker entirely and stream natively from the CMS, and the <video> always uses the real http(s) URL (never a blob).",
        "Videos are no longer pre-downloaded/cached — they stream on demand (also avoids out-of-memory on 1 GB panels from caching many large clips).",
        "Combined with the earlier transition fix (outgoing clip kept playing as the mask, every clip loops, advance on a timer), playback should cut cleanly with no black before video.",
        "App-shell cache bumped to v10 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.2.2",
      "releasedAt": "2026-06-05T09:01:35Z",
      "filename": "com.lg.app.signage_1.2.2_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.2.2_all.ipk",
      "size": 500068,
      "sha256": "f473fab863afa062df1ee099d7811d5895c497121dc57e662210796119ab76a6",
      "title": "Video black-screen fix, scheduler weekday fix, and auto-reload after updates",
      "notes": [
        "Video: fixed the black screen before a clip. The outgoing video was being PAUSED the instant the next clip started — and on webOS a paused/ended <video> blanks its hardware plane to black, so that black was the mask during the new clip's warm-up. The outgoing clip is now kept PLAYING (muted) as the mask and only removed once the new clip's first real frame is on screen.",
        "Video: every clip now loops and the rotation advances on a duration timer instead of the 'ended' event — an ended clip goes black, so we never let one end between items.",
        "Update reliability: the app now reloads itself when a new version's service worker takes over, and actively checks for updates. Previously a new IPK could keep running the PREVIOUSLY cached player code, so fixes appeared not to take effect. (For THIS update, uninstall + reinstall once to clear the old cache.)",
        "Scheduler: a schedule limited to certain weekdays (e.g. Mondays only) no longer plays on other days. Chosen weekdays are now authoritative regardless of the schedule's mode, matching the dashboard.",
        "App-shell cache bumped to v9 so devices pick up the new code on next launch."
      ]
    },
    {
      "version": "1.2.1",
      "releasedAt": "2026-06-05T08:08:36Z",
      "filename": "com.lg.app.signage_1.2.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.2.1_all.ipk",
      "size": 499288,
      "sha256": "82f9f97baeadf3caaa7fc385b7987c4636884c1057622f31fbac12b3e9a1de30",
      "title": "Video: no more first-frame flash — clips decode live and reveal on their first real frame; first-frame poster images are gone",
      "notes": [
        "Removed the first-frame poster image entirely. The poster→video hand-off was itself causing a visible flash at the start of every clip; the outgoing item now masks the brief decode instead, so the cut to video is clean.",
        "First-frame and last-frame snapshot images are no longer downloaded at all — less bandwidth and storage on the device, and nothing to flash.",
        "A video is now revealed only when a REAL decoded frame has painted (requestVideoFrameCallback, or the first genuine timeupdate past the start point), never on a fixed timer guess — so the hardware video plane is never shown while still black.",
        "App-shell cache bumped to v8 so devices pick up the new player code on next launch."
      ]
    },
    {
      "version": "1.2.0",
      "releasedAt": "2026-06-05T07:23:25Z",
      "filename": "com.lg.app.signage_1.2.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.2.0_all.ipk",
      "size": 498942,
      "sha256": "833183efb113d6e284e4393359884fb3dbe4e8edc2dd58e0caf4f439343036ea",
      "title": "Database-driven widgets — widgets now render from a bundled engine, identical to the Android player, and work fully offline",
      "notes": [
        "Widgets are now controlled entirely from the CMS database (the widget_definitions table): each widget carries a renderer engine, its UI/branding config, a data source, and an is_ok readiness gate. New widgets that reuse an existing engine can be added from the dashboard with no player update.",
        "Widgets render through the SAME bundled renderer the Android player uses, so the on-screen result is byte-identical across platforms. The old webOS-only news/weather renderer has been retired.",
        "The renderer is packaged inside the app and reads cached feed data, so widgets keep showing their last-known headlines/forecast even with no network. Widget data now persists across restarts (offline parity with the Android player).",
        "A widget that isn't marked ready (is_ok = false), has no data yet, or needs a newer player build now shows a clean 'This widget will be available soon' card instead of a blank or broken frame.",
        "App-shell cache bumped to v7 (now includes the widget bundle) so devices pick up the new player code and renderer on next launch."
      ]
    },
    {
      "version": "1.1.1",
      "releasedAt": "2026-06-03T14:04:44Z",
      "filename": "com.lg.app.signage_1.1.1_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.1.1_all.ipk",
      "size": 352760,
      "sha256": "2e47084e0bfb5017d3990ed992ac2441d017c90e7d20c4d86cbcc20acdea9f2e",
      "title": "Fixes the player getting stuck forever on \"N items · preparing…\" — preload can no longer wedge the boot",
      "notes": [
        "Root cause: on a fresh device the rotation only started AFTER the whole content preload finished, and not a single preload fetch had a timeout. One stalled connection (slow CMS serving dozens of large files, a captive portal, flaky Wi-Fi, or memory pressure on a 1 GB box) left a fetch pending forever, so the download pass never settled and the screen sat on 'preparing…' with zero progress.",
        "Every preload fetch (stills, frames, logos, videos, widget JSON, widget images) now has a hard per-file timeout (20 s images, 90 s videos, 15 s widget data) via AbortController, with a Promise.race fallback for older firmwares. A stalled file now fails fast, counts as done, and the bar keeps moving instead of freezing.",
        "New boot watchdog: playback now starts within 25 s no matter what. Whatever finished downloading plays from cache; the rest keeps downloading in the background while the rotation runs. The preload is no longer a hard gate in front of the screen.",
        "The background loops (playlist poll, command poll, SSE, event flush) now start immediately at boot instead of being chained behind the first sync — so a slow first download no longer also blocks updates, screenshots, and remote commands.",
        "Service worker: media fetches during live playback now have a 90 s timeout too, so a stalled network can't wedge a <video>/<img> mid-rotation; it errors and the rotation advances. App-shell cache bumped to v6 so devices pick up the new player code on next launch."
      ]
    },
    {
      "version": "1.1.0",
      "releasedAt": "2026-06-03T12:36:56Z",
      "filename": "com.lg.app.signage_1.1.0_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.1.0_all.ipk",
      "size": 351644,
      "sha256": "0369fa2a574c8643b4aeae8426be9685da025a3bb37720a81a791779778ed8de",
      "title": "Version is now shown on-screen — pairing, downloading and live-playback screens carry the build number",
      "notes": [
        "Built on top of the stable 1.0.16 playback core (no changes to the video-sync path).",
        "Pairing screen: the app version now sits in a polished pill right beside the ImageSignage logo in the top bar, so an installer can confirm the build at a glance before the screen is even claimed.",
        "Downloading / status screen: the centered status card now carries a subtle version line beneath the progress bar (with a small brand accent dot), visible during content download, no-content and error states.",
        "Live playback screen: a low-contrast, click-through version watermark (brand mark + version) sits in the bottom-right corner. It is intentionally faint so it never competes with the playing media, and it auto-hides whenever the full-screen status overlay is up so the version is never shown twice.",
        "Single source of truth: the displayed version is derived from the same APP_VERSION reported to the backend, so the on-screen label and the heartbeat-reported version can never drift apart."
      ]
    },
    {
      "version": "1.0.27",
      "releasedAt": "2026-06-03T12:01:20Z",
      "filename": "com.lg.app.signage_1.0.27_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.27_all.ipk",
      "size": 350674,
      "sha256": "db1f1b79f0a29febee2a1bcc327816a1882f7444d584bf56e5083d3fc96eed1c",
      "title": "Fix stuck-on-Downloading; stream uncached video; bigger pairing version",
      "notes": [
        "1.0.16 blocked ALL playback until every file finished downloading - one stalled video left it stuck on 'Downloading content ... preparing'. Playback now starts IMMEDIATELY and the cache warms in the background, so it can never freeze on that screen.",
        "Videos not cached yet stream natively: the Service Worker no longer proxies video requests (an SW-proxied video fails with SRC_UNSUPPORTED on webOS). Cached videos still play from the on-disk cache on later passes.",
        "Kept 1.0.26: no first-frame image; the previous (still-playing, muted) clip masks each video's load - no flash, no black.",
        "Made the version number on the connect/pair screen larger."
      ]
    },
    {
      "version": "1.0.26",
      "releasedAt": "2026-06-03T11:33:06Z",
      "filename": "com.lg.app.signage_1.0.26_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.26_all.ipk",
      "size": 350376,
      "sha256": "71a18021b68e3e3382e9d5ad3aada8a08ebc486fe54d243bf5cedca99b8cc5a3",
      "title": "1.0.16 + remove first-frame image; previous content masks during video load",
      "notes": [
        "Based on the working 1.0.16 build.",
        "Removed the video first-frame poster image entirely — it is no longer shown OR downloaded.",
        "During a video’s load/warm-up the PREVIOUS content stays on screen as the mask (the outgoing video is kept playing, muted, instead of paused — a paused video goes black on webOS), so there is no first-frame flash and no black between clips.",
        "Version shown on the connect screen and bottom-right of the player screen (v1.0.26)."
      ]
    },
    {
      "version": "1.0.16",
      "releasedAt": "2026-06-03T10:34:27Z",
      "filename": "com.lg.app.signage_1.0.16_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.16_all.ipk",
      "size": 354120,
      "sha256": "96a7a94f03aac69a9c423fa5dc1862fadda9ea9b44c9529fd42db67bfa7600b7",
      "title": "Video: decode in the visible layer, swap via visibility — the actual LG video-sync method (fixes 1.0.15 regression)",
      "notes": [
        "1.0.15 was wrong and made it worse: it warmed the incoming video up in an opacity:0 / inactive layer. webOS does NOT decode a video in a non-composited layer, so nothing painted, the first-frame wait timed out at ~1.5 s, and ONLY THEN did the layer activate and the video start decoding — giving both the long delay AND the black you saw. Fixed.",
        "Correct method, exactly mirroring LG's video-sync player: the new <video> is appended ON TOP of the content already on screen, inside the layer that is ALREADY VISIBLE (composited) — that is what makes webOS decode it immediately, with no delay. It is hidden only via visibility:hidden (on webOS `visibility` toggles the hardware video plane itself), so the previous content / first-frame poster masks the warm-up.",
        "The instant a real frame has decoded (first timeupdate, or requestVideoFrameCallback), the video is flipped visible and the previous content is removed. Mask covers the whole warm-up ⇒ no black; the clip was decoding the entire time ⇒ no slow start.",
        "Successive videos append into the same visible container and the previous one is removed once the new one is up (no opaque black layer ever sits between them), and single-video playlists still loop natively."
      ]
    },
    {
      "version": "1.0.15",
      "releasedAt": "2026-06-02T07:10:16Z",
      "filename": "com.lg.app.signage_1.0.15_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.15_all.ipk",
      "size": 348222,
      "sha256": "40c24ed738bfdb380082668c4a6bddaf21cb0a8f5666e337456b2fd1a3257546",
      "title": "Seamless video — keep previous content on screen until the new video actually paints (LG video-sync model)",
      "notes": [
        "Studied LG's official webOS video-sync player (the build that 'just works'). Its trick: when a new video is set it is appended HIDDEN on top of the still-visible old content, plays underneath, and only becomes visible once it is actually playing — the old node is removed seconds later. The previous frame therefore covers the entire decoder warm-up, so there is never a black gap.",
        "Our player now does the same. On a normal transition the incoming video's layer stays transparent and the video warms up behind the still-visible previous content; the transition runs, the layer is promoted, and the old layer is dropped ONLY after the new video has painted its first real frame (detected via requestVideoFrameCallback, with a timeupdate + 1.5 s fallback). This is independent of whether the hardware video plane composites above or below the HTML layer — the previous content does the covering, not a z-index bet.",
        "Cold start (first item after boot, nothing to cover with) still uses the first-frame poster, then cross-fades to the live video.",
        "Single-video playlists now loop NATIVELY via <video loop> instead of tearing the element down and re-decoding every cycle — that re-decode was the visible stutter/black-blink at each repeat of a lone promo clip. (Playlists with end-of-loop schedules keep the rotate path so schedules still fire.)"
      ]
    },
    {
      "version": "1.0.14",
      "releasedAt": "2026-06-02T06:36:39Z",
      "filename": "com.lg.app.signage_1.0.14_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.14_all.ipk",
      "size": 347510,
      "sha256": "cdc59f4ee8dfc7efb3e44fb90ffb7101f4a7c03c8137770e83d5e19e60c63c48",
      "title": "Black screen before video — fixed for real on hardware video-plane panels",
      "notes": [
        "Root cause 1.0.13 missed: on most webOS Signage panels the `<video>` decodes into a HARDWARE video plane that composites ABOVE the HTML graphics plane regardless of z-index. So the poster `<img>` placed 'above' the video was actually drawn BEHIND the black plane and never covered it — the screen stayed black until the first frame.",
        "Fix part 1: the `<video>` element itself is now transparent (opacity:0) until a frame is ready, so its black hardware plane is never composited. The HTML poster shows through underneath. The video is revealed instantly (opacity:1, no fade) so the live picture pops in fully opaque — never a half-faded, half-black plane.",
        "Fix part 2: the reveal now waits for an ACTUAL painted frame via `requestVideoFrameCallback` (falling back to the first `timeupdate`, then a 400 ms safety net), instead of a fixed 2-frame guess after `play()` resolved. `play()` resolving does not mean a frame is on screen — the old guess fired while the plane was still black.",
        "Fix part 3: the layer now waits for the poster image to finish loading (not just the video's `canplay`) before going active, so the poster is never blank at the moment of activation."
      ]
    },
    {
      "version": "1.0.13",
      "releasedAt": "2026-06-01T15:36:14Z",
      "filename": "com.lg.app.signage_1.0.13_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.13_all.ipk",
      "size": 346486,
      "sha256": "bc1e0ad804ddc6b0969069fb6f3a4beea02a8eff02163d5ed28d971121a777a1",
      "title": "Black flash before video — actual root cause + actual fix",
      "notes": [
        "The `<video>` element renders its content in the GPU's NATIVE VIDEO PLANE — a layer composited outside the HTML DOM stack. That plane is black for the brief moment between layer activation and the decoder pushing its first frame. `v.poster` is also drawn in the native plane and is wiped by the decoder during warm-up, which is why 1.0.9–1.0.12 didn't help.",
        "Fix: poster is now a separate `<img>` element placed ABOVE the `<video>` in the HTML composited layer (z-index 2 over z-index 1). The img paints reliably with the rest of the DOM. The video plays underneath, hidden by the img.",
        "Once `media.play()` resolves and one animation frame has passed (giving the decoder time to push its first frame to the compositor), `.playing` is added to the wrap and the img cross-fades out over 200 ms. What the user sees: poster still → smooth fade → live video.",
        "On videos without a server-side first-frame snapshot, the layer falls back to the previous (brief) black behaviour. Re-uploading those clips with the backend's frame extractor gives them the full smooth path."
      ]
    },
    {
      "version": "1.0.12",
      "releasedAt": "2026-06-01T13:56:22Z",
      "filename": "com.lg.app.signage_1.0.12_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.12_all.ipk",
      "size": 345332,
      "sha256": "570b1f02cd72a8eaf057aad60e940c78ea4f7915388dc5e7c502b45d2af50923",
      "title": "True offline playback — read from local cache, no network URL at playback time",
      "notes": [
        "Before each item plays, the player reads the cached bytes out of the on-disk Cache API and mints a short-lived object URL the `<video>` / `<img>` element loads from directly. The network URL is never set on the element at playback time, so disconnecting the network mid-rotation does nothing to playback.",
        "Object URLs are ephemeral — created per item, revoked the moment the layer rotates out. RAM hold ≈ one playing item's media; no permanent in-memory blob inventory.",
        "Cleanup runs on both armSwap's layer-wipe pass AND cancelRotation (so a playlist swap mid-transition doesn't leak the half-rotated layer's URLs).",
        "Removes the Service Worker dependency for video playback — Range slicing in sw.js stays as a safety net for cold-cache scenarios but the hot path goes straight to local data.",
        "Items whose cache entry is missing fall back to streaming from the network URL, same as before — so first-pair scenarios with no preload yet still play."
      ]
    },
    {
      "version": "1.0.11",
      "releasedAt": "2026-06-01T13:36:38Z",
      "filename": "com.lg.app.signage_1.0.11_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.11_all.ipk",
      "size": 344502,
      "sha256": "1564f4f541fcbd4f89bda1afc32b91ec13bf769d189fcacfad3690bf599dce0e",
      "title": "Videos cached as real files (not blob URLs) + SW Range slicing",
      "notes": [
        "Videos are now stored in the Service Worker's Cache API as full Responses keyed by their URL — persistent across app reloads and reboots. The previous blob: URL approach lived in RAM only and vanished the moment the player app restarted.",
        "Service Worker now handles `Range: bytes=…` requests by slicing the cached full Response into a 206 Partial Content reply with the right `Content-Range` header. That's the missing piece that was making videos bypass cache on Chrome 94 (webOS Signage 6.1) — every `<video src=URL>` issues Range requests for seeking, and the cache miss was sending traffic straight to the network.",
        "`<video>` elements now point at `item.url` directly. The SW intercepts the fetch transparently and slices the cached blob byte-by-byte. No more `URL.createObjectURL`, no more revoke bookkeeping, no more RAM-pressure invalidation.",
        "Preload still verifies the full body landed (resp.blob() resolves only after the last byte arrives) but writes it to disk as a Response instead of holding it in memory.",
        "Removed `revokeStaleBlobs` and the `blobUrls` controller state. The SW handles cache trimming via the existing TRIM_MEDIA message."
      ]
    },
    {
      "version": "1.0.10",
      "releasedAt": "2026-06-01T12:34:38Z",
      "filename": "com.lg.app.signage_1.0.10_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.10_all.ipk",
      "size": 343582,
      "sha256": "260f771185ec7c0af3e02490ca4c6f293921f554a4fcc4aff642b886fa32f912",
      "title": "Weather widget icons + news image fallback + video retry",
      "notes": [
        "Weather widget now maps Open-Meteo WMO integer codes (0=clear, 45-48=fog, 51-67=rain, 71-77=snow, 95-99=thunder, etc.) to icons. Before this every weather widget showed the '·' unknown icon because the renderer was passing integer codes into a regex-based string matcher.",
        "News slides handle broken image URLs gracefully — third-party CDNs (Sky/CNN/NYT etc.) occasionally 404 syndicated images. onerror hides the broken `<img>` and adds `.no-img` to the slide so the text takes the full width instead of leaving 50% blank.",
        "Video preload retries 3× with exponential back-off (600/1200/2400 ms) before giving up. Caught a class of transient first-pair fetch failures that were leaving videos to stream from network.",
        "Widget render decisions log to console at info level — `[widget] native NEWS render — slug=skynews items=10 age=71s` etc. — so `ares-inspect` shows exactly which path each widget took (native vs iframe fallback) and why."
      ]
    },
    {
      "version": "1.0.9",
      "releasedAt": "2026-06-01T12:03:03Z",
      "filename": "com.lg.app.signage_1.0.9_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.9_all.ipk",
      "size": 342322,
      "sha256": "42cd5c3288fada9ef0cb5504952ca0dd526c75e2222fb800d306ccb16978584d",
      "title": "Kill the residual black flash before video + widget paints",
      "notes": [
        "Videos now carry `poster=firstFrameUrl` on the `<video>` element. `<video>` paints black until `play()` succeeds the first time — even with a valid blob src and `canplay` fired. The poster (the server-extracted first-frame snapshot, already preloaded into Cache API) covers the element during the transition window and is swapped for the real frame the moment `armSwap` calls `play()`.",
        "`baseItem` now actually carries `firstFrameUrl` / `lastFrameUrl` / `logoUrl` and `materialise` absolutizes them via `api.absolutize`. The fields used to be dropped on the floor — the player couldn't have set the poster even if it tried.",
        "Widget containers (`.widget-wrap`, `.widget-native`) now use `position: absolute; inset: 0` instead of width/height: 100% inside the layer's flex parent. A 1–2 px gap could appear at some webOS resolutions and the layer's `background: #000` bled through during transitions.",
        "News-widget image URLs (from items[i].imageUrl) are now warmed into the Cache API alongside the widget data fetch. First slide paints from cache, not over a blank `<img>`."
      ]
    },
    {
      "version": "1.0.8",
      "releasedAt": "2026-06-01T11:39:03Z",
      "filename": "com.lg.app.signage_1.0.8_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.8_all.ipk",
      "size": 341318,
      "sha256": "60b35f453478cd548025b9a0b13e6c2fad9ac5658027b09dd95ec75d971c6067",
      "title": "Video instant-start + native widget rendering",
      "notes": [
        "Videos now load as in-RAM blob URLs during preload. Chrome 94 on Signage 6.1 issues Range requests for `<video src=…>` that the service worker's stale-while-revalidate handler can't satisfy from a full-Response cache; routing video bytes through `URL.createObjectURL` skips Range requests entirely so playback starts in single-digit milliseconds.",
        "Removed `<video autoplay>` from renderItem. The element used to play invisibly while the transition ran — users saw the wrong half of the clip. armSwap now calls `media.play()` at the exact moment the layer activates.",
        "Native widget rendering for news + weather. Reads cached JSON from `/api/widgets/data/<installation_id>` (5-min server-side refresher), renders directly into the DOM. No iframe SPA bootstrap, no CSP surprises, no third-party RSS round-trip on every play. Falls back to the iframe path for slugs we don't have native templates for.",
        "Progress display now shows the playlist's item count + the per-file index in the subtitle — '5 items · file 7 of 13 · holiday-promo.mp4' — instead of just a raw file count.",
        "Blob URLs revoked on playlist swap so memory doesn't grow forever."
      ]
    },
    {
      "version": "1.0.7",
      "releasedAt": "2026-05-31T19:05:09Z",
      "filename": "com.lg.app.signage_1.0.7_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.7_all.ipk",
      "size": 337998,
      "sha256": "8758ff519d05f579b67317b93769c329a47241e4b4323a1e85262ed8b9a4741d",
      "title": "Pre-download playlist content (matches Android player)",
      "notes": [
        "After first pair (and after every playlist update that brings in new files), the player now pulls every image and video into the local Cache API up front, with the same progress bar the Android player shows.",
        "Video bytes are on disk by the time `playOne` runs, so playback starts in single-digit milliseconds instead of black-screen-then-buffer.",
        "Layer swap waits for the inbound media's `canplay` / `load` event (1.5 s cap) — eliminates the black flash that used to land between transition end and first painted frame.",
        "Heartbeat sends `isDownloading=true` while the preload runs, so the CMS dashboard's download badge appears just like it does for the Android player.",
        "Subsequent playlist polls compare against `preloadedUrls` and only show the bar if there's genuinely new content. A poll that brings in no new files transitions silently.",
        "Includes the 1.0.6 widget rendering fixes (iframe sandbox order, cache-bust, allow-presentation, loading spinner)."
      ]
    },
    {
      "version": "1.0.6",
      "releasedAt": "2026-05-30T04:36:20Z",
      "filename": "com.lg.app.signage_1.0.6_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.6_all.ipk",
      "size": 336554,
      "sha256": "3bb5c15c0acedf04ee5b82e2e3dbb116138048dc656314564e5ad1bac5d7739e",
      "title": "Widget rendering fixes (news, weather)",
      "notes": [
        "Widget iframe URL is cache-busted with `?t=<ms>` per play — news headlines and weather data refresh every cycle instead of pinning the first cached response.",
        "Iframe sandbox / allow / referrerpolicy are now set BEFORE `src`. Chrome < 96 (UL5Q-E ships Chrome 94) was ignoring sandbox flags applied after navigation.",
        "Adds `allow-presentation`, `allow-modals` and `allow-popups-to-escape-sandbox` so widgets that use the Presentation API or open helper popups (some weather widgets) actually run.",
        "Widget URL now carries `tz`, `lang`, `w` and `h` query params so the renderer can pick the right timezone / units / layout for this display.",
        "Loading spinner sits behind the iframe while it boots — replaces the blank black flash on slow first paints.",
        "Widget fallback timeout reduced from 8 s to 5 s — broken widgets advance the rotation faster."
      ]
    },
    {
      "version": "1.0.5",
      "releasedAt": "2026-05-29T08:30:03Z",
      "filename": "com.lg.app.signage_1.0.5_all.ipk",
      "downloadUrl": "/source/com.lg.app.signage_1.0.5_all.ipk",
      "size": 334928,
      "sha256": "009eb73abb60e9733ec8ec35c66559ce7facdf68f48230074535c73265c84989",
      "title": "Commercial Signage install fix — privileged namespace",
      "notes": [
        "App id moved into the `com.lg.app.*` namespace so commercial LG Signage firmware grants install privileges without an LG partner certificate.",
        "Adds `inspectable: true` and `autoRecovery: true` — standard Signage flags so DevTools attach works and the app restarts on crash.",
        "Confirmed install path on field UL5Q-E: Settings → Easy Setting → Software Update → Custom App, point at the IPK URL — the existing webOS 6.1 Signage upgrade flow now accepts the package."
      ]
    },
    {
      "version": "1.0.4",
      "releasedAt": "2026-05-29T06:48:38Z",
      "filename": "com.imagesignage.player_1.0.4_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.0.4_all.ipk",
      "size": 334554,
      "sha256": "c69697db0785f7d39f1e789cbe016ba6ac7ddb7f2e4e6476f0092daf16ced517",
      "title": "Fix commercial Signage install rejection (UL5Q / webOS 6.1)",
      "notes": [
        "Removed `trustLevel: trusted` from appinfo.json — that flag tells the Signage installer to expect an LG-partner signature, and rejecting an unsigned package was producing the 'upgrade complete → failed at boot' pattern on field displays.",
        "Removed the invented `vendorExtension.signageOSPermissions` array — not a real field; strict installers refuse the package outright when they see unknown vendorExtension keys.",
        "Kept all Signage-friendly fields that don't require partner signing (useSplashOnLaunch, transparent, spinnerOnLaunch, handlesRelaunch, disableBackHistoryAPI).",
        "Added `bgColor` for the splash backdrop colour."
      ]
    },
    {
      "version": "1.0.3",
      "releasedAt": "2026-05-29T05:08:31Z",
      "filename": "com.imagesignage.player_1.0.3_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.0.3_all.ipk",
      "size": 334240,
      "sha256": "f67c1b1265e4f484895b49110143506d72e718d2731b965c4c6000f90b71d130",
      "title": "Scheduler & L-shape fixes",
      "notes": [
        "L-shape overlay now resets the stage geometry on deactivation. Previously the playlist stayed shoved into the top-right corner after the schedule expired.",
        "L-shape watcher cadence tightened from 1 s to 500 ms so slot boundaries fire within half a second of the start/end offsets.",
        "End-of-loop schedule activation is logged with title, mode, time band and duration — visible in `ares-inspect` for easy diagnosis.",
        "L-shape pixel offsets now round to whole pixels (no more subpixel flicker on non-1920×1080 panels)."
      ]
    },
    {
      "version": "1.0.2",
      "releasedAt": "2026-05-28T16:23:51Z",
      "filename": "com.imagesignage.player_1.0.2_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.0.2_all.ipk",
      "size": 333284,
      "sha256": "64fd143e9e6321f620b5b4233f31768f593fe2b2a5b6016ab03f1ac2565285c4",
      "title": "Playlist rendering fixes",
      "notes": [
        "`transitionIn: 'none'` now swaps instantly (was hanging on a black frame for 500 ms).",
        "Honours `transitionInDurationMs: 0` from the dashboard instead of silently rewriting it to 500 ms.",
        "Outgoing layer no longer gets wiped mid-frame when the exit animation is longer than the enter animation.",
        "Video `startTime` (trim from) now applies after `loadedmetadata` so webOS doesn't snap back to 0.",
        "Video advances to the next item the moment it reaches its natural end / `endTime`, instead of freezing on the last frame for the rest of the slot.",
        "Video autoplay falls back to muted if the browser blocks it, instead of dropping the item.",
        "Image / audio load errors are logged so a single broken asset doesn't stall the rotation."
      ]
    },
    {
      "version": "1.0.1",
      "releasedAt": "2026-05-28T12:19:50Z",
      "filename": "com.imagesignage.player_1.0.1_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.0.1_all.ipk",
      "size": 332406,
      "sha256": "1df914431026ffb8ceaccc5001fd8361d7bd299b54ca7a90ab1e490a2e022d85",
      "title": "Signage compatibility & offline playback",
      "notes": [
        "Adds the `trustLevel: trusted` and `vendorExtension` fields required by commercial Signage firmware.",
        "Service worker caches the app shell and every media file for full offline playback.",
        "Player now boots from the last cached playlist when the CMS is unreachable.",
        "Pairing screen shows a clear error when the wrong device type tries to claim a screen slot."
      ]
    },
    {
      "version": "1.0.0",
      "releasedAt": "2026-05-28T12:09:43.913Z",
      "filename": "com.imagesignage.player_1.0.0_all.ipk",
      "downloadUrl": "/source/com.imagesignage.player_1.0.0_all.ipk",
      "size": 328736,
      "sha256": "8b84c9ae3f0030fefc2c99698a5710f2107d647fdd6fd3560beeeeed1fcec629",
      "title": "Initial release",
      "notes": [
        "Pairing screen with QR + Screen ID + PIN, mirrors Android player.",
        "Playlist rotation, transitions, schedules, L-shape squeeze-back overlays, widgets.",
        "Heartbeat + SSE push + content-event reporting to the CMS."
      ]
    }
  ]
}
