Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Add: DataChannel Latency Test #311

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions Frontend/library/src/DataChannel/DataChannelLatencyTestController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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<DataChannelLatencyTestSeq, DataChannelLatencyTestRecord>;
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) {
Logger.Error(
Logger.GetStackTrace(),
"Undefined response from server"
);
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) : ""
}
}

}
67 changes: 67 additions & 0 deletions Frontend/library/src/DataChannel/DataChannelLatencyTestResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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<DataChannelLatencyTestSeq, DataChannelLatencyTestRecord>
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;
}

}
41 changes: 41 additions & 0 deletions Frontend/library/src/PixelStreaming/PixelStreaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,20 @@ import {
WebRtcDisconnectedEvent,
WebRtcFailedEvent,
WebRtcSdpEvent,
DataChannelLatencyTestResponseEvent,
DataChannelLatencyTestResultEvent,
PlayerCountEvent
} from '../Util/EventEmitter';
import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive';
import { WebXRController } from '../WebXR/WebXRController';
import {
DataChannelLatencyTestConfig,
DataChannelLatencyTestController
} 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.
Expand All @@ -48,6 +58,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.
Expand Down Expand Up @@ -460,6 +471,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
Expand Down Expand Up @@ -581,6 +598,30 @@ 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!
*/
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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export class StreamMessageController {
byteLength: 0,
structure: []
});
this.toStreamerMessages.add('DataChannelLatencyTest', {
id: 9,
byteLength: 0,
structure: []
});
/*
* Input Messages. Range = 50..89.
*/
Expand Down Expand Up @@ -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);
}

Expand Down
37 changes: 37 additions & 0 deletions Frontend/library/src/Util/EventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -366,6 +370,37 @@ export class LatencyTestResultEvent extends Event {
}
}

/**
* 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';
readonly data: {
/** Latency test result object */
response: DataChannelLatencyTestResponse
};
constructor(data: DataChannelLatencyTestResponseEvent['data']) {
super('dataChannelLatencyTestResponse');
this.data = data;
}
}

/**
* An event that is emitted when data channel latency test results are ready.
*/
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.
*/
Expand Down Expand Up @@ -513,6 +548,8 @@ export type PixelStreamingEvent =
| StatsReceivedEvent
| StreamerListMessageEvent
| LatencyTestResultEvent
| DataChannelLatencyTestResponseEvent
| DataChannelLatencyTestResultEvent
| InitialSettingsEvent
| SettingsChangedEvent
| XrSessionStartedEvent
Expand Down
Loading