# @aphrodite/streaming

@aphrodite/streaming — part of the Oshun monorepo.

TypeScript package. Built with tsc/tsup; consumed by other workspaces.

## Scripts

| Command          | Definition             |
| ---------------- | ---------------------- |
| `pnpm build`     | tsup                   |
| `pnpm dev`       | tsx watch src/index.ts |
| `pnpm start`     | node dist/index.js     |
| `pnpm lint`      | eslint src/            |
| `pnpm typecheck` | tsc --noEmit           |

## Layout

- `src/`

## Media-edge authentication and reconciliation

MediaMTX publisher external auth posts to `/api/ingest/rtmp/auth` with its
publisher grant as `token` (or `password`). The route accepts the canonical
`live-media.v1/aphrodite/<stream UUID>` path only. Bare UUIDs and canonical
sibling-tenant paths are rejected before publisher-grant lookup. The route name
is retained for compatibility, but the contract authorizes MediaMTX RTMP, SRT,
WebRTC, and RTSP publisher requests. A grant is accepted only for its exact
stream and an active broadcaster.

Configure these values for every streaming-service deployment:

| Variable                         | Purpose                                                                                                 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `MEDIAMTX_HOOK_SECRET`           | Separate shared secret required in `X-Media-Hook-Key` on lifecycle and manual-reconciliation callbacks. |
| `MEDIAMTX_CONTROL_URL`           | HTTP(S) base URL of the MediaMTX Control API, for example `http://mediamtx:9997`.                       |
| `MEDIAMTX_EDGE_ID`               | Stable, unique identity for this MediaMTX edge.                                                         |
| `MEDIAMTX_RECONCILE_INTERVAL_MS` | Poll interval from 1,000 to 300,000 ms; defaults to 15,000.                                             |

Lifecycle callbacks post `path`, `sourceType`, and `sourceId` to
`/api/ingest/rtmp/on-publish` and `/api/ingest/rtmp/on-publish-done`. They fail
closed if the hook secret, edge identity, stream, or durable publisher-edge
store is unavailable. MediaMTX external auth, lifecycle callbacks, edge
reconciliation, recording RTSP sources, and publisher provisioning all use the
same canonical tenant-scoped path. The separately shaped legacy `streamId` and
RTMP `app`/`name` callback bodies remain compatibility inputs; they do not make
bare UUID MediaMTX `path` values valid. Configure MediaMTX's Control API and the
same edge ID as well: the service reconciles immediately at startup and
periodically thereafter, recovering missed callbacks without overlapping polls.
A provider failure is reported and retried; it is never interpreted as an empty
edge.

Apply the Aphrodite database migrations before enabling the callbacks. Current
publisher state is stored in the RLS-protected
`live_media_publisher_edge_sessions` table and is retained across service
restarts.

