Files
ske087 4f4e017ad2 fix: player connectivity and media download pipeline
- 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
2026-06-29 20:04:11 +03:00

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 in content.url) and emits weblink items from GET /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:

  1. Syncs (src/get_playlists_v2.pydownload_media_files()): downloads url into the local media/ directory, then rewrites each item keeping only file_name, url (now a local relative path), duration, edit_on_player. Note: the type field is currently discarded here.
  2. Renders (src/main.pyplay_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, ...)

  1. Skip download for web links. At the top of the per-item loop, detect media.get('type') == 'weblink' and do not call session.get() / write any file for it.
  2. Preserve type and the original url. The updated_media dict that is appended to updated_playlist currently drops type and rewrites url to a local path. It must now carry type through, and for web links keep url as 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),
})
  1. delete_unused_media() walks media/ using file_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 a weblink item 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 duration seconds, then advance with self.next_media().
  • Wrap in try/except; on failure increment self.consecutive_errors and call self.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:

  1. 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-browser on 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.
  2. 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.

  3. 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 image item; 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 when type == 'weblink'.
  • get_playlists_v2.py: preserve type in the rewritten playlist items (fixes the current loss of type).
  • get_playlists_v2.py: keep the original web url for weblink items.
  • main.py play_current_media(): branch to play_weblink() before the os.stat() file check.
  • main.py: implement play_weblink(url, duration) (Chromium kiosk).
  • main.py: validate scheme is http/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/https schemes on both server and player; never open file://, 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.