SFU update
This commit is contained in:
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Parse passed arguments
|
||||
let passedPublicIP = null;
|
||||
for(let arg of process.argv){
|
||||
if(arg && arg.startsWith("--PublicIP=")){
|
||||
let splitArr = arg.split("=");
|
||||
if(splitArr.length == 2){
|
||||
passedPublicIP = splitArr[1];
|
||||
console.log("--PublicIP=" + passedPublicIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
signallingURL: "ws://localhost:0000",
|
||||
|
||||
mediasoup: {
|
||||
worker: {
|
||||
rtcMinPort: 40000,
|
||||
rtcMaxPort: 49999,
|
||||
logLevel: "debug",
|
||||
logTags: [
|
||||
"info",
|
||||
"ice",
|
||||
"dtls",
|
||||
"rtp",
|
||||
"srtp",
|
||||
"rtcp",
|
||||
"sctp",
|
||||
// 'rtx',
|
||||
// 'bwe',
|
||||
// 'score',
|
||||
// 'simulcast',
|
||||
// 'svc'
|
||||
],
|
||||
},
|
||||
router: {
|
||||
mediaCodecs: [
|
||||
{
|
||||
kind: "audio",
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
mimeType: 'video/VP8',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/h264",
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1
|
||||
},
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
// here you must specify ip addresses to listen on
|
||||
// some browsers have issues with connecting to ICE on
|
||||
// localhost so you might have to specify a proper
|
||||
// private or public ip here.
|
||||
webRtcTransport: {
|
||||
listenIps: passedPublicIP != null ? [{ ip: "0.0.0.0", announcedIp: passedPublicIP}] : getLocalListenIps(),
|
||||
// 100 megabits
|
||||
initialAvailableOutgoingBitrate: 100_000_000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function getLocalListenIps() {
|
||||
const listenIps = []
|
||||
if (typeof window === 'undefined') {
|
||||
const os = require('os')
|
||||
const networkInterfaces = os.networkInterfaces()
|
||||
const ips = []
|
||||
if (networkInterfaces) {
|
||||
for (const [key, addresses] of Object.entries(networkInterfaces)) {
|
||||
addresses.forEach(address => {
|
||||
if (address.family === 'IPv4') {
|
||||
listenIps.push({ ip: address.address, announcedIp: null })
|
||||
}
|
||||
/* ignore link-local and other special ipv6 addresses.
|
||||
* https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.xhtml
|
||||
*/
|
||||
else if (address.family === 'IPv6' && address.address[0] !== 'f') {
|
||||
listenIps.push({ ip: address.address, announcedIp: null })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listenIps.length === 0) {
|
||||
listenIps.push({ ip: '127.0.0.1', announcedIp: null })
|
||||
}
|
||||
return listenIps
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright © 2020, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -0,0 +1,182 @@
|
||||
# mediasoup-sdp-bridge v3
|
||||
|
||||
[![][npm-shield-mediasoup-sdp-bridge]][npm-mediasoup-sdp-bridge]
|
||||
[![][travis-ci-shield-mediasoup-sdp-bridge]][travis-ci-mediasoup-sdp-bridge]
|
||||
|
||||
Node.js library to allow integration of SDP based clients with [mediasoup][mediasoup-website].
|
||||
|
||||
|
||||
## Website and Documentation
|
||||
|
||||
* [mediasoup.org][mediasoup-website]
|
||||
|
||||
|
||||
## Support Forum
|
||||
|
||||
* [mediasoup.discourse.group][mediasoup-discourse]
|
||||
|
||||
|
||||
## Use-Case Design Proposal
|
||||
|
||||
This section contains a use-case that can serve as usage example to guide design of the internal implementation.
|
||||
|
||||
Within the Node.js server app running mediasoup:
|
||||
|
||||
|
||||
### mediasoup receiving media from a remote SDP endpoint
|
||||
|
||||
```typescript
|
||||
import * as SdpBridge from "mediasoup-sdp-bridge";
|
||||
import Signaling from "./my-signaling"; // Our own signaling stuff.
|
||||
|
||||
const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport.
|
||||
|
||||
// Create an SdpEndpoint to handle SDP negotiation with the remote endpoint.
|
||||
const sdpEndpoint = await SdpBridge.createSdpEndpoint({
|
||||
transport: transport,
|
||||
});
|
||||
|
||||
// Upon receipt of an SDP Offer from the remote endpoint, apply it.
|
||||
Signaling.on("sdp-offer", async (sdpOffer: string) => {
|
||||
// For each media section in the SDP Offer, SdpEndpoint creates a new Producer
|
||||
// on top of the Transport that was provided.
|
||||
const producers: Producer[] = await sdpEndpoint.processOffer(sdpOffer);
|
||||
|
||||
// Generate an SDP Answer and reply to the remote endpoint with it.
|
||||
const sdpAnswer: string = sdpEndpoint.createAnswer();
|
||||
|
||||
await Signaling.sendAnswer(sdpAnswer);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### mediasoup sending media to a remote SDP endpoint
|
||||
|
||||
```typescript
|
||||
import * as SdpBridge from "mediasoup-sdp-bridge";
|
||||
import Signaling from "./my-signaling"; // Our own signaling stuff.
|
||||
|
||||
const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport.
|
||||
|
||||
// Create an SdpEndpoint to send media to the remote endpoint.
|
||||
const sdpEndpoint = await createSdpEndpoint({
|
||||
transport: transport,
|
||||
});
|
||||
|
||||
// Listen for the "negotiationneeded" event, to send an SDP Offer to the remote
|
||||
// endpoint. This event is emitted when transport.consume() is called, or when
|
||||
// a Producer being consumed is closed or paused/resumed.
|
||||
sdpEndpoint.on("negotiationneeded", () => {
|
||||
// For each Consumer present in the Transport that was provided,
|
||||
// SdpEndpoint creates a new media section in the SDP Offer.
|
||||
const sdpOffer: string = sdpEndpoint.createOffer();
|
||||
|
||||
// Send the SDP Offer to the remote endpoint.
|
||||
await Signaling.sendOffer(sdpOffer);
|
||||
});
|
||||
|
||||
// Upon receipt of an SDP Answer from the remote endpoint, apply it.
|
||||
Signaling.on("sdp-answer", async (sdpAnswer: string) => {
|
||||
await sdpEndpoint.processAnswer(sdpAnswer);
|
||||
});
|
||||
|
||||
// Generate remote endpoint's RTP capabilities based on a remote SDP or based
|
||||
// on handmade capabilities.
|
||||
const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities(
|
||||
router.rtpCapabilities,
|
||||
remoteSdp
|
||||
);
|
||||
// or:
|
||||
const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities(
|
||||
router.rtpCapabilities,
|
||||
handmadeRtpCapabilities
|
||||
);
|
||||
|
||||
// If there were mediasoup Producers already created in the Router, or if a new
|
||||
// one is created, and we want to consume them in the remote endpoint, tell the
|
||||
// Transport to consume them. transport.consume() method will trigger the
|
||||
// "negotiationneeded" event, handled above.
|
||||
//
|
||||
// NOTE: By calling consume() method in parallel (without waiting for the
|
||||
// previous one to complete) we ensure that the "negotiationneeded" event will
|
||||
// just be emitted once upon completion of all consume() calls, so a single
|
||||
// SDP Offer/Answer roundtrip will be needed.
|
||||
transport
|
||||
.consume({
|
||||
producerId: producer1.id,
|
||||
rtpCapabilities: endpointRtpCapabilities,
|
||||
})
|
||||
.catch((error) => console.error("transport.consume() failed:", error));
|
||||
|
||||
transport
|
||||
.consume({
|
||||
producerId: producer2.id,
|
||||
rtpCapabilities: endpointRtpCapabilities,
|
||||
})
|
||||
.catch((error) => console.error("transport.consume() failed:", error));
|
||||
```
|
||||
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Design limitations
|
||||
|
||||
The initial Use-Case Design Proposal lacks an important detail: it uses an `endpointRtpCapabilities` object, which represents the WebRTC and RTP capabilities of the remote endpoint that will receive media from mediasoup. This *RtpCapabilities* object is assumed to be written either by hand, or obtained from a previous SDP message that somehow might have been obtained from the remote endpoint. It is only *after* having these *RtpCapabilities*, that the SDP Offer/Answer process starts.
|
||||
|
||||
All this, however, goes backwards with the normal flow of the SDP Offer/Answer model. **The remote capabilities should be obtained from the SDP Offer/Answer exchange itself**, not as an unspecified out-of-band mechanism. In theory, how the mediasoup application learns about remote capabilities should come from one of these sources:
|
||||
|
||||
1. An SDP Offer (with *recvonly* or *sendrecv* direction) from a remote endpoint that wants to receive media.
|
||||
|
||||
2. An SDP Answer (with *recvonly* or *sendrecv* direction) from a remote endpoint, in response to an SDP Offer (with *sendonly* or *sendrecv* direction) that the application had previously sent.
|
||||
|
||||
However, in practice both of these options conflict with the current design proposal:
|
||||
|
||||
* (1) is not being considered for now. mediasoup is designed around the assumption that the participant sending media should always be the one starting the connection; thus, the endpoint that will send media is also the one sending the SDP Offer.
|
||||
|
||||
* (2) is the ideal but *it's not possible* with the current design, because the remote capabilities must be already known by the time `sdpEndpoint.createOffer()` is called.
|
||||
|
||||
(Note: Additionally, the *sendrecv* direction is also not considered for now. Both the local application or the remote endpoints are be assumed to be either *sendonly* or *recvonly*.)
|
||||
|
||||
|
||||
### Current implementation
|
||||
|
||||
The implementation found in this repo is enough to cover basic usage, but is not complete by any means. Also, it tries to work around the limitations described above, using alternatives that are far from ideal.
|
||||
|
||||
Some notes:
|
||||
|
||||
* Receiving media is the part that works best. It suffices to call `SdpEndpoint.processOffer()`, and this will return one Producer for each media section found in the SDP Offer.
|
||||
|
||||
* Sending media, on the other hand, suffers from the limitation described in (2) above. To work around this, the class `BrowserRtpCapabilities` contains predefined capabilities objects for some of the most common web browsers. These can be used by the application to provide `transport.consume()` with something to work with.
|
||||
|
||||
The obvious drawback to this solution is that the objects in `BrowserRtpCapabilities` must be kept up to date from time to time, in order to accurately represent the actual capabilities of web browsers. To help with this task, there is a handy tool that can be found in the [tools/browser-rtpcapabilities](./tools/browser-rtpcapabilities/) subdirectory.
|
||||
|
||||
* SDP renegotiation is not implemented. The local endpoint cannot make an SDP Re-Offer when the state of the Producers or Consumers changes.
|
||||
|
||||
* There are minor improvements to be done in the implementation.
|
||||
|
||||
- Right now the SdpEndpoint class exports an `SdpEndpoint.addConsumer()` method, which the application uses to provide all Consumers that are created from the corresponding Transport. However, chances are that this is unnecessary: the Transport class already provides an observer event `Transport.observer.on("newconsumer")`, which could be used by the SdpEndpoint to be notified of all new Consumers in its Transport, saving the application the need to provide them explicitly with `addConsumer()`.
|
||||
|
||||
- Some unexpected errors are not handled gracefully, and instead a "BUG" error is logged before the application is forced to exit. These should probably be replaced by a `throw new Error(...)`.
|
||||
|
||||
- Some debug messages are simply commented out to avoid causing too much noise. A proper logging library should be used to allow setting different levels and hiding the less interesting ones.
|
||||
|
||||
- The code hasn't been linted. The default linter rules were left as per the initial commit, but haven't been used yet. For now, the code has just been formatted with the default rules from *Prettier.js*.
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)]
|
||||
* Juan Navarro [[github](https://github.com/j1elo)]
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[ISC](./LICENSE)
|
||||
|
||||
|
||||
[mediasoup-website]: https://mediasoup.org
|
||||
[mediasoup-discourse]: https://mediasoup.discourse.group
|
||||
[npm-shield-mediasoup-sdp-bridge]: https://img.shields.io/npm/v/mediasoup-sdp-bridge.svg
|
||||
[npm-mediasoup-sdp-bridge]: https://npmjs.org/package/mediasoup-sdp-bridge
|
||||
[travis-ci-shield-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge.svg?branch=master
|
||||
[travis-ci-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sdpToSendRtpParameters = exports.sdpToRecvRtpCapabilities = void 0;
|
||||
const MsRtpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/unifiedPlanUtils"));
|
||||
const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils"));
|
||||
const MsOrtc = __importStar(require("mediasoup-client/lib/ortc"));
|
||||
require("util").inspect.defaultOptions.depth = null;
|
||||
function sdpToRecvRtpCapabilities(sdpObject, localCaps) {
|
||||
const caps = MsSdpUtils.extractRtpCapabilities({
|
||||
sdpObject,
|
||||
});
|
||||
try {
|
||||
MsOrtc.validateRtpCapabilities(caps);
|
||||
}
|
||||
catch (err) {
|
||||
console.error("FIXME BUG:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps);
|
||||
const recvCaps = MsOrtc.getRecvRtpCapabilities(extendedCaps);
|
||||
{
|
||||
}
|
||||
return recvCaps;
|
||||
}
|
||||
exports.sdpToRecvRtpCapabilities = sdpToRecvRtpCapabilities;
|
||||
function sdpToSendRtpParameters(sdpObject, sdpMediaObj, localCaps, kind) {
|
||||
var _a;
|
||||
const caps = MsSdpUtils.extractRtpCapabilities({
|
||||
sdpObject,
|
||||
});
|
||||
try {
|
||||
MsOrtc.validateRtpCapabilities(caps);
|
||||
}
|
||||
catch (err) {
|
||||
console.error("FIXME BUG:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps);
|
||||
const sendParams = MsOrtc.getSendingRemoteRtpParameters(kind, extendedCaps);
|
||||
// const sdpMediaObj = (sdpObject.media || []).find((m) => m.type === kind) ||
|
||||
// {};
|
||||
if ("mid" in sdpMediaObj) {
|
||||
sendParams.mid = String(sdpMediaObj.mid);
|
||||
}
|
||||
else {
|
||||
sendParams.mid = kind === "audio" ? "0" : "1";
|
||||
}
|
||||
if ("rids" in sdpMediaObj) {
|
||||
for (const mediaRid of sdpMediaObj.rids) {
|
||||
(_a = sendParams.encodings) === null || _a === void 0 ? void 0 : _a.push({ rid: mediaRid.id });
|
||||
}
|
||||
}
|
||||
else {
|
||||
// sendParams.encodings = MsRtpUtils.getRtpEncodings({
|
||||
// sdpObject,
|
||||
// kind,
|
||||
// });
|
||||
sendParams.encodings = MsRtpUtils.getRtpEncodings({ offerMediaObject: sdpMediaObj });
|
||||
}
|
||||
sendParams.rtcp = {
|
||||
cname: MsSdpUtils.getCname({ offerMediaObject: sdpMediaObj }),
|
||||
reducedSize: "rtcpRsize" in sdpMediaObj && sdpMediaObj.rtcpRsize,
|
||||
mux: "rtcpMux" in sdpMediaObj && sdpMediaObj.rtcpMux,
|
||||
};
|
||||
{
|
||||
}
|
||||
return sendParams;
|
||||
}
|
||||
exports.sdpToSendRtpParameters = sdpToSendRtpParameters;
|
||||
//# sourceMappingURL=SdpUtils.js.map
|
||||
@@ -0,0 +1,198 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateRtpCapabilities2 = exports.generateRtpCapabilities1 = exports.generateRtpCapabilities0 = exports.createSdpEndpoint = exports.SdpEndpoint = void 0;
|
||||
const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils"));
|
||||
const RemoteSdp_1 = require("mediasoup-client/lib/handlers/sdp/RemoteSdp");
|
||||
const SdpTransform = __importStar(require("sdp-transform"));
|
||||
const uuid_1 = require("uuid");
|
||||
const BrowserRtpCapabilities = __importStar(require("./BrowserRtpCapabilities"));
|
||||
const SdpUtils = __importStar(require("./SdpUtils"));
|
||||
const MediaSection_1 = require("mediasoup-client/lib/handlers/sdp/MediaSection");
|
||||
require("util").inspect.defaultOptions.depth = null;
|
||||
class SdpEndpoint {
|
||||
constructor(webRtcTransport, localCaps) {
|
||||
this.producers = [];
|
||||
this.producerMedias = [];
|
||||
this.consumers = [];
|
||||
this.webRtcTransport = webRtcTransport;
|
||||
this.transport = webRtcTransport;
|
||||
this.localCaps = localCaps;
|
||||
this.sctpMedia = null;
|
||||
this.consumeData = false;
|
||||
}
|
||||
async processOffer(sdpOffer) {
|
||||
if (this.remoteSdp) {
|
||||
console.error("[SdpEndpoint.processOffer] ERROR: A remote description was already set");
|
||||
return [];
|
||||
}
|
||||
this.remoteSdp = sdpOffer;
|
||||
const remoteSdpObj = SdpTransform.parse(sdpOffer);
|
||||
await this.webRtcTransport.connect({
|
||||
dtlsParameters: MsSdpUtils.extractDtlsParameters({
|
||||
sdpObject: remoteSdpObj,
|
||||
}),
|
||||
});
|
||||
for (const media of remoteSdpObj.media) {
|
||||
if (media.type == "application") {
|
||||
this.sctpMedia = media;
|
||||
console.log("[SdpEndpoint.processOffer] SCTP association received");
|
||||
}
|
||||
else {
|
||||
if (!("rtp" in media)) {
|
||||
continue;
|
||||
}
|
||||
if (!("direction" in media)) {
|
||||
continue;
|
||||
}
|
||||
if (media.direction !== "sendonly") {
|
||||
continue;
|
||||
}
|
||||
const sendParams = SdpUtils.sdpToSendRtpParameters(remoteSdpObj, media, this.localCaps, media.type);
|
||||
let producer;
|
||||
try {
|
||||
producer = await this.transport.produce({
|
||||
kind: media.type,
|
||||
rtpParameters: sendParams,
|
||||
paused: false,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.error("FIXME BUG:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
this.producers.push(producer);
|
||||
this.producerMedias.push(media);
|
||||
console.log("[SdpEndpoint.processOffer] mediasoup Producer created, kind: %s, type: %s, paused: %s", producer.kind, producer.type, producer.paused);
|
||||
}
|
||||
}
|
||||
return this.producers;
|
||||
}
|
||||
createAnswer() {
|
||||
if (this.localSdp) {
|
||||
console.error("[SdpEndpoint.createAnswer] ERROR: A local description was already set");
|
||||
return "";
|
||||
}
|
||||
const sdpBuilder = new RemoteSdp_1.RemoteSdp({
|
||||
iceParameters: this.webRtcTransport.iceParameters,
|
||||
iceCandidates: this.webRtcTransport.iceCandidates,
|
||||
dtlsParameters: this.webRtcTransport.dtlsParameters,
|
||||
sctpParameters: this.webRtcTransport.sctpParameters,
|
||||
planB: false,
|
||||
});
|
||||
console.log("[SdpEndpoint.createAnswer] Make 'recvonly' SDP Answer");
|
||||
for (let i = 0; i < this.producers.length; i++) {
|
||||
const sdpMediaObj = this.producerMedias[i];
|
||||
const recvParams = this.producers[i].rtpParameters;
|
||||
sdpBuilder.send({
|
||||
offerMediaObject: sdpMediaObj,
|
||||
reuseMid: undefined,
|
||||
offerRtpParameters: recvParams,
|
||||
answerRtpParameters: recvParams,
|
||||
codecOptions: undefined,
|
||||
extmapAllowMixed: false,
|
||||
});
|
||||
}
|
||||
if (this.sctpMedia != null) {
|
||||
sdpBuilder.sendSctpAssociation({offerMediaObject: this.sctpMedia});
|
||||
}
|
||||
this.localSdp = sdpBuilder.getSdp();
|
||||
return this.localSdp;
|
||||
}
|
||||
addConsumer(consumer) {
|
||||
this.consumers.push(consumer);
|
||||
}
|
||||
addConsumeData() {
|
||||
this.consumeData = true;
|
||||
}
|
||||
createOffer() {
|
||||
var _a;
|
||||
if (this.localSdp) {
|
||||
console.error("[SdpEndpoint.createOffer] ERROR: A local description was already set");
|
||||
return "";
|
||||
}
|
||||
const sdpBuilder = new RemoteSdp_1.RemoteSdp({
|
||||
iceParameters: this.webRtcTransport.iceParameters,
|
||||
iceCandidates: this.webRtcTransport.iceCandidates,
|
||||
dtlsParameters: this.webRtcTransport.dtlsParameters,
|
||||
sctpParameters: this.webRtcTransport.sctpParameters,
|
||||
planB: false,
|
||||
});
|
||||
const sendMsid = uuid_1.v4().substr(0, 8);
|
||||
console.log("[SdpEndpoint.createOffer] Make 'sendonly' SDP Offer");
|
||||
for (let i = 0; i < this.consumers.length; i++) {
|
||||
const mid = (_a = this.consumers[i].rtpParameters.mid) !== null && _a !== void 0 ? _a : "nomid";
|
||||
const kind = this.consumers[i].kind;
|
||||
const sendParams = this.consumers[i].rtpParameters;
|
||||
sdpBuilder.receive({
|
||||
mid,
|
||||
kind,
|
||||
offerRtpParameters: sendParams,
|
||||
streamId: sendMsid,
|
||||
trackId: `${sendMsid}-${kind}`,
|
||||
});
|
||||
}
|
||||
if (this.consumeData) {
|
||||
sdpBuilder.receiveSctpAssociation();
|
||||
}
|
||||
this.localSdp = sdpBuilder.getSdp();
|
||||
return this.localSdp;
|
||||
}
|
||||
async processAnswer(sdpAnswer) {
|
||||
if (this.remoteSdp) {
|
||||
console.error("[SdpEndpoint.processAnswer] ERROR: A remote description was already set");
|
||||
return;
|
||||
}
|
||||
this.remoteSdp = sdpAnswer;
|
||||
const remoteSdpObj = SdpTransform.parse(sdpAnswer);
|
||||
await this.webRtcTransport.connect({
|
||||
dtlsParameters: MsSdpUtils.extractDtlsParameters({
|
||||
sdpObject: remoteSdpObj,
|
||||
}),
|
||||
});
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.SdpEndpoint = SdpEndpoint;
|
||||
function createSdpEndpoint(webRtcTransport, localCaps) {
|
||||
return new SdpEndpoint(webRtcTransport, localCaps);
|
||||
}
|
||||
exports.createSdpEndpoint = createSdpEndpoint;
|
||||
function generateRtpCapabilities0() {
|
||||
return BrowserRtpCapabilities.chrome;
|
||||
}
|
||||
exports.generateRtpCapabilities0 = generateRtpCapabilities0;
|
||||
function generateRtpCapabilities1(localCaps, remoteSdp) {
|
||||
console.error("[SdpEndpoint.generateRtpCapabilities1] BUG: Not implemented");
|
||||
process.exit(1);
|
||||
let caps;
|
||||
return caps;
|
||||
}
|
||||
exports.generateRtpCapabilities1 = generateRtpCapabilities1;
|
||||
function generateRtpCapabilities2(localCaps, remoteCaps) {
|
||||
console.error("[SdpEndpoint.generateRtpCapabilities2] BUG: Not implemented");
|
||||
process.exit(1);
|
||||
let caps;
|
||||
return caps;
|
||||
}
|
||||
exports.generateRtpCapabilities2 = generateRtpCapabilities2;
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "mediasoup-sdp-bridge",
|
||||
"version": "3.6.5",
|
||||
"description": "Node.js library to allow integration of SDP based clients with mediasoup",
|
||||
"contributors": [
|
||||
"Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
|
||||
"Juan Navarro <juan.navarro@gmx.es> (https://github.com/j1elo)"
|
||||
],
|
||||
"homepage": "https://mediasoup.org",
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/versatica/mediasoup-sdp-bridge.git"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mediasoup"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"dependencies": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
}
|
||||
}
|
||||
Generated
+386
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"name": "pixelstreaming-sfu",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pixelstreaming-sfu",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"mediasoup_prebuilt": "^3.8.4",
|
||||
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
|
||||
"run-script-os": "^1.1.6",
|
||||
"ws": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"mediasoup-sdp-bridge": {
|
||||
"version": "3.6.5",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mediasoup"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/events": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
|
||||
"integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA=="
|
||||
},
|
||||
"node_modules/awaitqueue": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz",
|
||||
"integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fake-mediastreamtrack": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz",
|
||||
"integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.1",
|
||||
"uuid": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/h264-profile-level-id": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz",
|
||||
"integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mediasoup_prebuilt": {
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz",
|
||||
"integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.9.1",
|
||||
"awaitqueue": "^2.3.3",
|
||||
"debug": "^4.3.2",
|
||||
"h264-profile-level-id": "^1.0.1",
|
||||
"netstring": "^0.3.0",
|
||||
"random-number": "^0.0.9",
|
||||
"supports-color": "^9.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/mediasoup-client": {
|
||||
"version": "3.6.46",
|
||||
"resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz",
|
||||
"integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/events": "^3.0.0",
|
||||
"awaitqueue": "^2.3.3",
|
||||
"bowser": "^2.11.0",
|
||||
"debug": "^4.3.2",
|
||||
"events": "^3.3.0",
|
||||
"fake-mediastreamtrack": "^1.1.6",
|
||||
"h264-profile-level-id": "^1.0.1",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"supports-color": "^9.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mediasoup"
|
||||
}
|
||||
},
|
||||
"node_modules/mediasoup-sdp-bridge": {
|
||||
"resolved": "mediasoup-sdp-bridge",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/netstring": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz",
|
||||
"integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI=",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/random-number": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz",
|
||||
"integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw=="
|
||||
},
|
||||
"node_modules/run-script-os": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz",
|
||||
"integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==",
|
||||
"bin": {
|
||||
"run-os": "index.js",
|
||||
"run-script-os": "index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
|
||||
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz",
|
||||
"integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
|
||||
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"requires": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
||||
},
|
||||
"@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
|
||||
"integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA=="
|
||||
},
|
||||
"awaitqueue": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz",
|
||||
"integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw=="
|
||||
},
|
||||
"bowser": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
|
||||
},
|
||||
"fake-mediastreamtrack": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz",
|
||||
"integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.1",
|
||||
"uuid": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"h264-profile-level-id": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz",
|
||||
"integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"mediasoup_prebuilt": {
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz",
|
||||
"integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==",
|
||||
"requires": {
|
||||
"@types/node": "^16.9.1",
|
||||
"awaitqueue": "^2.3.3",
|
||||
"debug": "^4.3.2",
|
||||
"h264-profile-level-id": "^1.0.1",
|
||||
"netstring": "^0.3.0",
|
||||
"random-number": "^0.0.9",
|
||||
"supports-color": "^9.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"mediasoup-client": {
|
||||
"version": "3.6.46",
|
||||
"resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz",
|
||||
"integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==",
|
||||
"requires": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/events": "^3.0.0",
|
||||
"awaitqueue": "^2.3.3",
|
||||
"bowser": "^2.11.0",
|
||||
"debug": "^4.3.2",
|
||||
"events": "^3.3.0",
|
||||
"fake-mediastreamtrack": "^1.1.6",
|
||||
"h264-profile-level-id": "^1.0.1",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"supports-color": "^9.1.0"
|
||||
}
|
||||
},
|
||||
"mediasoup-sdp-bridge": {
|
||||
"version": "file:mediasoup-sdp-bridge",
|
||||
"requires": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"netstring": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz",
|
||||
"integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI="
|
||||
},
|
||||
"random-number": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz",
|
||||
"integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw=="
|
||||
},
|
||||
"run-script-os": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz",
|
||||
"integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
|
||||
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz",
|
||||
"integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
|
||||
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "pixelstreaming-sfu",
|
||||
"version": "1.0.0",
|
||||
"description": "Reference implementation for a PixelStreaming SFU",
|
||||
"scripts": {
|
||||
"start-local": "run-script-os --",
|
||||
"start-local:windows": ".\\platform_scripts\\cmd\\run.bat",
|
||||
"start-local:default": "./platform_scripts/bash/run_local.sh",
|
||||
"start-cloud": "run-script-os --",
|
||||
"start-cloud:windows": ".\\platform_scripts\\cmd\\run_cloud.bat",
|
||||
"start-cloud:default": "./platform_scripts/bash/run_cloud.sh",
|
||||
"start": "run-script-os",
|
||||
"start:windows": "platform_scripts\\cmd\\node\\node.exe sfu_server.js",
|
||||
"start:default": "if [ `id -u` -eq 0 ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node sfu_server.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node sfu_server.js\"\nfi\n$process "
|
||||
|
||||
},
|
||||
"dependencies": {
|
||||
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
|
||||
"ws": "^7.1.2",
|
||||
"mediasoup_prebuilt": "^3.8.4",
|
||||
"run-script-os": "^1.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
FROM node:latest
|
||||
|
||||
# Make sure Mediasoup requirements are met
|
||||
RUN apt -y update
|
||||
RUN apt -y install python3-pip
|
||||
|
||||
# Copy the Selective Forwarding Unit (SFU) to the Docker build context
|
||||
COPY . /opt/SFU
|
||||
|
||||
# Install the dependencies for the mediasoup server
|
||||
WORKDIR /opt/SFU
|
||||
RUN npm update
|
||||
RUN npm install .
|
||||
|
||||
# Expose TCP port 80 for player WebSocket connections and web server HTTP access
|
||||
EXPOSE 40000-49999
|
||||
|
||||
# Expose TCP port 8888 for streamer WebSocket connections
|
||||
EXPOSE 8889
|
||||
|
||||
# Set the signalling server as the container's entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/node", "/opt/SFU/sfu_server.js"]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
function log_msg() { #message
|
||||
if [ ! -z $VERBOSE ]; then
|
||||
echo $1
|
||||
fi
|
||||
}
|
||||
|
||||
function print_usage() {
|
||||
echo "
|
||||
Usage:
|
||||
${0} [--help] [--publicip <IP Address>] [sfu options...]
|
||||
Where:
|
||||
--help will print this message and stop this script.
|
||||
--debug will run all scripts with --inspect
|
||||
--nosudo will run all scripts without \`sudo\` command useful for when run in containers.
|
||||
--verbose will enable additional logging
|
||||
--package-manager <package manager name> specify an alternative package manager to apt-get
|
||||
--publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for
|
||||
default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the SFU
|
||||
Other options: stored and passed to the SFU. All parameters printed once the script values are set.
|
||||
"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function print_parameters() {
|
||||
echo ""
|
||||
echo "${0} is running with the following parameters:"
|
||||
echo "--------------------------------------"
|
||||
echo "Public IP address : ${publicip}"
|
||||
echo "SFU command line arguments: ${sfucmd}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function set_start_default_values() {
|
||||
# publicip and sfucmd are always needed
|
||||
publicip=$(curl -s https://api.ipify.org)
|
||||
if [[ -z $publicip ]]; then
|
||||
publicip="127.0.0.1"
|
||||
fi
|
||||
|
||||
sfucmd=""
|
||||
}
|
||||
|
||||
function use_args() {
|
||||
while(($#)) ; do
|
||||
case "$1" in
|
||||
--debug ) IS_DEBUG=1; shift;;
|
||||
--nosudo ) NO_SUDO=1; shift;;
|
||||
--verbose ) VERBOSE=1; shift;;
|
||||
--publicip ) publicip="$2"; shift 2;;
|
||||
--help ) print_usage;;
|
||||
* ) echo "Unknown command, adding to SFU command line: $1"; sfucmd+=" $1"; shift;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function call_setup_sh() {
|
||||
bash "setup.sh"
|
||||
}
|
||||
|
||||
function start_process() {
|
||||
if [ ! -z $NO_SUDO ]; then
|
||||
log_msg "running with sudo removed"
|
||||
eval $(echo "$@" | sed 's/sudo//g')
|
||||
else
|
||||
eval $@
|
||||
fi
|
||||
}
|
||||
|
||||
function get_version() {
|
||||
local version=$1
|
||||
|
||||
if command -v $version; then
|
||||
version=$($@)
|
||||
fi
|
||||
|
||||
echo $version | sed -E 's/[^0-9.]//g'
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Build docker image for the Selective Forwarding Unit (SFU)
|
||||
|
||||
# When run from SFU/platform_scripts/bash, this uses the SFU directory
|
||||
# as the build context so the SFU files can be successfully copied into the container image
|
||||
docker build -t 'mediasoup_sfu:latest' -f ./Dockerfile ../..
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Start docker container by name using host networking
|
||||
docker run --name sfu_latest --network host --rm mediasoup_sfu
|
||||
|
||||
# Interactive start example
|
||||
#docker run --name sfu_latest --network host --rm -it --entrypoint /bin/bash mediasoup_sfu
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Stop the docker container
|
||||
PSID=$(docker ps -a -q --filter="name=sfu_latest")
|
||||
if [ -z "$PSID" ]; then
|
||||
echo "Docker SFU is not running, no stopping will be done"
|
||||
exit 1;
|
||||
fi
|
||||
echo "Stopping Mediasoup SFU server ..."
|
||||
docker stop sfu_latest
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values # No server specific defaults
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
print_parameters
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
arguments="--PublicIP=${publicip}"
|
||||
|
||||
# Add arguments passed to script to arguments for executable
|
||||
arguments+=" ${sfucmd}"
|
||||
|
||||
pushd ../.. > /dev/null
|
||||
|
||||
echo "Running: $process $arguments"
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process $arguments
|
||||
popd
|
||||
|
||||
popd
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values # No server specific defaults
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
|
||||
pushd ../.. > /dev/null
|
||||
|
||||
echo ""
|
||||
echo "Starting (S)elective (F)orwarding (U)nit use ctrl-c to exit"
|
||||
echo "-----------------------------------------"
|
||||
echo ""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process
|
||||
|
||||
popd > /dev/null # ../..
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
use_args $@
|
||||
# Azure specific fix to allow installing NodeJS from NodeSource
|
||||
if test -f "/etc/apt/sources.list.d/azure-cli.list"; then
|
||||
sudo touch /etc/apt/sources.list.d/nodesource.list
|
||||
sudo touch /usr/share/keyrings/nodesource.gpg
|
||||
sudo chmod 644 /etc/apt/sources.list.d/nodesource.list
|
||||
sudo chmod 644 /usr/share/keyrings/nodesource.gpg
|
||||
sudo chmod 644 /etc/apt/sources.list.d/azure-cli.list
|
||||
fi
|
||||
|
||||
function check_version() { #current_version #min_version
|
||||
#check if same string
|
||||
if [ -z "$2" ] || [ "$1" = "$2" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local i current minimum
|
||||
|
||||
IFS="." read -r -a current <<< $1
|
||||
IFS="." read -r -a minimum <<< $2
|
||||
|
||||
# fill empty fields in current with zeros
|
||||
for ((i=${#current[@]}; i<${#minimum[@]}; i++))
|
||||
do
|
||||
current[i]=0
|
||||
done
|
||||
|
||||
for ((i=0; i<${#current[@]}; i++))
|
||||
do
|
||||
if [[ -z ${minimum[i]} ]]; then
|
||||
# fill empty fields in minimum with zeros
|
||||
minimum[i]=0
|
||||
fi
|
||||
|
||||
if ((10#${current[i]} > 10#${minimum[i]})); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ((10#${current[i]} < 10#${minimum[i]})); then
|
||||
return 2
|
||||
fi
|
||||
done
|
||||
|
||||
# if got this far string is the same once we added missing 0
|
||||
return 0
|
||||
}
|
||||
|
||||
function check_and_install() { #dep_name #get_version_string #version_min #install_command
|
||||
local is_installed=0
|
||||
|
||||
log_msg "Checking for required $1 install"
|
||||
|
||||
local current=$(echo $2 | sed -E 's/[^0-9.]//g')
|
||||
local minimum=$(echo $3 | sed -E 's/[^0-9.]//g')
|
||||
|
||||
if [ $# -ne 4 ]; then
|
||||
log_msg "check_and_install expects 4 args (dep_name get_version_string version_min install_command) got $#"
|
||||
return -1
|
||||
fi
|
||||
|
||||
if [ ! -z $current ]; then
|
||||
log_msg "Current version: $current checking >= $minimum"
|
||||
check_version "$current" "$minimum"
|
||||
if [ "$?" -lt 2 ]; then
|
||||
log_msg "$1 is installed."
|
||||
return 0
|
||||
else
|
||||
log_msg "Required install of $1 not found installing"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $is_installed -ne 1 ]; then
|
||||
echo "$1 installation not found installing..."
|
||||
|
||||
start_process $4
|
||||
|
||||
if [ $? -ge 1 ]; then
|
||||
echo "Installation of $1 failed try running `export VERBOSE=1` then run this script again for more details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking Pixel Streaming SFU dependencies."
|
||||
|
||||
# navigate to SFU root
|
||||
pushd ../.. > /dev/null
|
||||
|
||||
node_version=""
|
||||
if [[ -f "${BASH_LOCATION}/node/bin/node" ]]; then
|
||||
node_version=$("${BASH_LOCATION}/node/bin/node" --version)
|
||||
fi
|
||||
check_and_install "node" "$node_version" "v16.4.2" "curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz --output node.tar.xz
|
||||
&& tar -xf node.tar.xz
|
||||
&& rm node.tar.xz
|
||||
&& mv node-v*-linux-x64 \"${BASH_LOCATION}/node\""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install
|
||||
|
||||
popd > /dev/null # SFU
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
|
||||
echo "All Pixel Streaming SFU dependencies up to date."
|
||||
@@ -0,0 +1,19 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script directory as working directory.
|
||||
pushd "%~dp0"
|
||||
|
||||
title SFU
|
||||
|
||||
@Rem Get our public IP if we are running this SFU on the cloud we will need this.
|
||||
FOR /F "tokens=*" %%g IN ('curl -L -S -s https://api.ipify.org') do (SET PUBLICIP=%%g)
|
||||
|
||||
@Rem Call out run.bat and pass in the Public IP we grabbed earlier.
|
||||
call run_local.bat --PublicIP=%PUBLICIP%
|
||||
|
||||
@Rem Pop script directory.
|
||||
popd
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,25 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script directory as working directory.
|
||||
pushd "%~dp0"
|
||||
|
||||
title SFU
|
||||
|
||||
@Rem Run setup to ensure we have node and mediasoup installed.
|
||||
call setup.bat
|
||||
|
||||
@Rem Move to sfu_server.js directory.
|
||||
pushd ..\..
|
||||
|
||||
@Rem Run node server and pass any argument along.
|
||||
platform_scripts\cmd\node\node.exe sfu_server %*
|
||||
|
||||
@Rem Pop sfu_server directory.
|
||||
popd
|
||||
|
||||
@Rem Pop script directory.
|
||||
popd
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,17 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script location as working directory for commands.
|
||||
pushd "%~dp0"
|
||||
|
||||
@Rem Ensure we have NodeJs available for calling.
|
||||
call setup_node.bat
|
||||
|
||||
@Rem Move to sfu_server.js directory and install its package.json
|
||||
pushd %~dp0\..\..\
|
||||
call platform_scripts\cmd\node\npm install --no-save
|
||||
popd
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,35 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script location as working directory for commands.
|
||||
pushd "%~dp0"
|
||||
|
||||
@Rem Name and version of node that we are downloading
|
||||
SET NodeVersion=v16.4.2
|
||||
SET NodeName=node-%NodeVersion%-win-x64
|
||||
|
||||
@Rem Look for a node directory next to this script
|
||||
if exist node\ (
|
||||
echo Node directory found...skipping install.
|
||||
) else (
|
||||
echo Node directory not found...beginning NodeJS download for Windows.
|
||||
|
||||
@Rem Download nodejs and follow redirects.
|
||||
curl -L -o ./node.zip "https://nodejs.org/dist/%NodeVersion%/%NodeName%.zip"
|
||||
|
||||
@Rem Unarchive the .zip
|
||||
tar -xf node.zip
|
||||
|
||||
@Rem Rename the extracted, versioned, directory that contains the NodeJS binaries to simply "node".
|
||||
ren "%NodeName%\" "node"
|
||||
|
||||
@Rem Delete the downloaded node.zip
|
||||
del node.zip
|
||||
)
|
||||
|
||||
@Rem Print node version
|
||||
echo Node version: & node\node.exe -v
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,335 @@
|
||||
var server_settings = process.argv.slice(2)
|
||||
if (!server_settings) {
|
||||
console.log('not enough arguments, exit()')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
server_settings = JSON.parse(server_settings)
|
||||
} catch (err) {
|
||||
console.log('can not process arguments, exit()')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const config = require('./config');
|
||||
config.signallingURL = 'ws://127.0.0.1:' + server_settings.sfu_port
|
||||
const WebSocket = require('ws');
|
||||
const mediasoup = require('mediasoup_prebuilt');
|
||||
const mediasoupSdp = require('mediasoup-sdp-bridge');
|
||||
|
||||
let signalServer = null;
|
||||
let mediasoupRouter;
|
||||
let streamer = null;
|
||||
let peers = new Map();
|
||||
|
||||
function connectSignalling(server) {
|
||||
console.log("Connecting to Signalling Server at %s", server);
|
||||
signalServer = new WebSocket(server);
|
||||
signalServer.addEventListener("open", _ => { console.log(`Connected to signalling server`); });
|
||||
signalServer.addEventListener("error", result => { console.log(`Error: ${result.message}`); });
|
||||
signalServer.addEventListener("message", result => onSignallingMessage(result.data));
|
||||
signalServer.addEventListener("close", result => {
|
||||
console.log(`Disconnected from signalling server: ${result.code} ${result.reason}`);
|
||||
console.log("Attempting reconnect to signalling server...");
|
||||
setTimeout(()=> {
|
||||
connectSignalling(server);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function onStreamerOffer(sdp) {
|
||||
console.log("Got offer from streamer");
|
||||
|
||||
if (streamer != null) {
|
||||
signalServer.close(1013 /* Try again later */, 'Producer is already connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = await createWebRtcTransport("Streamer");
|
||||
const sdpEndpoint = mediasoupSdp.createSdpEndpoint(transport, mediasoupRouter.rtpCapabilities);
|
||||
const producers = await sdpEndpoint.processOffer(sdp);
|
||||
const sdpAnswer = sdpEndpoint.createAnswer();
|
||||
const answer = { type: "answer", sdp: sdpAnswer };
|
||||
|
||||
console.log("Sending answer to streamer.");
|
||||
signalServer.send(JSON.stringify(answer));
|
||||
streamer = { transport: transport, producers: producers };
|
||||
}
|
||||
|
||||
function getNextStreamerSCTPId() {
|
||||
if(!streamer){
|
||||
throw new TypeError('Cannot generate an SCTP stream id - streamer was null.');
|
||||
}
|
||||
if (!streamer.transport || !streamer.transport.sctpParameters || typeof streamer.transport.sctpParameters.MIS !== 'number') {
|
||||
throw new TypeError('Streamer was not setup with the following require properties: streamer.transport.sctpParameters.MIS');
|
||||
}
|
||||
const numStreams = streamer.transport.sctpParameters.MIS;
|
||||
if (!streamer.dataStreamIds){
|
||||
streamer.dataStreamIds = Buffer.alloc(numStreams, 0);
|
||||
}
|
||||
if (!streamer.nextDataStreamId) {
|
||||
streamer.nextDataStreamId = 0;
|
||||
}
|
||||
|
||||
let sctpStreamId;
|
||||
for (let idx = streamer.nextDataStreamId; idx < streamer.dataStreamIds.length; ++idx) {
|
||||
sctpStreamId = idx % streamer.dataStreamIds.length;
|
||||
if (!streamer.dataStreamIds[sctpStreamId]) {
|
||||
streamer.nextDataStreamId = sctpStreamId + 1;
|
||||
return sctpStreamId;
|
||||
}
|
||||
}
|
||||
console.error("No available SCTP ids, they are all allocated.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
function onStreamerDisconnected() {
|
||||
console.log("Streamer disconnected from SFU");
|
||||
disconnectAllPeers();
|
||||
|
||||
if (streamer != null) {
|
||||
for (const mediaProducer of streamer.producers) {
|
||||
mediaProducer.close();
|
||||
}
|
||||
streamer.transport.close();
|
||||
streamer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onPeerConnected(peerId) {
|
||||
console.log("Player %s joined", peerId);
|
||||
|
||||
if (streamer == null) {
|
||||
console.log("No streamer connected, ignoring player.");
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = await createWebRtcTransport("Peer " + peerId);
|
||||
const sdpEndpoint = mediasoupSdp.createSdpEndpoint( transport, mediasoupRouter.rtpCapabilities );
|
||||
sdpEndpoint.addConsumeData(); // adds the sctp 'application' section to the offer
|
||||
|
||||
// media consumers
|
||||
let consumers = [];
|
||||
try {
|
||||
for (const mediaProducer of streamer.producers) {
|
||||
const consumer = await transport.consume({ producerId: mediaProducer.id, rtpCapabilities: mediasoupRouter.rtpCapabilities });
|
||||
consumer.observer.on("layerschange", function() { console.log("layer changed!", consumer.currentLayers); });
|
||||
sdpEndpoint.addConsumer(consumer);
|
||||
consumers.push(consumer);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error("transport.consume() failed:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
const offerSignal = {
|
||||
type: "offer",
|
||||
playerId: peerId,
|
||||
sdp: sdpEndpoint.createOffer(),
|
||||
sfu: true // indicate we're offering from sfu
|
||||
};
|
||||
|
||||
// send offer to peer
|
||||
signalServer.send(JSON.stringify(offerSignal));
|
||||
|
||||
const newPeer = {
|
||||
id: peerId,
|
||||
transport: transport,
|
||||
sdpEndpoint: sdpEndpoint,
|
||||
consumers: consumers
|
||||
};
|
||||
|
||||
// add the new peer
|
||||
peers.set(peerId, newPeer);
|
||||
}
|
||||
|
||||
async function setupPeerDataChannels(peerId) {
|
||||
const peer = peers.get(peerId);
|
||||
if (!peer) {
|
||||
console.error(`Could not send browser any datachannels for peer=${peerId} because peer was not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStreamerSCTPStreamId = getNextStreamerSCTPId();
|
||||
const nextPeerSCTPStreamId = getNextStreamerSCTPId();
|
||||
|
||||
console.log(`Attempting streamer SCTP id=${nextStreamerSCTPStreamId}`);
|
||||
|
||||
// streamer data producer (produces data for the peer)
|
||||
peer.streamerDataProducer = await streamer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextStreamerSCTPStreamId, ordered: true}});
|
||||
|
||||
console.log(`Attempting peer SCTP id=${nextPeerSCTPStreamId}`);
|
||||
|
||||
// peer data producer (produces data for the streamer)
|
||||
peer.peerDataProducer = await peer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextPeerSCTPStreamId, ordered: true}});
|
||||
|
||||
// peer data consumer (consumes streamer data)
|
||||
peer.peerDataConsumer = await peer.transport.consumeData({ dataProducerId: peer.streamerDataProducer.id });
|
||||
|
||||
// streamer data consumer (consumes peer data)
|
||||
peer.streamerDataConsumer = await streamer.transport.consumeData({ dataProducerId: peer.peerDataProducer.id });
|
||||
|
||||
const peerSignal = {
|
||||
type: 'peerDataChannels',
|
||||
playerId: peerId,
|
||||
sendStreamId: peer.peerDataProducer.sctpStreamParameters.streamId,
|
||||
recvStreamId: peer.peerDataConsumer.sctpStreamParameters.streamId
|
||||
};
|
||||
|
||||
// Send browser a message with a send/recv data channel SCTP stream id
|
||||
signalServer.send(JSON.stringify(peerSignal));
|
||||
|
||||
}
|
||||
|
||||
async function setupStreamerDataChannelsForPeer(peerId) {
|
||||
|
||||
const peer = peers.get(peerId);
|
||||
if (!peer) {
|
||||
console.error(`Could not send streamer any datachannels for peer=${peerId} because peer was not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!peer.streamerDataProducer || !peer.streamerDataConsumer){
|
||||
console.error(`There was no streamer data producer/consumer setup for peer=${peerId}. Did you make sure to send "dataChannelRequest" first?`);
|
||||
return;
|
||||
}
|
||||
|
||||
const streamerSignal = {
|
||||
type: "streamerDataChannels",
|
||||
playerId: peerId,
|
||||
sendStreamId: peer.streamerDataProducer.sctpStreamParameters.streamId,
|
||||
recvStreamId: peer.streamerDataConsumer.sctpStreamParameters.streamId
|
||||
};
|
||||
|
||||
// send streamer a message with a send/recv data channel SCTP stream id
|
||||
signalServer.send(JSON.stringify(streamerSignal));
|
||||
}
|
||||
|
||||
async function onPeerAnswer(peerId, sdp) {
|
||||
console.log("Got answer from player %s", peerId);
|
||||
|
||||
const consumer = peers.get(peerId);
|
||||
if (!consumer){
|
||||
console.error(`Unable to find player ${peerId}`);
|
||||
}
|
||||
else{
|
||||
consumer.sdpEndpoint.processAnswer(sdp);
|
||||
}
|
||||
}
|
||||
|
||||
function onPeerDisconnected(peerId) {
|
||||
console.log("Player %s disconnected", peerId);
|
||||
const peer = peers.get(peerId);
|
||||
if (peer != null) {
|
||||
for (consumer of peer.consumers) {
|
||||
consumer.close();
|
||||
}
|
||||
if (peer.peerDataConsumer) {
|
||||
peer.peerDataConsumer.close();
|
||||
peer.peerDataProducer.close();
|
||||
}
|
||||
if(peer.streamerDataConsumer){
|
||||
// Set the streamer sctp id we generated back to zero indicating it can be reused.
|
||||
if(streamer && streamer.dataStreamIds){
|
||||
const allocatedStreamId = peer.streamerDataProducer.sctpStreamParameters.streamId;
|
||||
const allocatedPeerStreamId = peer.peerDataProducer.sctpStreamParameters.streamId;
|
||||
streamer.dataStreamIds[allocatedStreamId] = 0;
|
||||
streamer.dataStreamIds[allocatedPeerStreamId] = 0;
|
||||
}
|
||||
peer.streamerDataConsumer.close();
|
||||
peer.streamerDataProducer.close();
|
||||
}
|
||||
peer.transport.close();
|
||||
}
|
||||
peers.delete(peerId);
|
||||
}
|
||||
|
||||
function disconnectAllPeers() {
|
||||
console.log("Disconnected all players");
|
||||
for (const [peerId, peer] of peers) {
|
||||
onPeerDisconnected(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSignallingMessage(message) {
|
||||
//console.log(`Got MSG: ${message}`);
|
||||
const msg = JSON.parse(message);
|
||||
|
||||
if (msg.type == 'offer') {
|
||||
onStreamerOffer(msg.sdp);
|
||||
}
|
||||
else if (msg.type == 'answer') {
|
||||
onPeerAnswer(msg.playerId, msg.sdp);
|
||||
}
|
||||
else if (msg.type == 'playerConnected') {
|
||||
onPeerConnected(msg.playerId);
|
||||
}
|
||||
else if (msg.type == 'playerDisconnected') {
|
||||
onPeerDisconnected(msg.playerId);
|
||||
}
|
||||
else if (msg.type == 'streamerDisconnected') {
|
||||
onStreamerDisconnected();
|
||||
}
|
||||
else if (msg.type == 'dataChannelRequest') {
|
||||
setupPeerDataChannels(msg.playerId);
|
||||
}
|
||||
else if (msg.type == 'peerDataChannelsReady') {
|
||||
setupStreamerDataChannelsForPeer(msg.playerId);
|
||||
}
|
||||
// todo a new message type for force layer switch (for debugging)
|
||||
// see: https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-setPreferredLayers
|
||||
// preferredLayers for debugging to select a particular simulcast layer, looks like { spatialLayer: 2, temporalLayer: 0 }
|
||||
}
|
||||
|
||||
async function startMediasoup() {
|
||||
let worker = await mediasoup.createWorker({
|
||||
logLevel: config.mediasoup.worker.logLevel,
|
||||
logTags: config.mediasoup.worker.logTags,
|
||||
rtcMinPort: config.mediasoup.worker.rtcMinPort,
|
||||
rtcMaxPort: config.mediasoup.worker.rtcMaxPort,
|
||||
});
|
||||
|
||||
worker.on('died', () => {
|
||||
console.error('mediasoup worker died (this should never happen)');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const mediaCodecs = config.mediasoup.router.mediaCodecs;
|
||||
const mediasoupRouter = await worker.createRouter({ mediaCodecs });
|
||||
|
||||
return mediasoupRouter;
|
||||
}
|
||||
|
||||
async function createWebRtcTransport(identifier) {
|
||||
const {
|
||||
listenIps,
|
||||
initialAvailableOutgoingBitrate
|
||||
} = config.mediasoup.webRtcTransport;
|
||||
|
||||
const transport = await mediasoupRouter.createWebRtcTransport({
|
||||
listenIps: listenIps,
|
||||
enableUdp: true,
|
||||
enableTcp: false,
|
||||
preferUdp: true,
|
||||
enableSctp: true, // datachannels
|
||||
initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate
|
||||
});
|
||||
|
||||
transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); });
|
||||
transport.on("iceselectedtuplechange", (iceTuple) => { console.log("%s ICE selected tuple %s", identifier, JSON.stringify(iceTuple)); });
|
||||
transport.on("sctpstatechange", (sctpState) => { console.log("%s SCTP state changed to %s", identifier, sctpState); });
|
||||
|
||||
return transport;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting Mediasoup...');
|
||||
console.log("Config = ");
|
||||
console.log(config);
|
||||
|
||||
mediasoupRouter = await startMediasoup();
|
||||
|
||||
connectSignalling(config.signallingURL);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user