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
45 changes: 43 additions & 2 deletions .github/workflows/vps-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,50 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

publish_media_image:
name: Publish isolated media image
runs-on: ubuntu-latest
needs: [build, test]
if: github.event_name != 'pull_request' && needs.build.outputs.has_dotnet_project == 'true'
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve media tags
id: image
shell: bash
run: |
image="ghcr.io/${{ github.repository_owner }}/sonicrelay-media:sha-${GITHUB_SHA}"
echo "image=$image" >> "$GITHUB_OUTPUT"
{
echo 'tags<<EOF'
echo "$image"
if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
echo 'ghcr.io/${{ github.repository_owner }}/sonicrelay-media:latest'
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v6
with:
context: .
file: ./MediaRelay.Dockerfile
push: true
tags: ${{ steps.image.outputs.tags }}
cache-from: type=gha,scope=media
cache-to: type=gha,scope=media,mode=max
deploy:
name: Deploy to VPS over SSH
runs-on: ubuntu-latest
needs: publish_image
needs: [publish_image, publish_media_image]
if: >-
${{
needs.publish_image.result == 'success' &&
Expand Down Expand Up @@ -306,13 +346,14 @@ jobs:
uses: appleboy/ssh-action@v1
env:
IMAGE: ${{ needs.publish_image.outputs.image }}
MEDIA_IMAGE: ${{ needs.publish_media_image.outputs.image }}
APP_DIR: ${{ steps.ssh_target.outputs.app_dir }}
with:
host: ${{ steps.ssh_target.outputs.host }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ steps.ssh_target.outputs.port }}
envs: IMAGE,APP_DIR
envs: IMAGE,MEDIA_IMAGE,APP_DIR
script: |
cd "$APP_DIR"
chmod +x deploy.sh efbundle
Expand Down
26 changes: 26 additions & 0 deletions MediaRelay.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# syntax=docker/dockerfile:1.7

ARG DOTNET_MAJOR=10.0
ARG APP_PROJECT=services/SonicRelay.MediaRelay/SonicRelay.MediaRelay.csproj

FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_MAJOR} AS build
ARG APP_PROJECT
WORKDIR /src

COPY . .
RUN dotnet restore "$APP_PROJECT"
RUN dotnet publish "$APP_PROJECT" \
--configuration Release \
--no-restore \
--output /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_MAJOR} AS runtime
WORKDIR /app

ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

COPY --from=build /app/publish ./
USER $APP_UID

ENTRYPOINT ["dotnet", "SonicRelay.MediaRelay.dll"]
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,23 @@ The automated deployment Compose file contains only RelayControl's API process.
## License

See [LICENSE](LICENSE).

### Feature switches

RelayControl uses Microsoft.FeatureManagement. `FeatureManagement__PublicRooms`, `FeatureManagement__DiscordActivity`, `FeatureManagement__DuplexAudio` and `FeatureManagement__ScreenShare` default to `true`; explicit `false` disables new work. Public-room and Activity routes return 404, disabled session modes return 409 with `feature_disabled`. Session end, credentials, cleanup, TURN and generic signaling remain available. Public rooms also require `PublicRoom__Enabled=true`. Docker environment changes require `docker compose up -d --force-recreate api`; restarting a container does not reload its environment. `FeatureManagement__DiscordWebSocketMedia` defaults to `false`.

### Discord WebSocket media rollout

See [media protocol](docs/protocol/discord-media-v1.md). Existing RTC publishers/viewers, TURN and generic signaling keep their transport. Media uses a separate `media-relay` process that forwards already encoded H.264/Opus with bounded queues, without transcoding. This service is single-instance; do not load-balance one session across instances.

Set a random `MediaRelay__ServiceToken` in the API deployment `.env`; Compose passes it only to the API and media relay. Set `MediaRelay__PublicBaseUrl=wss://media.example.com/ws/media` to the public upload endpoint. Proxy that host to `sonicrelay-media:8080`, preserving `/ws/media`, with WebSocket Upgrade headers and timeouts longer than 30 seconds. Do not expose `/api/internal/media-relay/*` through a separate public proxy rule; it requires a distinct service credential.

New transport is disabled by default. To roll out, set `COMPOSE_PROFILES=media`, `FeatureManagement__DiscordWebSocketMedia=true`, and keep `FeatureManagement__DiscordActivity=true` and `FeatureManagement__ScreenShare=true`. Use the API and media images from the same commit (`IMAGE` and `MEDIA_IMAGE`); CI publishes separate SHA tags and leaves existing API image selection intact. Recreate both containers with `docker compose -f docker-compose.prod.yml --profile media up -d --force-recreate api media-relay`.

