From 67248f61e431b32e1290da8fc23a7388a8802918 Mon Sep 17 00:00:00 2001 From: Nazar Rudenko Date: Fri, 23 Jun 2023 10:32:45 +1000 Subject: [PATCH 1/2] Add DataChannelLatencyTest --- .../EpicGames/src/stresstest.ts | 3 +- .../DataChannelLatencyTestController.ts | 128 ++++++++++++++++++ .../DataChannelLatencyTestResults.ts | 73 ++++++++++ .../src/PixelStreaming/PixelStreaming.ts | 43 ++++++ .../SendDescriptorController.ts | 9 ++ .../StreamMessageController.ts | 6 + Frontend/library/src/Util/EventEmitter.ts | 36 +++++ .../WebRtcPlayer/WebRtcPlayerController.ts | 33 +++++ .../ui-library/src/Application/Application.ts | 36 +++-- .../src/UI/DataChannelLatencyTest.ts | 114 ++++++++++++++++ Frontend/ui-library/src/UI/StatsPanel.ts | 46 ++++++- 11 files changed, 505 insertions(+), 22 deletions(-) create mode 100644 Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts create mode 100644 Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts create mode 100644 Frontend/ui-library/src/UI/DataChannelLatencyTest.ts diff --git a/Frontend/implementations/EpicGames/src/stresstest.ts b/Frontend/implementations/EpicGames/src/stresstest.ts index e5bece5f..c95e12dc 100644 --- a/Frontend/implementations/EpicGames/src/stresstest.ts +++ b/Frontend/implementations/EpicGames/src/stresstest.ts @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -import { Config, Flags, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; +import { Config, Flags, TextParameters, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3'; const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); @@ -157,6 +157,7 @@ export class StressTester { config.setFlagEnabled(Flags.AutoConnect, true); config.setFlagEnabled(Flags.AutoPlayVideo, true); config.setFlagEnabled(Flags.StartVideoMuted, true); + config.setTextSetting(TextParameters.SignallingServerUrl, "ws://127.0.0.1:80/") // Create a Native DOM delegate instance that implements the Delegate interface class const stream = new PixelStreaming(config); diff --git a/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts b/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts new file mode 100644 index 00000000..307aaf11 --- /dev/null +++ b/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts @@ -0,0 +1,128 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +import { Logger } from '../Logger/Logger'; +import { + DataChannelLatencyTestRecord, + DataChannelLatencyTestRequest, + DataChannelLatencyTestResponse, + DataChannelLatencyTestResult, + DataChannelLatencyTestSeq, + DataChannelLatencyTestTimestamp +} from "./DataChannelLatencyTestResults"; + +export type DataChannelLatencyTestConfig = { + // test duration in milliseconds + duration: number; + //requests per second + rps: number; + //request filler size + requestSize: number; + //response filler size + responseSize: number; +} + +export type DataChannelLatencyTestSink = (request: DataChannelLatencyTestRequest) => void; +export type DataChannelLatencyTestResultCallback = (result: DataChannelLatencyTestResult) => void; + +export class DataChannelLatencyTestController { + startTime: DataChannelLatencyTestTimestamp; + sink: DataChannelLatencyTestSink; + callback: DataChannelLatencyTestResultCallback; + records: Map; + seq: DataChannelLatencyTestSeq; + interval: NodeJS.Timer; + + constructor(sink: DataChannelLatencyTestSink, callback: DataChannelLatencyTestResultCallback) { + this.sink = sink; + this.callback = callback; + this.records = new Map(); + this.seq = 0; + } + + start(config: DataChannelLatencyTestConfig) { + if (this.isRunning()) { + return false; + } + this.startTime = Date.now(); + this.records.clear(); + this.interval = setInterval((() => { + if (Date.now() - this.startTime >= config.duration) { + this.stop(); + } else { + this.sendRequest(config.requestSize, config.responseSize); + } + }).bind(this), Math.floor(1000/config.rps)); + return true; + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = undefined; + this.callback(this.produceResult()); + } + } + + produceResult(): DataChannelLatencyTestResult { + const resultRecords = new Map(this.records); + return { + records: resultRecords, + dataChannelRtt: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => { + return acc + (next.playerReceivedTimestamp - next.playerSentTimestamp); + }, 0) / this.records.size), + playerToStreamerTime: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => { + return acc + (next.streamerReceivedTimestamp - next.playerSentTimestamp); + }, 0) / this.records.size), + streamerToPlayerTime: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => { + return acc + (next.playerReceivedTimestamp - next.streamerSentTimestamp); + }, 0) / this.records.size), + exportLatencyAsCSV: () => { + let csv = "Timestamp;RTT;PlayerToStreamer;StreamerToPlayer;\n"; + resultRecords.forEach((record) => { + csv += record.playerSentTimestamp + ";"; + csv += (record.playerReceivedTimestamp - record.playerSentTimestamp) + ";"; + csv += (record.streamerReceivedTimestamp - record.playerSentTimestamp) + ";"; + csv += (record.playerReceivedTimestamp - record.streamerSentTimestamp) + ";"; + csv += "\n"; + }) + return csv; + } + } + } + + isRunning() { + return !!this.interval; + } + + receive(response: DataChannelLatencyTestResponse) { + if (!this.isRunning()) { + return; + } + if (!response) { + console.log("Undefined response from server O^O") + return; + } + let record = this.records.get(response.Seq); + if (record) { + record.update(response); + } + } + + sendRequest(requestSize: number, responseSize: number) { + let request = this.createRequest(requestSize, responseSize); + let record = new DataChannelLatencyTestRecord(request); + this.records.set(record.seq, record); + this.sink(request); + } + + createRequest(requestSize: number, responseSize: number): DataChannelLatencyTestRequest { + return { + Seq: this.seq++, + FillResponseSize: responseSize, + Filler: requestSize ? "A".repeat(requestSize) : "" + } + } + +} + + diff --git a/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts b/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts new file mode 100644 index 00000000..73e14e95 --- /dev/null +++ b/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts @@ -0,0 +1,73 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/** + * Data Channel Latency Test types + */ + + +/** + * Unix epoch + */ +export type DataChannelLatencyTestTimestamp = number; + +/** + * Sequence number represented by unsigned int + */ +export type DataChannelLatencyTestSeq = number; + +/** + * Request sent to Streamer + */ +export type DataChannelLatencyTestRequest = { + Seq: DataChannelLatencyTestSeq; + FillResponseSize: number; + Filler: string; +} + +/** + * Response from the Streamer + */ +export type DataChannelLatencyTestResponse = { + Seq: DataChannelLatencyTestSeq; + Filler: string; + ReceivedTimestamp: DataChannelLatencyTestTimestamp; + SentTimestamp: DataChannelLatencyTestTimestamp; +} + +export type DataChannelLatencyTestResult = { + records: Map + dataChannelRtt: number, + playerToStreamerTime: number, + streamerToPlayerTime: number, + exportLatencyAsCSV: () => string +} + +export class DataChannelLatencyTestRecord { + seq: DataChannelLatencyTestSeq; + playerSentTimestamp: DataChannelLatencyTestTimestamp; + playerReceivedTimestamp: DataChannelLatencyTestTimestamp; + streamerReceivedTimestamp: DataChannelLatencyTestTimestamp; + streamerSentTimestamp: DataChannelLatencyTestTimestamp; + requestFillerSize: number; + responseFillerSize: number; + + constructor(request: DataChannelLatencyTestRequest) { + this.seq = request.Seq; + this.playerSentTimestamp = Date.now(); + this.requestFillerSize = request.Filler ? request.Filler.length : 0; + } + + update(response: DataChannelLatencyTestResponse) { + this.playerReceivedTimestamp = Date.now(); + this.streamerReceivedTimestamp = response.ReceivedTimestamp; + this.streamerSentTimestamp = response.SentTimestamp; + this.responseFillerSize = response.Filler ? response.Filler.length : 0; + } + +} + + + + + + diff --git a/Frontend/library/src/PixelStreaming/PixelStreaming.ts b/Frontend/library/src/PixelStreaming/PixelStreaming.ts index 6d07728f..2412b074 100644 --- a/Frontend/library/src/PixelStreaming/PixelStreaming.ts +++ b/Frontend/library/src/PixelStreaming/PixelStreaming.ts @@ -9,6 +9,7 @@ import { Logger } from '../Logger/Logger'; import { InitialSettings } from '../DataChannel/InitialSettings'; import { OnScreenKeyboard } from '../UI/OnScreenKeyboard'; import { + DataChannelLatencyTestResponseEvent, DataChannelLatencyTestResultEvent, EventEmitter, InitialSettingsEvent, LatencyTestResultEvent, @@ -26,6 +27,14 @@ import { } from '../Util/EventEmitter'; import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive'; import { WebXRController } from '../WebXR/WebXRController'; +import { + DataChannelLatencyTestConfig, + DataChannelLatencyTestController, DataChannelLatencyTestResultCallback +} from "../DataChannel/DataChannelLatencyTestController"; +import { + DataChannelLatencyTestResponse, + DataChannelLatencyTestResult +} from "../DataChannel/DataChannelLatencyTestResults"; export interface PixelStreamingOverrides { /** The DOM elment where Pixel Streaming video and user input event handlers are attached to. @@ -44,6 +53,7 @@ export interface PixelStreamingOverrides { 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. @@ -453,6 +463,12 @@ export class PixelStreaming { ); } + _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 @@ -568,6 +584,33 @@ export class PixelStreaming { return true; } + /** + * Request a data channel latency test. + * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! + * @returns + */ + public requestDataChannelLatencyTest(config: DataChannelLatencyTestConfig) { + if (!this._webRtcController.videoPlayer.isVideoReady()) { + return false; + } + if (!this._dataChannelLatencyTestController) { + let that = this; + this._dataChannelLatencyTestController = new DataChannelLatencyTestController( + this._webRtcController.sendDataChannelLatencyTest.bind(this._webRtcController), + (result: DataChannelLatencyTestResult) => { + console.log(that); + that._eventEmitter.dispatchEvent(new DataChannelLatencyTestResultEvent( { result })) + }); + this.addEventListener( + "dataChannelLatencyTestResponse", + ({data: {response} }) => { + that._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! diff --git a/Frontend/library/src/UeInstanceMessage/SendDescriptorController.ts b/Frontend/library/src/UeInstanceMessage/SendDescriptorController.ts index 553007f5..817629c6 100644 --- a/Frontend/library/src/UeInstanceMessage/SendDescriptorController.ts +++ b/Frontend/library/src/UeInstanceMessage/SendDescriptorController.ts @@ -3,6 +3,7 @@ import { DataChannelSender } from '../DataChannel/DataChannelSender'; import { Logger } from '../Logger/Logger'; import { StreamMessageController } from './StreamMessageController'; +import {DataChannelLatencyTestRequest} from "../DataChannel/DataChannelLatencyTestResults"; export class SendDescriptorController { toStreamerMessagesMapProvider: StreamMessageController; @@ -24,6 +25,14 @@ export class SendDescriptorController { this.sendDescriptor('LatencyTest', descriptor); } + /** + * Send a Data Channel Latency Test to the UE Instance + * @param descriptor - the descriptor for a latency test + */ + sendDataChannelLatencyTest(descriptor: DataChannelLatencyTestRequest) { + this.sendDescriptor('DataChannelLatencyTest', descriptor); + } + /** * Send a Latency Test to the UE Instance * @param descriptor - the descriptor for a command diff --git a/Frontend/library/src/UeInstanceMessage/StreamMessageController.ts b/Frontend/library/src/UeInstanceMessage/StreamMessageController.ts index 024fefc3..31bcd290 100644 --- a/Frontend/library/src/UeInstanceMessage/StreamMessageController.ts +++ b/Frontend/library/src/UeInstanceMessage/StreamMessageController.ts @@ -82,6 +82,11 @@ export class StreamMessageController { byteLength: 0, structure: [] }); + this.toStreamerMessages.add('DataChannelLatencyTest', { + id: 9, + byteLength: 0, + structure: [] + }); /* * Input Messages. Range = 50..89. */ @@ -220,6 +225,7 @@ export class StreamMessageController { this.fromStreamerMessages.add('TestEcho', 11); this.fromStreamerMessages.add('InputControlOwnership', 12); this.fromStreamerMessages.add('GamepadResponse', 13); + this.fromStreamerMessages.add('DataChannelLatencyTest', 14); this.fromStreamerMessages.add('Protocol', 255); } diff --git a/Frontend/library/src/Util/EventEmitter.ts b/Frontend/library/src/Util/EventEmitter.ts index 89eb3a51..a96ce1b1 100644 --- a/Frontend/library/src/Util/EventEmitter.ts +++ b/Frontend/library/src/Util/EventEmitter.ts @@ -12,6 +12,10 @@ import { SettingFlag } from '../Config/SettingFlag'; import { SettingNumber } from '../Config/SettingNumber'; import { SettingText } from '../Config/SettingText'; import { SettingOption } from '../Config/SettingOption'; +import { + DataChannelLatencyTestResponse, + DataChannelLatencyTestResult +} from "../DataChannel/DataChannelLatencyTestResults"; /** * An event that is emitted when AFK disconnect is about to happen. @@ -336,6 +340,36 @@ export class LatencyTestResultEvent extends Event { } } +/** + * An event that is emitted when receiving data channel latency test results. + */ +export class DataChannelLatencyTestResponseEvent extends Event { + readonly type: 'dataChannelLatencyTestResponse'; + readonly data: { + /** Latency test result object */ + response: DataChannelLatencyTestResponse + }; + constructor(data: DataChannelLatencyTestResponseEvent['data']) { + super('dataChannelLatencyTestResponse'); + this.data = data; + } +} + +/** + * An event that is emitted when receiving data channel latency test results. + */ +export class DataChannelLatencyTestResultEvent extends Event { + readonly type: 'dataChannelLatencyTestResult'; + readonly data: { + /** Latency test result object */ + result: DataChannelLatencyTestResult + }; + constructor(data: DataChannelLatencyTestResultEvent['data']) { + super('dataChannelLatencyTestResult'); + this.data = data; + } +} + /** * An event that is emitted when receiving initial settings from UE. */ @@ -465,6 +499,8 @@ export type PixelStreamingEvent = | StatsReceivedEvent | StreamerListMessageEvent | LatencyTestResultEvent + | DataChannelLatencyTestResponseEvent + | DataChannelLatencyTestResultEvent | InitialSettingsEvent | SettingsChangedEvent | XrSessionStartedEvent diff --git a/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts b/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts index 050a8514..17219c08 100644 --- a/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts +++ b/Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts @@ -62,6 +62,10 @@ import { PlayStreamRejectedEvent, StreamerListMessageEvent } from '../Util/EventEmitter'; +import { + DataChannelLatencyTestRequest, + DataChannelLatencyTestResponse +} from "../DataChannel/DataChannelLatencyTestResults"; /** * Entry point for the WebRTC Player */ @@ -377,6 +381,11 @@ export class WebRtcPlayerController { 'LatencyTest', (data: ArrayBuffer) => this.handleLatencyTestResult(data) ); + this.streamMessageController.registerMessageHandler( + MessageDirection.FromStreamer, + 'DataChannelLatencyTest', + (data: ArrayBuffer) => this.handleDataChannelLatencyTestResponse(data) + ) this.streamMessageController.registerMessageHandler( MessageDirection.FromStreamer, 'InitialSettings', @@ -1626,6 +1635,13 @@ export class WebRtcPlayerController { }); } + /** + * Send a Data Channel Latency Test Request to the UE Instance + */ + sendDataChannelLatencyTest(descriptor: DataChannelLatencyTestRequest) { + this.sendDescriptorController.sendDataChannelLatencyTest(descriptor); + } + /** * Send the MinQP encoder setting to the UE Instance. * @param minQP - The lower bound for QP when encoding @@ -1827,6 +1843,23 @@ export class WebRtcPlayerController { this.pixelStreaming._onLatencyTestResult(latencyTestResults); } + /** + * Handles when a Data Channel Latency Test Response is received from the UE Instance + * @param message - Data Channel Latency Test Response + */ + handleDataChannelLatencyTestResponse(message: ArrayBuffer) { + Logger.Log( + Logger.GetStackTrace(), + 'DataChannelReceiveMessageType.dataChannelLatencyResponse', + 6 + ); + const responseAsString = new TextDecoder('utf-16').decode( + message.slice(1) + ); + const latencyTestResponse: DataChannelLatencyTestResponse = JSON.parse(responseAsString); + this.pixelStreaming._onDataChannelLatencyTestResponse(latencyTestResponse); + } + /** * Handles when the Encoder and Web RTC Settings are received from the UE Instance * @param message - Initial Encoder and Web RTC Settings diff --git a/Frontend/ui-library/src/Application/Application.ts b/Frontend/ui-library/src/Application/Application.ts index dfaaa9cd..d97d0e9a 100644 --- a/Frontend/ui-library/src/Application/Application.ts +++ b/Frontend/ui-library/src/Application/Application.ts @@ -31,6 +31,9 @@ import { UIElementConfig } from '../UI/UIConfigurationTypes' import { FullScreenIconBase, FullScreenIconExternal } from '../UI/FullscreenIcon'; +import { + DataChannelLatencyTestResult +} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/types/DataChannel/DataChannelLatencyTestResults"; /** @@ -356,6 +359,11 @@ export class Application { ({ data: { latencyTimings } }) => this.onLatencyTestResults(latencyTimings) ); + this.stream.addEventListener( + 'dataChannelLatencyTestResult', + ({data: { result } }) => + this.onDataChannelLatencyTestResults(result) + ) this.stream.addEventListener( 'streamerListMessage', ({ data: { messageStreamerList, autoSelectedStreamerId } }) => @@ -569,10 +577,8 @@ export class Application { `Disconnected: ${eventString}
Click To Restart
` ); } - // disable starting a latency check - this.statsPanel.latencyTest.latencyTestButton.onclick = () => { - // do nothing - }; + // disable starting a latency checks + this.statsPanel.onDisconnect(); } /** @@ -619,11 +625,7 @@ export class Application { if (!this.stream.config.isFlagEnabled(Flags.AutoPlayVideo)) { this.showPlayOverlay(); } - - // starting a latency check - this.statsPanel.latencyTest.latencyTestButton.onclick = () => { - this.stream.requestLatencyTest(); - }; + this.statsPanel.onVideoInitialized(this.stream); } /** @@ -639,17 +641,7 @@ export class Application { onInitialSettings(settings: InitialSettings) { if (settings.PixelStreamingSettings) { - const disableLatencyTest = - settings.PixelStreamingSettings.DisableLatencyTest; - if (disableLatencyTest) { - this.statsPanel.latencyTest.latencyTestButton.disabled = true; - this.statsPanel.latencyTest.latencyTestButton.title = - 'Disabled by -PixelStreamingDisableLatencyTester=true'; - Logger.Info( - Logger.GetStackTrace(), - '-PixelStreamingDisableLatencyTester=true, requesting latency report from the the browser to UE is disabled.' - ); - } + this.statsPanel.configure(settings.PixelStreamingSettings); } } @@ -662,6 +654,10 @@ export class Application { this.statsPanel.latencyTest.handleTestResult(latencyTimings); } + onDataChannelLatencyTestResults(result: DataChannelLatencyTestResult) { + this.statsPanel.dataChannelLatencyTest.handleTestResult(result); + } + handleStreamerListMessage(messageStreamingList: MessageStreamerList, autoSelectedStreamerId: string | null) { if (autoSelectedStreamerId === null) { if(messageStreamingList.ids.length === 0) { diff --git a/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts b/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts new file mode 100644 index 00000000..5aa7732a --- /dev/null +++ b/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts @@ -0,0 +1,114 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +import { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; +import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; +import { + DataChannelLatencyTestResult +} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/types/DataChannel/DataChannelLatencyTestResults"; + +/** + * DataChannel Latency test UI elements and results handling. + */ +export class DataChannelLatencyTest { + _rootElement: HTMLElement; + _latencyTestButton: HTMLInputElement; + _latencyTestResultsElement: HTMLElement; + + /** + * Get the button containing the stats icon. + */ + public get rootElement(): HTMLElement { + if (!this._rootElement) { + this._rootElement = document.createElement('section'); + this._rootElement.classList.add('settingsContainer'); + + // make heading + const heading = document.createElement('div'); + heading.id = 'dataChannelLatencyTestHeader'; + heading.classList.add('settings-text'); + heading.classList.add('settingsHeader'); + this._rootElement.appendChild(heading); + + const headingText = document.createElement('div'); + headingText.innerHTML = 'Data Channel Latency Test'; + heading.appendChild(headingText); + heading.appendChild(this.latencyTestButton); + + // make test results element + const resultsParentElem = document.createElement('div'); + resultsParentElem.id = 'dataChannelLatencyTestContainer'; + resultsParentElem.classList.add('d-none'); + this._rootElement.appendChild(resultsParentElem); + + resultsParentElem.appendChild(this.latencyTestResultsElement); + } + return this._rootElement; + } + + public get latencyTestResultsElement(): HTMLElement { + if (!this._latencyTestResultsElement) { + this._latencyTestResultsElement = document.createElement('div'); + this._latencyTestResultsElement.id = 'dataChannelLatencyStatsResults'; + this._latencyTestResultsElement.classList.add('StatsResult'); + } + return this._latencyTestResultsElement; + } + + public get latencyTestButton(): HTMLInputElement { + if (!this._latencyTestButton) { + this._latencyTestButton = document.createElement('input'); + this._latencyTestButton.type = 'button'; + this._latencyTestButton.value = 'Run Test'; + this._latencyTestButton.id = 'btn-start-data-channel-latency-test'; + this._latencyTestButton.classList.add('streamTools-button'); + this._latencyTestButton.classList.add('btn-flat'); + } + return this._latencyTestButton; + } + + /** + * Populate the UI based on the latency test's results. + * @param result The latency test results. + */ + public handleTestResult(result: DataChannelLatencyTestResult) { + console.dir(result); + Logger.Log(Logger.GetStackTrace(), result.toString(), 6); + let latencyStatsInnerHTML = ''; + latencyStatsInnerHTML += + '
Data channel RTT (ms): ' + + result.dataChannelRtt + + '
'; + latencyStatsInnerHTML += + '
Player to Streamer path (ms): ' + result.playerToStreamerTime + '
'; + latencyStatsInnerHTML += + '
Streamer to Player path (ms): ' + + result.streamerToPlayerTime + + '
'; + this.latencyTestResultsElement.innerHTML = latencyStatsInnerHTML; + //setup button to download the detailed results + let downloadButton: HTMLInputElement = document.createElement('input'); + downloadButton.type = 'button'; + downloadButton.value = 'Download'; + downloadButton.classList.add('streamTools-button'); + downloadButton.classList.add('btn-flat'); + downloadButton.onclick = () => { + let file = new Blob([result.exportLatencyAsCSV()], {type: 'text/plain'}); + let a = document.createElement("a"), + url = URL.createObjectURL(file); + a.href = url; + a.download = "data_channel_latency_test_results.csv"; + document.body.appendChild(a); + a.click(); + setTimeout(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 0); + } + this.latencyTestResultsElement.appendChild(downloadButton); + } + + public handleTestStart() { + this.latencyTestResultsElement.innerHTML = + '
Test in progress
'; + } +} diff --git a/Frontend/ui-library/src/UI/StatsPanel.ts b/Frontend/ui-library/src/UI/StatsPanel.ts index 2c9a5040..d9adb3cf 100644 --- a/Frontend/ui-library/src/UI/StatsPanel.ts +++ b/Frontend/ui-library/src/UI/StatsPanel.ts @@ -1,9 +1,11 @@ // Copyright Epic Games, Inc. All Rights Reserved. import { LatencyTest } from './LatencyTest'; -import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; +import {InitialSettings, Logger, PixelStreaming} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { AggregatedStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { MathUtils } from '../Util/MathUtils'; +import {DataChannelLatencyTest} from "./DataChannelLatencyTest"; +import {PixelStreamingSettings} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/types/DataChannel/InitialSettings"; /** * A stat structure, an id, the stat string, and the element where it is rendered. @@ -26,12 +28,14 @@ export class StatsPanel { _statsResult: HTMLElement; latencyTest: LatencyTest; + dataChannelLatencyTest: DataChannelLatencyTest; /* A map stats we are storing/rendering */ statsMap = new Map(); constructor() { this.latencyTest = new LatencyTest(); + this.dataChannelLatencyTest = new DataChannelLatencyTest(); } /** @@ -91,6 +95,7 @@ export class StatsPanel { statistics.appendChild(this.statisticsContainer); controlStats.appendChild(this.latencyTest.rootElement); + controlStats.appendChild(this.dataChannelLatencyTest.rootElement); } return this._statsContentElement; } @@ -122,6 +127,45 @@ export class StatsPanel { return this._statsCloseButton; } + public onDisconnect(): void { + this.latencyTest.latencyTestButton.onclick = () => { + // do nothing + } + this.dataChannelLatencyTest.latencyTestButton.onclick = () => { + //do nothing + } + } + + public onVideoInitialized(stream: PixelStreaming): void { + // starting a latency check + this.latencyTest.latencyTestButton.onclick = () => { + stream.requestLatencyTest(); + }; + this.dataChannelLatencyTest.latencyTestButton.onclick = () => { + stream.requestDataChannelLatencyTest({ + duration: 10000, + rps: 100, + requestSize: 200, + responseSize: 200 + }); + }; + } + + public configure(settings: PixelStreamingSettings): void { + if (settings.DisableLatencyTest) { + this.latencyTest.latencyTestButton.disabled = true; + this.latencyTest.latencyTestButton.title = + 'Disabled by -PixelStreamingDisableLatencyTester=true'; + this.dataChannelLatencyTest.latencyTestButton.disabled = true; + this.dataChannelLatencyTest.latencyTestButton.title = + 'Disabled by -PixelStreamingDisableLatencyTester=true'; + Logger.Info( + Logger.GetStackTrace(), + '-PixelStreamingDisableLatencyTester=true, requesting latency report from the the browser to UE is disabled.' + ); + } + } + /** * Show stats panel. */ From 2f217331715f554a76f597a684f3a4683f3c206f Mon Sep 17 00:00:00 2001 From: Nazar Rudenko Date: Tue, 18 Jul 2023 14:24:23 +1000 Subject: [PATCH 2/2] Prepare for PR --- .../EpicGames/src/stresstest.ts | 3 +-- .../DataChannelLatencyTestController.ts | 7 ++--- .../DataChannelLatencyTestResults.ts | 6 ----- .../src/PixelStreaming/PixelStreaming.ts | 14 +++++----- Frontend/library/src/Util/EventEmitter.ts | 5 ++-- .../src/UI/DataChannelLatencyTest.ts | 26 ++++++++++++------- Frontend/ui-library/src/UI/StatsPanel.ts | 9 ++++--- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/Frontend/implementations/EpicGames/src/stresstest.ts b/Frontend/implementations/EpicGames/src/stresstest.ts index c95e12dc..e5bece5f 100644 --- a/Frontend/implementations/EpicGames/src/stresstest.ts +++ b/Frontend/implementations/EpicGames/src/stresstest.ts @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -import { Config, Flags, TextParameters, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; +import { Config, Flags, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3'; const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); @@ -157,7 +157,6 @@ export class StressTester { config.setFlagEnabled(Flags.AutoConnect, true); config.setFlagEnabled(Flags.AutoPlayVideo, true); config.setFlagEnabled(Flags.StartVideoMuted, true); - config.setTextSetting(TextParameters.SignallingServerUrl, "ws://127.0.0.1:80/") // Create a Native DOM delegate instance that implements the Delegate interface class const stream = new PixelStreaming(config); diff --git a/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts b/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts index 307aaf11..20b642b3 100644 --- a/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts +++ b/Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts @@ -99,7 +99,10 @@ export class DataChannelLatencyTestController { return; } if (!response) { - console.log("Undefined response from server O^O") + Logger.Error( + Logger.GetStackTrace(), + "Undefined response from server" + ); return; } let record = this.records.get(response.Seq); @@ -124,5 +127,3 @@ export class DataChannelLatencyTestController { } } - - diff --git a/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts b/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts index 73e14e95..22b3b761 100644 --- a/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts +++ b/Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts @@ -65,9 +65,3 @@ export class DataChannelLatencyTestRecord { } } - - - - - - diff --git a/Frontend/library/src/PixelStreaming/PixelStreaming.ts b/Frontend/library/src/PixelStreaming/PixelStreaming.ts index 9e359d5b..463e0615 100644 --- a/Frontend/library/src/PixelStreaming/PixelStreaming.ts +++ b/Frontend/library/src/PixelStreaming/PixelStreaming.ts @@ -9,7 +9,6 @@ import { Logger } from '../Logger/Logger'; import { InitialSettings } from '../DataChannel/InitialSettings'; import { OnScreenKeyboard } from '../UI/OnScreenKeyboard'; import { - DataChannelLatencyTestResponseEvent, DataChannelLatencyTestResultEvent, EventEmitter, InitialSettingsEvent, LatencyTestResultEvent, @@ -26,13 +25,15 @@ import { WebRtcConnectingEvent, WebRtcDisconnectedEvent, WebRtcFailedEvent, - WebRtcSdpEvent + WebRtcSdpEvent, + DataChannelLatencyTestResponseEvent, + DataChannelLatencyTestResultEvent } from '../Util/EventEmitter'; import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive'; import { WebXRController } from '../WebXR/WebXRController'; import { DataChannelLatencyTestConfig, - DataChannelLatencyTestController, DataChannelLatencyTestResultCallback + DataChannelLatencyTestController } from "../DataChannel/DataChannelLatencyTestController"; import { DataChannelLatencyTestResponse, @@ -593,24 +594,21 @@ export class PixelStreaming { /** * Request a data channel latency test. * NOTE: There are plans to refactor all request* functions. Expect changes if you use this! - * @returns */ public requestDataChannelLatencyTest(config: DataChannelLatencyTestConfig) { if (!this._webRtcController.videoPlayer.isVideoReady()) { return false; } if (!this._dataChannelLatencyTestController) { - let that = this; this._dataChannelLatencyTestController = new DataChannelLatencyTestController( this._webRtcController.sendDataChannelLatencyTest.bind(this._webRtcController), (result: DataChannelLatencyTestResult) => { - console.log(that); - that._eventEmitter.dispatchEvent(new DataChannelLatencyTestResultEvent( { result })) + this._eventEmitter.dispatchEvent(new DataChannelLatencyTestResultEvent( { result })) }); this.addEventListener( "dataChannelLatencyTestResponse", ({data: {response} }) => { - that._dataChannelLatencyTestController.receive(response); + this._dataChannelLatencyTestController.receive(response); } ) } diff --git a/Frontend/library/src/Util/EventEmitter.ts b/Frontend/library/src/Util/EventEmitter.ts index fc173d38..9a568369 100644 --- a/Frontend/library/src/Util/EventEmitter.ts +++ b/Frontend/library/src/Util/EventEmitter.ts @@ -371,7 +371,8 @@ export class LatencyTestResultEvent extends Event { } /** - * An event that is emitted when receiving data channel latency test results. + * An event that is emitted when receiving data channel latency test response from server. + * This event is handled by DataChannelLatencyTestController */ export class DataChannelLatencyTestResponseEvent extends Event { readonly type: 'dataChannelLatencyTestResponse'; @@ -386,7 +387,7 @@ export class DataChannelLatencyTestResponseEvent extends Event { } /** - * An event that is emitted when receiving data channel latency test results. + * An event that is emitted when data channel latency test results are ready. */ export class DataChannelLatencyTestResultEvent extends Event { readonly type: 'dataChannelLatencyTestResult'; diff --git a/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts b/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts index 5aa7732a..fad6a24e 100644 --- a/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts +++ b/Frontend/ui-library/src/UI/DataChannelLatencyTest.ts @@ -1,6 +1,5 @@ // Copyright Epic Games, Inc. All Rights Reserved. -import { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'; import { DataChannelLatencyTestResult @@ -71,19 +70,27 @@ export class DataChannelLatencyTest { * @param result The latency test results. */ public handleTestResult(result: DataChannelLatencyTestResult) { - console.dir(result); - Logger.Log(Logger.GetStackTrace(), result.toString(), 6); + Logger.Log( + Logger.GetStackTrace(), + result.toString(), + 6 + ); let latencyStatsInnerHTML = ''; latencyStatsInnerHTML += '
Data channel RTT (ms): ' + result.dataChannelRtt + '
'; - latencyStatsInnerHTML += - '
Player to Streamer path (ms): ' + result.playerToStreamerTime + '
'; - latencyStatsInnerHTML += - '
Streamer to Player path (ms): ' + - result.streamerToPlayerTime + - '
'; + /** + * Separate path time discovery works only when UE and Player clocks have been synchronized. + */ + if (result.playerToStreamerTime >= 0 && result.streamerToPlayerTime >= 0) { + latencyStatsInnerHTML += + '
Player to Streamer path (ms): ' + result.playerToStreamerTime + '
'; + latencyStatsInnerHTML += + '
Streamer to Player path (ms): ' + + result.streamerToPlayerTime + + '
'; + } this.latencyTestResultsElement.innerHTML = latencyStatsInnerHTML; //setup button to download the detailed results let downloadButton: HTMLInputElement = document.createElement('input'); @@ -111,4 +118,5 @@ export class DataChannelLatencyTest { this.latencyTestResultsElement.innerHTML = '
Test in progress
'; } + } diff --git a/Frontend/ui-library/src/UI/StatsPanel.ts b/Frontend/ui-library/src/UI/StatsPanel.ts index d9adb3cf..8fdf9f69 100644 --- a/Frontend/ui-library/src/UI/StatsPanel.ts +++ b/Frontend/ui-library/src/UI/StatsPanel.ts @@ -142,12 +142,15 @@ export class StatsPanel { stream.requestLatencyTest(); }; this.dataChannelLatencyTest.latencyTestButton.onclick = () => { - stream.requestDataChannelLatencyTest({ - duration: 10000, - rps: 100, + let started = stream.requestDataChannelLatencyTest({ + duration: 1000, + rps: 10, requestSize: 200, responseSize: 200 }); + if (started) { + this.dataChannelLatencyTest.handleTestStart(); + } }; }