UNPKG

@epicgames-ps/lib-pixelstreamingfrontend-ue5.4

Version:
892 lines (793 loc) 32.2 kB
// Copyright Epic Games, Inc. All Rights Reserved. import { Config, OptionParameters } from '../Config/Config'; import { LatencyTestResults } from '../DataChannel/LatencyTestResults'; import { AggregatedStats } from '../PeerConnectionController/AggregatedStats'; import { WebRtcPlayerController } from '../WebRtcPlayer/WebRtcPlayerController'; import { Flags, NumericParameters } from '../Config/Config'; import { Logger } from '../Logger/Logger'; import { InitialSettings } from '../DataChannel/InitialSettings'; import { OnScreenKeyboard } from '../UI/OnScreenKeyboard'; import { EventEmitter, InitialSettingsEvent, LatencyTestResultEvent, PixelStreamingEvent, StatsReceivedEvent, StreamLoadingEvent, StreamPreConnectEvent, StreamReconnectEvent, StreamPreDisconnectEvent, VideoEncoderAvgQPEvent, VideoInitializedEvent, WebRtcAutoConnectEvent, WebRtcConnectedEvent, WebRtcConnectingEvent, WebRtcDisconnectedEvent, WebRtcFailedEvent, WebRtcSdpEvent, DataChannelLatencyTestResponseEvent, DataChannelLatencyTestResultEvent, PlayerCountEvent, WebRtcTCPRelayDetectedEvent } from '../Util/EventEmitter'; import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive'; import { WebXRController } from '../WebXR/WebXRController'; import { MessageDirection } from '../UeInstanceMessage/StreamMessageController'; import { DataChannelLatencyTestConfig, DataChannelLatencyTestController } from "../DataChannel/DataChannelLatencyTestController"; import { DataChannelLatencyTestResponse, DataChannelLatencyTestResult } from "../DataChannel/DataChannelLatencyTestResults"; import { RTCUtils } from '../Util/RTCUtils'; export interface PixelStreamingOverrides { /** The DOM elment where Pixel Streaming video and user input event handlers are attached to. * You can give an existing DOM element here. If not given, the library will create a new div element * that is not attached anywhere. In this case you can later get access to this new element and * attach it to your web page. */ videoElementParent?: HTMLElement; } /** * The key class for the browser side of a Pixel Streaming application, it includes: * WebRTC handling, XR support, input handling, and emitters for lifetime and state change events. * Users are encouraged to use this class as is, through composition, or extend it. In any case, * this will likely be the core of your Pixel Streaming experience in terms of functionality. */ export class PixelStreaming { protected _webRtcController: WebRtcPlayerController; protected _webXrController: WebXRController; protected _dataChannelLatencyTestController: DataChannelLatencyTestController; /** * Configuration object. You can read or modify config through this object. Whenever * the configuration is changed, the library will emit a `settingsChanged` event. */ public config: Config; private _videoElementParent: HTMLElement; private allowConsoleCommands = false; private onScreenKeyboardHelper: OnScreenKeyboard; private _videoStartTime: number; private _inputController: boolean; private _eventEmitter: EventEmitter; /** * @param config - A newly instantiated config object * @param overrides - Parameters to override default behaviour * returns the base Pixel streaming object */ constructor(config: Config, overrides?: PixelStreamingOverrides) { this.config = config; if (overrides?.videoElementParent) { this._videoElementParent = overrides.videoElementParent; } this._eventEmitter = new EventEmitter(); this.configureSettings(); // setup WebRTC this.setWebRtcPlayerController( new WebRtcPlayerController(this.config, this) ); // Onscreen keyboard this.onScreenKeyboardHelper = new OnScreenKeyboard( this.videoElementParent ); this.onScreenKeyboardHelper.unquantizeAndDenormalizeUnsigned = ( x: number, y: number ) => this._webRtcController.requestUnquantizedAndDenormalizeUnsigned( x, y ); this._activateOnScreenKeyboard = (command: MessageOnScreenKeyboard) => this.onScreenKeyboardHelper.showOnScreenKeyboard(command); this._webXrController = new WebXRController(this._webRtcController); this._setupWebRtcTCPRelayDetection = this._setupWebRtcTCPRelayDetection.bind(this) // Add event listener for the webRtcConnected event this._eventEmitter.addEventListener("webRtcConnected", (webRtcConnectedEvent: WebRtcConnectedEvent) => { // Bind to the stats received event this._eventEmitter.addEventListener("statsReceived", this._setupWebRtcTCPRelayDetection); }); } /** * Gets the element that contains the video stream element. */ public get videoElementParent(): HTMLElement { if (!this._videoElementParent) { this._videoElementParent = document.createElement('div'); this._videoElementParent.id = 'videoElementParent'; } return this._videoElementParent; } /** * Configure the settings with on change listeners and any additional per experience settings. */ private configureSettings(): void { this.config._addOnSettingChangedListener( Flags.IsQualityController, (wantsQualityController: boolean) => { // If the setting has been set to true (either programatically or the user has flicked the toggle) // and we aren't currently quality controller, send the request if ( wantsQualityController === true && !this._webRtcController.isQualityController ) { this._webRtcController.sendRequestQualityControlOwnership(); } } ); this.config._addOnSettingChangedListener( Flags.AFKDetection, (isAFKEnabled: boolean) => { this._webRtcController.setAfkEnabled(isAFKEnabled); } ); this.config._addOnSettingChangedListener( Flags.MatchViewportResolution, () => { this._webRtcController.videoPlayer.updateVideoStreamSize(); } ); this.config._addOnSettingChangedListener( Flags.HoveringMouseMode, (isHoveringMouse: boolean) => { this.config.setFlagLabel( Flags.HoveringMouseMode, `Control Scheme: ${ isHoveringMouse ? 'Hovering' : 'Locked' } Mouse` ); this._webRtcController.setMouseInputEnabled(this.config.isFlagEnabled(Flags.MouseInput)); } ); // user input this.config._addOnSettingChangedListener( Flags.KeyboardInput, (isEnabled: boolean) => { this._webRtcController.setKeyboardInputEnabled(isEnabled); } ); this.config._addOnSettingChangedListener( Flags.MouseInput, (isEnabled: boolean) => { this._webRtcController.setMouseInputEnabled(isEnabled); } ); this.config._addOnSettingChangedListener( Flags.FakeMouseWithTouches, (_isFakeMouseEnabled: boolean) => { this._webRtcController.setTouchInputEnabled(this.config.isFlagEnabled(Flags.TouchInput)); } ); this.config._addOnSettingChangedListener( Flags.TouchInput, (isEnabled: boolean) => { this._webRtcController.setTouchInputEnabled(isEnabled); } ); this.config._addOnSettingChangedListener( Flags.GamepadInput, (isEnabled: boolean) => { this._webRtcController.setGamePadInputEnabled(isEnabled); } ); // encoder settings this.config._addOnNumericSettingChangedListener( NumericParameters.MinQP, (newValue: number) => { Logger.Log( Logger.GetStackTrace(), '-------- Sending MinQP --------', 7 ); this._webRtcController.sendEncoderMinQP(newValue); Logger.Log( Logger.GetStackTrace(), '-------------------------------------------', 7 ); } ); this.config._addOnNumericSettingChangedListener( NumericParameters.MaxQP, (newValue: number) => { Logger.Log( Logger.GetStackTrace(), '-------- Sending encoder settings --------', 7 ); this._webRtcController.sendEncoderMaxQP(newValue); Logger.Log( Logger.GetStackTrace(), '-------------------------------------------', 7 ); } ); // WebRTC settings this.config._addOnNumericSettingChangedListener( NumericParameters.WebRTCMinBitrate, (newValue: number) => { Logger.Log( Logger.GetStackTrace(), '-------- Sending web rtc settings --------', 7 ); this._webRtcController.sendWebRTCMinBitrate(newValue * 1000 /* kbps to bps */); Logger.Log( Logger.GetStackTrace(), '-------------------------------------------', 7 ); } ); this.config._addOnNumericSettingChangedListener( NumericParameters.WebRTCMaxBitrate, (newValue: number) => { Logger.Log( Logger.GetStackTrace(), '-------- Sending web rtc settings --------', 7 ); this._webRtcController.sendWebRTCMaxBitrate(newValue * 1000 /* kbps to bps */); Logger.Log( Logger.GetStackTrace(), '-------------------------------------------', 7 ); } ); this.config._addOnNumericSettingChangedListener( NumericParameters.WebRTCFPS, (newValue: number) => { Logger.Log( Logger.GetStackTrace(), '-------- Sending web rtc settings --------', 7 ); this._webRtcController.sendWebRTCFps(newValue); Logger.Log( Logger.GetStackTrace(), '-------------------------------------------', 7 ); } ); this.config._addOnOptionSettingChangedListener( OptionParameters.PreferredCodec, (newValue: string) => { if (this._webRtcController) { this._webRtcController.setPreferredCodec(newValue); } } ); this.config._registerOnChangeEvents(this._eventEmitter); } /** * Activate the on screen keyboard when receiving the command from the streamer * @param command - the keyboard command */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _activateOnScreenKeyboard(command: MessageOnScreenKeyboard): void { throw new Error('Method not implemented.'); } /** * Set the input control ownership * @param inputControlOwnership - does the user have input control ownership */ _onInputControlOwnership(inputControlOwnership: boolean): void { this._inputController = inputControlOwnership; } /** * Instantiate the WebRTCPlayerController interface to provide WebRTCPlayerController functionality within this class and set up anything that requires it * @param webRtcPlayerController - a WebRtcPlayerController controller instance */ private setWebRtcPlayerController( webRtcPlayerController: WebRtcPlayerController ) { this._webRtcController = webRtcPlayerController; this._webRtcController.setPreferredCodec( this.config.getSettingOption(OptionParameters.PreferredCodec) .selected ); this._webRtcController.resizePlayerStyle(); // connect if auto connect flag is enabled this.checkForAutoConnect(); } /** * Connect to signaling server. */ public connect() { this._eventEmitter.dispatchEvent(new StreamPreConnectEvent()); this._webRtcController.connectToSignallingServer(); } /** * Reconnects to the signaling server. If connection is up, disconnects first * before establishing a new connection */ public reconnect() { this._eventEmitter.dispatchEvent(new StreamReconnectEvent()); this._webRtcController.tryReconnect("Reconnecting..."); } /** * Disconnect from the signaling server and close open peer connections. */ public disconnect() { this._eventEmitter.dispatchEvent(new StreamPreDisconnectEvent()); this._webRtcController.close(); } /** * Play the stream. Can be called only after a peer connection has been established. */ public play() { this._onStreamLoading(); this._webRtcController.playStream(); } /** * Auto connect if AutoConnect flag is enabled */ private checkForAutoConnect() { // set up if the auto play will be used or regular click to start if (this.config.isFlagEnabled(Flags.AutoConnect)) { // if autoplaying show an info overlay while while waiting for the connection to begin this._onWebRtcAutoConnect(); this._webRtcController.connectToSignallingServer(); } } /** * Will unmute the microphone track which is sent to Unreal Engine. * By default, will only unmute an existing mic track. * * @param forceEnable Can be used for cases when this object wasn't initialized with a mic track. * If this parameter is true, the connection will be restarted with a microphone. * Warning: this takes some time, as a full renegotiation and reconnection will happen. */ public unmuteMicrophone(forceEnable = false) : void { // If there's an existing mic track, we just set muted state if (this.config.isFlagEnabled('UseMic')) { this.setMicrophoneMuted(false); return; } // If there's no pre-existing mic track, and caller is ok with full reset, we enable and reset if (forceEnable) { this.config.setFlagEnabled("UseMic", true); this.reconnect(); return; } // If we prefer not to force a reconnection, just warn the user that this operation didn't happen Logger.Warning( Logger.GetStackTrace(), 'Trying to unmute mic, but PixelStreaming was initialized with no microphone track. Call with forceEnable == true to re-connect with a mic track.' ); } public muteMicrophone() : void { if (this.config.isFlagEnabled('UseMic')) { this.setMicrophoneMuted(true); return; } // If there wasn't a mic track, just let user know there's nothing to mute Logger.Info( Logger.GetStackTrace(), 'Trying to mute mic, but PixelStreaming has no microphone track, so sending sound is already disabled.' ); } private setMicrophoneMuted(mute: boolean) : void { for (const transceiver of this._webRtcController?.peerConnectionController?.peerConnection?.getTransceivers() ?? []) { if (RTCUtils.canTransceiverSendAudio(transceiver)) { transceiver.sender.track.enabled = !mute; } } } /** * Emit an event on auto connecting */ _onWebRtcAutoConnect() { this._eventEmitter.dispatchEvent(new WebRtcAutoConnectEvent()); } /** * Set up functionality to happen when receiving a webRTC answer */ _onWebRtcSdp() { this._eventEmitter.dispatchEvent(new WebRtcSdpEvent()); } /** * Emits a StreamLoading event */ _onStreamLoading() { this._eventEmitter.dispatchEvent(new StreamLoadingEvent()); } /** * Event fired when the video is disconnected - emits given eventString or an override * message from webRtcController if one has been set * @param eventString - a string describing why the connection closed * @param allowClickToReconnect - true if we want to allow the user to retry the connection with a click */ _onDisconnect(eventString: string, allowClickToReconnect: boolean) { this._eventEmitter.dispatchEvent( new WebRtcDisconnectedEvent({ eventString: eventString, allowClickToReconnect: allowClickToReconnect }) ); } /** * Handles when Web Rtc is connecting */ _onWebRtcConnecting() { this._eventEmitter.dispatchEvent(new WebRtcConnectingEvent()); } /** * Handles when Web Rtc has connected */ _onWebRtcConnected() { this._eventEmitter.dispatchEvent(new WebRtcConnectedEvent()); } /** * Handles when Web Rtc fails to connect */ _onWebRtcFailed() { this._eventEmitter.dispatchEvent(new WebRtcFailedEvent()); } /** * Handle when the Video has been Initialized */ _onVideoInitialized() { this._eventEmitter.dispatchEvent(new VideoInitializedEvent()); this._videoStartTime = Date.now(); } /** * Set up functionality to happen when receiving latency test results * @param latency - latency test results object */ _onLatencyTestResult(latencyTimings: LatencyTestResults) { this._eventEmitter.dispatchEvent( new LatencyTestResultEvent({ latencyTimings }) ); } _onDataChannelLatencyTestResponse(response: DataChannelLatencyTestResponse) { this._eventEmitter.dispatchEvent( new DataChannelLatencyTestResponseEvent({ response }) ); } /** * Set up functionality to happen when receiving video statistics * @param videoStats - video statistics as a aggregate stats object */ _onVideoStats(videoStats: AggregatedStats) { // Duration if (!this._videoStartTime || this._videoStartTime === undefined) { this._videoStartTime = Date.now(); } videoStats.handleSessionStatistics( this._videoStartTime, this._inputController, this._webRtcController.videoAvgQp ); this._eventEmitter.dispatchEvent( new StatsReceivedEvent({ aggregatedStats: videoStats }) ); } /** * Set up functionality to happen when calculating the average video encoder qp * @param QP - the quality number of the stream */ _onVideoEncoderAvgQP(QP: number) { this._eventEmitter.dispatchEvent( new VideoEncoderAvgQPEvent({ avgQP: QP }) ); } /** * Set up functionality to happen when receiving and handling initial settings for the UE app * @param settings - initial UE app settings */ _onInitialSettings(settings: InitialSettings) { this._eventEmitter.dispatchEvent( new InitialSettingsEvent({ settings }) ); if (settings.PixelStreamingSettings) { this.allowConsoleCommands = settings.PixelStreamingSettings.AllowPixelStreamingCommands ?? false; if (this.allowConsoleCommands === false) { Logger.Info( Logger.GetStackTrace(), '-AllowPixelStreamingCommands=false, sending arbitrary console commands from browser to UE is disabled.' ); } } const useUrlParams = this.config.useUrlParams; const urlParams = new URLSearchParams(window.location.search); Logger.Info( Logger.GetStackTrace(), `using URL parameters ${useUrlParams}` ); if (settings.EncoderSettings) { this.config.setNumericSetting( NumericParameters.MinQP, // If a setting is set in the URL, make sure we respect that value as opposed to what the application sends us (useUrlParams && urlParams.has(NumericParameters.MinQP)) ? Number.parseFloat(urlParams.get(NumericParameters.MinQP)) : settings.EncoderSettings.MinQP ); this.config.setNumericSetting( NumericParameters.MaxQP, (useUrlParams && urlParams.has(NumericParameters.MaxQP)) ? Number.parseFloat(urlParams.get(NumericParameters.MaxQP)) : settings.EncoderSettings.MaxQP ); } if (settings.WebRTCSettings) { this.config.setNumericSetting( NumericParameters.WebRTCMinBitrate, (useUrlParams && urlParams.has(NumericParameters.WebRTCMinBitrate)) ? Number.parseFloat(urlParams.get(NumericParameters.WebRTCMinBitrate)) : (settings.WebRTCSettings.MinBitrate / 1000) /* bps to kbps */ ); this.config.setNumericSetting( NumericParameters.WebRTCMaxBitrate, (useUrlParams && urlParams.has(NumericParameters.WebRTCMaxBitrate)) ? Number.parseFloat(urlParams.get(NumericParameters.WebRTCMaxBitrate)) : (settings.WebRTCSettings.MaxBitrate / 1000) /* bps to kbps */ ); this.config.setNumericSetting( NumericParameters.WebRTCFPS, (useUrlParams && urlParams.has(NumericParameters.WebRTCFPS)) ? Number.parseFloat(urlParams.get(NumericParameters.WebRTCFPS)) : settings.WebRTCSettings.FPS ); } } /** * Set up functionality to happen when setting quality control ownership of a stream * @param hasQualityOwnership - does this user have quality ownership of the stream true / false */ _onQualityControlOwnership(hasQualityOwnership: boolean) { this.config.setFlagEnabled( Flags.IsQualityController, hasQualityOwnership ); } _onPlayerCount(playerCount: number) { this._eventEmitter.dispatchEvent( new PlayerCountEvent({ count: playerCount }) ); } // Sets up to emit the webrtc tcp relay detect event _setupWebRtcTCPRelayDetection(statsReceivedEvent: StatsReceivedEvent) { // Get the active candidate pair let activeCandidatePair = statsReceivedEvent.data.aggregatedStats.getActiveCandidatePair(); // Check if the active candidate pair is not null if (activeCandidatePair != null) { // Get the local candidate assigned to the active candidate pair let localCandidate = statsReceivedEvent.data.aggregatedStats.localCandidates.find((candidate) => candidate.id == activeCandidatePair.localCandidateId, null) // Check if the local candidate is not null, candidate type is relay and the relay protocol is tcp if (localCandidate != null && localCandidate.candidateType == 'relay' && localCandidate.relayProtocol == 'tcp') { // Send the web rtc tcp relay detected event this._eventEmitter.dispatchEvent(new WebRtcTCPRelayDetectedEvent()); } // The check is completed and the stats listen event can be removed this._eventEmitter.removeEventListener("statsReceived", this._setupWebRtcTCPRelayDetection); } } /** * Request a connection latency test. * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! * @returns */ public requestLatencyTest() { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } this._webRtcController.sendLatencyTest(); return true; } /** * Request a data channel latency test. * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! */ public requestDataChannelLatencyTest(config: DataChannelLatencyTestConfig) { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } if (!this._dataChannelLatencyTestController) { this._dataChannelLatencyTestController = new DataChannelLatencyTestController( this._webRtcController.sendDataChannelLatencyTest.bind(this._webRtcController), (result: DataChannelLatencyTestResult) => { this._eventEmitter.dispatchEvent(new DataChannelLatencyTestResultEvent( { result })) }); this.addEventListener( "dataChannelLatencyTestResponse", ({data: {response} }) => { this._dataChannelLatencyTestController.receive(response); } ) } return this._dataChannelLatencyTestController.start(config); } /** * Request for the UE application to show FPS counter. * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! * @returns */ public requestShowFps() { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } this._webRtcController.sendShowFps(); return true; } /** * Request for a new IFrame from the UE application. * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! * @returns */ public requestIframe() { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } this._webRtcController.sendIframeRequest(); return true; } /** * Send data to UE application. The data will be run through JSON.stringify() so e.g. strings * and any serializable plain JSON objects with no recurrence can be sent. * @returns true if succeeded, false if rejected */ public emitUIInteraction(descriptor: object | string) { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } this._webRtcController.emitUIInteraction(descriptor); return true; } /** * Send a command to UE application. Blocks ConsoleCommand descriptors unless UE * has signaled that it allows console commands. * @returns true if succeeded, false if rejected */ public emitCommand(descriptor: object) { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } if (!this.allowConsoleCommands && 'ConsoleCommand' in descriptor) { return false; } this._webRtcController.emitCommand(descriptor); return true; } /** * Send a console command to UE application. Only allowed if UE has signaled that it allows * console commands. * @returns true if succeeded, false if rejected */ public emitConsoleCommand(command: string) { if (!this.allowConsoleCommands || !this._webRtcController.videoPlayer.isVideoReady()) { return false; } this._webRtcController.emitConsoleCommand(command); return true; } /** * Add a UE -> browser response event listener * @param name - The name of the response handler * @param listener - The method to be activated when a message is received */ public addResponseEventListener( name: string, listener: (response: string) => void ) { this._webRtcController.responseController.addResponseEventListener(name, listener); } /** * Remove a UE -> browser response event listener * @param name - The name of the response handler */ public removeResponseEventListener(name: string) { this._webRtcController.responseController.removeResponseEventListener(name); } /** * Dispatch a new event. * @param e event * @returns */ public dispatchEvent(e: PixelStreamingEvent): boolean { return this._eventEmitter.dispatchEvent(e); } /** * Register an event handler. * @param type event name * @param listener event handler function */ public addEventListener< T extends PixelStreamingEvent['type'], E extends PixelStreamingEvent & { type: T } >(type: T, listener: (e: Event & E) => void) { this._eventEmitter.addEventListener(type, listener); } /** * Remove an event handler. * @param type event name * @param listener event handler function */ public removeEventListener< T extends PixelStreamingEvent['type'], E extends PixelStreamingEvent & { type: T } >(type: T, listener: (e: Event & E) => void) { this._eventEmitter.removeEventListener(type, listener); } /** * Enable/disable XR mode. */ public toggleXR() { this.webXrController.xrClicked(); } /** * Pass in a function to generate a signalling server URL. * This function is useful if you need to programmatically construct your signalling server URL. * @param signallingUrlBuilderFunc A function that generates a signalling server url. */ public setSignallingUrlBuilder(signallingUrlBuilderFunc: ()=>string) { this._webRtcController.signallingUrlBuilder = signallingUrlBuilderFunc; } /** * Public getter for the websocket controller. Access to this property allows you to send * custom websocket messages. */ public get webSocketController() { return this._webRtcController.webSocketController; } /** * Public getter for the webXrController controller. Used for all XR features. */ public get webXrController() { return this._webXrController; } public registerMessageHandler(name: string, direction: MessageDirection, handler?: (data: ArrayBuffer | Array<number | string>) => void) { if(direction === MessageDirection.FromStreamer && typeof handler === 'undefined') { Logger.Warning(Logger.GetStackTrace(), `Unable to register an undefined handler for ${name}`) return; } if(direction === MessageDirection.ToStreamer && typeof handler === 'undefined') { this._webRtcController.streamMessageController.registerMessageHandler( direction, name, (data: Array<number | string>) => this._webRtcController.sendMessageController.sendMessageToStreamer( name, data ) ); } else { this._webRtcController.streamMessageController.registerMessageHandler( direction, name, (data: ArrayBuffer) => handler(data) ); } } public get toStreamerHandlers() { return this._webRtcController.streamMessageController.toStreamerHandlers; } public isReconnecting() { return this._webRtcController.isReconnecting; } }