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.json name is janus-sdk (private: true). Base branch master; latest commit at clone time caa4590 (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) to https://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 .mjr recordings to .webm/.opus via janus-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 in docker-compose.yml:159 and Dockerfile.media-converter:39). This repo only ships config for Janus under docker/run/janus/ and the orchestration around it.


2. Tech stack & runtime

AspectValueSource
LanguageTypeScript ^5.9.3 (all packages), ESM ("type": "module")every */package.json
RuntimeNode 24Dockerfile.* (node:24-alpine, node-build:24, node-runtime:24, NodeSource setup_24.x)
Package managerpnpm workspaces (pnpm-workspace.yaml, pnpm-lock.yaml, 195 resolutions)root
Dev runnertsx (watch) for services; esbuild for browser bundles*/package.json scripts
MessagingRabbitMQ via amqplib ^0.10.9service deps
WebSocketsocket.io ^4.8.1 (server) / socket.io-client ^4.8.1 (client)server-sdk, client
DBPostgres via pg ^8.18.0 + Kysely ^0.28.11 query builderroom-state-server/package.json
Auth tokensjsonwebtoken ^9.0.3 (room-join session tokens)room-state-server
IDsuuid ^13.0.0shared, server-sdk, record-path-manager
TestsVitest ^4.1.5 (room-state-server only)room-state-server
Recording convertjanus-pp-rec (external binary, invoked via child_process.spawn)media-converter/src/mediaConverter.ts:41

No engines / packageManager field anywhere

No package.json declares engines.node, engines.pnpm, or packageManager. 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 .gitattributes and 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/.opus artifacts 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 -v

make 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 up requires .env.publish

docker-compose-dev.yml / docker-compose.yml devcontainer service has env_file: [.env.publish], which is gitignored and not committed. readme.md:7 says “Create not commited env files: .env.publish”. Template: .env.publish.examplePUBLISH_AUTH_TOKEN="auth-token". Get the real token from https://npm.facekom.net/.

3.2 Root package.json scripts (verbatim)

ScriptCommandWhat it does
buildtsc -bTS project-references build of root (tsconfig.json references only ./shared + ./client).
cleanrm -rf dist node_modules tsconfig.tsbuildinfoWipe root build artifacts.
harbordocker buildx bake -f docker-bake.hcl --pushBuild+push all 3 service images to harbor.
harbor:drydocker buildx bake -f docker-bake.hcl --loadBuild images locally without pushing.
publish-dry:sharedpnpm publish --filter @janus-sdk/shared --dry-runDry-run publish of shared.
publish-dry:clientpnpm publish --filter @janus-sdk/client --dry-runDry-run publish of client.
publish:sharedpnpm publish --filter @janus-sdk/sharedPublish shared to npm.facekom.net.
publish:clientpnpm publish --filter @janus-sdk/clientPublish client to npm.facekom.net.

Makefile also defines publish-images = the same docker buildx bake --push as harbor. readme.md references make publish-images.

3.3 Per-package scripts (verbatim)

dev/ (@janus-sdk/dev):

  • clean = rm -rf dist node_modules tsconfig.tsbuildinfo
  • start = tsx watch --include src src/index.ts (static file server, see §6)
  • build:w = tsc -b -w
  • watch = esbuild dist/client/index.js --bundle --format=esm --outfile=public/index.js --watch=forever
  • test = 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=forever
  • build: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 generated src contents
  • generate = ./scripts/generate.sh <spec>.yml <target> <outdir> (see §5)
  • build = tsc --build
  • build:watch = tsc -b -w
  • dev = npm run generate && npm run build

room-state-server:

  • clean, build (tsc --build), build:watch
  • start = tsx watch --include dist dist/index.js
  • test = vitest --config ./src/vitest.config.ts
  • migrate:latest = tsx src/db/migrate.ts latest
  • migrate:up = tsx src/db/migrate.ts up
  • migrate:down = tsx src/db/migrate.ts down
  • migrate:status = tsx src/db/migrate.ts status

signaling-server:

  • clean, build (tsc -b), build:watch
  • start = tsx watch --include dist dist/index.js

