# Web Link Playlist Items — Player Integration Guide This document describes the changes required on the **Kiwy-Signage player** () 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: ```json { "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.py` → `download_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.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: ```json { "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-`), **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: ```python 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), }) ``` 3. **`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**: ```python 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: ```python 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.