# Media service (SFU / MCU) — AI deployment prerequisites
**Document type:** Requirements / handoff (pre-SPEC)
**Date:** 2026-06-12
**Audience:** PO / sysop → AI agent for full deploy + tune on fresh Alpine HVM(s)
**Related:** [docs/specs/20100612_1_scaling.md](../docs/specs/20100612_1_scaling.md), [docs/DRs/20100612_1_scaling.md](../docs/DRs/20100612_1_scaling.md), [examples/sfu/README.md](../examples/sfu/README.md), [docs/20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) §SFU
**PDF:** `tmp/20260612-MEDIASERVICE-AI-requirements.pdf` — regenerate with `.venv-pdf/bin/python docs/_pdf_build/md_to_pdf.py` on the copy under `tmp/`
**Code names (product):** **SPIDERWEB** = SFU-alike, **SPIDER** = MCU-alike ([draft glossary](../docs/drafts/20100611_1_net_topology_glossary.txt))
---
---
---
## Table of contents
- [1. Executive answer — HVM vs Docker vs monolith BE](#1-executive-answer-hvm-vs-docker-vs-monolith-be)
- [2. What you give the AI on day zero](#2-what-you-give-the-ai-on-day-zero)
- [3. Recommended instance layout (up to 3 HVMs)](#3-recommended-instance-layout-up-to-3-hvms)
- [4. Network, DNS, and firewall prerequisites](#4-network-dns-and-firewall-prerequisites)
- [5. Integration with existing BE (isolation model)](#5-integration-with-existing-be-isolation-model)
- [6. SFU vs MCU stack choice](#6-sfu-vs-mcu-stack-choice)
- [7. Packages and software (installed later by AI)](#7-packages-and-software-installed-later-by-ai)
- [8. Sizing and bandwidth (200/200 Mbit context)](#8-sizing-and-bandwidth-200200-mbit-context)
- [9. AI workflow after prerequisites are met](#9-ai-workflow-after-prerequisites-are-met)
- [10. Out of scope for first media PoP](#10-out-of-scope-for-first-media-pop)
- [11. PO checklist (copy before handoff)](#11-po-checklist-copy-before-handoff)
- [12. Changelog](#12-changelog)
---
## 1. Executive answer — HVM vs Docker vs monolith BE
### Recommendation (DR-aligned)
| Approach | Verdict | Why |
|----------|---------|-----|
| **SFU/MCU on current androidcast BE monolith** | **No** | PHP/MariaDB/RSSH/Gitea share CPU/RAM/NIC; `pm.max_children=5` today; blast radius; [INFRA.md](../docs/INFRA.md) keeps `:8089` as **other** Janus stack |
| **Fresh dedicated Alpine HVM(s)** | **Yes (primary)** | Isolated **media plane**; snapshots; UDP/WebRTC without touching hub/crashes |
| **Docker on dedicated HVM** | **Yes (preferred delivery)** | Reproducible Janus/mediasoup/coturn/ffmpeg; easy rollback; same host still “empty” at handoff |
| **Docker on existing BE** | **No** | Does not isolate bandwidth or failure domain |
**Practical model:** PO provides **1–3 empty Alpine HVMs** with SSH + DNS. AI installs **Docker (+ compose)** on them and runs **SPIDERWEB (SFU)** or **SPIDER (MCU)** stacks in containers. **Control plane** (signaling auth, room lifecycle, RBAC) talks to existing crashes BE via **HTTPS/OpenAPI-style APIs** — no co-location of MariaDB or PHP-FPM with media.
```mermaid
flowchart TB
subgraph Today["Today — monolith BE (unchanged)"]
FE["Gentoo FE apps.f0xx.org"]
BE["Alpine BE :80
PHP MariaDB Gitea RSSH"]
end
subgraph New["New — media PoP (1–3 HVMs)"]
MED["media-1 Alpine HVM"]
DOCK["Docker: SFU + TURN + signaling sidecar"]
end
MOB["Android app"] --> FE
FE --> BE
MOB -.->|"WebRTC UDP/TCP
post-alpha"| MED
DOCK --> MED
BE -.->|"REST: token room RBAC
OpenAPI"| DOCK
class New fill:#e8f5e9,stroke:#2e7d32
```
**Not too heavy:** one HVM + Docker Compose is enough for **lab/stress**; three HVMs match **prod-shaped** split (SFU / TURN / transcode). Start with **1**; split when metrics or PO asks.
---
## 2. What you give the AI on day zero
Minimum handoff — **no media packages required yet** on the empty machine.
### 2.1 Access
| Item | Required | Example |
|------|----------|---------|
| **SSH target** | Yes | `ssh media@media-lab.example.org` or intra `10.7.x.x` |
| **Sudo** | Yes | passwordless `sudo` for `apk`, `rc-service`, docker |
| **SSH key** | Yes | laptop → HVM; optional: HVM → git read for deploy scripts |
| **Jump host** | If no public SSH | `ProxyJump` via `f0xx-monstro` / `alpine-be` (document in `~/.ssh/config`) |
### 2.2 Machine identity
| Item | Required | Notes |
|------|----------|-------|
| **Hostname** | Yes | e.g. `media-1.intra.raptor.org` |
| **OS** | Yes | Alpine 3.x x86_64 (match prod BE) |
| **vCPU / RAM** | Yes | See [§8](#8-sizing-and-bandwidth-200200-mbit-context) |
| **Root disk** | ≥ 40 GiB SSD | containers + logs |
| **Intra IP** | Yes | `10.7.0.0/8` address stable across reboots |
| **Public reach** | For external test | DNS name OR FE proxy only (see [§4](#4-network-dns-and-firewall-prerequisites)) |
### 2.3 PO-owned (snapshots, DNS, firewall)
| Item | Owner | AI needs |
|------|-------|----------|
| **VM snapshots** | PO | “Snapshot before AI session” + rollback name |
| **External DNS** | PO | A/AAAA or CNAME for WebRTC/signaling test |
| **Firewall / DNAT** | PO | UDP/TCP port list from [§4](#4-network-dns-and-firewall-prerequisites) opened on router/FE |
| **TLS certs** | PO or AI via DNS-01 | Let’s Encrypt on FE or on media host |
### 2.4 Integration secrets (existing BE — read-only API)
| Item | Required | Purpose |
|------|----------|---------|
| **Crashes base URL** | Yes | `https://apps.f0xx.org/app/androidcast_project/crashes` |
| **Service account or API token** | Yes | Mint rooms / validate session (new `sfu` API or extend OpenAPI) |
| **RBAC model** | Reference | Same pattern as remote access / short links |
| **Gitea / deploy** | Optional | Pull `android_cast` branch `feature/sfu-relay` or deploy tarball |
AI does **not** need MariaDB root on monolith BE if all integration is **HTTP APIs**.
### 2.5 Decisions PO should state (or defaults)
| Decision | Default if silent |
|----------|-------------------|
| **Topology code name** | **SPIDERWEB** (SFU) first; MCU later |
| **Engine** | **mediasoup** (greenfield) — Janus on `:8089` is **other** stack, avoid unless PO picks Janus on **new** host |
| **Instance count** | **1** for lab; **3** for prod-shaped |
| **Public path** | Subdomain `media.apps.f0xx.org` → FE → media HVM (keeps TLS at FE) |
| **TURN** | **coturn** on same HVM (lab) or instance 2 (prod) |
---
## 3. Recommended instance layout (up to 3 HVMs)
### 3.1 Lab — single HVM (minimum handoff)
```mermaid
flowchart LR
subgraph HVM1["media-1 (single Alpine HVM)"]
NG["nginx :443 reverse proxy"]
SIG["signaling sidecar
Node or Go"]
SFU["mediasoup / Janus SFU"]
TURN["coturn STUN/TURN"]
FF["ffmpeg worker optional"]
end
Client["App / browser"] --> NG
NG --> SIG
Client --> TURN
Client --> SFU
SIG --> SFU
BE["Existing BE PHP"] -.->|HTTPS| SIG
```
All services in **one docker-compose** stack; fine for stress and first WAN cast tests.
### 3.2 Prod-shaped — three HVMs
| VM | Role | Containers / daemons |
|----|------|----------------------|
| **media-1** | **SPIDERWEB SFU** | mediasoup (or Janus), nginx edge, signaling sidecar |
| **media-2** | **TURN + signaling** | coturn, optional dedicated signaling if scaled |
| **media-3** | **Transcode / MCU path** | ffmpeg workers, optional MCU (SPIDER) if not SFU-only |
```mermaid
flowchart TB
FE["FE TLS"] --> M1["media-1 SFU"]
FE --> M2["media-2 TURN"]
APP["Android"] -->|UDP WebRTC| M1
APP -->|UDP TURN| M2
BE["Monolith BE APIs"] -->|REST| M1
M1 --> M3["media-3 ffmpeg optional"]
```
PO can hand **one** machine first; AI documents split boundaries so instances 2–3 are **additive**, not rewrite.
---
## 4. Network, DNS, and firewall prerequisites
### 4.1 Traffic paths
```mermaid
flowchart TB
Internet["Internet clients"]
Router["Router 200/200 Mbit"]
FE["Gentoo FE"]
M["Media HVM(s)"]
BE["Androidcast BE :80"]
Internet --> Router --> FE
FE -->|"HTTPS signaling
wss / REST"| M
FE --> BE
Internet -->|"UDP WebRTC
after DNAT or 1:1"| M
```
**Do not** attach media to androidcast **`:8089`** vhost ([INFRA.md](../docs/INFRA.md) §2).
### 4.2 Ports to plan (PO opens on FE/router → media HVM)
| Port / proto | Service | Notes |
|--------------|---------|-------|
| **443/tcp** | HTTPS / WSS signaling | Via FE reverse proxy recommended |
| **3478/udp,tcp** | STUN/TURN (coturn) | Required for many NATs |
| **49152–65535/udp** | WebRTC media (ICE) | Range size depends on engine; document in SFU SPEC |
| **8080/tcp** | SFU HTTP API (internal) | **Not** public; FE or intra only |
| **22/tcp** | SSH | Intra or VPN only in prod |
Exact ICE port range: set in mediasoup/Janus config during deploy; PO must allow **UDP range** on path to media HVM.
### 4.3 DNS names (examples)
| Name | Points to | Use |
|------|-----------|-----|
| `media.apps.f0xx.org` | FE → proxy → media-1 | Public signaling + health |
| `turn.apps.f0xx.org` | FE or media-2 | TURN URI in app |
| `media-lab.intra.raptor.org` | 10.7.x.x | AI + sysop intra testing |
---
## 5. Integration with existing BE (isolation model)
Media HVM **must not** share MariaDB or PHP-FPM with crashes console. Integration is **API-only**:
```mermaid
sequenceDiagram
participant App as Android app
participant FE as FE nginx
participant BE as Crashes BE PHP
participant SIG as Media signaling
participant SFU as SFU engine
App->>FE: HTTPS session / cast token
FE->>BE: existing auth APIs
App->>SIG: WSS create room (bearer)
SIG->>BE: POST validate token / RBAC
BE-->>SIG: room allowed + ttl
SIG->>SFU: create room
App->>SFU: WebRTC publish/subscribe UDP
```
| Integration | Method | Existing artifact |
|-------------|--------|-------------------|
| Health gate | GET | `api/sfu_health.php` (stub today) |
| Feature flag | config | `config.php` → `sfu.enabled`, URLs |
| Room mint / revoke | **New REST** | OpenAPI under crashes or `media-api` submodule |
| Auth | Bearer + session RBAC | Same as remote access / short links pattern |
| Metrics | POST ingest | Optional → `graph_upload.php` or new series |
| No direct DB | — | Media DB (if any) is **local** to HVM (Redis/SQLite) |
**OpenAPI:** extend [examples/crash_reporter/backend](../examples/crash_reporter/backend) or new `backend/media-signaling` repo later — AI needs **written** API contract before prod wire-up.
---
## 6. SFU vs MCU stack choice
| | **SPIDERWEB (SFU)** | **SPIDER (MCU)** |
|---|---------------------|------------------|
| **Use** | Multiparty cast; subscribers receive per-stream | Mix down; single output; higher CPU per room |
| **Engine candidates** | **mediasoup** (preferred greenfield), Janus VideoRoom | Janus MCU plugin, ffmpeg bridge, custom |
| **CPU** | Moderate per stream; scales with subscribers | High per room (encode) |
| **Latency** | Lower | Higher |
| **First milestone** | **Yes** | Phase 2 / media-3 |
**Janus on existing `:8089`:** treat as **foreign** — do not piggyback androidcast nginx. New Janus instance on **media HVM** only if PO rejects mediasoup.
**Docker vs bare metal on HVM:** use **Docker** for SFU/TURN/signaling; host only needs Alpine + docker + nginx + firewall tools.
---
## 7. Packages and software (installed later by AI)
PO delivers **almost empty** Alpine; AI installs when handoff complete.
### 7.1 Host (Alpine packages)
```text
# Baseline (all media HVMs)
openssh sudo bash curl wget git ca-certificates
nginx openssl iptables ip6tables
docker docker-cli-compose
# Optional host tools
tcpdump bind-tools jq python3
# If bare-metal fallback (no Docker) — not recommended first
# janus-gateway mediasoup — prefer official containers
```
### 7.2 Container images (typical compose)
| Service | Image / build | Role |
|---------|---------------|------|
| **mediasoup** or **janus** | Official or pinned Dockerfile in repo | SFU |
| **coturn** | `coturn/coturn` | STUN/TURN |
| **signaling** | Custom Node/Go from repo | Rooms, BE API client |
| **nginx** | `nginx:alpine` or host nginx | TLS termination at edge or intra |
| **ffmpeg** | `jrottenberg/ffmpeg` or alpine | Transcode sidecar (media-3) |
| **redis** | `redis:alpine` | Room state optional |
### 7.3 Repo deliverables (AI produces after deploy)
| Artifact | Location (proposed) |
|----------|---------------------|
| `docker-compose.yml` | `examples/sfu/deploy/` or `backend/media-pop/` |
| nginx FE fragment | `examples/sfu/deploy/nginx.fe-media.snippet` |
| BE config keys | `config.example.php` → `sfu.*` (already stubbed) |
| Smoke script | `examples/sfu/scripts/smoke_sfu.sh` |
| OpenAPI | `examples/sfu/openapi.yaml` |
---
## 8. Sizing and bandwidth (200/200 Mbit context)
WAN **200/200 Mbit** sits behind FE; media UDP may be **DNAT** to media HVM.
| Profile | vCPU | RAM | Notes |
|---------|------|-----|-------|
| **Lab / stress (1 HVM)** | **4–8** | **8–16 GiB** | SFU + TURN + signaling; ~10–30 parallel WebRTC streams |
| **Prod SFU node** | **8+** | **16 GiB** | Dedicated NIC budget; see [SPEC §12.3](../docs/specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) |
| **TURN only** | **2** | **4 GiB** | Mostly relay bandwidth |
| **ffmpeg node** | **8+** | **16–32 GiB** | CPU-bound transcodes |
**Bandwidth rule of thumb (SFU):**
| Streams | Approx WAN need |
|---------|-----------------|
| 10 × 1 Mbps video | **~10–15 Mbps** + overhead |
| 50 × 1 Mbps | **~50–80 Mbps** — approaches 200 Mbit shared with rest of estate |
| 100+ viewers | **CDN/SFU PoP or second site** — not single 200 Mbit |
Stress test: measure on **intra** first, then **public DNS** through FE.
---
## 9. AI workflow after prerequisites are met
```mermaid
flowchart TD
A["PO: empty HVM + SSH + DNS + firewall list"] --> B["AI: verify ssh sudo intra ping"]
B --> C["AI: apk + docker + compose"]
C --> D["AI: deploy SFU stack lab compose"]
D --> E["AI: nginx FE fragment + BE sfu.enabled URLs"]
E --> F["Smoke: sfu_health + WSS + ICE"]
F --> G{"PO: load test OK?"}
G -->|yes| H["Document + optional 2nd/3rd HVM split"]
G -->|no| I["Tune UDP range CPU docker limits"]
I --> F
```
| Step | AI action | PO action |
|------|-----------|-----------|
| 0 | — | Snapshot empty VM |
| 1 | Probe CPU/RAM/NIC, `apk update` | Confirm DNS/firewall |
| 2 | Install docker, compose, pin versions | — |
| 3 | Bring up mediasoup + coturn + signaling | — |
| 4 | FE nginx `location /sfu/` or subdomain proxy | Reload FE nginx |
| 5 | Enable `sfu.enabled` + URLs on monolith BE **only** | Sync config.php |
| 6 | Run smoke + optional load/WebRTC client | Review metrics |
| 7 | Snapshot “golden” media PoP | — |
---
## 10. Out of scope for first media PoP
- Replacing LAN UDP cast (alpha path unchanged)
- Moving crashes/tickets/Gitea to media HVM
- Full GEO policy ([SPEC §14](../docs/specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) — later with F3/F10
- Production payments / recording storage
- Merging with Janus stack on BE `:8089`
---
## 11. PO checklist (copy before handoff)
```text
[ ] Alpine 3.x HVM provisioned (1–3); specs noted
[ ] SSH + sudo for AI; key installed
[ ] Intra IP 10.7.x.x stable
[ ] External DNS name for test (or FE-only proxy agreed)
[ ] Firewall: UDP 3478 + WebRTC range → media HVM
[ ] Snapshot taken (empty baseline)
[ ] Decision: SPIDERWEB (SFU) first — mediasoup default
[ ] Crashes BE base URL + API token for room validation
[ ] Confirm: do NOT use androidcast :8089 / monolith BE for SFU
[ ] Optional: 2nd/3rd HVM reserved for TURN + ffmpeg
```
---
## 12. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial prerequisites — HVM vs Docker, 3-instance layout, BE API isolation, PO checklist |
---
**Documentation index:** [docs/README.md](../docs/README.md)