media-converter:

  • clean, build (tsc --build), build:watch
  • start = tsx watch --include dist dist/index.js
  • convert = tsx src/scripts/convertRoom.ts (one-off room convert; usage npm run convert -- 123 per media-converter/readme.md)

shared, client, server-sdk, record-path-manager (libraries):

  • clean, build (tsc --build), build:watch only. (shared & client also have files: ["dist"] + publishConfig.)

3.4 Service entrypoints / start commands

ServicemainContainer CMDSource
signaling-serverdist/index.js["node", "index.js"]Dockerfile.signaling-server:69
room-state-serverdist/index.js["node", "index.js"]Dockerfile.room-state-server:46
media-converterdist/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 prunedruntime 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 is FROM harbor.techteamer.com/orbit/janus + installs NodeSource Node 24 (so janus-pp-rec is present). Note: this Dockerfile does not use --frozen-lockfile (commented out, :23).
  • Dockerfile.media-converter-devFROM .../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 + global pnpm, stages generated sources into /tmp/... for the devcontainer to copy in.
  • docker-bake.hcl — group default targets room-state-postgres(*), room-state-server, signaling-server, media-converter; tags harbor.techteamer.com/orbit/janus-sdk-<svc>:{latest,1.0.1}.

docker-bake.hcl group lists a target it does not define

The default group references "room-state-postgres" but there is no target "room-state-postgres" block in docker-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.)

PathRole
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/.
rootdocker-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 committed

