Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental WHIP support #107

Open
wants to merge 3 commits into
base: v3
Choose a base branch
from
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
44 changes: 44 additions & 0 deletions server/lib/Room.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ class Room extends EventEmitter
return this._mediasoupRouter.rtpCapabilities;
}

/**
* Get a Broadcaster.
*
* @type {String} broadcasterId - Broadcaster id.
*/
getBroadcaster({ broadcasterId })
{
return this._broadcasters.get(broadcasterId);
}

/**
* Create a Broadcaster. This is for HTTP API requests (see server.js).
*
Expand Down Expand Up @@ -510,6 +520,40 @@ class Room extends EventEmitter
await transport.connect({ dtlsParameters });
}

/**
* Restart ICE for a Broadcaster mediasoup WebRtcTransport.
*
* @async
*
* @type {String} broadcasterId
* @type {String} transportId
*/
async restartBroadcasterTransportICE(
{
broadcasterId,
transportId
}
)
{
const broadcaster = this._broadcasters.get(broadcasterId);

if (!broadcaster)
throw new Error(`broadcaster with id "${broadcasterId}" does not exist`);

const transport = broadcaster.data.transports.get(transportId);

if (!transport)
throw new Error(`transport with id "${transportId}" does not exist`);

if (transport.constructor.name !== 'WebRtcTransport')
{
throw new Error(
`transport with id "${transportId}" is not a WebRtcTransport`);
}

return await transport.restartIce();
}

/**
* Create a mediasoup Producer associated to a Broadcaster.
*
Expand Down
2 changes: 1 addition & 1 deletion server/lib/interactiveServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ class Interactive
useColors : true,
useGlobal : true,
ignoreUndefined : false,
preview : false,
preview : false
});

this._isTerminalOpen = true;
Expand Down
5 changes: 4 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
"awaitqueue": "^2.3.3",
"body-parser": "^1.19.0",
"colors": "^1.4.0",
"cors": "^2.8.5",
"debug": "^4.3.1",
"express": "^4.17.1",
"heapdump": "^0.3.15",
"mediasoup": "github:versatica/mediasoup#v3",
"mediasoup-client": "github:versatica/mediasoup-client#v3",
"pidusage": "^2.0.21",
"protoo-server": "^4.0.5"
"protoo-server": "^4.0.5",
"sdp-transform": "^2.14.1"
},
"devDependencies": {
"eslint": "^6.8.0",
Expand Down
218 changes: 209 additions & 9 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ const url = require('url');
const protoo = require('protoo-server');
const mediasoup = require('mediasoup');
const express = require('express');
const bodyParser = require('body-parser');
const { AwaitQueue } = require('awaitqueue');
const Logger = require('./lib/Logger');
const Room = require('./lib/Room');
const interactiveServer = require('./lib/interactiveServer');
const interactiveClient = require('./lib/interactiveClient');
const sdpTransform = require('sdp-transform');
const sdpCommonUtils = require('mediasoup-client/lib/handlers/sdp/commonUtils');
const ortc = require('mediasoup-client/lib/ortc');
const { RemoteSdp } = require('mediasoup-client/lib/handlers/sdp/RemoteSdp');
const sdpUnifiedPlanUtils = require('mediasoup-client/lib/handlers/sdp/unifiedPlanUtils');
const utils = require('mediasoup-client/lib/utils');
const cors = require('cors');

const logger = new Logger();

Expand Down Expand Up @@ -134,22 +140,31 @@ async function createExpressApp()

expressApp = express();

expressApp.use(bodyParser.json());
expressApp.use(express.json());
expressApp.use(express.text({
type : [
'application/sdp',
'application/trickle-ice-sdpfrag',
'text/plain'
]
}));
expressApp.use(
cors({
origin : true
})
);

/**
* For every API request, verify that the roomId in the path matches and
* existing room.
*/
expressApp.param(
'roomId', (req, res, next, roomId) =>
'roomId', async (req, res, next, roomId) =>
{
// The room must exist for all API requests.
if (!rooms.has(roomId))
{
const error = new Error(`room with id "${roomId}" not found`);

error.status = 404;
throw error;
await getOrCreateRoom({ roomId });
}

req.room = rooms.get(roomId);
Expand Down Expand Up @@ -233,7 +248,7 @@ async function createExpressApp()
broadcasterId,
type,
rtcpMux,
comedia,
comedia,
sctpCapabilities
});

Expand Down Expand Up @@ -363,7 +378,7 @@ async function createExpressApp()
next(error);
}
});

