Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,13 @@ UI-delivered, or rendered-frame counters stop.
## Discord launch links

FrameRelay registers `framerelay://open/share/{token}` and `framerelay://open/watch/{token}` for the current Windows user. Launches are forwarded to the running app; share links wait for the user to choose a capture target and start sharing. Tokens are redeemed with the device identity and are not written to application logs.

### Discord Activity media option

`FRAMERELAY_WEBSOCKET_MEDIA_ENABLED` is a process environment variable, disabled unless its value parses as `true`. Set it before launching FrameRelay and restart the app. It is independent of preferences and does not load a desktop `.env` file.

The optional upload sends existing encoded H.264/Opus samples to a separate WebSocket forwarding service using a short-lived, source-bound RelayControl grant. When RTC uses AV1, an independent H.264 branch owns copied capture buffers; it does not downgrade RTC. Upload failures retry with bounded backoff and do not stop the existing RTC media stack.

RelayControl must enable `FeatureManagement__DiscordWebSocketMedia` (false by default), deploy its isolated `media-relay` container and set `MediaRelay__PublicBaseUrl` plus a distinct internal `MediaRelay__ServiceToken`. The bot does not need that service secret. Discord Activity URL Mapping must include `/media` to the relay and `/relay` to the API. See [wire protocol](docs/protocol/discord-media-v1.md).

Local validation checks copies, queue bounds, default-off configuration and RTC pipeline regressions. Browser fixtures in the Activity/API repositories verify actual decoded pixels and PCM over the forwarding service. A live Discord/Windows publisher test is still required before rollout.
61 changes: 61 additions & 0 deletions docs/protocol/discord-media-v1-vectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[
{
"name": "config",
"hex": "46524d31010100000100000000000000850000000000000040420f00000000001b410000000000007b22766964656f223a7b22636f646563223a22617663312e343265303166222c227769647468223a3634302c22686569676874223a3336302c22666f726d6174223a22616e6e657862227d2c22617564696f223a7b22636f646563223a226f707573222c2273616d706c6552617465223a34383030302c226368616e6e656c73223a327d7d",
"type": 1,
"timestampUs": 1000000
},
{
"name": "video",
"hex": "46524d310102010001000000010000000e0000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"type": 2,
"timestampUs": 1000000
},
{
"name": "audio",
"hex": "46524d31010300000100000002000000030000000000000040420f00000000001b41000000000000f8fffe",
"type": 3,
"timestampUs": 1000000
},
{
"name": "keyframe-request",
"hex": "46524d31010400000100000003000000000000000000000040420f00000000001b41000000000000",
"type": 4,
"timestampUs": 1000000
},
{
"name": "bad-version",
"hex": "46524d310202010001000000010000000e0000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "bad-type",
"hex": "46524d310109010001000000010000000e0000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "bad-flags",
"hex": "46524d310102020001000000010000000e0000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "zero-generation",
"hex": "46524d310102010000000000010000000e0000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "reserved",
"hex": "46524d310102010001000000010000000e0000000100000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "bad-length",
"hex": "46524d31010201000100000001000000630000000000000040420f00000000001b41000000000000000000016742e01f000000016588",
"invalid": true
},
{
"name": "truncated",
"hex": "46524d31",
"invalid": true
}
]
9 changes: 9 additions & 0 deletions docs/protocol/discord-media-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Discord media v1

WebSocket subprotocol: `framerelay-media-v1`. First complete text message is `{grant}`; every subsequent message is binary. Credentials never appear in URLs. The relay validates grants with RelayControl and renews 30-second leases every 10 seconds.

Header: 40 bytes, little endian. FRM1 at 0, version 1 at 4, type at 5 (1 configuration, 2 video, 3 audio, 4 keyframe request), flags u16 at 6 (bit 0 video keyframe only), generation u32 at 8, sequence u32 at 12, payload length u32 at 16, reserved zero u32 at 20, timestamp i64 microseconds at 24, duration i64 microseconds at 32. Whole message limit 8 MiB. Reject zero generation, invalid types/flags, negative or JavaScript-unsafe times, trailing or missing bytes.

Configuration payload is UTF-8 JSON `{video:{codec,width,height,format:"annexb"},audio:{codec:"opus",sampleRate:48000,channels:2}|null}`. H.264 codec string comes from SPS profile/compatibility/level. Video contains complete Annex-B access units, parameter sets on recovery keyframes. Audio contains complete Opus packets. Control has empty payload. No base64 media.

Each generation starts with configuration and a fresh keyframe. Sequences increase strictly; gaps, backlog, reconnect and codec/size changes require recovery. Queues bound both 64 messages and 16 MiB. A viewer may request one keyframe per second. Supported browser codecs do not imply Discord support; live validation is required before rollout.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Discord WebSocket media implementation and verification

