Compare commits
105 Commits
UE5.4-0.0.
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
cb741add4f | |
|
|
88d82e1112 | |
|
|
caddb00ed3 | |
|
|
f0fd72768c | |
|
|
7cbe5227dd | |
|
|
672397e6a8 | |
|
|
fa75e9d8c5 | |
|
|
4969548535 | |
|
|
e91781b6fa | |
|
|
eea0faff60 | |
|
|
664d40b801 | |
|
|
5cf2735c5f | |
|
|
2544b717ae | |
|
|
b2cdcf10f3 | |
|
|
e44a9ff734 | |
|
|
8734e5bb56 | |
|
|
f272cefb93 | |
|
|
d61b989bff | |
|
|
4aded6b752 | |
|
|
dee225c03a | |
|
|
ce2a493114 | |
|
|
44367dfa8a | |
|
|
23e562dfdf | |
|
|
20987e006e | |
|
|
a0f9b34dde | |
|
|
f4380ab2c0 | |
|
|
556ecc5df3 | |
|
|
cb58c2c068 | |
|
|
6d59c44126 | |
|
|
04c2bc5e70 | |
|
|
d355d6685a | |
|
|
1b00462e1c | |
|
|
ce299ebd37 | |
|
|
7315292918 | |
|
|
8b838f48fe | |
|
|
87b9c9aaed | |
|
|
898504d3a6 | |
|
|
6ac47fffcd | |
|
|
606298d40d | |
|
|
d8a4e118a6 | |
|
|
eb7d4c6262 | |
|
|
4ebc522437 | |
|
|
c6ad00f2ca | |
|
|
c05f3b18ce | |
|
|
db6273513a | |
|
|
e5f2ff8dd1 | |
|
|
2c5628124a | |
|
|
05821c7656 | |
|
|
92de29dbc9 | |
|
|
28e7c0c1f3 | |
|
|
649d6b2dcb | |
|
|
a3b76a3acc | |
|
|
5a734f2204 | |
|
|
6fe29baa95 | |
|
|
5e71c6ff97 | |
|
|
6462905b76 | |
|
|
7eba788faa | |
|
|
e9b860428d | |
|
|
4fd2948d99 | |
|
|
78640671d4 | |
|
|
cfbe5a0f86 | |
|
|
ae4b5b7a6d | |
|
|
a8de900166 | |
|
|
12a08e1624 | |
|
|
a9b4f31a7d | |
|
|
d8ac1b62d8 | |
|
|
e66dacf750 | |
|
|
8080d2aebb | |
|
|
4fd080675e | |
|
|
bbba5e0a67 | |
|
|
c37095d229 | |
|
|
f94039dac8 | |
|
|
76bdc5e683 | |
|
|
0f5e2d9e12 | |
|
|
1aad4a6e58 | |
|
|
65ebcfe03e | |
|
|
d5efa38acf | |
|
|
7d3a6b64c4 | |
|
|
32d6e2b0b1 | |
|
|
7f54018320 | |
|
|
5bea564755 | |
|
|
7320398ef3 | |
|
|
082284b6e6 | |
|
|
a7afbc3d0c | |
|
|
ffcf7b4b3a | |
|
|
bbf812b9c2 | |
|
|
8847ee7440 | |
|
|
72ebc33558 | |
|
|
edef2915ee | |
|
|
1e051866c1 | |
|
|
458e8aa9cb | |
|
|
ea7b1ea030 | |
|
|
9cb4d2c572 | |
|
|
5cffec3b42 | |
|
|
ac6450fbbd | |
|
|
449079871c | |
|
|
3af6bff71b | |
|
|
84c8b96a66 | |
|
|
44d6c4c829 | |
|
|
912265a6f8 | |
|
|
537acc2476 | |
|
|
c78be0404c | |
|
|
41e7029e0c | |
|
|
e90a685096 | |
|
|
ea74d91658 |
|
|
@ -7,10 +7,10 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**UE Version: **
|
||||
**UE Version:**
|
||||
E.g. UE 5.1.1
|
||||
|
||||
**Frontend Version: **
|
||||
**Frontend Version:**
|
||||
E.g. UE5.3-0.3.0
|
||||
|
||||
**Problem component**
|
||||
|
|
|
|||
|
|
@ -31,3 +31,12 @@ jobs:
|
|||
tags: 'ghcr.io/epicgames/pixel-streaming-signalling-server:5.4'
|
||||
push: true
|
||||
file: SignallingWebServer/Dockerfile
|
||||
-
|
||||
name: Build and push the SFU container image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
tags: 'ghcr.io/epicgames/pixel-streaming-sfu:5.4'
|
||||
push: true
|
||||
file: SFU/Dockerfile
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
name: Run signalling tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ['signalling_tester']
|
||||
paths: ['SignallingWebServer/**']
|
||||
pull_request:
|
||||
branches: ['signalling_tester']
|
||||
paths: ['SS_Test/**']
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: "Checkout source code"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Run signalling server
|
||||
working-directory: ./SignallingWebServer
|
||||
run: ./platform_scripts/bash/run_local.sh &
|
||||
|
||||
- name: Install library deps
|
||||
working-directory: ./SS_Test
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend lib tests
|
||||
working-directory: ./SS_Test
|
||||
run: npm run start
|
||||
|
|
@ -25,7 +25,7 @@ If you have encountered a bug, have suggestions for our documentation or infrast
|
|||
|
||||
If you have a solution to a problem you've encountered or to any other open issue, you can create a pull request with your changes.
|
||||
1. Fork the repo and branch off of the `main` branch in your fork.
|
||||
2. Implement your changes in your branch.
|
||||
2. Implement your changes in your branch and make sure your commits are Verified! Signed commits are required for merging! [Github Signing Documentation](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
3. Do as much testing as you can, and when you are happy, tidy up your work and commit the update.
|
||||
4. Create a [pull request](https://github.com/EpicGames/PixelStreamingInfrastructure/pulls) and don't forget to link it to an issue if there's an existing one. Add as much information as possible to your PR: describe the problem your change solves, mention any testing you have done and attach any relevant documents and screenshots.
|
||||
5. If your are contributing a PR for a new feature, we strongly encourage you to accompany it with relevant documentation and a detailed description of the tests you have done. PRs that don't have this information may take a long time to be addressed, since our team will have to do the testing.
|
||||
|
|
@ -65,4 +65,4 @@ Documentation should be broken up into separate `.md` files per directory, ideal
|
|||
|
||||
## Legal
|
||||
|
||||
© 2004-2023, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ Welcome to the general documentation page for Pixel Streaming. This page serves
|
|||
|
||||
|
||||
## Legal
|
||||
© 2004-2023, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
|
|
|
|||
|
|
@ -87,4 +87,4 @@ The [/library](/Frontend/library) project has unit tests that test the Pixel Str
|
|||
|
||||
## Legal
|
||||
|
||||
Copyright © 2023, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.
|
||||
Copyright © 2024, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# Angular Implementations
|
||||
|
||||
Here are a selection of community contributed implementations of Angular frontends.
|
||||
|
||||
- [cheikhnadiouf](https://github.com/cheikhnadiouf)'s implementation - [LINK](https://github.com/cheikhnadiouf/PixelStreamingInfrastructure/tree/AngularImplementations/Frontend/implementations/angular)
|
||||
|
||||
If you wish to contribute your own example frontend, please open an issue/PR.
|
||||
|
||||
**Disclaimer: We do not warrant these for any fitness of purpose, nor do we maintain them.**
|
||||
|
|
@ -1576,9 +1576,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1390,8 +1390,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4898,8 +4899,9 @@
|
|||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"dev": true
|
||||
},
|
||||
"forwarded": {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"watch": "npx webpack --watch",
|
||||
"serve": "webpack serve --config webpack.dev.js",
|
||||
"serve-prod": "webpack serve --config webpack.prod.js",
|
||||
"build-all": "npm link ../../library ../../ui-library && cd ../../library && npm run build && cd ../ui-library && npm run build-all && cd ../implementations/typescript && npm run build",
|
||||
"build-dev-all": "npm link ../../library ../../ui-library && cd ../../library && npm run build-dev && cd ../ui-library && npm run build-dev-all && cd ../implementations/typescript && npm run build-dev"
|
||||
"build-all": "cd ../../library && npm run build && cd ../ui-library && npm run build-all && cd ../implementations/typescript && npm link ../../library ../../ui-library && npm run build",
|
||||
"build-dev-all": "cd ../../library && npm run build-dev && cd ../ui-library && npm run build-dev-all && cd ../implementations/typescript && npm link ../../library ../../ui-library && npm run build-dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-cli": "^5.0.1",
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%; height: 100%">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Michroma&family=Montserrat:wght@600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Required - the Login style sheet -->
|
||||
<link rel="stylesheet" type="text/css" href="./assets/css/login.css">
|
||||
|
||||
<!-- Optional: set some favicons -->
|
||||
<link id="favPng" rel="icon" type="image/png" href="./assets/images/favicon.png">
|
||||
|
||||
<!-- Optional: set a title for your page -->
|
||||
<title>Pixel Streaming Login</title>
|
||||
</head>
|
||||
|
||||
<body style="width: 100vw; height: 100vh; min-height: -webkit-fill-available; font-family: 'Montserrat'; margin: 0px">
|
||||
<form action="/login" method="post">
|
||||
<div class="entry">
|
||||
<input type="text" id="username" name="username" placeholder="Username"
|
||||
autocomplete="username">
|
||||
</div>
|
||||
<div class="entry">
|
||||
<input type="password" id="password" name="password" placeholder="Password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<div class="entry button">
|
||||
<button type="submit">LOGIN</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
|
@ -6,6 +6,11 @@ const PixelStreamingApplicationStyles =
|
|||
new PixelStreamingApplicationStyle();
|
||||
PixelStreamingApplicationStyles.applyStyleSheet();
|
||||
|
||||
// expose the pixel streaming object for hooking into. tests etc.
|
||||
declare global {
|
||||
interface Window { pixelStreaming: PixelStreaming; }
|
||||
}
|
||||
|
||||
document.body.onload = function() {
|
||||
// Example of how to set the logger level
|
||||
// Logger.SetLoggerVerbosity(10);
|
||||
|
|
@ -22,4 +27,6 @@ document.body.onload = function() {
|
|||
});
|
||||
// document.getElementById("centrebox").appendChild(application.rootElement);
|
||||
document.body.appendChild(application.rootElement);
|
||||
|
||||
window.pixelStreaming = stream;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sdp": "^3.1.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"description": "Frontend library for Unreal Engine 5.4 Pixel Streaming",
|
||||
"main": "dist/lib-pixelstreamingfrontend.js",
|
||||
"module": "dist/lib-pixelstreamingfrontend.esm.js",
|
||||
|
|
|
|||
|
|
@ -156,10 +156,7 @@ export class Config {
|
|||
constructor(config: ConfigParams = {}) {
|
||||
const { initialSettings, useUrlParams } = config;
|
||||
this._useUrlParams = !!useUrlParams;
|
||||
this.populateDefaultSettings(this._useUrlParams);
|
||||
if (initialSettings) {
|
||||
this.setSettings(initialSettings);
|
||||
}
|
||||
this.populateDefaultSettings(this._useUrlParams, initialSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +170,7 @@ export class Config {
|
|||
/**
|
||||
* Populate the default settings for a Pixel Streaming application
|
||||
*/
|
||||
private populateDefaultSettings(useUrlParams: boolean): void {
|
||||
private populateDefaultSettings(useUrlParams: boolean, settings: Partial<AllSettings>): void {
|
||||
/**
|
||||
* Text Parameters
|
||||
*/
|
||||
|
|
@ -184,13 +181,15 @@ export class Config {
|
|||
TextParameters.SignallingServerUrl,
|
||||
'Signalling url',
|
||||
'Url of the signalling server',
|
||||
(location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
||||
window.location.hostname +
|
||||
// for readability, we omit the port if it's 80
|
||||
(window.location.port === '80' ||
|
||||
window.location.port === ''
|
||||
? ''
|
||||
: `:${window.location.port}`),
|
||||
settings && settings.hasOwnProperty(TextParameters.SignallingServerUrl) ?
|
||||
settings[TextParameters.SignallingServerUrl] :
|
||||
(location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
||||
window.location.hostname +
|
||||
// for readability, we omit the port if it's 80
|
||||
(window.location.port === '80' ||
|
||||
window.location.port === ''
|
||||
? ''
|
||||
: `:${window.location.port}`),
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -201,7 +200,9 @@ export class Config {
|
|||
OptionParameters.StreamerId,
|
||||
'Streamer ID',
|
||||
'The ID of the streamer to stream.',
|
||||
'',
|
||||
settings && settings.hasOwnProperty(OptionParameters.StreamerId) ?
|
||||
settings[OptionParameters.StreamerId] :
|
||||
'',
|
||||
[],
|
||||
useUrlParams
|
||||
)
|
||||
|
|
@ -217,29 +218,31 @@ export class Config {
|
|||
'Preferred Codec',
|
||||
'The preferred codec to be used during codec negotiation',
|
||||
'H264 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
||||
(function (): Array<string> {
|
||||
const browserSupportedCodecs: Array<string> = [];
|
||||
// Try get the info needed from the RTCRtpReceiver. This is only available on chrome
|
||||
if (!RTCRtpReceiver.getCapabilities) {
|
||||
browserSupportedCodecs.push('Only available on Chrome');
|
||||
return browserSupportedCodecs;
|
||||
}
|
||||
|
||||
const matcher = /(VP\d|H26\d|AV1).*/;
|
||||
const codecs =
|
||||
RTCRtpReceiver.getCapabilities('video').codecs;
|
||||
codecs.forEach((codec) => {
|
||||
const str =
|
||||
codec.mimeType.split('/')[1] +
|
||||
' ' +
|
||||
(codec.sdpFmtpLine || '');
|
||||
const match = matcher.exec(str);
|
||||
if (match !== null) {
|
||||
browserSupportedCodecs.push(str);
|
||||
settings && settings.hasOwnProperty(OptionParameters.PreferredCodec) ?
|
||||
[settings[OptionParameters.PreferredCodec]] :
|
||||
(function (): Array<string> {
|
||||
const browserSupportedCodecs: Array<string> = [];
|
||||
// Try get the info needed from the RTCRtpReceiver. This is only available on chrome
|
||||
if (!RTCRtpReceiver.getCapabilities) {
|
||||
browserSupportedCodecs.push('Only available on Chrome');
|
||||
return browserSupportedCodecs;
|
||||
}
|
||||
});
|
||||
return browserSupportedCodecs;
|
||||
})(),
|
||||
|
||||
const matcher = /(VP\d|H26\d|AV1).*/;
|
||||
const codecs =
|
||||
RTCRtpReceiver.getCapabilities('video').codecs;
|
||||
codecs.forEach((codec) => {
|
||||
const str =
|
||||
codec.mimeType.split('/')[1] +
|
||||
' ' +
|
||||
(codec.sdpFmtpLine || '');
|
||||
const match = matcher.exec(str);
|
||||
if (match !== null) {
|
||||
browserSupportedCodecs.push(str);
|
||||
}
|
||||
});
|
||||
return browserSupportedCodecs;
|
||||
})(),
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -254,7 +257,9 @@ export class Config {
|
|||
Flags.AutoConnect,
|
||||
'Auto connect to stream',
|
||||
'Whether we should attempt to auto connect to the signalling server or show a click to start prompt.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.AutoConnect) ?
|
||||
settings[Flags.AutoConnect] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -265,7 +270,9 @@ export class Config {
|
|||
Flags.AutoPlayVideo,
|
||||
'Auto play video',
|
||||
'When video is ready automatically start playing it as opposed to showing a play button.',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.AutoPlayVideo) ?
|
||||
settings[Flags.AutoPlayVideo] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -276,7 +283,9 @@ export class Config {
|
|||
Flags.BrowserSendOffer,
|
||||
'Browser send offer',
|
||||
'Browser will initiate the WebRTC handshake by sending the offer to the streamer',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.BrowserSendOffer) ?
|
||||
settings[Flags.BrowserSendOffer] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -287,7 +296,9 @@ export class Config {
|
|||
Flags.UseMic,
|
||||
'Use microphone',
|
||||
'Make browser request microphone access and open an input audio track.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.UseMic) ?
|
||||
settings[Flags.UseMic] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -298,7 +309,9 @@ export class Config {
|
|||
Flags.StartVideoMuted,
|
||||
'Start video muted',
|
||||
'Video will start muted if true.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.StartVideoMuted) ?
|
||||
settings[Flags.StartVideoMuted] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -309,7 +322,9 @@ export class Config {
|
|||
Flags.SuppressBrowserKeys,
|
||||
'Suppress browser keys',
|
||||
'Suppress certain browser keys that we use in UE, for example F5 to show shader complexity instead of refresh the page.',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.SuppressBrowserKeys) ?
|
||||
settings[Flags.SuppressBrowserKeys] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -320,7 +335,9 @@ export class Config {
|
|||
Flags.IsQualityController,
|
||||
'Is quality controller?',
|
||||
'True if this peer controls stream quality',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.IsQualityController) ?
|
||||
settings[Flags.IsQualityController] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -331,7 +348,9 @@ export class Config {
|
|||
Flags.ForceMonoAudio,
|
||||
'Force mono audio',
|
||||
'Force browser to request mono audio in the SDP',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.ForceMonoAudio) ?
|
||||
settings[Flags.ForceMonoAudio] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -342,7 +361,9 @@ export class Config {
|
|||
Flags.ForceTURN,
|
||||
'Force TURN',
|
||||
'Only generate TURN/Relayed ICE candidates.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.ForceTURN) ?
|
||||
settings[Flags.ForceTURN] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -353,7 +374,9 @@ export class Config {
|
|||
Flags.AFKDetection,
|
||||
'AFK if idle',
|
||||
'Timeout the experience if user is AFK for a period.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.AFKDetection) ?
|
||||
settings[Flags.AFKDetection] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -364,7 +387,9 @@ export class Config {
|
|||
Flags.MatchViewportResolution,
|
||||
'Match viewport resolution',
|
||||
'Pixel Streaming will be instructed to dynamically resize the video stream to match the size of the video element.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.MatchViewportResolution) ?
|
||||
settings[Flags.MatchViewportResolution] :
|
||||
false,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -375,7 +400,9 @@ export class Config {
|
|||
Flags.HoveringMouseMode,
|
||||
'Control Scheme: Locked Mouse',
|
||||
'Either locked mouse, where the pointer is consumed by the video and locked to it, or hovering mouse, where the mouse is not consumed.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.HoveringMouseMode) ?
|
||||
settings[Flags.HoveringMouseMode] :
|
||||
false,
|
||||
useUrlParams,
|
||||
(isHoveringMouse: boolean, setting: SettingBase) => {
|
||||
setting.label = `Control Scheme: ${isHoveringMouse ? 'Hovering' : 'Locked'} Mouse`;
|
||||
|
|
@ -389,7 +416,9 @@ export class Config {
|
|||
Flags.FakeMouseWithTouches,
|
||||
'Fake mouse with touches',
|
||||
'A single finger touch is converted into a mouse event. This allows a non-touch application to be controlled partially via a touch device.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.FakeMouseWithTouches) ?
|
||||
settings[Flags.FakeMouseWithTouches] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -400,7 +429,9 @@ export class Config {
|
|||
Flags.KeyboardInput,
|
||||
'Keyboard input',
|
||||
'If enabled, send keyboard events to streamer',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.KeyboardInput) ?
|
||||
settings[Flags.KeyboardInput] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -411,7 +442,9 @@ export class Config {
|
|||
Flags.MouseInput,
|
||||
'Mouse input',
|
||||
'If enabled, send mouse events to streamer',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.MouseInput) ?
|
||||
settings[Flags.MouseInput] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -422,7 +455,9 @@ export class Config {
|
|||
Flags.TouchInput,
|
||||
'Touch input',
|
||||
'If enabled, send touch events to streamer',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.TouchInput) ?
|
||||
settings[Flags.TouchInput] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -433,7 +468,9 @@ export class Config {
|
|||
Flags.GamepadInput,
|
||||
'Gamepad input',
|
||||
'If enabled, send gamepad events to streamer',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.GamepadInput) ?
|
||||
settings[Flags.GamepadInput] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -444,7 +481,9 @@ export class Config {
|
|||
Flags.XRControllerInput,
|
||||
'XR controller input',
|
||||
'If enabled, send XR controller events to streamer',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.XRControllerInput) ?
|
||||
settings[Flags.XRControllerInput] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -455,7 +494,9 @@ export class Config {
|
|||
Flags.WaitForStreamer,
|
||||
'Wait for streamer',
|
||||
'Will continue trying to connect to the first streamer available.',
|
||||
true,
|
||||
settings && settings.hasOwnProperty(Flags.WaitForStreamer) ?
|
||||
settings[Flags.WaitForStreamer] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -472,7 +513,9 @@ export class Config {
|
|||
'The time (in seconds) it takes for the application to time out if AFK timeout is enabled.',
|
||||
0 /*min*/,
|
||||
600 /*max*/,
|
||||
120 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.AFKTimeoutSecs) ?
|
||||
settings[NumericParameters.AFKTimeoutSecs] :
|
||||
120, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -485,7 +528,9 @@ export class Config {
|
|||
'Maximum number of reconnects the application will attempt when a streamer disconnects.',
|
||||
0 /*min*/,
|
||||
999 /*max*/,
|
||||
3 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.MaxReconnectAttempts) ?
|
||||
settings[NumericParameters.MaxReconnectAttempts] :
|
||||
3, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -498,7 +543,9 @@ export class Config {
|
|||
'The lower bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
|
||||
0 /*min*/,
|
||||
51 /*max*/,
|
||||
0 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.MinQP) ?
|
||||
settings[NumericParameters.MinQP] :
|
||||
0, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -511,7 +558,9 @@ export class Config {
|
|||
'The upper bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
|
||||
0 /*min*/,
|
||||
51 /*max*/,
|
||||
51 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.MaxQP) ?
|
||||
settings[NumericParameters.MaxQP] :
|
||||
51, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -524,7 +573,9 @@ export class Config {
|
|||
'The maximum FPS that WebRTC will try to transmit frames at.',
|
||||
1 /*min*/,
|
||||
999 /*max*/,
|
||||
60 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.WebRTCFPS) ?
|
||||
settings[NumericParameters.WebRTCFPS] :
|
||||
60, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -537,7 +588,9 @@ export class Config {
|
|||
'The minimum bitrate that WebRTC should use.',
|
||||
0 /*min*/,
|
||||
500000 /*max*/,
|
||||
0 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.WebRTCMinBitrate) ?
|
||||
settings[NumericParameters.WebRTCMinBitrate] :
|
||||
0, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -550,7 +603,9 @@ export class Config {
|
|||
'The maximum bitrate that WebRTC should use.',
|
||||
0 /*min*/,
|
||||
500000 /*max*/,
|
||||
0 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.WebRTCMaxBitrate) ?
|
||||
settings[NumericParameters.WebRTCMaxBitrate] :
|
||||
0, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -563,7 +618,9 @@ export class Config {
|
|||
'Delay between retries when waiting for an available streamer.',
|
||||
500 /*min*/,
|
||||
900000 /*max*/,
|
||||
3000 /*value*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.StreamerAutoJoinInterval) ?
|
||||
settings[NumericParameters.StreamerAutoJoinInterval] :
|
||||
3000, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -742,7 +799,13 @@ export class Config {
|
|||
`Cannot set text setting called ${id} - it does not exist in the Config.enumParameters map.`
|
||||
);
|
||||
} else {
|
||||
this.optionParameters.get(id).selected = settingValue;
|
||||
const optionSetting = this.optionParameters.get(id);
|
||||
const existingOptions = optionSetting.options;
|
||||
if (!existingOptions.includes(settingValue)) {
|
||||
existingOptions.push(settingValue);
|
||||
optionSetting.options = existingOptions;
|
||||
}
|
||||
optionSetting.selected = settingValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -762,24 +825,24 @@ export class Config {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a subset of all settings in one function call.
|
||||
*
|
||||
* @param settings A (partial) list of settings to set
|
||||
*/
|
||||
setSettings(settings: Partial<AllSettings>) {
|
||||
for (const key of Object.keys(settings)) {
|
||||
if (isFlagId(key)) {
|
||||
this.setFlagEnabled(key, settings[key]);
|
||||
} else if (isNumericId(key)) {
|
||||
this.setNumericSetting(key, settings[key]);
|
||||
} else if (isTextId(key)) {
|
||||
this.setTextSetting(key, settings[key]);
|
||||
} else if (isOptionId(key)) {
|
||||
this.setOptionSettingValue(key, settings[key]);
|
||||
/**
|
||||
* Set a subset of all settings in one function call.
|
||||
*
|
||||
* @param settings A (partial) list of settings to set
|
||||
*/
|
||||
setSettings(settings: Partial<AllSettings>) {
|
||||
for (const key of Object.keys(settings)) {
|
||||
if (isFlagId(key)) {
|
||||
this.setFlagEnabled(key, settings[key]);
|
||||
} else if (isNumericId(key)) {
|
||||
this.setNumericSetting(key, settings[key]);
|
||||
} else if (isTextId(key)) {
|
||||
this.setTextSetting(key, settings[key]);
|
||||
} else if (isOptionId(key)) {
|
||||
this.setOptionSettingValue(key, settings[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class SettingNumber<
|
|||
if (!useUrlParams || !urlParams.has(this.id)) {
|
||||
this.number = defaultNumber;
|
||||
} else {
|
||||
const parsedValue = Number.parseInt(urlParams.get(this.id));
|
||||
const parsedValue = Number.parseFloat(urlParams.get(this.id));
|
||||
this.number = Number.isNaN(parsedValue)
|
||||
? defaultNumber
|
||||
: parsedValue;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class SettingOption<
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
defaultOnChangeListener: (changedValue: unknown, setting: SettingBase) => void = () => { /* Do nothing, to be overridden. */ }
|
||||
) {
|
||||
super(id, label, description, [defaultTextValue, defaultTextValue], defaultOnChangeListener);
|
||||
super(id, label, description, defaultTextValue, defaultOnChangeListener);
|
||||
|
||||
this.options = options;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ export class GamePadController {
|
|||
window.requestAnimationFrame
|
||||
).bind(window);
|
||||
const browserWindow = window as Window;
|
||||
|
||||
const onBeforeUnload = (ev: Event) =>
|
||||
this.onBeforeUnload(ev);
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
|
||||
if ('GamepadEvent' in browserWindow) {
|
||||
const onGamePadConnected = (ev: GamepadEvent) =>
|
||||
this.gamePadConnectHandler(ev);
|
||||
|
|
@ -197,7 +202,8 @@ export class GamePadController {
|
|||
} else {
|
||||
toStreamerHandlers.get('GamepadButtonReleased')([
|
||||
controllerIndex,
|
||||
i
|
||||
i,
|
||||
0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +259,14 @@ export class GamePadController {
|
|||
onGamepadDisconnected(controllerIdx: number) {
|
||||
// Default Functionality: Do Nothing
|
||||
}
|
||||
|
||||
onBeforeUnload(ev: Event) {
|
||||
// When a user navigates away from the page, we need to inform UE of all the disconnecting
|
||||
// controllers
|
||||
for(const controller of this.controllers) {
|
||||
this.onGamepadDisconnected(controller.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -150,16 +150,21 @@ export class KeyboardController {
|
|||
* Registers document keyboard events with the controller
|
||||
*/
|
||||
registerKeyBoardEvents() {
|
||||
const compositionEndHandler = (ev: CompositionEvent) => this.handleOnCompositionEnd(ev);
|
||||
const keyDownHandler = (ev: KeyboardEvent) => this.handleOnKeyDown(ev);
|
||||
const keyUpHandler = (ev: KeyboardEvent) => this.handleOnKeyUp(ev);
|
||||
const keyPressHandler = (ev: KeyboardEvent) => this.handleOnKeyPress(ev);
|
||||
|
||||
document.addEventListener("compositionend", compositionEndHandler);
|
||||
document.addEventListener("keydown", keyDownHandler);
|
||||
document.addEventListener("keyup", keyUpHandler);
|
||||
|
||||
//This has been deprecated as at Jun 13 2021
|
||||
document.addEventListener("keypress", keyPressHandler);
|
||||
|
||||
this.keyboardEventListenerTracker.addUnregisterCallback(
|
||||
() => document.removeEventListener("compositionend", compositionEndHandler)
|
||||
);
|
||||
this.keyboardEventListenerTracker.addUnregisterCallback(
|
||||
() => document.removeEventListener("keydown", keyDownHandler)
|
||||
);
|
||||
|
|
@ -184,7 +189,7 @@ export class KeyboardController {
|
|||
*/
|
||||
handleOnKeyDown(keyboardEvent: KeyboardEvent) {
|
||||
const keyCode = this.getKeycode(keyboardEvent);
|
||||
if (!keyCode) {
|
||||
if (!keyCode || keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -263,6 +268,37 @@ export class KeyboardController {
|
|||
toStreamerHandlers.get('KeyPress')([charCode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle whenever composition ends (eg chinese simplified)
|
||||
* @param compositionEvent - the composition event
|
||||
*/
|
||||
handleOnCompositionEnd(compositionEvent: CompositionEvent) {
|
||||
if (compositionEvent.data && compositionEvent.data.length) {
|
||||
compositionEvent.data.split('').forEach((char) => {
|
||||
// This keydown, keypress, keyup flow is required to mimic the way characters are
|
||||
// normally triggered
|
||||
this.handleOnKeyDown(
|
||||
new KeyboardEvent('keydown', {
|
||||
keyCode: char.toUpperCase().charCodeAt(0),
|
||||
charCode: char.charCodeAt(0)
|
||||
})
|
||||
);
|
||||
this.handleOnKeyPress(
|
||||
new KeyboardEvent('keypress', {
|
||||
keyCode: char.toUpperCase().charCodeAt(0),
|
||||
charCode: char.charCodeAt(0)
|
||||
})
|
||||
);
|
||||
this.handleOnKeyUp(
|
||||
new KeyboardEvent('keyup', {
|
||||
keyCode: char.toUpperCase().charCodeAt(0),
|
||||
charCode: char.charCodeAt(0)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Keycode of the Key pressed
|
||||
* @param keyboardEvent - Key board Event
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export class TouchController implements ITouchController {
|
|||
coord.x,
|
||||
coord.y,
|
||||
this.fingerIds.get(touch.identifier),
|
||||
this.maxByteValue * touch.force,
|
||||
this.maxByteValue * (touch.force > 0 ? touch.force : 1),
|
||||
coord.inRange ? 1 : 0
|
||||
]);
|
||||
break;
|
||||
|
|
@ -198,7 +198,7 @@ export class TouchController implements ITouchController {
|
|||
coord.x,
|
||||
coord.y,
|
||||
this.fingerIds.get(touch.identifier),
|
||||
this.maxByteValue * touch.force,
|
||||
this.maxByteValue * (touch.force > 0 ? touch.force : 1),
|
||||
coord.inRange ? 1 : 0
|
||||
]);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export class AggregatedStats {
|
|||
inboundAudioStats: InboundAudioStats;
|
||||
lastVideoStats: InboundVideoStats;
|
||||
lastAudioStats: InboundAudioStats;
|
||||
candidatePair: CandidatePairStats;
|
||||
candidatePairs: Array<CandidatePairStats>;
|
||||
DataChannelStats: DataChannelStats;
|
||||
localCandidates: Array<CandidateStat>;
|
||||
remoteCandidates: Array<CandidateStat>;
|
||||
|
|
@ -37,7 +37,6 @@ export class AggregatedStats {
|
|||
constructor() {
|
||||
this.inboundVideoStats = new InboundVideoStats();
|
||||
this.inboundAudioStats = new InboundAudioStats();
|
||||
this.candidatePair = new CandidatePairStats();
|
||||
this.DataChannelStats = new DataChannelStats();
|
||||
this.outBoundVideoStats = new OutBoundVideoStats();
|
||||
this.sessionStats = new SessionStats();
|
||||
|
|
@ -52,6 +51,7 @@ export class AggregatedStats {
|
|||
processStats(rtcStatsReport: RTCStatsReport) {
|
||||
this.localCandidates = new Array<CandidateStat>();
|
||||
this.remoteCandidates = new Array<CandidateStat>();
|
||||
this.candidatePairs = new Array<CandidatePairStats>();
|
||||
|
||||
rtcStatsReport.forEach((stat) => {
|
||||
const type: RTCStatsTypePS = stat.type;
|
||||
|
|
@ -120,16 +120,10 @@ export class AggregatedStats {
|
|||
* @param stat - the stats coming in from ice candidates
|
||||
*/
|
||||
handleCandidatePair(stat: CandidatePairStats) {
|
||||
this.candidatePair.bytesReceived = stat.bytesReceived;
|
||||
this.candidatePair.bytesSent = stat.bytesSent;
|
||||
this.candidatePair.localCandidateId = stat.localCandidateId;
|
||||
this.candidatePair.remoteCandidateId = stat.remoteCandidateId;
|
||||
this.candidatePair.nominated = stat.nominated;
|
||||
this.candidatePair.readable = stat.readable;
|
||||
this.candidatePair.selected = stat.selected;
|
||||
this.candidatePair.writable = stat.writable;
|
||||
this.candidatePair.state = stat.state;
|
||||
this.candidatePair.currentRoundTripTime = stat.currentRoundTripTime;
|
||||
|
||||
// Add the candidate pair to the candidate pair array
|
||||
this.candidatePairs.push(stat)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,6 +156,8 @@ export class AggregatedStats {
|
|||
localCandidate.protocol = stat.protocol;
|
||||
localCandidate.candidateType = stat.candidateType;
|
||||
localCandidate.id = stat.id;
|
||||
localCandidate.relayProtocol = stat.relayProtocol;
|
||||
localCandidate.transportId = stat.transportId;
|
||||
this.localCandidates.push(localCandidate);
|
||||
}
|
||||
|
||||
|
|
@ -171,12 +167,14 @@ export class AggregatedStats {
|
|||
*/
|
||||
handleRemoteCandidate(stat: CandidateStat) {
|
||||
const RemoteCandidate = new CandidateStat();
|
||||
RemoteCandidate.label = 'local-candidate';
|
||||
RemoteCandidate.label = 'remote-candidate';
|
||||
RemoteCandidate.address = stat.address;
|
||||
RemoteCandidate.port = stat.port;
|
||||
RemoteCandidate.protocol = stat.protocol;
|
||||
RemoteCandidate.id = stat.id;
|
||||
RemoteCandidate.candidateType = stat.candidateType;
|
||||
RemoteCandidate.relayProtocol = stat.relayProtocol;
|
||||
RemoteCandidate.transportId = stat.transportId
|
||||
this.remoteCandidates.push(RemoteCandidate);
|
||||
}
|
||||
|
||||
|
|
@ -308,4 +306,12 @@ export class AggregatedStats {
|
|||
isNumber(value: unknown): boolean {
|
||||
return typeof value === 'number' && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return the active candidate pair
|
||||
* @returns The candidate pair that is currently receiving data
|
||||
*/
|
||||
public getActiveCandidatePair(): CandidatePairStats | null {
|
||||
return this.candidatePairs.find((candidatePair) => candidatePair.bytesReceived > 0, null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,19 @@
|
|||
export class CandidatePairStats {
|
||||
bytesReceived: number;
|
||||
bytesSent: number;
|
||||
currentRoundTripTime: number;
|
||||
id: string;
|
||||
lastPacketReceivedTimestamp: number;
|
||||
lastPacketSentTimestamp: number;
|
||||
localCandidateId: string;
|
||||
remoteCandidateId: string;
|
||||
nominated: boolean;
|
||||
priority: number;
|
||||
readable: boolean;
|
||||
writable: boolean;
|
||||
remoteCandidateId: string;
|
||||
selected: boolean;
|
||||
state: string;
|
||||
currentRoundTripTime: number;
|
||||
timestamp: number;
|
||||
transportId: string;
|
||||
type: string;
|
||||
writable: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
* ICE Candidate Stat collected from the RTC Stats Report
|
||||
*/
|
||||
export class CandidateStat {
|
||||
label: string;
|
||||
id: string;
|
||||
address: string;
|
||||
candidateType: string;
|
||||
id: string;
|
||||
label: string;
|
||||
port: number;
|
||||
protocol: 'tcp' | 'udp';
|
||||
relayProtocol: 'tcp' | 'udp' | 'tls';
|
||||
transportId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,8 @@ describe('PixelStreaming', () => {
|
|||
type: MessageRecvTypes.STREAMER_LIST,
|
||||
ids: streamerIdList
|
||||
}),
|
||||
autoSelectedStreamerId: streamerId
|
||||
autoSelectedStreamerId: streamerId,
|
||||
wantedStreamerId: null
|
||||
}));
|
||||
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/"type":"subscribe".*MOCK_PIXEL_STREAMING/)
|
||||
|
|
@ -298,7 +299,8 @@ describe('PixelStreaming', () => {
|
|||
type: MessageRecvTypes.STREAMER_LIST,
|
||||
ids: extendedStreamerIdList
|
||||
}),
|
||||
autoSelectedStreamerId: null
|
||||
autoSelectedStreamerId: null,
|
||||
wantedStreamerId: null
|
||||
}));
|
||||
expect(webSocketSpyFunctions.sendSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringMatching(/"type":"subscribe"/)
|
||||
|
|
@ -396,9 +398,9 @@ describe('PixelStreaming', () => {
|
|||
expect.objectContaining({
|
||||
data: {
|
||||
aggregatedStats: expect.objectContaining({
|
||||
candidatePair: expect.objectContaining({
|
||||
bytesReceived: 123
|
||||
}),
|
||||
candidatePairs: [
|
||||
expect.objectContaining({ bytesReceived: 123 })
|
||||
],
|
||||
localCandidates: [
|
||||
expect.objectContaining({ address: 'mock-address' })
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ import {
|
|||
WebRtcSdpEvent,
|
||||
DataChannelLatencyTestResponseEvent,
|
||||
DataChannelLatencyTestResultEvent,
|
||||
PlayerCountEvent
|
||||
PlayerCountEvent,
|
||||
WebRtcTCPRelayDetectedEvent
|
||||
} from '../Util/EventEmitter';
|
||||
import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive';
|
||||
import { WebXRController } from '../WebXR/WebXRController';
|
||||
|
|
@ -62,6 +63,7 @@ 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.
|
||||
|
|
@ -70,7 +72,6 @@ export class PixelStreaming {
|
|||
|
||||
private _videoElementParent: HTMLElement;
|
||||
|
||||
_showActionOrErrorOnDisconnect = true;
|
||||
private allowConsoleCommands = false;
|
||||
|
||||
private onScreenKeyboardHelper: OnScreenKeyboard;
|
||||
|
|
@ -117,6 +118,15 @@ export class PixelStreaming {
|
|||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -353,7 +363,7 @@ export class PixelStreaming {
|
|||
*/
|
||||
public reconnect() {
|
||||
this._eventEmitter.dispatchEvent(new StreamReconnectEvent());
|
||||
this._webRtcController.restartStreamAutomatically();
|
||||
this._webRtcController.tryReconnect("Reconnecting...");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -440,7 +450,6 @@ export class PixelStreaming {
|
|||
*/
|
||||
_onWebRtcAutoConnect() {
|
||||
this._eventEmitter.dispatchEvent(new WebRtcAutoConnectEvent());
|
||||
this._showActionOrErrorOnDisconnect = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -460,30 +469,16 @@ export class PixelStreaming {
|
|||
/**
|
||||
* Event fired when the video is disconnected - emits given eventString or an override
|
||||
* message from webRtcController if one has been set
|
||||
* @param eventString - the event text that will be emitted
|
||||
* @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) {
|
||||
// if we have overridden the default disconnection message, assign the new value here
|
||||
if (
|
||||
this._webRtcController.getDisconnectMessageOverride() != '' &&
|
||||
this._webRtcController.getDisconnectMessageOverride() !==
|
||||
undefined &&
|
||||
this._webRtcController.getDisconnectMessageOverride() != null
|
||||
) {
|
||||
eventString = this._webRtcController.getDisconnectMessageOverride();
|
||||
this._webRtcController.setDisconnectMessageOverride('');
|
||||
}
|
||||
|
||||
_onDisconnect(eventString: string, allowClickToReconnect: boolean) {
|
||||
this._eventEmitter.dispatchEvent(
|
||||
new WebRtcDisconnectedEvent({
|
||||
eventString,
|
||||
showActionOrErrorOnDisconnect:
|
||||
this._showActionOrErrorOnDisconnect
|
||||
eventString: eventString,
|
||||
allowClickToReconnect: allowClickToReconnect
|
||||
})
|
||||
);
|
||||
if (this._showActionOrErrorOnDisconnect == false) {
|
||||
this._showActionOrErrorOnDisconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -582,12 +577,16 @@ export class PixelStreaming {
|
|||
|
||||
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.parseInt(urlParams.get(NumericParameters.MinQP))
|
||||
? Number.parseFloat(urlParams.get(NumericParameters.MinQP))
|
||||
: settings.EncoderSettings.MinQP
|
||||
);
|
||||
|
||||
|
|
@ -595,7 +594,7 @@ export class PixelStreaming {
|
|||
this.config.setNumericSetting(
|
||||
NumericParameters.MaxQP,
|
||||
(useUrlParams && urlParams.has(NumericParameters.MaxQP))
|
||||
? Number.parseInt(urlParams.get(NumericParameters.MaxQP))
|
||||
? Number.parseFloat(urlParams.get(NumericParameters.MaxQP))
|
||||
: settings.EncoderSettings.MaxQP
|
||||
);
|
||||
}
|
||||
|
|
@ -603,20 +602,20 @@ export class PixelStreaming {
|
|||
this.config.setNumericSetting(
|
||||
NumericParameters.WebRTCMinBitrate,
|
||||
(useUrlParams && urlParams.has(NumericParameters.WebRTCMinBitrate))
|
||||
? Number.parseInt(urlParams.get(NumericParameters.WebRTCMinBitrate))
|
||||
: settings.WebRTCSettings.MinBitrate / 1000 /* bps to kbps */
|
||||
? Number.parseFloat(urlParams.get(NumericParameters.WebRTCMinBitrate))
|
||||
: (settings.WebRTCSettings.MinBitrate / 1000) /* bps to kbps */
|
||||
);
|
||||
this.config.setNumericSetting(
|
||||
NumericParameters.WebRTCMaxBitrate,
|
||||
(useUrlParams && urlParams.has(NumericParameters.WebRTCMaxBitrate))
|
||||
? Number.parseInt(urlParams.get(NumericParameters.WebRTCMaxBitrate))
|
||||
: settings.WebRTCSettings.MaxBitrate / 1000 /* bps to kbps */
|
||||
? Number.parseFloat(urlParams.get(NumericParameters.WebRTCMaxBitrate))
|
||||
: (settings.WebRTCSettings.MaxBitrate / 1000) /* bps to kbps */
|
||||
|
||||
);
|
||||
this.config.setNumericSetting(
|
||||
NumericParameters.WebRTCFPS,
|
||||
(useUrlParams && urlParams.has(NumericParameters.WebRTCFPS))
|
||||
? Number.parseInt(urlParams.get(NumericParameters.WebRTCFPS))
|
||||
? Number.parseFloat(urlParams.get(NumericParameters.WebRTCFPS))
|
||||
: settings.WebRTCSettings.FPS
|
||||
);
|
||||
}
|
||||
|
|
@ -639,6 +638,28 @@ export class PixelStreaming {
|
|||
);
|
||||
}
|
||||
|
||||
// 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!
|
||||
|
|
@ -856,4 +877,8 @@ export class PixelStreaming {
|
|||
public get toStreamerHandlers() {
|
||||
return this._webRtcController.streamMessageController.toStreamerHandlers;
|
||||
}
|
||||
|
||||
public isReconnecting() {
|
||||
return this._webRtcController.isReconnecting;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export class WebRtcDisconnectedEvent extends Event {
|
|||
/** Message describing the disconnect reason */
|
||||
eventString: string;
|
||||
/** true if the user is able to reconnect, false if disconnected because of unrecoverable reasons like not able to connect to the signaling server */
|
||||
showActionOrErrorOnDisconnect: boolean;
|
||||
allowClickToReconnect: boolean;
|
||||
};
|
||||
constructor(data: WebRtcDisconnectedEvent['data']) {
|
||||
super('webRtcDisconnected');
|
||||
|
|
@ -347,7 +347,9 @@ export class StreamerListMessageEvent extends Event {
|
|||
/** Streamer list message containing an array of streamer ids */
|
||||
messageStreamerList: MessageStreamerList;
|
||||
/** Auto-selected streamer from the list, or null if unable to auto-select and user should be prompted to select */
|
||||
autoSelectedStreamerId: string | null;
|
||||
autoSelectedStreamerId: string;
|
||||
/** Wanted streamer id from various configurations. */
|
||||
wantedStreamerId: string;
|
||||
};
|
||||
constructor(data: StreamerListMessageEvent['data']) {
|
||||
super('streamerListMessage');
|
||||
|
|
@ -355,6 +357,21 @@ export class StreamerListMessageEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when a subscribed to streamer's id changes.
|
||||
*/
|
||||
export class StreamerIDChangedMessageEvent extends Event {
|
||||
readonly type: 'streamerIDChangedMessage';
|
||||
readonly data: {
|
||||
/** The new ID of the streamer. */
|
||||
newID: string;
|
||||
};
|
||||
constructor(data: StreamerIDChangedMessageEvent['data']) {
|
||||
super('StreamerIDChangedMessage');
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when receiving latency test results.
|
||||
*/
|
||||
|
|
@ -520,6 +537,16 @@ export class PlayerCountEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when the webRTC connections is relayed over TCP.
|
||||
*/
|
||||
export class WebRtcTCPRelayDetectedEvent extends Event {
|
||||
readonly type: 'webRtcTCPRelayDetected';
|
||||
constructor() {
|
||||
super('webRtcTCPRelayDetected');
|
||||
}
|
||||
}
|
||||
|
||||
export type PixelStreamingEvent =
|
||||
| AfkWarningActivateEvent
|
||||
| AfkWarningUpdateEvent
|
||||
|
|
@ -547,6 +574,7 @@ export type PixelStreamingEvent =
|
|||
| HideFreezeFrameEvent
|
||||
| StatsReceivedEvent
|
||||
| StreamerListMessageEvent
|
||||
| StreamerIDChangedMessageEvent
|
||||
| LatencyTestResultEvent
|
||||
| DataChannelLatencyTestResponseEvent
|
||||
| DataChannelLatencyTestResultEvent
|
||||
|
|
@ -555,7 +583,8 @@ export type PixelStreamingEvent =
|
|||
| XrSessionStartedEvent
|
||||
| XrSessionEndedEvent
|
||||
| XrFrameEvent
|
||||
| PlayerCountEvent;
|
||||
| PlayerCountEvent
|
||||
| WebRtcTCPRelayDetectedEvent;
|
||||
|
||||
export class EventEmitter extends EventTarget {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { WebSocketController } from '../WebSockets/WebSocketController';
|
||||
import { ExtraOfferParameters, ExtraAnswerParameters } from '../WebSockets/MessageSend';
|
||||
import { StreamController } from '../VideoPlayer/StreamController';
|
||||
import {
|
||||
MessageAnswer,
|
||||
MessageOffer,
|
||||
MessageConfig,
|
||||
MessageStreamerList
|
||||
MessageStreamerList,
|
||||
MessageStreamerIDChanged
|
||||
} from '../WebSockets/MessageReceive';
|
||||
import { FreezeFrameController } from '../FreezeFrame/FreezeFrameController';
|
||||
import { AFKController } from '../AFK/AFKController';
|
||||
|
|
@ -59,7 +61,8 @@ import {
|
|||
PlayStreamErrorEvent,
|
||||
PlayStreamEvent,
|
||||
PlayStreamRejectedEvent,
|
||||
StreamerListMessageEvent
|
||||
StreamerListMessageEvent,
|
||||
StreamerIDChangedMessageEvent
|
||||
} from '../Util/EventEmitter';
|
||||
import {
|
||||
DataChannelLatencyTestRequest,
|
||||
|
|
@ -104,16 +107,13 @@ export class WebRtcPlayerController {
|
|||
preferredCodec: string;
|
||||
peerConfig: RTCConfiguration;
|
||||
videoAvgQp: number;
|
||||
locallyClosed: boolean;
|
||||
shouldReconnect: boolean;
|
||||
isReconnecting: boolean;
|
||||
reconnectAttempt: number;
|
||||
subscribedStream: string | null;
|
||||
disconnectMessage: string;
|
||||
subscribedStream: string;
|
||||
signallingUrlBuilder: () => string;
|
||||
|
||||
// if you override the disconnection message by calling the interface method setDisconnectMessageOverride
|
||||
// it will use this property to store the override message string
|
||||
disconnectMessageOverride: string;
|
||||
|
||||
autoJoinTimer: ReturnType<typeof setTimeout> = undefined;
|
||||
|
||||
/**
|
||||
|
|
@ -139,10 +139,7 @@ export class WebRtcPlayerController {
|
|||
this.onAfkTriggered.bind(this)
|
||||
);
|
||||
this.afkController.onAFKTimedOutCallback = () => {
|
||||
this.setDisconnectMessageOverride(
|
||||
'You have been disconnected due to inactivity'
|
||||
);
|
||||
this.closeSignalingServer();
|
||||
this.closeSignalingServer('You have been disconnected due to inactivity');
|
||||
};
|
||||
|
||||
this.freezeFrameController = new FreezeFrameController(
|
||||
|
|
@ -202,14 +199,9 @@ export class WebRtcPlayerController {
|
|||
this.webSocketController.onStreamerList = (
|
||||
messageList: MessageReceive.MessageStreamerList
|
||||
) => this.handleStreamerListMessage(messageList);
|
||||
this.webSocketController.onWebSocketOncloseOverlayMessage = (event) => {
|
||||
this.pixelStreaming._onDisconnect(
|
||||
`Websocket disconnect (${event.code}) ${
|
||||
event.reason != '' ? '- ' + event.reason : ''
|
||||
}`
|
||||
);
|
||||
this.setVideoEncoderAvgQP(0);
|
||||
};
|
||||
this.webSocketController.onStreamerIDChanged = (
|
||||
message: MessageReceive.MessageStreamerIDChanged
|
||||
) => this.handleStreamerIDChangedMessage(message);
|
||||
this.webSocketController.onPlayerCount = (playerCount: MessageReceive.MessagePlayerCount) => {
|
||||
this.pixelStreaming._onPlayerCount(playerCount.count);
|
||||
};
|
||||
|
|
@ -223,6 +215,19 @@ export class WebRtcPlayerController {
|
|||
}
|
||||
});
|
||||
this.webSocketController.onClose.addEventListener('close', (event : CustomEvent) => {
|
||||
// when we refresh the page during a stream we get the going away code.
|
||||
// in that case we don't want to reconnect since we're navigating away.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
|
||||
// lists all the codes.
|
||||
const CODE_GOING_AWAY = 1001;
|
||||
|
||||
const willTryReconnect = this.shouldReconnect
|
||||
&& event.detail.code != CODE_GOING_AWAY
|
||||
&& this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts) > 0
|
||||
|
||||
const disconnectMessage = this.disconnectMessage ? this.disconnectMessage : event.detail.reason;
|
||||
this.pixelStreaming._onDisconnect(disconnectMessage, !willTryReconnect && !this.isReconnecting);
|
||||
|
||||
this.afkController.stopAfkWarningTimer();
|
||||
|
||||
// stop sending stats on interval if we have closed our connection
|
||||
|
|
@ -230,21 +235,22 @@ export class WebRtcPlayerController {
|
|||
window.clearInterval(this.statsTimerHandle);
|
||||
}
|
||||
|
||||
// reset the stream quality icon.
|
||||
this.setVideoEncoderAvgQP(0);
|
||||
|
||||
// unregister all input device event handlers on disconnect
|
||||
this.setTouchInputEnabled(false);
|
||||
this.setMouseInputEnabled(false);
|
||||
this.setKeyboardInputEnabled(false);
|
||||
this.setGamePadInputEnabled(false);
|
||||
|
||||
// when we refresh the page during a stream we get the going away code.
|
||||
// in that case we don't want to reconnect since we're navigating away.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
|
||||
// lists all the codes.
|
||||
const CODE_GOING_AWAY = 1001;
|
||||
if(this.shouldReconnect && event.detail.code != CODE_GOING_AWAY && this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts) > 0) {
|
||||
this.isReconnecting = true;
|
||||
this.reconnectAttempt++;
|
||||
this.restartStreamAutomatically();
|
||||
if (willTryReconnect) {
|
||||
// need a small delay here to prevent reconnect spamming
|
||||
setTimeout(() => {
|
||||
this.isReconnecting = true;
|
||||
this.reconnectAttempt++;
|
||||
this.tryReconnect(event.detail.reason);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -947,9 +953,9 @@ export class WebRtcPlayerController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Restart the stream automatically without refreshing the page
|
||||
* Attempt a reconnection to the signalling server
|
||||
*/
|
||||
restartStreamAutomatically() {
|
||||
tryReconnect(message: string) {
|
||||
// if there is no webSocketController return immediately or this will not work
|
||||
if (!this.webSocketController) {
|
||||
Logger.Log(
|
||||
|
|
@ -959,33 +965,16 @@ export class WebRtcPlayerController {
|
|||
return;
|
||||
}
|
||||
|
||||
// if a websocket object has not been created connect normally without closing
|
||||
if (
|
||||
!this.webSocketController.webSocket ||
|
||||
this.webSocketController.webSocket.readyState === WebSocket.CLOSED
|
||||
) {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'A websocket connection has not been made yet so we will start the stream'
|
||||
);
|
||||
// if the connection is open, first close it. wait some time and try again.
|
||||
this.isReconnecting = true;
|
||||
if (this.webSocketController.webSocket && this.webSocketController.webSocket.readyState != WebSocket.CLOSED) {
|
||||
this.closeSignalingServer(`${message} Restarting stream...`);
|
||||
setTimeout(() => {
|
||||
this.tryReconnect(message);
|
||||
}, 3000);
|
||||
} else {
|
||||
this.pixelStreaming._onWebRtcAutoConnect();
|
||||
this.connectToSignallingServer();
|
||||
} else {
|
||||
// set the replay status so we get a text overlay over an action overlay
|
||||
this.pixelStreaming._showActionOrErrorOnDisconnect = false;
|
||||
|
||||
// set the disconnect message
|
||||
this.setDisconnectMessageOverride('Restarting stream...');
|
||||
|
||||
// close the connection
|
||||
this.closeSignalingServer();
|
||||
|
||||
// wait for the connection to close and restart the connection
|
||||
const autoConnectTimeout = setTimeout(() => {
|
||||
this.pixelStreaming._onWebRtcAutoConnect();
|
||||
this.connectToSignallingServer();
|
||||
clearTimeout(autoConnectTimeout);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1087,13 +1076,8 @@ export class WebRtcPlayerController {
|
|||
);
|
||||
Logger.Error(Logger.GetStackTrace(), message);
|
||||
|
||||
// set the disconnect message
|
||||
this.setDisconnectMessageOverride(
|
||||
'Stream not initialized correctly'
|
||||
);
|
||||
|
||||
// close the connection
|
||||
this.closeSignalingServer();
|
||||
this.closeSignalingServer('Stream not initialized correctly');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1176,6 +1160,9 @@ export class WebRtcPlayerController {
|
|||
* Connect to the Signaling server
|
||||
*/
|
||||
connectToSignallingServer() {
|
||||
this.locallyClosed = false;
|
||||
this.shouldReconnect = true;
|
||||
this.disconnectMessage = null;
|
||||
const signallingUrl = this.signallingUrlBuilder();
|
||||
this.webSocketController.connect(signallingUrl);
|
||||
}
|
||||
|
|
@ -1198,10 +1185,7 @@ export class WebRtcPlayerController {
|
|||
Logger.GetStackTrace(),
|
||||
'No turn server was found in the Peer Connection Options. TURN cannot be forced, closing connection. Please use STUN instead'
|
||||
);
|
||||
this.setDisconnectMessageOverride(
|
||||
'TURN cannot be forced, closing connection. Please use STUN instead.'
|
||||
);
|
||||
this.closeSignalingServer();
|
||||
this.closeSignalingServer('TURN cannot be forced, closing connection. Please use STUN instead.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1343,84 +1327,124 @@ export class WebRtcPlayerController {
|
|||
6
|
||||
);
|
||||
|
||||
if(this.isReconnecting) {
|
||||
if(messageStreamerList.ids.includes(this.subscribedStream)) {
|
||||
// If we're reconnecting and the previously subscribed stream has come back, resubscribe to it
|
||||
this.isReconnecting = false;
|
||||
this.reconnectAttempt = 0;
|
||||
this.webSocketController.sendSubscribe(this.subscribedStream);
|
||||
} else if(this.reconnectAttempt < this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts)) {
|
||||
// Our previous stream hasn't come back, wait 2 seconds and request an updated stream list
|
||||
this.reconnectAttempt++;
|
||||
setTimeout(() => {
|
||||
this.webSocketController.requestStreamerList();
|
||||
}, 2000)
|
||||
} else {
|
||||
// We've exhausted our reconnect attempts, return to main screen
|
||||
this.reconnectAttempt = 0;
|
||||
this.isReconnecting = false;
|
||||
this.shouldReconnect = false;
|
||||
this.webSocketController.close();
|
||||
let wantedStreamerId: string = null;
|
||||
|
||||
this.config.setOptionSettingValue(
|
||||
OptionParameters.StreamerId,
|
||||
""
|
||||
);
|
||||
this.config.setOptionSettingOptions(
|
||||
OptionParameters.StreamerId,
|
||||
[]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
|
||||
settingOptions.unshift(''); // add an empty option at the top
|
||||
this.config.setOptionSettingOptions(
|
||||
// get the current selected streamer id option
|
||||
var streamerIDOption = this.config.getSettingOption(OptionParameters.StreamerId);
|
||||
const existingSelection = streamerIDOption.selected.toString().trim();
|
||||
if (!!existingSelection) {
|
||||
// default to selected option if it exists
|
||||
wantedStreamerId = streamerIDOption.selected;
|
||||
}
|
||||
|
||||
// add the streamers to the UI
|
||||
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
|
||||
settingOptions.unshift(''); // add an empty option at the top
|
||||
this.config.setOptionSettingOptions(
|
||||
OptionParameters.StreamerId,
|
||||
settingOptions
|
||||
);
|
||||
|
||||
let autoSelectedStreamerId: string = null;
|
||||
const waitForStreamer = this.config.isFlagEnabled(Flags.WaitForStreamer);
|
||||
const reconnectLimit = this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts);
|
||||
const reconnectDelay = this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval);
|
||||
|
||||
// first we figure out a wanted streamer id through various means
|
||||
const useUrlParams = this.config.useUrlParams;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (useUrlParams && urlParams.has(OptionParameters.StreamerId)) {
|
||||
// if we've set the streamer id on the url we only want that streamer id
|
||||
wantedStreamerId = urlParams.get(OptionParameters.StreamerId);
|
||||
} else if (this.subscribedStream) {
|
||||
// we were previously subscribed to a streamer, we want that
|
||||
wantedStreamerId = this.subscribedStream;
|
||||
}
|
||||
|
||||
// now lets see if we can pick it.
|
||||
if (wantedStreamerId && messageStreamerList.ids.includes(wantedStreamerId)) {
|
||||
// if the wanted stream is in the list. we pick that
|
||||
autoSelectedStreamerId = wantedStreamerId;
|
||||
} else if ((!wantedStreamerId || !waitForStreamer) && messageStreamerList.ids.length == 1) {
|
||||
// otherwise, if we're not waiting for the wanted streamer and there's only one streamer, connect to it
|
||||
autoSelectedStreamerId = messageStreamerList.ids[0];
|
||||
}
|
||||
|
||||
// if we found a streamer id to auto select, select it
|
||||
if (autoSelectedStreamerId) {
|
||||
this.isReconnecting = false;
|
||||
this.reconnectAttempt = 0;
|
||||
this.config.setOptionSettingValue(
|
||||
OptionParameters.StreamerId,
|
||||
settingOptions
|
||||
autoSelectedStreamerId
|
||||
);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let autoSelectedStreamerId: string | null = null;
|
||||
if (messageStreamerList.ids.length == 1) {
|
||||
// If there's only a single streamer, subscribe to it regardless of what is in the URL
|
||||
autoSelectedStreamerId = messageStreamerList.ids[0];
|
||||
} else if (
|
||||
urlParams.has(OptionParameters.StreamerId) &&
|
||||
messageStreamerList.ids.includes(
|
||||
urlParams.get(OptionParameters.StreamerId)
|
||||
)
|
||||
) {
|
||||
// If there's a streamer ID in the URL and a streamer with this ID is connected, set it as the selected streamer
|
||||
autoSelectedStreamerId = urlParams.get(OptionParameters.StreamerId);
|
||||
}
|
||||
if (autoSelectedStreamerId !== null) {
|
||||
this.config.setOptionSettingValue(
|
||||
OptionParameters.StreamerId,
|
||||
autoSelectedStreamerId
|
||||
);
|
||||
} else {
|
||||
// no auto selected streamer
|
||||
if (messageStreamerList.ids.length == 0 && this.config.isFlagEnabled(Flags.WaitForStreamer)) {
|
||||
this.closeSignalingServer();
|
||||
this.startAutoJoinTimer();
|
||||
} else {
|
||||
// no auto selected streamer.
|
||||
// if we're waiting for a streamer then try reconnecting
|
||||
if (waitForStreamer) {
|
||||
if (this.reconnectAttempt < reconnectLimit) {
|
||||
// still reconnects available
|
||||
this.isReconnecting = true;
|
||||
this.reconnectAttempt++;
|
||||
setTimeout(() => {
|
||||
this.webSocketController.requestStreamerList();
|
||||
}, reconnectDelay);
|
||||
} else {
|
||||
// We've exhausted our reconnect attempts, return to main screen
|
||||
this.reconnectAttempt = 0;
|
||||
this.isReconnecting = false;
|
||||
this.shouldReconnect = false;
|
||||
}
|
||||
}
|
||||
this.pixelStreaming.dispatchEvent(
|
||||
new StreamerListMessageEvent({
|
||||
messageStreamerList,
|
||||
autoSelectedStreamerId
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// dispatch this event finally
|
||||
this.pixelStreaming.dispatchEvent(
|
||||
new StreamerListMessageEvent({
|
||||
messageStreamerList,
|
||||
autoSelectedStreamerId,
|
||||
wantedStreamerId
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
startAutoJoinTimer() {
|
||||
clearTimeout(this.autoJoinTimer);
|
||||
this.autoJoinTimer = setTimeout(() => this.tryAutoJoin(), this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval));
|
||||
}
|
||||
handleStreamerIDChangedMessage(streamerIDChangedMessage: MessageStreamerIDChanged) {
|
||||
const newID = streamerIDChangedMessage.newID;
|
||||
|
||||
tryAutoJoin() {
|
||||
this.connectToSignallingServer();
|
||||
// need to edit the selected streamer in the settings list
|
||||
var streamerListOptions = this.config.getSettingOption(OptionParameters.StreamerId);
|
||||
|
||||
// temporarily prevent onChange from firing (it would try to subscribe to the streamer again)
|
||||
var oldOnChange = streamerListOptions.onChange;
|
||||
streamerListOptions.onChange = ()=>{};
|
||||
|
||||
// change the selected entry.
|
||||
var streamerList = streamerListOptions.options;
|
||||
for (var i = 0; i < streamerList.length; ++i) {
|
||||
if (streamerList[i] == this.subscribedStream) {
|
||||
streamerList[i] = newID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update the list
|
||||
streamerListOptions.options = streamerList;
|
||||
|
||||
// update the selected entry
|
||||
streamerListOptions.selected = newID;
|
||||
|
||||
// restore the old change notifier.
|
||||
streamerListOptions.onChange = oldOnChange;
|
||||
|
||||
// remember which stream we're subscribe to
|
||||
this.subscribedStream = streamerIDChangedMessage.newID;
|
||||
|
||||
// notify any listeners
|
||||
this.pixelStreaming.dispatchEvent(
|
||||
new StreamerIDChangedMessageEvent({
|
||||
newID
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1582,7 +1606,13 @@ export class WebRtcPlayerController {
|
|||
'Sending the offer to the Server',
|
||||
6
|
||||
);
|
||||
this.webSocketController.sendWebRtcOffer(offer);
|
||||
|
||||
const extraParams: ExtraOfferParameters = {
|
||||
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
|
||||
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
};
|
||||
|
||||
this.webSocketController.sendWebRtcOffer(offer, extraParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1595,7 +1625,13 @@ export class WebRtcPlayerController {
|
|||
'Sending the answer to the Server',
|
||||
6
|
||||
);
|
||||
this.webSocketController.sendWebRtcAnswer(answer);
|
||||
|
||||
const extraParams: ExtraAnswerParameters = {
|
||||
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
|
||||
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
};
|
||||
|
||||
this.webSocketController.sendWebRtcAnswer(answer, extraParams);
|
||||
|
||||
if (this.isUsingSFU) {
|
||||
this.webSocketController.sendWebRtcDatachannelRequest();
|
||||
|
|
@ -1617,9 +1653,11 @@ export class WebRtcPlayerController {
|
|||
/**
|
||||
* Close the Connection to the signaling server
|
||||
*/
|
||||
closeSignalingServer() {
|
||||
closeSignalingServer(message: string) {
|
||||
// We explicitly called close, therefore we don't want to trigger auto reconnect
|
||||
this.locallyClosed = true;
|
||||
this.shouldReconnect = false;
|
||||
this.disconnectMessage = message;
|
||||
this.webSocketController?.close();
|
||||
}
|
||||
|
||||
|
|
@ -1634,7 +1672,7 @@ export class WebRtcPlayerController {
|
|||
* Close all connections
|
||||
*/
|
||||
close() {
|
||||
this.closeSignalingServer();
|
||||
this.closeSignalingServer('');
|
||||
this.closePeerConnection();
|
||||
}
|
||||
|
||||
|
|
@ -2018,20 +2056,6 @@ export class WebRtcPlayerController {
|
|||
this.videoPlayer.resizePlayerStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the overridden disconnect message
|
||||
*/
|
||||
getDisconnectMessageOverride(): string {
|
||||
return this.disconnectMessageOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the override for the disconnect message
|
||||
*/
|
||||
setDisconnectMessageOverride(message: string): void {
|
||||
this.disconnectMessageOverride = message;
|
||||
}
|
||||
|
||||
setPreferredCodec(codec: string) {
|
||||
this.preferredCodec = codec;
|
||||
if (this.peerConnectionController) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
export enum MessageRecvTypes {
|
||||
CONFIG = 'config',
|
||||
STREAMER_LIST = 'streamerList',
|
||||
STREAMER_ID_CHANGED = 'streamerIDChanged',
|
||||
PLAYER_COUNT = 'playerCount',
|
||||
OFFER = 'offer',
|
||||
ANSWER = 'answer',
|
||||
|
|
@ -42,6 +43,13 @@ export class MessageStreamerList extends MessageRecv {
|
|||
ids: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Streamer ID Changed Message Wrapper
|
||||
*/
|
||||
export class MessageStreamerIDChanged extends MessageRecv {
|
||||
newID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player Count Message wrapper
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,20 @@ export class MessageSend implements Send {
|
|||
type: string;
|
||||
peerConnectionOptions: object;
|
||||
|
||||
/**
|
||||
* A filter for controlling what parameters to actually send.
|
||||
* Good for excluding default values or hidden internals.
|
||||
* Example for including everything but zero bitrate fields...
|
||||
* sendFilter(key: string, value: any) {
|
||||
* if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
* return value;
|
||||
* }
|
||||
* Return undefined to exclude the property completely.
|
||||
*/
|
||||
sendFilter(key: string, value: any) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the wrapper into a JSON String
|
||||
* @returns - JSON String of the Message to send
|
||||
|
|
@ -31,10 +45,10 @@ export class MessageSend implements Send {
|
|||
payload() {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'Sending => \n' + JSON.stringify(this, undefined, 4),
|
||||
'Sending => \n' + JSON.stringify(this, this.sendFilter, 4),
|
||||
6
|
||||
);
|
||||
return JSON.stringify(this);
|
||||
return JSON.stringify(this, this.sendFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,24 +97,45 @@ export class MessagePong extends MessageSend {
|
|||
}
|
||||
}
|
||||
|
||||
export type ExtraOfferParameters = {
|
||||
minBitrateBps: number;
|
||||
maxBitrateBps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web RTC Offer message wrapper
|
||||
*/
|
||||
export class MessageWebRTCOffer extends MessageSend {
|
||||
sdp: string;
|
||||
minBitrate: number;
|
||||
maxBitrate: number;
|
||||
|
||||
/**
|
||||
* @param offer - Generated Web RTC Offer
|
||||
*/
|
||||
constructor(offer?: RTCSessionDescriptionInit) {
|
||||
constructor(offer: RTCSessionDescriptionInit, extraParams: ExtraOfferParameters) {
|
||||
super();
|
||||
this.type = MessageSendTypes.OFFER;
|
||||
this.minBitrate = 0;
|
||||
this.maxBitrate = 0;
|
||||
|
||||
if (offer) {
|
||||
this.type = offer.type as MessageSendTypes;
|
||||
this.sdp = offer.sdp;
|
||||
this.minBitrate = extraParams.minBitrateBps;
|
||||
this.maxBitrate = extraParams.maxBitrateBps;
|
||||
}
|
||||
}
|
||||
|
||||
sendFilter(key: string, value: any) {
|
||||
if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtraAnswerParameters = {
|
||||
minBitrateBps: number;
|
||||
maxBitrateBps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,19 +143,30 @@ export class MessageWebRTCOffer extends MessageSend {
|
|||
*/
|
||||
export class MessageWebRTCAnswer extends MessageSend {
|
||||
sdp: string;
|
||||
minBitrate: number;
|
||||
maxBitrate: number;
|
||||
|
||||
/**
|
||||
* @param answer - Generated Web RTC Offer
|
||||
*/
|
||||
constructor(answer?: RTCSessionDescriptionInit) {
|
||||
constructor(answer: RTCSessionDescriptionInit, extraParams: ExtraAnswerParameters) {
|
||||
super();
|
||||
this.type = MessageSendTypes.ANSWER;
|
||||
this.minBitrate = 0;
|
||||
this.maxBitrate = 0;
|
||||
|
||||
if (answer) {
|
||||
this.type = answer.type as MessageSendTypes;
|
||||
this.sdp = answer.sdp;
|
||||
this.minBitrate = extraParams.minBitrateBps;
|
||||
this.maxBitrate = extraParams.maxBitrateBps;
|
||||
}
|
||||
}
|
||||
|
||||
sendFilter(key: string, value: any) {
|
||||
if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
MessageRecvTypes,
|
||||
MessageConfig,
|
||||
MessageStreamerList,
|
||||
MessageStreamerIDChanged,
|
||||
MessagePlayerCount,
|
||||
MessageAnswer,
|
||||
MessageOffer,
|
||||
|
|
@ -92,6 +93,21 @@ export class SignallingProtocol {
|
|||
}
|
||||
);
|
||||
|
||||
// STREAMER_ID_CHANGED
|
||||
websocketController.signallingProtocol.addMessageHandler(
|
||||
MessageRecvTypes.STREAMER_ID_CHANGED,
|
||||
(idPayload: string) => {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
MessageRecvTypes.STREAMER_ID_CHANGED,
|
||||
6
|
||||
);
|
||||
const streamerIdMessage: MessageStreamerIDChanged =
|
||||
JSON.parse(idPayload);
|
||||
websocketController.onStreamerIDChanged(streamerIdMessage);
|
||||
}
|
||||
);
|
||||
|
||||
// PLAYER_COUNT
|
||||
websocketController.signallingProtocol.addMessageHandler(
|
||||
MessageRecvTypes.PLAYER_COUNT,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ export class WebSocketController {
|
|||
* @param event - Close Event
|
||||
*/
|
||||
handleOnClose(event: CloseEvent) {
|
||||
this.onWebSocketOncloseOverlayMessage(event);
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'Disconnected to the signalling server via WebSocket: ' +
|
||||
|
|
@ -160,13 +159,13 @@ export class WebSocketController {
|
|||
this.webSocket.send(payload.payload());
|
||||
}
|
||||
|
||||
sendWebRtcOffer(offer: RTCSessionDescriptionInit) {
|
||||
const payload = new MessageSend.MessageWebRTCOffer(offer);
|
||||
sendWebRtcOffer(offer: RTCSessionDescriptionInit, extraParams: MessageSend.ExtraOfferParameters) {
|
||||
const payload = new MessageSend.MessageWebRTCOffer(offer, extraParams);
|
||||
this.webSocket.send(payload.payload());
|
||||
}
|
||||
|
||||
sendWebRtcAnswer(answer: RTCSessionDescriptionInit) {
|
||||
const payload = new MessageSend.MessageWebRTCAnswer(answer);
|
||||
sendWebRtcAnswer(answer: RTCSessionDescriptionInit, extraParams: MessageSend.ExtraAnswerParameters) {
|
||||
const payload = new MessageSend.MessageWebRTCAnswer(answer, extraParams);
|
||||
this.webSocket.send(payload.payload());
|
||||
}
|
||||
|
||||
|
|
@ -204,10 +203,6 @@ export class WebSocketController {
|
|||
this.webSocket?.close();
|
||||
}
|
||||
|
||||
/** Event used for Displaying websocket closed messages */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
onWebSocketOncloseOverlayMessage(event: CloseEvent) {}
|
||||
|
||||
/**
|
||||
* The Message Contains the payload of the peer connection options used for the RTC Peer hand shake
|
||||
* @param messageConfig - Config Message received from he signaling server
|
||||
|
|
@ -216,12 +211,19 @@ export class WebSocketController {
|
|||
onConfig(messageConfig: MessageReceive.MessageConfig) {}
|
||||
|
||||
/**
|
||||
* The Message Contains the payload of the peer connection options used for the RTC Peer hand shake
|
||||
* @param messageConfig - Config Message received from he signaling server
|
||||
* The Message contains all the ids of streamers available on the server.
|
||||
* @param messageStreamerList - The message with the list of the available streamer ids.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
onStreamerList(messageStreamerList: MessageReceive.MessageStreamerList) {}
|
||||
|
||||
/**
|
||||
* The Message contains the new id of a subscribed to streamer.
|
||||
* @param message - Message conaining the new id of the streamer.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
onStreamerIDChanged(message: MessageReceive.MessageStreamerIDChanged) {}
|
||||
|
||||
/**
|
||||
* @param iceCandidate - Ice Candidate sent from the Signaling server server's RTC hand shake
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jss": "^10.9.2",
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"jss-plugin-global": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1",
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"cspell": "^4.1.0",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1"
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
|
|
@ -249,9 +249,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/-/lib-pixelstreamingfrontend-ue5.4-0.0.1.tgz",
|
||||
"integrity": "sha512-zlQupJOcnRGAE4SjfFH1lh0DK4KXOanXOGVo1h64CBWpfj8QBgUz1CWXYgG/X8V8p+ZDfkzz30LNqKtwII3krA==",
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/-/lib-pixelstreamingfrontend-ue5.4-0.0.3.tgz",
|
||||
"integrity": "sha512-Llp6aQHjQYg6eYlf8GBB60uQJ+/ueVCPrFnR7SP5muqjXKdBJPXn5hiZpG6tR9Z/soHCyxrudXtGhrObcbsSVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"sdp": "^3.1.0"
|
||||
|
|
@ -3799,9 +3799,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/-/lib-pixelstreamingfrontend-ue5.4-0.0.1.tgz",
|
||||
"integrity": "sha512-zlQupJOcnRGAE4SjfFH1lh0DK4KXOanXOGVo1h64CBWpfj8QBgUz1CWXYgG/X8V8p+ZDfkzz30LNqKtwII3krA==",
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/-/lib-pixelstreamingfrontend-ue5.4-0.0.3.tgz",
|
||||
"integrity": "sha512-Llp6aQHjQYg6eYlf8GBB60uQJ+/ueVCPrFnR7SP5muqjXKdBJPXn5hiZpG6tR9Z/soHCyxrudXtGhrObcbsSVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sdp": "^3.1.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"description": "Reference frontend UI library for Unreal Engine 5.4 Pixel Streaming - gives the stock look and feel.",
|
||||
"main": "dist/lib-pixelstreamingfrontend-ui.js",
|
||||
"module": "dist/lib-pixelstreamingfrontend-ui.esm.js",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"spellcheck": "cspell \"{README.md,.github/*.md,src/**/*.ts}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1",
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"cspell": "^4.1.0",
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"jss-plugin-global": "^10.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1"
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -46,4 +46,3 @@
|
|||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -324,8 +324,8 @@ export class Application {
|
|||
);
|
||||
this.stream.addEventListener(
|
||||
'webRtcDisconnected',
|
||||
({ data: { eventString, showActionOrErrorOnDisconnect } }) =>
|
||||
this.onDisconnect(eventString, showActionOrErrorOnDisconnect)
|
||||
({ data: { eventString, allowClickToReconnect } }) =>
|
||||
this.onDisconnect(eventString, allowClickToReconnect)
|
||||
);
|
||||
this.stream.addEventListener('videoInitialized', () =>
|
||||
this.onVideoInitialized()
|
||||
|
|
@ -366,8 +366,8 @@ export class Application {
|
|||
)
|
||||
this.stream.addEventListener(
|
||||
'streamerListMessage',
|
||||
({ data: { messageStreamerList, autoSelectedStreamerId } }) =>
|
||||
this.handleStreamerListMessage(messageStreamerList, autoSelectedStreamerId)
|
||||
({ data: { messageStreamerList, autoSelectedStreamerId, wantedStreamerId } }) =>
|
||||
this.handleStreamerListMessage(messageStreamerList, autoSelectedStreamerId, wantedStreamerId)
|
||||
);
|
||||
this.stream.addEventListener(
|
||||
'settingsChanged',
|
||||
|
|
@ -378,6 +378,14 @@ export class Application {
|
|||
({ data: { count }}) =>
|
||||
this.onPlayerCount(count)
|
||||
);
|
||||
this.stream.addEventListener(
|
||||
'webRtcTCPRelayDetected',
|
||||
({}) =>
|
||||
Logger.Warning(
|
||||
Logger.GetStackTrace(),
|
||||
`Stream quailty degraded due to network enviroment, stream is relayed over TCP.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -573,14 +581,14 @@ export class Application {
|
|||
/**
|
||||
* Event fired when the video is disconnected - displays the error overlay and resets the buttons stream tools upon disconnect
|
||||
* @param eventString - the event text that will be shown in the overlay
|
||||
* @param allowClickToReconnect - true if we want to allow the user to click to reconnect. Otherwise it's just a message.
|
||||
*/
|
||||
onDisconnect(eventString: string, showActionOrErrorOnDisconnect: boolean) {
|
||||
if (showActionOrErrorOnDisconnect == false) {
|
||||
this.showErrorOverlay(`Disconnected: ${eventString}`);
|
||||
onDisconnect(eventString: string, allowClickToReconnect: boolean) {
|
||||
const overlayMessage = 'Disconnected' + (eventString ? `: ${eventString}` : '');
|
||||
if (allowClickToReconnect) {
|
||||
this.showDisconnectOverlay(`${overlayMessage} Click To Restart.`);
|
||||
} else {
|
||||
this.showDisconnectOverlay(
|
||||
`Disconnected: ${eventString} <div class="clickableState">Click To Restart</div>`
|
||||
);
|
||||
this.showErrorOverlay(overlayMessage);
|
||||
}
|
||||
// disable starting a latency checks
|
||||
this.statsPanel?.onDisconnect();
|
||||
|
|
@ -667,18 +675,41 @@ export class Application {
|
|||
this.statsPanel?.handlePlayerCount(playerCount);
|
||||
}
|
||||
|
||||
handleStreamerListMessage(messageStreamingList: MessageStreamerList, autoSelectedStreamerId: string | null) {
|
||||
if (autoSelectedStreamerId === null) {
|
||||
if(messageStreamingList.ids.length === 0) {
|
||||
var message = 'No streamers connected. ' +
|
||||
(this.stream.config.isFlagEnabled(Flags.WaitForStreamer)
|
||||
? 'Waiting for streamer...'
|
||||
: '<div style="clickableState">Click To Restart</div>');
|
||||
handleStreamerListMessage(messageStreamingList: MessageStreamerList, autoSelectedStreamerId: string, wantedStreamerId: string) {
|
||||
const waitForStreamer = this.stream.config.isFlagEnabled(Flags.WaitForStreamer);
|
||||
const isReconnecting = this.stream.isReconnecting();
|
||||
let message: string = null;
|
||||
let allowRestart: boolean = true;
|
||||
|
||||
if (!autoSelectedStreamerId) {
|
||||
if (waitForStreamer && wantedStreamerId) {
|
||||
if (isReconnecting) {
|
||||
message = `Waiting for ${wantedStreamerId} to become available.`;
|
||||
allowRestart = false;
|
||||
} else {
|
||||
message = `Gave up waiting for ${wantedStreamerId} to become available. Click to try again`;
|
||||
if (messageStreamingList.ids.length > 0) {
|
||||
message += ` or select a streamer from the settings menu.`;
|
||||
}
|
||||
allowRestart = true;
|
||||
}
|
||||
} else if (messageStreamingList.ids.length == 0) {
|
||||
if (isReconnecting) {
|
||||
message = `Waiting for a streamer to become available.`;
|
||||
allowRestart = false;
|
||||
} else {
|
||||
message = `No streamers available. Click to try again.`;
|
||||
allowRestart = true;
|
||||
}
|
||||
} else {
|
||||
message = `Multiple streamers available. Select one from the settings menu.`;
|
||||
allowRestart = false;
|
||||
}
|
||||
|
||||
if (allowRestart) {
|
||||
this.showDisconnectOverlay(message);
|
||||
} else {
|
||||
this.showTextOverlay(
|
||||
'Multiple streamers detected. Use the dropdown in the settings menu to select the streamer'
|
||||
);
|
||||
this.showTextOverlay(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class SettingUINumber<
|
|||
this.spinner.onchange = (event: Event) => {
|
||||
const inputElem = event.target as HTMLInputElement;
|
||||
|
||||
const parsedValue = Number.parseInt(inputElem.value);
|
||||
const parsedValue = Number.parseFloat(inputElem.value);
|
||||
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
Logger.Warning(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { LatencyTest } from './LatencyTest';
|
||||
import { InitialSettings, Logger, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { CandidatePairStats, InitialSettings, Logger, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { AggregatedStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { MathUtils } from '../Util/MathUtils';
|
||||
import {DataChannelLatencyTest} from "./DataChannelLatencyTest";
|
||||
|
|
@ -318,14 +318,17 @@ export class StatsPanel {
|
|||
);
|
||||
}
|
||||
|
||||
// Store the active candidate pair return a new Candidate pair stat if getActiveCandidate is null
|
||||
let activeCandidatePair = stats.getActiveCandidatePair() != null ? stats.getActiveCandidatePair() : new CandidatePairStats();
|
||||
|
||||
// RTT
|
||||
const netRTT =
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
stats.candidatePair,
|
||||
activeCandidatePair,
|
||||
'currentRoundTripTime'
|
||||
) && stats.isNumber(stats.candidatePair.currentRoundTripTime)
|
||||
) && stats.isNumber(activeCandidatePair.currentRoundTripTime)
|
||||
? numberFormat.format(
|
||||
stats.candidatePair.currentRoundTripTime * 1000
|
||||
activeCandidatePair.currentRoundTripTime * 1000
|
||||
)
|
||||
: "Can't calculate";
|
||||
this.addOrUpdateStat('RTTStat', 'Net RTT (ms)', netRTT);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ module.exports = {
|
|||
})
|
||||
],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
globalObject: 'this'
|
||||
}
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright 2004-2022, Epic Games, Inc.
|
||||
Copyright 2004-2024, Epic Games, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
108
README.md
108
README.md
|
|
@ -1,107 +1,3 @@
|
|||
| Branch | | | | |
|
||||
| -------|--|--|--|--|
|
||||
| UE5.4 | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml) |
|
||||
| UE5.3 | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml) | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml) |
|
||||
| Master | [](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/run-library-unit-tests.yml) |
|
||||
# PixelStreamingInfrastructure has moved [here!](https://github.com/EpicGamesExt/PixelStreamingInfrastructure)
|
||||
|
||||
# The official home for the Pixel Streaming servers and frontend!
|
||||
The frontend and web server elements for Unreal Pixel Streaming (previously located in `Samples/PixelStreaming/WebServers`) are now in this repository, for all to contribute to. They are referred to as the **Pixel Streaming Infrastructure**.
|
||||
|
||||
## Goals
|
||||
|
||||
The goals of this repository are to:
|
||||
|
||||
- Increase the release cadence for the Pixel Streaming servers (to mitigate browser breaking changes sooner).
|
||||
- Encourage easier contribution of these components by Unreal Engine licensees.
|
||||
- Facilitate a more standard web release mechanism.
|
||||
- Grant a permissive license to distribute and modify this code wherever you see fit (MIT licensed).
|
||||
|
||||
## Contributing
|
||||
|
||||
If you would like to contribute to our repository, please reference our [contribution guide](CONTRIBUTING.md). Thank you for your time and your efforts!
|
||||
|
||||
## Contents
|
||||
|
||||
The Pixel Streaming Infrastructure contains reference implementations for all the components needed to run a pixel streaming application. They are structured as separate projects, which work together, but are designed to be modular and interoperable with other implementations which use WebRTC technology. These implementations include:
|
||||
- A signalling web server, called Cirrus, found in [`SignallingWebServer/`](SignallingWebServer/).
|
||||
- An SFU (Selective Forwarding Unit), found in [`SFU/`](SFU/).
|
||||
- A matchmaker, found in [`Matchmaker/`](Matchmaker/).
|
||||
- Several frontend projects for the WebRTC player and input, found in [`Frontend/`](Frontend/):
|
||||
- shared libraries for [communication](Frontend/library/) and [UI](Frontend/ui-library/) functionality
|
||||
- separate [implementations](Frontend/implementations/) using different techologies such as TypeScript or React/JSX
|
||||
|
||||
For detailed information, see the [/frontend](/Frontend/).
|
||||
|
||||
## Releases
|
||||
We release a number of different components under this repository, specifically:
|
||||
|
||||
- Container images for the signalling server
|
||||
- NPM packages for the frontend
|
||||
- Source releases of this repo with the reference frontend built as a minified js bundle
|
||||
|
||||
### Container images
|
||||
|
||||
The following container images are built from this repository:
|
||||
|
||||
- [ghcr.io/epicgames/pixel-streaming-signalling-server](https://github.com/orgs/EpicGames/packages/container/package/pixel-streaming-signalling-server) (since Unreal Engine 5.1)
|
||||
( This link requires you to join Epic's Github org )
|
||||
|
||||
### NPM Packages
|
||||
The following are `unofficial` NPM packages (official ones coming soon):
|
||||
|
||||
| Branch | Frontend library | Frontend reference ui |
|
||||
|--------|------------------|-----------------------|
|
||||
| UE5.3 |[lib-pixelstreamingfrontend-ue5.3](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3)|[lib-pixelstreamingfrontend-ui-ue5.3](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3)|
|
||||
|
||||
### NPM getting started
|
||||
|
||||
```bash
|
||||
#frontend (core lib)
|
||||
npm i @epicgames-ps/lib-pixelstreamingfrontend-ue5.3
|
||||
#frontend ui
|
||||
npm i @epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3
|
||||
```
|
||||
|
||||
## Documentation
|
||||
* [General Docs](/Docs/README.md)
|
||||
* [Frontend Docs](/Frontend/)
|
||||
* Signalling Server Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/255)
|
||||
* Matchmaker Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/256)
|
||||
* SFU Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/257)
|
||||
|
||||
### Tagged source releases + built typescript frontend
|
||||
|
||||
[Github releases](https://github.com/EpicGames/PixelStreamingInfrastructure/releases)
|
||||
|
||||
## Versions
|
||||
|
||||
We maintain versions of the servers and frontend that are compatible with existing and in-development version of Unreal Engine.
|
||||
|
||||
:warning: **There are breaking changes between UE versions - so make sure you get the right version**. :warning:
|
||||
|
||||
<ins>For a list of major changes between versions please refer to the [changelog](https://github.com/EpicGames/PixelStreamingInfrastructure/blob/master/CHANGELOG.md).</ins>
|
||||
|
||||
This repository contains the following in branches that track Unreal Engine versions:
|
||||
|
||||
| Branch | Status |
|
||||
|--------|--------|
|
||||
|[Master](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/master)| Dev |
|
||||
|[UE5.4](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.4)| Pre-release |
|
||||
|[UE5.3](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.3)| Current |
|
||||
|[UE5.2](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.2)| Supported |
|
||||
|[UE5.1](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.1)| End of life |
|
||||
|[UE5.0](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.0)| Unsupported |
|
||||
|[UE4.27](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.27)| Unsupported |
|
||||
|[UE4.26](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.26)| Unsupported |
|
||||
|
||||
| Legend | Meaning |
|
||||
|---------|-----------|
|
||||
| Dev | This is our dev branch, intended to be paired with [ue5-main](https://github.com/EpicGames/UnrealEngine/tree/ue5-main) - experimental. |
|
||||
|Pre-release| Code in here will be paired with the next UE release, we periodically update this branch from `master`. |
|
||||
| Current | Supported and this is the branch tracking the **latest released** version of UE. |
|
||||
| Supported | We will accept bugfixes/issues for this version. |
|
||||
| End of life | Once the next UE version is released we will not support this version anymore. |
|
||||
| Unsupported | We will not be supporting this version with bugfixes. |
|
||||
|
||||
## Legal
|
||||
© 2004-2023, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
For more details read [here](https://forums.unrealengine.com/t/migrating-optional-epic-games-git-repositories-to-new-github-organization/1718666).
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.0.1
|
||||
0.0.3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Use the current Long Term Support (LTS) version of Node.js
|
||||
FROM node:lts
|
||||
|
||||
COPY /SFU /SFU
|
||||
|
||||
RUN SFU/platform_scripts/bash/setup.sh
|
||||
|
||||
ENV SIGNALLING_URL ws://localhost:8889
|
||||
|
||||
EXPOSE 40000-49999/tcp
|
||||
EXPOSE 40000-49999/udp
|
||||
|
||||
CMD node /SFU/sfu_server.js --signallingURL=${SIGNALLING_URL}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Pixel Streaming Selective Forwarding Unit
|
||||
|
||||
The SFU (Selective Forwarding Unit) is a mechanism to allow distributing a single stream out to a large number of peers. This is useful because when peers connect directly to the streamer, resources must be allocated per peer to allow encoding of the stream. This means the resources can be quickly drained after only a handful of peers. The SFU can receive multiple streams using simulcast and selectively forward out streams to remote peers based on their available resources, without requiring to actually re-encode the stream.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is handled through the single config.js file.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| signallingURL | String | 'http://localhost:8889' | The URL pointing to the signalling server we want to connect to. |
|
||||
| SFUId | String | 'SFU' | The name this peer will be given that will then be displayed in the streamer list. Peers wishing to receive from this SFU should subscribe to this ID. |
|
||||
| subscribeStreamerId | String | 'DefaultStreamer' | This is the name of the streamer that this SFU should subscribe to and re-stream. |
|
||||
| retrySubscribeDelaySecs | Number | 10 | If subscribing to the given streamer fails, wait this many seconds before trying again. |
|
||||
| mediasoup | Object | | Mediasoup-related configuration options. See below. |
|
||||
|
||||
### Mediasoup related configuration options.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| worker | Object | | Worker-related configuration options. See below. |
|
||||
| router | Object | | Router-related configuration options. See below. |
|
||||
| webRtcTransport | Object | | WebRTC transport-related configuration options. See below. |
|
||||
|
||||
### Worker-related configuration options.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| rtcMinPort | Number | 40000 | Minimun RTC port for ICE, DTLS, RTP, etc. |
|
||||
| rtcMaxPort | Number | 49999 | Maximum RTC port for ICE, DTLS, RTP, etc. |
|
||||
| logLevel | String | 'debug' | The log level for the worker. See Mediasoup [docs](https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerLogLevel) |
|
||||
| logTags | Array<WorkerLogTag> | | The log tags to include in logs. See Mediasoup [docs](https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerLogTag) |
|
||||
|
||||
### Router-related configuration options.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| mediaCodecs | Array<RtpCodecCapability> | | Codecs to support. See Mediasoup [docs](https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/#RtpCodecCapability) |
|
||||
|
||||
### WebRTC transport-related configuration options.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| listenIps | Array<TransportListenIp|String> | | Listening IP address or addresses in order of preference (first one is the preferred one). See Mediasoup [docs](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp) |
|
||||
| initialAvailableOutgoingBitrate | Number | Initial available outgoing bitrate (in bps/bits per second). |
|
||||
|
||||
## Running
|
||||
|
||||
Several scripts are supplied for Windows and Linux in the [platform_scripts](platform_scripts/) folder. These are the easiest way to get the server running under common situations. They can also be used as a reference for new situations.
|
||||
|
||||
## Streaming from UE
|
||||
|
||||
The best way to fully utilize the SFU is to have a single streamer streaming simulcast to the SFU and then have peers subscribe to the SFU stream.
|
||||
|
||||
Launch the streaming app with the following arguments
|
||||
`-SimulcastParameters="1.0,5000000,20000000,2.0,1000000,5000000,4.0,50000,1000000"`
|
||||
This tells the Pixel Streaming plugin to stream simulcast with 3 streams, each one scaling video resolution by half. The sequence of values is as follows, `scale_down_factor,min_bitrate,max_bitrate,...repeating for each stream`
|
||||
|
||||
When this streams to the SFU, the SFU will detect these 3 streams and then selectively stream these out to connected peers based on their connection quality.
|
||||
|
||||
## Running the Docker image
|
||||
|
||||
The Docker image needs to know where the signalling server to connect to is. You will need to set the `SIGNALLING_URL` environment variable to the URL for your signalling server. This URL needs to point to the configured SFU port (default 8889).
|
||||
You will also need to use the `host` network driver on docker because of the way the SFU collects and reports its available ports.
|
||||
An example for running might be as follows.
|
||||
|
||||
```docker run -e SIGNALLING_URL=ws://192.168.1.10:8889 --network="host" ghcr.io/epicgames/pixel-streaming-sfu:5.4```
|
||||
|
|
@ -1,19 +1,35 @@
|
|||
{
|
||||
"name": "pixelstreaming-sfu",
|
||||
"name": "@epicgames-ps/pixelstreaming-sfu",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pixelstreaming-sfu",
|
||||
"name": "@epicgames-ps/pixelstreaming-sfu",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"mediasoup_prebuilt": "^3.8.4",
|
||||
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
|
||||
"minimist": "^1.2.8",
|
||||
"run-script-os": "^1.1.6",
|
||||
"ws": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"mediasoup-sdp-bridge": {
|
||||
"name": "@epicgames-ps/mediasoup-sdp-bridge",
|
||||
"version": "3.6.5",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mediasoup"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
|
|
@ -142,18 +158,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mediasoup-sdp-bridge": {
|
||||
"version": "3.6.5",
|
||||
"resolved": "file:mediasoup-sdp-bridge",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"resolved": "mediasoup-sdp-bridge",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mediasoup"
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
|
|
@ -333,11 +346,16 @@
|
|||
}
|
||||
},
|
||||
"mediasoup-sdp-bridge": {
|
||||
"version": "3.6.5",
|
||||
"version": "file:mediasoup-sdp-bridge",
|
||||
"requires": {
|
||||
"mediasoup-client": "^3.6.41"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"start-local": "run-script-os --",
|
||||
"start-local:windows": ".\\platform_scripts\\cmd\\run.bat",
|
||||
"start-local:default": "./platform_scripts/bash/run_local.sh",
|
||||
"start-local:windows": ".\\platform_scripts\\cmd\\run.bat",
|
||||
"start-local:default": "./platform_scripts/bash/run_local.sh",
|
||||
"start-cloud": "run-script-os --",
|
||||
"start-cloud:windows": ".\\platform_scripts\\cmd\\run_cloud.bat",
|
||||
"start-cloud:default": "./platform_scripts/bash/run_cloud.sh",
|
||||
"start-cloud:windows": ".\\platform_scripts\\cmd\\run_cloud.bat",
|
||||
"start-cloud:default": "./platform_scripts/bash/run_cloud.sh",
|
||||
"start": "run-script-os",
|
||||
"start:windows": "platform_scripts\\cmd\\node\\node.exe sfu_server.js",
|
||||
"start:default": "if [ `id -u` -eq 0 ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node sfu_server.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node sfu_server.js\"\nfi\n$process "
|
||||
|
||||
"start:windows": "platform_scripts\\cmd\\node\\node.exe sfu_server.js",
|
||||
"start:default": "if [ `id -u` -eq 0 ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node sfu_server.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node sfu_server.js\"\nfi\n$process "
|
||||
},
|
||||
"dependencies": {
|
||||
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
|
||||
"ws": "^7.1.2",
|
||||
"mediasoup_prebuilt": "^3.8.4",
|
||||
"run-script-os": "^1.1.6"
|
||||
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
|
||||
"minimist": "^1.2.8",
|
||||
"run-script-os": "^1.1.6",
|
||||
"ws": "^7.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const config = require('./config');
|
|||
const WebSocket = require('ws');
|
||||
const mediasoup = require('mediasoup_prebuilt');
|
||||
const mediasoupSdp = require('mediasoup-sdp-bridge');
|
||||
const minimist = require('minimist');
|
||||
|
||||
if (!config.retrySubscribeDelaySecs) {
|
||||
config.retrySubscribeDelaySecs = 10;
|
||||
|
|
@ -351,6 +352,12 @@ async function createWebRtcTransport(identifier) {
|
|||
}
|
||||
|
||||
async function main() {
|
||||
var argv = minimist(process.argv.slice(2));
|
||||
|
||||
if ('signallingURL' in argv) {
|
||||
config.signallingURL = argv['signallingURL'];
|
||||
}
|
||||
|
||||
console.log('Starting Mediasoup...');
|
||||
console.log("Config = ");
|
||||
console.log(config);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
The following is a complete reference to the current signalling server messaging protocol. These messages are sent as stringified JSON packets. Some parameters are JSON strings themselves and require escape sequences to be contained in the string parameter.
|
||||
|
||||
## Version
|
||||
1.0.0 - Current
|
||||
1.1.0 - Current
|
||||
|
||||
Major version number - breaking protocol change such as a required new message or field or deleting an existing message.
|
||||
Minor version number - independent new message.
|
||||
|
|
@ -30,6 +30,7 @@ Hotfix version - a non-breaking new field in an existing message type.
|
|||
- [Signalling Server Sent Messages](#source-signalling)
|
||||
- [config](#signalling-config)
|
||||
- [identify](#signalling-identify)
|
||||
- [streamerIDChanged](#signalling-streameridchanged)
|
||||
- [playerConnected](#signalling-playerconnected)
|
||||
- [playerCount](#signalling-playercount)
|
||||
- [playerDisconnected](#signalling-playerdisconnected)
|
||||
|
|
@ -229,6 +230,14 @@ end
|
|||
| Param Name | Type | Description |
|
||||
|-|-|-|
|
||||
|
||||
### streamerIDChanged<a name="signalling-streameridchanged"></a>
|
||||
|
||||
> Message is used to communicate to [Player](#term-player)s that the [Streamer](#term-streamer) it is currently subscribed to is changing its ID. This allows Players to keep track of its currently subscribed Streamer and allow auto reconnects to the correct Streamer. This happens if a Streamer sends an [endpointID](#streamer-endpointid) message after it already has an ID assigned. (Can happen if it is late to respond to the [identify](#signalling-identify) message and is auto assigned a legacy ID.)
|
||||
|
||||
| Param Name | Type | Description |
|
||||
|-|-|-|
|
||||
| newID | string | The new ID of the subscribed to Streamer |
|
||||
|
||||
### playerConnected<a name="signalling-playerconnected"></a>
|
||||
|
||||
> Message is used to notify a [Streamer](#term-streamer) that a new [Player](#term-player) has subscribed to the stream.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# Pixel Streaming Signalling Server
|
||||
|
||||
The signalling server is a small intermediary application that sits between streamers and other peers. It handles the initial connection negotiations and some other small ongoing control messages between peers as well as acting as a simple web server for serving the [Frontend](/Frontend/README.md) web application.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration of the signalling server is handled via the config.js file in the SignallingWebServer directory. The following are its supported options.
|
||||
| Name | Type | Default | Description |
|
||||
|-|-|-|-|
|
||||
| UseFrontend | Boolean | false | Enables or disables the use of the Frontend. |
|
||||
| UseMatchmaker | Boolean | false | Enables or disables the use of the [Matchmaker](/Matchmaker) application. |
|
||||
| UseHTTPS | Boolean | false | Enables or disables ssl for the serving of the Frontend. |
|
||||
| HTTPSCertFile | String | './certificates/client-cert.pem' | The path to the SSL cert file for when HTTPS is enabled. |
|
||||
| HTTPSKeyFile | String | './certificates/client-key.pem' | The path to the SSL key file for when HTTPS is enabled. |
|
||||
| LogToFile | Boolean | true | Enable or disable logging to a file in the 'logs' folder. |
|
||||
| LogVerbose | Boolean | true | Enable or disable verbose logging. Adds a lot of extra information to logs. |
|
||||
| HomepageFile | String | 'player.html' | The root file of the frontend web application. |
|
||||
| AdditionalRoutes | Map | | Additional routes for the web application. |
|
||||
| EnableWebserver | Boolean | true | Enables or disables the serving of the frontend through the internal web server. Disbable this if you are serving your own frontend. |
|
||||
| MatchmakerAddress | String | | The IP/hostname of the matchmaker application. |
|
||||
| MatchmakerPort | Number | 9999 | The port the matchmaker is listening on. |
|
||||
| PublicIp | String | "localhost" | The public IP/hostname of the host that the signalling server is listening on. This is used by the matchmaker. |
|
||||
| HttpPort | Number | 80 | The port for the internal webserver to listen on. |
|
||||
| HttpsPort | Number | 443 | The port for the internal webserver to listen on when HTTPS is enabled. |
|
||||
| StreamerPort | Number | 8888 | The port to listen on for new streamer connections. |
|
||||
| SFUPort | Number | 8889 | The port to listen on for new SFU connections. |
|
||||
| MaxPlayerCount | Number | -1 | A limit for connected players in total on this signalling server. -1 to disable limit. |
|
||||
| DisableSSLCert | Boolean | true | When HTTPS is enabled and this is true, insecure certificates can be used. This is convenient for local testing but please DO NOT SHIP THIS IN PRODUCTION |
|
||||
|
||||
## Running
|
||||
|
||||
Several scripts are supplied for Windows and Linux in the [platform_scripts](platform_scripts/) folder. These are the easiest way to get the server running under common situations. They can also be used as a reference for new situations.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Getting Started
|
||||
|
||||
## Running
|
||||
Run `node cirrus.js` or `platform_scripts/cmd/run_local.bat|.sh`
|
||||
|
||||
## Changing the frontend
|
||||
Replace the contents of `/Public`
|
||||
|
||||
# Documentation
|
||||
[Signalling Protocol](./Docs/SignallingProtocol.md)
|
||||
|
|
@ -20,7 +20,6 @@ const defaultConfig = {
|
|||
UseHTTPS: false,
|
||||
HTTPSCertFile: './certificates/client-cert.pem',
|
||||
HTTPSKeyFile: './certificates/client-key.pem',
|
||||
UseAuthentication: false,
|
||||
LogToFile: true,
|
||||
LogVerbose: true,
|
||||
HomepageFile: 'player.html',
|
||||
|
|
@ -60,18 +59,6 @@ if (config.UseHTTPS) {
|
|||
var https = require('https').Server(options, app);
|
||||
}
|
||||
|
||||
//If not using authetication then just move on to the next function/middleware
|
||||
var isAuthenticated = redirectUrl => function (req, res, next) { return next(); }
|
||||
|
||||
if (config.UseAuthentication && config.UseHTTPS) {
|
||||
var passport = require('passport');
|
||||
require('./modules/authentication').init(app);
|
||||
// Replace the isAuthenticated with the one setup on passport module
|
||||
isAuthenticated = passport.authenticationMiddleware ? passport.authenticationMiddleware : isAuthenticated
|
||||
} else if (config.UseAuthentication && !config.UseHTTPS) {
|
||||
console.error('Trying to use authentication without using HTTPS, this is not allowed and so authentication will NOT be turned on, please turn on HTTPS to turn on authentication');
|
||||
}
|
||||
|
||||
const helmet = require('helmet');
|
||||
var hsts = require('hsts');
|
||||
var net = require('net');
|
||||
|
|
@ -205,44 +192,19 @@ var limiter = RateLimit({
|
|||
// apply rate limiter to all requests
|
||||
app.use(limiter);
|
||||
|
||||
//Setup the login page if we are using authentication
|
||||
if(config.UseAuthentication){
|
||||
if(config.EnableWebserver) {
|
||||
app.get('/login', function(req, res){
|
||||
res.sendFile(path.join(__dirname, '/Public', '/login.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// create application/x-www-form-urlencoded parser
|
||||
var urlencodedParser = bodyParser.urlencoded({ extended: false })
|
||||
|
||||
//login page form data is posted here
|
||||
app.post('/login',
|
||||
urlencodedParser,
|
||||
passport.authenticate('local', { failureRedirect: '/login' }),
|
||||
function(req, res){
|
||||
//On success try to redirect to the page that they originally tired to get to, default to '/' if no redirect was found
|
||||
var redirectTo = req.session.redirectTo ? req.session.redirectTo : '/';
|
||||
delete req.session.redirectTo;
|
||||
console.log(`Redirecting to: '${redirectTo}'`);
|
||||
res.redirect(redirectTo);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if(config.EnableWebserver) {
|
||||
//Setup folders
|
||||
app.use(express.static(path.join(__dirname, '/Public')))
|
||||
app.use('/images', express.static(path.join(__dirname, './images')))
|
||||
app.use('/scripts', [isAuthenticated('/login'),express.static(path.join(__dirname, '/scripts'))]);
|
||||
app.use('/', [isAuthenticated('/login'), express.static(path.join(__dirname, '/custom_html'))])
|
||||
app.use('/scripts', express.static(path.join(__dirname, '/scripts')));
|
||||
app.use('/', express.static(path.join(__dirname, '/custom_html')))
|
||||
}
|
||||
|
||||
try {
|
||||
for (var property in config.AdditionalRoutes) {
|
||||
if (config.AdditionalRoutes.hasOwnProperty(property)) {
|
||||
console.log(`Adding additional routes "${property}" -> "${config.AdditionalRoutes[property]}"`)
|
||||
app.use(property, [isAuthenticated('/login'), express.static(path.join(__dirname, config.AdditionalRoutes[property]))]);
|
||||
app.use(property, express.static(path.join(__dirname, config.AdditionalRoutes[property])));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -252,7 +214,7 @@ try {
|
|||
if(config.EnableWebserver) {
|
||||
|
||||
// Request has been sent to site root, send the homepage file
|
||||
app.get('/', isAuthenticated('/login'), function (req, res) {
|
||||
app.get('/', function (req, res) {
|
||||
homepageFile = (typeof config.HomepageFile != 'undefined' && config.HomepageFile != '') ? config.HomepageFile.toString() : defaultConfig.HomepageFile;
|
||||
|
||||
let pathsToTry = [ path.join(__dirname, homepageFile), path.join(__dirname, '/Public', homepageFile), path.join(__dirname, '/custom_html', homepageFile), homepageFile ];
|
||||
|
|
@ -529,7 +491,7 @@ function requestStreamerId(streamer) {
|
|||
|
||||
streamer.idTimer = setTimeout(function() {
|
||||
// streamer did not respond in time. give it a legacy id.
|
||||
const newLegacyId = getUniqueLegacyId();
|
||||
const newLegacyId = getUniqueLegacyStreamerId();
|
||||
if (newLegacyId.length == 0) {
|
||||
const error = `Ran out of legacy ids.`;
|
||||
console.error(error);
|
||||
|
|
@ -562,6 +524,20 @@ function sanitizeStreamerId(id) {
|
|||
}
|
||||
|
||||
function registerStreamer(id, streamer) {
|
||||
// remove any existing streamer id
|
||||
if (!!streamer.id) {
|
||||
// notify any connected peers of rename
|
||||
const renameMessage = { type: "streamerIDChanged", newID: id };
|
||||
let clone = new Map(players);
|
||||
for (let player of clone.values()) {
|
||||
if (player.streamerId == streamer.id) {
|
||||
logOutgoing(player.id, renameMessage);
|
||||
player.sendTo(renameMessage);
|
||||
player.streamerId = id; // reassign the subscription
|
||||
}
|
||||
}
|
||||
streamers.delete(streamer.id);
|
||||
}
|
||||
// make sure the id is unique
|
||||
const uniqueId = sanitizeStreamerId(id);
|
||||
streamer.commitId(uniqueId);
|
||||
|
|
@ -574,6 +550,10 @@ function registerStreamer(id, streamer) {
|
|||
}
|
||||
|
||||
function onStreamerDisconnected(streamer) {
|
||||
if (!!streamer.idTimer) {
|
||||
clearTimeout(streamer.idTimer);
|
||||
}
|
||||
|
||||
if (!streamer.id || !streamers.has(streamer.id)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -683,13 +663,16 @@ streamerServer.on('connection', function (ws, req) {
|
|||
console.error(`streamer ${streamer.id} connection error: ${error}`);
|
||||
onStreamerDisconnected(streamer);
|
||||
try {
|
||||
ws.close(1006 /* abnormal closure */, error);
|
||||
ws.close(1006 /* abnormal closure */, `streamer ${streamer.id} connection error: ${error}`);
|
||||
} catch(err) {
|
||||
console.error(`ERROR: ws.on error: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify(clientConfig));
|
||||
const configStr = JSON.stringify(clientConfig);
|
||||
logOutgoing(streamer.id, configStr)
|
||||
ws.send(configStr);
|
||||
|
||||
requestStreamerId(streamer);
|
||||
});
|
||||
|
||||
|
|
@ -847,7 +830,7 @@ sfuServer.on('connection', function (ws, req) {
|
|||
console.error(`SFU connection error: ${error}`);
|
||||
onSFUDisconnected(playerComponent);
|
||||
try {
|
||||
ws.close(1006 /* abnormal closure */, error);
|
||||
ws.close(1006 /* abnormal closure */, `SFU connection error: ${error}`);
|
||||
} catch(err) {
|
||||
console.error(`ERROR: ws.on error: ${err.message}`);
|
||||
}
|
||||
|
|
@ -975,7 +958,7 @@ playerServer.on('connection', function (ws, req) {
|
|||
|
||||
ws.on('error', function(error) {
|
||||
console.error(`player ${playerId} connection error: ${error}`);
|
||||
ws.close(1006 /* abnormal closure */, error);
|
||||
ws.close(1006 /* abnormal closure */, `player ${playerId} connection error: ${error}`);
|
||||
onPlayerDisconnected(playerId);
|
||||
|
||||
console.logColor(logging.Red, `Trying to reconnect...`);
|
||||
|
|
@ -984,7 +967,11 @@ playerServer.on('connection', function (ws, req) {
|
|||
|
||||
sendPlayerConnectedToFrontend();
|
||||
sendPlayerConnectedToMatchmaker();
|
||||
player.ws.send(JSON.stringify(clientConfig));
|
||||
|
||||
const configStr = JSON.stringify(clientConfig);
|
||||
logOutgoing(player.id, configStr)
|
||||
player.ws.send(configStr);
|
||||
|
||||
sendPlayersCount();
|
||||
});
|
||||
|
||||
|
|
@ -992,7 +979,7 @@ function disconnectAllPlayers(streamerId) {
|
|||
console.log(`unsubscribing all players on ${streamerId}`);
|
||||
let clone = new Map(players);
|
||||
for (let player of clone.values()) {
|
||||
if (player.streamerId == streamerId) {
|
||||
if (player.streamerId == streamerId) {
|
||||
// disconnect players but just unsubscribe the SFU
|
||||
const sfuPlayer = getSFUForStreamer(streamerId);
|
||||
if (sfuPlayer && player.id == sfuPlayer.id) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
"UseFrontend": false,
|
||||
"UseMatchmaker": false,
|
||||
"UseHTTPS": false,
|
||||
"UseAuthentication": false,
|
||||
"LogToFile": true,
|
||||
"LogVerbose": true,
|
||||
"HomepageFile": "player.html",
|
||||
|
|
|
|||
|
|
@ -7,18 +7,6 @@ var loggers=[];
|
|||
var logFunctions=[];
|
||||
var logColorFunctions=[];
|
||||
|
||||
console.log = function(msg, ...args) {
|
||||
logFunctions.forEach((logFunction) => {
|
||||
logFunction(msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
console.logColor = function(color, msg, ...args) {
|
||||
logColorFunctions.forEach((logColorFunction) => {
|
||||
logColorFunction(color, msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
const AllAttributesOff = '\x1b[0m';
|
||||
const BoldOn = '\x1b[1m';
|
||||
const Black = '\x1b[30m';
|
||||
|
|
@ -31,6 +19,30 @@ const Cyan = '\x1b[36m';
|
|||
const White = '\x1b[37m';
|
||||
const Orange = '\x1b[38;5;215m';
|
||||
|
||||
console.log = function(msg, ...args) {
|
||||
logFunctions.forEach((logFunction) => {
|
||||
logFunction(msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
console.warn = function(msg, ...args) {
|
||||
logColorFunctions.forEach((logColorFunction) => {
|
||||
logColorFunction(Yellow, msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
console.error = function(msg, ...args) {
|
||||
logColorFunctions.forEach((logColorFunction) => {
|
||||
logColorFunction(Red, msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
console.logColor = function(color, msg, ...args) {
|
||||
logColorFunctions.forEach((logColorFunction) => {
|
||||
logColorFunction(color, msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad the start of the given number with zeros so it takes up the number of digits.
|
||||
* e.g. zeroPad(5, 3) = '005' and zeroPad(23, 2) = '23'.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -75,10 +75,11 @@ function use_args() {
|
|||
}
|
||||
|
||||
function call_setup_sh() {
|
||||
bash "setup.sh"
|
||||
bash "setup.sh" $*
|
||||
}
|
||||
|
||||
function start_process() {
|
||||
export NO_SUDO=$NO_SUDO
|
||||
if [ ! -z $NO_SUDO ]; then
|
||||
log_msg "running with sudo removed"
|
||||
eval $(echo "$@" | sed 's/sudo//g')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values "n" "n" # No server specific defaults
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
use_args "$*"
|
||||
call_setup_sh $*
|
||||
print_parameters
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
|
|
@ -34,3 +34,4 @@ start_process $process $arguments
|
|||
popd > /dev/null # ../..
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
|
||||
NODE_VERSION=v18.17.0
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
use_args $@
|
||||
use_args $*
|
||||
# Azure specific fix to allow installing NodeJS from NodeSource
|
||||
if test -f "/etc/apt/sources.list.d/azure-cli.list"; then
|
||||
sudo touch /etc/apt/sources.list.d/nodesource.list
|
||||
|
|
|
|||
Loading…
Reference in New Issue