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

Add: DataChannel latency test #315

Merged
merged 7 commits into from
Sep 12, 2023
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,11 +26,21 @@ import {
WebRtcDisconnectedEvent,
WebRtcFailedEvent,
WebRtcSdpEvent,
DataChannelLatencyTestResponseEvent,
DataChannelLatencyTestResultEvent,
PlayerCountEvent
} 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";

export interface PixelStreamingOverrides {
/** The DOM elment where Pixel Streaming video and user input event handlers are attached to.
Expand All @@ -49,6 +59,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 @@ -461,6 +472,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 @@ -582,6 +599,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 @@ -71,6 +71,10 @@ export class StreamMessageController {
id: 8,
structure: []
});
this.toStreamerMessages.set('DataChannelLatencyTest', {
id: 9,
structure: []
});
/*
* Input Messages. Range = 50..89.
*/
Expand Down Expand Up @@ -189,6 +193,7 @@ export class StreamMessageController {
this.fromStreamerMessages.set(11, 'TestEcho');
this.fromStreamerMessages.set(12, 'InputControlOwnership');
this.fromStreamerMessages.set(13, 'GamepadResponse');
this.fromStreamerMessages.set(14, 'DataChannelLatencyTest');
this.fromStreamerMessages.set(255, 'Protocol');
}

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