diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts index cd8507fc..b670687c 100644 --- a/examples/typescript/src/index.ts +++ b/examples/typescript/src/index.ts @@ -1,4 +1,11 @@ -import { Config, PixelStreaming, SPSApplication, TextParameters, PixelStreamingApplicationStyle, MessageRecv, Flags } from "@tensorworks/libspsfrontend"; +import { + Config, + Flags, + MessageRecv, + PixelStreaming, + PixelStreamingApplicationStyle, + SPSApplication +} from "@tensorworks/libspsfrontend"; // Apply default styling from Epic Games Pixel Streaming Frontend export const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); @@ -20,6 +27,7 @@ class ScalablePixelStreaming extends PixelStreaming { } }; +// Initialise the SPS frontend on load of the body element document.body.onload = function () { // Create a config object. We default to sending the WebRTC offer from the browser as true, TimeoutIfIdle to true, AutoConnect to false and MaxReconnectAttempts to 0 @@ -34,10 +42,12 @@ document.body.onload = function () { stream.handleOnConfig(messageExtendedConfig); } - // Create and append our application + // Create the SPS application const spsApplication = new SPSApplication({ stream, onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode) /* Light/Dark mode support. */ }); + + // Append the SPS application element to the document body element document.body.appendChild(spsApplication.rootElement); } \ No newline at end of file diff --git a/library/src/LoadingOverlay.ts b/library/src/LoadingOverlay.ts index 86067e31..5a3c0cae 100644 --- a/library/src/LoadingOverlay.ts +++ b/library/src/LoadingOverlay.ts @@ -4,7 +4,7 @@ export class LoadingOverlay extends TextOverlay { private static _rootElement: HTMLElement; private static _textElement: HTMLElement; - private static _spinner: HTMLElement; + private static _spinnerElement: HTMLElement; /** * @returns The created root element of this overlay. @@ -31,27 +31,27 @@ export class LoadingOverlay extends TextOverlay { public static spinner(): HTMLElement { - if (!LoadingOverlay._spinner) { + if (!LoadingOverlay._spinnerElement) { // build the spinner div const size = LoadingOverlay._rootElement.clientWidth * 0.03; - LoadingOverlay._spinner = document.createElement('div'); - LoadingOverlay._spinner.id = "loading-spinner" - LoadingOverlay._spinner.className = "loading-spinner"; - LoadingOverlay._spinner.setAttribute("style", "width: " + size + "px; height: " + size + "px;"); + LoadingOverlay._spinnerElement = document.createElement('div'); + LoadingOverlay._spinnerElement.id = "loading-spinner" + LoadingOverlay._spinnerElement.className = "loading-spinner"; + LoadingOverlay._spinnerElement.setAttribute("style", "width: " + size + "px; height: " + size + "px;"); const SpinnerSectionOne = document.createElement("div"); SpinnerSectionOne.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); - LoadingOverlay._spinner.appendChild(SpinnerSectionOne); + LoadingOverlay._spinnerElement.appendChild(SpinnerSectionOne); const SpinnerSectionTwo = document.createElement("div"); SpinnerSectionTwo.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); - LoadingOverlay._spinner.appendChild(SpinnerSectionTwo); + LoadingOverlay._spinnerElement.appendChild(SpinnerSectionTwo); const SpinnerSectionThree = document.createElement("div"); SpinnerSectionThree.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); - LoadingOverlay._spinner.appendChild(SpinnerSectionThree); + LoadingOverlay._spinnerElement.appendChild(SpinnerSectionThree); } - return LoadingOverlay._spinner; + return LoadingOverlay._spinnerElement; } /** * Construct a connect overlay with a connection button. diff --git a/library/src/Messages.ts b/library/src/Messages.ts index 8430cb62..d496c36a 100644 --- a/library/src/Messages.ts +++ b/library/src/Messages.ts @@ -14,7 +14,7 @@ export enum MessageSendTypes { } /** - * Aggregated Stats Message Wrapper + * Aggregated Stats Message Wrapper to send stats to the signalling server */ export class MessageStats extends MessageSend { inboundVideoStats: InboundVideoStats; @@ -30,7 +30,11 @@ export class MessageStats extends MessageSend { */ constructor(aggregatedStats: AggregatedStats) { super(); + + // Set the message type as stats this.type = MessageSendTypes.STATS + + // Map the aggregated stats to the message stats properties this.inboundVideoStats = aggregatedStats.inboundVideoStats; this.inboundAudioStats = aggregatedStats.inboundAudioStats; this.candidatePair = aggregatedStats.getActiveCandidatePair(); diff --git a/library/src/SPSApplication.ts b/library/src/SPSApplication.ts index 880e96c9..2b0da763 100644 --- a/library/src/SPSApplication.ts +++ b/library/src/SPSApplication.ts @@ -1,32 +1,45 @@ -import { Application, SettingUIFlag, UIOptions } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -import { AggregatedStats, SettingFlag, TextParameters } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; import { LoadingOverlay } from './LoadingOverlay'; import { SPSSignalling } from './SignallingExtension'; import { MessageStats } from './Messages'; +import { + AggregatedStats, + SettingFlag, + TextParameters +} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; +import { + Application, + SettingUIFlag, + UIOptions +} from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; // For local testing. Declare a websocket URL that can be imported via a .env file that will override // the signalling server URL builder. declare var WEBSOCKET_URL: string; - export class SPSApplication extends Application { private loadingOverlay: LoadingOverlay; private signallingExtension: SPSSignalling; + // Create a flags class for the send stats to server static Flags = class { static sendToServer = "sendStatsToServer" } constructor(config: UIOptions) { super(config); + + // Initialise the signaling server extensions to support sps signalling messages this.signallingExtension = new SPSSignalling(this.stream.webSocketController); this.signallingExtension.onAuthenticationResponse = this.handleSignallingResponse.bind(this); this.signallingExtension.onInstanceStateChanged = this.handleSignallingResponse.bind(this); + // Enforce the munging of the websocket address to support SPS this.enforceSpecialSignallingServerUrl(); - // Add 'Send Stats to Server' checkbox + // Add a SPS settings section to the settings panel const spsSettingsSection = this.configUI.buildSectionWithHeading(this.settingsPanel.settingsContentElement, "Scalable Pixel Streaming"); + + // Add the 'Send Stats to server' check box to the list of settings const sendStatsToServerSetting = new SettingFlag( SPSApplication.Flags.sendToServer, "Send stats to server", @@ -35,9 +48,13 @@ export class SPSApplication extends Application { this.stream.config.useUrlParams ); + // Add the 'Send Stats to server' check box to the sps settings section spsSettingsSection.appendChild(new SettingUIFlag(sendStatsToServerSetting).rootElement); + + // Initialise the loading overlay this.loadingOverlay = new LoadingOverlay(this.stream.videoElementParent); + // Add an event handler for when the statReceive event is emitted this.stream.addEventListener( 'statsReceived', ({ data: { aggregatedStats } }) => { @@ -48,38 +65,68 @@ export class SPSApplication extends Application { ); } + /** + * Handled the response when the sps messages are emitted + * @param signallingResp the informative response message + * @param isError if the message is an error + */ handleSignallingResponse(signallingResp: string, isError: boolean) { + + // Check if the message is an error if (isError) { + + // Show the error overlay this.showErrorOverlay(signallingResp); } else { + + // Show the loading overlay this.showLoadingOverlay(signallingResp); } } + /** + * Enforces changes the websocket path to conform to the SPS specification + * Can be overridden if the WEBSOCKET_URL var defined in the .env file has been defined + */ enforceSpecialSignallingServerUrl() { + // SPS needs to build a specific signalling server url based on the application name so K8s can distinguish it this.stream.setSignallingUrlBuilder(() => { - // if we have overriden the signalling server URL with a .env file use it here - if (WEBSOCKET_URL !== undefined ) { + // Check if the WEBSOCKET_URL var in the .env file has been defined + if (WEBSOCKET_URL !== undefined) { + + // Return the value of the WEBSOCKET_URL var defined in the .env file return WEBSOCKET_URL as string; } - // get the current signalling url + // Get the current signalling server URL from the settings let signallingUrl = this.stream.config.getTextSettingValue(TextParameters.SignallingServerUrl); - // build the signalling URL based on the existing window location, the result should be 'domain.com/signalling/app-name' + // Build the signalling URL based on the existing window location, the result should be 'domain.com/signalling/app-name' signallingUrl = signallingUrl.endsWith("/") ? signallingUrl + "signalling" + window.location.pathname : signallingUrl + "/signalling" + window.location.pathname; + // Return the modified signalling server URL return signallingUrl }); } - showLoadingOverlay(signallingResp: string) { + /** + * Shows the loading overlay + * @param message The message to display + */ + showLoadingOverlay(message: string) { + + // Hide the current overlay this.hideCurrentOverlay(); + + // Show the loading overlay this.loadingOverlay.show(); - this.loadingOverlay.update(signallingResp); + // Update the loading overlay with the signalling response + this.loadingOverlay.update(message); + + // Set the current overlay to the loading overlay this.currentOverlay = this.loadingOverlay; } @@ -88,7 +135,11 @@ export class SPSApplication extends Application { * @param stats - Aggregated Stats */ sendStatsToSignallingServer(stats: AggregatedStats) { + + // Create a new stats signalling message const data = new MessageStats(stats); + + // Send the stats message to the signalling server this.stream.webSocketController.webSocket.send(data.payload()); } } diff --git a/library/src/SignallingExtension.ts b/library/src/SignallingExtension.ts index 6071c5b3..da2987be 100644 --- a/library/src/SignallingExtension.ts +++ b/library/src/SignallingExtension.ts @@ -6,14 +6,14 @@ import { } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; /** - * Auth Request Message Wrapper + * Authentication request message wrapper */ export class MessageAuthRequest extends MessageSend { token: string; provider: string; /** - * @param token - Token Provided by the Auth Provider + * @param token - Token provided by the authentication provider * @param provider - Name of the provider that is registered in the auth plugin */ constructor(token: string, provider: string) { @@ -25,17 +25,33 @@ export class MessageAuthRequest extends MessageSend { } /** - * States of the UE Instance + * States of the UE instance request */ export enum InstanceState { + + /** + * The instance is currently unallocated + */ UNALLOCATED = "UNALLOCATED", + + /** + * The instance is currently pending + */ PENDING = "PENDING", + + /** + * The instance failed to start + */ FAILED = "FAILED", + + /** + * The instance is ready + */ READY = "READY" } /** - * Instance State Message wrapper + * Instance state message wrapper */ export class MessageInstanceState extends MessageRecv { state: InstanceState; @@ -47,9 +63,25 @@ export class MessageInstanceState extends MessageRecv { * Types of Authentication reposes */ export enum MessageAuthResponseOutcomeType { + + /** + * The authentication redirected used with Oauth 2.0 + */ REDIRECT = "REDIRECT", + + /** + * The token provided is invalid + */ INVALID_TOKEN = "INVALID_TOKEN", + + /** + * The authentication was successfully authenticated + */ AUTHENTICATED = "AUTHENTICATED", + + /** + * There was an error with authentication + */ ERROR = "ERROR" } @@ -82,10 +114,15 @@ export class MessageRequestInstance extends MessageSend { */ export class SPSSignalling { + // Define the instance state changed event onInstanceStateChanged: (stateChangedMsg: string, isError: boolean) => void; + + // Define the authentication response event onAuthenticationResponse: (authRespMsg: string, isError: boolean) => void; constructor(websocketController: WebSocketController) { + + // Initialise the signalling protocol extensions this.extendSignallingProtocol(websocketController); } @@ -94,38 +131,52 @@ export class SPSSignalling { */ extendSignallingProtocol(webSocketController: WebSocketController) { - // authenticationRequired + // Add the authentication required signalling message to the signalling protocol webSocketController.signallingProtocol.addMessageHandler("authenticationRequired", (authReqPayload: string) => { Logger.Log(Logger.GetStackTrace(), "AUTHENTICATION_REQUIRED", 6); const url_string = window.location.href; const url = new URL(url_string); + + // Create a authentication request message with the token and provider if supplied from the url parameters const authRequest = new MessageAuthRequest(url.searchParams.get("code"), url.searchParams.get("provider")); + + // Send the authentication request message to the signalling server webSocketController.webSocket.send(authRequest.payload()); }); - // instanceState + // Add the instance state signalling message to the signalling protocol webSocketController.signallingProtocol.addMessageHandler("instanceState", (instanceStatePayload: string) => { Logger.Log(Logger.GetStackTrace(), "INSTANCE_STATE", 6); + + // Create a instance state message from the instance state message payload const instanceState: MessageInstanceState = JSON.parse(instanceStatePayload); + + // Call how to handle the instance state changed this.handleInstanceStateChanged(instanceState); }); - // authenticationResponse + // Add the authentication response signalling message to the signalling protocol webSocketController.signallingProtocol.addMessageHandler("authenticationResponse", (authRespPayload: string) => { Logger.Log(Logger.GetStackTrace(), "AUTHENTICATION_RESPONSE", 6); + // Create the authentication response from the authentication response payload const authenticationResponse: MessageAuthResponse = JSON.parse(authRespPayload); + // Call how to handle the authentication response this.handleAuthenticationResponse(authenticationResponse); + // Handle the type of the authentication response switch (authenticationResponse.outcome) { case MessageAuthResponseOutcomeType.REDIRECT: { + + // Redirect the location to the redirect value window.location.href = authenticationResponse.redirect; break; } case MessageAuthResponseOutcomeType.AUTHENTICATED: { Logger.Log(Logger.GetStackTrace(), "User is authenticated and now requesting an instance", 6); + // Send a instance request massage to the signalling server webSocketController.webSocket.send(new MessageRequestInstance().payload()); break; } @@ -155,7 +206,7 @@ export class SPSSignalling { let isInstancePending = false; let isError = false; - // get the response type + // Create an informative message based on the state of the instance switch (instanceState.state) { case InstanceState.UNALLOCATED: instanceStateMessage = "Instance Unallocated: " + instanceState.details; @@ -186,11 +237,8 @@ export class SPSSignalling { break; } - if (isError) { - this.onInstanceStateChanged(instanceStateMessage, true); - } else { - this.onInstanceStateChanged(instanceStateMessage, false); - } + // Emit an instance state changed with an informative message and if error occurred + this.onInstanceStateChanged(instanceStateMessage, isError); } /** @@ -201,7 +249,7 @@ export class SPSSignalling { let instanceStateMessage = ""; let isError = false; - // get the response type + // Create an informative message based on the state of the authentication response switch (authResponse.outcome) { case MessageAuthResponseOutcomeType.AUTHENTICATED: instanceStateMessage = "Step 1/3: Requesting Instance"; @@ -222,6 +270,7 @@ export class SPSSignalling { break; } + // Emit an authentication response with an informative message and if an error occurred this.onAuthenticationResponse(instanceStateMessage, isError); } -} \ No newline at end of file +} diff --git a/library/src/index.ts b/library/src/index.ts index 995d9dca..0d278c0e 100644 --- a/library/src/index.ts +++ b/library/src/index.ts @@ -1,50 +1,78 @@ // Scalable Pixel Streaming Frontend exports export { SPSApplication } from "./SPSApplication"; export { LoadingOverlay } from "./LoadingOverlay"; -export { MessageSendTypes, MessageStats } from "./Messages"; -export { MessageAuthRequest, InstanceState, MessageInstanceState, MessageAuthResponseOutcomeType, MessageAuthResponse, MessageRequestInstance, SPSSignalling } from "./SignallingExtension"; +export { + MessageSendTypes, + MessageStats +} from "./Messages"; +export { + InstanceState, + MessageAuthRequest, + MessageAuthResponse, + MessageAuthResponseOutcomeType, + MessageInstanceState, + MessageRequestInstance, + SPSSignalling +} from "./SignallingExtension"; // Epic Games Pixel Streaming Frontend exports -export { WebRtcPlayerController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { WebXRController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { Config, ControlSchemeType, Flags, NumericParameters, TextParameters, OptionParameters, FlagsIds, NumericParametersIds, TextParametersIds, OptionParametersIds, AllSettings } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SettingBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SettingFlag } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SettingNumber } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SettingOption } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SettingText } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { AfkLogic } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { EncoderSettings, InitialSettings, WebRTCSettings } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { AggregatedStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { UnquantizedAndDenormalizeUnsigned } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { MessageSend } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { MessageRecv, MessageStreamerList } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { WebSocketController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { SignallingProtocol } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { CandidatePairStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { CandidateStat } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { DataChannelStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { InboundAudioStats, InboundVideoStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; -export { OutBoundVideoStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; +export { + AfkLogic, + AggregatedStats, + AllSettings, + CandidatePairStats, + CandidateStat, + Config, ControlSchemeType, + DataChannelStats, + EncoderSettings, + Flags, + FlagsIds, + InboundAudioStats, + InboundVideoStats, + InitialSettings, + LatencyTestResults, + Logger, + MessageRecv, MessageStreamerList, + MessageSend, + NumericParameters, + NumericParametersIds, + OptionParameters, + OptionParametersIds, + OutBoundVideoStats, + PixelStreaming, + SettingBase, + SettingFlag, + SettingNumber, + SettingOption, + SettingText, + SignallingProtocol, + TextParameters, + TextParametersIds, + UnquantizedAndDenormalizeUnsigned, + WebRtcPlayerController, + WebRTCSettings, + WebSocketController, + WebXRController +} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4'; // Epic Games Pixel Streaming Frontend UI exports -export { Application, UIOptions } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { AFKOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { ActionOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { OverlayBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { ConnectOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { DisconnectOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { ErrorOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { InfoOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { PlayOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { TextOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { ConfigUI } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { SettingUIBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { SettingUIFlag } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { SettingUINumber } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { SettingUIOption } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; -export { SettingUIText } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4'; +export { + ActionOverlay, + AFKOverlay, + Application, + ConfigUI, + ConnectOverlay, + DisconnectOverlay, + ErrorOverlay, + InfoOverlay, + OverlayBase, + PixelStreamingApplicationStyle, + PlayOverlay, + SettingUIBase, + SettingUIFlag, + SettingUINumber, + SettingUIOption, + SettingUIText, + TextOverlay, + UIOptions +} from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4';