Add unified WeblinkSession controller and interaction-driven playback
Web links were implemented three times (main.py subprocess, run_win.py subprocess + Win32 overlay, cef_browser.py embedded CEF), each owning its own process handle, watchdog and teardown. That ambiguity caused leaked browsers, skipped items, lost foreground and blank screens when a page failed to load. Replace all three with a single owner in src/weblink_session.py: - WeblinkSession: validate -> launch -> verify -> watch -> teardown. Generation-tokened so stale callbacks are ignored, idempotent close(), atexit-safe, never more than one browser alive. - WeblinkAdapter: the only platform-specific part (launch / wait_visible / is_alive / teardown / prewarm). Platform layers inject engines through SignagePlayer.weblink_adapter_factory. - ChromiumSubprocessAdapter: default engine (Pi chromium, Windows chrome/msedge). - InteractionWatcher: decides when an item is finished. - WebInputSources: /dev/input/event* (Linux) plus a GetCursorPos pointer tap (needed on Windows for embedded CEF, which has no child process). Interaction model: web links are an interactive surface, not timed media. The player advances only when the configured duration has elapsed AND the viewer has not interacted for 10s, measured from the most recent interaction. A touch in the final seconds of a slot therefore pushes the advance 10s past that touch, and each further touch pushes it again, so a page is never pulled out from under someone using it. An untouched page still advances on schedule. A drag burst counts as one interaction but the countdown tracks its last event, so an item cannot be cut off mid-gesture. max_dwell (duration x factor, floored by min_max_dwell) is an absolute backstop against a wedged browser or a jammed touchscreen. Verified start-up: the visibility wait runs on the watcher thread, never on Kivy's main thread. If the browser window never appears the item is reported failed and skipped, instead of resetting the error counter and leaving a black screen up for the whole duration. Config: new "weblink" block in config/app_config.json (engine, interaction_postpone, interaction_debounce, interaction_grace, max_dwell_factor, min_max_dwell, launch_timeout, prewarm) with safe defaults, so an absent block still works. Also add weblink_session to the PyInstaller hiddenimports so the frozen exe bundles the new module.
This commit is contained in:
@@ -1,21 +1,22 @@
|
||||
# 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).
|
||||
This document describes how the **Kiwy-Signage player**
|
||||
(<https://gitea.moto-adv.com/ske087/Kiwy-Signage.git>) supports the **`weblink`**
|
||||
playlist item type (display a live web page / URL instead of an uploaded media
|
||||
file).
|
||||
|
||||
> The DigiServer (this repo, `digiserver-v2`) side will be updated to emit
|
||||
> `weblink` items in the playlist API. The player does **not** yet support them.
|
||||
> Use this guide to implement the player side later.
|
||||
> **Status: implemented.** The player supports `weblink` items on both
|
||||
> Raspberry Pi (`chromium` subprocess) and Windows (embedded CEF with a
|
||||
> Chrome/Edge subprocess fallback). Sections 1–4 describe the original design
|
||||
> plan; section 6 documents the shipped architecture and the interaction model.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background — how items flow today
|
||||
## 1. Background — how items flow
|
||||
|
||||
```
|
||||
DigiServer API ──JSON──▶ player sync (get_playlists_v2.py) ──▶ playlist.json ──▶ main.py renders
|
||||
/api/playlists downloads files to media/ by file extension
|
||||
/api/playlists downloads files to media/ by item type
|
||||
```
|
||||
|
||||
Each playlist item the server returns currently looks like:
|
||||
@@ -243,3 +244,98 @@ Recommended options, in order of robustness:
|
||||
depth).
|
||||
- Consider running Chromium with `--incognito` (no persistent cookies/cache) as
|
||||
shown above.
|
||||
|
||||
---
|
||||
|
||||
## 6. Shipped architecture (`src/weblink_session.py`)
|
||||
|
||||
The player-side implementation lives in **one** module, so launch, verification,
|
||||
timing and teardown have a single owner instead of being duplicated per
|
||||
platform:
|
||||
|
||||
| Piece | Responsibility |
|
||||
|--------------------------|----------------|
|
||||
| `WeblinkSession` | Owns one weblink item: validate → launch → verify → watch → teardown. Generation-tokened so stale callbacks are ignored, and `atexit`-safe. |
|
||||
| `WeblinkAdapter` | The only platform-specific part: launch / wait for the window / is it alive / tear it down / pre-warm. |
|
||||
| `ChromiumSubprocessAdapter` | Default engine (Raspberry Pi `chromium`, Windows `chrome.exe`/`msedge.exe`). |
|
||||
| `InteractionWatcher` | Decides when the item is finished (see the interaction model below). |
|
||||
| `WebInputSources` | Reads `/dev/input/event*` (Linux) and does a pointer-position tap (Windows, needed for embedded CEF). |
|
||||
|
||||
Platform wrappers inject their engines through
|
||||
`SignagePlayer.weblink_adapter_factory`:
|
||||
|
||||
* **Raspberry Pi / Linux** — built-in Chromium subprocess adapter.
|
||||
* **Windows** (`windows/run_win.py`) — embedded CEF first (`cef_browser.py`,
|
||||
renders inside the Kivy window: no z-order fights, no subprocess), then the
|
||||
Chrome/Edge subprocess adapter as fallback.
|
||||
|
||||
### 6.1 Interaction model — web links are not passive media
|
||||
|
||||
`duration` on a weblink is **not** a hard cut-off. The player advances only when
|
||||
**both** conditions are true:
|
||||
|
||||
1. the configured `duration` has elapsed; **and**
|
||||
2. the viewer has not interacted with the page for `interaction_postpone`
|
||||
seconds (default **10 s**), measured from the **most recent** interaction.
|
||||
|
||||
Consequences:
|
||||
|
||||
- A viewer who taps, scrolls or navigates the page during the final seconds of
|
||||
the slot **keeps the page on screen** — the advance is pushed 10 s past that
|
||||
touch, and every further touch pushes it again. The link is never pulled out
|
||||
from under someone who is using it.
|
||||
- An untouched page still advances on schedule, exactly like a media item.
|
||||
- A multi-event burst (a drag, a page transition) counts as **one** interaction
|
||||
but the countdown is measured from the **last** event of that burst, so an
|
||||
item can never be cut off mid-gesture.
|
||||
- `max_dwell` (duration × `max_dwell_factor`, at least `min_max_dwell`) is an
|
||||
absolute backstop so a wedged browser or a jammed touchscreen cannot park the
|
||||
playlist forever.
|
||||
|
||||
**Pause/play does not apply to web links.** A web link is an interactive
|
||||
surface, so `toggle_pause()` is a no-op while one is on screen — the interaction
|
||||
watcher owns its lifecycle. The pause button continues to work normally for
|
||||
images and videos.
|
||||
|
||||
### 6.2 Verified start-up
|
||||
|
||||
Launching a browser is not the same as displaying a page. The session therefore
|
||||
does **not** report success immediately after spawning the process (that used to
|
||||
reset the error counter and leave a black screen for the whole duration). The
|
||||
watcher thread — never the Kivy main thread — waits for the browser window to
|
||||
appear, and if it never does the item is reported as failed and skipped.
|
||||
|
||||
### 6.3 Configuration
|
||||
|
||||
All timings are tunable in `config/app_config.json` under `weblink`:
|
||||
|
||||
```json
|
||||
"weblink": {
|
||||
"engine": "auto",
|
||||
"interaction_postpone": 10,
|
||||
"interaction_debounce": 0.5,
|
||||
"interaction_grace": 5.0,
|
||||
"max_dwell_factor": 6.0,
|
||||
"min_max_dwell": 300,
|
||||
"launch_timeout": 15,
|
||||
"prewarm": true
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `engine` | Preferred engine (`auto`, `cef`, `subprocess`). |
|
||||
| `interaction_postpone` | Seconds the advance is postponed, measured from each interaction (default 10). |
|
||||
| `interaction_debounce` | Logging/trace throttle for continuous drags (default 0.5). |
|
||||
| `interaction_grace` | Settle window after the last raw event still counted as interacting (default 5). |
|
||||
| `max_dwell_factor` | Hard ceiling = `duration × factor`. |
|
||||
| `min_max_dwell` | Floor for that hard ceiling, in seconds. |
|
||||
| `launch_timeout` | How long to wait for the browser window to appear. |
|
||||
| `prewarm` | Pre-warm the next weblink (disabled on Windows). |
|
||||
|
||||
### 6.4 Diagnostics
|
||||
|
||||
The watcher traces structured events through `playback_trace.py`:
|
||||
`weblink_launch`, `weblink_visible`, `weblink_interaction`, `weblink_end`
|
||||
(with reason `viewer_idle`, `browser_exited` or `max_dwell`),
|
||||
`weblink_not_visible` and `weblink_failed`.
|
||||
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"server_ip": "192.168.0.107",
|
||||
"server_ip": "192.168.0.108",
|
||||
"port": "8080",
|
||||
"screen_name": "WINDOWS-PC",
|
||||
"quickconnect_key": "8887779",
|
||||
@@ -8,5 +8,18 @@
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true,
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
"verify_ssl": false,
|
||||
"card_reader_mode": "auto",
|
||||
"card_reader_device": "",
|
||||
"card_reader_timeout": 5,
|
||||
"weblink": {
|
||||
"engine": "auto",
|
||||
"interaction_postpone": 10,
|
||||
"interaction_debounce": 0.5,
|
||||
"interaction_grace": 5.0,
|
||||
"max_dwell_factor": 6.0,
|
||||
"min_max_dwell": 300,
|
||||
"launch_timeout": 15,
|
||||
"prewarm": true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,11 @@ hidden_imports = [
|
||||
'cef_browser',
|
||||
'win32gui',
|
||||
'win32con',
|
||||
# Unified web-link controller (launch / verified visibility / interaction
|
||||
# watcher / teardown) — imported by main.py and run_win.py
|
||||
'weblink_session',
|
||||
# Windows-native card reader (Raw Input API + LL-hook fallback)
|
||||
'win_card_reader',
|
||||
]
|
||||
|
||||
# Exclude Linux-only modules
|
||||
|
||||
Reference in New Issue
Block a user