Skip to content
Draft
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,47 @@ The `osc_eyevinn_intercom_manager` resource requires these variables:
| `ENDPOINT_IDLE_TIMEOUT_S` | Idle timeout in seconds for SMB endpoints (default: `60`) |
| `OSC_ACCESS_TOKEN` | Personal Access Token from OSC for link sharing and reauthenticating (optional) |
| `ICE_SERVERS` | Comma-separated list of ICE servers in the format: `turn:username:password@turn.example.com,stun:stun.example.com`. If no STUN server is provided, and WHIP endpoints are used, Google's default STUN server (`stun:stun.l.google.com:19302`) will be used. |
| `WHIP_GATEWAY_URL` | URL of the [SRT-WHIP Gateway](https://github.com/Eyevinn/srt-whip-gateway) for IO bridge transmitters (optional). Enables the transmitter bridge API when set |
| `WHIP_GATEWAY_API_KEY` | API key for the SRT-WHIP Gateway (optional) |
| `WHEP_GATEWAY_URL` | URL of the [WHEP-SRT Gateway](https://github.com/Eyevinn/whep-srt-gateway) for IO bridge receivers (optional). Enables the receiver bridge API when set |
| `WHEP_GATEWAY_API_KEY` | API key for the WHEP-SRT Gateway (optional) |
| `DEBUG_BRIDGE` | Set to `true` to enable verbose logging from the bridge manager reconcile loop (optional) |
| `MONGODB_CONNECTION_STRING` | DEPRECATED: Use `DB_CONNECTION_STRING` instead |

## IO Bridge

The IO bridge enables SRT-to-WebRTC and WebRTC-to-SRT bridging, allowing external SRT streams to be ingested into intercom production lines (transmitters) and intercom audio to be sent out as SRT streams (receivers).

- **Transmitters** (SRT to WebRTC): An SRT source is received by the [SRT-WHIP Gateway](https://github.com/Eyevinn/srt-whip-gateway) and ingested into a production line via WHIP.
- **Receivers** (WebRTC to SRT): Audio from a production line is received via WHEP from the [WHEP-SRT Gateway](https://github.com/Eyevinn/whep-srt-gateway) and output as an SRT stream.

The bridge is enabled by setting `WHIP_GATEWAY_URL` and/or `WHEP_GATEWAY_URL`. A bridge manager runs a sync loop (1s interval) that reconciles the desired state in the database with the actual state on the gateways.

The bridge API is available at `/api/v1/bridge/transmitters` and `/api/v1/bridge/receivers`, and a configuration endpoint at `/api/v1/bridge/config` reports which gateways are enabled.

### Local development with gateways

To run the gateways locally for development:

```sh
# SRT-WHIP Gateway (transmitters) — requires Node.js
git clone https://github.com/Eyevinn/srt-whip-gateway.git
cd srt-whip-gateway && npm install && npm run dev
# Runs on port 3000

# WHEP-SRT Gateway (receivers) — requires Node.js
git clone https://github.com/Eyevinn/whep-srt-gateway.git
cd whep-srt-gateway && npm install && npm run dev
# Runs on port 3001
```

Then set the environment variables:

```sh
WHIP_GATEWAY_URL=http://localhost:3000
WHEP_GATEWAY_URL=http://localhost:3001
```

## Installation / Usage

Start an Intercom Manager instance:
Expand Down
14 changes: 13 additions & 1 deletion src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,19 @@
getPreset: jest.fn().mockResolvedValue(undefined),
getPresets: jest.fn().mockResolvedValue([]),
deletePreset: jest.fn().mockResolvedValue(true),
updatePreset: jest.fn().mockResolvedValue(undefined)
updatePreset: jest.fn().mockResolvedValue(undefined),
addTransmitter: jest.fn().mockResolvedValue(undefined),
getTransmitter: jest.fn().mockResolvedValue(undefined),
getTransmitters: jest.fn().mockResolvedValue([]),
getTransmittersLength: jest.fn().mockResolvedValue(0),
updateTransmitter: jest.fn().mockResolvedValue(undefined),
deleteTransmitter: jest.fn().mockResolvedValue(true),
addReceiver: jest.fn().mockResolvedValue(undefined),
getReceiver: jest.fn().mockResolvedValue(undefined),
getReceivers: jest.fn().mockResolvedValue([]),
getReceiversLength: jest.fn().mockResolvedValue(0),
updateReceiver: jest.fn().mockResolvedValue(undefined),
deleteReceiver: jest.fn().mockResolvedValue(true)
};

const mockProductionManager = {
Expand Down Expand Up @@ -71,12 +83,12 @@
on: jest.fn(),
once: jest.fn(),
emit: jest.fn()
} as any;

Check warning on line 86 in src/api.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

const mockIngestManager = {
load: jest.fn().mockResolvedValue(undefined),
startPolling: jest.fn()
} as any;

Check warning on line 91 in src/api.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

describe('api', () => {
it('responds with hello, world!', async () => {
Expand Down
60 changes: 55 additions & 5 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import apiReAuth from './api_re_auth';
import apiShare from './api_share';
import apiWhip, { ApiWhipOptions } from './api_whip';
import apiWhep, { ApiWhepOptions } from './api_whep';
import apiBridgeTx, { ApiBridgeTxOptions } from './api_bridge_tx';
import apiBridgeRx, { ApiBridgeRxOptions } from './api_bridge_rx';
import { DbManager } from './db/interface';
import { IngestManager } from './ingest_manager';
import { ProductionManager } from './production_manager';
Expand Down Expand Up @@ -54,15 +56,22 @@ export interface ApiGeneralOptions {
endpointIdleTimeout: string;
smbServerApiKey?: string;
publicHost: string;
whipAuthKey?: string;
dbManager: DbManager;
productionManager: ProductionManager;
ingestManager: IngestManager;
whipGatewayUrl?: string;
whipGatewayApiKey?: string;
whepGatewayUrl?: string;
whepGatewayApiKey?: string;
}

export type ApiOptions = ApiGeneralOptions &
ApiProductionsOptions &
ApiWhipOptions &
ApiWhepOptions;
ApiWhepOptions &
ApiBridgeTxOptions &
ApiBridgeRxOptions;

export default async (opts: ApiOptions) => {
const api = fastify({
Expand Down Expand Up @@ -128,8 +137,7 @@ export default async (opts: ApiOptions) => {
smbServerApiKey: opts.smbServerApiKey,
dbManager: opts.dbManager,
productionManager: opts.productionManager,
coreFunctions: opts.coreFunctions,
smb: opts.smb
coreFunctions: opts.coreFunctions
});
api.register(apiWhip, {
prefix: 'api/v1',
Expand All @@ -140,7 +148,7 @@ export default async (opts: ApiOptions) => {
productionManager: opts.productionManager,
dbManager: opts.dbManager,
whipAuthKey: opts.whipAuthKey,
smb: opts.smb
whipGatewayUrl: opts.whipGatewayUrl
});
api.register(apiWhep, {
prefix: 'api/v1',
Expand All @@ -151,12 +159,54 @@ export default async (opts: ApiOptions) => {
productionManager: opts.productionManager,
dbManager: opts.dbManager,
whipAuthKey: opts.whipAuthKey,
smb: opts.smb
whepGatewayUrl: opts.whepGatewayUrl
});
api.register(apiShare, { publicHost: opts.publicHost, prefix: 'api/v1' });
api.register(apiReAuth, { prefix: 'api/v1' });
api.register(apiGroups, { prefix: 'api/v1', dbManager: opts.dbManager });

// Bridge configuration endpoint
const BridgeConfig = Type.Object({
whipGatewayEnabled: Type.Boolean(),
whepGatewayEnabled: Type.Boolean()
});

api.get<{ Reply: Static<typeof BridgeConfig> }>(
'/api/v1/bridge/config',
{
schema: {
description: 'Get bridge gateway configuration',
response: {
200: BridgeConfig
}
}
},
async (_, reply) => {
reply.send({
whipGatewayEnabled: !!opts.whipGatewayUrl,
whepGatewayEnabled: !!opts.whepGatewayUrl
});
}
);

// Register bridge IO endpoints (only if gateways are configured)
if (opts.whipGatewayUrl) {
api.register(apiBridgeTx, {
prefix: 'api/v1',
dbManager: opts.dbManager,
whipGatewayUrl: opts.whipGatewayUrl,
whipGatewayApiKey: opts.whipGatewayApiKey
});
}
if (opts.whepGatewayUrl) {
api.register(apiBridgeRx, {
prefix: 'api/v1',
dbManager: opts.dbManager,
whepGatewayUrl: opts.whepGatewayUrl,
whepGatewayApiKey: opts.whepGatewayApiKey
});
}

api.all('/whip/:productionId/:lineId', async (request, reply) => {
if (request.method !== 'POST' && request.method !== 'OPTIONS') {
return reply
Expand Down
Loading
Loading