The real local edge gate requires Docker, FFmpeg, and ffprobe. It starts pinned
MediaMTX and PostgreSQL images, rejects a forged SRT grant, publishes generated
encoded media, probes decoded edge HLS, and runs the selected Neith/FFmpeg
adapter against the same live RTSP source. It applies live-media migrations
`00005`–`00010` plus the optional-provider ledgers in `00013`–`00016`, drives
the durable job through claim/restart/expired-lease recovery and fenced
completion, packages the transcode into HLS and DASH, and publishes immutable
local origin objects. The same edge issues a Veritas-fixed publisher grant only
after registering `v1.veritas-live` in the forced-RLS PostgreSQL registry,
authenticates canonical SRT publishing through a reconstructed SQL-backed
control, decodes its HLS, and reauthenticates the persisted grant after database
client/service reconstruction. The same generated rehearsal bytes become a
durable Veritas HLS/DASH publication; tenant-fixed Veritas viewer and grant
controls then issue signed protected playback through SQL authority. A V3 Stage
tenant-fixed publisher control also registers the same stream UUID as
`v3.stage:<stream UUID>` in its isolated forced-RLS scope, issues and
authenticates a durable grant, rejects the Veritas secret while Veritas rejects
the Stage secret, and revokes the Stage grant without publishing fictitious
Stage content. A Playwright-driven Chromium run mounts the actual Rail
first-party-live panel, decodes both protocols, persists positive QoE and
presence, releases the lane, and proves the former token immediately receives
`VIEWER_SESSION_INACTIVE`. A separate Aphrodite SQL entitlement decision joins a
durable viewer lease and issues protected playback, whose manifest and segments
are decoded as H.264/AAC. The gate reconstructs the database client, shared
services, and disk origin; proves forced-RLS isolation, age-policy denial,
idempotent refresh, multi-node presence, exact QoE persistence, crash expiry,
token-fenced reconnect, explicit leave, and immediate session-bound origin
denial; rejects missing/tampered credentials; invalidates playback on durable
publication revocation; and reconciles publisher teardown. MediaMTX uses
environment-only configuration from an empty working directory so the test does
not consume a host configuration-file watcher. The same gate extracts a JPEG
from a real published transport segment, persists privacy-bounded analysis
evidence, replays it without a second provider call after database restart,
fences concurrent and stale analysis workers, and proves cross-tenant denial. It
also captures the live publisher through RTSP into a checksummed,
FFprobe-verified MP4 object, replays that durable result without a second FFmpeg
call, recovers and fences an abandoned recording lease, and denies a second
tenant. Finally, it provisions a nonextractable RSA KEK in SoftHSM, protects the
real HLS with AES-128, proves PostgreSQL contains only wrapped custody metadata,
rejects unauthenticated key delivery, and decodes the authenticated protected
H.264/AAC publication through FFprobe before restart replay and lease-fencing
checks. Build the Neith runner first:

```bash
cargo build --manifest-path ../../../libs/neith/stream-encoder/crates/neith-stream-encoder/Cargo.toml --bin neith-stream-transcoder
LIVE_MEDIA_EDGE_E2E=1 pnpm exec vitest run src/mediamtx-edge.e2e.spec.ts
```

Set `LIVE_MEDIA_MEDIAMTX_IMAGE` only when deliberately testing another MediaMTX
build. Set `LIVE_MEDIA_NEITH_TRANSCODER_BIN` only when the runner was built at a
non-default path; the default is
`libs/neith/target/debug/neith-stream-transcoder`.

## Durable transcode/publication jobs

The internal job API is real and tenant-fixed:

| Route                                            | Behavior                                 |
| ------------------------------------------------ | ---------------------------------------- |
| `POST /api/transcoding`                          | Enqueue or replay an idempotent job.     |
| `GET /api/transcoding`                           | List durable jobs with bounded filters.  |
| `GET /api/transcoding/:jobId?streamId=<uuid>`    | Read one stream-owned job.               |
| `DELETE /api/transcoding/:jobId?streamId=<uuid>` | Request cancellation or cancel due work. |

All four routes require `X-API-Key` to equal the nonempty deployment
`INTERNAL_API_KEY`; development mode never accepts an arbitrary presented key.
The caller supplies a stream UUID, an idempotency key, and bounded
media/rendition parameters. Tenant, job, attempt, execution, publication, and
lease identities are generated inside the Aphrodite control boundary.

Set `LIVE_MEDIA_PIPELINE_ENABLED=1` only after migration `00009` is applied and
the following required values are configured:

| Variable                         | Purpose                                   |
| -------------------------------- | ----------------------------------------- |
| `LIVE_MEDIA_PIPELINE_WORKER_ID`  | Stable identity for this worker process.  |
| `LIVE_MEDIA_NEITH_RUNNER_BINARY` | Absolute compiled Neith runner path.      |
| `LIVE_MEDIA_TRANSCODE_ROOT`      | Absolute private transcode-output root.   |
| `LIVE_MEDIA_PACKAGE_ROOT`        | Absolute private HLS/DASH packaging root. |
| `LIVE_MEDIA_S3_BUCKET`           | Create-only origin bucket.                |
| `LIVE_MEDIA_S3_REGION`           | AWS/MinIO signing region.                 |

