The dashboard exposes landscape / portrait / landscape-flipped / portrait-flipped
and the README promises rotation, but neither player ever read the device's
orientation field - it was hardcoded landscape. Reported by a customer testing
Firestick + Samsung signage.
Rotate the CONTENT in software, not the panel: Fire TV / Android TV / Tizen are
fixed-landscape and ignore setRequestedOrientation (can't physically rotate).
- Android (MainActivity): applyOrientation() resizes rootView to the rotated
dimensions, recenters, and rotates 0/90/180/270. rootView is the shared
container for single-zone AND multi-zone, so both are covered. Driven from the
playlist-update payload.
- Tizen (app.js): CSS transform on the stage (rotate + swapped 100vh/100vw),
same four values, from the playlist payload.
Verified on an Android 16 emulator: device set to portrait -> 'Applied
orientation: portrait (rotation=90, swap=true)' and the video renders rotated.
The player has a launcher (category.HOME) + a boot receiver, but auto-start was
unreliable where you can't set a home launcher (Android TV) and on Android 14+,
where USE_FULL_SCREEN_INTENT is auto-revoked for non-calling apps so the boot
full-screen launcher silently no-ops.
Boot launch:
- BootReceiver now does a direct background startActivity when 'display over other
apps' (SYSTEM_ALERT_WINDOW) is granted — a real exception to the bg-activity-launch
restriction, and the one path that works on Android TV. Full-screen-intent
notification kept as a fallback (locked screen / no overlay).
- Boot notification moved to a dedicated HIGH-importance channel (full-screen
intents are only honored from one), and it auto-dismisses once the UI is up.
Setup screen — new permission rows so operators can grant what boot-launch needs:
- Launch on Boot (USE_FULL_SCREEN_INTENT, shown on Android 14+)
- Background Activity (battery-optimization exemption)
- Display Over Apps (SYSTEM_ALERT_WINDOW)
Made the screen scrollable and ~50% smaller text/buttons so all rows + Continue
fit on one screen (incl. landscape signage). Install-Unknown-Apps subtitle now
states updates are signature-verified, so it doesn't read as 'install anything'.
Verified end-to-end on an Android 16 emulator: after reboot the app auto-launched
(Direct launch via overlay) and the boot notice cleared itself; all rows toggle.
From Chris's live debug logs on the L-Bar layout:
- ZoneManager only rendered the FIRST assignment per zone -> the Main zone (3
images) never rotated ('says it's switching but it's not'). Now each zone
cycles its own assignments: images/widgets on a duration timer, videos on
end (single-item zones still loop).
- The fullscreen PlaylistController kept running BEHIND the zones (playItem every
10s, would leak audio for a zone video) because startIfNeeded() ran after every
playlist update. Now only start it when not in multi-zone (zoneManager.hasZones).
- renderAssignments still called container.removeAllViews() (the same static-view
nuke the cleanup() fix addressed) -> now removes only its own zone views.
Widgets worked in multi-zone layouts (ZoneManager renders them in a WebView) but
were broken in "default fullscreen" (no layout) and the fullscreen template (a
single-zone layout) - both take the single-zone PlaylistController path, which:
1) called getString("content_id"), throwing on a widget assignment (no
content_id) - in both the playlist builder AND the pre-download loop, which
could break the whole fullscreen playlist; and
2) had no widget render case in playItem (so a widget never displayed).
Fix:
- PlaylistItem gains widgetId/widgetType + isWidget; the builder reads them and
tolerates a missing content_id.
- playItem renders a widget fullscreen via MediaPlayerManager.showWidget() (loads
/api/widgets/:id/render in the full-screen WebView, mirroring ZoneManager).
- Widgets auto-advance on their duration like images.
- Pre-download loop skips widget assignments (no file to fetch).
Compile-checked; signed APK builds. Needs on-device check: a widget plays in
default-fullscreen and the fullscreen template, and mixed widget+media playlists
advance correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 4K image assigned to a 1080p display decoded as a ~33 MB ARGB_8888
bitmap and OOM'd. Worse, the cached playlist on disk meant relaunch
hit the same image and crashed again — only a reinstall recovered.
New ImageLoader utility reads bounds via inJustDecodeBounds, computes
inSampleSize against the device screen (or zone size for multi-zone
layouts), and returns null on OOM/Throwable so callers skip the item
instead of crashing. MediaPlayerManager exposes an onImageError
callback wired to playlistController.next() so a bad item advances
the playlist. The cached-playlist restore in onCreate now catches
Throwable (was Exception) and clears the cache on any failure,
breaking the crash loop. android:largeHeap="true" added as belt and
braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web player:
- Cache playlist JSON to localStorage on every update
- Restore and start playing immediately on boot before connecting
- Clear cache on unpair/reset
Android app:
- Cache playlist JSON to EncryptedSharedPreferences on every update
- Restore cached playlist on cold-start, play from disk-cached content
- Update cache on content deletion, clear on unpair
Server (device socket):
- Fingerprint reconnect: issue fresh token instead of rejecting
- Send device:paired on fingerprint recovery for claimed devices
- Add status logging and dashboard notification on fingerprint reconnect
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>