diff --git a/Frontend/Docs/Settings Panel.md b/Frontend/Docs/Settings Panel.md index d636f76a..54fd26e1 100644 --- a/Frontend/Docs/Settings Panel.md +++ b/Frontend/Docs/Settings Panel.md @@ -19,7 +19,6 @@ This page will be updated with new features and commands as they become availabl | **Browser send offer** | The browser will start the WebRTC handshake instead of the Unreal Engine application. This is an advanced setting for users customising the frontend. Primarily for backwards compatibility for 4.x versions of the engine. | | **Use microphone** | Will start receiving audio input from your microphone and transmit it to the Unreal Engine. | | **Start video muted** | Muted audio when the stream starts. | -| **Prefer SFU** | Will attempt to use the Selective Forwarding Unit (SFU), if you have one running. | | **Is quality controller?** | Makes the encoder of the Pixel Streaming Plugin use the current browser connection to determine the bandwidth available, and therefore the quality of the stream encoding. **See notes below** | | **Force mono audio** | Force the browser to request mono audio in the SDP. | | **Force TURN** | Will attempt to connect exclusively via the TURN server. Will not work without an active TURN server. | diff --git a/Frontend/implementations/typescript/package-lock.json b/Frontend/implementations/typescript/package-lock.json index c9f2fffb..94705322 100644 --- a/Frontend/implementations/typescript/package-lock.json +++ b/Frontend/implementations/typescript/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3", + "name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3", + "name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4", "version": "0.0.1", "devDependencies": { "css-loader": "^6.7.3", diff --git a/Frontend/library/src/Config/Config.ts b/Frontend/library/src/Config/Config.ts index 8f2c8e7d..b4892b6a 100644 --- a/Frontend/library/src/Config/Config.ts +++ b/Frontend/library/src/Config/Config.ts @@ -23,7 +23,6 @@ export class Flags { static FakeMouseWithTouches = 'FakeMouseWithTouches' as const; static IsQualityController = 'ControlsQuality' as const; static MatchViewportResolution = 'MatchViewportRes' as const; - static PreferSFU = 'preferSFU' as const; static StartVideoMuted = 'StartVideoMuted' as const; static SuppressBrowserKeys = 'SuppressBrowserKeys' as const; static UseMic = 'UseMic' as const; @@ -315,17 +314,6 @@ export class Config { ) ); - this.flags.set( - Flags.PreferSFU, - new SettingFlag( - Flags.PreferSFU, - 'Prefer SFU', - 'Try to connect to the SFU instead of P2P.', - false, - useUrlParams - ) - ); - this.flags.set( Flags.IsQualityController, new SettingFlag( diff --git a/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts b/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts index 5878073b..390d701e 100644 --- a/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts +++ b/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts @@ -1384,12 +1384,6 @@ export class WebRtcPlayerController { if (messageStreamerList.ids.length == 1) { // If there's only a single streamer, subscribe to it regardless of what is in the URL autoSelectedStreamerId = messageStreamerList.ids[0]; - } else if ( - this.config.isFlagEnabled(Flags.PreferSFU) && - messageStreamerList.ids.includes('SFU') - ) { - // If the SFU toggle is on and there's an SFU connected, subscribe to it regardless of what is in the URL - autoSelectedStreamerId = 'SFU'; } else if ( urlParams.has(OptionParameters.StreamerId) && messageStreamerList.ids.includes( @@ -1406,8 +1400,9 @@ export class WebRtcPlayerController { ); } else { // no auto selected streamer - if (this.config.isFlagEnabled(Flags.WaitForStreamer)) { - this.startAutoJoinTimer() + if (messageStreamerList.ids.length == 0 && this.config.isFlagEnabled(Flags.WaitForStreamer)) { + this.closeSignalingServer(); + this.startAutoJoinTimer(); } } this.pixelStreaming.dispatchEvent( diff --git a/Frontend/ui-library/src/Config/ConfigUI.ts b/Frontend/ui-library/src/Config/ConfigUI.ts index f4ea65bd..e28c1aff 100644 --- a/Frontend/ui-library/src/Config/ConfigUI.ts +++ b/Frontend/ui-library/src/Config/ConfigUI.ts @@ -174,10 +174,6 @@ export class ConfigUI { psSettingsSection, this.flagsUi.get(Flags.StartVideoMuted) ); - this.addSettingFlag( - psSettingsSection, - this.flagsUi.get(Flags.PreferSFU) - ); this.addSettingFlag( psSettingsSection, this.flagsUi.get(Flags.IsQualityController) diff --git a/SFU/config.js b/SFU/config.js index 1387c88c..fbf11ff8 100644 --- a/SFU/config.js +++ b/SFU/config.js @@ -11,8 +11,18 @@ for(let arg of process.argv){ } const config = { + // The URL of the signalling server to connect to signallingURL: "ws://localhost:8889", + // The ID for this SFU to use. This will show up as a streamer ID on the signalling server + SFUId: "SFU", + + // The ID of the streamer to subscribe to. If you leave this blank it will subscribe to the first streamer it sees. + subscribeStreamerId: "DefaultStreamer", + + // Delay between list requests when looking for a specifc streamer. + retrySubscribeDelaySecs: 10, + mediasoup: { worker: { rtcMinPort: 40000, diff --git a/SFU/sfu_server.js b/SFU/sfu_server.js index 3401395f..cc26cfa2 100644 --- a/SFU/sfu_server.js +++ b/SFU/sfu_server.js @@ -3,6 +3,10 @@ const WebSocket = require('ws'); const mediasoup = require('mediasoup_prebuilt'); const mediasoupSdp = require('mediasoup-sdp-bridge'); +if (!config.retrySubscribeDelaySecs) { + config.retrySubscribeDelaySecs = 10; +} + let signalServer = null; let mediasoupRouter; let streamer = null; @@ -24,6 +28,35 @@ function connectSignalling(server) { }); } +async function onStreamerList(msg) { + let success = false; + + // subscribe to either the configured streamer, or if not configured, just grab the first id + if (msg.ids.length > 0) { + if (!!config.subscribeStreamerId && config.subscribeStreamerId.length != 0) { + if (msg.ids.includes(config.subscribeStreamerId)) { + signalServer.send(JSON.stringify({type: 'subscribe', streamerId: config.subscribeStreamerId})); + success = true; + } + } else { + signalServer.send(JSON.stringify({type: 'subscribe', streamerId: msg.ids[0]})); + success = true; + } + } + + if (!success) { + // did not subscribe to anything + setTimeout(function() { + signalServer.send(JSON.stringify({type: 'listStreamers'})); + }, config.retrySubscribeDelaySecs * 1000); + } +} + +async function onIdentify(msg) { + signalServer.send(JSON.stringify({type: 'endpointId', id: config.SFUId})); + signalServer.send(JSON.stringify({type: 'listStreamers'})); +} + async function onStreamerOffer(sdp) { console.log("Got offer from streamer"); @@ -57,6 +90,11 @@ function onStreamerDisconnected() { } streamer.transport.close(); streamer = null; + signalServer.send(JSON.stringify({type: 'stopStreaming'})); + + setTimeout(function() { + signalServer.send(JSON.stringify({type: 'listStreamers'})); + }, config.retrySubscribeDelaySecs * 1000); } } @@ -228,7 +266,7 @@ function onLayerPreference(msg) { } async function onSignallingMessage(message) { - //console.log(`Got MSG: ${message}`); + //console.log(`Got MSG: ${message}`); const msg = JSON.parse(message); if (msg.type == 'offer') { @@ -255,6 +293,12 @@ async function onSignallingMessage(message) { else if (msg.type == 'layerPreference') { onLayerPreference(msg); } + else if (msg.type == 'streamerList') { + onStreamerList(msg); + } + else if (msg.type == 'identify') { + onIdentify(msg); + } } async function startMediasoup() { @@ -276,6 +320,14 @@ async function startMediasoup() { return mediasoupRouter; } +async function onICEStateChange(identifier, iceState) { + console.log("%s ICE state changed to %s", identifier, iceState); + + if (identifier == 'Streamer' && iceState == 'completed') { + signalServer.send(JSON.stringify({type: 'startStreaming'})); + } +} + async function createWebRtcTransport(identifier) { const { listenIps, @@ -291,7 +343,7 @@ async function createWebRtcTransport(identifier) { initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate }); - transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); }); + transport.on("icestatechange", (iceState) => onICEStateChange(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); }); diff --git a/SignallingWebServer/cirrus.js b/SignallingWebServer/cirrus.js index 5dc2b03e..615a4a98 100644 --- a/SignallingWebServer/cirrus.js +++ b/SignallingWebServer/cirrus.js @@ -288,14 +288,83 @@ console.logColor(logging.Cyan, `Running Cirrus - The Pixel Streaming reference i let nextPlayerId = 1; +const StreamerType = { Regular: 0, SFU: 1 }; + +class Streamer { + constructor(initialId, ws, type) { + this.id = initialId; + this.ws = ws; + this.type = type; + this.idCommitted = false; + } + + // registers this streamers id + commitId(id) { + this.id = id; + this.idCommitted = true; + } + + // returns true if we have a valid id + isIdCommitted() { + return this.idCommitted; + } + + // links this streamer to a subscribed SFU player (player component of an SFU) + addSFUPlayer(sfuPlayerId) { + if (!!this.SFUPlayerId && this.SFUPlayerId != sfuPlayerId) { + console.error(`Streamer ${this.id} already has an SFU ${this.SFUPlayerId}. Trying to add ${sfuPlayerId} as SFU.`); + return; + } + this.SFUPlayerId = sfuPlayerId; + } + + // removes the previously subscribed SFU player + removeSFUPlayer() { + delete this.SFUPlayerId; + } + + // gets the player id of the subscribed SFU if any + getSFUPlayerId() { + return this.SFUPlayerId; + } + + // returns true if this streamer is forwarding another streamer + isSFU() { + return this.type == StreamerType.SFU; + } + + // links this streamer to a player, used for SFU connections since they have both components + setSFUPlayerComponent(playerComponent) { + if (!this.isSFU()) { + console.error(`Trying to add an SFU player component ${playerComponent.id} to streamer ${this.id} but it is not an SFU type.`); + return; + } + this.sfuPlayerComponent = playerComponent; + } + + // gets the player component for this sfu + getSFUPlayerComponent() { + if (!this.isSFU()) { + console.error(`Trying to get an SFU player component from streamer ${this.id} but it is not an SFU type.`); + return null; + } + return this.sfuPlayerComponent; + } +} + const PlayerType = { Regular: 0, SFU: 1 }; +const WhoSendsOffer = { Streamer: 0, Browser: 1 }; class Player { - constructor(id, ws, type, browserSendOffer) { + constructor(id, ws, type, whoSendsOffer) { this.id = id; this.ws = ws; this.type = type; - this.browserSendOffer = browserSendOffer; + this.whoSendsOffer = whoSendsOffer; + } + + isSFU() { + return this.type == PlayerType.SFU; } subscribe(streamerId) { @@ -304,13 +373,25 @@ class Player { return; } this.streamerId = streamerId; - const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: !this.browserSendOffer }; + if (this.type == PlayerType.SFU) { + let streamer = streamers.get(this.streamerId); + streamer.addSFUPlayer(this.id); + } + const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: this.whoSendsOffer == WhoSendsOffer.Streamer }; logOutgoing(this.streamerId, msg); this.sendFrom(msg); } unsubscribe() { if (this.streamerId && streamers.has(this.streamerId)) { + if (this.type == PlayerType.SFU) { + let streamer = streamers.get(this.streamerId); + if (streamer.getSFUPlayerId() != this.id) { + console.error(`Trying to unsibscribe SFU player ${this.id} from streamer ${streamer.id} but the current SFUId does not match (${streamer.getSFUPlayerId()}).`) + } else { + streamer.removeSFUPlayer(); + } + } const msg = { type: 'playerDisconnected', playerId: this.id }; logOutgoing(this.streamerId, msg); this.sendFrom(msg); @@ -348,20 +429,41 @@ class Player { const msgString = JSON.stringify(message); this.ws.send(msgString); } -}; -let streamers = new Map(); // streamerId <-> streamer socket -let players = new Map(); // playerId <-> player, where player is either a web-browser or a native webrtc player -const SFUPlayerId = "SFU"; -const LegacyStreamerId = "__LEGACY__"; // old streamers that dont know how to ID will be assigned this id. + setSFUStreamerComponent(streamerComponent) { + if (!this.isSFU()) { + console.error(`Trying to add an SFU streamer component ${streamerComponent.id} to player ${this.id} but it is not an SFU type.`); + return; + } + this.sfuStreamerComponent = streamerComponent; + } -function sfuIsConnected() { - const sfuPlayer = players.get(SFUPlayerId); - return sfuPlayer && sfuPlayer.ws && sfuPlayer.ws.readyState == 1; -} + getSFUStreamerComponent() { + if (!this.isSFU()) { + console.error(`Trying to get an SFU streamer component from player ${this.id} but it is not an SFU type.`); + return null; + } + return this.sfuStreamerComponent; + } +}; -function getSFU() { - return players.get(SFUPlayerId); +let streamers = new Map(); // streamerId <-> streamer +let players = new Map(); // playerId <-> player/peer/viewer +const LegacyStreamerPrefix = "__LEGACY_STREAMER__"; // old streamers that dont know how to ID will be assigned this id prefix. +const LegacySFUPrefix = "__LEGACY_SFU__"; // same as streamer version but for SFUs +const streamerIdTimeoutSecs = 5; + +// gets the SFU subscribed to this streamer if any. +function getSFUForStreamer(streamerId) { + if (!streamers.has(streamerId)) { + return null; + } + const streamer = streamers.get(streamerId); + const sfuPlayerId = streamer.getSFUPlayerId(); + if (!sfuPlayerId) { + return null; + } + return players.get(sfuPlayerId); } function logIncoming(sourceName, msg) { @@ -401,30 +503,91 @@ function getPlayerIdFromMessage(msg) { return sanitizePlayerId(msg.playerId); } +let uniqueLegacyStreamerPostfix = 0; +function getUniqueLegacyStreamerId() { + const finalId = LegacyStreamerPrefix + uniqueLegacyStreamerPostfix; + ++uniqueLegacyStreamerPostfix; + return finalId; +} + +let uniqueLegacySFUPostfix = 0; +function getUniqueLegacySFUId() { + const finalId = LegacySFUPrefix + uniqueLegacySFUPostfix; + ++uniqueLegacySFUPostfix; + return finalId; +} + +function requestStreamerId(streamer) { + // first we ask the streamer to id itself. + // if it doesnt reply within a time limit we assume it's an older streamer + // and assign it an id. + + // request id + const msg = { type: "identify" }; + logOutgoing(streamer.id, msg); + streamer.ws.send(JSON.stringify(msg)); + + streamer.idTimer = setTimeout(function() { + // streamer did not respond in time. give it a legacy id. + const newLegacyId = getUniqueLegacyId(); + if (newLegacyId.length == 0) { + const error = `Ran out of legacy ids.`; + console.error(error); + streamer.ws.close(1008, error); + } else { + registerStreamer(newLegacyId, streamer); + } + + }, streamerIdTimeoutSecs * 1000); +} + +function sanitizeStreamerId(id) { + let maxPostfix = -1; + for (let [streamerId, streamer] of streamers) { + const idMatchRegex = /^(.*?)(\d*)$/; + const [, baseId, postfix] = streamerId.match(idMatchRegex); + // if the id is numeric then base id will be empty and we need to compare with the postfix + if ((baseId != '' && baseId != id) || (baseId == '' && postfix != id)) { + continue; + } + const numPostfix = Number(postfix); + if (numPostfix > maxPostfix) { + maxPostfix = numPostfix + } + } + if (maxPostfix >= 0) { + return id + (maxPostfix + 1); + } + return id; +} + function registerStreamer(id, streamer) { - streamer.id = id; - streamers.set(streamer.id, streamer); + // make sure the id is unique + const uniqueId = sanitizeStreamerId(id); + streamer.commitId(uniqueId); + if (!!streamer.idTimer) { + clearTimeout(streamer.idTimer); + delete streamer.idTimer; + } + streamers.set(uniqueId, streamer); + console.logColor(logging.Green, `Registered new streamer: ${streamer.id}`); } function onStreamerDisconnected(streamer) { - if (!streamer.id) { + if (!streamer.id || !streamers.has(streamer.id)) { return; } - if (!streamers.has(streamer.id)) { - console.error(`Disconnecting streamer ${streamer.id} does not exist.`); - } else { - sendStreamerDisconnectedToMatchmaker(); - let sfuPlayer = getSFU(); - if (sfuPlayer) { - const msg = { type: "streamerDisconnected" }; - logOutgoing(sfuPlayer.id, msg); - sfuPlayer.sendTo(msg); - disconnectAllPlayers(sfuPlayer.id); - } - disconnectAllPlayers(streamer.id); - streamers.delete(streamer.id); + sendStreamerDisconnectedToMatchmaker(); + let sfuPlayer = getSFUForStreamer(streamer.id); + if (sfuPlayer) { + const msg = { type: "streamerDisconnected" }; + logOutgoing(sfuPlayer.id, msg); + sfuPlayer.sendTo(msg); + disconnectAllPlayers(sfuPlayer.id); } + disconnectAllPlayers(streamer.id); + streamers.delete(streamer.id); } function onStreamerMessageId(streamer, msg) { @@ -432,15 +595,6 @@ function onStreamerMessageId(streamer, msg) { let streamerId = msg.id; registerStreamer(streamerId, streamer); - - // subscribe any sfu to the latest connected streamer - const sfuPlayer = getSFU(); - if (sfuPlayer) { - sfuPlayer.subscribe(streamer.id); - } - - // if any streamer id's assume the legacy streamer is not needed. - streamers.delete(LegacyStreamerId); } function onStreamerMessagePing(streamer, msg) { @@ -461,7 +615,7 @@ function onStreamerMessageDisconnectPlayer(streamer, msg) { } function onStreamerMessageLayerPreference(streamer, msg) { - let sfuPlayer = getSFU(); + let sfuPlayer = getSFUForStreamer(streamer.id); if (sfuPlayer) { logOutgoing(sfuPlayer.id, msg); sfuPlayer.sendTo(msg); @@ -495,7 +649,8 @@ streamerServer.on('connection', function (ws, req) { console.logColor(logging.Green, `Streamer connected: ${req.connection.remoteAddress}`); sendStreamerConnectedToMatchmaker(); - let streamer = { ws: ws }; + const temporaryId = req.connection.remoteAddress; + let streamer = new Streamer(temporaryId, ws, StreamerType.Regular); ws.on('message', (msgRaw) => { var msg; @@ -535,69 +690,124 @@ streamerServer.on('connection', function (ws, req) { }); ws.send(JSON.stringify(clientConfig)); - - // request id - const msg = { type: "identify" }; - logOutgoing("unknown", msg); - ws.send(JSON.stringify(msg)); - - registerStreamer(LegacyStreamerId, streamer); + requestStreamerId(streamer); }); -function forwardSFUMessageToPlayer(msg) { +function forwardSFUMessageToPlayer(sfuPlayer, msg) { const playerId = getPlayerIdFromMessage(msg); const player = players.get(playerId); if (player) { - logForward(SFUPlayerId, playerId, msg); + logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg); player.sendTo(msg); } } -function forwardSFUMessageToStreamer(msg) { - const sfuPlayer = getSFU(); - if (sfuPlayer) { - logForward(SFUPlayerId, sfuPlayer.streamerId, msg); - msg.sfuId = SFUPlayerId; - sfuPlayer.sendFrom(msg); - } +function forwardSFUMessageToStreamer(sfuPlayer, msg) { + logForward(sfuPlayer.getSFUStreamerComponent().id, sfuPlayer.streamerId, msg); + msg.sfuId = sfuPlayer.id; + sfuPlayer.sendFrom(msg); } -function onPeerDataChannelsSFUMessage(msg) { +function onPeerDataChannelsSFUMessage(sfuPlayer, msg) { // sfu is telling a peer what stream id to use for a data channel const playerId = getPlayerIdFromMessage(msg); const player = players.get(playerId); if (player) { - logForward(SFUPlayerId, playerId, msg); + logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg); player.sendTo(msg); player.datachannel = true; } } -function onSFUDisconnected() { - console.log("disconnecting SFU from streamer"); - disconnectAllPlayers(SFUPlayerId); - const sfuPlayer = getSFU(); - if (sfuPlayer) { - sfuPlayer.unsubscribe(); - sfuPlayer.ws.close(4000, "SFU Disconnected"); +// basically a duplicate of the streamer id request but this one does not register the streamer +function requestSFUStreamerId(sfuPlayer) { + // request id + const msg = { type: "identify" }; + const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent(); + logOutgoing(sfuStreamerComponent.id, msg); + sfuStreamerComponent.ws.send(JSON.stringify(msg)); + + sfuStreamerComponent.idTimer = setTimeout(function() { + // streamer did not respond in time. give it a legacy id. + const newLegacyId = getUniqueSFUId(); + if (newLegacyId.length == 0) { + const error = `Ran out of legacy ids.`; + console.error(error); + sfuPlayer.ws.close(1008, error); + } else { + sfuStreamerComponent.id = newLegacyId; + } + }, streamerIdTimeoutSecs * 1000); +} + +function onSFUMessageId(sfuPlayer, msg) { + const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent(); + logIncoming(sfuStreamerComponent.id, msg); + sfuStreamerComponent.id = msg.id; + + if (!!sfuStreamerComponent.idTimer) { + clearTimeout(sfuStreamerComponent.idTimer); + delete sfuStreamerComponent.idTimer; } - players.delete(SFUPlayerId); - streamers.delete(SFUPlayerId); } +function onSFUMessageStartStreaming(sfuPlayer, msg) { + const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent(); + logIncoming(sfuStreamerComponent.id, msg); + if (streamers.has(sfuStreamerComponent.id)) { + console.error(`SFU ${sfuStreamerComponent.id} is already registered as a streamer and streaming.`) + return; + } + + registerStreamer(sfuStreamerComponent.id, sfuStreamerComponent); +} + +function onSFUMessageStopStreaming(sfuPlayer, msg) { + const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent(); + logIncoming(sfuStreamerComponent.id, msg); +if (!streamers.has(sfuStreamerComponent.id)) { + console.error(`SFU ${sfuStreamerComponent.id} is not registered as a streamer or streaming.`) + return; + } + + onStreamerDisconnected(sfuStreamerComponent); +} + +function onSFUDisconnected(sfuPlayer) { + console.log("disconnecting SFU from streamer"); + disconnectAllPlayers(sfuPlayer.id); + onStreamerDisconnected(sfuPlayer.getSFUStreamerComponent()); + sfuPlayer.unsubscribe(); + sfuPlayer.ws.close(4000, "SFU Disconnected"); + players.delete(sfuPlayer.id); + streamers.delete(sfuPlayer.id); +} + +sfuMessageHandlers.set('listStreamers', onPlayerMessageListStreamers); +sfuMessageHandlers.set('subscribe', onPlayerMessageSubscribe); +sfuMessageHandlers.set('unsubscribe', onPlayerMessageUnsubscribe); sfuMessageHandlers.set('offer', forwardSFUMessageToPlayer); sfuMessageHandlers.set('answer', forwardSFUMessageToStreamer); sfuMessageHandlers.set('streamerDataChannels', forwardSFUMessageToStreamer); sfuMessageHandlers.set('peerDataChannels', onPeerDataChannelsSFUMessage); +sfuMessageHandlers.set('endpointId', onSFUMessageId); +sfuMessageHandlers.set('startStreaming', onSFUMessageStartStreaming); +sfuMessageHandlers.set('stopStreaming', onSFUMessageStopStreaming); console.logColor(logging.Green, `WebSocket listening for SFU connections on :${sfuPort}`); let sfuServer = new WebSocket.Server({ port: sfuPort }); sfuServer.on('connection', function (ws, req) { - // reject if we already have an sfu - if (sfuIsConnected()) { - ws.close(1013, 'Already have an SFU'); - return; - } + + let playerId = sanitizePlayerId(nextPlayerId++); + console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `); + + let streamerComponent = new Streamer(req.connection.remoteAddress, ws, StreamerType.SFU); + let playerComponent = new Player(playerId, ws, PlayerType.SFU, WhoSendsOffer.Streamer); + + streamerComponent.setSFUPlayerComponent(playerComponent); + playerComponent.setSFUStreamerComponent(streamerComponent); + + players.set(playerId, playerComponent); ws.on('message', (msgRaw) => { var msg; @@ -609,26 +819,33 @@ sfuServer.on('connection', function (ws, req) { return; } + let sfuPlayer = players.get(playerId); + if (!sfuPlayer) { + console.error(`Received a message from an SFU not in the player list ${playerId}`); + ws.close(1001, 'Broken'); + return; + } + let handler = sfuMessageHandlers.get(msg.type); if (!handler || (typeof handler != 'function')) { if (config.LogVerbose) { - console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", SFUPlayerId, msgRaw); + console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", sfuPlayer.id, msgRaw); } console.error(`unsupported SFU message type: ${msg.type}`); ws.close(1008, 'Unsupported message type'); return; } - handler(msg); + handler(sfuPlayer, msg); }); ws.on('close', function(code, reason) { console.error(`SFU disconnected: ${code} - ${reason}`); - onSFUDisconnected(); + onSFUDisconnected(playerComponent); }); ws.on('error', function(error) { console.error(`SFU connection error: ${error}`); - onSFUDisconnected(); + onSFUDisconnected(playerComponent); try { ws.close(1006 /* abnormal closure */, error); } catch(err) { @@ -636,18 +853,7 @@ sfuServer.on('connection', function (ws, req) { } }); - let sfuPlayer = new Player(SFUPlayerId, ws, PlayerType.SFU, false); - players.set(SFUPlayerId, sfuPlayer); - console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `); - - // TODO subscribe it to one of any of the streamers for now - for (let [streamerId, streamer] of streamers) { - sfuPlayer.subscribe(streamerId); - break; - } - - // sfu also acts as a streamer - registerStreamer(SFUPlayerId, { ws: ws }); + requestStreamerId(playerComponent.getSFUStreamerComponent()); }); let playerCount = 0; @@ -718,7 +924,7 @@ playerServer.on('connection', function (ws, req) { var url = require('url'); const parsedUrl = url.parse(req.url); const urlParams = new URLSearchParams(parsedUrl.search); - const browserSendOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false'; + const whoSendsOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false' ? WhoSendsOffer.Browser : WhoSendsOffer.Streamer; if (playerCount + 1 > maxPlayerCount && maxPlayerCount !== -1) { @@ -730,7 +936,7 @@ playerServer.on('connection', function (ws, req) { ++playerCount; let playerId = sanitizePlayerId(nextPlayerId++); console.logColor(logging.Green, `player ${playerId} (${req.connection.remoteAddress}) connected`); - let player = new Player(playerId, ws, PlayerType.Regular, browserSendOffer); + let player = new Player(playerId, ws, PlayerType.Regular, whoSendsOffer); players.set(playerId, player); ws.on('message', (msgRaw) =>{ @@ -788,9 +994,9 @@ function disconnectAllPlayers(streamerId) { for (let player of clone.values()) { if (player.streamerId == streamerId) { // disconnect players but just unsubscribe the SFU - if (player.id == SFUPlayerId) { - // because we're working on a clone here we have to access directly - getSFU().unsubscribe(); + const sfuPlayer = getSFUForStreamer(streamerId); + if (sfuPlayer && player.id == sfuPlayer.id) { + sfuPlayer.unsubscribe(); } else { player.ws.close(); } diff --git a/SignallingWebServer/platform_scripts/cmd/refreshenv.cmd b/SignallingWebServer/platform_scripts/cmd/refreshenv.cmd deleted file mode 100644 index e0a272c0..00000000 --- a/SignallingWebServer/platform_scripts/cmd/refreshenv.cmd +++ /dev/null @@ -1,66 +0,0 @@ -:: -:: RefreshEnv.cmd -:: -:: Batch file to read environment variables from registry and -:: set session variables to these values. -:: -:: With this batch file, there should be no need to reload command -:: environment every time you want environment changes to propagate - -::echo "RefreshEnv.cmd only works from cmd.exe, please install the Chocolatey Profile to take advantage of refreshenv from PowerShell" -echo | set /p dummy="Refreshing environment variables from registry for cmd.exe. Please wait..." - -goto main - -:: Set one environment variable from registry key -:SetFromReg - "%WinDir%\System32\Reg" QUERY "%~1" /v "%~2" > "%TEMP%\_envset.tmp" 2>NUL - for /f "usebackq skip=2 tokens=2,*" %%A IN ("%TEMP%\_envset.tmp") do ( - echo/set "%~3=%%B" - ) - goto :EOF - -:: Get a list of environment variables from registry -:GetRegEnv - "%WinDir%\System32\Reg" QUERY "%~1" > "%TEMP%\_envget.tmp" - for /f "usebackq skip=2" %%A IN ("%TEMP%\_envget.tmp") do ( - if /I not "%%~A"=="Path" ( - call :SetFromReg "%~1" "%%~A" "%%~A" - ) - ) - goto :EOF - -:main - echo/@echo off >"%TEMP%\_env.cmd" - - :: Slowly generating final file - call :GetRegEnv "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" >> "%TEMP%\_env.cmd" - call :GetRegEnv "HKCU\Environment">>"%TEMP%\_env.cmd" >> "%TEMP%\_env.cmd" - - :: Special handling for PATH - mix both User and System - call :SetFromReg "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" Path Path_HKLM >> "%TEMP%\_env.cmd" - call :SetFromReg "HKCU\Environment" Path Path_HKCU >> "%TEMP%\_env.cmd" - - :: Caution: do not insert space-chars before >> redirection sign - echo/set "Path=%%Path_HKLM%%;%%Path_HKCU%%" >> "%TEMP%\_env.cmd" - - :: Cleanup - del /f /q "%TEMP%\_envset.tmp" 2>nul - del /f /q "%TEMP%\_envget.tmp" 2>nul - - :: capture user / architecture - SET "OriginalUserName=%USERNAME%" - SET "OriginalArchitecture=%PROCESSOR_ARCHITECTURE%" - - :: Set these variables - call "%TEMP%\_env.cmd" - - :: Cleanup - del /f /q "%TEMP%\_env.cmd" 2>nul - - :: reset user / architecture - SET "USERNAME=%OriginalUserName%" - SET "PROCESSOR_ARCHITECTURE=%OriginalArchitecture%" - - echo | set /p dummy="Finished." - echo ... \ No newline at end of file diff --git a/SignallingWebServer/platform_scripts/cmd/setenv/License.txt b/SignallingWebServer/platform_scripts/cmd/setenv/License.txt deleted file mode 100644 index ff66d6bc..00000000 --- a/SignallingWebServer/platform_scripts/cmd/setenv/License.txt +++ /dev/null @@ -1,24 +0,0 @@ -License -------- - -Copyright (C) 1999-2008 - Jonathan Wilkes -http://www.xanya.net - -Installing and using this software (or source code) signifies acceptance of these terms and the conditions of the license. -This license applies to everything in this package (Including any supplied Source Code), except where otherwise noted. - -License Agreement ------------------ - -This software is provided 'as-is', without any express or implied warranty. -In no event will the author be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software/source code. -(If you use the supplied source code (if any) in a product, then an acknowledgment in the product documentation would be appreciated but is not required.) - -2. If you have downloaded the Source Code for this application (where available) then altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any distribution of the software. -(If you use the supplied source code (if any) in a product, including commercial applications, then you do NOT need to distribute this license with your product.) diff --git a/SignallingWebServer/platform_scripts/cmd/setenv/ReadMe.txt b/SignallingWebServer/platform_scripts/cmd/setenv/ReadMe.txt deleted file mode 100644 index dc193079..00000000 --- a/SignallingWebServer/platform_scripts/cmd/setenv/ReadMe.txt +++ /dev/null @@ -1,46 +0,0 @@ - -SetEnv -Version 1.09 - ( For Windows 9x/NT/2000/XP/S2K3/Vista ) - -Copyright (C) 2005-2008 - Jonathan Wilkes - All Rights Reserved. -http://www.xanya.net - -================================================================================ - -1. Installation - - Simply download and run the Setup_SetEnv.exe application to install SetEnv. - -2. Using SetEnv - - The SetEnv is a free tool for setting/updating/deleting System Environment Variables. - Type the following at a command prompt (assumes SetEnv.exe is in current path), for command line usage information. - - setenv -? - - See our website for full usage details, http://www.xanya.net/site/utils/setenv.php - -3. Version History - - 1.09 [Fix] - (Feb 9, 2008) - Fixed a problem on Windows 98 where it sometimes failed to open the Autoexec.bat file. - 1.08 [New] - (May 31, 2007) - Added how to delete a USER environment variable to the usage information. - 1.07 [Fix] - (Jan 25, 2007) - Fixed a bug found by depaolim. - 1.06 [New] - (Jan 14, 2007) - Added dynamic expansion support (same as using ~ with setx) - - Originally requested by Andre Amaral, further Request by Synetech - 1.05 [New] - (Sep 06, 2006) - Added support to prepend (rather than append) a value to an expanded string - - Requested by Masuia - 1.04 [New] - (May 30, 2006) - Added support for User environment variables. - 1.03 [Fix] - (Apr 20, 2006) - Bug fix in ProcessWinXP() discovered by attiasr - 1.01 [Fix] - (Nov 15, 2005) - Bug fix in IsWinME() discovered by frankd - 1.00 [New] - (Oct 29, 2005) - Initial Public Release. - -4. License and Terms of Use - - Please see the License.txt file for licensing information. - -5. Reporting Problems - - If you encounter any problems whilst using SetEnv, please try downloading the latest version from http://www.xanya.net to see if the problem has already been resolved. - If this does not help, then please send an e-mail to darka@xanya.net with details describing the problem. - -================================================================================ \ No newline at end of file diff --git a/SignallingWebServer/platform_scripts/cmd/setenv/SetEnv.exe b/SignallingWebServer/platform_scripts/cmd/setenv/SetEnv.exe deleted file mode 100644 index b1d5d555..00000000 Binary files a/SignallingWebServer/platform_scripts/cmd/setenv/SetEnv.exe and /dev/null differ diff --git a/SignallingWebServer/platform_scripts/cmd/setup_frontend.bat b/SignallingWebServer/platform_scripts/cmd/setup_frontend.bat index c8767f76..4108befd 100644 --- a/SignallingWebServer/platform_scripts/cmd/setup_frontend.bat +++ b/SignallingWebServer/platform_scripts/cmd/setup_frontend.bat @@ -43,10 +43,10 @@ @Rem Save our current directory (the NodeJS dir) in a variable set "NodeDir=%CD%\SignallingWebServer\platform_scripts\cmd\node" - @Rem Prepend NodeDir to PATH temporarily using a custom tool called SetEnv - call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -uap PATH %%%%"%NodeDir%" - @Rem Refresh the cmd session with new PATH - call %~dp0\refreshenv.cmd + @rem Save the old path variable + set OLDPATH=%PATH% + @Rem Prepend NodeDir to PATH temporarily + set PATH=%PATH%;%NodeDir% @Rem Do npm install in the Frontend\lib directory (note we use start because that loads PATH) echo ---------------------------- @@ -73,7 +73,7 @@ echo End of build reference frontend step. echo ---------------------------- - @Rem Remove our NodeJS from the PATH - call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -ud PATH %%%%"%NodeDir%" + @Rem Restore path + set PATH=%OLDPATH% goto :eof \ No newline at end of file