Compare commits
154 Commits
v4.6.2-cot
...
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 | |
|
|
da0a871234 | |
|
|
b16bb6c75c | |
|
|
22de24e61a | |
|
|
fdb6d6d8a0 | |
|
|
3d7bfb0723 | |
|
|
63f8a320e1 | |
|
|
78d68a8a8b | |
|
|
e88871c226 | |
|
|
48b256753a | |
|
|
ddb4c4776e | |
|
|
f8de08ab61 | |
|
|
1e5d075d5f | |
|
|
25024f6848 | |
|
|
7f07f4b29e | |
|
|
f2cde5176a | |
|
|
5c65721da3 | |
|
|
ea74d91658 | |
|
|
b97dcb11cd | |
|
|
339aa088cc | |
|
|
403fe39f4b | |
|
|
adfca6c42d | |
|
|
bbcfe8a6b5 | |
|
|
2ce53023ee | |
|
|
090cc89b08 | |
|
|
c0e715ca9d | |
|
|
01d8056bee | |
|
|
127feac2e4 | |
|
|
1478eceb9b | |
|
|
d8007a3530 | |
|
|
23eb2601e1 | |
|
|
52f8a17e48 | |
|
|
ac6cafae85 | |
|
|
df2ef8ba04 | |
|
|
8a6a5bbfc5 | |
|
|
c76284041e | |
|
|
2a21ee6566 | |
|
|
bf6dcade68 | |
|
|
ee82bd398c | |
|
|
952b309c71 | |
|
|
16d80e27e1 | |
|
|
1301fde89a | |
|
|
ad85b02b5e | |
|
|
828123c6c8 | |
|
|
75cd975400 | |
|
|
3816f4b535 | |
|
|
f6d724a3fe | |
|
|
cabf32a879 | |
|
|
24b8082f35 | |
|
|
f7c4fd2f3c | |
|
|
e6797144d4 |
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ This page will be updated with new features and commands as they become availabl
|
|||
| **Browser send offer** | The browser will start the WebRTC handshake instead of the Unreal Engine application. This is an advanced setting for users customising the frontend. Primarily for backwards compatibility for 4.x versions of the engine. |
|
||||
| **Use microphone** | Will start receiving audio input from your microphone and transmit it to the Unreal Engine. |
|
||||
| **Start video muted** | Muted audio when the stream starts. |
|
||||
| **Prefer SFU** | Will attempt to use the Selective Forwarding Unit (SFU), if you have one running. |
|
||||
| **Is quality controller?** | Makes the encoder of the Pixel Streaming Plugin use the current browser connection to determine the bandwidth available, and therefore the quality of the stream encoding. **See notes below** |
|
||||
| **Force mono audio** | Force the browser to request mono audio in the SDP. |
|
||||
| **Force TURN** | Will attempt to connect exclusively via the TURN server. Will not work without an active TURN server. |
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3",
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3",
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"css-loader": "^6.7.3",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export class Flags {
|
|||
static FakeMouseWithTouches = 'FakeMouseWithTouches' as const;
|
||||
static IsQualityController = 'ControlsQuality' as const;
|
||||
static MatchViewportResolution = 'MatchViewportRes' as const;
|
||||
static PreferSFU = 'preferSFU' as const;
|
||||
static StartVideoMuted = 'StartVideoMuted' as const;
|
||||
static SuppressBrowserKeys = 'SuppressBrowserKeys' as const;
|
||||
static UseMic = 'UseMic' as const;
|
||||
|
|
@ -157,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,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
|
||||
*/
|
||||
|
|
@ -185,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
|
||||
)
|
||||
);
|
||||
|
|
@ -202,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
|
||||
)
|
||||
|
|
@ -218,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
|
||||
)
|
||||
);
|
||||
|
|
@ -255,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
|
||||
)
|
||||
);
|
||||
|
|
@ -266,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
|
||||
)
|
||||
);
|
||||
|
|
@ -277,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
|
||||
)
|
||||
);
|
||||
|
|
@ -288,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
|
||||
)
|
||||
);
|
||||
|
|
@ -299,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
|
||||
)
|
||||
);
|
||||
|
|
@ -310,18 +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,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
||||
this.flags.set(
|
||||
Flags.PreferSFU,
|
||||
new SettingFlag(
|
||||
Flags.PreferSFU,
|
||||
'Prefer SFU',
|
||||
'Try to connect to the SFU instead of P2P.',
|
||||
false,
|
||||
settings && settings.hasOwnProperty(Flags.SuppressBrowserKeys) ?
|
||||
settings[Flags.SuppressBrowserKeys] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -332,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
|
||||
)
|
||||
);
|
||||
|
|
@ -343,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
|
||||
)
|
||||
);
|
||||
|
|
@ -354,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
|
||||
)
|
||||
);
|
||||
|
|
@ -365,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
|
||||
)
|
||||
);
|
||||
|
|
@ -376,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
|
||||
)
|
||||
);
|
||||
|
|
@ -387,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`;
|
||||
|
|
@ -401,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
|
||||
)
|
||||
);
|
||||
|
|
@ -412,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
|
||||
)
|
||||
);
|
||||
|
|
@ -423,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
|
||||
)
|
||||
);
|
||||
|
|
@ -434,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
|
||||
)
|
||||
);
|
||||
|
|
@ -445,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
|
||||
)
|
||||
);
|
||||
|
|
@ -456,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
|
||||
)
|
||||
);
|
||||
|
|
@ -467,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
|
||||
)
|
||||
);
|
||||
|
|
@ -484,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
|
||||
)
|
||||
);
|
||||
|
|
@ -497,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
|
||||
)
|
||||
);
|
||||
|
|
@ -510,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
|
||||
)
|
||||
);
|
||||
|
|
@ -523,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
|
||||
)
|
||||
);
|
||||
|
|
@ -536,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
|
||||
)
|
||||
);
|
||||
|
|
@ -549,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
|
||||
)
|
||||
);
|
||||
|
|
@ -562,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
|
||||
)
|
||||
);
|
||||
|
|
@ -575,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
|
||||
)
|
||||
);
|
||||
|
|
@ -754,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -774,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);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touch - the activating touch event
|
||||
*/
|
||||
onTouchStart(touch: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || touch.target !== this.videoElementProvider.getVideoElement()) {
|
||||
return;
|
||||
}
|
||||
if (this.fakeTouchFinger == null) {
|
||||
|
|
@ -108,7 +108,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touchEvent - the activating touch event
|
||||
*/
|
||||
onTouchEnd(touchEvent: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
|
||||
return;
|
||||
}
|
||||
const videoElementParent =
|
||||
|
|
@ -144,7 +144,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touchEvent - the activating touch event
|
||||
*/
|
||||
onTouchMove(touchEvent: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
|
||||
return;
|
||||
}
|
||||
const toStreamerHandlers =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export class PeerConnectionController {
|
|||
this.onVideoStats(this.aggregatedStats);
|
||||
|
||||
// Update the preferred codec selection based on what was actually negotiated
|
||||
if (this.updateCodecSelection) {
|
||||
if (this.updateCodecSelection && !!this.aggregatedStats.inboundVideoStats.codecId) {
|
||||
this.config.setOptionSettingValue(
|
||||
OptionParameters.PreferredCodec,
|
||||
this.aggregatedStats.codecs.get(
|
||||
|
|
@ -370,7 +370,7 @@ export class PeerConnectionController {
|
|||
if (RTCRtpReceiver.getCapabilities && this.preferredCodec != '') {
|
||||
for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
|
||||
if (
|
||||
transceiver &&
|
||||
transceiver &&
|
||||
transceiver.receiver &&
|
||||
transceiver.receiver.track &&
|
||||
transceiver.receiver.track.kind === 'video' &&
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export class StreamController {
|
|||
constructor(videoElementProvider: VideoPlayer) {
|
||||
this.videoElementProvider = videoElementProvider;
|
||||
this.audioElement = document.createElement('Audio') as HTMLAudioElement;
|
||||
this.videoElementProvider.setAudioElement(this.audioElement);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ declare global {
|
|||
export class VideoPlayer {
|
||||
private config: Config;
|
||||
private videoElement: HTMLVideoElement;
|
||||
private audioElement?: HTMLAudioElement;
|
||||
private orientationChangeTimeout: number;
|
||||
private lastTimeResized = new Date().getTime();
|
||||
|
||||
|
|
@ -52,8 +53,11 @@ export class VideoPlayer {
|
|||
);
|
||||
};
|
||||
|
||||
// set play for video
|
||||
// set play for video (and audio)
|
||||
this.videoElement.onclick = () => {
|
||||
if (this.audioElement != undefined && this.audioElement.paused) {
|
||||
this.audioElement.play();
|
||||
}
|
||||
if (this.videoElement.paused) {
|
||||
this.videoElement.play();
|
||||
}
|
||||
|
|
@ -70,6 +74,10 @@ export class VideoPlayer {
|
|||
);
|
||||
}
|
||||
|
||||
public setAudioElement(audioElement: HTMLAudioElement) : void {
|
||||
this.audioElement = audioElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the video element with any application config and plays the video element.
|
||||
* @returns A promise for if playing the video was successful or not.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -1109,26 +1093,30 @@ export class WebRtcPlayerController {
|
|||
this.pixelStreaming.dispatchEvent(new PlayStreamEvent());
|
||||
|
||||
if (this.streamController.audioElement.srcObject) {
|
||||
this.streamController.audioElement.muted =
|
||||
this.config.isFlagEnabled(Flags.StartVideoMuted);
|
||||
const startMuted = this.config.isFlagEnabled(Flags.StartVideoMuted)
|
||||
this.streamController.audioElement.muted = startMuted;
|
||||
|
||||
this.streamController.audioElement
|
||||
.play()
|
||||
.then(() => {
|
||||
this.playVideo();
|
||||
})
|
||||
.catch((onRejectedReason) => {
|
||||
Logger.Log(Logger.GetStackTrace(), onRejectedReason);
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.'
|
||||
);
|
||||
this.pixelStreaming.dispatchEvent(
|
||||
new PlayStreamRejectedEvent({
|
||||
reason: onRejectedReason
|
||||
})
|
||||
);
|
||||
});
|
||||
if (startMuted) {
|
||||
this.playVideo();
|
||||
} else {
|
||||
this.streamController.audioElement
|
||||
.play()
|
||||
.then(() => {
|
||||
this.playVideo();
|
||||
})
|
||||
.catch((onRejectedReason) => {
|
||||
Logger.Log(Logger.GetStackTrace(), onRejectedReason);
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.'
|
||||
);
|
||||
this.pixelStreaming.dispatchEvent(
|
||||
new PlayStreamRejectedEvent({
|
||||
reason: onRejectedReason
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.playVideo();
|
||||
}
|
||||
|
|
@ -1172,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);
|
||||
}
|
||||
|
|
@ -1194,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1339,89 +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();
|
||||
|
||||
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(
|
||||
OptionParameters.StreamerId,
|
||||
settingOptions
|
||||
);
|
||||
let wantedStreamerId: string = null;
|
||||
|
||||
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 (
|
||||
this.config.isFlagEnabled(Flags.PreferSFU) &&
|
||||
messageStreamerList.ids.includes('SFU')
|
||||
) {
|
||||
// If the SFU toggle is on and there's an SFU connected, subscribe to it regardless of what is in the URL
|
||||
autoSelectedStreamerId = 'SFU';
|
||||
} 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 (this.config.isFlagEnabled(Flags.WaitForStreamer)) {
|
||||
this.startAutoJoinTimer()
|
||||
// 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,
|
||||
autoSelectedStreamerId
|
||||
);
|
||||
} 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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1583,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1596,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();
|
||||
|
|
@ -1618,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();
|
||||
}
|
||||
|
||||
|
|
@ -1635,7 +1672,7 @@ export class WebRtcPlayerController {
|
|||
* Close all connections
|
||||
*/
|
||||
close() {
|
||||
this.closeSignalingServer();
|
||||
this.closeSignalingServer('');
|
||||
this.closePeerConnection();
|
||||
}
|
||||
|
||||
|
|
@ -2019,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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,10 +174,6 @@ export class ConfigUI {
|
|||
psSettingsSection,
|
||||
this.flagsUi.get(Flags.StartVideoMuted)
|
||||
);
|
||||
this.addSettingFlag(
|
||||
psSettingsSection,
|
||||
this.flagsUi.get(Flags.PreferSFU)
|
||||
);
|
||||
this.addSettingFlag(
|
||||
psSettingsSection,
|
||||
this.flagsUi.get(Flags.IsQualityController)
|
||||
|
|
|
|||
|
|
@ -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,7 +1,7 @@
|
|||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ echo "Starting Matchmaker use ctrl-c to exit"
|
|||
echo "-----------------------------------------"
|
||||
echo ""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process
|
||||
|
||||
popd > /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) |
|
||||
|
||||
# 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**.
|
||||
# PixelStreamingInfrastructure has moved [here!](https://github.com/EpicGamesExt/PixelStreamingInfrastructure)
|
||||
|
||||
## 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 readme](Frontend/README.md).
|
||||
|
||||
## 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/Docs/README.md)
|
||||
* 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```
|
||||
|
|
@ -11,8 +11,18 @@ for(let arg of process.argv){
|
|||
}
|
||||
|
||||
const config = {
|
||||
// The URL of the signalling server to connect to
|
||||
signallingURL: "ws://localhost:8889",
|
||||
|
||||
// The ID for this SFU to use. This will show up as a streamer ID on the signalling server
|
||||
SFUId: "SFU",
|
||||
|
||||
// The ID of the streamer to subscribe to. If you leave this blank it will subscribe to the first streamer it sees.
|
||||
subscribeStreamerId: "DefaultStreamer",
|
||||
|
||||
// Delay between list requests when looking for a specifc streamer.
|
||||
retrySubscribeDelaySecs: 10,
|
||||
|
||||
mediasoup: {
|
||||
worker: {
|
||||
rtcMinPort: 40000,
|
||||
|
|
|
|||
|
|
@ -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,11 @@ 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;
|
||||
}
|
||||
|
||||
let signalServer = null;
|
||||
let mediasoupRouter;
|
||||
|
|
@ -24,6 +29,35 @@ function connectSignalling(server) {
|
|||
});
|
||||
}
|
||||
|
||||
async function onStreamerList(msg) {
|
||||
let success = false;
|
||||
|
||||
// subscribe to either the configured streamer, or if not configured, just grab the first id
|
||||
if (msg.ids.length > 0) {
|
||||
if (!!config.subscribeStreamerId && config.subscribeStreamerId.length != 0) {
|
||||
if (msg.ids.includes(config.subscribeStreamerId)) {
|
||||
signalServer.send(JSON.stringify({type: 'subscribe', streamerId: config.subscribeStreamerId}));
|
||||
success = true;
|
||||
}
|
||||
} else {
|
||||
signalServer.send(JSON.stringify({type: 'subscribe', streamerId: msg.ids[0]}));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
// did not subscribe to anything
|
||||
setTimeout(function() {
|
||||
signalServer.send(JSON.stringify({type: 'listStreamers'}));
|
||||
}, config.retrySubscribeDelaySecs * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function onIdentify(msg) {
|
||||
signalServer.send(JSON.stringify({type: 'endpointId', id: config.SFUId}));
|
||||
signalServer.send(JSON.stringify({type: 'listStreamers'}));
|
||||
}
|
||||
|
||||
async function onStreamerOffer(sdp) {
|
||||
console.log("Got offer from streamer");
|
||||
|
||||
|
|
@ -57,6 +91,11 @@ function onStreamerDisconnected() {
|
|||
}
|
||||
streamer.transport.close();
|
||||
streamer = null;
|
||||
signalServer.send(JSON.stringify({type: 'stopStreaming'}));
|
||||
|
||||
setTimeout(function() {
|
||||
signalServer.send(JSON.stringify({type: 'listStreamers'}));
|
||||
}, config.retrySubscribeDelaySecs * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +267,7 @@ function onLayerPreference(msg) {
|
|||
}
|
||||
|
||||
async function onSignallingMessage(message) {
|
||||
//console.log(`Got MSG: ${message}`);
|
||||
//console.log(`Got MSG: ${message}`);
|
||||
const msg = JSON.parse(message);
|
||||
|
||||
if (msg.type == 'offer') {
|
||||
|
|
@ -255,6 +294,12 @@ async function onSignallingMessage(message) {
|
|||
else if (msg.type == 'layerPreference') {
|
||||
onLayerPreference(msg);
|
||||
}
|
||||
else if (msg.type == 'streamerList') {
|
||||
onStreamerList(msg);
|
||||
}
|
||||
else if (msg.type == 'identify') {
|
||||
onIdentify(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function startMediasoup() {
|
||||
|
|
@ -276,6 +321,14 @@ async function startMediasoup() {
|
|||
return mediasoupRouter;
|
||||
}
|
||||
|
||||
async function onICEStateChange(identifier, iceState) {
|
||||
console.log("%s ICE state changed to %s", identifier, iceState);
|
||||
|
||||
if (identifier == 'Streamer' && iceState == 'completed') {
|
||||
signalServer.send(JSON.stringify({type: 'startStreaming'}));
|
||||
}
|
||||
}
|
||||
|
||||
async function createWebRtcTransport(identifier) {
|
||||
const {
|
||||
listenIps,
|
||||
|
|
@ -291,7 +344,7 @@ async function createWebRtcTransport(identifier) {
|
|||
initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate
|
||||
});
|
||||
|
||||
transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); });
|
||||
transport.on("icestatechange", (iceState) => onICEStateChange(identifier, iceState));
|
||||
transport.on("iceselectedtuplechange", (iceTuple) => { console.log("%s ICE selected tuple %s", identifier, JSON.stringify(iceTuple)); });
|
||||
transport.on("sctpstatechange", (sctpState) => { console.log("%s SCTP state changed to %s", identifier, sctpState); });
|
||||
|
||||
|
|
@ -299,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 ];
|
||||
|
|
@ -288,14 +250,83 @@ console.logColor(logging.Cyan, `Running Cirrus - The Pixel Streaming reference i
|
|||
|
||||
let nextPlayerId = 1;
|
||||
|
||||
const StreamerType = { Regular: 0, SFU: 1 };
|
||||
|
||||
class Streamer {
|
||||
constructor(initialId, ws, type) {
|
||||
this.id = initialId;
|
||||
this.ws = ws;
|
||||
this.type = type;
|
||||
this.idCommitted = false;
|
||||
}
|
||||
|
||||
// registers this streamers id
|
||||
commitId(id) {
|
||||
this.id = id;
|
||||
this.idCommitted = true;
|
||||
}
|
||||
|
||||
// returns true if we have a valid id
|
||||
isIdCommitted() {
|
||||
return this.idCommitted;
|
||||
}
|
||||
|
||||
// links this streamer to a subscribed SFU player (player component of an SFU)
|
||||
addSFUPlayer(sfuPlayerId) {
|
||||
if (!!this.SFUPlayerId && this.SFUPlayerId != sfuPlayerId) {
|
||||
console.error(`Streamer ${this.id} already has an SFU ${this.SFUPlayerId}. Trying to add ${sfuPlayerId} as SFU.`);
|
||||
return;
|
||||
}
|
||||
this.SFUPlayerId = sfuPlayerId;
|
||||
}
|
||||
|
||||
// removes the previously subscribed SFU player
|
||||
removeSFUPlayer() {
|
||||
delete this.SFUPlayerId;
|
||||
}
|
||||
|
||||
// gets the player id of the subscribed SFU if any
|
||||
getSFUPlayerId() {
|
||||
return this.SFUPlayerId;
|
||||
}
|
||||
|
||||
// returns true if this streamer is forwarding another streamer
|
||||
isSFU() {
|
||||
return this.type == StreamerType.SFU;
|
||||
}
|
||||
|
||||
// links this streamer to a player, used for SFU connections since they have both components
|
||||
setSFUPlayerComponent(playerComponent) {
|
||||
if (!this.isSFU()) {
|
||||
console.error(`Trying to add an SFU player component ${playerComponent.id} to streamer ${this.id} but it is not an SFU type.`);
|
||||
return;
|
||||
}
|
||||
this.sfuPlayerComponent = playerComponent;
|
||||
}
|
||||
|
||||
// gets the player component for this sfu
|
||||
getSFUPlayerComponent() {
|
||||
if (!this.isSFU()) {
|
||||
console.error(`Trying to get an SFU player component from streamer ${this.id} but it is not an SFU type.`);
|
||||
return null;
|
||||
}
|
||||
return this.sfuPlayerComponent;
|
||||
}
|
||||
}
|
||||
|
||||
const PlayerType = { Regular: 0, SFU: 1 };
|
||||
const WhoSendsOffer = { Streamer: 0, Browser: 1 };
|
||||
|
||||
class Player {
|
||||
constructor(id, ws, type, browserSendOffer) {
|
||||
constructor(id, ws, type, whoSendsOffer) {
|
||||
this.id = id;
|
||||
this.ws = ws;
|
||||
this.type = type;
|
||||
this.browserSendOffer = browserSendOffer;
|
||||
this.whoSendsOffer = whoSendsOffer;
|
||||
}
|
||||
|
||||
isSFU() {
|
||||
return this.type == PlayerType.SFU;
|
||||
}
|
||||
|
||||
subscribe(streamerId) {
|
||||
|
|
@ -304,13 +335,25 @@ class Player {
|
|||
return;
|
||||
}
|
||||
this.streamerId = streamerId;
|
||||
const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: !this.browserSendOffer };
|
||||
if (this.type == PlayerType.SFU) {
|
||||
let streamer = streamers.get(this.streamerId);
|
||||
streamer.addSFUPlayer(this.id);
|
||||
}
|
||||
const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: this.whoSendsOffer == WhoSendsOffer.Streamer };
|
||||
logOutgoing(this.streamerId, msg);
|
||||
this.sendFrom(msg);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
if (this.streamerId && streamers.has(this.streamerId)) {
|
||||
if (this.type == PlayerType.SFU) {
|
||||
let streamer = streamers.get(this.streamerId);
|
||||
if (streamer.getSFUPlayerId() != this.id) {
|
||||
console.error(`Trying to unsibscribe SFU player ${this.id} from streamer ${streamer.id} but the current SFUId does not match (${streamer.getSFUPlayerId()}).`)
|
||||
} else {
|
||||
streamer.removeSFUPlayer();
|
||||
}
|
||||
}
|
||||
const msg = { type: 'playerDisconnected', playerId: this.id };
|
||||
logOutgoing(this.streamerId, msg);
|
||||
this.sendFrom(msg);
|
||||
|
|
@ -348,20 +391,41 @@ class Player {
|
|||
const msgString = JSON.stringify(message);
|
||||
this.ws.send(msgString);
|
||||
}
|
||||
|
||||
setSFUStreamerComponent(streamerComponent) {
|
||||
if (!this.isSFU()) {
|
||||
console.error(`Trying to add an SFU streamer component ${streamerComponent.id} to player ${this.id} but it is not an SFU type.`);
|
||||
return;
|
||||
}
|
||||
this.sfuStreamerComponent = streamerComponent;
|
||||
}
|
||||
|
||||
getSFUStreamerComponent() {
|
||||
if (!this.isSFU()) {
|
||||
console.error(`Trying to get an SFU streamer component from player ${this.id} but it is not an SFU type.`);
|
||||
return null;
|
||||
}
|
||||
return this.sfuStreamerComponent;
|
||||
}
|
||||
};
|
||||
|
||||
let streamers = new Map(); // streamerId <-> streamer socket
|
||||
let players = new Map(); // playerId <-> player, where player is either a web-browser or a native webrtc player
|
||||
const SFUPlayerId = "SFU";
|
||||
const LegacyStreamerId = "__LEGACY__"; // old streamers that dont know how to ID will be assigned this id.
|
||||
let streamers = new Map(); // streamerId <-> streamer
|
||||
let players = new Map(); // playerId <-> player/peer/viewer
|
||||
const LegacyStreamerPrefix = "__LEGACY_STREAMER__"; // old streamers that dont know how to ID will be assigned this id prefix.
|
||||
const LegacySFUPrefix = "__LEGACY_SFU__"; // same as streamer version but for SFUs
|
||||
const streamerIdTimeoutSecs = 5;
|
||||
|
||||
function sfuIsConnected() {
|
||||
const sfuPlayer = players.get(SFUPlayerId);
|
||||
return sfuPlayer && sfuPlayer.ws && sfuPlayer.ws.readyState == 1;
|
||||
}
|
||||
|
||||
function getSFU() {
|
||||
return players.get(SFUPlayerId);
|
||||
// gets the SFU subscribed to this streamer if any.
|
||||
function getSFUForStreamer(streamerId) {
|
||||
if (!streamers.has(streamerId)) {
|
||||
return null;
|
||||
}
|
||||
const streamer = streamers.get(streamerId);
|
||||
const sfuPlayerId = streamer.getSFUPlayerId();
|
||||
if (!sfuPlayerId) {
|
||||
return null;
|
||||
}
|
||||
return players.get(sfuPlayerId);
|
||||
}
|
||||
|
||||
function logIncoming(sourceName, msg) {
|
||||
|
|
@ -401,30 +465,109 @@ function getPlayerIdFromMessage(msg) {
|
|||
return sanitizePlayerId(msg.playerId);
|
||||
}
|
||||
|
||||
let uniqueLegacyStreamerPostfix = 0;
|
||||
function getUniqueLegacyStreamerId() {
|
||||
const finalId = LegacyStreamerPrefix + uniqueLegacyStreamerPostfix;
|
||||
++uniqueLegacyStreamerPostfix;
|
||||
return finalId;
|
||||
}
|
||||
|
||||
let uniqueLegacySFUPostfix = 0;
|
||||
function getUniqueLegacySFUId() {
|
||||
const finalId = LegacySFUPrefix + uniqueLegacySFUPostfix;
|
||||
++uniqueLegacySFUPostfix;
|
||||
return finalId;
|
||||
}
|
||||
|
||||
function requestStreamerId(streamer) {
|
||||
// first we ask the streamer to id itself.
|
||||
// if it doesnt reply within a time limit we assume it's an older streamer
|
||||
// and assign it an id.
|
||||
|
||||
// request id
|
||||
const msg = { type: "identify" };
|
||||
logOutgoing(streamer.id, msg);
|
||||
streamer.ws.send(JSON.stringify(msg));
|
||||
|
||||
streamer.idTimer = setTimeout(function() {
|
||||
// streamer did not respond in time. give it a legacy id.
|
||||
const newLegacyId = getUniqueLegacyStreamerId();
|
||||
if (newLegacyId.length == 0) {
|
||||
const error = `Ran out of legacy ids.`;
|
||||
console.error(error);
|
||||
streamer.ws.close(1008, error);
|
||||
} else {
|
||||
registerStreamer(newLegacyId, streamer);
|
||||
}
|
||||
|
||||
}, streamerIdTimeoutSecs * 1000);
|
||||
}
|
||||
|
||||
function sanitizeStreamerId(id) {
|
||||
let maxPostfix = -1;
|
||||
for (let [streamerId, streamer] of streamers) {
|
||||
const idMatchRegex = /^(.*?)(\d*)$/;
|
||||
const [, baseId, postfix] = streamerId.match(idMatchRegex);
|
||||
// if the id is numeric then base id will be empty and we need to compare with the postfix
|
||||
if ((baseId != '' && baseId != id) || (baseId == '' && postfix != id)) {
|
||||
continue;
|
||||
}
|
||||
const numPostfix = Number(postfix);
|
||||
if (numPostfix > maxPostfix) {
|
||||
maxPostfix = numPostfix
|
||||
}
|
||||
}
|
||||
if (maxPostfix >= 0) {
|
||||
return id + (maxPostfix + 1);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function registerStreamer(id, streamer) {
|
||||
streamer.id = id;
|
||||
streamers.set(streamer.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);
|
||||
if (!!streamer.idTimer) {
|
||||
clearTimeout(streamer.idTimer);
|
||||
delete streamer.idTimer;
|
||||
}
|
||||
streamers.set(uniqueId, streamer);
|
||||
console.logColor(logging.Green, `Registered new streamer: ${streamer.id}`);
|
||||
}
|
||||
|
||||
function onStreamerDisconnected(streamer) {
|
||||
if (!streamer.id) {
|
||||
if (!!streamer.idTimer) {
|
||||
clearTimeout(streamer.idTimer);
|
||||
}
|
||||
|
||||
if (!streamer.id || !streamers.has(streamer.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!streamers.has(streamer.id)) {
|
||||
console.error(`Disconnecting streamer ${streamer.id} does not exist.`);
|
||||
} else {
|
||||
sendStreamerDisconnectedToMatchmaker();
|
||||
let sfuPlayer = getSFU();
|
||||
if (sfuPlayer) {
|
||||
const msg = { type: "streamerDisconnected" };
|
||||
logOutgoing(sfuPlayer.id, msg);
|
||||
sfuPlayer.sendTo(msg);
|
||||
disconnectAllPlayers(sfuPlayer.id);
|
||||
}
|
||||
disconnectAllPlayers(streamer.id);
|
||||
streamers.delete(streamer.id);
|
||||
sendStreamerDisconnectedToMatchmaker();
|
||||
let sfuPlayer = getSFUForStreamer(streamer.id);
|
||||
if (sfuPlayer) {
|
||||
const msg = { type: "streamerDisconnected" };
|
||||
logOutgoing(sfuPlayer.id, msg);
|
||||
sfuPlayer.sendTo(msg);
|
||||
disconnectAllPlayers(sfuPlayer.id);
|
||||
}
|
||||
disconnectAllPlayers(streamer.id);
|
||||
streamers.delete(streamer.id);
|
||||
}
|
||||
|
||||
function onStreamerMessageId(streamer, msg) {
|
||||
|
|
@ -432,15 +575,6 @@ function onStreamerMessageId(streamer, msg) {
|
|||
|
||||
let streamerId = msg.id;
|
||||
registerStreamer(streamerId, streamer);
|
||||
|
||||
// subscribe any sfu to the latest connected streamer
|
||||
const sfuPlayer = getSFU();
|
||||
if (sfuPlayer) {
|
||||
sfuPlayer.subscribe(streamer.id);
|
||||
}
|
||||
|
||||
// if any streamer id's assume the legacy streamer is not needed.
|
||||
streamers.delete(LegacyStreamerId);
|
||||
}
|
||||
|
||||
function onStreamerMessagePing(streamer, msg) {
|
||||
|
|
@ -461,7 +595,7 @@ function onStreamerMessageDisconnectPlayer(streamer, msg) {
|
|||
}
|
||||
|
||||
function onStreamerMessageLayerPreference(streamer, msg) {
|
||||
let sfuPlayer = getSFU();
|
||||
let sfuPlayer = getSFUForStreamer(streamer.id);
|
||||
if (sfuPlayer) {
|
||||
logOutgoing(sfuPlayer.id, msg);
|
||||
sfuPlayer.sendTo(msg);
|
||||
|
|
@ -495,7 +629,8 @@ streamerServer.on('connection', function (ws, req) {
|
|||
console.logColor(logging.Green, `Streamer connected: ${req.connection.remoteAddress}`);
|
||||
sendStreamerConnectedToMatchmaker();
|
||||
|
||||
let streamer = { ws: ws };
|
||||
const temporaryId = req.connection.remoteAddress;
|
||||
let streamer = new Streamer(temporaryId, ws, StreamerType.Regular);
|
||||
|
||||
ws.on('message', (msgRaw) => {
|
||||
var msg;
|
||||
|
|
@ -528,76 +663,134 @@ 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);
|
||||
|
||||
// request id
|
||||
const msg = { type: "identify" };
|
||||
logOutgoing("unknown", msg);
|
||||
ws.send(JSON.stringify(msg));
|
||||
|
||||
registerStreamer(LegacyStreamerId, streamer);
|
||||
requestStreamerId(streamer);
|
||||
});
|
||||
|
||||
function forwardSFUMessageToPlayer(msg) {
|
||||
function forwardSFUMessageToPlayer(sfuPlayer, msg) {
|
||||
const playerId = getPlayerIdFromMessage(msg);
|
||||
const player = players.get(playerId);
|
||||
if (player) {
|
||||
logForward(SFUPlayerId, playerId, msg);
|
||||
logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg);
|
||||
player.sendTo(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function forwardSFUMessageToStreamer(msg) {
|
||||
const sfuPlayer = getSFU();
|
||||
if (sfuPlayer) {
|
||||
logForward(SFUPlayerId, sfuPlayer.streamerId, msg);
|
||||
msg.sfuId = SFUPlayerId;
|
||||
sfuPlayer.sendFrom(msg);
|
||||
}
|
||||
function forwardSFUMessageToStreamer(sfuPlayer, msg) {
|
||||
logForward(sfuPlayer.getSFUStreamerComponent().id, sfuPlayer.streamerId, msg);
|
||||
msg.sfuId = sfuPlayer.id;
|
||||
sfuPlayer.sendFrom(msg);
|
||||
}
|
||||
|
||||
function onPeerDataChannelsSFUMessage(msg) {
|
||||
function onPeerDataChannelsSFUMessage(sfuPlayer, msg) {
|
||||
// sfu is telling a peer what stream id to use for a data channel
|
||||
const playerId = getPlayerIdFromMessage(msg);
|
||||
const player = players.get(playerId);
|
||||
if (player) {
|
||||
logForward(SFUPlayerId, playerId, msg);
|
||||
logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg);
|
||||
player.sendTo(msg);
|
||||
player.datachannel = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onSFUDisconnected() {
|
||||
console.log("disconnecting SFU from streamer");
|
||||
disconnectAllPlayers(SFUPlayerId);
|
||||
const sfuPlayer = getSFU();
|
||||
if (sfuPlayer) {
|
||||
sfuPlayer.unsubscribe();
|
||||
sfuPlayer.ws.close(4000, "SFU Disconnected");
|
||||
}
|
||||
players.delete(SFUPlayerId);
|
||||
streamers.delete(SFUPlayerId);
|
||||
// basically a duplicate of the streamer id request but this one does not register the streamer
|
||||
function requestSFUStreamerId(sfuPlayer) {
|
||||
// request id
|
||||
const msg = { type: "identify" };
|
||||
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
|
||||
logOutgoing(sfuStreamerComponent.id, msg);
|
||||
sfuStreamerComponent.ws.send(JSON.stringify(msg));
|
||||
|
||||
sfuStreamerComponent.idTimer = setTimeout(function() {
|
||||
// streamer did not respond in time. give it a legacy id.
|
||||
const newLegacyId = getUniqueSFUId();
|
||||
if (newLegacyId.length == 0) {
|
||||
const error = `Ran out of legacy ids.`;
|
||||
console.error(error);
|
||||
sfuPlayer.ws.close(1008, error);
|
||||
} else {
|
||||
sfuStreamerComponent.id = newLegacyId;
|
||||
}
|
||||
}, streamerIdTimeoutSecs * 1000);
|
||||
}
|
||||
|
||||
function onSFUMessageId(sfuPlayer, msg) {
|
||||
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
|
||||
logIncoming(sfuStreamerComponent.id, msg);
|
||||
sfuStreamerComponent.id = msg.id;
|
||||
|
||||
if (!!sfuStreamerComponent.idTimer) {
|
||||
clearTimeout(sfuStreamerComponent.idTimer);
|
||||
delete sfuStreamerComponent.idTimer;
|
||||
}
|
||||
}
|
||||
|
||||
function onSFUMessageStartStreaming(sfuPlayer, msg) {
|
||||
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
|
||||
logIncoming(sfuStreamerComponent.id, msg);
|
||||
if (streamers.has(sfuStreamerComponent.id)) {
|
||||
console.error(`SFU ${sfuStreamerComponent.id} is already registered as a streamer and streaming.`)
|
||||
return;
|
||||
}
|
||||
|
||||
registerStreamer(sfuStreamerComponent.id, sfuStreamerComponent);
|
||||
}
|
||||
|
||||
function onSFUMessageStopStreaming(sfuPlayer, msg) {
|
||||
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
|
||||
logIncoming(sfuStreamerComponent.id, msg);
|
||||
if (!streamers.has(sfuStreamerComponent.id)) {
|
||||
console.error(`SFU ${sfuStreamerComponent.id} is not registered as a streamer or streaming.`)
|
||||
return;
|
||||
}
|
||||
|
||||
onStreamerDisconnected(sfuStreamerComponent);
|
||||
}
|
||||
|
||||
function onSFUDisconnected(sfuPlayer) {
|
||||
console.log("disconnecting SFU from streamer");
|
||||
disconnectAllPlayers(sfuPlayer.id);
|
||||
onStreamerDisconnected(sfuPlayer.getSFUStreamerComponent());
|
||||
sfuPlayer.unsubscribe();
|
||||
sfuPlayer.ws.close(4000, "SFU Disconnected");
|
||||
players.delete(sfuPlayer.id);
|
||||
streamers.delete(sfuPlayer.id);
|
||||
}
|
||||
|
||||
sfuMessageHandlers.set('listStreamers', onPlayerMessageListStreamers);
|
||||
sfuMessageHandlers.set('subscribe', onPlayerMessageSubscribe);
|
||||
sfuMessageHandlers.set('unsubscribe', onPlayerMessageUnsubscribe);
|
||||
sfuMessageHandlers.set('offer', forwardSFUMessageToPlayer);
|
||||
sfuMessageHandlers.set('answer', forwardSFUMessageToStreamer);
|
||||
sfuMessageHandlers.set('streamerDataChannels', forwardSFUMessageToStreamer);
|
||||
sfuMessageHandlers.set('peerDataChannels', onPeerDataChannelsSFUMessage);
|
||||
sfuMessageHandlers.set('endpointId', onSFUMessageId);
|
||||
sfuMessageHandlers.set('startStreaming', onSFUMessageStartStreaming);
|
||||
sfuMessageHandlers.set('stopStreaming', onSFUMessageStopStreaming);
|
||||
|
||||
console.logColor(logging.Green, `WebSocket listening for SFU connections on :${sfuPort}`);
|
||||
let sfuServer = new WebSocket.Server({ port: sfuPort });
|
||||
sfuServer.on('connection', function (ws, req) {
|
||||
// reject if we already have an sfu
|
||||
if (sfuIsConnected()) {
|
||||
ws.close(1013, 'Already have an SFU');
|
||||
return;
|
||||
}
|
||||
|
||||
let playerId = sanitizePlayerId(nextPlayerId++);
|
||||
console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `);
|
||||
|
||||
let streamerComponent = new Streamer(req.connection.remoteAddress, ws, StreamerType.SFU);
|
||||
let playerComponent = new Player(playerId, ws, PlayerType.SFU, WhoSendsOffer.Streamer);
|
||||
|
||||
streamerComponent.setSFUPlayerComponent(playerComponent);
|
||||
playerComponent.setSFUStreamerComponent(streamerComponent);
|
||||
|
||||
players.set(playerId, playerComponent);
|
||||
|
||||
ws.on('message', (msgRaw) => {
|
||||
var msg;
|
||||
|
|
@ -609,45 +802,41 @@ sfuServer.on('connection', function (ws, req) {
|
|||
return;
|
||||
}
|
||||
|
||||
let sfuPlayer = players.get(playerId);
|
||||
if (!sfuPlayer) {
|
||||
console.error(`Received a message from an SFU not in the player list ${playerId}`);
|
||||
ws.close(1001, 'Broken');
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = sfuMessageHandlers.get(msg.type);
|
||||
if (!handler || (typeof handler != 'function')) {
|
||||
if (config.LogVerbose) {
|
||||
console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", SFUPlayerId, msgRaw);
|
||||
console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", sfuPlayer.id, msgRaw);
|
||||
}
|
||||
console.error(`unsupported SFU message type: ${msg.type}`);
|
||||
ws.close(1008, 'Unsupported message type');
|
||||
return;
|
||||
}
|
||||
handler(msg);
|
||||
handler(sfuPlayer, msg);
|
||||
});
|
||||
|
||||
ws.on('close', function(code, reason) {
|
||||
console.error(`SFU disconnected: ${code} - ${reason}`);
|
||||
onSFUDisconnected();
|
||||
onSFUDisconnected(playerComponent);
|
||||
});
|
||||
|
||||
ws.on('error', function(error) {
|
||||
console.error(`SFU connection error: ${error}`);
|
||||
onSFUDisconnected();
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
|
||||
let sfuPlayer = new Player(SFUPlayerId, ws, PlayerType.SFU, false);
|
||||
players.set(SFUPlayerId, sfuPlayer);
|
||||
console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `);
|
||||
|
||||
// TODO subscribe it to one of any of the streamers for now
|
||||
for (let [streamerId, streamer] of streamers) {
|
||||
sfuPlayer.subscribe(streamerId);
|
||||
break;
|
||||
}
|
||||
|
||||
// sfu also acts as a streamer
|
||||
registerStreamer(SFUPlayerId, { ws: ws });
|
||||
requestStreamerId(playerComponent.getSFUStreamerComponent());
|
||||
});
|
||||
|
||||
let playerCount = 0;
|
||||
|
|
@ -718,7 +907,7 @@ playerServer.on('connection', function (ws, req) {
|
|||
var url = require('url');
|
||||
const parsedUrl = url.parse(req.url);
|
||||
const urlParams = new URLSearchParams(parsedUrl.search);
|
||||
const browserSendOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false';
|
||||
const whoSendsOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false' ? WhoSendsOffer.Browser : WhoSendsOffer.Streamer;
|
||||
|
||||
if (playerCount + 1 > maxPlayerCount && maxPlayerCount !== -1)
|
||||
{
|
||||
|
|
@ -730,7 +919,7 @@ playerServer.on('connection', function (ws, req) {
|
|||
++playerCount;
|
||||
let playerId = sanitizePlayerId(nextPlayerId++);
|
||||
console.logColor(logging.Green, `player ${playerId} (${req.connection.remoteAddress}) connected`);
|
||||
let player = new Player(playerId, ws, PlayerType.Regular, browserSendOffer);
|
||||
let player = new Player(playerId, ws, PlayerType.Regular, whoSendsOffer);
|
||||
players.set(playerId, player);
|
||||
|
||||
ws.on('message', (msgRaw) =>{
|
||||
|
|
@ -769,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...`);
|
||||
|
|
@ -778,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();
|
||||
});
|
||||
|
||||
|
|
@ -786,11 +979,11 @@ 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
|
||||
if (player.id == SFUPlayerId) {
|
||||
// because we're working on a clone here we have to access directly
|
||||
getSFU().unsubscribe();
|
||||
const sfuPlayer = getSFUForStreamer(streamerId);
|
||||
if (sfuPlayer && player.id == sfuPlayer.id) {
|
||||
sfuPlayer.unsubscribe();
|
||||
} else {
|
||||
player.ws.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -28,11 +28,8 @@ realm="PixelStreaming"
|
|||
process=""
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
process="${BASH_LOCATION}/coturn/bin/turnserver"
|
||||
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
||||
process="turnserver"
|
||||
else
|
||||
echo 'Incorrect host OS for use with Start_TURNServer.sh'
|
||||
exit -1
|
||||
process="turnserver"
|
||||
fi
|
||||
arguments="-c turnserver.conf --allowed-peer-ip=$localip -p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -u ${turnusername}:${turnpassword}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 --"
|
||||
|
|
@ -33,4 +33,5 @@ start_process $process $arguments
|
|||
|
||||
popd > /dev/null # ../..
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
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
|
||||
|
|
@ -141,16 +141,13 @@ if [ "$(uname)" == "Darwin" ]; then
|
|||
echo 'Incompatible architecture. Only x86_64 and ARM64 are supported'
|
||||
exit -1
|
||||
fi
|
||||
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
||||
node_url="https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz"
|
||||
else
|
||||
echo 'Incorrect OS for use with setup.sh'
|
||||
exit -1
|
||||
node_url="https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz"
|
||||
fi
|
||||
check_and_install "node" "$node_version" "$NODE_VERSION" "curl $node_url --output node.tar.xz
|
||||
&& tar -xf node.tar.xz
|
||||
&& rm node.tar.xz
|
||||
&& mv node-v*-*-* \"${BASH_LOCATION}/node\""
|
||||
&& tar -xf node.tar.xz
|
||||
&& rm node.tar.xz
|
||||
&& mv node-v*-*-* \"${BASH_LOCATION}/node\""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install
|
||||
|
|
@ -167,19 +164,19 @@ if [ "$(uname)" == "Darwin" ]; then
|
|||
if [ -d "${BASH_LOCATION}/coturn" ]; then
|
||||
echo 'CoTURN directory found...skipping install.'
|
||||
else
|
||||
echo 'CoTURN directory not found...beginning CoTURN download for Mac.'
|
||||
echo 'CoTURN directory not found...beginning CoTURN download for Mac.'
|
||||
coturn_url=""
|
||||
if [[ $arch == x86_64* ]]; then
|
||||
coturn_url="https://github.com/Belchy06/coturn/releases/download/v4.6.2-mac-x84_64/turnserver.zip"
|
||||
coturn_url="https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.6.2-coturn-mac-x86_64/turnserver.zip"
|
||||
elif [[ $arch == arm* ]]; then
|
||||
coturn_url="https://github.com/Belchy06/coturn/releases/download/v4.6.2-mac-arm64/turnserver.zip"
|
||||
coturn_url="https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.6.2-coturn-mac-arm64/turnserver.zip"
|
||||
fi
|
||||
curl -L -o ./turnserver.zip "$coturn_url"
|
||||
mkdir "${BASH_LOCATION}/coturn"
|
||||
tar -xf turnserver.zip -C "${BASH_LOCATION}/coturn"
|
||||
rm turnserver.zip
|
||||
fi
|
||||
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
||||
else
|
||||
#command #dep_name #get_version_string #version_min #install command
|
||||
coturn_version=$(if command -v turnserver &> /dev/null; then echo 1; else echo 0; fi)
|
||||
if [ $coturn_version -eq 0 ]; then
|
||||
|
|
@ -198,4 +195,3 @@ elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
|||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
::
|
||||
:: RefreshEnv.cmd
|
||||
::
|
||||
:: Batch file to read environment variables from registry and
|
||||
:: set session variables to these values.
|
||||
::
|
||||
:: With this batch file, there should be no need to reload command
|
||||
:: environment every time you want environment changes to propagate
|
||||
|
||||
::echo "RefreshEnv.cmd only works from cmd.exe, please install the Chocolatey Profile to take advantage of refreshenv from PowerShell"
|
||||
echo | set /p dummy="Refreshing environment variables from registry for cmd.exe. Please wait..."
|
||||
|
||||
goto main
|
||||
|
||||
:: Set one environment variable from registry key
|
||||
:SetFromReg
|
||||
"%WinDir%\System32\Reg" QUERY "%~1" /v "%~2" > "%TEMP%\_envset.tmp" 2>NUL
|
||||
for /f "usebackq skip=2 tokens=2,*" %%A IN ("%TEMP%\_envset.tmp") do (
|
||||
echo/set "%~3=%%B"
|
||||
)
|
||||
goto :EOF
|
||||
|
||||
:: Get a list of environment variables from registry
|
||||
:GetRegEnv
|
||||
"%WinDir%\System32\Reg" QUERY "%~1" > "%TEMP%\_envget.tmp"
|
||||
for /f "usebackq skip=2" %%A IN ("%TEMP%\_envget.tmp") do (
|
||||
if /I not "%%~A"=="Path" (
|
||||
call :SetFromReg "%~1" "%%~A" "%%~A"
|
||||
)
|
||||
)
|
||||
goto :EOF
|
||||
|
||||
:main
|
||||
echo/@echo off >"%TEMP%\_env.cmd"
|
||||
|
||||
:: Slowly generating final file
|
||||
call :GetRegEnv "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" >> "%TEMP%\_env.cmd"
|
||||
call :GetRegEnv "HKCU\Environment">>"%TEMP%\_env.cmd" >> "%TEMP%\_env.cmd"
|
||||
|
||||
:: Special handling for PATH - mix both User and System
|
||||
call :SetFromReg "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" Path Path_HKLM >> "%TEMP%\_env.cmd"
|
||||
call :SetFromReg "HKCU\Environment" Path Path_HKCU >> "%TEMP%\_env.cmd"
|
||||
|
||||
:: Caution: do not insert space-chars before >> redirection sign
|
||||
echo/set "Path=%%Path_HKLM%%;%%Path_HKCU%%" >> "%TEMP%\_env.cmd"
|
||||
|
||||
:: Cleanup
|
||||
del /f /q "%TEMP%\_envset.tmp" 2>nul
|
||||
del /f /q "%TEMP%\_envget.tmp" 2>nul
|
||||
|
||||
:: capture user / architecture
|
||||
SET "OriginalUserName=%USERNAME%"
|
||||
SET "OriginalArchitecture=%PROCESSOR_ARCHITECTURE%"
|
||||
|
||||
:: Set these variables
|
||||
call "%TEMP%\_env.cmd"
|
||||
|
||||
:: Cleanup
|
||||
del /f /q "%TEMP%\_env.cmd" 2>nul
|
||||
|
||||
:: reset user / architecture
|
||||
SET "USERNAME=%OriginalUserName%"
|
||||
SET "PROCESSOR_ARCHITECTURE=%OriginalArchitecture%"
|
||||
|
||||
echo | set /p dummy="Finished."
|
||||
echo ...
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
License
|
||||
-------
|
||||
|
||||
Copyright (C) 1999-2008 - Jonathan Wilkes
|
||||
http://www.xanya.net
|
||||
|
||||
Installing and using this software (or source code) signifies acceptance of these terms and the conditions of the license.
|
||||
This license applies to everything in this package (Including any supplied Source Code), except where otherwise noted.
|
||||
|
||||
License Agreement
|
||||
-----------------
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty.
|
||||
In no event will the author be held liable for any damages arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software/source code.
|
||||
(If you use the supplied source code (if any) in a product, then an acknowledgment in the product documentation would be appreciated but is not required.)
|
||||
|
||||
2. If you have downloaded the Source Code for this application (where available) then altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any distribution of the software.
|
||||
(If you use the supplied source code (if any) in a product, including commercial applications, then you do NOT need to distribute this license with your product.)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
|
||||
SetEnv
|
||||
Version 1.09 - ( For Windows 9x/NT/2000/XP/S2K3/Vista )
|
||||
|
||||
Copyright (C) 2005-2008 - Jonathan Wilkes - All Rights Reserved.
|
||||
http://www.xanya.net
|
||||
|
||||
================================================================================
|
||||
|
||||
1. Installation
|
||||
|
||||
Simply download and run the Setup_SetEnv.exe application to install SetEnv.
|
||||
|
||||
2. Using SetEnv
|
||||
|
||||
The SetEnv is a free tool for setting/updating/deleting System Environment Variables.
|
||||
Type the following at a command prompt (assumes SetEnv.exe is in current path), for command line usage information.
|
||||
|
||||
setenv -?
|
||||
|
||||
See our website for full usage details, http://www.xanya.net/site/utils/setenv.php
|
||||
|
||||
3. Version History
|
||||
|
||||
1.09 [Fix] - (Feb 9, 2008) - Fixed a problem on Windows 98 where it sometimes failed to open the Autoexec.bat file.
|
||||
1.08 [New] - (May 31, 2007) - Added how to delete a USER environment variable to the usage information.
|
||||
1.07 [Fix] - (Jan 25, 2007) - Fixed a bug found by depaolim.
|
||||
1.06 [New] - (Jan 14, 2007) - Added dynamic expansion support (same as using ~ with setx)
|
||||
- Originally requested by Andre Amaral, further Request by Synetech
|
||||
1.05 [New] - (Sep 06, 2006) - Added support to prepend (rather than append) a value to an expanded string
|
||||
- Requested by Masuia
|
||||
1.04 [New] - (May 30, 2006) - Added support for User environment variables.
|
||||
1.03 [Fix] - (Apr 20, 2006) - Bug fix in ProcessWinXP() discovered by attiasr
|
||||
1.01 [Fix] - (Nov 15, 2005) - Bug fix in IsWinME() discovered by frankd
|
||||
1.00 [New] - (Oct 29, 2005) - Initial Public Release.
|
||||
|
||||
4. License and Terms of Use
|
||||
|
||||
Please see the License.txt file for licensing information.
|
||||
|
||||
5. Reporting Problems
|
||||
|
||||
If you encounter any problems whilst using SetEnv, please try downloading the latest version from http://www.xanya.net to see if the problem has already been resolved.
|
||||
If this does not help, then please send an e-mail to darka@xanya.net with details describing the problem.
|
||||
|
||||
================================================================================
|
||||
Binary file not shown.
|
|
@ -12,7 +12,7 @@ if exist coturn\ (
|
|||
echo CoTURN directory not found...beginning CoTURN download for Windows.
|
||||
|
||||
@Rem Download nodejs and follow redirects.
|
||||
curl -L -o ./turnserver.zip "https://github.com/mcottontensor/coturn/releases/download/v4.5.2-windows/turnserver.zip"
|
||||
curl -L -o ./turnserver.zip "https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.5.2-coturn-windows/turnserver.zip"
|
||||
|
||||
@Rem Unarchive the .zip to a directory called "turnserver"
|
||||
mkdir coturn & tar -xf turnserver.zip -C coturn
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@
|
|||
@Rem Save our current directory (the NodeJS dir) in a variable
|
||||
set "NodeDir=%CD%\SignallingWebServer\platform_scripts\cmd\node"
|
||||
|
||||
@Rem Prepend NodeDir to PATH temporarily using a custom tool called SetEnv
|
||||
call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -uap PATH %%%%"%NodeDir%"
|
||||
@Rem Refresh the cmd session with new PATH
|
||||
call %~dp0\refreshenv.cmd
|
||||
@rem Save the old path variable
|
||||
set OLDPATH=%PATH%
|
||||
@Rem Prepend NodeDir to PATH temporarily
|
||||
set PATH=%PATH%;%NodeDir%
|
||||
|
||||
@Rem Do npm install in the Frontend\lib directory (note we use start because that loads PATH)
|
||||
echo ----------------------------
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
echo End of build reference frontend step.
|
||||
echo ----------------------------
|
||||
|
||||
@Rem Remove our NodeJS from the PATH
|
||||
call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -ud PATH %%%%"%NodeDir%"
|
||||
@Rem Restore path
|
||||
set PATH=%OLDPATH%
|
||||
|
||||
goto :eof
|
||||
Loading…
Reference in New Issue