Approved design: [spec](../specs/2026-09-30-discord-websocket-media-design.md). Execution covered RelayControl, desktop publisher and Go repository Activity. Original dirty checkouts were preserved; changes are local and not deployed.

## Changes

| Repository | Main files | Result |
| --- | --- | --- |
| RelayControl | API Features, Session/PublicRoom/Activity endpoints, MediaRelayGrantService, capability cleanup, Program | Official Microsoft.FeatureManagement with existing-feature defaults true; optional media default false; scoped grants and fenced leases |
| RelayControl | services/SonicRelay.MediaRelay, src/SonicRelay.MediaProtocol, Dockerfile/Compose/CI, protocol fixtures | Separate single-instance forwarder; no transcode; 8 MiB messages and 64-message/16 MiB queues; lease renewal; persistent slow-viewer disconnect |
| Desktop | AppComposition, RtcVideoPublishHost, MediaRelayApiClient, Media/WebSocket | Process opt-in upload; shared H264/Opus samples; independent copied-frame H264 branch when RTC uses AV1 |
| Go/Activity | media-protocol/client/player/audio-playback, main, canvas, tests, CI | WebSocket first-message grant; WebCodecs capability checks; bounded canvas/PCM playback and disposal; retry network failures with fresh admission |

## Feature boundaries

`FeatureManagement__PublicRooms=true`, `FeatureManagement__DiscordActivity=true`, `FeatureManagement__DuplexAudio=true` and `FeatureManagement__ScreenShare=true` preserve existing behavior when missing. Explicit false overrides defaults. Public-room HTTP and background startup are gated. Activity authorization/admission and bot Activity-only launch routes return404 when disabled; desktop completion/watch fallback routes remain. Disabled duplex creation and screen-share creation/code join/ID join/Activity admission reject before mutation. Existing session end, device identity, auth, retention, TURN and generic signaling stay available.

The new API `FeatureManagement__DiscordWebSocketMedia=false` and publisher `FRAMERELAY_WEBSOCKET_MEDIA_ENABLED=false` require explicit opt-in. Desktop reads a process variable; it does not load .env. Docker env changes require container recreation. Separate `MediaRelay__ServiceToken` is shared only by API/forwarder, not bot/Activity bundle. `MediaRelay__PublicBaseUrl` configures desktop upload; Discord `/media` mapping serves Activity sockets while `/relay` remains the API mapping.

## Evidence

- API solution restore/build verified with zero warnings/errors; full suite: 339 pass (274 API integration, 12 protocol, 6 forwarder, 40 virtual publisher, 7 signaling).
- Desktop focused media tests:50 pass, including fragmented keyframe control and real ClientWebSocket upload, raw/encoded memory ownership, default off, bounds and existing pipeline regression tests. API client/session tests:9 pass.
- Activity:26 Node tests pass; TypeScript and Vite production build pass. Tests include shared binary vectors, invalid headers, support check, keyframe recovery, stale epochs, closed decoder replacement, cleanup and no RTC/TURN access.
- Protocol fixture SHA256 identical in all three repositories: `D91E1E99F4E20C296E7F48FF3328C5D9D0A7D98E993C599BB277909C9D6A237A`.
- Real headless Edge fixture, explicit Run button click: native generated H264/Opus through the actual forwarding registry and sockets,3 rendered video frames,9 decoded audio blocks, green canvas pixel `[0,255,1,255]`, nonzero PCM energy154.314, actual codec `avc1.42c00b`. Audio was measured as PCM; audible output was not claimed.
- Separate forwarder Release publish succeeds. Deployment and infra Compose configurations validate with nonsecret fixtures.
- Repeated release of a removed admission is idempotent; the regression test passes.
- Fresh independent review identified closed decoder recovery and persistent slow viewers; both reproduced and corrected with regression tests. Slow viewer disconnects after three queue overflows within ten seconds; another viewer continues receiving.

## Validation limits

Docker daemon unavailable on this machine: image build/run and production deployment were not verified. No live Discord Activity, real Windows capture opt-in, audible A/V synchronization, portal mapping or simultaneous two-machine RTC viewer was exercised. The browser fixture verifies native codec decoding and transport locally; it is not Discord runtime proof. Keep both new flags false until the documented live rollout checks pass. No dependency upgrades were made; only the requested official Microsoft.FeatureManagement4.7.0 package was added to the API.

## Local fixture

Run API repository `dotnet run --project tests/SonicRelay.MediaRelay.BrowserFixture` (localhost5174), start Activity Vite on5173, open `/tests/browser-media-fixture.html` and click Run. Fixture fake credentials are local test data; this project is not included in the production images. The optional dependency-free `activity/tests/run-browser-media-fixture.mjs` runs against an owned Chromium debugging instance on9227, opens/closes its fixture tab and supplies the button click.
Loading
Loading