On the publisher process, set `FRAMERELAY_WEBSOCKET_MEDIA_ENABLED=true` and restart FrameRelay. This is a process environment variable, not a desktop `.env` loader. When false or missing, there are no upload requests or extra encoders. H.264 output is reused; RTC AV1 uses an independent H.264 encoder for Activity without changing RTC codec selection.

Configure Discord Activity URL Mapping `/relay` to the existing API, and `/media` to the public media host. The Activity connects to `/media/ws/media` on its mapped origin; it does not use a direct desktop upload URL. It requires supported VideoDecoder/AudioDecoder and checks the actual H.264 profile and Opus configuration; unavailable codecs produce a desktop-viewer message. The play button resumes audio after a user gesture.

Disabling the media flag stops new grants and denies lease renewal. Leases expire after 30 seconds and renew every 10 seconds; disconnect/release and grant cleanup free Activity capacity without disconnecting RTC publishers. Live Discord verification must check pixels, audible synchronized audio, late join/keyframe recovery, reconnect, session end and a simultaneous desktop RTC viewer before enabling production rollout.

Local browser fixture: run `dotnet run --project tests/SonicRelay.MediaRelay.BrowserFixture` and the Activity Vite server on port 5173. Open `http://127.0.0.1:5173/tests/browser-media-fixture.html` and click Run. It encodes generated colors and a tone, forwards via real WebSockets, checks actual decoded canvas pixels and PCM energy, and closes resources. The fixture uses a fake grant API on localhost only; production never installs that project.
60 changes: 60 additions & 0 deletions SonicRelay.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.Infrastructure.V
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.VirtualPublisher.Tests", "tests\SonicRelay.VirtualPublisher.Tests\SonicRelay.VirtualPublisher.Tests.csproj", "{7C068177-2166-4549-80D8-04B7BC4DA6BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.MediaProtocol", "src\SonicRelay.MediaProtocol\SonicRelay.MediaProtocol.csproj", "{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.MediaProtocol.Tests", "tests\SonicRelay.MediaProtocol.Tests\SonicRelay.MediaProtocol.Tests.csproj", "{312F3367-82E4-49EE-BDDA-9E5634127740}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.MediaRelay", "services\SonicRelay.MediaRelay\SonicRelay.MediaRelay.csproj", "{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SonicRelay.MediaRelay.Tests", "tests\SonicRelay.MediaRelay.Tests\SonicRelay.MediaRelay.Tests.csproj", "{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -147,6 +155,54 @@ Global
{7C068177-2166-4549-80D8-04B7BC4DA6BC}.Release|x64.Build.0 = Release|Any CPU
{7C068177-2166-4549-80D8-04B7BC4DA6BC}.Release|x86.ActiveCfg = Release|Any CPU
{7C068177-2166-4549-80D8-04B7BC4DA6BC}.Release|x86.Build.0 = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|x64.ActiveCfg = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|x64.Build.0 = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|x86.ActiveCfg = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Debug|x86.Build.0 = Debug|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|Any CPU.Build.0 = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|x64.ActiveCfg = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|x64.Build.0 = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|x86.ActiveCfg = Release|Any CPU
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D}.Release|x86.Build.0 = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|Any CPU.Build.0 = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|x64.ActiveCfg = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|x64.Build.0 = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|x86.ActiveCfg = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Debug|x86.Build.0 = Debug|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|Any CPU.ActiveCfg = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|Any CPU.Build.0 = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|x64.ActiveCfg = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|x64.Build.0 = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|x86.ActiveCfg = Release|Any CPU
{312F3367-82E4-49EE-BDDA-9E5634127740}.Release|x86.Build.0 = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|x64.ActiveCfg = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|x64.Build.0 = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|x86.ActiveCfg = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Debug|x86.Build.0 = Debug|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|Any CPU.Build.0 = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|x64.ActiveCfg = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|x64.Build.0 = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|x86.ActiveCfg = Release|Any CPU
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0}.Release|x86.Build.0 = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|x64.ActiveCfg = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|x64.Build.0 = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|x86.ActiveCfg = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Debug|x86.Build.0 = Debug|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|Any CPU.Build.0 = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|x64.ActiveCfg = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|x64.Build.0 = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|x86.ActiveCfg = Release|Any CPU
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -157,5 +213,9 @@ Global
{777CEF1C-C33D-466B-8910-41F80DF6BE38} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{A3D8C7F8-9C8A-4789-B471-B4803B149E32} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{7C068177-2166-4549-80D8-04B7BC4DA6BC} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{00F83A1F-4FB8-427F-9BAD-2C6F5F1C492D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{312F3367-82E4-49EE-BDDA-9E5634127740} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{57A3C7BA-4F0F-4E1F-8291-72097B0410B0} = {5854FF58-B8C5-05C6-5BF2-ACE42193F22B}
{F870A3A0-7F27-47D1-9AC6-6C42BEBBAB7B} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
3 changes: 2 additions & 1 deletion deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ run_migrations() {
}

export IMAGE
export MEDIA_IMAGE="${MEDIA_IMAGE:-}"

echo "Pulling image: $IMAGE"
docker compose -f "$COMPOSE_FILE" pull api
docker compose -f "$COMPOSE_FILE" pull

run_migrations

Expand Down
17 changes: 17 additions & 0 deletions deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
env_file:
- .env
environment:
FeatureManagement__DiscordWebSocketMedia: "${FeatureManagement__DiscordWebSocketMedia:-false}"
PUBLICROOM__ENABLED: "${PUBLICROOM_ENABLED:-false}"
PUBLICROOM__TRACKSPATH: "/app/tracks"
PUBLICROOM__MAXVIEWERS: "${PUBLICROOM_MAXVIEWERS:-20}"
Expand All @@ -16,6 +17,22 @@ services:
networks:
- infra_network

media-relay:
profiles: ["media"]
image: ${MEDIA_IMAGE:-ghcr.io/vitorhugo-dotnet/sonicrelay-media:latest}
container_name: sonicrelay-media
restart: unless-stopped
mem_limit: 512m
environment:
ASPNETCORE_URLS: http://+:8080
MediaRelay__ApiBaseUrl: http://api:8080/
MediaRelay__ServiceToken: ${MediaRelay__ServiceToken:-}
MediaRelay__MaxUploads: ${MediaRelay__MaxUploads:-64}
MediaRelay__MaxViewers: ${MediaRelay__MaxViewers:-256}
expose: ["8080"]
depends_on: ["api"]
networks: ["infra_network"]

networks:
infra_network:
external: true
Expand Down
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.
13 changes: 13 additions & 0 deletions infra/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,16 @@ PGADMIN_PASSWORD=admin
RelayLaunch__DiscordClientId=
RelayLaunch__DiscordClientSecret=
RelayLaunch__DiscordBotToken=

# Optional feature switches; absent flags preserve current behavior.
FeatureManagement__PublicRooms=true
FeatureManagement__DiscordActivity=true
FeatureManagement__DuplexAudio=true
FeatureManagement__ScreenShare=true
FeatureManagement__DiscordWebSocketMedia=false
# New opt-in transport. Use a separate random service secret shared only by API and forwarder.
MediaRelay__ServiceToken=
MediaRelay__PublicBaseUrl=wss://media.example.com/ws/media
MediaRelay__MaxUploads=64
MediaRelay__MaxViewers=256
# Enable the dedicated container during rollout: COMPOSE_PROFILES=media (deploy stack).
13 changes: 13 additions & 0 deletions infra/.env.prod.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,16 @@ TURN_EXTERNAL_IP=203.0.113.10
TURN_MIN_PORT=49160
TURN_MAX_PORT=49200
COTURN_VERSION=latest

# Optional feature switches; absent flags preserve current behavior.
FeatureManagement__PublicRooms=true
FeatureManagement__DiscordActivity=true
FeatureManagement__DuplexAudio=true
FeatureManagement__ScreenShare=true
FeatureManagement__DiscordWebSocketMedia=false
# New opt-in transport. Use a separate random service secret shared only by API and forwarder.
MediaRelay__ServiceToken=
MediaRelay__PublicBaseUrl=wss://media.example.com/ws/media
MediaRelay__MaxUploads=64
MediaRelay__MaxViewers=256
# Enable the dedicated container during rollout: COMPOSE_PROFILES=media (deploy stack).
6 changes: 6 additions & 0 deletions infra/compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ services:
- "8080"
restart: always

media-relay:
profiles: ["prod", "media"]
image: ${MEDIA_IMAGE:-ghcr.io/vitorhugo-dotnet/sonicrelay-media:latest}
build: null
expose: ["8080"]

postgres:
profiles: ["prod"]
ports: []
Expand Down
Loading
Loading