@epicgames-ps/lib-pixelstreamingfrontend-ue5.5 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell.json +48 -0
- package/.eslintignore +8 -0
- package/.eslintrc.js +8 -0
- package/.prettierignore +0 -0
- package/.prettierrc.json +6 -0
- package/dist/lib-pixelstreamingfrontend.esm.js +1 -0
- package/dist/lib-pixelstreamingfrontend.js +1 -0
- package/jest.config.js +18 -0
- package/package.json +48 -0
- package/readme.md +15 -0
- package/src/AFK/AFKController.test.ts +162 -0
- package/src/AFK/AFKController.ts +158 -0
- package/src/Config/Config.test.ts +222 -0
- package/src/Config/Config.ts +970 -0
- package/src/Config/SettingBase.ts +65 -0
- package/src/Config/SettingFlag.ts +99 -0
- package/src/Config/SettingNumber.ts +111 -0
- package/src/Config/SettingOption.ts +124 -0
- package/src/Config/SettingText.ts +82 -0
- package/src/DataChannel/DataChannelController.ts +138 -0
- package/src/DataChannel/DataChannelLatencyTestController.ts +129 -0
- package/src/DataChannel/DataChannelLatencyTestResults.ts +67 -0
- package/src/DataChannel/DataChannelSender.ts +59 -0
- package/src/DataChannel/InitialSettings.ts +61 -0
- package/src/DataChannel/LatencyTestResults.ts +76 -0
- package/src/FreezeFrame/FreezeFrame.ts +114 -0
- package/src/FreezeFrame/FreezeFrameController.ts +114 -0
- package/src/Inputs/FakeTouchController.ts +199 -0
- package/src/Inputs/GamepadController.ts +314 -0
- package/src/Inputs/GamepadTypes.ts +10 -0
- package/src/Inputs/HoveringMouseEvents.ts +192 -0
- package/src/Inputs/IMouseEvents.ts +64 -0
- package/src/Inputs/ITouchController.ts +29 -0
- package/src/Inputs/InputClassesFactory.ts +140 -0
- package/src/Inputs/KeyboardController.ts +354 -0
- package/src/Inputs/LockedMouseEvents.ts +287 -0
- package/src/Inputs/MouseButtons.ts +25 -0
- package/src/Inputs/MouseController.ts +362 -0
- package/src/Inputs/SpecialKeyCodes.ts +16 -0
- package/src/Inputs/TouchController.ts +208 -0
- package/src/Inputs/XRGamepadController.ts +126 -0
- package/src/PeerConnectionController/AggregatedStats.ts +311 -0
- package/src/PeerConnectionController/CandidatePairStats.ts +17 -0
- package/src/PeerConnectionController/CandidateStat.ts +13 -0
- package/src/PeerConnectionController/CodecStats.ts +19 -0
- package/src/PeerConnectionController/DataChannelStats.ts +17 -0
- package/src/PeerConnectionController/InboundRTPStats.ts +154 -0
- package/src/PeerConnectionController/InboundTrackStats.ts +34 -0
- package/src/PeerConnectionController/OutBoundRTPStats.ts +26 -0
- package/src/PeerConnectionController/PeerConnectionController.ts +563 -0
- package/src/PeerConnectionController/SessionStats.ts +10 -0
- package/src/PeerConnectionController/StreamStats.ts +11 -0
- package/src/PixelStreaming/PixelStreaming.test.ts +626 -0
- package/src/PixelStreaming/PixelStreaming.ts +851 -0
- package/src/UI/OnScreenKeyboard.ts +97 -0
- package/src/UeInstanceMessage/ResponseController.ts +47 -0
- package/src/UeInstanceMessage/SendMessageController.ts +154 -0
- package/src/UeInstanceMessage/StreamMessageController.ts +233 -0
- package/src/UeInstanceMessage/ToStreamerMessagesController.ts +62 -0
- package/src/Util/CoordinateConverter.ts +289 -0
- package/src/Util/EventEmitter.ts +611 -0
- package/src/Util/EventListenerTracker.ts +29 -0
- package/src/Util/FileUtil.ts +140 -0
- package/src/Util/RTCUtils.ts +41 -0
- package/src/Util/WebGLUtils.ts +49 -0
- package/src/Util/WebXRUtils.ts +25 -0
- package/src/VideoPlayer/StreamController.ts +89 -0
- package/src/VideoPlayer/VideoPlayer.ts +246 -0
- package/src/WebRtcPlayer/WebRtcPlayerController.ts +2158 -0
- package/src/WebXR/WebXRController.ts +319 -0
- package/src/__test__/mockMediaStream.ts +124 -0
- package/src/__test__/mockRTCPeerConnection.ts +347 -0
- package/src/__test__/mockRTCRtpReceiver.ts +22 -0
- package/src/__test__/mockWebSocket.ts +136 -0
- package/src/pixelstreamingfrontend.ts +46 -0
- package/tsconfig.jest.json +8 -0
- package/tsconfig.json +24 -0
- package/types/AFK/AFKController.d.ts +39 -0
- package/types/Config/Config.d.ts +218 -0
- package/types/Config/SettingBase.d.ts +30 -0
- package/types/Config/SettingFlag.d.ts +33 -0
- package/types/Config/SettingNumber.d.ts +45 -0
- package/types/Config/SettingOption.d.ts +43 -0
- package/types/Config/SettingText.d.ts +29 -0
- package/types/DataChannel/DataChannelController.d.ts +59 -0
- package/types/DataChannel/DataChannelLatencyTestController.d.ts +26 -0
- package/types/DataChannel/DataChannelLatencyTestResults.d.ts +46 -0
- package/types/DataChannel/DataChannelSender.d.ts +21 -0
- package/types/DataChannel/InitialSettings.d.ts +44 -0
- package/types/DataChannel/LatencyTestResults.d.ts +31 -0
- package/types/FreezeFrame/FreezeFrame.d.ts +36 -0
- package/types/FreezeFrame/FreezeFrameController.d.ts +37 -0
- package/types/Inputs/FakeTouchController.d.ts +61 -0
- package/types/Inputs/GamepadController.d.ts +85 -0
- package/types/Inputs/GamepadTypes.d.ts +8 -0
- package/types/Inputs/HoveringMouseEvents.d.ts +56 -0
- package/types/Inputs/IMouseEvents.d.ts +53 -0
- package/types/Inputs/ITouchController.d.ts +24 -0
- package/types/Inputs/InputClassesFactory.d.ts +54 -0
- package/types/Inputs/KeyboardController.d.ts +62 -0
- package/types/Inputs/LockedMouseEvents.d.ts +80 -0
- package/types/Inputs/MouseButtons.d.ts +22 -0
- package/types/Inputs/MouseController.d.ts +75 -0
- package/types/Inputs/SpecialKeyCodes.d.ts +14 -0
- package/types/Inputs/TouchController.d.ts +53 -0
- package/types/Inputs/XRGamepadController.d.ts +15 -0
- package/types/PeerConnectionController/AggregatedStats.d.ts +77 -0
- package/types/PeerConnectionController/CandidatePairStats.d.ts +15 -0
- package/types/PeerConnectionController/CandidateStat.d.ts +11 -0
- package/types/PeerConnectionController/CodecStats.d.ts +14 -0
- package/types/PeerConnectionController/DataChannelStats.d.ts +15 -0
- package/types/PeerConnectionController/InboundRTPStats.d.ts +141 -0
- package/types/PeerConnectionController/InboundTrackStats.d.ts +32 -0
- package/types/PeerConnectionController/OutBoundRTPStats.d.ts +23 -0
- package/types/PeerConnectionController/PeerConnectionController.d.ts +132 -0
- package/types/PeerConnectionController/SessionStats.d.ts +8 -0
- package/types/PeerConnectionController/StreamStats.d.ts +9 -0
- package/types/PixelStreaming/PixelStreaming.d.ts +259 -0
- package/types/UI/OnScreenKeyboard.d.ts +31 -0
- package/types/UeInstanceMessage/ResponseController.d.ts +19 -0
- package/types/UeInstanceMessage/SendMessageController.d.ts +18 -0
- package/types/UeInstanceMessage/StreamMessageController.d.ts +29 -0
- package/types/UeInstanceMessage/ToStreamerMessagesController.d.ts +32 -0
- package/types/Util/CoordinateConverter.d.ts +100 -0
- package/types/Util/EventEmitter.d.ts +422 -0
- package/types/Util/EventListenerTracker.d.ts +14 -0
- package/types/Util/FileUtil.d.ts +32 -0
- package/types/Util/RTCUtils.d.ts +8 -0
- package/types/Util/WebGLUtils.d.ts +4 -0
- package/types/Util/WebXRUtils.d.ts +9 -0
- package/types/VideoPlayer/StreamController.d.ts +24 -0
- package/types/VideoPlayer/VideoPlayer.d.ts +78 -0
- package/types/WebRtcPlayer/WebRtcPlayerController.d.ts +377 -0
- package/types/WebXR/WebXRController.d.ts +26 -0
- package/types/pixelstreamingfrontend.d.ts +22 -0
- package/webpack.common.js +35 -0
- package/webpack.dev.js +35 -0
- package/webpack.prod.js +36 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
import { Logger } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility function for populate file information from byte buffers.
|
|
7
|
+
*/
|
|
8
|
+
export class FileUtil {
|
|
9
|
+
/**
|
|
10
|
+
* Processes a files extension when received over data channel
|
|
11
|
+
* @param view - the file extension data
|
|
12
|
+
*/
|
|
13
|
+
static setExtensionFromBytes(view: Uint8Array, file: FileTemplate) {
|
|
14
|
+
// Reset file if we got a file message and we are not "receiving" it yet
|
|
15
|
+
if (!file.receiving) {
|
|
16
|
+
file.mimetype = '';
|
|
17
|
+
file.extension = '';
|
|
18
|
+
file.receiving = true;
|
|
19
|
+
file.valid = false;
|
|
20
|
+
file.size = 0;
|
|
21
|
+
file.data = [];
|
|
22
|
+
file.timestampStart = new Date().getTime();
|
|
23
|
+
Logger.Log(
|
|
24
|
+
Logger.GetStackTrace(),
|
|
25
|
+
'Received first chunk of file',
|
|
26
|
+
6
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extensionAsString = new TextDecoder('utf-16').decode(
|
|
31
|
+
view.slice(1)
|
|
32
|
+
);
|
|
33
|
+
Logger.Log(Logger.GetStackTrace(), extensionAsString, 6);
|
|
34
|
+
file.extension = extensionAsString;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Processes a files mime type when received over data channel
|
|
39
|
+
* @param view - the file mime type data
|
|
40
|
+
*/
|
|
41
|
+
static setMimeTypeFromBytes(view: Uint8Array, file: FileTemplate) {
|
|
42
|
+
// Reset file if we got a file message and we are not "receiving" it yet
|
|
43
|
+
if (!file.receiving) {
|
|
44
|
+
file.mimetype = '';
|
|
45
|
+
file.extension = '';
|
|
46
|
+
file.receiving = true;
|
|
47
|
+
file.valid = false;
|
|
48
|
+
file.size = 0;
|
|
49
|
+
file.data = [];
|
|
50
|
+
file.timestampStart = new Date().getTime();
|
|
51
|
+
Logger.Log(
|
|
52
|
+
Logger.GetStackTrace(),
|
|
53
|
+
'Received first chunk of file',
|
|
54
|
+
6
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const mimeAsString = new TextDecoder('utf-16').decode(view.slice(1));
|
|
59
|
+
Logger.Log(Logger.GetStackTrace(), mimeAsString, 6);
|
|
60
|
+
file.mimetype = mimeAsString;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Processes a files contents when received over data channel
|
|
65
|
+
* @param view - the file contents data
|
|
66
|
+
*/
|
|
67
|
+
static setContentsFromBytes(view: Uint8Array, file: FileTemplate) {
|
|
68
|
+
// If we haven't received the initial setup instructions, return
|
|
69
|
+
if (!file.receiving) return;
|
|
70
|
+
|
|
71
|
+
// Extract the total size of the file (across all chunks)
|
|
72
|
+
file.size = Math.ceil(
|
|
73
|
+
new DataView(view.slice(1, 5).buffer).getInt32(0, true) /
|
|
74
|
+
16379 /* The maximum number of payload bits per message*/
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Get the file part of the payload
|
|
78
|
+
const fileBytes = view.slice(1 + 4);
|
|
79
|
+
|
|
80
|
+
// Append to existing data that holds the file
|
|
81
|
+
file.data.push(fileBytes);
|
|
82
|
+
|
|
83
|
+
// Uncomment for debug
|
|
84
|
+
Logger.Log(
|
|
85
|
+
Logger.GetStackTrace(),
|
|
86
|
+
`Received file chunk: ${file.data.length}/${file.size}`,
|
|
87
|
+
6
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (file.data.length === file.size) {
|
|
91
|
+
file.receiving = false;
|
|
92
|
+
file.valid = true;
|
|
93
|
+
Logger.Log(Logger.GetStackTrace(), 'Received complete file', 6);
|
|
94
|
+
const transferDuration = new Date().getTime() - file.timestampStart;
|
|
95
|
+
const transferBitrate = Math.round(
|
|
96
|
+
(file.size * 16 * 1024) / transferDuration
|
|
97
|
+
);
|
|
98
|
+
Logger.Log(
|
|
99
|
+
Logger.GetStackTrace(),
|
|
100
|
+
`Average transfer bitrate: ${transferBitrate}kb/s over ${
|
|
101
|
+
transferDuration / 1000
|
|
102
|
+
} seconds`,
|
|
103
|
+
6
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// File reconstruction
|
|
107
|
+
/**
|
|
108
|
+
* Example code to reconstruct the file
|
|
109
|
+
*
|
|
110
|
+
* This code reconstructs the received data into the original file based on the mime type and extension provided and then downloads the reconstructed file
|
|
111
|
+
*/
|
|
112
|
+
const received = new Blob(file.data, { type: file.mimetype });
|
|
113
|
+
const a = document.createElement('a');
|
|
114
|
+
a.setAttribute('href', URL.createObjectURL(received));
|
|
115
|
+
a.setAttribute('download', `transfer.${file.extension}`);
|
|
116
|
+
document.body.append(a);
|
|
117
|
+
// if you are so inclined to make it auto-download, do something like: a.click();
|
|
118
|
+
a.remove();
|
|
119
|
+
} else if (file.data.length > file.size) {
|
|
120
|
+
file.receiving = false;
|
|
121
|
+
Logger.Error(
|
|
122
|
+
Logger.GetStackTrace(),
|
|
123
|
+
`Received bigger file than advertised: ${file.data.length}/${file.size}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A class that represents a template for a downloaded file
|
|
131
|
+
*/
|
|
132
|
+
export class FileTemplate {
|
|
133
|
+
mimetype = '';
|
|
134
|
+
extension = '';
|
|
135
|
+
receiving = false;
|
|
136
|
+
size = 0;
|
|
137
|
+
data: Array<Uint8Array> = [];
|
|
138
|
+
valid = false;
|
|
139
|
+
timestampStart: number;
|
|
140
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class RTCUtils {
|
|
2
|
+
static isVideoTransciever(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
3
|
+
return this.canTransceiverReceiveVideo(transceiver) || this.canTransceiverSendVideo(transceiver);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
static canTransceiverReceiveVideo(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
7
|
+
return !!transceiver &&
|
|
8
|
+
(transceiver.direction === 'sendrecv' || transceiver.direction === 'recvonly') &&
|
|
9
|
+
transceiver.receiver &&
|
|
10
|
+
transceiver.receiver.track &&
|
|
11
|
+
transceiver.receiver.track.kind === 'video';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static canTransceiverSendVideo(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
15
|
+
return !!transceiver &&
|
|
16
|
+
(transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly') &&
|
|
17
|
+
transceiver.sender &&
|
|
18
|
+
transceiver.sender.track &&
|
|
19
|
+
transceiver.sender.track.kind === 'video';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static isAudioTransciever(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
23
|
+
return this.canTransceiverReceiveAudio(transceiver) || this.canTransceiverSendAudio(transceiver);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static canTransceiverReceiveAudio(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
27
|
+
return !!transceiver &&
|
|
28
|
+
(transceiver.direction === 'sendrecv' || transceiver.direction === 'recvonly') &&
|
|
29
|
+
transceiver.receiver &&
|
|
30
|
+
transceiver.receiver.track &&
|
|
31
|
+
transceiver.receiver.track.kind === 'audio';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static canTransceiverSendAudio(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
|
35
|
+
return !!transceiver &&
|
|
36
|
+
(transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly') &&
|
|
37
|
+
transceiver.sender &&
|
|
38
|
+
transceiver.sender.track &&
|
|
39
|
+
transceiver.sender.track.kind === 'audio';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
export class WebGLUtils {
|
|
4
|
+
static vertexShader(): string {
|
|
5
|
+
return `
|
|
6
|
+
attribute vec2 a_position;
|
|
7
|
+
attribute vec2 a_texCoord;
|
|
8
|
+
|
|
9
|
+
// input
|
|
10
|
+
uniform vec2 u_resolution;
|
|
11
|
+
uniform vec4 u_offset;
|
|
12
|
+
|
|
13
|
+
//
|
|
14
|
+
varying vec2 v_texCoord;
|
|
15
|
+
|
|
16
|
+
void main() {
|
|
17
|
+
// convert the rectangle from pixels to 0.0 to 1.0
|
|
18
|
+
vec2 zeroToOne = a_position / u_resolution;
|
|
19
|
+
|
|
20
|
+
// convert from 0->1 to 0->2
|
|
21
|
+
vec2 zeroToTwo = zeroToOne * 2.0;
|
|
22
|
+
|
|
23
|
+
// convert from 0->2 to -1->+1 (clipspace)
|
|
24
|
+
vec2 clipSpace = zeroToTwo - 1.0;
|
|
25
|
+
|
|
26
|
+
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
|
27
|
+
// pass the texCoord to the fragment shader
|
|
28
|
+
// The GPU will interpolate this value between points.
|
|
29
|
+
v_texCoord = (a_texCoord * u_offset.xy) + u_offset.zw;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static fragmentShader(): string {
|
|
35
|
+
return `
|
|
36
|
+
precision mediump float;
|
|
37
|
+
|
|
38
|
+
// our texture
|
|
39
|
+
uniform sampler2D u_image;
|
|
40
|
+
|
|
41
|
+
// the texCoords passed in from the vertex shader.
|
|
42
|
+
varying vec2 v_texCoord;
|
|
43
|
+
|
|
44
|
+
void main() {
|
|
45
|
+
gl_FragColor = texture2D(u_image, v_texCoord);
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
export class WebXRUtils {
|
|
4
|
+
/**
|
|
5
|
+
* Deep copies a gamepad's values by first converting it to a JSON object and then back to a gamepad
|
|
6
|
+
*
|
|
7
|
+
* @param gamepad the original gamepad
|
|
8
|
+
* @returns a new gamepad object, populated with the original gamepads values
|
|
9
|
+
*/
|
|
10
|
+
static deepCopyGamepad(gamepad: Gamepad): Gamepad {
|
|
11
|
+
return JSON.parse(
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
buttons: gamepad.buttons.map((b) =>
|
|
14
|
+
JSON.parse(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
pressed: b.pressed,
|
|
17
|
+
touched: b.touched
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
axes: gamepad.axes
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
import { MouseController } from '../Inputs/MouseController';
|
|
4
|
+
import { Logger } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
5
|
+
import { VideoPlayer } from './VideoPlayer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Video Player Controller handles the creation of the video HTML element and all handlers
|
|
9
|
+
*/
|
|
10
|
+
export class StreamController {
|
|
11
|
+
videoElementProvider: VideoPlayer;
|
|
12
|
+
audioElement: HTMLAudioElement;
|
|
13
|
+
mouseController: MouseController;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param videoElementProvider Video Player instance
|
|
17
|
+
*/
|
|
18
|
+
constructor(videoElementProvider: VideoPlayer) {
|
|
19
|
+
this.videoElementProvider = videoElementProvider;
|
|
20
|
+
this.audioElement = document.createElement('Audio') as HTMLAudioElement;
|
|
21
|
+
this.videoElementProvider.setAudioElement(this.audioElement);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handles when the Peer connection has a track event
|
|
26
|
+
* @param rtcTrackEvent - RTC Track Event
|
|
27
|
+
*/
|
|
28
|
+
handleOnTrack(rtcTrackEvent: RTCTrackEvent) {
|
|
29
|
+
Logger.Log(
|
|
30
|
+
Logger.GetStackTrace(),
|
|
31
|
+
'handleOnTrack ' + JSON.stringify(rtcTrackEvent.streams),
|
|
32
|
+
6
|
|
33
|
+
);
|
|
34
|
+
const videoElement = this.videoElementProvider.getVideoElement();
|
|
35
|
+
|
|
36
|
+
if (rtcTrackEvent.track) {
|
|
37
|
+
Logger.Log(
|
|
38
|
+
Logger.GetStackTrace(),
|
|
39
|
+
'Got track - ' +
|
|
40
|
+
rtcTrackEvent.track.kind +
|
|
41
|
+
' id=' +
|
|
42
|
+
rtcTrackEvent.track.id +
|
|
43
|
+
' readyState=' +
|
|
44
|
+
rtcTrackEvent.track.readyState,
|
|
45
|
+
6
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (rtcTrackEvent.track.kind == 'audio') {
|
|
50
|
+
this.CreateAudioTrack(rtcTrackEvent.streams[0]);
|
|
51
|
+
return;
|
|
52
|
+
} else if (
|
|
53
|
+
rtcTrackEvent.track.kind == 'video' &&
|
|
54
|
+
videoElement.srcObject !== rtcTrackEvent.streams[0]
|
|
55
|
+
) {
|
|
56
|
+
videoElement.srcObject = rtcTrackEvent.streams[0];
|
|
57
|
+
Logger.Log(
|
|
58
|
+
Logger.GetStackTrace(),
|
|
59
|
+
'Set video source from video track ontrack.'
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates the audio device when receiving an RTCTrackEvent with the kind of "audio"
|
|
67
|
+
* @param audioMediaStream - Audio Media stream track
|
|
68
|
+
*/
|
|
69
|
+
CreateAudioTrack(audioMediaStream: MediaStream) {
|
|
70
|
+
const videoElement = this.videoElementProvider.getVideoElement();
|
|
71
|
+
|
|
72
|
+
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
|
73
|
+
if (videoElement.srcObject == audioMediaStream) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// video element has some other media stream that is not associated with this audio track
|
|
77
|
+
else if (
|
|
78
|
+
videoElement.srcObject &&
|
|
79
|
+
videoElement.srcObject !== audioMediaStream
|
|
80
|
+
) {
|
|
81
|
+
// create a new audio element
|
|
82
|
+
this.audioElement.srcObject = audioMediaStream;
|
|
83
|
+
Logger.Log(
|
|
84
|
+
Logger.GetStackTrace(),
|
|
85
|
+
'Created new audio element to play separate audio stream.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
import { Config, Flags } from '../Config/Config';
|
|
4
|
+
import { Logger } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extra types for the HTMLElement
|
|
8
|
+
*/
|
|
9
|
+
declare global {
|
|
10
|
+
interface HTMLElement {
|
|
11
|
+
mozRequestPointerLock?(): void;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The video player html element
|
|
17
|
+
*/
|
|
18
|
+
export class VideoPlayer {
|
|
19
|
+
private config: Config;
|
|
20
|
+
private videoElement: HTMLVideoElement;
|
|
21
|
+
private audioElement?: HTMLAudioElement;
|
|
22
|
+
private orientationChangeTimeout: number;
|
|
23
|
+
private lastTimeResized = new Date().getTime();
|
|
24
|
+
|
|
25
|
+
onMatchViewportResolutionCallback: (width: number, height: number) => void;
|
|
26
|
+
onResizePlayerCallback: () => void;
|
|
27
|
+
resizeTimeoutHandle: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param videoElementParent the html div the the video player will be injected into
|
|
31
|
+
* @param config the applications configuration. We're interested in the startVideoMuted flag
|
|
32
|
+
*/
|
|
33
|
+
constructor(videoElementParent: HTMLElement, config: Config) {
|
|
34
|
+
this.videoElement = document.createElement('video');
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.videoElement.id = 'streamingVideo';
|
|
37
|
+
this.videoElement.disablePictureInPicture = true;
|
|
38
|
+
this.videoElement.playsInline = true;
|
|
39
|
+
this.videoElement.style.width = '100%';
|
|
40
|
+
this.videoElement.style.height = '100%';
|
|
41
|
+
this.videoElement.style.position = 'absolute';
|
|
42
|
+
this.videoElement.style.pointerEvents = 'all';
|
|
43
|
+
videoElementParent.appendChild(this.videoElement);
|
|
44
|
+
|
|
45
|
+
this.onResizePlayerCallback = () => {
|
|
46
|
+
console.log(
|
|
47
|
+
'Resolution changed, restyling player, did you forget to override this function?'
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
this.onMatchViewportResolutionCallback = () => {
|
|
51
|
+
console.log(
|
|
52
|
+
'Resolution changed and match viewport resolution is turned on, did you forget to override this function?'
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// set play for video (and audio)
|
|
57
|
+
this.videoElement.onclick = () => {
|
|
58
|
+
if (this.audioElement != undefined && this.audioElement.paused) {
|
|
59
|
+
this.audioElement.play();
|
|
60
|
+
}
|
|
61
|
+
if (this.videoElement.paused) {
|
|
62
|
+
this.videoElement.play();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.videoElement.onloadedmetadata = () => {
|
|
67
|
+
this.onVideoInitialized();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// set resize events to the windows if it is resized or its orientation is changed
|
|
71
|
+
window.addEventListener('resize', () => this.resizePlayerStyle(), true);
|
|
72
|
+
window.addEventListener('orientationchange', () =>
|
|
73
|
+
this.onOrientationChange()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public setAudioElement(audioElement: HTMLAudioElement) : void {
|
|
78
|
+
this.audioElement = audioElement;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sets up the video element with any application config and plays the video element.
|
|
83
|
+
* @returns A promise for if playing the video was successful or not.
|
|
84
|
+
*/
|
|
85
|
+
play(): Promise<void> {
|
|
86
|
+
this.videoElement.muted = this.config.isFlagEnabled(
|
|
87
|
+
Flags.StartVideoMuted
|
|
88
|
+
);
|
|
89
|
+
this.videoElement.autoplay = this.config.isFlagEnabled(
|
|
90
|
+
Flags.AutoPlayVideo
|
|
91
|
+
);
|
|
92
|
+
return this.videoElement.play();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns True if the video element is paused.
|
|
97
|
+
*/
|
|
98
|
+
isPaused(): boolean {
|
|
99
|
+
return this.videoElement.paused;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @returns - whether the video element is playing.
|
|
104
|
+
*/
|
|
105
|
+
isVideoReady(): boolean {
|
|
106
|
+
return (
|
|
107
|
+
this.videoElement.readyState !== undefined &&
|
|
108
|
+
this.videoElement.readyState > 0
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @returns True if the video element has a valid video source (srcObject).
|
|
114
|
+
*/
|
|
115
|
+
hasVideoSource(): boolean {
|
|
116
|
+
return (
|
|
117
|
+
this.videoElement.srcObject !== undefined &&
|
|
118
|
+
this.videoElement.srcObject !== null
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the current context of the html video element
|
|
124
|
+
* @returns - the current context of the video element
|
|
125
|
+
*/
|
|
126
|
+
getVideoElement(): HTMLVideoElement {
|
|
127
|
+
return this.videoElement;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the current context of the html video elements parent
|
|
132
|
+
* @returns - the current context of the video elements parent
|
|
133
|
+
*/
|
|
134
|
+
getVideoParentElement(): HTMLElement {
|
|
135
|
+
return this.videoElement.parentElement;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set the Video Elements src object tracks to enable
|
|
140
|
+
* @param enabled - Enable Tracks on the Src Object
|
|
141
|
+
*/
|
|
142
|
+
setVideoEnabled(enabled: boolean) {
|
|
143
|
+
// this is a temporary hack until type scripts video element is updated to reflect the need for tracks on a html video element
|
|
144
|
+
const videoElement = this.videoElement;
|
|
145
|
+
(<MediaStream>videoElement.srcObject)
|
|
146
|
+
.getTracks()
|
|
147
|
+
.forEach((track: MediaStreamTrack) => (track.enabled = enabled));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* An override for when the video has been initialized with a srcObject
|
|
152
|
+
*/
|
|
153
|
+
onVideoInitialized() {
|
|
154
|
+
// Default Functionality: Do Nothing
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* On the orientation change of a window clear the timeout
|
|
159
|
+
*/
|
|
160
|
+
onOrientationChange() {
|
|
161
|
+
clearTimeout(this.orientationChangeTimeout);
|
|
162
|
+
this.orientationChangeTimeout = window.setTimeout(() => {
|
|
163
|
+
this.resizePlayerStyle();
|
|
164
|
+
}, 500);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resizes the player style based on the window height and width
|
|
169
|
+
* @returns - nil if requirements are satisfied
|
|
170
|
+
*/
|
|
171
|
+
resizePlayerStyle() {
|
|
172
|
+
const videoElementParent = this.getVideoParentElement();
|
|
173
|
+
|
|
174
|
+
if (!videoElementParent) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.updateVideoStreamSize();
|
|
179
|
+
|
|
180
|
+
if (videoElementParent.classList.contains('fixed-size')) {
|
|
181
|
+
this.onResizePlayerCallback();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// controls for resizing the player
|
|
186
|
+
this.resizePlayerStyleToFillParentElement();
|
|
187
|
+
this.onResizePlayerCallback();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resizes the player element to fill the parent element
|
|
192
|
+
*/
|
|
193
|
+
resizePlayerStyleToFillParentElement() {
|
|
194
|
+
const videoElementParent = this.getVideoParentElement();
|
|
195
|
+
|
|
196
|
+
//Video is not initialized yet so set videoElementParent to size of parent element
|
|
197
|
+
const styleWidth = '100%';
|
|
198
|
+
const styleHeight = '100%';
|
|
199
|
+
const styleTop = 0;
|
|
200
|
+
const styleLeft = 0;
|
|
201
|
+
videoElementParent.setAttribute(
|
|
202
|
+
'style',
|
|
203
|
+
'top: ' +
|
|
204
|
+
styleTop +
|
|
205
|
+
'px; left: ' +
|
|
206
|
+
styleLeft +
|
|
207
|
+
'px; width: ' +
|
|
208
|
+
styleWidth +
|
|
209
|
+
'; height: ' +
|
|
210
|
+
styleHeight +
|
|
211
|
+
'; cursor: default;'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateVideoStreamSize() {
|
|
216
|
+
if (!this.config.isFlagEnabled(Flags.MatchViewportResolution)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const now = new Date().getTime();
|
|
221
|
+
if (now - this.lastTimeResized > 300) {
|
|
222
|
+
const videoElementParent = this.getVideoParentElement();
|
|
223
|
+
if (!videoElementParent) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.onMatchViewportResolutionCallback(
|
|
228
|
+
videoElementParent.clientWidth,
|
|
229
|
+
videoElementParent.clientHeight
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
this.lastTimeResized = new Date().getTime();
|
|
233
|
+
} else {
|
|
234
|
+
Logger.Log(
|
|
235
|
+
Logger.GetStackTrace(),
|
|
236
|
+
'Resizing too often - skipping',
|
|
237
|
+
6
|
|
238
|
+
);
|
|
239
|
+
clearTimeout(this.resizeTimeoutHandle);
|
|
240
|
+
this.resizeTimeoutHandle = window.setTimeout(
|
|
241
|
+
() => this.updateVideoStreamSize(),
|
|
242
|
+
100
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|