Optional S3/MinIO settings are `LIVE_MEDIA_S3_ENDPOINT`,
`LIVE_MEDIA_S3_ACCESS_KEY_ID`, `LIVE_MEDIA_S3_SECRET_ACCESS_KEY`,
`LIVE_MEDIA_S3_FORCE_PATH_STYLE`, `LIVE_MEDIA_S3_SERVER_SIDE_ENCRYPTION`, and
`LIVE_MEDIA_S3_KMS_KEY_ID`. Lease/retry bounds are configurable with
`LIVE_MEDIA_PIPELINE_LEASE_MS`, `LIVE_MEDIA_PIPELINE_RENEWAL_MS`,
`LIVE_MEDIA_PIPELINE_INITIAL_RETRY_MS`, `LIVE_MEDIA_PIPELINE_MAXIMUM_RETRY_MS`,
and `LIVE_MEDIA_PIPELINE_IDLE_INTERVAL_MS`. Partial credentials, relative roots,
unsafe bounds, and invalid encryption settings prevent startup.

## Publication-bound content analysis

The optional analysis API is available only when the durable pipeline runtime is
enabled and `LIVE_MEDIA_CONTENT_ANALYSIS_ENABLED=1`. All routes require a valid
`X-API-Key` and bounded `X-Service-ID`:

| Route                                             | Behavior                                        |
| ------------------------------------------------- | ----------------------------------------------- |
| `GET /api/content-analysis/probe?streamId=<uuid>` | Probe object store, FFmpeg, policy, and vendor. |
| `POST /api/content-analysis`                      | Execute or replay one exact publication sample. |
| `DELETE /api/content-analysis/:operationId?...`   | Verify release of a succeeded one-shot result.  |

Apply migration `00013_live_media_content_analysis` and configure these values
before enabling the provider:

| Variable                                     | Purpose                                         |
| -------------------------------------------- | ----------------------------------------------- |
| `LIVE_MEDIA_CONTENT_ANALYSIS_API_KEY`        | Provider credential; never accepted by the API. |
| `LIVE_MEDIA_CONTENT_ANALYSIS_ENDPOINT`       | HTTPS OpenAI-compatible base URL.               |
| `LIVE_MEDIA_CONTENT_ANALYSIS_MODEL`          | Bounded provider model identity.                |
| `LIVE_MEDIA_CONTENT_ANALYSIS_POLICY_PROFILE` | Aphrodite-owned advisory policy reference.      |
| `LIVE_MEDIA_CONTENT_ANALYSIS_WORKER_ID`      | Stable lease-owner identity.                    |

Loopback HTTP is accepted only to exercise a local provider in tests. Optional
bounded controls are `LIVE_MEDIA_CONTENT_ANALYSIS_VENDOR_TIMEOUT_MS`,
`LIVE_MEDIA_CONTENT_ANALYSIS_VENDOR_RETRIES`,
`LIVE_MEDIA_CONTENT_ANALYSIS_LEASE_MS`,
`LIVE_MEDIA_CONTENT_ANALYSIS_RENEWAL_MS`,
`LIVE_MEDIA_CONTENT_ANALYSIS_MAXIMUM_ATTEMPTS`,
`LIVE_MEDIA_CONTENT_ANALYSIS_MAXIMUM_SOURCE_BYTES`,
`LIVE_MEDIA_CONTENT_ANALYSIS_MAXIMUM_FRAME_BYTES`, and
`LIVE_MEDIA_CONTENT_ANALYSIS_SAMPLER_TIMEOUT_MS`. Renewal must occur before
lease expiry. Requests name only an exact ready publication object and stable
idempotency identity; callers cannot upload frames, select another tenant or
adapter, supply provider keys, or request automatic moderation action.

## Durable live recording

