janus-sdk
A pnpm monorepo (11 workspace packages) that implements FaceKom v2’s WebRTC stack: a horizontally-scalable signaling server that orchestrates one or more Janus media servers (janus.plugin.videoroom) over RabbitMQ, backed by a Postgres-backed room-state server, plus a browser client SDK, a server SDK, AMQP codegen packages, and a recording media-converter. All TypeScript, ESM, Node 24.
Repo identity
Top-level
package.jsonnameisjanus-sdk(private: true). Base branchmaster; latest commit at clone timecaa4590(2026-05-20, “feat: add site to media record and converted paths”). Workspace member packages are all scoped@janus-sdk/*and publish (the two public ones) tohttps://npm.facekom.net. See architecture-overview.
Related: vuer_oss · customization-architecture
1. Purpose & role in FaceKom
Verified from readme.md, root package.json, pnpm-workspace.yaml, and the service entrypoints.
- What it is: the real-time media plane for FaceKom v2 — it terminates browser WebRTC, drives Janus VideoRoom rooms, and persists distributed room/member/stream state so that multiple stateless signaling-server replicas can share Janus instances and survive replica failover.
- Two published consumable libraries (the actual “SDK” in the repo name), restricted-scope on
npm.facekom.net:@janus-sdk/client(client/, v1.0.2) — browser-side signaling + WebRTC peer/track management.@janus-sdk/shared(shared/, v1.0.2) — message contracts shared by client and server.
- Three deployable services (Docker images pushed to
harbor.techteamer.com/orbit/):signaling-server— WebSocket (socket.io) front door; orchestrates Janus via AMQP. (signaling-server/src/index.ts:110)room-state-server— AMQP RPC server over Postgres; the single source of truth for rooms, members, streams, Janus load, and room-lifecycle locks. (room-state-server/src/roomStateServer.ts:91)media-converter— AMQP RPC server that converts Janus.mjrrecordings to.webm/.opusviajanus-pp-rec. (media-converter/src/index.ts:6)
- Internal (private) building blocks:
server-sdk,record-path-manager, and three AsyncAPI-generated AMQP transport packages (amqp-janus-client,amqp-room-state,amqp-media-converter). dev/— a browser harness + static server for manual/stress testing the client SDK (not shipped).
Janus itself is NOT in this repo
The Janus media server runs from the external image
harbor.techteamer.com/orbit/janus(referenced indocker-compose.yml:159andDockerfile.media-converter:39). This repo only ships config for Janus underdocker/run/janus/and the orchestration around it.
2. Tech stack & runtime
| Aspect | Value | Source |
|---|---|---|
| Language | TypeScript ^5.9.3 (all packages), ESM ("type": "module") | every */package.json |
| Runtime | Node 24 | Dockerfile.* (node:24-alpine, node-build:24, node-runtime:24, NodeSource setup_24.x) |
| Package manager | pnpm workspaces (pnpm-workspace.yaml, pnpm-lock.yaml, 195 resolutions) | root |
| Dev runner | tsx (watch) for services; esbuild for browser bundles | */package.json scripts |
| Messaging | RabbitMQ via amqplib ^0.10.9 | service deps |
| WebSocket | socket.io ^4.8.1 (server) / socket.io-client ^4.8.1 (client) | server-sdk, client |
| DB | Postgres via pg ^8.18.0 + Kysely ^0.28.11 query builder | room-state-server/package.json |
| Auth tokens | jsonwebtoken ^9.0.3 (room-join session tokens) | room-state-server |
| IDs | uuid ^13.0.0 | shared, server-sdk, record-path-manager |
| Tests | Vitest ^4.1.5 (room-state-server only) | room-state-server |
| Recording convert | janus-pp-rec (external binary, invoked via child_process.spawn) | media-converter/src/mediaConverter.ts:41 |
No
engines/packageManagerfield anywhereNo
package.jsondeclaresengines.node,engines.pnpm, orpackageManager. The Node 24 / pnpm requirement is only encoded in the Dockerfiles, not enforced for local installs. Verified by grepping all manifests (no matches).
No Git-LFS in this repo
There is no
.gitattributesand no LFS filter config (git cat-file -p HEAD:.gitattributes→ does not exist). So there are no model-weight/binary pointer files here; the recording.mjr/.webm/.opusartifacts are runtime output, gitignored (.gitignore:recordings,janus-recordings,janus-converted). The one non-TS source file checked in is a vendored Janus C reference (see §6).
3. Build & run
3.1 Local dev (the intended path)
From readme.md + Makefile:
make up # docker compose -f docker-compose-dev.yml up -d --build
make down # docker compose -f docker-compose-dev.yml down -vmake up brings up the devcontainer (which builds all packages in watch mode and sleep infinity), Postgres, RabbitMQ, three Janus instances, the room-state server, media-converter, and signaling servers. The dev browser harness is at http://localhost:3000/track-manager.html?roomId=100&userId=110 (readme.md:23).
make uprequires.env.publish
docker-compose-dev.yml/docker-compose.ymldevcontainerservice hasenv_file: [.env.publish], which is gitignored and not committed.readme.md:7says “Create not commited env files:.env.publish”. Template:.env.publish.example→PUBLISH_AUTH_TOKEN="auth-token". Get the real token fromhttps://npm.facekom.net/.
3.2 Root package.json scripts (verbatim)
| Script | Command | What it does |
|---|---|---|
build | tsc -b | TS project-references build of root (tsconfig.json references only ./shared + ./client). |
clean | rm -rf dist node_modules tsconfig.tsbuildinfo | Wipe root build artifacts. |
harbor | docker buildx bake -f docker-bake.hcl --push | Build+push all 3 service images to harbor. |
harbor:dry | docker buildx bake -f docker-bake.hcl --load | Build images locally without pushing. |
publish-dry:shared | pnpm publish --filter @janus-sdk/shared --dry-run | Dry-run publish of shared. |
publish-dry:client | pnpm publish --filter @janus-sdk/client --dry-run | Dry-run publish of client. |
publish:shared | pnpm publish --filter @janus-sdk/shared | Publish shared to npm.facekom.net. |
publish:client | pnpm publish --filter @janus-sdk/client | Publish client to npm.facekom.net. |
Makefilealso definespublish-images= the samedocker buildx bake --pushasharbor.readme.mdreferencesmake publish-images.
3.3 Per-package scripts (verbatim)
dev/ (@janus-sdk/dev):
clean=rm -rf dist node_modules tsconfig.tsbuildinfostart=tsx watch --include src src/index.ts(static file server, see §6)build:w=tsc -b -wwatch=esbuild dist/client/index.js --bundle --format=esm --outfile=public/index.js --watch=forevertest=esbuild dist/client/test.js --bundle ... --outfile=public/test.js --watch=forever(stress-test bundle, not a unit-test runner)track-manager=esbuild dist/client/track-manager.js --bundle ... --outfile=public/track-manager.js --watch=foreverbuild:watch=concurrently "npm:watch" "npm:start" "npm:test" "npm:track-manager" "npm:build:w"
amqp-janus-client / amqp-room-state / amqp-media-converter (codegen packages):
clean= rm dist/node_modules + delete generatedsrccontentsgenerate=./scripts/generate.sh <spec>.yml <target> <outdir>(see §5)build=tsc --buildbuild:watch=tsc -b -wdev=npm run generate && npm run build
room-state-server:
clean,build(tsc --build),build:watchstart=tsx watch --include dist dist/index.jstest=vitest --config ./src/vitest.config.tsmigrate:latest=tsx src/db/migrate.ts latestmigrate:up=tsx src/db/migrate.ts upmigrate:down=tsx src/db/migrate.ts downmigrate:status=tsx src/db/migrate.ts status
signaling-server:
clean,build(tsc -b),build:watchstart=tsx watch --include dist dist/index.js
media-converter:
clean,build(tsc --build),build:watchstart=tsx watch --include dist dist/index.jsconvert=tsx src/scripts/convertRoom.ts(one-off room convert; usagenpm run convert -- 123permedia-converter/readme.md)
shared, client, server-sdk, record-path-manager (libraries):
clean,build(tsc --build),build:watchonly. (shared&clientalso havefiles: ["dist"]+publishConfig.)
3.4 Service entrypoints / start commands
| Service | main | Container CMD | Source |
|---|---|---|---|
| signaling-server | dist/index.js | ["node", "index.js"] | Dockerfile.signaling-server:69 |
| room-state-server | dist/index.js | ["node", "index.js"] | Dockerfile.room-state-server:46 |
| media-converter | dist/index.js | ["node", "index.js"] | Dockerfile.media-converter:60 |
| migrator (same image as room-state) | — | ["node", "/app/dist/db/migrate.js", "latest"] | readme.md:167; compose uses ["node", "/app/db/migrate.js", "latest"] (docker-compose.yml:72) |
3.5 Dockerfiles (5 + bake)
All builds use a multi-stage pattern: AsyncAPI codegen stage (image harbor.techteamer.com/facekom-devel/amqplib-asyncapi-generator:1.0.2) → build stage (node-build:24) that pnpm install --filter ... --frozen-lockfile --ignore-scripts, builds the relevant packages, then pnpm ... deploy --legacy --prod --ignore-scripts pruned → runtime stage (node-runtime:24) copying pruned/dist + pruned/node_modules.
Dockerfile.signaling-server— builds amqp-room-state, amqp-janus-client, shared, record-path-manager, server-sdk, signaling-server.Dockerfile.room-state-server— builds amqp-room-state (consumer) + room-state-server. Final stage commented alt# FROM node:24.Dockerfile.media-converter— builds amqp-media-converter (consumer), record-path-manager, media-converter; final stage isFROM harbor.techteamer.com/orbit/janus+ installs NodeSource Node 24 (sojanus-pp-recis present). Note: this Dockerfile does not use--frozen-lockfile(commented out,:23).Dockerfile.media-converter-dev—FROM .../orbit/janus+ Node 24,WORKDIR /workspace/dev, no CMD (dev shell).Dockerfile.devcontainer— codegen for all three AMQP specs (server+client targets) →node:24-alpine+ globalpnpm, stages generated sources into/tmp/...for the devcontainer to copy in.docker-bake.hcl— groupdefaulttargetsroom-state-postgres(*),room-state-server,signaling-server,media-converter; tagsharbor.techteamer.com/orbit/janus-sdk-<svc>:{latest,1.0.1}.
docker-bake.hclgroup lists a target it does not defineThe
defaultgroup references"room-state-postgres"but there is notarget "room-state-postgres"block indocker-bake.hcl(only room-state-server, signaling-server, media-converter are defined). Verified by reading the whole file (21 lines).
4. Top-level structure
Verified via git ls-files + sampling. (Excludes gitignored node_modules/, dist/, .pnpm-store.)
| Path | Role |
|---|---|
signaling-server/ | Deployable service: socket.io WS server wiring JanusSdk + SignalingServer (server-sdk). Entry src/index.ts, config src/config.ts. |
room-state-server/ | Deployable AMQP-RPC service over Postgres/Kysely. Entities, migrations (src/db/), JWT room-join tokens, room-lifecycle locks, Vitest tests. |
media-converter/ | Deployable AMQP-RPC service wrapping janus-pp-rec. |
server-sdk/ | Private lib: the brains. Janus client/session/plugin model + the SignalingServer orchestrator + WS server + AMQP signaling pub/sub. Re-exports shared + record-path-manager. |
client/ | Published browser SDK: SignalingSdk, WebSocketClient, WebRTC peer/trackManager. |
shared/ | Published lib: socket message types, signaling message contracts, AsyncQueue. |
record-path-manager/ | Private lib: pure path/filename builder+parser for Janus recordings (no I/O). |
amqp-janus-client/ | Generated AMQP transport for Janus control (AsyncAPI janus-sdk.yml, publisher target). |
amqp-room-state/ | Generated AMQP transport for room-state (AsyncAPI roomStateRepository.yml, client+server targets). |
amqp-media-converter/ | Generated AMQP transport for convert RPC (AsyncAPI mediaConverter.yml, client+server targets). |
dev/ | Browser harness (public/*.html) + tiny static server; client SDK demos & stress test. |
docker/run/janus/ | Janus .jcfg configs (videoroom, transports, event handlers) + supervisor conf, mounted into Janus containers. |
docker/run/rabbitmq/ | RabbitMQ conf.d/*.conf + definitions.json (vhosts/users/permissions). |
scripts/ | One first-party host script: extract_janus_recordings.sh (see §5). |
.github/workflows/ | publish.yaml — tag-triggered npm publish of client/. |
| root | docker-compose.yml, docker-compose-dev.yml, 5 Dockerfile.*, docker-bake.hcl, Makefile, .npmrc, .env, tsconfig.json, pnpm-*. |
The three
amqp-*packages'src/is mostly generated, not committedOnly
amqp-room-state/src/index.tsandamqp-media-converter/src/index.tsare tracked;amqp-janus-client/src/has no tracked source. The real transport code is produced at build time byscripts/generate.sh/ the codegen Docker stage from the*.ymlAsyncAPI specs. Each has a.gitignore. Treatsrc/here as build output. See §11.
5. First-party scripts (read in full)
Scope
“First-party bin scripts” = shell scripts under repo-owned
scripts/dirs (NOTnode_modules/**/bin). There are exactly 4: one host util + three identical codegen wrappers.
scripts/extract_janus_recordings.sh
POSIX sh, set -eu. Host-side recording extractor. For each named Docker container: verifies it’s running and has janus-pp-rec, finds *.mjr under REC_DIR (default /workspace/app/recordings, override -r), converts each to .opus (filename contains “audio”, any case) or .webm (otherwise) inside the container, docker cps results to <output>/<container>/ on the host (default ./janus-recordings, override -o), removes the converted file from the container, and — only with -d and only if that container had zero errors — deletes the source .mjrs. Flags: -c (required, space-sep container names), -o, -r, -d, -v, -h. Exit 0 all-ok / 1 any-error. -h/usage is rendered by sed-printing the leading comment block.
amqp-janus-client/scripts/generate.sh · amqp-room-state/scripts/generate.sh · amqp-media-converter/scripts/generate.sh
Byte-for-byte identical (verified via diff — all three match). bash, set -e. Args: $1 AsyncAPI yaml, $2 target (publisher/consumer/…), $3 output dir. It docker creates harbor.techteamer.com/facekom-devel/amqplib-asyncapi-generator:1.0.2 running generate fromTemplate /asyncapi.yml /template --output /output --force-write -p target=$TARGET, streams logs, checks exit code, then docker cps only /output/src/. into the host output dir (wiping it first). Used by each package’s generate/dev npm scripts.
6. Key modules / services / entities
server-sdk — the orchestration core
server-sdk/src/index.ts re-exports @janus-sdk/shared, the janus/, signaling/, socket/ barrels, and @janus-sdk/record-path-manager — so signaling-server only imports from @janus-sdk/server-sdk.
JanusSdk(janus/JanusSdk.ts:17) — holds aJanusAmqpClientper Janus id, aPluginRegistry(pre-registersjanus.plugin.videoroom→JanusVideoRoomPlugin).init()callsroomStateRepository.setJanusInstances(...)then connects each Janus AMQP node.createSession()/destroySession()issue Januscreate/destroy; sessions get a keepalive callback wired toroomStateRepository.keepalive(janusId, sessionId).JanusAmqpClient(janus/janusAmqpClient.ts:15) extends generatedJanusPublisher. Implements Janus’s transaction/correlation protocol: distinguishes sync responses, sync-acks-of-async, async responses, and events; crucially it ignores messages whose transaction id is not addressed to this signaling server (isValidTransactionId(..., signalingServerId),:47) — this is what lets many signaling replicas share one Janus AMQP fanout reply exchange. Event tags:event|webrtcup|media|slowlink|hangup|timeout|detached.SignalingServer<Session>(signaling/SignalingServer.ts:62) — the heart. Bridges socket.io requests/events ↔ Janus VideoRoom ↔ room-state repo ↔ cross-replica AMQP. Per-socket serialized work viaQueueManager(memberQueues) to avoid races;SocketManagermaps publisher/subscriber handle ids → sockets. Handles WS verbs:enter-room,offer,rtc-configuration(requests);leave-room,answer,candidate,client-event-mute,connect(events). Drives publish/subscribe, simulcast/active-stream filtering, recording filenames (viaRecordPathManager), and room create/destroy under distributed locks (createRoom/destroyRoomusepollUntil+lockRoomLifecycle).initializeRooms()callstakeOverDanglingRooms()on boot to recover rooms orphaned by a dead replica.SignalingAmqp(signaling/signalingAmqp.ts:37) — a topic exchange (signaling-exchange, typetopic, name routed by roomId) for cross-replica room events:mute(TrackMuteEvent),left-room,joined-room. Each replica binds an exclusive auto-delete queue${signalingServerId}-${topicId}; on receipt it fans the event to the local sockets in that room (skipping the originatingclientId). Enables “user joined/left/muted elsewhere” + the unique-user-per-room kick.- Other janus/:
JanusSession,JanusPlugin,PluginRegistry,plugins/JanusVideoRoomPlugin/*(videoroom API msgs,janusErrorCode.ts, types),roomStateRepository/(amqpRoomStateRepository.ts,interface.ts),utils/pollUntil.ts,utils/transaction.ts. signaling/:queueManager.ts,socketManager.ts,signalingAmqp.ts. socket/:WebSocketServer.ts(socket.io wrapper).
Auth is a stub
signaling-server/src/index.ts:22sessionHandlerhas// todo: resolve signed token and get userIdand simply trustssocket.handshake.auth['roomId'|'userId']. There is no token verification at WS handshake. (Member identity within a room is later protected by the JWT room-join token from room-state-server, but the initial room/user binding is client-asserted.)
room-state-server — distributed state authority
RoomStateServer(roomStateServer.ts:91) extends generatedRoomStateConsumer; every method is an AMQP RPC handler over Kysely/Postgres. Notable logic:useLeastLoadedJanusInstance()(:668) — picks the Janus with fewest rooms usingFOR UPDATE SKIP LOCKED+ atomicnumber_of_rooms+1, looping until it grabs one (race-free load balancing across replicas).lockRoomLifecycle()/unlockRoomLifecycle()(:494,:561) — optimistic distributed lock viaroom_lifecyclesupsert with a lease (ROOM_LOCK_LEASE_MS, default 10s); expired locks (creating/deletingolder than lease) can be stolen.takeOverDanglingRooms()(:619) — claims rooms whosekeepalive_sent_atis older than 30s (hardcoded) or already owned by this replica → failover recovery.addMember()mints a JWT viasignRoomJoinSession(publisher_id);getMember/removeMemberaccept that token and verify it.
- DB schema (
room-state-server/src/db/migrations/2026-05-12-init.ts, the only migration): tablesjanus_instances,rooms,members,subscribed_streams,published_streams,room_lifecycles(+ enumroom_lifecycle_status= creating|created|deleting|deleted), with FKs cascading onjanus_id,updated_attriggers, and indexes. Variable Janus room options live inrooms.optionsjsonb; per-stream extras insubscribed_streams.stream_statejsonb. - Migrations runner
src/db/migrate.ts— KyselyMigrator+FileMigrationProvider;process.argv[2]∈latest|up|down|status(defaultlatest). - Supporting:
lockToken.ts,roomJoinSession.ts(JWT sign/verify, secretROOM_JOIN_SESSION_SECRET),db/index.ts(Kysely+pg pool),db/types.ts.
media-converter
MediaConverter(mediaConverter.ts:8) buildsjanus-pp-recargs fromconfig.janusPPRec,spawns it, and converts a publisher’s cam(-video-0)/mic(-audio-1)/scr(-video-2) tracks or a whole room.convert()(:126) dispatches on the AMQPConvertPayloadshape (full publisher tuple → 3 tracks; janusId+roomId → room; roomId only → discover janusId from dir names). Output →CONVERT_OUTPUT_PATH/<site>/janus-<id>-room-<id>/<base>.{webm,opus}.index.tssubclasses the generatedMediaConverterConsumerand delegatesconvert()toMediaConverter.
client (published)
SignalingSdk(client/src/signaling/SignalingSdk.ts:25) — the public browser API:enterRoom,sendOffer/sendAnswer,forwardCandidate,mute,leaveRoom,getRTCConfiguration, andon*callbacks (onOffer,onStreams,onUserJoined/Left,onMuteEvent,onRejoinRoom,onSessionEnded). Holds the room-jointoken, queues pre-token calls viaAsyncQueue. Alsosocket/WebSocketClient.ts,webrtc/peer.ts,webrtc/trackManager.ts.
shared (published)
- Message contracts only:
socket/messages/WebSocketMessage.ts,signaling/signaling.messages.ts,utils/asyncQueue.ts. The single source of the client↔server wire types.
record-path-manager
RecordPathManager(recordPathManager.ts:27) — pure functions:getRecordPath=<recordPath>/<site>/janus-<janusId>-room-<roomId>;getBaseFileName=janus-<j>-room-<r>-user-<u>-publisher-<p>; cam/mic/scr.mjrpath helpers;parseFileNameregex. Matches the naming documented inreadme.md:104-112. Used by both signaling-server (record dir/filename) and media-converter.
dev (harness, not shipped)
dev/src/index.ts+static.tsservedev/public/{index,test,track-manager}.html;dev/src/client/{index,test,track-manager}.tsare esbuild-bundled demos.track-manager.tsis the documented usage example (readme.md:21).
Vendored Janus C reference file
server-sdk/src/janus/plugins/JanusVideoRoomPlugin/api/janus_videoroom.cis a checked-in copy of upstream Janus’s videoroom plugin C source, kept as the reference the TS message types (videroom.messages.ts) are modeled on. It is not compiled by this repo.
7. Configuration
Three runtime configs are plain objects reading process.env with defaults and a validate() that throws on bad input (called at import). No AJV / getconfig / JSON-schema config validation is used (verified — no such deps/files).
signaling-server (signaling-server/src/config.ts)
| Env | Default | Notes |
|---|---|---|
SIGNALING_SERVER_ID (or HOSTNAME) | — | replica identity (used in queue/reply names) |
PORT | — (required) | WS port |
JANUS_IDS | — (required) | JSON array string, e.g. '["janus1","janus2","janus3"]'; JSON.parse of empty string throws |
MAX_NUMBER_OF_PUBLISHERS | 10 | default room publisher cap |
RABBITMQ_HOST / RABBITMQ_PORT | janus-sdk-rabbitmq / 5672 | |
UNIQUE_USER_PER_ROOM | true | |
TURN_URL / TURN_CREDENTIAL / TURN_USERNAME | unset | if TURN_URL set → ICE policy relay, else all |
REMOVE_ROOM_AFTER_LAST_MEMBER | true | |
RECORD | false | enable Janus recording |
RECORDING_PATH | /workspace/app/recordings | |
SITE | test_site | path namespacing for recordings |
room-state-server (room-state-server/src/config.ts)
| Env | Default |
|---|---|
DATABASE | appdb |
HOST | room-state-postgres |
USER | appuser |
PASSWORD | apppass |
PORT | 5432 |
MAX_CONNECTIONS | 10 |
ROOM_JOIN_SESSION_SECRET | test-secret |
RABBITMQ_HOST / RABBITMQ_PORT | janus-sdk-rabbitmq / 5672 |
ROOM_LOCK_LEASE_MS | 10000 |
media-converter (media-converter/src/config.ts)
RABBITMQ_HOST/PORT,RECORDING_PATH(/workspace/recordings),CONVERT_OUTPUT_PATH(/workspace/converted), plus a full set of optionalJANUSPPREC_*tuning vars mapped tojanus-pp-recflags (audio/video format, faststart, restamp*, audioskew, silence-distance, etc.).validate()is a no-op here.
Boolean envs are not parsed correctly
Patterns like
Boolean(process.env['RECORD']) || false(signaling config:13, alsoUNIQUE_USER_PER_ROOM,REMOVE_ROOM_AFTER_LAST_MEMBER) coerce any non-empty string totrue(soRECORD=false→true) and otherwise fall back to the|| true/falsedefault. Effective behavior: these flags are on whenever the env var is set to anything non-empty. Confirmed insignaling-server/src/config.ts:8,12,13.
Hardcoded RabbitMQ credentials & infra hostnames
AMQP usernames/passwords are hardcoded in source, not env-driven: signaling uses
signaling/signaling, room-state-server connects asroom-state/signaling(note: userroom-state, passwordsignaling), media-converter asmedia-convert/signaling(*/config.ts,signaling-server/src/index.ts:37-87). The committed RabbitMQdefinitions.jsonships matching users (signaling,room-state,media-convert,janus,admin,app) with SHA-256 password hashes and per-vhost permissions. Multiple// todo: put this into envcomments acknowledge this.
Other config files
.npmrc—@janus-sdk:registry=https://npm.facekom.net/,always-auth, auth via${PUBLISH_AUTH_TOKEN},store-dir=/.pnpm-store..env(committed) — compose/devcontainer knobs:APP_USER,APP_HOME=/workspace/app,DEV_UID/GID=1000,JANUS_UID/GID=9000,COMPOSE_BAKE=true.docker/run/rabbitmq/config/definitions.json— vhosts:/,signaling,media-convert,janus1,janus2,janus3,janus,room-state; users + permissions as above.docker/run/janus/config/janus/*.jcfg— Janus core, videoroom plugin, HTTP/WS/RabbitMQ transports, RabbitMQ event handler, supervisor conf (mounted into Janus containers; per-instancejanus{1,2,3}.transport.rabbitmq.jcfg).
8. Tests
- Framework: Vitest
^4.1.5, only inroom-state-server. No test deps or*.spec/*.testfiles exist in any other package (verified viagit ls-files). - Location:
room-state-server/src/tests/roomStateServer.spec.ts; configroom-state-server/src/vitest.config.ts. - Run:
pnpm --filter room-state-server run test→vitest --config ./src/vitest.config.ts. - Prereq:
readme.md:39— “Needs database and devcontainer to run.” (Tests exercise the real Postgres-backedRoomStateServer.) - No root test script and no CI test job —
.github/workflows/publish.yamlonly publishes; it does not run tests.
9. Dependencies on other FaceKom services / infra (evidenced in code)
RabbitMQ (central nervous system) — amqplib
All three services + server-sdk connect to RabbitMQ at RABBITMQ_HOST:RABBITMQ_PORT (default janus-sdk-rabbitmq:5672). Vhosts/queues by purpose:
| Purpose | vhost | queue / exchange | direction | Source |
|---|---|---|---|---|
| Signaling → Janus control | per-Janus (janus1/janus2/janus3; topology id janus) | queue to-janus; reply via fanout exchange janus-exchange | RPC, timeoutMs 15000 | signaling-server/src/index.ts:46-77 |
| Signaling → room-state | room-state | queue room-state; reply queue = SIGNALING_SERVER_ID | RPC | signaling-server/src/index.ts:80-108 |
| room-state-server (consumer) | room-state | queue room-state | serves RPC | room-state-server/src/config.ts:36-64 |
| Cross-replica room events | signaling | topic exchange signaling-exchange (+signaling-exchange const) | pub/sub | signaling-server/src/index.ts:119, signalingAmqp.ts |
| media-converter (consumer) | media-convert | queue media-convert | serves RPC | media-converter/src/config.ts:63-91 |
| Janus events → signaling | (Janus side) | Janus RabbitMQ transport/event-handler | events | docker/run/janus/...rabbitmq.jcfg |
The three
amqp-*packages are AsyncAPI-generated RPC clients/servers (janus-sdk.yml,roomStateRepository.yml,mediaConverter.yml) over these queues.
Postgres — pg + Kysely
Only room-state-server touches the DB (appdb@room-state-postgres:5432 default; compose uses janus-sdk-postgres, image postgres:18-alpine, exposed 5444:5432). Schema in §6. No Redis anywhere (verified — no redis dep/usage).
Janus media server (external)
Image harbor.techteamer.com/orbit/janus. Controlled purely over RabbitMQ via amqp-janus-client; recordings written to a shared volume (./janus-recordings:/workspace/app/recordings) consumed by media-converter / extract_janus_recordings.sh.
TURN/STUN (optional, external)
If TURN_URL is set the signaling server returns it as the single ICE server with iceTransportPolicy: 'relay' (signaling-server/src/index.ts:121-130).
Container registries
harbor.techteamer.com/orbit/— published service images + base imagesnode-build:24,node-runtime:24,janus,rabbitmq-service:4.3.0.harbor.techteamer.com/facekom-devel/amqplib-asyncapi-generator:1.0.2— the codegen image (build-time dependency for all AMQP packages).https://npm.facekom.net/— restricted npm registry for@janus-sdk/{shared,client}.
10. Compose topology (dev wiring quick map)
From docker-compose.yml (prod-ish) — docker-compose-dev.yml is the larger dev variant used by make up:
janus-sdk-postgres → migrator (runs migrate latest, depends on devcontainer healthy) → room-state-server; rabbitmq (mounts conf.d + definitions.json); janus-1/2/3 (each mounts its janus{n}.transport.rabbitmq.jcfg as janus.transport.rabbitmq.jcfg + shared recordings volume); signaling-server-1/2/3 (SIGNALING_SERVER_ID=signaling-{n}, JANUS_IDS='["janus1","janus2","janus3"]', ports 3001/3002/3003); media-converter (volumes ./janus-recordings→/workspace/recordings, ./janus-converted→/workspace/converted); permission-fix (chowns pnpm store to DEV_UID:GID, recordings/converted to JANUS_UID:GID=9000); devcontainer (mounts repo at /app, builds all packages in watch, healthcheck waits for signaling + room-state dist/index.js, port 3000).
Committed secrets in compose
docker-compose.yml:36setsROOM_JOIN_SESSION_SECRET: .vs29o9@wpk&ep.-pspq_and Postgres credsappuser/apppassin plaintext. These are dev defaults committed to the repo — do not reuse in production.
11. Verified gotchas (summary)
Generated source is build-time only
Building locally requires Docker (the AsyncAPI generator image) and a working
npm run generate/ codegen Docker stage before TS compiles, becauseamqp-*/src/is regenerated.make up’s devcontainer copies pre-generated/tmp/.../srcin (docker-compose.yml:146). A barepnpm install && pnpm -r buildon a fresh checkout will fail for the AMQP packages without first running generation. The.devscripts (generate && build) and the Dockerfiles encode the correct order.
docker-bake.hcldefaultgroup references undefinedroom-state-postgrestarget.
Boolean env coercion bug (
Boolean(process.env[...])) — see §7.
WS handshake auth is an unimplemented stub — see §6.
Hardcoded AMQP credentials & multiple
// todo: put this into env— see §7.
Dockerfile.media-converteromits--frozen-lockfile(commented out), so its image can drift frompnpm-lock.yamlwhile the other two services pin it.
Recording filename contract is load-bearing
RecordPathManagerregexjanus-(.+)-room-(.+)-user-(.+)-publisher-(\d+)-(video|audio)-(\d+)\.mjr$and the cam/mic/scr index convention (video-0,audio-1,video-2) are duplicated knowledge betweenrecord-path-manager,media-converter, andextract_janus_recordings.sh. Changing Janus’s naming breaks conversion silently (convertMjrreturns null / “failed to parse”).
Failover relies on hardcoded 30s dangling threshold + 10s lock lease
Replica takeover (
takeOverDanglingRooms, 30s,roomStateServer.ts:620) and lifecycle lock lease (ROOM_LOCK_LEASE_MS10s) are the only liveness guards; keepalive is driven from eachJanusSession. Tune together.
Unverified / gaps
- AsyncAPI spec bodies (
amqp-janus-client/janus-sdk.yml,amqp-room-state/roomStateRepository.yml,amqp-media-converter/mediaConverter.yml): I read only the first ~60 lines ofjanus-sdk.yml(confirming AsyncAPI 3.0.0 + the Janus channel/message set: CreateSession, DestroySession, KeepAlive, AttachHandle, DetachHandle, CreateVideoRoom, CheckRoomExists, DestroyRoom, JoinAsPublisher, …). The full schema/payload definitions and the other two specs were not read line-by-line. docker-compose-dev.yml(the filemake upactually uses, 11.6K): I read the siblingdocker-compose.ymlin full but only confirmedmake uptargets-dev.yml; differences between the two were not line-verified.- Janus
.jcfgconfig contents: existence and mount mapping verified viadocker-compose.yml; individual.jcfgfile contents (videoroom params, transport ports, RabbitMQ event-handler routing keys) were not read. - RabbitMQ
conf.d/*.conf(9 files): existence verified; contents (TLS, clustering, security) not read. - server-sdk surveyed structurally, key files read line-by-line: I read
index.ts,JanusSdk.ts,janusAmqpClient.ts,signaling/SignalingServer.ts,signaling/signalingAmqp.ts. I did not line-readJanusSession.ts,JanusPlugin.ts,PluginRegistry.ts,core.messages.ts, theJanusVideoRoomPlugin/*files (incl.janusVideoRoomPlugin.ts,videroom.messages.ts,janusErrorCode.ts,types.ts),roomStateRepository/{amqpRoomStateRepository,interface}.ts,utils/{pollUntil,transaction}.ts,signaling/{queueManager,socketManager}.ts,socket/WebSocketServer.ts— their roles are inferred from imports/usage at call sites, not from reading each file. - client/ WebRTC internals:
SignalingSdk.tsread in full;webrtc/peer.ts,webrtc/trackManager.ts,socket/WebSocketClient.tsnot line-read. - shared/ message definitions: barrels read;
signaling.messages.ts,WebSocketMessage.ts,asyncQueue.tsbodies not line-read. - dev/ harness sources (
client/{index,test,track-manager}.ts,static.ts): not read beyondreadme.mddescription. pnpm-lock.yaml: only counted resolutions (195); not audited.- Password-hash plaintexts in
definitions.jsonare SHA-256 hashes (not reversible here); the actual passwords are the hardcoded ones in the*.config.tsfiles.
Sources
Files read in full (or substantially) for this doc:
readme.md,package.json,pnpm-workspace.yaml,tsconfig.json,.npmrc,.env,.env.publish.example,Makefile,.gitignore,docker-bake.hcl- All 11 workspace
package.json(dev,amqp-janus-client,amqp-room-state,amqp-media-converter,room-state-server,signaling-server,shared,server-sdk,client,record-path-manager,media-converter) Dockerfile.devcontainer,Dockerfile.signaling-server,Dockerfile.room-state-server,Dockerfile.media-converter,Dockerfile.media-converter-dev,docker-compose.ymlscripts/extract_janus_recordings.sh;amqp-janus-client/scripts/generate.sh(+diffproving the other two identical).github/workflows/publish.yamlsignaling-server/src/{index,config}.tsroom-state-server/src/{index,config}.ts,room-state-server/src/roomStateServer.ts,room-state-server/src/db/migrate.ts,room-state-server/src/db/migrations/2026-05-12-init.tsmedia-converter/src/{index,config,mediaConverter}.ts,media-converter/src/scripts/convertRoom.ts,media-converter/readme.mdserver-sdk/src/index.ts,server-sdk/src/janus/JanusSdk.ts,server-sdk/src/janus/janusAmqpClient.ts,server-sdk/src/signaling/SignalingServer.ts,server-sdk/src/signaling/signalingAmqp.tsclient/src/index.ts,client/src/signaling/SignalingSdk.tsshared/src/index.tsrecord-path-manager/src/recordPathManager.tsdocker/run/rabbitmq/config/definitions.jsonamqp-janus-client/janus-sdk.yml(first ~60 lines)
Structure verified via git ls-files (full tracked-file listing) and git log/git branch/git cat-file (read-only). Directories listed but not file-by-file: docker/run/janus/, docker/run/rabbitmq/config/conf.d/, the generated amqp-*/src/ trees.