# Board Enablement Test Bed A reproducible playback test bed for validating the viewer stack across boards. Use this when changing anything that touches mpv flags, hwdec, the cage/Wayland stack, or `src/anthias_viewer/media_player.py`. The viewer is tuned per-board (see `media_player.py` and `bin/start_viewer.sh`): | Device | Qt platform | Compositor | video output | |----------|-------------|-----------|----------------------------------| | Pi 1/2/3 | Qt5 linuxfb | none | GStreamer `v4l2h264dec ! v4l2convert ! fbdevsink` | | Pi 4-64 | Qt6 linuxfb | none | `--vo=gpu --gpu-context=drm` | | Pi 5 | Qt6 wayland | cage | `--vo=gpu --gpu-context=wayland` | | arm64 | Qt6 wayland | cage | `--vo=gpu --gpu-context=wayland` | | x86 | Qt6 wayland | cage | `--vo=gpu --gpu-context=wayland` | Each combination has different hwdec, scaling, and compositing characteristics, so a regression on one board can hide behind a clean run on another. Run the same asset rotation everywhere and compare drop counts. ### Pi 1 / 2 / 3 video: GStreamer V4L2 → fbdev (`GstFbdevMediaPlayer`) The Qt5 linuxfb boards play video by spawning the `anthias_viewer/gst_fbdev_player.py` helper, which runs a GStreamer `playbin` with a fully-hardware sink: `v4l2h264dec` (auto-plugged by decodebin at PRIMARY rank) decodes on the `bcm2835-codec` (`/dev/video10`), `v4l2convert` hardware-scales and color-converts (YUV→framebuffer format) on the bcm2835 ISP, and `fbdevsink` paints frames straight to the framebuffer (`/dev/fb0`). The helper loops the clip gaplessly in-process (playbin `about-to-finish` — issue #2987: the original `gst-launch` relaunch-on-EOS loop froze the last frame for seconds of pipeline rebuild per iteration), pins aspect-fit output caps discovered from the decoder's CAPS event so portrait/4:3 content letterboxes instead of stretching (`fbdevsink` ignores pixel-aspect-ratio and centers smaller-than-fb frames), and caps the sink-side frame rate at 30 fps (`videorate drop-only=true` — the decode→ISP→memcpy chain sustains ~40 fps at 1080p, so 50/60 fps sources drop to an even half-cadence instead of juddering on irregular late-frame drops). This is deliberate: on a bare linuxfb console with no compositor, a uid-1000 process **cannot acquire DRM master** (the viewer's python process already holds `card0`), so every DRM/KMS-master video output fails — VLC's `kms` vout and mpv's `--vo=drm` both return `EBUSY`/`EPERM`, and the rpidistro VLC `drm_vout` only knows how to lease a connector from a Wayland/X compositor (`Failed to get xlease`). fbdev needs none of that. History: these boards used to play video with VLC's Broadcom `--vout=mmal_vout` (HW decode **and** HW scale/convert/scanout, no DRM master). The Bookworm upgrade (#1980) dropped `mmal`, and nothing replaced the vout, so VLC silently rendered to nowhere — `Showing asset … (video)` was logged but the screen stayed black on *every* OS version. `GstFbdevMediaPlayer` is the modern equivalent and drives the **same VPU + ISP silicon** mmal used: decode, scale, and color-convert all stay in hardware, only the final fbdev write is a CPU memcpy. On a Pi 3 it sustains 1080p30 → rgb565 at ~40 fps with zero dropped frames. (An interim `ffmpeg → fbdev` attempt was abandoned: the bcm2835 HW *decode* worked, but doing the YUV→RGB convert + scale on the ARM CPU via swscale managed only ~6 fps to rgb565 — swscale has no NEON rgb565 path and CPU scaling is unaccelerated. `v4l2convert` moves that work back onto the ISP.) `playbin` is used so demux is container-agnostic, the HW decoder is auto-plugged, and a clip with no audio track degrades gracefully. Validate with the BBB sample pack below and confirm `dropped: 0` at the source framerate. > **Note:** validated on a Pi 3 (the only linuxfb board in the testbed pool). > Pi 1 / Pi 2 share the same VideoCore IV decode + ISP block, so the hardware > path is identical; their slower ARM cores only affect demux/parse + the fb > memcpy, not the offloaded decode/scale/convert. #### Screen rotation and video on Pi 1 / 2 / 3 `screen_rotation` interacts badly with video on these boards, and the outcome depends on the angle — because VideoCore IV has **no rotate control at all**. `v4l2-ctl -l` on the ISP (`/dev/video12`) exposes only `horizontal_flip` and `vertical_flip`; no bcm2835 video device exposes `V4L2_CID_ROTATE`. (The same limit shows up on the pi3-64 vc4 DRM plane, which offers 0°/180° + reflect only — forum 6730.) | `screen_rotation` | path | Pi 2, 1080p30 | |---|---|---| | 0 | all hardware | ~27 fps | | 180 | ISP `horizontal_flip` + `vertical_flip` | ~27 fps | | 90 / 270 | software `videoflip` (no HW path) | **~2 fps** | * **180 is free.** hflip + vflip compose to a 180 rotation and ride along on the `v4l2convert` pass that already does the aspect-fit scale, so the pipeline stays fully hardware and the result is pixel-identical to the software rotation. * **90 / 270 are effectively unsupported for video on Pi 1 / 2 / 3** (issue #3198). The quarter turns need a transpose, which no VideoCore IV block can do, so `videoflip` rotates each full-resolution frame on a single CPU core and 1080p collapses to ~2 fps. The picture is *correct*, just far too slow to use. Rotating still images and web pages costs nothing per frame and is unaffected — this limit is video-only. **Use a Pi 4 / Pi 5 for portrait-mounted video**, where the platform layer rotates instead. Note that reordering to scale-before-flip does not rescue 90/270: the case that actually matters — portrait content on a portrait-mounted panel — rotates to exactly fill the framebuffer, so there is no downscale to exploit ahead of the flip (measured slightly *worse*, 1.9 vs 2.1 fps). ## Goal: hardware-accelerated playback on every board Every clip Anthias displays should decode in hardware on the target board. Software decode produces drops, heats the SoC, and on low-end Pis can't keep up at 1080p30, let alone 4K. Anthias does *not* re-encode video uploads on-device — see the asset processor docstring (`src/anthias_server/processing.py`) for why. The viewer's per-board mpv hwdec dispatch handles every codec a modern board can decode in hardware (H.264, HEVC, plus VAAPI's wider set on x86). For codecs the board can't decode (MPEG-2 DVD rips, MPEG-4 ASP DivX clips, AV1 outside x86, …), playback will visibly stutter; the asset list surfaces `metadata['video_codec']` / `metadata['video_width']` / `metadata['video_height']` / `metadata['video_fps']` so operators can spot a misfit clip before pushing it to the field. The viewer (`src/anthias_viewer/media_player.py`) selects the correct mpv hwdec per codec on the target board. On Pi 4 / Pi 5 the launcher ffprobes the asset and passes `--hwdec=v4l2m2m-copy` for H.264 or `--hwdec=drm-copy` for HEVC directly on the mpv command line. (An earlier attempt used a Lua `on_load` hook, but `video-codec-name` is empty at every script event before hwdec init, so the hook was a silent no-op and `--hwdec=auto-copy` leaked through; `auto-copy`'s upstream whitelist excludes `v4l2m2m-copy`, so H.264 fell back to software.) ## Hardware decode capabilities per Pi What the SoC can do, regardless of player: | Pi | SoC | H.264 HW | HEVC HW | VP9 / AV1 HW | |------|----------|---------------------------|------------------------|--------------| | 2 | BCM2836 | yes, up to 1080p (V3D IV) | **no** — no HEVC block | no | | 3 | BCM2837 | yes, up to 1080p (V3D IV) | **no** — no HEVC block | no | | 4 | BCM2711 | yes, up to 1080p60 (V3D 6.0 V4L2 M2M); 4K H.264 is past the V3D's envelope | yes, up to 4Kp60 (dedicated HEVC block, exposed as `v4l2_request_hevc`) | no | | 5 | BCM2712 | yes in silicon (Hantro G1), but **not reachable through mpv** — no `v4l2-request` H.264 hwdec exists upstream | yes, up to 4Kp60 (Hantro G2, exposed as `v4l2_request_hevc`) | no | HEVC HW decode arrived with the Pi 4. Pi 2 / Pi 3 cannot decode HEVC in hardware at all, and software HEVC on a Cortex-A53 won't even hit 1080p30 — so uploading an HEVC asset for a Pi 2 / Pi 3 fleet member will play (badly) on the SoC's software fallback. If you need HEVC content on that fleet, transcode it upstream of the upload. > **Pi 5 4K HEVC requires `dtoverlay=vc4-kms-v3d,cma-512`.** The Hantro G2 > driver allocates DMA buffers from the kernel's Contiguous Memory Allocator. > Pi OS for Pi 5 reserves only 64 MB CMA by default (vs. 512 MB on Pi 4), > which is enough for 1080p HEVC reference + output buffers but not 4K — at > 4K mpv hits `v4l2_request_hevc_start_frame: Failed to get dst buffer` and > silently SW-falls-back. Bumping `cma=512M` on the kernel cmdline does > **not** work: the kernel takes the cmdline value over the device-tree > `linux,cma` node, which leaves `rpi-hevc-dec` orphaned > (`Failed to probe hardware -517`) and `/dev/video*` disappears entirely, > killing HEVC HW at every resolution. The right fix is the > `dtoverlay=vc4-kms-v3d,cma-512` line in `/boot/firmware/config.txt` — > the vc4 overlay carries the `cma-N` knob and resizes the DT-declared > region without orphaning the HEVC driver. The Anthias ansible template > at `ansible/roles/system/templates/config.txt.j2` writes that line on > install. ## Rock Pi 4 / arm64 `bin/install.sh` sets `DEVICE_TYPE=arm64` for every aarch64 SBC it doesn't recognise as a Pi. The board subtype that upgrades that catch-all comes from `anthias_common.device_helper.detect_board_subtype` (the single source of the model-string table — "Radxa ROCK Pi 4" → `'rockpi4'`), consumed through two paths: * on docker-compose installs, `anthias_host_agent` runs on the host, detects the subtype, and publishes `host:board_subtype` to Redis; * when Redis has no value — the agent is down, or never ran — `anthias_common.board` falls back to reading `/proc/device-tree/model` directly from inside the container. **That fallback does not work in the server/celery containers.** `/proc/device-tree` is a symlink to `/sys/firmware/devicetree/base`, and `/sys/firmware` is on Docker's default masked-paths list, so an unprivileged container reads an empty directory on every board. Measured on a NanoPi R3S LTS: readable on the host and in the privileged `anthias-viewer` container, empty in `anthias-server` / `anthias-celery`. A bind mount onto the masked path stays empty too. The practical consequences: on docker-compose installs the host_agent's Redis value is the only working source, and on balena — no host_agent, unprivileged server container — the subtype has no source at all, so the `screenly_ose/anthias-rockpi4` fleet's codec gate does not actually get upgraded there. Fixing balena needs a host-side publisher (or `io.balena.features.sysfs` plus a path change); untested so far. The server uses the resolved key to pick the right entry in `processing._HW_DECODE_VIDEO_CODECS` — Rock Pi 4 accepts H.264 + HEVC uploads, the catch-all `arm64` accepts nothing (because we can't certify a decoder on an unknown SBC). The balena fleet (`screenly_ose/anthias-rockpi4`, device type `rockpi-4b-rk3399`) deploys the generic **arm64** container images — there is no rockpi4-specific image build; only the balenaOS device type and the fleet are board-specific. `bin/balena_ota_deploy.sh` owns that mapping. The arm64 viewer image pulls `ffmpeg` and the libav* family from `archive.raspberrypi.com` (the `+rpt1` build), which adds `--enable-v4l2-request --enable-libudev --enable-vout-drm` — the same package family Pi 4 / Pi 5 use. The `start_viewer.sh` entrypoint creates the `/dev/video-dec*` symlinks the v4l2_request decoder discovery code expects (privileged docker mounts its own /dev tmpfs without udev's symlinks). The `+rpt1` repo is pinned to only override ffmpeg + libav* on arm64; Pi userspace baseline is unaffected on every board. ### Decode path (post-#2905) Playback runs through QtMultimedia (`QMediaPlayer` rendering into a QML `VideoOutput` hosted in a `QQuickWidget` — issue #2967 replaced the original `QGraphicsVideoItem` substrate, whose per-frame `toImage()` GPU→CPU readback capped presentation at 8–12 fps) embedded in `AnthiasViewer`. Qt 6.5 dropped the upstream gstreamer media backend, so Debian Trixie ships only the ffmpeg-backed `libffmpegmediaplugin.so`, which calls libavcodec internally. The libmpv subprocess and the per-codec `--hwdec=` dispatch the prior viewer revisions relied on are both gone — Anthias no longer hand-selects a decoder. **As a consequence: libavcodec's default decoder choice is what runs on this board.** ### Known hardware-decode limitation on RK3399 On-device validation against Armbian community 6.18 (`rkvdec` mainline driver) confirmed that QtMultimedia's libavcodec backend does **not** request a hwaccel — `ffmpeg -i sample.mp4 -f null -` picks `h264 (native)` / `hevc (native)` (software). Forcing `-hwaccel drm` engages rkvdec via `/dev/media0,/dev/video2` (the stateless v4l2_request entry point), but the Armbian 6.18 driver produces `frame_post_process: Decode fail` errors at 0.77× real-time. The H.264 path (`rk3399-vpu-dec` / Hantro VPU) has no libavcodec `v4l2_request` binding in the `+rpt1` 7.1.3 build — it would have to ship through `h264_v4l2m2m`, but that wrapper is stateful and the RK3399 nodes are stateless, so the M2M discovery fails with "Could not find a valid device". Practical state on Rock Pi 4 today: * H.264 1080p30 SW-decode delivers ~99 % of frames on the A72 (measured: 1 dropped per 14 s); below the threshold where HW would matter. * HEVC 1080p30 SW-decode drops ~22 % even with 5 idle cores — single-thread libavcodec stall + paging on 1 GB SKUs. * 4K HEVC OOM-kills the viewer container on 1 GB SKUs and is rejected at upload by the low-RAM resolution gate (see "Low-RAM mode" below). Fixing this requires either an upstream `rkvdec` driver fix landing in Debian-shipped Armbian kernels, or a v4l2_request H.264 binding in libavcodec that targets Hantro's stateless API. Both are outside the Anthias repo; Anthias does not maintain a custom kernel or distro (["we don't want to maintain our own Yocto distro"]). When that day comes, the QtMultimedia side will also need a hwaccel-selection hook — Qt 6.5+ has no public knob today. ### RK3566 (NanoPi R3S LTS) — measured playback envelope Same image as the device-tree section above. Decode is software throughout: the arm64 viewer image's Qt 6 multimedia backend is `libffmpegmediaplugin.so`, and that libavcodec exposes only the stateful `*_v4l2m2m` wrappers, which cannot drive RK3566's stateless rkvdec (the RK3399 mismatch again). Decode throughput, measured in the viewer image against 20 s noise-heavy 1080p30 clips: ``` $ ffmpeg -hide_banner -benchmark -i clip.mp4 -f null - ... bench: utime=25.197s stime=0.714s rtime=7.802s ``` `rtime` is the wall clock for the whole decode, so 20 s of video in 7.8 s is 2.56x real time: | codec | rtime for 20 s | speed | | --- | --- | --- | | H.264 | 7.8 s | 2.56x real time | | HEVC | 17.5 s | 1.15x real time | End-to-end playback on the attached 4K panel (cage/wayland, viewer rendering to the display, 30 s H.264 clips at 8 Mbit/s 1080p and 4 Mbit/s 720p): | clip | viewer container CPU | host idle | viewer RSS | | --- | --- | --- | --- | | 1080p30 | 200-240 % of 400 % | ~28 % | ~990 MiB | | 720p30 | 100-130 % of 400 % | ~58 % | ~890 MiB | Frames advance in both cases (successive `grim` captures differ). The numbers are the argument for the envelope in `processing._HW_DECODE_VIDEO_CODECS`: H.264 only, capped at 1080p by `_SW_DECODE_MAX_PIXELS`. HEVC at 1.15x leaves nothing for the compositor, and 4K H.264 is ~4x the 1080p work, which lands under real time. Note the RSS column against the container's 1.54 GiB cap: at 1080p the viewer sits around 990 MiB before anything else runs, so memory is as close to the limit as CPU is on this 2 GB board. Caveat on the drop question: neither the Qt path nor this board exposes a frame-drop counter — `ANTHIAS_DEBUG_DROPS` and `~/.anthias/mpv.log` in the sections above belong to the retired mpv subprocess player and do nothing here. Drops on real content are an operator observation, not something these measurements quantify. Wiring a drop counter into the QtMultimedia path is the missing instrument. ### Low-RAM mode Boards with less than 1.5 GiB MemTotal (Pi 2/Pi 3 1GB, Pi 4 1GB, Rock Pi 4 1GB, generic-arm64 1GB SKUs) run in a degraded "low-RAM" mode: * `anthias_host_agent` publishes `host:total_mem_kb` to Redis; `anthias_server.processing` rejects uploads above 1920×1080 with the existing recipe machinery (`-vf scale=1920:1080:force_original_aspect_ratio=decrease`). * The diagnostics page's Memory card surfaces a "Low-RAM mode" banner so operators can see *why* the device degraded. The single-`QWebEngineView` renderer is **not** low-RAM-specific — every board runs it. The viewer used to instantiate a second, off-screen `QWebEngineView` to preload the next URL asset and swap it in, gated on `ANTHIAS_LOW_RAM` so 1 GB boards skipped the ~100 MB extra renderer. That preload path is gone (issue #2954): on the single fullscreen surface the Qt6 boards use (Wayland/cage on Pi 5 / x86, eglfs on pi4-64) a hidden `QWebEngineView` is frame-throttled by Chromium and never composited the preloaded page, so revealing it flashed the *stale* page it had last shown two rotations earlier for ~1 frame on every webpage transition. Collapsing to one view fixed the flash and reclaimed the ~100 MB on all 2 GB+ boards too. The 1.5 GiB threshold cleanly separates 1 GB SKUs from 2 GB+ SKUs in the supported fleet. The upload cap was sized against on-device measurements: idle viewer + one QtWebEngine renderer + zygotes leave roughly 500 MB on Rock Pi 4 1GB for the kernel, host services, and decode pipeline. A 4K HEVC capture-buffer allocation pushes the container past the cgroup limit; the kernel logs `global_oom` and the container restart-loops. ### armv7 (Pi 2 / Pi 3) WebEngine-init crash + spawn retry On the 32-bit (armv7) Qt5 viewer build — Pi 2 and Pi 3 — `AnthiasViewer` intermittently aborts during Qt/WebEngine initialization with `malloc(): unaligned tcache chunk detected`, dying before it emits the `Anthias service start` D-Bus handshake. Reproduced and root-caused on a loaned 64-bit Pi 3B+ running the armv7 `anthias-viewer:*-pi3` image: * It is **genuine heap corruption in Chromium/WebEngine init**, not a font bug. The font-database enumeration is only where the corruption *surfaces* (the heaviest main-thread `malloc` activity at that instant — the abort always trails the synthetic `Monospace … StyleOblique` line). A build that stops before WebEngine actually initializes enumerates the same fonts cleanly every time; only letting WebEngine init proceed triggers the corruption. * It is **heap-layout dependent**, firing on ~75–90 % of launches — so a *fresh* launch clears it ~10–25 % of the time. No userspace mitigation fixes it: trimming the CJK fonts, `--single-process`, `--no-zygote`, the single `QWebEngineView`, a jemalloc preload, and disabling glibc's tcache check (which just turns the abort into a raw `SIGSEGV`) all still crash. Because each spawn is a fresh process and a retry usually succeeds within a handful of attempts, `load_browser()` in `src/anthias_viewer/__init__.py` **retries the spawn in-process with capped exponential backoff** (one throttled log line per attempt) instead of letting the exception escape `main()` into a tight container restart loop (which floods journald and makes no faster progress). The budget differs by where it runs: * **At startup** (`setup()`): a generous budget (`BROWSER_SPAWN_MAX_ATTEMPTS` / `BROWSER_SPAWN_BACKOFF_CAP_SECONDS`) — nothing is on screen yet, so it's worth spending time to bring the webview up. * **Mid-playback** (`view_image` / `view_webpage` respawn): a small, short budget (`BROWSER_SPAWN_INLINE_*`). These run on the single `asset_loop` thread, so a long retry here would freeze the whole viewer — no rotations, no skips, no standby, and `watchdog()` starved. A persistent failure raises instead, and the container restart re-rolls from a clean process. A missing/unlinkable binary raises `WebviewBinaryMissingError` and short-circuits the retry (it's permanent, so burning the backoff budget would only hide a packaging regression). Operator-visible status is the throttled `logging.warning` output (`balena logs` / journald). This is a **stop-gap** — the clean fix is to run 64-bit-capable Pi 3 hardware on a 64-bit OS + the arm64/Qt6 viewer, which sidesteps the entire 32-bit Qt5 stack. That clean fix now ships as its own board target: **`pi3-64`** — the arm64 Qt 6 image stream for Pi 3 hardware booted on a 64-bit OS. It folds into the same Qt 6 / eglfs_kms display path as `pi4-64` (the VideoCore IV is weaker, so hardware decode is H.264-only — see the per-board codec set in `src/anthias_server/processing.py`), and has its own balena fleet (`screenly_ose/anthias-pi3-64`, device type `raspberrypi3-64`) and disk image. `bin/install.sh` and `bin/upgrade_containers.sh` select it automatically when a Pi 3 is running a 64-bit (`aarch64`) OS; the legacy 32-bit armhf/Qt5 `pi3` stream stays for devices still on a 32-bit OS and is flagged as maintenance/ legacy in the Raspberry Pi Imager listing. ## Sample pack Run `bin/generate_board_enablement_testbed.sh` on a workstation (not the device under test) to produce the 8-clip pack: ```bash bash bin/generate_board_enablement_testbed.sh ~/bbb-testbed ``` The script: 1. Downloads four Big Buck Bunny H.264 + AAC sources (public-domain, from `download.blender.org/demo/movies/BBB`) — skipped if already present. 2. Trims each to 60 seconds via `-c copy` (instant, no re-encode) — produces the H.264 half of the pack. 3. Re-encodes each cut with `libx265 -preset medium -crf 23 -tag:v hvc1` — produces the HEVC half. 4. Prints a verification table (codec + resolution + fps + duration from `ffprobe`). | File | Codec | Resolution | fps | |----------------------------------|-------|------------|-----| | `bbb_1080p_30fps.mp4` | H.264 | 1920×1080 | 30 | | `bbb_1080p_60fps.mp4` | H.264 | 1920×1080 | 60 | | `bbb_4k_30fps.mp4` | H.264 | 3840×2160 | 30 | | `bbb_4k_60fps.mp4` | H.264 | 3840×2160 | 60 | | `bbb_1080p_30fps_hevc.mp4` | HEVC | 1920×1080 | 30 | | `bbb_1080p_60fps_hevc.mp4` | HEVC | 1920×1080 | 60 | | `bbb_4k_30fps_hevc.mp4` | HEVC | 3840×2160 | 30 | | `bbb_4k_60fps_hevc.mp4` | HEVC | 3840×2160 | 60 | 60 seconds per clip is enough to capture mpv's `hwdec-current` banner and read a stable `Dropped:` count, while keeping a full pack regen achievable in a few minutes on a laptop. Pass `CUT_SECONDS=N` to the script to change the per-clip length; pass `HEVC_CRF=N` to override the encoder's quality target. The script is idempotent: clips that already exist (and pass an `ffprobe` sanity check) are skipped on re-run. A power cycle mid-encode leaves the temp file as `*.tmp.mp4`; the next invocation regenerates from scratch. ## The rotation Upload all eight files (4 × H.264 + 4 × HEVC) as Anthias assets and schedule them back-to-back. Per-asset boundaries in the drop log make it easy to slice results by resolution / fps / codec. ## Drop logging Set `ANTHIAS_DEBUG_DROPS=1` on the `anthias-viewer` service (compose override or `~/anthias/.env`). When enabled, `media_player.py`: - drops mpv's `--no-terminal`, so the status line (`AV: 00:00:30 / ... Dropped: N`) is emitted continuously; - redirects stdout/stderr to `/data/.anthias/mpv.log` inside the container, which is `~/.anthias/mpv.log` on the host (or `~/.screenly/mpv.log` on pre-rebrand installs); - writes a `--- mpv launch ---` marker before each mpv launch so the log can be sliced per asset; - captures mpv's `hwdec-current` and VO init banners on stderr — confirms `--vo=gpu --gpu-context=drm` (Pi 4) vs `--gpu-context=wayland` (Pi 5 / x86 / arm64) actually took effect, and confirms which hwdec the per-codec dispatch selected. With the env var unset, the viewer keeps its silent `DEVNULL` behaviour — no host-side log file. ## Reading the log `Dropped:` in mpv's status line is cumulative for a single mpv process, and the viewer spawns one process per asset, so the last `Dropped:` before the next `--- mpv launch` marker is that asset's final count over its playback window. ```bash grep -E "^--- mpv launch|Dropped:" ~/.anthias/mpv.log | tail -80 ``` For a single rolling sample on a running device: ```bash tail -F ~/.anthias/mpv.log | grep --line-buffered -E "launch|Dropped:|hwdec-current|VO:" ``` ## Reporting When attaching results to a PR, include: - board + Qt/compositor combination (one row of the table above); - one drop count per asset, taken from the last `Dropped:N` of each asset's window; - the matching `VO:` / `hwdec-current` banner lines so the run can be tied to a specific stack. For comparable numbers, let the rotation play at least two full cycles before sampling — first cycle includes asset-cache warmup and webview teardown.