The recording API is available only when the durable pipeline runtime is enabled
and `LIVE_MEDIA_RECORDING_ENABLED=1`. The create and status controls require the
deployment `INTERNAL_API_KEY` plus a bounded `X-Service-ID`:

| Route                                               | Behavior                                            |
| --------------------------------------------------- | --------------------------------------------------- |
| `GET /api/recording/probe?streamId=<uuid>`          | Probe publisher, object store, FFmpeg, and policy.  |
| `POST /api/recording`                               | Queue or replay one bounded recording operation.    |
| `GET /api/recording/operations/:id?streamId=<uuid>` | Read restart-safe queued/processing/terminal state. |

Apply migration `00014_live_media_recording` before enabling the provider. The
base pipeline S3/MinIO configuration is reused for private create-only recording
objects; these additional values are required:

| Variable                                | Purpose                                                     |
| --------------------------------------- | ----------------------------------------------------------- |
| `LIVE_MEDIA_RECORDING_ROOT`             | Absolute private FFmpeg attempt workspace.                  |
| `LIVE_MEDIA_RECORDING_WORKER_ID`        | Stable durable lease-owner identity.                        |
| `LIVE_MEDIA_RECORDING_RETENTION_POLICY` | Aphrodite-owned policy reference accepted by the queue.     |
| `MEDIAMTX_RTSP_BASE_URL`                | Operator-owned RTSP(S) base used to derive the stream path. |

`MEDIAMTX_CONTROL_URL` and `MEDIAMTX_EDGE_ID` must identify the same reconciled
edge. Callers provide no media URL, tenant, adapter, worker, object key, or
credential. They must name the exact current publisher connection, a unique
recording UUID, format (`mp4` or `matroska`), duration, retention reference,
operation UUID, and semantic idempotency key. Creation returns `202`; the
supervised worker performs real capture asynchronously. Interactive stop, DVR,
segment tracking, export jobs, and cleanup jobs remain explicit `501` gaps.

Optional bounds are `LIVE_MEDIA_RECORDING_POLICY_MAXIMUM_DURATION_SECONDS`,
`LIVE_MEDIA_RECORDING_MAXIMUM_OBSERVATION_AGE_MS`,
`LIVE_MEDIA_RECORDING_LEASE_MS`, `LIVE_MEDIA_RECORDING_RENEWAL_MS`,
`LIVE_MEDIA_RECORDING_MAXIMUM_ATTEMPTS`,
`LIVE_MEDIA_RECORDING_INITIAL_RETRY_MS`,
`LIVE_MEDIA_RECORDING_MAXIMUM_RETRY_MS`,
`LIVE_MEDIA_RECORDING_SHUTDOWN_GRACE_MS`,
`LIVE_MEDIA_RECORDING_COMPLETION_GRACE_MS`, and
`LIVE_MEDIA_RECORDING_IDLE_INTERVAL_MS`. Renewal must occur before lease expiry.
Process shutdown aborts the active FFmpeg capture, records retryable durable
state while its lease is current, and waits for the worker tick to drain.

## Durable HLS segment encryption

The segment-encryption API is available only when the durable pipeline runtime
is enabled and `LIVE_MEDIA_SEGMENT_ENCRYPTION_ENABLED=1`. Every route requires
the deployment `INTERNAL_API_KEY` plus a bounded `X-Service-ID`:

| Route                                                            | Behavior                                              |
| ---------------------------------------------------------------- | ----------------------------------------------------- |
| `GET /api/segment-encryption/probe?streamId=<uuid>`              | Probe publication, object store, and HSM custody.     |
| `POST /api/segment-encryption`                                   | Queue or replay one exact AES-128 HLS transformation. |
| `GET /api/segment-encryption/operations/:id?streamId=<uuid>`     | Read restart-safe queued/processing/terminal state.   |
| `GET /api/segment-encryption/operations/:id/key?streamId=<uuid>` | Recover exactly 16 bytes for a succeeded operation.   |

