mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
PiP overlay: add Android + web players (#109)
Extends the #109 PiP MVP to the other two players so the protocol (device:pip-show / device:pip-clear) is honored fleet-wide, not just on Tizen. No server/protocol changes — the route and socket messages are player-agnostic; these are the two missing surfaces. Web player (server/player/index.html): - New #pipContainer layer above #playerContainer, pointer-transparent, that the playlist render never touches. The same orientation transform is applied to it as to #playerContainer (extended to also reset width/height on landscape so a portrait->landscape switch realigns), so corner positions track the visible content. - Inline PiP logic mirroring tizen/js/pip-overlay.js: image -> <img>, web -> <iframe> (muted by default via empty allow=), position/size/bg/opacity/radius/title, single slot last-show-wins, duration timer (0 = until cleared), id-aware clear, wrapped teardown. - device:pip-show/clear handlers; reports show/clear over device:log (tag "pip"). Android player: - activity_main.xml: a pipLayout FrameLayout as the LAST child of rootLayout — it draws above the content AND inherits rootView's orientation rotation/translation, so corner positioning is orientation-matched for free. - PipOverlay.kt (new): builds the overlay box into pipLayout. image -> ImageView (decoded off-thread via ImageLoader, dropped if torn down mid-decode); web -> WebView with mediaPlaybackRequiresUserGesture=true (mute-by-default). Gravity-based corner/center placement with a 4% inset, GradientDrawable bg + corner radius, alpha=opacity, optional title bar. Single slot last-show-wins; duration timer; id-aware clear; teardown wrapped and also run on activity destroy (WebView cleanup). - WebSocketService: onPipShow/onPipClear callbacks + safeOn handlers posted to the main thread (they build Views) + a sendLog(tag, level, message) emitter for device:log. - MainActivity: instantiate PipOverlay (log -> wsService.sendLog("pip", ...)), wire the callbacks, tear down on destroy. Verified: Android assembleDebug builds clean; web player inline JS parses; server suite still 161/161 (no server changes this commit). Not yet validated on real hardware — four-orientation corner positioning mirrors the player container/rootView transform but should be eyeballed on a panel. Refs #109 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7eab9c6092
commit
1c76fa865c
|
|
@ -26,6 +26,7 @@ import com.remotedisplay.player.data.ServerConfig
|
|||
import com.remotedisplay.player.player.MediaPlayerManager
|
||||
import com.remotedisplay.player.player.PlaylistController
|
||||
import com.remotedisplay.player.player.PlaylistItem
|
||||
import com.remotedisplay.player.player.PipOverlay
|
||||
import com.remotedisplay.player.player.WallController
|
||||
import com.remotedisplay.player.player.ZoneManager
|
||||
import com.remotedisplay.player.remote.ScreenshotCapture
|
||||
|
|
@ -49,6 +50,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var updateChecker: UpdateChecker
|
||||
private var zoneManager: ZoneManager? = null
|
||||
private lateinit var wallController: WallController
|
||||
private lateinit var pipOverlay: PipOverlay // #109: PiP overlay layer
|
||||
|
||||
private lateinit var playerView: PlayerView
|
||||
private lateinit var imageView: ImageView
|
||||
|
|
@ -132,6 +134,12 @@ class MainActivity : AppCompatActivity() {
|
|||
// Hide player controls
|
||||
playerView.useController = false
|
||||
|
||||
// #109: PiP overlay layer (top child of rootLayout; inherits the orientation
|
||||
// transform). Reports show/clear over device:log (tag "pip").
|
||||
pipOverlay = PipOverlay(this, findViewById(R.id.pipLayout)) { level, message ->
|
||||
wsService?.sendLog("pip", level, message)
|
||||
}
|
||||
|
||||
// Setup zone manager for multi-zone layouts
|
||||
zoneManager = ZoneManager(this, rootView as FrameLayout) {
|
||||
playlistController.onVideoComplete()
|
||||
|
|
@ -550,6 +558,10 @@ class MainActivity : AppCompatActivity() {
|
|||
wsService?.onWallSync = { data -> if (::wallController.isInitialized) wallController.onSync(data) }
|
||||
wsService?.onWallSyncRequest = { data -> if (::wallController.isInitialized) wallController.onSyncRequest(data) }
|
||||
|
||||
// #109: PiP overlay show/clear (posted to the main thread by the service).
|
||||
wsService?.onPipShow = { data -> if (::pipOverlay.isInitialized) pipOverlay.show(data) }
|
||||
wsService?.onPipClear = { data -> if (::pipOverlay.isInitialized) pipOverlay.clearFrom(data) }
|
||||
|
||||
wsService?.onRegistered = { _ ->
|
||||
hideStatus()
|
||||
}
|
||||
|
|
@ -734,6 +746,7 @@ class MainActivity : AppCompatActivity() {
|
|||
override fun onDestroy() {
|
||||
remoteStreaming = false
|
||||
zoneManager?.cleanup()
|
||||
if (::pipOverlay.isInitialized) pipOverlay.clear(null) // #109: tear down overlay WebView
|
||||
if (::mediaPlayer.isInitialized) {
|
||||
stopScreenshotStreaming()
|
||||
mediaPlayer.release()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
package com.remotedisplay.player.player
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.remotedisplay.player.util.ImageLoader
|
||||
import com.remotedisplay.player.util.WebViewSupport
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* PiP overlay layer (#109). Renders an image or web (WebView) overlay into [pipLayout],
|
||||
* which is the top child of rootLayout — so it draws above the playlist AND inherits the
|
||||
* orientation rotation/translation applied to rootView (corner positions track the visible
|
||||
* content). The playlist renderers never touch pipLayout.
|
||||
*
|
||||
* MVP semantics (mirrors the web/Tizen players): single overlay slot, last-show-wins;
|
||||
* duration timer (0 = until cleared); device:pip-clear (id-aware) or the timer tears it
|
||||
* down; teardown is wrapped so a malformed payload can't wedge the layer. Reports show/clear
|
||||
* via [log] (device:log tag "pip").
|
||||
*
|
||||
* All methods must run on the main thread (WebSocketService posts the socket events there).
|
||||
*/
|
||||
class PipOverlay(
|
||||
private val context: Context,
|
||||
private val pipLayout: FrameLayout,
|
||||
private val log: (level: String, message: String) -> Unit = { _, _ -> }
|
||||
) {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var timer: Runnable? = null
|
||||
private var current: String? = null
|
||||
private var webView: WebView? = null
|
||||
|
||||
fun show(p: JSONObject) {
|
||||
try {
|
||||
teardown() // single slot, last-show-wins
|
||||
val type = p.optString("type", "image")
|
||||
val uri = p.optString("uri", "")
|
||||
if (uri.isEmpty()) { log("warn", "pip show ignored: empty uri"); return }
|
||||
|
||||
val dm = context.resources.displayMetrics
|
||||
val w = p.optInt("width", 480).coerceIn(1, dm.widthPixels * 4)
|
||||
val h = p.optInt("height", 360).coerceIn(1, dm.heightPixels * 4)
|
||||
|
||||
val box = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
clipToOutline = true
|
||||
val radius = p.optInt("border_radius", 0).toFloat()
|
||||
background = GradientDrawable().apply {
|
||||
setColor(parseColor(p.optString("background_color", ""), Color.BLACK))
|
||||
cornerRadius = radius
|
||||
}
|
||||
alpha = p.optDouble("opacity", 1.0).toFloat().coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
val title = p.optString("title", "")
|
||||
if (title.isNotEmpty()) {
|
||||
box.addView(TextView(context).apply {
|
||||
text = title
|
||||
setTextColor(parseColor(p.optString("title_color", ""), Color.WHITE))
|
||||
setBackgroundColor(Color.argb(115, 0, 0, 0))
|
||||
textSize = 14f
|
||||
maxLines = 1
|
||||
setPadding(20, 12, 20, 12)
|
||||
}, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
|
||||
// Media fills the remaining box height (weight 1).
|
||||
val mediaLp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||
if (type == "web") {
|
||||
val wv = WebView(context)
|
||||
WebViewSupport.configure(wv, "PiP")
|
||||
// Mute web audio by default: deny autoplay-with-gesture so audio can't start.
|
||||
try { wv.settings.mediaPlaybackRequiresUserGesture = true } catch (_: Throwable) {}
|
||||
wv.setBackgroundColor(Color.TRANSPARENT)
|
||||
wv.loadUrl(uri)
|
||||
webView = wv
|
||||
box.addView(wv, mediaLp)
|
||||
} else {
|
||||
val img = ImageView(context).apply { scaleType = ImageView.ScaleType.CENTER_CROP }
|
||||
box.addView(img, mediaLp)
|
||||
loadImageInto(img, uri, w, h)
|
||||
}
|
||||
|
||||
// Corner/center placement; 4% inset off the edges (matches web/Tizen).
|
||||
val lp = FrameLayout.LayoutParams(w, h)
|
||||
val mx = (dm.widthPixels * 0.04f).toInt()
|
||||
val my = (dm.heightPixels * 0.04f).toInt()
|
||||
lp.gravity = when (p.optString("position", "top-right")) {
|
||||
"top-left" -> { lp.leftMargin = mx; lp.topMargin = my; Gravity.TOP or Gravity.START }
|
||||
"bottom-right" -> { lp.rightMargin = mx; lp.bottomMargin = my; Gravity.BOTTOM or Gravity.END }
|
||||
"bottom-left" -> { lp.leftMargin = mx; lp.bottomMargin = my; Gravity.BOTTOM or Gravity.START }
|
||||
"center" -> Gravity.CENTER
|
||||
else -> { lp.rightMargin = mx; lp.topMargin = my; Gravity.TOP or Gravity.END } // top-right
|
||||
}
|
||||
|
||||
pipLayout.addView(box, lp)
|
||||
pipLayout.visibility = View.VISIBLE
|
||||
current = p.optString("pip_id", "(anon)")
|
||||
|
||||
val dur = p.optInt("duration", 0)
|
||||
if (dur > 0) {
|
||||
val id = current
|
||||
timer = Runnable { clear(id) }.also { handler.postDelayed(it, dur * 1000L) }
|
||||
}
|
||||
log("info", "pip show $type ${p.optString("pip_id", "")} pos=${p.optString("position", "top-right")} dur=$dur")
|
||||
} catch (e: Throwable) {
|
||||
// A malformed payload must never wedge the layer.
|
||||
teardown()
|
||||
log("warn", "pip show failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear a showing overlay. A pip_id only clears if it matches; an empty id clears any. */
|
||||
fun clear(pipId: String?) {
|
||||
if (!pipId.isNullOrEmpty() && current != null && pipId != current) return
|
||||
val had = current != null
|
||||
teardown()
|
||||
if (had) log("info", "pip cleared" + (if (!pipId.isNullOrEmpty()) " $pipId" else ""))
|
||||
}
|
||||
|
||||
/** Convenience for the device:pip-clear payload ({ pip_id? }). */
|
||||
fun clearFrom(data: JSONObject) = clear(if (data.has("pip_id")) data.optString("pip_id") else null)
|
||||
|
||||
private fun teardown() {
|
||||
try { timer?.let { handler.removeCallbacks(it) } } catch (_: Throwable) {}
|
||||
timer = null
|
||||
current = null
|
||||
try { webView?.apply { stopLoading(); loadUrl("about:blank"); destroy() } } catch (_: Throwable) {}
|
||||
webView = null
|
||||
try { pipLayout.removeAllViews(); pipLayout.visibility = View.GONE } catch (_: Throwable) {}
|
||||
}
|
||||
|
||||
private fun loadImageInto(img: ImageView, url: String, w: Int, h: Int) {
|
||||
val token = current
|
||||
Thread {
|
||||
val bmp = try { ImageLoader.decodeUrl(url, w, h) } catch (e: Throwable) { null }
|
||||
img.post {
|
||||
// Drop the result if this overlay was torn down / replaced while decoding.
|
||||
if (img.parent == null || token != current) return@post
|
||||
if (bmp != null) img.setImageBitmap(bmp)
|
||||
else { log("warn", "pip image failed to load"); clear(token) }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun parseColor(hex: String, fallback: Int): Int =
|
||||
if (hex.matches(Regex("^#[0-9A-Fa-f]{6}$"))) try { Color.parseColor(hex) } catch (e: Throwable) { fallback } else fallback
|
||||
}
|
||||
|
|
@ -42,6 +42,8 @@ class WebSocketService : Service() {
|
|||
var onCommand: ((String, JSONObject?) -> Unit)? = null
|
||||
var onWallSync: ((JSONObject) -> Unit)? = null
|
||||
var onWallSyncRequest: ((JSONObject) -> Unit)? = null
|
||||
var onPipShow: ((JSONObject) -> Unit)? = null
|
||||
var onPipClear: ((JSONObject) -> Unit)? = null
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): WebSocketService = this@WebSocketService
|
||||
|
|
@ -236,6 +238,16 @@ class WebSocketService : Service() {
|
|||
handler.post { try { onWallSyncRequest?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onWallSyncRequest cb: ${e.message}") } }
|
||||
}
|
||||
|
||||
// #109: PiP overlay. Post to the main thread — the handlers build Views.
|
||||
safeOn("device:pip-show") { args ->
|
||||
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||
handler.post { try { onPipShow?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onPipShow cb: ${e.message}") } }
|
||||
}
|
||||
safeOn("device:pip-clear") { args ->
|
||||
val data = (args.firstOrNull() as? JSONObject) ?: JSONObject()
|
||||
handler.post { try { onPipClear?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onPipClear cb: ${e.message}") } }
|
||||
}
|
||||
|
||||
safeOn("device:command") { args ->
|
||||
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||
val type = data.optString("type", "")
|
||||
|
|
@ -527,6 +539,20 @@ class WebSocketService : Service() {
|
|||
} catch (e: Throwable) { Log.w("WebSocketService", "sendContentAck: ${e.message}") }
|
||||
}
|
||||
|
||||
// #109: surface a log line to the dashboard device-detail screen (dashboard:device-log).
|
||||
// Used for PiP show/clear (tag "pip"); guarded like the other emitters.
|
||||
fun sendLog(tag: String, level: String, message: String) {
|
||||
if (socket?.connected() != true) return
|
||||
try {
|
||||
socket?.emit("device:log", JSONObject().apply {
|
||||
put("device_id", config.deviceId)
|
||||
put("tag", tag)
|
||||
put("level", level)
|
||||
put("message", message)
|
||||
})
|
||||
} catch (e: Throwable) { Log.w("WebSocketService", "sendLog: ${e.message}") }
|
||||
}
|
||||
|
||||
fun sendPlaybackState(contentId: String, positionSec: Float) {
|
||||
if (socket?.connected() != true) return
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -76,4 +76,14 @@
|
|||
android:indeterminate="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- #109: PiP overlay layer. Last child = drawn on top of the playlist content.
|
||||
It lives INSIDE rootLayout, so the orientation rotation/translation applied to
|
||||
rootView covers it too — corner positions track the visible content. PipOverlay
|
||||
renders the overlay box into this (otherwise-transparent, empty) layer; the
|
||||
playlist renderers never touch it. -->
|
||||
<FrameLayout
|
||||
android:id="@+id/pipLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
|||
|
|
@ -179,6 +179,16 @@
|
|||
.wall-mode #playerContainer > iframe,
|
||||
.wall-mode #playerContainer > div > iframe { position: static !important; width: 100% !important; height: 100% !important; }
|
||||
|
||||
/* #109: PiP overlay layer — a fixed full-viewport layer ABOVE #playerContainer that
|
||||
the playlist never touches. The same orientation transform is applied to it as to
|
||||
#playerContainer, so corner positions track the visible content in every orientation.
|
||||
Pointer-transparent and empty (invisible) until an overlay is shown. */
|
||||
#pipContainer { position: fixed; inset: 0; pointer-events: none; z-index: 9000; }
|
||||
#pipContainer > .pip-box { position: absolute; overflow: hidden; box-sizing: border-box; box-shadow: 0 6px 28px rgba(0,0,0,0.55); }
|
||||
#pipContainer > .pip-box > .pip-title { font: 600 16px sans-serif; padding: 6px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#pipContainer > .pip-box > img,
|
||||
#pipContainer > .pip-box > iframe { display: block; width: 100%; border: 0; }
|
||||
|
||||
/* Status overlay */
|
||||
#statusOverlay {
|
||||
position: fixed; inset: 0; background: #000; display: flex; flex-direction: column;
|
||||
|
|
@ -210,6 +220,9 @@
|
|||
<!-- Player Container -->
|
||||
<div id="playerContainer" style="display:none"></div>
|
||||
|
||||
<!-- #109: PiP overlay layer (above the player; never touched by playlist render) -->
|
||||
<div id="pipContainer"></div>
|
||||
|
||||
<!-- Status Overlay -->
|
||||
<div id="statusOverlay" style="display:none">
|
||||
<div class="spinner"></div>
|
||||
|
|
@ -918,6 +931,88 @@
|
|||
if (data.type === 'screen_off') toggleScreenOff();
|
||||
if (data.type === 'screen_on') { document.getElementById('screenOffOverlay')?.remove(); }
|
||||
});
|
||||
|
||||
// #109: PiP overlay — a pushed floating layer above the playlist. The player
|
||||
// fetches uri itself (same trust model as remote_url content).
|
||||
socket.on('device:pip-show', (data) => pipShow(data));
|
||||
socket.on('device:pip-clear', (data) => pipClear(data && data.pip_id));
|
||||
}
|
||||
|
||||
// ==================== PiP overlay (#109) ====================
|
||||
// Single overlay slot, last-show-wins; duration timer (0 = until cleared); pip-clear
|
||||
// (id-aware) or timer tears down. Renders into #pipContainer, never the player. Mirrors
|
||||
// the Tizen PipOverlay (tizen/js/pip-overlay.js). Teardown is wrapped so a malformed
|
||||
// payload can't wedge the layer.
|
||||
let pipTimer = null, pipCurrent = null;
|
||||
const PIP_POS = {
|
||||
'top-left': { top: '4%', left: '4%' }, 'top-right': { top: '4%', right: '4%' },
|
||||
'bottom-left': { bottom: '4%', left: '4%' }, 'bottom-right': { bottom: '4%', right: '4%' },
|
||||
'center': { top: '50%', left: '50%', transform: 'translate(-50%,-50%)' },
|
||||
};
|
||||
const pipColor = (c) => (typeof c === 'string' && /^#[0-9A-Fa-f]{6}$/.test(c)) ? c : null;
|
||||
const pipPx = (v, d) => { const n = Number(v); return (isFinite(n) && n > 0 ? n : d) + 'px'; };
|
||||
function pipReport(level, msg) {
|
||||
try { if (socket?.connected && config.deviceId) socket.emit('device:log', { device_id: config.deviceId, tag: 'pip', level, message: msg }); } catch (e) {}
|
||||
}
|
||||
function pipTeardown() {
|
||||
try { if (pipTimer) clearTimeout(pipTimer); } catch (e) {}
|
||||
pipTimer = null; pipCurrent = null;
|
||||
const c = document.getElementById('pipContainer'); if (c) c.innerHTML = '';
|
||||
}
|
||||
function pipShow(p) {
|
||||
const container = document.getElementById('pipContainer');
|
||||
if (!p || !container) return;
|
||||
try {
|
||||
pipTeardown(); // single slot, last-show-wins
|
||||
const box = document.createElement('div');
|
||||
box.className = 'pip-box';
|
||||
box.style.width = pipPx(p.width, 480);
|
||||
box.style.height = pipPx(p.height, 360);
|
||||
box.style.background = pipColor(p.background_color) || '#000000';
|
||||
if (p.opacity != null && isFinite(Number(p.opacity))) box.style.opacity = String(Math.max(0, Math.min(1, Number(p.opacity))));
|
||||
if (p.border_radius != null && isFinite(Number(p.border_radius))) box.style.borderRadius = pipPx(p.border_radius, 0);
|
||||
const pos = PIP_POS[p.position] || PIP_POS['top-right'];
|
||||
Object.keys(pos).forEach((k) => { box.style[k] = pos[k]; });
|
||||
|
||||
const hasTitle = p.title != null && p.title !== '';
|
||||
if (hasTitle) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'pip-title';
|
||||
bar.textContent = String(p.title);
|
||||
bar.style.color = pipColor(p.title_color) || '#ffffff';
|
||||
bar.style.background = 'rgba(0,0,0,0.45)';
|
||||
box.appendChild(bar);
|
||||
}
|
||||
let media;
|
||||
if (p.type === 'web') {
|
||||
media = document.createElement('iframe');
|
||||
media.setAttribute('frameborder', '0');
|
||||
media.setAttribute('scrolling', 'no');
|
||||
media.setAttribute('allow', ''); // mute web audio by default (deny autoplay)
|
||||
media.src = p.uri;
|
||||
} else {
|
||||
media = document.createElement('img');
|
||||
media.src = p.uri;
|
||||
}
|
||||
media.style.height = hasTitle ? 'calc(100% - 32px)' : '100%';
|
||||
media.style.objectFit = 'cover';
|
||||
box.appendChild(media);
|
||||
container.appendChild(box);
|
||||
pipCurrent = p.pip_id || '(anon)';
|
||||
const dur = Number(p.duration);
|
||||
if (isFinite(dur) && dur > 0) pipTimer = setTimeout(() => pipClear(pipCurrent), dur * 1000);
|
||||
pipReport('info', `pip show ${p.type || '?'} ${p.pip_id || ''} pos=${p.position || 'top-right'} dur=${isFinite(dur) ? dur : 0}`);
|
||||
} catch (e) {
|
||||
pipTeardown();
|
||||
pipReport('warn', 'pip show failed: ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
function pipClear(pipId) {
|
||||
// A clear carrying a pip_id only clears if it matches the showing overlay.
|
||||
if (pipId && pipCurrent && pipId !== pipCurrent) return;
|
||||
const had = !!pipCurrent;
|
||||
pipTeardown();
|
||||
if (had) pipReport('info', 'pip cleared' + (pipId ? ' ' + pipId : ''));
|
||||
}
|
||||
|
||||
function register() {
|
||||
|
|
@ -1134,15 +1229,23 @@
|
|||
const newFp = fingerprint(newItems);
|
||||
const oldFp = fingerprint(playlist);
|
||||
|
||||
// Apply orientation
|
||||
// Apply orientation. #109: the PiP layer gets the SAME transform as the player so a
|
||||
// corner overlay tracks the visible content (not the physical panel) in every orientation.
|
||||
if (data.orientation) {
|
||||
const rotations = { 'landscape': '0deg', 'portrait': '90deg', 'landscape-flipped': '180deg', 'portrait-flipped': '270deg' };
|
||||
document.getElementById('playerContainer').style.transform = `rotate(${rotations[data.orientation] || '0deg'})`;
|
||||
if (data.orientation.includes('portrait')) {
|
||||
document.getElementById('playerContainer').style.transformOrigin = 'center center';
|
||||
document.getElementById('playerContainer').style.width = '100vh';
|
||||
document.getElementById('playerContainer').style.height = '100vw';
|
||||
const portrait = data.orientation.includes('portrait');
|
||||
[document.getElementById('playerContainer'), document.getElementById('pipContainer')].forEach((el) => {
|
||||
if (!el) return;
|
||||
el.style.transform = `rotate(${rotations[data.orientation] || '0deg'})`;
|
||||
if (portrait) {
|
||||
el.style.transformOrigin = 'center center';
|
||||
el.style.width = '100vh';
|
||||
el.style.height = '100vw';
|
||||
} else {
|
||||
el.style.width = '';
|
||||
el.style.height = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply (or clear) wall mode. Force re-render when wall config changes
|
||||
|
|
|
|||
Loading…
Reference in a new issue