Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f30f97683b | |||
| 609d9169d2 |
+1
-1
@@ -1 +1 @@
|
||||
1768682062.8464386
|
||||
1782752485.750587
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"server_ip": "192.168.0.121",
|
||||
"port": "443",
|
||||
"screen_name": "rpi-tvcanba1",
|
||||
"server_ip": "192.168.0.159",
|
||||
"port": "8080",
|
||||
"screen_name": "rpi-Receptie",
|
||||
"quickconnect_key": "8887779",
|
||||
"orientation": "Landscape",
|
||||
"touch": "True",
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true,
|
||||
"use_https": true,
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"hostname": "rpi-tvcanba1",
|
||||
"auth_code": "LhfERILw4cFxejhbIUuQ72QddisRgHMAm7kUSty64LA",
|
||||
"player_id": 1,
|
||||
"player_name": "TVacasa",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "https://192.168.0.121:443"
|
||||
}
|
||||
@@ -1,27 +1,23 @@
|
||||
{
|
||||
"count": 3,
|
||||
"count": 2,
|
||||
"player_id": 1,
|
||||
"player_name": "TVacasa",
|
||||
"player_name": "Receptie TV",
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "2026efvev-1428673176.jpg",
|
||||
"url": "media/2026efvev-1428673176.jpg",
|
||||
"duration": 50,
|
||||
"edit_on_player": true
|
||||
"file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
|
||||
"type": "image",
|
||||
"url": "media/sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
|
||||
"duration": 25,
|
||||
"edit_on_player": false
|
||||
},
|
||||
{
|
||||
"file_name": "4k1.jpg",
|
||||
"url": "media/4k1.jpg",
|
||||
"duration": 30,
|
||||
"edit_on_player": true
|
||||
},
|
||||
{
|
||||
"file_name": "1416529-hd_1920_1080_30fps.mp4",
|
||||
"url": "media/1416529-hd_1920_1080_30fps.mp4",
|
||||
"duration": 13,
|
||||
"file_name": "robert-lukeman-zNN6ubHmruI-unsplash.jpg",
|
||||
"type": "image",
|
||||
"url": "media/robert-lukeman-zNN6ubHmruI-unsplash.jpg",
|
||||
"duration": 20,
|
||||
"edit_on_player": false
|
||||
}
|
||||
],
|
||||
"playlist_id": 1,
|
||||
"playlist_version": 34
|
||||
"playlist_version": 15
|
||||
}
|
||||
@@ -74,7 +74,7 @@ def ensure_authenticated(config):
|
||||
# Use HTTPS for IP addresses
|
||||
server_url = f'https://{server_ip}:{port}' if port else f'https://{server_ip}'
|
||||
else:
|
||||
server_url = f'http://{server_ip}:{port}'
|
||||
server_url = f'http://{server_ip}:{port}' if port else f'http://{server_ip}'
|
||||
else:
|
||||
# For domain names, use HTTPS by default
|
||||
if use_https:
|
||||
|
||||
+328
-11
@@ -3,6 +3,8 @@ Kivy Signage Player - Main Application
|
||||
Displays content from DigiServer playlists using Kivy framework
|
||||
"""
|
||||
|
||||
PLAYER_VERSION = "1.2.0"
|
||||
|
||||
import os
|
||||
import json
|
||||
import platform
|
||||
@@ -640,6 +642,7 @@ class SettingsPopup(Popup):
|
||||
|
||||
# Populate current values
|
||||
self.ids.server_input.text = self.player.config.get('server_ip', 'localhost')
|
||||
self.ids.port_input.text = str(self.player.config.get('port', ''))
|
||||
self.ids.screen_input.text = self.player.config.get('screen_name', 'kivy-player')
|
||||
self.ids.quickconnect_input.text = self.player.config.get('quickconnect_key', '1234567')
|
||||
self.ids.orientation_input.text = self.player.config.get('orientation', 'Landscape')
|
||||
@@ -723,7 +726,7 @@ class SettingsPopup(Popup):
|
||||
server_ip = self.ids.server_input.text.strip()
|
||||
screen_name = self.ids.screen_input.text.strip()
|
||||
quickconnect = self.ids.quickconnect_input.text.strip()
|
||||
port = self.player.config.get('port', '443')
|
||||
port = self.ids.port_input.text.strip() or self.player.config.get('port', '')
|
||||
use_https = self.player.config.get('use_https', True)
|
||||
verify_ssl = self.player.config.get('verify_ssl', True)
|
||||
|
||||
@@ -741,7 +744,11 @@ class SettingsPopup(Popup):
|
||||
server_url = f"{server_ip}:{port}"
|
||||
else:
|
||||
protocol = "https" if use_https else "http"
|
||||
server_url = f"{protocol}://{server_ip}:{port}"
|
||||
if ':' in server_ip:
|
||||
# server_ip already contains a port (e.g. 192.168.0.230:8080)
|
||||
server_url = f"{protocol}://{server_ip}"
|
||||
else:
|
||||
server_url = f"{protocol}://{server_ip}:{port}" if port else f"{protocol}://{server_ip}"
|
||||
|
||||
Logger.info(f"SettingsPopup: Testing connection to {server_url} (HTTPS: {use_https}, Verify SSL: {verify_ssl})")
|
||||
|
||||
@@ -875,6 +882,7 @@ class SettingsPopup(Popup):
|
||||
"""Save configuration and close popup"""
|
||||
# Update config
|
||||
self.player.config['server_ip'] = self.ids.server_input.text
|
||||
self.player.config['port'] = self.ids.port_input.text.strip()
|
||||
self.player.config['screen_name'] = self.ids.screen_input.text
|
||||
self.player.config['quickconnect_key'] = self.ids.quickconnect_input.text
|
||||
self.player.config['orientation'] = self.ids.orientation_input.text
|
||||
@@ -913,6 +921,9 @@ class SignagePlayer(Widget):
|
||||
self.current_index = 0
|
||||
self.current_widget = None
|
||||
self._weblink_proc = None # Handle to the Chromium kiosk process for weblink items
|
||||
self._weblink_preload_proc = None # Hidden Chromium pre-warming the next weblink URL
|
||||
self._watchdog_stop = None # threading.Event to stop the inactivity watchdog
|
||||
self._weblink_watchdog_thread = None # Background thread monitoring touch inactivity
|
||||
self.is_playing = False
|
||||
self.is_paused = False
|
||||
self.auto_resume_event = None # Track scheduled auto-resume
|
||||
@@ -1271,11 +1282,13 @@ class SignagePlayer(Widget):
|
||||
self.current_index = 0
|
||||
self.play_current_media()
|
||||
|
||||
def play_current_media(self, force_reload=False):
|
||||
def play_current_media(self, force_reload=False, _after_weblink=False):
|
||||
"""Play the current media item
|
||||
|
||||
Args:
|
||||
force_reload: If True, clears image cache before loading (for edited images)
|
||||
_after_weblink: Internal flag — True when called after the weblink
|
||||
dismiss delay so we skip the delay logic a second time.
|
||||
"""
|
||||
# Don't play if paused (unless we're explicitly resuming)
|
||||
if self.is_paused:
|
||||
@@ -1294,14 +1307,72 @@ class SignagePlayer(Widget):
|
||||
|
||||
Logger.info(f"SignagePlayer: Playing item {self.current_index + 1}/{len(self.playlist)}: {file_name} ({duration}s)")
|
||||
|
||||
# Close any kiosk browser left over from a previous web-link item
|
||||
self._kill_weblink_process()
|
||||
# ── Weblink → media transition (Wayland-safe) ──────────────────
|
||||
# On Wayland (Labwc) Window.raise_window() is a no-op, so we must
|
||||
# ensure Chromium's fullscreen window is fully gone BEFORE we try
|
||||
# to render Kivy content. We:
|
||||
# 1. Stop watchdog + preload immediately
|
||||
# 2. Terminate Chromium (non-blocking)
|
||||
# 3. Hide the Kivy content_area so nothing stale is visible
|
||||
# 4. Wait 200 ms for the compositor to remove Chromium's window
|
||||
# 5. Show content_area + re-call play_current_media to render
|
||||
if not _after_weblink:
|
||||
proc = getattr(self, '_weblink_proc', None)
|
||||
if proc is not None:
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
self._weblink_proc = None
|
||||
|
||||
# Hide content while Chromium is still closing
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Terminate Chromium
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception as exc:
|
||||
Logger.warning(f"SignagePlayer: weblink terminate: {exc}")
|
||||
|
||||
def _resume(dt):
|
||||
# Force-kill if still alive after the delay
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# Restore content area and show Kivy window
|
||||
try:
|
||||
self.ids.content_area.opacity = 1
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
Window.show()
|
||||
Window.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
# Now render the actual media
|
||||
self.play_current_media(
|
||||
force_reload=force_reload, _after_weblink=True
|
||||
)
|
||||
|
||||
# 200 ms gives Labwc time to remove the fullscreen surface
|
||||
Clock.schedule_once(_resume, 0.2)
|
||||
return
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
|
||||
# Handle web links before any file/path handling (no local file exists)
|
||||
if media_item.get('type') == 'weblink':
|
||||
Logger.debug("SignagePlayer: Media type: WEBLINK")
|
||||
self.ids.status_label.opacity = 0
|
||||
self._remove_current_widget()
|
||||
# Hide content_area — Chromium will cover it; avoids stale frame
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
except Exception:
|
||||
pass
|
||||
started = self.play_weblink(media_item.get('url', ''), duration)
|
||||
if started:
|
||||
self.consecutive_errors = 0
|
||||
@@ -1532,10 +1603,19 @@ class SignagePlayer(Widget):
|
||||
return False
|
||||
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Opening weblink in kiosk browser for {duration}s: {url}")
|
||||
Logger.info(f"SignagePlayer: Opening weblink in kiosk browser: {url}")
|
||||
Logger.info(f"SignagePlayer: Inactivity timeout set to {duration}s (touch resets the countdown)")
|
||||
# Kill the hidden pre-warm instance first; its work (binary in RAM,
|
||||
# page in disk cache) makes the kiosk relaunch below near-instant.
|
||||
self._kill_weblink_preload()
|
||||
self._weblink_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--kiosk',
|
||||
# Use --start-fullscreen instead of --kiosk.
|
||||
# --kiosk requests Wayland's exclusive-fullscreen protocol which
|
||||
# prevents Labwc from restoring the previous window on close.
|
||||
# --start-fullscreen is a normal maximised window that the
|
||||
# compositor can un-stack without issues.
|
||||
'--start-fullscreen',
|
||||
'--app=' + url,
|
||||
'--noerrdialogs',
|
||||
'--disable-infobars',
|
||||
@@ -1543,25 +1623,184 @@ class SignagePlayer(Widget):
|
||||
'--no-first-run',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--check-for-update-interval=31536000',
|
||||
# Suppress the GNOME keyring / wallet unlock popup
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
# Prevent any other credential / sync dialogs
|
||||
'--disable-sync',
|
||||
'--disable-background-networking',
|
||||
'--no-default-browser-check',
|
||||
'--window-position=0,0',
|
||||
])
|
||||
|
||||
# Advance after the configured duration. The kiosk browser is closed
|
||||
# at the start of the next play_current_media() via _kill_weblink_process().
|
||||
# Unschedule any previous fixed timer — the watchdog thread takes
|
||||
# over and only advances after 'duration' seconds of NO touch activity.
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, duration)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
|
||||
# Preload the next image so the transition after the weblink is smooth.
|
||||
self.preload_next_media()
|
||||
return True
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error opening weblink {url}: {e}")
|
||||
# Restore content area if launch failed
|
||||
try:
|
||||
self.ids.content_area.opacity = 1
|
||||
except Exception:
|
||||
pass
|
||||
self.consecutive_errors += 1
|
||||
self._weblink_proc = None
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
def _start_inactivity_watchdog(self, duration):
|
||||
"""Start a background thread that monitors /dev/input/* for touch/key
|
||||
activity. The thread resets the idle counter on every event; when the
|
||||
screen has been idle for *duration* seconds it fires next_media().
|
||||
Works even while Chromium owns the display (raw device reads bypass the
|
||||
window-manager focus).
|
||||
Also fires immediately if Chromium exits on its own (e.g. user closes it).
|
||||
"""
|
||||
import glob
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
|
||||
# Stop any previous watchdog cleanly before starting a new one.
|
||||
self._stop_inactivity_watchdog()
|
||||
|
||||
stop_event = threading.Event()
|
||||
self._watchdog_stop = stop_event
|
||||
weblink_proc = self._weblink_proc # snapshot so thread sees the right process
|
||||
|
||||
def watchdog():
|
||||
# Open every available input event device (touchscreen, mouse, kbd).
|
||||
devices = []
|
||||
for path in sorted(glob.glob('/dev/input/event*')):
|
||||
try:
|
||||
devices.append(open(path, 'rb')) # noqa: WPS515
|
||||
except (PermissionError, OSError) as exc:
|
||||
Logger.debug(f"SignagePlayer: Watchdog cannot open {path}: {exc}")
|
||||
|
||||
if not devices:
|
||||
# No input devices accessible → fall back to a plain fixed timer.
|
||||
Logger.warning(
|
||||
"SignagePlayer: Watchdog — no /dev/input devices accessible; "
|
||||
"falling back to fixed timer"
|
||||
)
|
||||
stop_event.wait(timeout=duration)
|
||||
if not stop_event.is_set():
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
return
|
||||
|
||||
Logger.info(
|
||||
f"SignagePlayer: Watchdog watching {len(devices)} input device(s), "
|
||||
f"idle threshold = {duration}s"
|
||||
)
|
||||
last_activity = time.monotonic()
|
||||
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
# If Chromium exited on its own (user closed it), advance immediately.
|
||||
if weblink_proc is not None and weblink_proc.poll() is not None:
|
||||
Logger.info(
|
||||
"SignagePlayer: Chromium exited — advancing to next media"
|
||||
)
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
break
|
||||
|
||||
idle = time.monotonic() - last_activity
|
||||
if idle >= duration:
|
||||
Logger.info(
|
||||
f"SignagePlayer: No touch for {duration}s — advancing to next media"
|
||||
)
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
break
|
||||
|
||||
# Wait up to 0.5 s for any raw input event.
|
||||
timeout = min(0.5, duration - idle)
|
||||
readable, _, _ = select.select(devices, [], [], timeout)
|
||||
|
||||
if readable:
|
||||
# Drain the data so the buffer doesn't fill up.
|
||||
for fd in readable:
|
||||
try:
|
||||
fd.read(24) # struct input_event = 24 bytes on 64-bit Linux
|
||||
except OSError:
|
||||
pass
|
||||
last_activity = time.monotonic()
|
||||
Logger.debug(
|
||||
"SignagePlayer: Touch/key detected — inactivity timer reset"
|
||||
)
|
||||
finally:
|
||||
for fd in devices:
|
||||
try:
|
||||
fd.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._weblink_watchdog_thread = threading.Thread(
|
||||
target=watchdog, daemon=True, name='weblink-watchdog'
|
||||
)
|
||||
self._weblink_watchdog_thread.start()
|
||||
|
||||
def _stop_inactivity_watchdog(self):
|
||||
"""Signal the watchdog thread to exit without firing next_media."""
|
||||
stop_event = getattr(self, '_watchdog_stop', None)
|
||||
if stop_event is not None:
|
||||
stop_event.set()
|
||||
self._watchdog_stop = None
|
||||
self._weblink_watchdog_thread = None
|
||||
|
||||
def _kill_weblink_after_frame(self):
|
||||
"""Gracefully transition away from a weblink item.
|
||||
|
||||
Stops the watchdog and preload immediately (so nothing fires a spurious
|
||||
next_media), clears self._weblink_proc so the slot is free for the next
|
||||
item, then schedules the actual Chromium termination for the *next Kivy
|
||||
frame*. By that time Kivy has already rendered the new media widget
|
||||
underneath Chromium, so when the browser window disappears the player
|
||||
content is instantly visible — no black flash.
|
||||
"""
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
|
||||
proc = self._weblink_proc # snapshot
|
||||
self._weblink_proc = None # free the slot right away
|
||||
|
||||
if proc is None or proc.poll() is not None:
|
||||
# Nothing to kill — still raise the window in case it got buried.
|
||||
try:
|
||||
Window.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
def _do_kill(dt):
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
Logger.debug("SignagePlayer: Closed weblink kiosk browser (deferred)")
|
||||
except Exception as exc:
|
||||
Logger.warning(f"SignagePlayer: Error in deferred weblink kill: {exc}")
|
||||
# Bring Kivy window to front now that Chromium is gone.
|
||||
try:
|
||||
Window.raise_window()
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: raise_window failed (non-fatal): {exc}")
|
||||
|
||||
Clock.schedule_once(_do_kill, 0)
|
||||
|
||||
def _kill_weblink_process(self):
|
||||
"""Terminate the kiosk browser process if one is running."""
|
||||
"""Terminate the kiosk browser, inactivity watchdog, and any pre-warm process."""
|
||||
# Stop watchdog first so it cannot fire next_media after we've moved on.
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
|
||||
proc = getattr(self, '_weblink_proc', None)
|
||||
if proc is not None and proc.poll() is None:
|
||||
try:
|
||||
@@ -1575,6 +1814,14 @@ class SignagePlayer(Widget):
|
||||
Logger.warning(f"SignagePlayer: Error closing weblink browser: {e}")
|
||||
self._weblink_proc = None
|
||||
|
||||
# Raise the Kivy window back to the front — when Chromium ran in kiosk
|
||||
# mode it covered the Kivy window entirely; the window manager won't
|
||||
# automatically bring it back on all compositors/WMs.
|
||||
try:
|
||||
Window.raise_window()
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: raise_window failed (non-fatal): {exc}")
|
||||
|
||||
def _skip_to_next_media(self):
|
||||
"""Advance past a failed item WITHOUT recursing.
|
||||
|
||||
@@ -1623,6 +1870,71 @@ class SignagePlayer(Widget):
|
||||
Clock.unschedule(self.next_media)
|
||||
self.play_current_media()
|
||||
|
||||
def _prewarm_weblink(self, url):
|
||||
"""Launch Chromium off-screen to warm up the binary and page cache.
|
||||
|
||||
The window is placed far outside the visible area so the user never
|
||||
sees it. When play_weblink() fires for real it kills this hidden
|
||||
instance first, then relaunches in kiosk mode. Because the Chromium
|
||||
binary is already resident in RAM and the page is in the disk cache,
|
||||
the visible kiosk window appears almost immediately.
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
from urllib.parse import urlparse
|
||||
|
||||
if not url:
|
||||
return
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
return
|
||||
|
||||
browser = shutil.which('chromium-browser') or shutil.which('chromium')
|
||||
if not browser:
|
||||
return
|
||||
|
||||
# Kill any stale preload first.
|
||||
self._kill_weblink_preload()
|
||||
|
||||
try:
|
||||
Logger.debug(f"SignagePlayer: Pre-warming weblink off-screen: {url}")
|
||||
self._weblink_preload_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--app=' + url,
|
||||
# Place window completely outside the visible display area.
|
||||
'--window-position=-9999,-9999',
|
||||
'--window-size=1920,1080', # pre-render at full resolution
|
||||
'--noerrdialogs',
|
||||
'--disable-infobars',
|
||||
'--incognito',
|
||||
'--no-first-run',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--check-for-update-interval=31536000',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--disable-sync',
|
||||
'--disable-background-networking',
|
||||
'--no-default-browser-check',
|
||||
])
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: Pre-warm launch failed (non-fatal): {exc}")
|
||||
self._weblink_preload_proc = None
|
||||
|
||||
def _kill_weblink_preload(self):
|
||||
"""Terminate the hidden pre-warm Chromium process if running."""
|
||||
proc = getattr(self, '_weblink_preload_proc', None)
|
||||
if proc is not None and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
Logger.debug("SignagePlayer: Killed weblink pre-warm process")
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: Error killing pre-warm process: {exc}")
|
||||
self._weblink_preload_proc = None
|
||||
|
||||
def preload_next_media(self):
|
||||
"""Preload the next media item asynchronously to improve transition smoothness
|
||||
|
||||
@@ -1662,6 +1974,11 @@ class SignagePlayer(Widget):
|
||||
Logger.debug(f"SignagePlayer: Preloading next image: {file_name}")
|
||||
Loader.image(media_path)
|
||||
|
||||
elif next_media_item.get('type') == 'weblink':
|
||||
# Pre-warm Chromium in a hidden off-screen window so the binary
|
||||
# and page content are in OS cache before the slot arrives.
|
||||
self._prewarm_weblink(next_media_item.get('url', ''))
|
||||
|
||||
except Exception as e:
|
||||
Logger.debug(f"SignagePlayer: Error preloading next media: {e}")
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"hostname": "rpi-tvcanba1",
|
||||
"auth_code": "LhfERILw4cFxejhbIUuQ72QddisRgHMAm7kUSty64LA",
|
||||
"hostname": "rpi-Receptie",
|
||||
"auth_code": "BrJrGzX_IT9oP_Lfke8Qgo4sDjMc49T1EhSnWmKKpDM",
|
||||
"player_id": 1,
|
||||
"player_name": "TVacasa",
|
||||
"player_name": "Receptie TV",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "https://192.168.0.121:443"
|
||||
"server_url": "http://192.168.0.159:8080"
|
||||
}
|
||||
@@ -382,6 +382,30 @@
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Server port
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Port:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: port_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
hint_text: '80 or 8080 (leave empty for default)'
|
||||
input_filter: 'int'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Screen name
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
|
||||
Reference in New Issue
Block a user