/**
* POST API to create a mediasoup DataProducer associated to a Broadcaster.
* The exact Transport in which the DataProducer must be created is signaled in
Expand Down Expand Up @@ -395,6 +410,191 @@ async function createExpressApp()
}
});

/**
* WHIP post handler.
*/
expressApp.post(
'/whip/:roomId/:broadcasterId', async (req, res, next) =>
{
logger.info('whip POST', req.params, req.headers, req.body);
const { broadcasterId } = req.params;

try
{
const localSdpObject = sdpTransform.parse(req.body);

const rtpCapabilities = sdpCommonUtils.extractRtpCapabilities(
{ sdpObject: localSdpObject });
const dtlsParameters = sdpCommonUtils.extractDtlsParameters(
{ sdpObject: localSdpObject });

const routerRtpCapabilities = req.room.getRouterRtpCapabilities();
const extendedRtpCapabilities = ortc.getExtendedRtpCapabilities(
rtpCapabilities, routerRtpCapabilities);

const sendingRtpParametersByKind =
{
audio : ortc.getSendingRtpParameters('audio', extendedRtpCapabilities),
video : ortc.getSendingRtpParameters('video', extendedRtpCapabilities)
};
const sendingRemoteRtpParametersByKind =
{
audio : ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities),
video : ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities)
};

// Create a broadcaster, if it not exists.
let broadcaster = req.room.getBroadcaster({ broadcasterId });

if (!broadcaster)
{
await req.room.createBroadcaster({
id : broadcasterId,
displayName : 'WHIP broadcaster',
device : { name: 'WHIP device' },
rtpCapabilities
});
broadcaster = req.room.getBroadcaster({ broadcasterId });
}

// Create a WebRTC transport.
const transport = await req.room.createBroadcasterTransport({
broadcasterId,
type : 'webrtc'
});

// Connect the WebRTC transport.
await req.room.connectBroadcasterTransport({
broadcasterId,
transportId : transport.id,
dtlsParameters
});

const remoteSdp = new RemoteSdp({
iceParameters : transport.iceParameters,
iceCandidates : transport.iceCandidates,
dtlsParameters : transport.dtlsParameters,
sctpParameters : transport.sctpParameters
});

broadcaster.data.transports.get(transport.id).appData.remoteSdp = remoteSdp;

// Publish audio and video.
for (const { type, mid } of localSdpObject.media)
{
const mediaSectionIdx = remoteSdp.getNextMediaSectionIdx();
const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];

const sendingRtpParameters =
utils.clone(sendingRtpParametersByKind[type], {});

const sendingRemoteRtpParameters =
utils.clone(sendingRemoteRtpParametersByKind[type], {});

// Set MID.
sendingRtpParameters.mid = String(mid);

// Set RTCP CNAME.
sendingRtpParameters.rtcp.cname =
sdpCommonUtils.getCname({ offerMediaObject });

// Set RTP encodings by parsing the SDP offer.
sendingRtpParameters.encodings =
sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject });

remoteSdp.send({
offerMediaObject,
reuseMid : mediaSectionIdx.reuseMid,
offerRtpParameters : sendingRtpParameters,
answerRtpParameters : sendingRemoteRtpParameters,
codecOptions : {},
extmapAllowMixed : true
});

await req.room.createBroadcasterProducer({
broadcasterId,
transportId : transport.id,
kind : type,
rtpParameters : sendingRtpParameters
});
}
const answer = remoteSdp.getSdp();

res.contentType('application/sdp')
.status(201)
.send(answer);
}
catch (error)
{
next(error);
}
});

/**
* WHIP patch handler.
*/
expressApp.patch(
'/whip/:roomId/:broadcasterId', async (req, res, next) =>
{
logger.info('whip PATCH', req.params, req.headers, req.body);
const { broadcasterId } = req.params;

try
{
const broadcaster = req.room.getBroadcaster({ broadcasterId });

if (!broadcaster)
throw Error(`broadcaster with id "${broadcasterId}" does not exist`);

if (!broadcaster.data.transports.size)
throw Error(`broadcaster with id "${broadcasterId}" has no transports`);

const transport = [ ...broadcaster.data.transports.values() ][0];
const { remoteSdp } = transport.appData;

if (!remoteSdp)
throw Error(`broadcaster with id "${broadcasterId}" has no remote SDP set`);

const iceParameters = await req.room.restartBroadcasterTransportICE({
broadcasterId,
transportId : transport.id
});

remoteSdp.updateIceParameters(iceParameters);

const answer = remoteSdp.getSdp();

res.contentType('application/sdp')
.status(200)
.send(answer);
}
catch (error)
{
next(error);
}
});

/**
* WHIP delete handler.
*/
expressApp.delete(
'/whip/:roomId/:broadcasterId', async (req, res, next) =>
{
logger.info('whip DELETE', req.params, req.headers);
const { broadcasterId } = req.params;

try
{
req.room.deleteBroadcaster({ broadcasterId });
res.contentType('text/plain').status(200)
.send();
}
catch (error)
{
next(error);
}
});

/**
* Error handler.
*/
Expand Down