UNPKG

@pureweb/platform-streaming-agent

Version:

The PureWeb platform streaming agent enables your game to communicate and stream through the PureWeb Platform

297 lines (296 loc) 15 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SignallingService = void 0; const platform_sdk_1 = require("@pureweb/platform-sdk"); const Log_1 = __importDefault(require("../../Log")); const ws_1 = __importDefault(require("ws")); const bimap_1 = require("./bimap"); const ConnectionStates_1 = require("../../ConnectionStates"); class SignallingService { constructor(agent, config, platform) { this.ADDRESS_IN_USE = 'EADDRINUSE'; this.iceCandidateMessageEvents = new Map(); /** * Notify through agent contributions that stream is available */ this.notifyRemoteAgentsStreamConnected = async () => { if (this.contribution) { const contributions = await this.agent.getContributions(platform_sdk_1.SignallingTopics.WEBRTC_UNREAL_STREAM, this.agent.id); for (let i = 0; i < contributions.length; i++) { if (contributions[i] && contributions[i].id === this.contribution.id) { Log_1.default.debug('Duplicate contribution is not allowed'); return; } } } const contribution = await this.agent .makeContribution({ type: platform_sdk_1.SignallingTopics.WEBRTC_UNREAL_STREAM, data: JSON.stringify({ launchRequestId: this.agent.serviceCredentials.launchRequestId, modelId: this.agent.serviceCredentials.modelId, ueVersion: this.config.ueVersion }) }) .catch((err) => { Log_1.default.error('Failed to make contribution: ' + err); this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.INTERNAL_ERROR); }); this.contribution = contribution; }; /** * Notifies through agent contributions that stream is not available */ this.notifyRemoteAgentsStreamDisconnected = async () => { if (this.contribution) { await this.agent.removeContribution(this.contribution).catch((err) => { Log_1.default.error('Failed to remove contribution', err); }); this.contribution = undefined; } }; /** * * @param streamerConnection WebSocket to unreal pixelStreaming */ this.connect = async (streamerConnection) => { if (this.streamerConnection) { return; } this.streamerConnection = streamerConnection; Log_1.default.info('Streamer connected to streaming agent'); this.streamerConnection.onclose = (ev) => { Log_1.default.info(`<- Streamer connection closed: ${ev.code} - ${ev.reason}`); this.agentPlayerMap = new bimap_1.Bimap(); }; this.streamerConnection.onmessage = (ev) => { Log_1.default.debug(`<- Streamer: ${ev.data}`); try { const msg = JSON.parse(ev.data.toString()); // As of unreal 4.27 they have changed this datatype to strings and force the clients to handle both. // parseInt returns parseInt(arg.ToString()) when not a string, so works in both cases. const playerId = parseInt(msg.playerId); delete msg.playerId; const agentId = this.agentPlayerMap.keyForValue(playerId); msg.topic = platform_sdk_1.SignallingTopics.WEBRTC_UNREAL_STREAM; if (msg.type === platform_sdk_1.SignallingTypes.answer || msg.type === platform_sdk_1.SignallingTypes.iceCandidate) { Log_1.default.debug('Sending message to: ' + agentId); this.agent.sendMessage(platform_sdk_1.SignallingTopics.WEBRTC_UNREAL_STREAM, JSON.stringify({ webrtc_type: msg.type, message: msg }), new platform_sdk_1.RemoteAgent(agentId)); } else if (msg.type == platform_sdk_1.SignallingTypes.disconnectPlayer) { Log_1.default.debug('Player disconnected: ' + playerId); } else { Log_1.default.debug(`Unsupported message type received from streaming websocket`); } } catch (err) { Log_1.default.error('Error parsing streamer message: ' + err); } }; // Provide ice server details to the streamer const iceServers = this.agent.serviceCredentials.iceServers; function getIceServers(initialIceServers) { const iceServers = initialIceServers.map(function callback(currentValue) { // Return element for new_array const newValue = JSON.parse(JSON.stringify(currentValue)); newValue.urls = [newValue.url]; delete newValue.url; return newValue; }); return iceServers; } const streamerConfig = { type: 'config', peerConnectionOptions: { iceServers: getIceServers(iceServers) } }; this.streamerConnection.send(JSON.stringify(streamerConfig)); try { if (this.platform?.credentials?.agent?.launchRequestId?.length > 0) { if (this.config.standbyMode === false) { await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StreamingAgentReady, 'Stream is ready to be consumed'); } else { await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StandbyStreamingAgentReady, 'Standby stream is ready to be consumed'); } } else { Log_1.default.info('No associated launch request. Assuming local development workflow'); } } catch (err) { Log_1.default.error('Unable to acknowledge streaming agent ready state: ' + err); } //Notify that unreal stream is available await this.notifyRemoteAgentsStreamConnected(); }; /** * Starts Signalling service. * Listens for pixel streaming connection event on streamer websocket server * Subscribes to agent platform messages that are used as a Signalling service */ this.start = async (presenceMonitor) => { this._presenceMonitor = presenceMonitor; if (this.config.streamerTimeoutSeconds >= 0) { setTimeout(() => { if (!this.streamerConnection) { if (this.config.standbyMode) { Log_1.default.info('Standby timeout reached. Streaming agent shutting down'); this.connectionStateChange(ConnectionStates_1.ConnectionStates.STANDBY_TIMEOUT); } else { Log_1.default.info(`Streamer Rendezvous missed.`); this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.STREAMER_RENDEZVOUS_MISSED); } } else { Log_1.default.info(`Streamer Rendezvous successful.`); } }, this.config.streamerTimeoutSeconds * 1000); } // eslint-disable-next-line @typescript-eslint/no-explicit-any this.streamerServer.on('error', (error) => { Log_1.default.error(`Streamer websocket error ${error}`); if (error && error.message.includes(this.ADDRESS_IN_USE)) { Log_1.default.error(`Specified streamer port: ${error.port} is already in use.`); this.connectionStateChange(ConnectionStates_1.ConnectionStates.STREAMER_PORT_UNAVAILABLE); } }); this.streamerServer.on('connection', async (ws, req) => { if (this.streamerConnection) { Log_1.default.error('Streamer already connected'); ws.close(1013 /* Try again later */, 'Streamer already connected'); this.connectionStateChange(ConnectionStates_1.ConnectionStates.STREAMER_DUPLICATE_CONNECTION); return; } Log_1.default.info(`Streamer connected: ${req.socket.remoteAddress}`); await this.connect(ws); ws.on('close', async (code, reason) => { Log_1.default.info(`streamer disconnected: ${code} - ${reason}`); await this.notifyRemoteAgentsStreamDisconnected(); this.streamerConnection = null; this.connectionStateChange(ConnectionStates_1.ConnectionStates.STREAMER_DISCONNECTED); }); ws.on('error', async (error) => { Log_1.default.error(`streamer connection error: ${error}`); ws.close(1006 /* abnormal closure */, error); await this.notifyRemoteAgentsStreamDisconnected(); this.streamerConnection = null; this.connectionStateChange(ConnectionStates_1.ConnectionStates.STREAMER_CONNECTION_ERROR); }); }); this.agent.subscribeToMessages(platform_sdk_1.MessageTypes.PRIVATE, { privateMessage: (data) => { this.processMessage(data); } }); }; this.agentPlayerMap = new bimap_1.Bimap(); this.playerId = 100; this.agent = agent; this.config = config; this.platform = platform; this.streamerServer = new ws_1.default.Server({ port: config.streamerPort, backlog: 1 }); } onStateChanged(handler) { this.connectionStateChange = handler; } reconnect() { Log_1.default.info('Reconnecting streamer to agent platform'); if (this.contribution) { this.notifyRemoteAgentsStreamConnected(); } } stop() { Log_1.default.info('stop'); this.streamerServer.close(); } async processMessage(data) { if (data.type !== platform_sdk_1.SignallingTopics.WEBRTC_UNREAL_STREAM) { return; } try { const msg = JSON.parse(data.payload); //Listen to offers and iceCandidates from agents if (msg.webrtc_type !== platform_sdk_1.SignallingTypes.offer && msg.webrtc_type !== platform_sdk_1.SignallingTypes.iceCandidate && msg.webrtc_type !== platform_sdk_1.SignallingTypes.webRTCConnectionFailure) { return; } if (msg.webrtc_type === platform_sdk_1.SignallingTypes.webRTCConnectionFailure) { Log_1.default.error(`WebRTC connection failure ${JSON.stringify(msg.payload)}`); if (this.platform?.credentials?.agent?.launchRequestId?.length > 0) { await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.WebRTCConnectionFailed, msg.payload); } await this.notifyRemoteAgentsStreamDisconnected(); this.streamerConnection = null; this.connectionStateChange(ConnectionStates_1.ConnectionStates.STREAMER_CONNECTION_ERROR); return; } //If offer send it means that the new agent wants to accept unreal stream if (msg.webrtc_type === platform_sdk_1.SignallingTypes.offer) { Log_1.default.info('Adding offer sender: ' + data.senderId); this.addRemoteAgentToMap(data.senderId); this._presenceMonitor.monitor(data.senderId, true); // globally replace sendrecv with recvonly //msg.payload.sdp = msg.payload.sdp.replace(/a=sendrecv/g, 'a=recvonly'); // Note: the above munge messes with browser sent audio, leave this up to the client // so that the `useMic` feature can work as intended. } const payload = msg.payload; //Get playerId for specific agent so pixel streaming will find proper socket connection on unreal side const playerId = this.agentPlayerMap.valueForKey(data.senderId); if (!playerId) { let events = this.iceCandidateMessageEvents.get(data.senderId); if (events === undefined) { events = []; } events.push(data); this.iceCandidateMessageEvents.set(data.senderId, events); return; } payload.playerId = playerId; Log_1.default.info(JSON.stringify(payload)); if (payload.webrtc_type === platform_sdk_1.SignallingTypes.offer) { this.streamerConnection.send(JSON.stringify(payload)); const events = this.iceCandidateMessageEvents.get(data.senderId); //Process cached iceCandidateEvents events?.forEach((iceCandidateEvent) => { this.streamerConnection.send(JSON.stringify(iceCandidateEvent)); }); this.iceCandidateMessageEvents.delete(data.senderId); } else { this.streamerConnection.send(JSON.stringify(payload)); } if (this.config.standbyMode === true) { await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StandbyAgentArrived, 'Streamer is ready to be consumed'); } } catch (e) { Log_1.default.error(e); } } /** * Add remote agent to the map so we can match agents ids with playerIds that are passed to the unreal game pixelStreamer connection * Note: pixelStreamer expects number hence the map since our agent ids are strings * @param agentId */ addRemoteAgentToMap(agentId) { if (!this.agentPlayerMap.hasKey(agentId)) { this.agentPlayerMap.set(agentId, this.playerId++); } } } exports.SignallingService = SignallingService;