- nginx: add /api/ shortcut block (no portal auth, X-Script-Name /digiserver, Host $http_host) so players can reach DigiServer API without /digiserver prefix - nginx: use $http_host in /api/ block so Flask host_url includes port — fixes media download URLs missing :8080 (was http://ip/digiserver/... not http://ip:8080/...) - player main.py: fix double-port bug when server_ip already contains a port (e.g. 192.168.0.230:8080 was producing http://192.168.0.230:8080:80) - get_playlists_v2.py: force re-sync when server version differs OR local media files are missing on disk — fixes stale playlist after server reset - digiserver api.py: playlist endpoint builds full media URLs using script_root from X-Script-Name header set by nginx - weblink support, player build/deploy improvements, manage-playlist AJAX prefix fix
9.4 KiB
Web Link Playlist Items — Player Integration Guide
This document describes the changes required on the Kiwy-Signage player
(https://gitea.moto-adv.com/ske087/Kiwy-Signage.git) to support a new
playlist item type: weblink (display a live web page / URL instead of an
uploaded media file).
The DigiServer (this repo,
digiserver-v2) side is now implemented: it stores web links (content_type='weblink', page URL incontent.url) and emitsweblinkitems fromGET /api/playlists. The player does not yet support them — use this guide to implement the player side.
1. Background — how items flow today
DigiServer API ──JSON──▶ player sync (get_playlists_v2.py) ──▶ playlist.json ──▶ main.py renders
/api/playlists downloads files to media/ by file extension
Each playlist item the server returns currently looks like:
{
"id": 42,
"file_name": "promo.jpg",
"type": "image",
"duration": 10,
"position": 1,
"url": "https://server/digiserver/static/uploads/promo.jpg",
"edit_on_player": false
}
The player:
- Syncs (
src/get_playlists_v2.py→download_media_files()): downloadsurlinto the localmedia/directory, then rewrites each item keeping onlyfile_name,url(now a local relative path),duration,edit_on_player. Note: thetypefield is currently discarded here. - Renders (
src/main.py→play_current_media()): opens the local file and chooses a Kivy widget purely by file extension (.mp4/.avi/...→Video,.jpg/.png/...→AsyncImage). Unknown extensions are skipped as "unsupported".
A web link breaks all three assumptions: there is no file to download, no extension to switch on, and no widget that renders a web page.
2. New server contract (what DigiServer will send)
A web-link playlist item will look like this:
{
"id": 91,
"file_name": "weblink-3f9c1a2b",
"type": "weblink",
"duration": 30,
"position": 4,
"url": "https://example.com/dashboard",
"edit_on_player": false
}
Key differences vs. a file item:
| Field | File item | Web-link item |
|---|---|---|
type |
image / video |
weblink |
url |
Path to a file on the server | The web page to display (the link) |
file_name |
Real filename on disk | Synthetic id (weblink-<uuid>), no file exists |
The player must branch on type == "weblink" and treat url as the page to
open — never try to download it as a file.
3. Required player changes
3.1 Sync step — src/get_playlists_v2.py
Function: download_media_files(playlist, media_dir, ...)
- Skip download for web links. At the top of the per-item loop, detect
media.get('type') == 'weblink'and do not callsession.get()/ write any file for it. - Preserve
typeand the originalurl. Theupdated_mediadict that is appended toupdated_playlistcurrently dropstypeand rewritesurlto a local path. It must now carrytypethrough, and for web links keepurlas the original web address (do not convert to a local relative path).
Suggested shape of the per-item logic:
item_type = media.get('type', '')
if item_type == 'weblink':
# No file to download — pass the web link through unchanged.
updated_playlist.append({
'file_name': media.get('file_name', ''),
'type': 'weblink',
'url': media.get('url', ''), # the actual web page
'duration': media.get('duration', 10),
'edit_on_player': False,
})
continue
# ... existing download logic for file items ...
updated_playlist.append({
'file_name': file_name,
'type': item_type, # <-- now preserved
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
'duration': duration,
'edit_on_player': media.get('edit_on_player', False),
})
delete_unused_media()walksmedia/usingfile_name. Web links have no file, so they simply won't match anything on disk — no change strictly required, but make sure a missing local file for aweblinkitem does not trigger a re-download or an error elsewhere.
3.2 Render step — src/main.py
Function: play_current_media(self, force_reload=False)
The current logic builds media_path = os.path.join(self.media_dir, file_name)
and then does os.stat(media_path) — which will fail for a web link (no file).
Add a web-link branch before the file-existence check:
media_item = self.playlist[self.current_index]
file_name = media_item.get('file_name', '')
duration = media_item.get('duration', 10)
# NEW: handle web links before any file/path handling
if media_item.get('type') == 'weblink':
self.play_weblink(media_item.get('url', ''), duration)
return
# ... existing file existence check + extension branching ...
Then add a new method play_weblink(self, url, duration):
- Validate the scheme is
http/https(reject anything else, e.g.file://). - Open the page for
durationseconds, then advance withself.next_media(). - Wrap in
try/except; on failure incrementself.consecutive_errorsand callself.next_media(), matching the existing error-handling pattern. - Make sure the previous widget (
self.current_widget) is removed/stopped just like the image/video paths do.
Rendering approach (pick one)
Kivy has no production-grade embedded web view, especially on Raspberry Pi. Recommended options, in order of robustness:
-
Chromium kiosk overlay (recommended). Launch Chromium over the Kivy window for the item's duration, then close it and return to Kivy:
import subprocess, shutil from urllib.parse import urlparse from kivy.clock import Clock def play_weblink(self, url, duration): scheme = urlparse(url).scheme.lower() if scheme not in ('http', 'https'): Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}") self.next_media() return try: browser = shutil.which('chromium-browser') or shutil.which('chromium') self._weblink_proc = subprocess.Popen([ browser, '--kiosk', '--app=' + url, '--noerrdialogs', '--disable-infobars', '--incognito', '--no-first-run', '--check-for-update-interval=31536000', ]) Clock.schedule_once(lambda dt: self._close_weblink_and_next(), duration) except Exception as e: Logger.error(f"SignagePlayer: Error opening weblink: {e}") self.consecutive_errors += 1 self.next_media() def _close_weblink_and_next(self): proc = getattr(self, '_weblink_proc', None) if proc and proc.poll() is None: proc.terminate() try: proc.wait(timeout=5) except Exception: proc.kill() self._weblink_proc = None self.next_media()Requirements / notes:
- Install Chromium on the player image (
chromium-browseron Raspberry Pi OS). - Ensure Chromium gets window focus over Kivy and is fully killed before the
next item, including on pause/stop/restart paths and on app shutdown
(
on_stop) so no stray browser window is left behind. - On Wayland/X11 the player already sets
SDL_VIDEODRIVER; verify Chromium launches on the same display/session.
- Install Chromium on the player image (
-
Embedded web view widget (
kivy_garden.webview, WebKit/GTK, or WebView2). Cleaner UX (stays inside the Kivy widget tree) but fragile and poorly supported on Pi/Wayland — only pursue if option 1 is unacceptable. -
Server-side screenshot fallback (no player change). If embedding a live browser is not desirable, DigiServer can periodically screenshot the URL and store it as a normal
imageitem; the player then needs no changes. This loses live/animated content. Documented here for completeness only.
4. Checklist for the player update
get_playlists_v2.py: skip download whentype == 'weblink'.get_playlists_v2.py: preservetypein the rewritten playlist items (fixes the current loss oftype).get_playlists_v2.py: keep the original weburlfor weblink items.main.pyplay_current_media(): branch toplay_weblink()before theos.stat()file check.main.py: implementplay_weblink(url, duration)(Chromium kiosk).main.py: validate scheme ishttp/https; reject others.- Kill/cleanup the browser process on next item, pause, stop, restart, and
on_stop. - Install Chromium on the player image / document it in the player README.
- Test: mixed playlist (image → video → weblink → image) cycles correctly
and respects per-item
duration.
5. Security notes
- Only allow
http/httpsschemes on both server and player; never openfile://,chrome://, etc. - The server validates and stores the URL when the operator adds it; the player should still re-validate the scheme before launching the browser (defence in depth).
- Consider running Chromium with
--incognito(no persistent cookies/cache) as shown above.