Only amqp-room-state/src/index.ts and amqp-media-converter/src/index.ts are tracked; amqp-janus-client/src/ has no tracked source. The real transport code is produced at build time by scripts/generate.sh / the codegen Docker stage from the *.yml AsyncAPI specs. Each has a .gitignore. Treat src/ 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 (NOT node_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 a JanusAmqpClient per Janus id, a PluginRegistry (pre-registers janus.plugin.videoroomJanusVideoRoomPlugin). init() calls roomStateRepository.setJanusInstances(...) then connects each Janus AMQP node. createSession()/destroySession() issue Janus create/destroy; sessions get a keepalive callback wired to roomStateRepository.keepalive(janusId, sessionId).
  • JanusAmqpClient (janus/janusAmqpClient.ts:15) extends generated JanusPublisher. 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 via QueueManager (memberQueues) to avoid races; SocketManager maps 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 (via RecordPathManager), and room create/destroy under distributed locks (createRoom/destroyRoom use pollUntil + lockRoomLifecycle). initializeRooms() calls takeOverDanglingRooms() on boot to recover rooms orphaned by a dead replica.
  • SignalingAmqp (signaling/signalingAmqp.ts:37) — a topic exchange (signaling-exchange, type topic, 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 originating clientId). 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:22 sessionHandler has // todo: resolve signed token and get userId and simply trusts socket.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 generated RoomStateConsumer; every method is an AMQP RPC handler over Kysely/Postgres. Notable logic:
    • useLeastLoadedJanusInstance() (:668) — picks the Janus with fewest rooms using FOR UPDATE SKIP LOCKED + atomic number_of_rooms+1, looping until it grabs one (race-free load balancing across replicas).
    • lockRoomLifecycle()/unlockRoomLifecycle() (:494,:561) — optimistic distributed lock via room_lifecycles upsert with a lease (ROOM_LOCK_LEASE_MS, default 10s); expired locks (creating/deleting older than lease) can be stolen.
    • takeOverDanglingRooms() (:619) — claims rooms whose keepalive_sent_at is older than 30s (hardcoded) or already owned by this replica → failover recovery.
    • addMember() mints a JWT via signRoomJoinSession(publisher_id); getMember/removeMember accept that token and verify it.
  • DB schema (room-state-server/src/db/migrations/2026-05-12-init.ts, the only migration): tables janus_instances, rooms, members, subscribed_streams, published_streams, room_lifecycles (+ enum room_lifecycle_status = creating|created|deleting|deleted), with FKs cascading on janus_id, updated_at triggers, and indexes. Variable Janus room options live in rooms.options jsonb; per-stream extras in subscribed_streams.stream_state jsonb.
  • Migrations runner src/db/migrate.ts — Kysely Migrator + FileMigrationProvider; process.argv[2]latest|up|down|status (default latest).
  • Supporting: lockToken.ts, roomJoinSession.ts (JWT sign/verify, secret ROOM_JOIN_SESSION_SECRET), db/index.ts (Kysely+pg pool), db/types.ts.

media-converter

  • MediaConverter (mediaConverter.ts:8) builds janus-pp-rec args from config.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 AMQP ConvertPayload shape (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.ts subclasses the generated MediaConverterConsumer and delegates convert() to MediaConverter.

client (published)

  • SignalingSdk (client/src/signaling/SignalingSdk.ts:25) — the public browser API: enterRoom, sendOffer/sendAnswer, forwardCandidate, mute, leaveRoom, getRTCConfiguration, and on* callbacks (onOffer, onStreams, onUserJoined/Left, onMuteEvent, onRejoinRoom, onSessionEnded). Holds the room-join token, queues pre-token calls via AsyncQueue. Also socket/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 .mjr path helpers; parseFileName regex. Matches the naming documented in readme.md:104-112. Used by both signaling-server (record dir/filename) and media-converter.

dev (harness, not shipped)

  • dev/src/index.ts + static.ts serve dev/public/{index,test,track-manager}.html; dev/src/client/{index,test,track-manager}.ts are esbuild-bundled demos. track-manager.ts is the documented usage example (readme.md:21).

Vendored Janus C reference file

server-sdk/src/janus/plugins/JanusVideoRoomPlugin/api/janus_videoroom.c is 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)

EnvDefaultNotes
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_PUBLISHERS10default room publisher cap
RABBITMQ_HOST / RABBITMQ_PORTjanus-sdk-rabbitmq / 5672
UNIQUE_USER_PER_ROOMtrue
TURN_URL / TURN_CREDENTIAL / TURN_USERNAMEunsetif TURN_URL set → ICE policy relay, else all
REMOVE_ROOM_AFTER_LAST_MEMBERtrue
RECORDfalseenable Janus recording
RECORDING_PATH/workspace/app/recordings
SITEtest_sitepath namespacing for recordings

room-state-server (room-state-server/src/config.ts)

EnvDefault
DATABASEappdb
HOSTroom-state-postgres
USERappuser
PASSWORDapppass
PORT5432
MAX_CONNECTIONS10
ROOM_JOIN_SESSION_SECRETtest-secret
RABBITMQ_HOST / RABBITMQ_PORTjanus-sdk-rabbitmq / 5672
ROOM_LOCK_LEASE_MS10000

media-converter (media-converter/src/config.ts)

  • RABBITMQ_HOST/PORT, RECORDING_PATH (/workspace/recordings), CONVERT_OUTPUT_PATH (/workspace/converted), plus a full set of optional JANUSPPREC_* tuning vars mapped to janus-pp-rec flags (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, also UNIQUE_USER_PER_ROOM, REMOVE_ROOM_AFTER_LAST_MEMBER) coerce any non-empty string to true (so RECORD=falsetrue) and otherwise fall back to the || true/false default. Effective behavior: these flags are on whenever the env var is set to anything non-empty. Confirmed in signaling-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 as room-state/signaling (note: user room-state, password signaling), media-converter as media-convert/signaling (*/config.ts, signaling-server/src/index.ts:37-87). The committed RabbitMQ definitions.json ships matching users (signaling, room-state, media-convert, janus, admin, app) with SHA-256 password hashes and per-vhost permissions. Multiple // todo: put this into env comments 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.jsonvhosts: /, 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-instance janus{1,2,3}.transport.rabbitmq.jcfg).

8. Tests

  • Framework: Vitest ^4.1.5, only in room-state-server. No test deps or *.spec/*.test files exist in any other package (verified via git ls-files).
  • Location: room-state-server/src/tests/roomStateServer.spec.ts; config room-state-server/src/vitest.config.ts.
  • Run: pnpm --filter room-state-server run testvitest --config ./src/vitest.config.ts.
  • Prereq: readme.md:39 — “Needs database and devcontainer to run.” (Tests exercise the real Postgres-backed RoomStateServer.)
  • No root test script and no CI test job.github/workflows/publish.yaml only 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:

Purposevhostqueue / exchangedirectionSource
Signaling → Janus controlper-Janus (janus1/janus2/janus3; topology id janus)queue to-janus; reply via fanout exchange janus-exchangeRPC, timeoutMs 15000signaling-server/src/index.ts:46-77
Signaling → room-stateroom-statequeue room-state; reply queue = SIGNALING_SERVER_IDRPCsignaling-server/src/index.ts:80-108
room-state-server (consumer)room-statequeue room-stateserves RPCroom-state-server/src/config.ts:36-64
Cross-replica room eventssignalingtopic exchange signaling-exchange (+signaling-exchange const)pub/subsignaling-server/src/index.ts:119, signalingAmqp.ts
media-converter (consumer)media-convertqueue media-convertserves RPCmedia-converter/src/config.ts:63-91
Janus events → signaling(Janus side)Janus RabbitMQ transport/event-handlereventsdocker/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 images node-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-postgresmigrator (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:36 sets ROOM_JOIN_SESSION_SECRET: .vs29o9@wpk&ep.-pspq_ and Postgres creds appuser/apppass in 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, because amqp-*/src/ is regenerated. make up’s devcontainer copies pre-generated /tmp/.../src in (docker-compose.yml:146). A bare pnpm install && pnpm -r build on a fresh checkout will fail for the AMQP packages without first running generation. The .dev scripts (generate && build) and the Dockerfiles encode the correct order.

docker-bake.hcl default group references undefined room-state-postgres target.

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-converter omits --frozen-lockfile (commented out), so its image can drift from pnpm-lock.yaml while the other two services pin it.

Recording filename contract is load-bearing

RecordPathManager regex janus-(.+)-room-(.+)-user-(.+)-publisher-(\d+)-(video|audio)-(\d+)\.mjr$ and the cam/mic/scr index convention (video-0, audio-1, video-2) are duplicated knowledge between record-path-manager, media-converter, and extract_janus_recordings.sh. Changing Janus’s naming breaks conversion silently (convertMjr returns 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_MS 10s) are the only liveness guards; keepalive is driven from each JanusSession. 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 of janus-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 file make up actually uses, 11.6K): I read the sibling docker-compose.yml in full but only confirmed make up targets -dev.yml; differences between the two were not line-verified.
  • Janus .jcfg config contents: existence and mount mapping verified via docker-compose.yml; individual .jcfg file 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-read JanusSession.ts, JanusPlugin.ts, PluginRegistry.ts, core.messages.ts, the JanusVideoRoomPlugin/* 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.ts read in full; webrtc/peer.ts, webrtc/trackManager.ts, socket/WebSocketClient.ts not line-read.
  • shared/ message definitions: barrels read; signaling.messages.ts, WebSocketMessage.ts, asyncQueue.ts bodies not line-read.
  • dev/ harness sources (client/{index,test,track-manager}.ts, static.ts): not read beyond readme.md description.
  • pnpm-lock.yaml: only counted resolutions (195); not audited.
  • Password-hash plaintexts in definitions.json are SHA-256 hashes (not reversible here); the actual passwords are the hardcoded ones in the *.config.ts files.

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.yml
  • scripts/extract_janus_recordings.sh; amqp-janus-client/scripts/generate.sh (+ diff proving the other two identical)
  • .github/workflows/publish.yaml
  • signaling-server/src/{index,config}.ts
  • room-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.ts
  • media-converter/src/{index,config,mediaConverter}.ts, media-converter/src/scripts/convertRoom.ts, media-converter/readme.md
  • server-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.ts
  • client/src/index.ts, client/src/signaling/SignalingSdk.ts
  • shared/src/index.ts
  • record-path-manager/src/recordPathManager.ts
  • docker/run/rabbitmq/config/definitions.json
  • amqp-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.