Skip to main content

anthias-viewer

Viewer & server application internals for Anthias — the server-rendered Django/ASGI + Redis pub/sub architecture, streaming-under-ASGI, SQLite WAL playlist reload, viewer memory/OOM behavior, upload codec gate, Sentry-noise-vs-real-bug triage, remote media, webview C++ features, PulseAudio, and the content-import framework. Read before touching viewer scheduling, messaging, streaming, upload/backup/import, or webview D-Bus code.

소스 정보

저장소
Screenly/Anthias
최근 소스 활동
2026년 7월 27일 17:57
감지된 SKILL.md 언어
영어
스타
3,691
포크
727

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
anthias-viewer
description
Viewer & server application internals for Anthias — the server-rendered Django/ASGI + Redis pub/sub architecture, streaming-under-ASGI, SQLite WAL playlist reload, viewer memory/OOM behavior, upload codec gate, Sentry-noise-vs-real-bug triage, remote media, webview C++ features, PulseAudio, and the content-import framework. Read before touching viewer scheduling, messaging, streaming, upload/backup/import, or webview D-Bus code.
# Anthias application & viewer architecture knowledge Durable engineering findings on the viewer/server internals and the subtle bugs that bit them. Each bullet carries the mechanism plus the fixing PR/CalVer where known; OPEN items are flagged. ## Application architecture (current layout) - The web UI is **server-rendered Django templates** (`src/anthias_server/app/templates/`) with **Alpine.js + htmx + flatpickr** — there is **no React/Redux frontend**. TypeScript is thin page bundles (`home.ts`, `apps.ts`, `splash.ts`, `vendor.ts`) under `src/anthias_server/app/static/src/`, built by bun. Don't grep for React components — look in `_asset_modal.html` / `home.ts`, etc. - Layout: server code under `src/anthias_server/` (`app`, `api`, `django_project`, `lib`); shared code in `src/anthias_common/`; viewer in `src/anthias_viewer/`; webview C++ in `src/anthias_webview/`; host agent in `src/anthias_host_agent/`. - Integration tests (`tests/test_app.py`, Playwright sync API) run inside the `docker-compose.test.yml` `anthias-test` container against uvicorn on `localhost:8080`, started by `bin/prepare_test_environment.sh -s`. uvicorn does **not** auto-reload — restart it after editing server code, and never `pkill -f uvicorn` from a wrapper whose own cmdline matches. - `anthias_common.board` is the canonical low-RAM system: `is_low_ram_device()` + `LOW_RAM_THRESHOLD_KB = 1_572_864` (1.5 GiB), reading host_agent-published `host:total_mem_kb` from **Redis** (keeps server/viewer consistent). Reuse it — do NOT reintroduce a private `/proc/meminfo` reader. It gates the processing.py 1080p video pixel cap and the Low-RAM badge, on both eglfs and cage/wayland stacks. ## Streaming under ASGI (backup download / large responses) - **A sync `StreamingHttpResponse` generator does NOT stream under Django ASGI/uvicorn.** Django 5.2's `__aiter__` hits the sync-iterator branch and does `await sync_to_async(list)(self.streaming_content)` — draining the WHOLE generator into a RAM list before the first body chunk. The browser shows a download dialog stuck at 0 B until the entire build finishes, then times out. Also an OOM risk on 1 GB SBCs. - This was issue **#3073**: the #3005 backup-streaming fix (shipped v2026.06.3) was fully neutralized because `stream_backup()` (`src/anthias_server/lib/backup_helper.py`) is a sync generator. Unit tests called it directly and never traversed ASGI `__aiter__`, so they passed while production buffered everything. - **Fix:** the iterator must be async so Django sets `is_async=True` and uses the `awrapper()` streaming path. Wrap a sync producer with `async def` + `await sync_to_async(next, thread_sensitive=False)(it, sentinel)` per chunk. Regression tests MUST drive the real ASGI view via `aiter(response)` and assert the first chunk arrives before the generator is exhausted. - **Backup E2E validated** (2026-07-08) on three 1 GB boards with a real ~640 MB archive: RSS peaked ~70-170 MiB, TTFB ~30 ms. Two surfaces: - **#3074 backup DOWNLOAD** = `POST /settings/backup/` (HTML view, CSRF-gated → fetch csrftoken from `GET /settings/` first; `StreamingHttpResponse(astream_backup())`). NOT the v2 API. - **#3143 recover UPLOAD** = `POST /api/v2/recover` (DRF, CSRF-exempt), streams to `static/<uuid>.tar.gz` via `file_upload.chunks()`. ## Viewer ↔ Redis pub/sub messaging - **Command channel is `anthias.viewer`** (Redis pub/sub); server↔viewer request-reply uses `anthias.viewer` publish + BLPOP on `anthias.reply.<correlation-id>`. - **Viewer subscriber can die silently and keep playing (OPEN).** Seen on a 1 GB arm64 board (~16 h uptime): `redis-cli pubsub channels` listed only `hostcmd`, **no `anthias.viewer`**, so `publish anthias.viewer "viewer blank"` returned `subscribers=0` and went nowhere — board deaf to next/previous/stop/blank, yet still cycling assets. `docker restart` restores it. **Diagnose with `redis-cli pubsub channels`, not the ready flag.** `viewer-subscriber-ready` lies: set once on subscribe, only cleared on `redis.ConnectionError`, so it stays `1` when the thread dies any other way. Suspected: `_consume()` raising anything that isn't `redis.ConnectionError` escapes `run()`'s except and kills `ViewerSubscriber.run()` for the process lifetime. No issue filed. - **Subscriber topic-prefix trap:** the viewer subscriber splits commands via `data.partition(' ')` and drops anything without the `viewer ` topic prefix. `processing.py` celery publishes raw `'reload'` (no prefix) which would be dropped — unverified suspect. - **stop/blank pause path — NOT a bug (issue #3136).** QA reported `stop`/`blank` not halting rotation on headless pi5, but it **did not reproduce** on the real pi5 RC (2026-07-08). The `global loop_is_stopped` fix (#3065) is an ancestor of the RC. The QA symptom was a measurement artifact (4 s durations → the natural `Showing asset...` line fires within ~20 ms of the publish). Recommend closing #3136 as not-reproducible. ## Playlist reload / SQLite WAL - **First asset on a fresh install never auto-displays** (out-of-the-box bug, RESOLVED #3062). `scheduling.py` `refresh_playlist()` only calls `update_playlist()` when `get_db_mtime() > last_update_db_mtime`. `get_db_mtime()` stat'd the **main `anthias.db` file only**, but the DB runs in **WAL mode** — writes land in `anthias.db-wal`, so the main file's mtime stays frozen until a checkpoint → the trigger never fires. With an empty starting playlist `_compute_deadline()` returns `None` so the deadline trigger never fires either. `_handle_reload()` reloads settings only, NOT the playlist. And v2 create returned 201 WITHOUT `send_to_viewer('reload')`. Net: first asset never picked up until viewer restart or a main-db mtime bump (`touch anthias.db` instantly fixed it). **Fix #3062:** `get_db_mtime()` now takes max mtime across `.db`/`-wal`/`-shm`. - **An asset's `uri` is immutable by design.** The edit modal renders `#edit-uri` `readonly` with no `name` attr (not submitted); neither `assets_update` nor `UpdateAssetSerializer` writes `instance.uri`. To change content you delete + re-create. "Edit the URL" silently no-ops — intended. ## Viewer memory / OOM behavior - **Viewer stdout accumulation (issue #3138, fixed #3147, 2026-07-08).** `src/anthias_viewer/__init__.py` spawns `AnthiasViewer` via `sh.Command(...)(_bg=True, _err_to_out=True, ...)` with **no `_out` sink**. sh (2.3.0) accumulates the subprocess's entire merged stdout+stderr in viewer-process RAM for the whole session. Chatty decoder spam (e.g. ffmpeg AAC `channel element 0.0 duplicate` per frame) → unbounded RSS growth → swap thrash on 2 GB → multi-minute blank. Fix: a bounded `_BoundedWebviewOutput` as `_out` keeping the last 64 KiB (a **deque of chunks**, not `buf += chunk` reslice). Measured: OLD +76 MB RSS for 12 MB of spam; NEW +0.5 MB. - **#3147 is NOT the fix for web-page OOM (forum thread 6731).** On a real 1 GB Pi3B+ (787 MB total, **no swap**) cycling web pages, `AnthiasViewer` emits ~304 KB one-time Qt startup dump then **~0 B/s**. The stdout leak needs *continuous* chatty stderr; bounding it changes nothing for web-page workloads. **Real pressure = Qt6 QtWebEngine footprint on a tiny board** — a 64-bit Pi3 OS now lands on the heavier Qt6 `pi3-64` stream (#2985, gated on userspace arch by #3076). **Fix #3178:** force Chromium's low-memory profile, gated on measured RAM via `is_low_ram_device()` (NOT board/DEVICE_TYPE — a 1 GB Pi5 exists). Prepend to `QTWEBENGINE_CHROMIUM_FLAGS`: `--enable-low-end-device-mode --js-flags=--max-old-space-size=64 --renderer-process-limit=1 --process-per-site --disable-dev-shm-usage`. HW-validated: Pi3B+ RSS 225→147 MB. Reverting Qt6→Qt5 is not an option. - **Upgrade OOM at swapoff (issue #3165, fixed 2026-07-09).** `run_upgrade.sh` aborted at ansible `system : Disable swap` with `rc: -9` (OOM SIGKILL) on 1 GB Pi4: `swapoff --all` faults every used swap page back into RAM at once. Root cause: `bin/install.sh::main()` runs the ENTIRE `site.yml` (dist_upgrade then swapoff) while the **previous-version Docker stack is still running**, so swapoff fires at peak memory. Fix: (1) `stop_docker_stack()` in install.sh BEFORE `run_ansible_playbook` (guarded on the rendered compose file existing); (2) misc.yml memory guard runs swapoff only when `SwapUsed < MemAvailable - 64MiB`, with `retries:3 until rc==0 failed_when:false`. Validated on real 1 GB Pi4. - **Asset upload must never wedge the device (P0 policy).** Uploading a normal asset pack must never make a device unresponsive. If it does, the asset-processor/walker hardening has a hole and the PR is not production-ready. Baseline hardening (not tunables): cgroup `cpus: 1.0`, CFS quota scaling, walker `--concurrency=1`, nice/ionice (PR #2885, from a Rock Pi 4 wedging on 4 parallel libx265 encodes). The fix belongs in the walker/cgroup/concurrency layer, never a docs note saying "upload one file at a time." ## Upload codec gate (by design, not bugs) - The upload codec gate in `src/anthias_server/processing.py` (`_HW_DECODE_VIDEO_CODECS` map + `_hw_decoded_codecs()`) rejects any uploaded video whose codec the board can't **hardware**-decode — by design. It surfaces a "Failed" pill + a copy-pasteable ffmpeg re-encode recipe rather than shipping a clip that would SW-decode janky. `'pi5': frozenset({'hevc'})` — Pi 5 has no H.264 HW decode, so H.264 is rejected (Sentry ANTHIAS-1J). Unknown DEVICE_TYPE (catch-all `arm64`, host_agent never published `host:board_subtype`) → empty set → every video rejected (Sentry ANTHIAS-20). - `UnsupportedVideoCodecError` is a handled user-facing outcome and should NOT reach Sentry. **Fix #3041:** added `throws=(UnsupportedVideoCodecError,)` to the `normalize_video_asset` celery task (sentry-sdk's CeleryIntegration skips `task.throws`). - Separately, `KeyError: 'WEBP'` on all pi3 (Sentry ANTHIAS-1Y) was a **real** bug: on armv7 Pillow builds from source and links `libwebp7`+`libwebpmux3`+`libwebpdemux2`, but the runtime image only got the first two. **Fix #3042:** add the three webp runtime libs to `base_apt_dependencies` in `tools/image_builder/__main__.py`. ## Sentry noise vs real bugs (triage pattern) - **Operator input validation leaking into Sentry as ERROR** (ANTHIAS-3D, `AuthSettingsError: New passwords do not match!`). Both settings-save surfaces caught it under broad `except Exception` + `logger.exception(...)`. Sentry's LoggingIntegration turns any ERROR-level log record into an event, so a password typo paged the team. **Fix #3068:** catch `AuthSettingsError` (a `ValueError` subclass with curated operator text) ahead of the generic handler; log at **warning** (no traceback → never a Sentry event) — the WARNING change is what actually stops the event; add to `_sentry_before_send`'s drop list as a backstop. **General rule: for input-validation Sentry noise, log at warning AND add to `before_send`.** - **Celery 30 s hard-limit SIGKILL trio** (ANTHIAS-A / -9 / -B) are ONE incident fanned into three groups (9 and B carry no task identity). #3017 added `soft_time_limit` only to the asset-probe path, leaving `get_display_power` and `send_telemetry_task` with bare `time_limit=30`. Root cause for telemetry: `requests_post(timeout=5)` bounds connect+read but NOT `getaddrinfo` — a wedged resolver hangs DNS past 30 s → SIGKILL. **Fixed #3063 / #3180:** `soft_time_limit=30`/`time_limit=60` + `SoftTimeLimitExceeded` catch on every periodic task; display_power/telemetry-cooldown keys use a single `SET ... ex=` so a soft-limit signal can't strand them without a TTL. ## Remote media (self-signed HTTPS + preview) — forum thread 6726 - **Bug 1 — self-signed/untrusted HTTPS media (RESOLVED #3176).** Two independent walls: the webview image loader (`view.cpp`, `QNetworkAccessManager`) had no `sslErrors`/`ignoreSslErrors` handler → blank; and server `url_fails` (`src/anthias_common/utils.py`) rejected self-signed → `Asset.is_reachable=False`. `verify_ssl` was **vestigial** (never in UI, `verify=True` even when False since a 2019 regression, and only affected the server probe, never the webview). Fix: surfaced `verify_ssl` in UI + v2 API; fixed the `else: verify=False`; added PER-ASSET `Asset.skip_ssl_verify` (migration 0007) composed with the global; viewer passes effective skip to webview `loadImage`/`loadPage` D-Bus slots; webview `ignoreSslErrors` on the image reply + `certificateError` override/signal → acceptCertificate. Video needs nothing (Qt6 QMediaPlayer/FFmpeg already handshakes self-signed). - **Bug 2 (OPEN) — remote-media backend preview blank (cert-independent, all remote jpg/mp4).** `assets_preview`/`assets_download` (`app/views.py`) only redirect-to-source for `mimetype in ('webpage','streaming')`. Remote `.jpg`/`.mp4` stored `mimetype='image'/'video'` → fall to `_safe_local_asset_path()` → no local file → 302 to home → blank. Regression from the React→Django rewrite. Not fixed by the SSL branch. ## Webview features (C++ / QWebEngine) - **Per-asset custom HTTP request headers (#2215, merged #3162).** Stored in `Asset.metadata['headers']` as `{name: value}` (no migration). Path: model helpers (CR/LF-reject to stop header splitting) → v2 serializers → edit-modal textarea → viewer `view_webpage(headers=...)` over a new `setRequestHeaders` D-Bus slot → C++ `RequestHeaderInterceptor` (`QWebEngineUrlRequestInterceptor` on the shared profile). Works identically on **Qt 5.13+ and Qt 6** (one non-gated path). Interceptor is **same-host scoped** (exact host match) so a token never leaks to third-party CDN/analytics. Use case: private Grafana dashboard via a service-account `Authorization` token. Cold-boot race: first `setRequestHeaders` after spawn can lose to D-Bus registration; `view_webpage` caches headers only on success so a single-asset playlist self-heals next tick. - **pi3-64 gapless in-slot video looping (PR #3174, "reduced-seam").** Switches overlay video+audio pipelines from flushing-seek restart to GStreamer SEGMENT playback (`videoview.cpp`). The original PR's bug: the init seek was issued **synchronously** right after `set_state(PLAYING)` while still prerolling (returns FALSE) → segment mode never engaged. Fix: enter segment mode from `GST_MESSAGE_ASYNC_DONE` (preroll complete), latching flags on seek SUCCESS. In segment mode the pipeline posts `SEGMENT_DONE`, never EOS — a failed non-flushing re-arm seek would freeze forever, so the segment-done handlers fall back to `gstRestartLoop` on failure. Loop drop ~6-10 → ~3 frames; NOT fully gapless (VC-IV decoder re-primes). ## PulseAudio / audio (Qt6 boards) - **HDMI audio dies after reboot/restart (issue #3112, follow-up to #3001).** Symptom: `Daemon startup failed` → "video will play without audio". Real reason (hidden behind the generic line): `pid.c: Daemon already running` / `pa_pid_file_create() failed` — a **stale `${XDG_RUNTIME_DIR}/pulse/pid`** (`/run/user/1000/pulse/pid`). In Docker, `/run` is NOT a per-boot tmpfs — it's the container's writable layer — so the pid file survives `docker restart` AND host reboot. `--daemonize=yes` self-re-execs and racily reads the stale PID as a live `pulseaudio` → aborts (intermittent). **Fix:** `rm -f "${XDG_RUNTIME_DIR}/pulse/pid"` in `start_pulseaudio()` before the `pulseaudio --daemonize` invocation. Debug: `docker restart` reproduces faster than a host reboot (persistent `/run`); capture the hidden reason with `--log-target=file:... --log-level=debug`. ## Config file ownership (legacy upgrade)
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기