Skip to content
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,15 @@ The object to pass when creating a new endpoint must refer to the following stru
label: "<Display name to use in the VideoRoom|AudioBridge room, Record&Play recording or as an NDI sender (optional)">,
token: "<token to require via Bearer authorization when using WHIP: can be either a string, or a callback function to validate the provided token (optional)>",
iceServers: [ array of STUN/TURN servers to return via Link headers (optional, overrides global ones) ],
recipient: { ... plain RTP recipient (optional, only supported for VideoRoom) ... },
recipients: [ { ... plain RTP recipient (optional, only supported for VideoRoom) ... } ],
secret: "<VideoRoom secret, if required for external RTP forwarding (optional)>",
adminKey: "<VideoRoom plugin Admin Key, if required for external RTP forwarding (optional)>"
adminKey: "<VideoRoom plugin Admin Key, if required for external RTP forwarding (optional)>",
customize: <callback function to provide most of the above properties dynamically, for each publisher>
}
```

See the [examples](https://github.com/meetecho/simple-whip-server/tree/master/examples) for more info.

Publishing to a WHIP endpoint via WebRTC can be done by sending an SDP offer to the created `<basePath>/endpoint/<id>` endpoint via HTTP POST, which will interact with Janus on your behalf and, if successful, return an SDP answer back in the 200 OK. If you're using [Simple WHIP Client](https://github.com/meetecho/simple-whip-client) to test, the full HTTP path to the endpoint is all you need to provide as the WHIP url.

As per the specification, the response to the publish request will contain a `Location` header which points to the resource to use to refer to the stream. In this implementation, the resource is handled by the same server instance, and is randomized to a `<basePath>/resource/<rid>` endpoint (returned as a relative path in the header). That's the address used for interacting with the session, i.e., for tricking candidates, restarting ICE, and tearing down the session. The server is configured to automatically allow trickle candidates to be sent via HTTP PATCH to the `<basePath>/resource/<rid>` endpoint: if you'd like the server to not allow trickle candidates instead (e.g., to test if your client handles a failure gracefully), you can disable them when creating the server via `allowTrickle`. ICE restarts are supported too. Finally, that's also the address you'll need to send the HTTP DELETE request to, in case you want to signal the intention to tear down the WebRTC PeerConnection.
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This folder contains a few example applications using the Janus WHIP library.

* The `server-shared` folder, instead, contains an example where the application pre-creates a REST server for its own needs, and then tells the library to re-use that server for the WHIP functionality too. A sample endpoint is created, with a callback function used to validate the token any time one is presented.

* The `server-dynamic` folder shows how you can use dynamic features for a new endpoint, e.g., by choosing different rooms or RTP forwarding recipients for different WHIP publishers contacting the same endpoint.

* The `audiobridge` folder shows how you can configure a WHIP endpoint to publish to the AudioBridge plugin, instead of the VideoRoom (which is the default).

* The `recordplay` folder shows how you can configure a WHIP endpoint to publish to the RecordPlay plugin, instead of the VideoRoom (which is the default), thus simply recording the published media to a Janus recording.
Expand Down
28 changes: 28 additions & 0 deletions examples/server-dynamic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "janus-whip-server-dynamic",
"description": "WHIP server where endpoints settings are dynamic",
"type": "module",
"keywords": [
"whip",
"wish",
"janus",
"webrtc",
"meetecho"
],
"author": {
"name": "Lorenzo Miniero",
"email": "[email protected]"
},
"repository": {
"type": "git",
"url": "https://github.com/meetecho/simple-whip-server.git"
},
"license": "ISC",
"private": true,
"main": "src/index.js",
"dependencies": {},
"scripts": {
"build": "npm install --omit=dev",
"start": "node src/index.js"
}
}
53 changes: 53 additions & 0 deletions examples/server-dynamic/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import express from 'express';
import http from 'http';
import { JanusWhipServer } from '../../../src/whip.js';

(async function main() {
console.log('Example: WHIP server using dynamic endpoints');
let server = null;

// Create an HTTP server and bind to port 7180 just to list endpoints
let myApp = express();
myApp.get('/endpoints', async (_req, res) => {
res.setHeader('content-type', 'application/json');
res.status(200);
res.send(JSON.stringify(server.listEndpoints()));
});
http.createServer({}, myApp).listen(7180);

// Create a WHIP server, binding to port 7080 and using base path /whip
server = new JanusWhipServer({
janus: {
address: 'ws://localhost:8188'
},
rest: {
port: 7080,
basePath: '/whip'
}
});
// Add a couple of global event handlers
server.on('janus-disconnected', () => {
console.log('WHIP server lost connection to Janus');
});
server.on('janus-reconnected', () => {
console.log('WHIP server reconnected to Janus');
});
// Start the server
await server.start();

// Create a test endpoint using a callback function to provide the settings
let count = 1;
let endpoint = server.createEndpoint({ id: 'abc123', customize: function(settings) {
// This is called every time a publisher connects, which means
// we can return different values for each of them dynamically
settings.room = 1234;
settings.label = 'WHIP Publisher #' + count;
count++;
}});
endpoint.on('endpoint-active', function() {
console.log(this.id + ': Endpoint is active');
});
endpoint.on('endpoint-inactive', function() {
console.log(this.id + ': Endpoint is inactive');
});
}());
98 changes: 68 additions & 30 deletions src/whip.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,29 @@ class JanusWhipServer extends EventEmitter {
return randomString;
}

createEndpoint({ id, plugin, room, secret, adminKey, pin, label, token, iceServers, recipient }) {
if(!id || (plugin !== 'ndi' && plugin !== 'recordplay' && !room))
createEndpoint({ id, plugin, room, secret, adminKey, pin, label, token, iceServers, recipient, recipients, customize }) {
if(!id || (plugin !== 'ndi' && plugin !== 'recordplay' && !room && !customize))
throw new Error('Invalid arguments');
if(!plugin)
plugin = 'videoroom';
if(plugin && plugin !== 'videoroom' && plugin !== 'audiobridge' && plugin !== 'recordplay' && plugin !== 'ndi')
throw new Error('Unsupported plugin');
if(this.endpoints.has(id))
throw new Error('Endpoint already exists');
if(customize && typeof customize !== 'function')
throw new Error('Invalid customize function');
if(recipient && recipients)
throw new Error('Can\'t provide both recipient and recipients');
if(recipient)
recipients = [ recipient ];
if(recipients) {
if(!Array.isArray(recipients))
throw new Error('Invalid recipients (not an array)');
for(let r of recipients) {
if(Object.prototype.toString.call(r) !== '[object Object]')
throw new Error('Invalid recipient (not an object)');
}
}
let endpoint = new JanusWhipEndpoint({
id: id,
plugin: plugin,
Expand All @@ -136,7 +150,8 @@ class JanusWhipServer extends EventEmitter {
label: label ? label : 'WHIP Publisher ' + room,
token: token,
iceServers: iceServers,
recipient: recipient
recipients: recipients,
customize: customize
});
this.logger.info('[' + id + '] Created new WHIP endpoint');
this.endpoints.set(id, endpoint);
Expand Down Expand Up @@ -373,11 +388,27 @@ class JanusWhipServer extends EventEmitter {
delete endpoint.latestEtag;
}
});
if(endpoint.plugin === 'videoroom') {
endpoint.publisher = await endpoint.handle.joinConfigurePublisher({
// Before attaching, let's check if there's a customize callback
// function so that the application can configure things dynamically
let settings = endpoint;
if(endpoint.customize) {
settings = {
room: endpoint.room,
secret: endpoint.secret,
adminKey: endpoint.adminKey,
pin: endpoint.pin,
display: endpoint.label,
label: endpoint.label,
iceServers: endpoint.iceServers ? JSON.parse(JSON.stringify(endpoint.iceServers)) : undefined,
recipients: endpoint.recipients ? JSON.parse(JSON.stringify(endpoint.recipients)) : undefined
};
endpoint.customize(settings);
}
// Check which plugin we should contact
if(endpoint.plugin === 'videoroom') {
endpoint.publisher = await endpoint.handle.joinConfigurePublisher({
room: settings.room,
pin: settings.pin,
display: settings.label,
audio: true,
video: true,
jsep: {
Expand All @@ -387,9 +418,9 @@ class JanusWhipServer extends EventEmitter {
});
} else if(endpoint.plugin === 'audiobridge') {
await endpoint.handle.join({
room: endpoint.room,
pin: endpoint.pin,
display: endpoint.label
room: settings.room,
pin: settings.pin,
display: settings.label
});
endpoint.publisher = await endpoint.handle.configure({
jsep: {
Expand All @@ -399,15 +430,15 @@ class JanusWhipServer extends EventEmitter {
});
} else if(endpoint.plugin === 'recordplay') {
endpoint.publisher = await endpoint.handle.record({
name: endpoint.label,
name: settings.label,
jsep: {
type: 'offer',
sdp: req.body
}
});
} else if(endpoint.plugin === 'ndi') {
endpoint.publisher = await endpoint.handle.translate({
name: endpoint.label,
name: settings.label,
jsep: {
type: 'offer',
sdp: req.body
Expand All @@ -418,22 +449,28 @@ class JanusWhipServer extends EventEmitter {
console.log('Tally:', data);
});
}
if(endpoint.plugin === 'videoroom' && endpoint.recipient && endpoint.recipient.host && (endpoint.recipient.audioPort > 0 || endpoint.recipient.videoPort > 0)) {
// Configure an RTP forwarder too
const max32 = Math.pow(2, 32) - 1;
let details = {
room: endpoint.room,
feed: endpoint.publisher.feed,
secret: endpoint.secret,
admin_key: endpoint.adminKey,
host: endpoint.recipient.host,
audio_port: endpoint.recipient.audioPort,
audio_ssrc: Math.floor(Math.random() * max32),
video_port: endpoint.recipient.videoPort,
video_ssrc: Math.floor(Math.random() * max32),
video_rtcp_port: endpoint.recipient.videoRtcpPort
};
await endpoint.handle.startForward(details);
if(endpoint.plugin === 'videoroom' && settings.recipients && settings.recipients.length > 0) {
for(let recipient of settings.recipients) {
if(recipient && recipient.host && (recipient.audioPort > 0 || recipient.videoPort > 0)) {
// Configure an RTP forwarder for this recipient
const max32 = Math.pow(2, 32) - 1;
let details = {
room: settings.room,
feed: endpoint.publisher.feed,
secret: settings.secret,
admin_key: settings.adminKey,
host: recipient.host,
audio_port: recipient.audioPort,
audio_ssrc: recipient.audioSsrc ?
recipient.audioSsrc : Math.floor(Math.random() * max32),
video_port: recipient.videoPort,
video_ssrc: recipient.videoSsrc ?
recipient.videoSsrc : Math.floor(Math.random() * max32),
video_rtcp_port: recipient.videoRtcpPort
};
await endpoint.handle.startForward(details);
}
}
}
endpoint.enabling = false;
endpoint.enabled = true;
Expand All @@ -442,7 +479,7 @@ class JanusWhipServer extends EventEmitter {
res.setHeader('Accept-Patch', 'application/trickle-ice-sdpfrag');
res.setHeader('Location', endpoint.resource);
res.set('ETag', '"' + endpoint.latestEtag + '"');
let iceServers = endpoint.iceServers ? endpoint.iceServers : this.config.iceServers;
let iceServers = settings.iceServers ? settings.iceServers : this.config.iceServers;
if(iceServers && iceServers.length > 0) {
// Add a Link header for each static ICE server
let links = [];
Expand Down Expand Up @@ -746,7 +783,7 @@ class JanusWhipServer extends EventEmitter {

// WHIP endpoint class
class JanusWhipEndpoint extends EventEmitter {
constructor({ id, plugin, room, secret, adminKey, pin, label, token, iceServers, recipient }) {
constructor({ id, plugin, room, secret, adminKey, pin, label, token, iceServers, recipient, recipients, customize }) {
super();
this.id = id;
this.plugin = plugin;
Expand All @@ -757,7 +794,8 @@ class JanusWhipEndpoint extends EventEmitter {
this.label = label;
this.token = token;
this.iceServers = iceServers;
this.recipient = recipient;
this.recipients = recipients;
this.customize = customize;
this.enabled = false;
}
}
Expand Down