@pureweb/platform-streaming-agent
Version:
The PureWeb platform streaming agent enables your game to communicate and stream through the PureWeb Platform
268 lines (267 loc) • 13.8 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PresenceMonitorExtension = void 0;
const Log_1 = __importDefault(require("../../Log"));
const IExtension_1 = require("../../IExtension");
const ConnectionStates_1 = require("../../ConnectionStates");
const platform_sdk_1 = require("@pureweb/platform-sdk");
class PresenceMonitorExtension extends IExtension_1.AbstractExtension {
constructor() {
super(...arguments);
this._disconnectedParticipants = new Set();
this._participants = new Set();
this.participantJoined = false;
this.claimed = false;
this.checkingParticipants = false;
this._bOfferSent = false;
this.start = async () => {
this.agent.subscribeToMessages(platform_sdk_1.MessageTypes.BROADCAST, {
broadcastMessage: (data) => {
const msg = data?.message;
let action;
let agentId;
Log_1.default.info('Received broadcast message: ' + msg);
if (msg === undefined) {
return;
}
try {
const message = JSON.parse(msg.replace(/\\"/g, '"'));
action = message.action;
agentId = message.agentId;
}
catch (e) {
return;
}
if (action === 'TERMINATE') {
this.checkForTermination(action);
}
else if (action === 'CLAIMED') {
this._claimingAgent = agentId;
this.claimed = true;
this.participantJoined = true;
this.checkForTermination('START_CLAIMED_TIMEOUT');
}
}
});
return this.monitorPresence()
.then(() => {
return true;
})
.catch(() => {
return false;
});
};
this.stop = () => {
this.participantJoined = false;
this.agent.unsubscribeFromPresence(platform_sdk_1.PresenceTypes.ALL);
clearInterval(this.presenceCheckInterval);
return Promise.resolve(true);
};
this.monitorPresence = async () => {
//We going to check presence of the agents on 30 seconds intervals in case
//we do not receive event on the agent departure
this.presenceCheckInterval = setInterval(async () => {
try {
if (!this.agent || this.checkingParticipants) {
return;
}
this.checkingParticipants = true;
const agents = await this.agent.getRemoteAgents();
this._participants.forEach((participant) => {
if (agents.indexOf(participant) < 0) {
//We show error here because we should have receive departure event before
Log_1.default.error(`Failed to receive participant ${participant} departure on presence subscription`);
Log_1.default.info(`Removing ${participant} from presence monitor participant list`);
this.removeParticipant(participant);
}
});
//Check if we need to terminate if there are no active streaming agents
this.checkForTermination();
this.checkingParticipants = false;
}
catch (e) {
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.PRESENCE_FAILURE);
}
}, 30000);
// subscribe to departures only. Client of this extension will add agents that
// we want to monitor.
this.agent.subscribeToPresence(platform_sdk_1.PresenceTypes.ALL, {
departures: (data) => {
if (this.agent.id != data.agentId) {
if (this._participants.has(data.agentId)) {
Log_1.default.info(`Removing ${data.agentId} from presence monitor participant list`);
this.removeParticipant(data.agentId);
}
if (this._claimingAgent === data.agentId) {
Log_1.default.info(`Claiming agent ${data.agentId} has departed.`);
this._claimingAgent = undefined;
}
Log_1.default.info(`${Array.from(this._participants).join(', ')} remote agents.`);
this.checkForTermination();
}
},
arrivals: (data) => {
//This can be any agent joining the environment.
//Log just for debugging/tracking purposes
// logger.info(`Agent arrived: ${data.agentId} EnvironmentId: ${this.agent.agentEnvironment.id}`);
//Check if the agent that previously was streaming is reconnecting.
//Put the agent back as a participant if it is reconected agent.
//All other agents have to send webRTC offer in order to become a participant that we monitor
if (this._disconnectedParticipants.has(data.agentId)) {
Log_1.default.info(`Reconnected agent ${data.agentId} is back in the participant list.`);
this._participants.add(data.agentId);
if (this._claimingAgent === data.agentId) {
this._claimingAgent = undefined;
}
}
}
});
try {
if (this.config.rendezvousTimeoutSeconds >= 0) {
setTimeout(async () => {
if (!this.participantJoined) {
if (this.config.standbyMode === true) {
Log_1.default.info(`Standby timeout reached. Streaming agent shutting down`);
await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StandbyAgentTimeout, 'Standby agent timed out');
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.STANDBY_TIMEOUT);
}
else {
if (this.config.streamReady) {
Log_1.default.info('Streaming Agent resetting');
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.TERMINATED);
}
else {
Log_1.default.info(`Agent Rendezvous missed.`);
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.AGENT_RENDEZVOUS_MISSED);
}
}
}
else {
Log_1.default.info(`Agent Rendezvous successful.`);
}
}, this.config.rendezvousTimeoutSeconds * 1000);
}
}
catch (err) {
Log_1.default.error(`Could not obtain participant list from Presence service: ${err}`);
this.stop();
}
};
}
async monitor(agentId, bOffer) {
const agents = await this.agent.getRemoteAgents();
//Make sure that the agent the we want to monitor for departure
//is still in the environment
if (agents.indexOf(agentId) < 0) {
Log_1.default.info('Requested agent ' + agentId + ' is not present in the environment');
return;
}
if (this._participants.has(agentId)) {
Log_1.default.error(`Multiple requests to monitor the same agent id: ${agentId}. Participants ${Array.from(this._participants).join(', ')}, EnvironmentId: ${this.agent.agentEnvironment.id}`);
return;
}
this._participants.add(agentId);
this.participantJoined = true;
this._bOfferSent = bOffer;
}
/**
* Removes participant and saves it as a disconnected participant
* so if the agent reconnects it will not require to send the offer
* in order to keep the SA going.
* @param agentId agent id
*/
removeParticipant(agentId) {
this._disconnectedParticipants.add(agentId);
this._participants.delete(agentId);
}
get participants() {
return this._participants.size;
}
onStateChanged(handler) {
this.connectionStateChange = handler;
}
/**
* Checks if we need to terminate if there are no active streaming agents
*/
checkForTermination(action) {
if (action === 'START_CLAIMED_TIMEOUT') {
setTimeout(async () => {
if (this.participantJoined && this._participants.size > 0 && this._bOfferSent === true) {
Log_1.default.info('Claimed timeout aborted. Agent joined the session before claimed timeout.');
return;
}
Log_1.default.info('Claimed timeout expired. Setting connection state to claimed timeout.');
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.CLAIMED_TIMOUT);
}, this.config.claimedTimeoutSeconds * 1000);
return;
}
if (action === 'TERMINATE' && this.config.streamReady) {
if (this.participantJoined && this._participants.size > 0) {
Log_1.default.info('Termination delayed. Session in progress.');
return;
}
setTimeout(async () => {
if (this.participantJoined && this._participants.size > 0) {
Log_1.default.info('Termination delayed. Session in progress.');
return;
}
Log_1.default.info('Terminating stream ready Streaming Agent');
try {
await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StreamingAgentFinished, 'Stream ready Streaming Agent terminated');
}
catch (err) {
Log_1.default.error('Unable to acknowledge streaming agent finish state: ' + err);
}
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.TERMINATED);
}, 2000);
}
// In Stream Ready we do not terminate if participant has not joined
if (this.config.streamReady === true && this.participantJoined === false && this._bOfferSent === false) {
Log_1.default.info('Waiting for participant to join before terminating.');
return;
}
if (this.participantJoined && this._participants.size == 0) {
Log_1.default.info('No active remote agents. Checking if we need to terminate.');
if (this.claimed && this._claimingAgent && this._bOfferSent === false) {
Log_1.default.info('The agent claimed the session but has not sent the offer. Keep waiting...');
return;
}
//If claimed we do not linger. We terminate immediately.
if (this.claimed) {
this.config.lingerTimeoutSeconds = 0;
}
// Negative values disables this completely.
if (this.config.lingerTimeoutSeconds >= 0) {
Log_1.default.info(`No active remote agents. Waiting ${this.config.lingerTimeoutSeconds} seconds to shutdown.`);
setTimeout(async () => {
//Abort termination if another agent join before lingering timeout
if (this._participants.size > 0) {
Log_1.default.info('Termination aborted. Another agent joined the session before lingering timeout.');
Log_1.default.info('Streaming agent will stay alive');
return;
}
Log_1.default.info('Lingering timeout expired. Setting connection state to all agents departed.');
try {
if (this.platform?.credentials?.agent?.launchRequestId?.length > 0) {
await this.platform.launchRequestAcknowledgements(platform_sdk_1.LaunchRequestAcknowledgementType.StreamingAgentFinished, 'All peers have finished consuming stream');
}
else {
Log_1.default.info('No associated launch request. Assuming local development workflow');
}
}
catch (err) {
Log_1.default.error('Unable to acknowledge streaming agent finish state: ' + err);
}
this.connectionStateChange?.(ConnectionStates_1.ConnectionStates.PEERS_EXITED);
}, this.config.lingerTimeoutSeconds * 1000);
}
else {
Log_1.default.info('Linger timeout disabled. Streaming agent will stay active.');
}
}
}
}
exports.PresenceMonitorExtension = PresenceMonitorExtension;
;