From dfc8a4e35820e136232e870ca0905a5338823838 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 9 Jun 2026 21:43:08 -0500 Subject: [PATCH] feat(player): software orientation (portrait + flipped) on both players (1.7.12) 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. --- VERSION | 2 +- android/app/build.gradle.kts | 4 +-- .../com/remotedisplay/player/MainActivity.kt | 32 +++++++++++++++++++ tizen/js/app.js | 22 +++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 8f8b3f7..e6a68e9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.11 +1.7.12 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ab9c1ee..6cb10cd 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.remotedisplay.player" minSdk = 26 targetSdk = 34 - versionCode = 14 - versionName = "1.7.11" + versionCode = 15 + versionName = "1.7.12" } signingConfigs { diff --git a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt index 4916ac4..8da5f5e 100644 --- a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt +++ b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt @@ -52,6 +52,7 @@ class MainActivity : AppCompatActivity() { private lateinit var statusOverlay: View private lateinit var statusText: TextView private lateinit var rootView: View + private var currentOrientation: String? = null private val handler = Handler(Looper.getMainLooper()) private var remoteStreaming = false @@ -198,9 +199,40 @@ class MainActivity : AppCompatActivity() { } + // Rotate the whole stage in software so portrait / flipped signage works even on + // fixed-landscape hardware (Fire TV, Android TV and most signage sticks ignore + // setRequestedOrientation - they can't physically rotate the panel). Resizes + // rootView to the rotated dimensions, recenters, and rotates. Covers single-zone + // (playerView/imageView/youtubeWebView) and multi-zone (ZoneManager renders into + // the same rootView). Values mirror the dashboard: landscape / portrait / + // landscape-flipped / portrait-flipped. + private fun applyOrientation(orientation: String) { + if (orientation == currentOrientation) return + currentOrientation = orientation + val m = resources.displayMetrics + val w = m.widthPixels.toFloat() + val h = m.heightPixels.toFloat() + val (rot, swap) = when (orientation) { + "portrait" -> 90f to true + "portrait-flipped" -> 270f to true + "landscape-flipped" -> 180f to false + else -> 0f to false // landscape + } + val lp = rootView.layoutParams + lp.width = (if (swap) h else w).toInt() + lp.height = (if (swap) w else h).toInt() + rootView.layoutParams = lp + rootView.translationX = if (swap) (w - h) / 2f else 0f + rootView.translationY = if (swap) (h - w) / 2f else 0f + rootView.rotation = rot + rootView.requestLayout() + Log.i("MainActivity", "Applied orientation: $orientation (rotation=$rot, swap=$swap)") + } + private fun setupServiceCallbacks() { wsService?.onPlaylistUpdate = { data -> try { + applyOrientation(data.optString("orientation", "landscape")) // Check if device is suspended (trial expired / over limit) if (data.optBoolean("suspended", false)) { val message = data.optString("message", "Account Suspended") diff --git a/tizen/js/app.js b/tizen/js/app.js index efd5bf8..a1c62ac 100644 --- a/tizen/js/app.js +++ b/tizen/js/app.js @@ -192,8 +192,30 @@ // ---- playback ---- var player = new PlaylistPlayer(elStage, function () { return serverUrl.replace(/\/+$/, ''); }); + // Rotate the playback stage in software for portrait / flipped signage. Tizen TVs + // are fixed-landscape, so we rotate the CONTENT (not the panel). Values mirror the + // dashboard: landscape / portrait / landscape-flipped / portrait-flipped. + function applyOrientation(o) { + var s = elStage; + if (!o || o === 'landscape') { + s.style.position = ''; s.style.top = ''; s.style.left = ''; + s.style.width = ''; s.style.height = ''; s.style.transform = ''; s.style.transformOrigin = ''; + return; + } + var deg = o === 'portrait' ? 90 : o === 'portrait-flipped' ? 270 : o === 'landscape-flipped' ? 180 : 0; + var swap = (deg === 90 || deg === 270); + s.style.position = 'absolute'; + s.style.top = '50%'; + s.style.left = '50%'; + s.style.width = swap ? '100vh' : '100vw'; + s.style.height = swap ? '100vw' : '100vh'; + s.style.transformOrigin = 'center center'; + s.style.transform = 'translate(-50%, -50%) rotate(' + deg + 'deg)'; + } + function onPlaylist(payload) { if (!payload) return; + applyOrientation(payload.orientation || 'landscape'); if (payload.suspended) { player.stop(); elStage.innerHTML = '

' +