# Private stream testing

Lumi Stream Testing previews the real OBS output without publishing it to the
normal stream destination. Start it from the core **Stream testing** page in
Lumi Companion, then watch it at **Admin > Stream testing**.

## Architecture

- The existing paired-device WebSocket authenticates session creation, OBS
  metrics, caption cues, stop requests, and server-ended notifications.
- The existing same-user OBS Bridge snapshots the complete current OBS service,
  temporarily installs an expiring custom service, reports real output facts,
  and restores the snapshot.
- Companion protects the minimum recovery record with Windows DPAPI and writes
  it atomically before OBS is redirected. Normal stop, server failure, expiry,
  Companion exit, WebSocket loss, and OBS restart all use that restore path.
- Core lazily starts a Lumi-managed MediaMTX process. MediaMTX accepts the
  private RTMP(S) publisher and repackages the original source into low-latency
  HLS without transcoding, scaling, a GPU requirement, or an adaptive ladder.
- The MediaMTX API, metrics, and HLS listeners bind only to loopback. The Admin
  WebUI accesses HLS through an authenticated, no-store, same-origin streaming
  proxy. RTSP, WebRTC, SRT, playback, pprof, and recording are disabled.
- Companion explicitly starts a server-hosted `stream_test` transcription
  session before redirecting OBS. It reuses the selected source and model while
  keeping normal live-caption delivery disabled. Interim revisions progressively
  reveal each caption in the private player; a finalized caption remains for a
  length-aware readability window, then ends when stale or when the next
  utterance replaces it. The player's Captions control toggles a wrapping,
  multiline Lumi caption layer immediately as revisions arrive instead of
  delaying display against the HLS media timeline. No second transcription
  pipeline and no local speech inference are added.

Media segments stay in bounded MediaMTX memory and roll out of the short live
window. Lumi does not retain a raw stream recording or create temporary HLS
files. Session state, captions, and diagnostic facts disappear when the test
ends or Lumi restarts.

## Managed setup

No operating-system package installation, FFmpeg installation, or executable
path is required. Open **Admin > Stream testing** and choose **Install
MediaMTX**. Lumi then:

1. downloads the pinned official Windows x64 archive only after confirmation;
2. enforces download and extraction size limits;
3. verifies the exact SHA-256 checksum and archive layout;
4. probes the executable and exact expected version; and
5. atomically installs it while retaining the previous installation until the
   replacement is known to be valid.

**Repair installation** repeats the same verified workflow. **Run health
check** starts MediaMTX with deny-by-default paths and tests its loopback API,
metrics, and HLS listeners; opening the page alone never downloads or starts a
runtime. `LUMI_MEDIAMTX_PATH` remains an advanced manual override for another
platform or operator-managed binary, but normal Windows installations do not
need it.

The managed release and checksum source are recorded in
`src/services/mediamtx-runtime-manifest.json`. MediaMTX's licence is retained in
the downloaded runtime and documented in
[`third-party/MediaMTX-MIT.txt`](third-party/MediaMTX-MIT.txt).

## Ingest network configuration

Lumi derives the network policy from the authenticated Companion pairing
origin. A Companion paired through `localhost`, `127.0.0.1`, or `::1` always
receives direct loopback RTMP on the configured local listener port. A
non-local Companion must have paired through HTTPS and always receives RTMPS
at the same paired Lumi hostname.

In production, Nginx Proxy Manager owns the public certificate and terminates
TLS:

```text
OBS → rtmps://<paired Lumi hostname>:1936
    → Nginx Proxy Manager decrypts TLS
    → plain RTMP to Lumi:19350
```

MediaMTX binds plain RTMP on `0.0.0.0:19350` for a production session. It never
requests, loads, renews, or stores a TLS certificate. Configure an NPM TCP
stream that listens on public port `1936` with TLS enabled and forwards to the
Lumi host on port `19350`. The WebUI reverse-proxy settings can change both
ports; values are stored in Lumi's settings database and apply to the next
session.

The collapsed **Reverse-proxy ingest** section in **Admin > Stream testing**
also provides **Check external access**. The check connects to the current
public Lumi hostname and port with TLS and SNI, validates the public
certificate, then completes an RTMP handshake through NPM to MediaMTX. A
server-side failure can still mean that the router does not support NAT
loopback/hairpinning, so confirm the path from an external network when the NPM
and firewall configuration otherwise look correct.

Every session receives an exact `lumi-test/<uuid>` path and high-entropy
publisher credentials. Only that path can be published, and the generated
MediaMTX configuration contains SHA-256 credential hashes rather than
plaintext credentials.

RTMPS is required for every non-local pairing. Unencrypted RTMP is advertised
only to an authenticated loopback pairing. Use the existing paired Lumi
hostname; no extra Stream Testing domain is required. Restrict direct access
to the local MediaMTX listener with the host firewall so production publishers
must enter through NPM.

Optional session limits remain:

```text
LUMI_STREAM_TEST_MAX_MS=1800000
LUMI_STREAM_TEST_INACTIVITY_MS=45000
```

## Safety and recovery

Companion refuses to start while OBS is streaming or recording. During a test
its navigation status reads **PRIVATE TEST ACTIVE**. The DPAPI recovery file is
removed only after OBS confirms that the saved service type and complete
settings JSON were restored and saved.

If OBS was closed or unavailable during cleanup, open OBS and leave Companion
running. Automatic restoration resumes when the same-user bridge reconnects.
The **Repair restoration** action retries the same operation and does not create
a new session.

The receiver stops on maximum duration, lack of inbound media, process failure,
server shutdown, Companion disconnect, or administrator stop. OBS status
messages do not reset the media-inactivity clock. Only one session is allowed.
MediaMTX is reconfigured to a deny-by-default path set after every session.

## Diagnostics and verification

The Admin page reports real OBS bitrate, dropped frames, congestion, MediaMTX
path readiness, media tracks, inbound/outbound bytes, HLS viewers, discarded
frames, player latency, buffer health, stalls, errors, and caption delay.
Only an allowlist of MediaMTX API and metrics fields is exposed. Publisher
credentials are redacted from status, timeline entries, process logs, and
errors.

The runtime health check replaces the former transcoded synthetic test pattern.
Focused verification uses a fake MediaMTX process with real loopback API,
metrics, and HLS listeners:

```text
npm run verify:stream-testing
dotnet run --project companion/tests/Lumi.Companion.Core.Tests/Lumi.Companion.Core.Tests.csproj
```