Apply migration `00015_live_media_segment_encryption` before enabling the
provider. The base pipeline S3/MinIO publication configuration is reused for
create-only protected objects; these values are required:

| Variable                                           | Purpose                                                |
| -------------------------------------------------- | ------------------------------------------------------ |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_WORKER_ID`          | Stable durable lease-owner identity.                   |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_KEY_REFERENCE`      | Aphrodite-owned public AES-128 policy reference.       |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_KEY_VERSION`        | Positive fixed policy version.                         |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_KEY_BASE_URI`       | HTTPS internal key-delivery base; loopback HTTP tests. |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_PKCS11_MODULE`      | Absolute PKCS#11 provider module path.                 |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_PKCS11_TOKEN_LABEL` | Operator-provisioned HSM/token label.                  |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_PKCS11_PIN`         | HSM PIN read only by the PKCS#11 backend process.      |
| `LIVE_MEDIA_SEGMENT_ENCRYPTION_HSM_KEK_ID`         | Existing nonextractable RSA-3072 KEK object ID.        |

`LIVE_MEDIA_SEGMENT_ENCRYPTION_PKCS11_EXECUTABLE` may select a non-default
`pkcs11-tool`. The configured key base URI must end at the mounted operations
collection (normally `/api/segment-encryption/operations`); the provider appends
the operation/key path and exact stream query. Lease/retry and graph bounds are
configurable with `LIVE_MEDIA_SEGMENT_ENCRYPTION_LEASE_MS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_RENEWAL_MS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_ATTEMPTS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_INITIAL_RETRY_MS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_RETRY_MS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_OBJECTS`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_MANIFEST_BYTES`,
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_SEGMENT_BYTES`, and
`LIVE_MEDIA_SEGMENT_ENCRYPTION_MAXIMUM_TOTAL_BYTES`. Renewal must precede lease
expiry. Partial HSM configuration, a mismatched key policy, unsafe remote HTTP,
and invalid bounds prevent startup.

The provider currently supports only HLS AES-128-CBC over complete MPEG-TS VOD
graphs. CENC/CBCS, vendor DRM/CDM licensing, viewer/client integration, and
protected-object/key cleanup are not exposed as working capabilities. Key
delivery is an internal authenticated media operation with strict no-store
headers, not an external DRM license endpoint.

## Durable visible watermarking

The watermark API is available only when the durable pipeline runtime is enabled
and `LIVE_MEDIA_WATERMARK_ENABLED=1`. Every route requires the deployment
`INTERNAL_API_KEY` plus a bounded `X-Service-ID`:

| Route                                                  | Behavior                                                |
| ------------------------------------------------------ | ------------------------------------------------------- |
| `GET /api/watermark/probe?streamId=<uuid>`             | Probe publication/object, FFmpeg, and HSM dependencies. |
| `POST /api/watermark`                                  | Queue or replay one exact visible-grid transform.       |
| `GET /api/watermark/operations/:id?streamId=<uuid>`    | Read restart-safe queued/processing/terminal state.     |
| `DELETE /api/watermark/operations/:id?streamId=<uuid>` | Delete output/evidence and durably fence recreation.    |

Apply migration `00016_live_media_watermark` before enabling the provider. The
base pipeline S3/MinIO publication configuration is reused for source reads and
create-only output. These values are required:

| Variable                                  | Purpose                                           |
| ----------------------------------------- | ------------------------------------------------- |
| `LIVE_MEDIA_WATERMARK_WORKER_ID`          | Stable durable lease-owner identity.              |
| `LIVE_MEDIA_WATERMARK_KEY_REFERENCE`      | Aphrodite-owned public watermark key policy.      |
| `LIVE_MEDIA_WATERMARK_KEY_VERSION`        | Positive fixed policy version.                    |
| `LIVE_MEDIA_WATERMARK_PKCS11_MODULE`      | Absolute PKCS#11 provider module path.            |
| `LIVE_MEDIA_WATERMARK_PKCS11_TOKEN_LABEL` | Operator-provisioned HSM/token label.             |
| `LIVE_MEDIA_WATERMARK_PKCS11_PIN`         | HSM PIN read only by the PKCS#11 backend process. |
| `LIVE_MEDIA_WATERMARK_HSM_KEK_ID`         | Existing nonextractable RSA-3072 KEK object ID.   |

`LIVE_MEDIA_WATERMARK_PKCS11_EXECUTABLE` may select a non-default `pkcs11-tool`.
The existing `LIVE_MEDIA_FFMPEG_BINARY` and `LIVE_MEDIA_FFPROBE_BINARY`
overrides are honored. Lease/retry, byte, quality, and execution bounds are
configurable with `LIVE_MEDIA_WATERMARK_LEASE_MS`,
`LIVE_MEDIA_WATERMARK_RENEWAL_MS`, `LIVE_MEDIA_WATERMARK_MAXIMUM_ATTEMPTS`,
`LIVE_MEDIA_WATERMARK_INITIAL_RETRY_MS`,
`LIVE_MEDIA_WATERMARK_MAXIMUM_RETRY_MS`,
`LIVE_MEDIA_WATERMARK_MAXIMUM_SOURCE_BYTES`,
`LIVE_MEDIA_WATERMARK_MAXIMUM_OUTPUT_BYTES`,
`LIVE_MEDIA_WATERMARK_MINIMUM_PSNR_DB`,
`LIVE_MEDIA_WATERMARK_FFMPEG_TIMEOUT_MS`, and
`LIVE_MEDIA_WATERMARK_IDLE_INTERVAL_MS`. Renewal must precede lease expiry.
Partial HSM configuration and invalid bounds prevent startup.

Callers name an operation/idempotency identity, exact ready publication,
published MPEG-TS source key and digest, and optionally a viewer session. They
cannot select the tenant, adapter, visible-grid profile, watermark payload, key,
output path, or worker. A non-null viewer session must be active and
entitlement-valid for the exact tenant and stream. The result is a visible keyed
8x8 grid with measured pixel extraction and PSNR evidence; it is not represented
as an invisible or forensically robust watermark.

## Protected origin authorization

`GET /media-origin/authorize/<exact-object-key>` is an authorization-subrequest
endpoint for the external HLS/DASH origin proxy. Forward the viewer's Bearer
grant and the exact requested object path. A valid current grant returns `204`
plus tenant/stream/publication routing headers; missing, malformed, expired,
wrong-object, or revoked grants all return an indistinguishable empty `401`. The
endpoint does not impersonate S3/CDN byte serving. The proxy must enforce the
returned decision and fetch from the private bucket.

Viewer issuance and origin verification must share:

- `LIVE_MEDIA_PLAYBACK_ENABLED=1`
- `LIVE_MEDIA_PLAYBACK_KEY_ID`
- `LIVE_MEDIA_PLAYBACK_HMAC_SECRET` (at least 32 bytes)
- `LIVE_MEDIA_ORIGIN_BASE_URL` (HTTPS, except loopback HTTP in tests)

Optional bounded settings are `LIVE_MEDIA_PLAYBACK_LIFETIME_SECONDS` and
`LIVE_MEDIA_PLAYBACK_CLOCK_SKEW_SECONDS`. Every authorization re-reads both the
forced-RLS publication ledger and the active viewer lease, so publication
revocation, lease expiry, reconnect fencing, and leave are immediate across
process restarts.

## Working on this package

The fastest local loop is via Nx:

```bash
pnpm nx test aphrodite-streaming
pnpm nx lint aphrodite-streaming
pnpm nx build aphrodite-streaming
```

When Nx is unhappy (usually because another worktree has duplicate project
names), bypass it with the underlying tools from this directory:

```bash
npx tsc --noEmit
npx vitest run
```

See the [root README](../../../README.md) for monorepo-wide conventions (catalog
deps, commit format, git workflow). See the
[Contributor Guide](../../../docs/CONTRIBUTING.md) for the full PR workflow.
