Compare commits

...

105 Commits

Author SHA1 Message Date
Luke Bermingham cb741add4f
Readme to new repo: https://github.com/EpicGamesExt/PixelStreamingInfrastructure
More details can be found here: https://forums.unrealengine.com/t/migrating-optional-epic-games-git-repositories-to-new-github-organization/1718666

Signed-off-by: Luke Bermingham <1215582+lukehb@users.noreply.github.com>
2024-02-29 15:02:21 +10:00
mcottontensor 88d82e1112
Merge pull request #492 from mcottontensor/setting_option_fix
Setting option fix
2024-02-20 15:20:32 +11:00
mcottontensor caddb00ed3
Merge branch 'master' into setting_option_fix 2024-02-20 15:18:34 +11:00
Matthew Cotton f0fd72768c
Fixed incorrect settings setup with SettingOption 2024-02-20 15:13:29 +11:00
Matthew Cotton 7cbe5227dd
Merge remote-tracking branch 'upstream/master' 2024-02-20 15:05:43 +11:00
mcottontensor 672397e6a8
Update README.md
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2024-02-20 14:58:20 +11:00
David MacPherson fa75e9d8c5
Implemented a bug fix to remove the setupWebRTCtcpDetectEvent from the event emitter (#490) 2024-02-19 13:23:01 +10:00
David MacPherson 4969548535
Add TCP+Relay detection event (#485)
* Change the label for the remote candidate
* Added the following missing WebRTC stats id,timestamp,type,lastPacketReceivedTimestamp,lastPacketSentTimestamp,priority,remoteCandidateId,transportId and writable
* Added relayProtocol and transport ID to the candidate Stat
* Refactored the candidate pair to be an array as many pairs are generated
* Implemeneted a WebRtcTCPRelayDetectedEvent that is fired when WebRTC transport is TCP AND we are using a relay candidate (e.g. a TURN server is being used).
* Refactored the stats panel to use the `getActiveCandidatePair()`
2024-02-19 10:52:32 +10:00
Luke Bermingham e91781b6fa
Create angular/readme.md
Added a readme.md for angular implementations that links to community provided examples.

Signed-off-by: Luke Bermingham <1215582+lukehb@users.noreply.github.com>
2024-02-16 14:04:17 +10:00
Bryan eea0faff60
Fix typo readme (#488)
Signed-off-by: Bryan <69793084+Ahhj93@users.noreply.github.com>
2024-02-15 11:49:07 +10:00
William Belcher 664d40b801
Update TouchStart and TouchMove handlers to force a touch force of 1 if the touch.force member is 0 (#486) 2024-02-14 14:02:48 +10:00
mcottontensor 5cf2735c5f
Update README.md
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2024-01-24 15:52:58 +11:00
mcottontensor 2544b717ae
Merge pull request #476 from EpicGames/UE5.4
Syncing master with UE5.4
2024-01-24 15:34:31 +11:00
mcottontensor b2cdcf10f3
Update RELEASE_VERSION
Updating main minor version for the release.

Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2024-01-24 15:31:20 +11:00
Matthew Cotton e44a9ff734
Bumping minor version and library reference. 2024-01-24 15:29:54 +11:00
mcottontensor 8734e5bb56
Update package.json
Bumping minor version

Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2024-01-24 15:26:57 +11:00
mcottontensor f272cefb93
Merge pull request #475 from EpicGames/master
Syncing with master
2024-01-24 15:22:53 +11:00
mcottontensor d61b989bff
Merge pull request #453 from kroecks/master
Fix for build-all script dependencies
2024-01-24 15:20:35 +11:00
mcottontensor 4aded6b752
Merge branch 'master' into master 2024-01-24 15:19:31 +11:00
mcottontensor dee225c03a
Merge pull request #464 from EpicGames/dependabot/npm_and_yarn/Frontend/implementations/react/follow-redirects-1.15.4
Bump follow-redirects from 1.15.2 to 1.15.4 in /Frontend/implementations/react
2024-01-24 15:18:42 +11:00
mcottontensor ce2a493114
Merge branch 'master' into dependabot/npm_and_yarn/Frontend/implementations/react/follow-redirects-1.15.4 2024-01-24 15:16:47 +11:00
mcottontensor 44367dfa8a
Merge pull request #465 from EpicGames/dependabot/npm_and_yarn/Frontend/implementations/typescript/follow-redirects-1.15.4
Bump follow-redirects from 1.15.2 to 1.15.4 in /Frontend/implementations/typescript
2024-01-24 15:16:22 +11:00
mcottontensor 23e562dfdf
Merge branch 'master' into master 2024-01-24 15:15:10 +11:00
mcottontensor 20987e006e
Merge branch 'master' into dependabot/npm_and_yarn/Frontend/implementations/react/follow-redirects-1.15.4 2024-01-24 15:14:37 +11:00
mcottontensor a0f9b34dde
Merge branch 'master' into dependabot/npm_and_yarn/Frontend/implementations/typescript/follow-redirects-1.15.4 2024-01-24 15:13:44 +11:00
Denis Phoenix f4380ab2c0
Update copyright/licence year (#474)
Update licence and copyright year in all relevant files.
2024-01-23 14:30:41 +10:00
Matthew Cotton 556ecc5df3
Allowing signalling test action to be manually triggered 2024-01-22 12:17:59 +11:00
Matthew Cotton cb58c2c068
adding a signalling test action to master so it can be run in the signalling_tester branch 2024-01-22 12:16:42 +11:00
William Belcher 6d59c44126
Export NO_SUDO so it's available for use in launched processes (#472) 2024-01-15 13:29:41 +10:00
William Belcher 04c2bc5e70
Move settings order so that they are parsed default settings -> intitial settings -> url params (#471) 2024-01-15 11:33:46 +10:00
dependabot[bot] d355d6685a
Bump follow-redirects in /Frontend/implementations/typescript
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 21:12:16 +00:00
dependabot[bot] 1b00462e1c
Bump follow-redirects in /Frontend/implementations/react
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 13:38:27 +00:00
mcottontensor ce299ebd37
Merge pull request #452 from Belchy06/master
Add support for non-latin characters
2023-12-15 10:17:09 +11:00
Ken Roecks 7315292918
Merge branch 'master' into master 2023-12-13 18:03:03 -08:00
William Belcher 8b838f48fe
Add support for non-latin characters 2023-12-13 13:22:45 +10:00
mcottontensor 87b9c9aaed
Merge pull request #451 from Belchy06/master
Fix warnings about GamepadButtonReleased
2023-12-13 11:02:50 +11:00
William Belcher 898504d3a6
Fix warnings about GamepadButtonReleased 2023-12-13 09:53:49 +10:00
Kenneth Roecks 6ac47fffcd Fix for build-all script dependencies 2023-12-12 13:54:01 -08:00
William Belcher 606298d40d Ensure gamepad disconnect is triggered whenever a user navigates away from the player page 2023-12-12 20:26:24 +10:00
mcottontensor d8a4e118a6
Merge pull request #448 from alien299/alien299-ContributingGuidelines-patch01-Closes-#445
Update CONTRIBUTING.md - Added Verified Commits as a Requirement - Closes#445
2023-12-11 09:12:30 +11:00
mcottontensor eb7d4c6262
Merge branch 'master' into alien299-ContributingGuidelines-patch01-Closes-#445 2023-12-11 09:12:10 +11:00
mcottontensor 4ebc522437
Merge pull request #443 from mcottontensor/stale_legacy_timer
Small signalling server fixes.
2023-12-11 09:10:12 +11:00
mcottontensor c6ad00f2ca
Merge branch 'master' into stale_legacy_timer 2023-12-11 09:09:56 +11:00
mcottontensor c05f3b18ce
Merge pull request #441 from alien299/alien299-WebsocketErrorPatch
Update cirrus.js - More informative error messages - Fixes EpicGames/PixelStreamingInfrastructure#184
2023-12-11 09:09:18 +11:00
Patrik Nagy db6273513a
Update CONTRIBUTING.md - Added Verified Commits as a Requirement
Signed-off-by: Patrik Nagy <52012944+alien299@users.noreply.github.com>
2023-12-07 18:30:26 +01:00
pnagy e5f2ff8dd1
Merge branch 'alien299-WebsocketErrorPatch' of https://github.com/alien299/PixelStreamingInfrastructure into alien299-WebsocketErrorPatch 2023-11-30 10:07:54 +01:00
pnagy 2c5628124a
Update cirrus.js - More informative error messages
Fixed an Issue regarding the Websocket disconnect error messages. I made them more informative so they can assist with debugging.
[Issue #184](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/184)
2023-11-30 10:05:45 +01:00
Matthew Cotton 05821c7656
Fixing small issue where if a streamer disconnected before identifying the signalling server would create a legacy name that would be left behind.
Fixing some logging calls that would not properly be handled by the logging system.
2023-11-29 12:02:02 +11:00
pnagy 92de29dbc9 Update cirrus.js - More informative error messages
Fixed an Issue regarding the Websocket disconnect error messages. I made them more informative so they can assist with debugging.
[Issue #184](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/184)
2023-11-28 12:38:50 +01:00
mcottontensor 28e7c0c1f3
Merge pull request #432 from mcottontensor/fix_late_streamer_id
Fixing left behind streamer IDs when they're late to ID themselves.
2023-11-23 11:24:00 +11:00
mcottontensor 649d6b2dcb
Merge branch 'master' into fix_late_streamer_id 2023-11-22 16:32:23 +11:00
mcottontensor a3b76a3acc
Merge pull request #438 from mcottontensor/player_config_streamerid
Fixing handling of StreamerId in the initial config settings.
2023-11-22 16:32:05 +11:00
mcottontensor 5a734f2204
Merge branch 'master' into fix_late_streamer_id 2023-11-22 13:08:54 +11:00
Matthew Cotton 6fe29baa95
Fixing small typos 2023-11-22 13:07:30 +11:00
Matthew Cotton 5e71c6ff97
Fixing default behaviour when joining with one streamer. 2023-11-22 12:52:26 +11:00
Matthew Cotton 6462905b76
Adding support to set the streamer id in the pixel streaming initial settings. Fixes #436 2023-11-22 11:55:24 +11:00
Even Stensberg 7eba788faa
chore: remove explicit /dist dir in Webpack as it is implicit (#434)
Signed-off-by: Even Stensberg <evenstensberg@gmail.com>
2023-11-22 09:13:59 +10:00
Matthew Cotton e9b860428d
Bumping protocol docs version number 2023-11-22 10:08:49 +11:00
Matthew Cotton 4fd2948d99
Adding a little more context to the changed id message documentation. 2023-11-22 10:06:00 +11:00
Matthew Cotton 78640671d4
Adding the id change message to the protocol docs for signalling. 2023-11-22 10:00:29 +11:00
timbotimbo cfbe5a0f86
Fix formatting in bug_report template. (#437)
Signed-off-by: timbotimbo <timbotimbo@users.noreply.github.com>
2023-11-22 08:33:58 +10:00
Matthew Cotton ae4b5b7a6d
Updating the settings UI when streamer id changes. 2023-11-21 13:26:40 +11:00
mcottontensor a8de900166
Merge branch 'master' into fix_late_streamer_id 2023-11-21 11:54:10 +11:00
mcottontensor 12a08e1624
Merge pull request #431 from mcottontensor/sfu_docker
Dockerfile for SFU
2023-11-21 11:53:53 +11:00
Matthew Cotton a9b4f31a7d
Adding some information on running the docker image to the new documentation. 2023-11-21 11:53:32 +11:00
mcottontensor d8ac1b62d8
Merge branch 'master' into sfu_docker 2023-11-21 11:44:01 +11:00
mcottontensor e66dacf750
Merge pull request #429 from mcottontensor/new_docs
New docs
2023-11-21 11:43:42 +11:00
Matthew Cotton 8080d2aebb
Adding in new streamer id changed event/message so that the frontend can keep track of its streamer (mostly for reconnecting). 2023-11-21 11:18:28 +11:00
Matthew Cotton 4fd080675e
Adding container build to existing container action. Also added container reference on main README. 2023-11-21 09:55:19 +11:00
Matthew Cotton bbba5e0a67
Small fixes from reviews 2023-11-20 16:19:29 +11:00
Matthew Cotton c37095d229
Fixing the issue when a streamer was late to ID itself, it would leave behind the legacy id 2023-11-20 15:55:38 +11:00
Matthew Cotton f94039dac8
Fixing up some linux scripts. Cleaning up SFU dockerfile 2023-11-20 04:09:56 +00:00
Matthew Cotton 76bdc5e683
Attempting to add dockerfile for SFU 2023-11-20 14:00:47 +11:00
mcottontensor 0f5e2d9e12
Merge branch 'master' into new_docs 2023-11-17 12:53:10 +11:00
Matthew Cotton 1aad4a6e58
Adding info about simulcast streaming from UE 2023-11-17 12:49:52 +11:00
Matthew Cotton 65ebcfe03e
Adding SFU docs. 2023-11-17 11:16:08 +11:00
Matthew Cotton d5efa38acf
Fixing readme filename. Maybe? 2023-11-17 10:28:18 +11:00
mcottontensor 7d3a6b64c4
Delete SignallingWebServer/Readme.md
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2023-11-17 10:25:48 +11:00
Matthew Cotton 32d6e2b0b1
Updating main doc to link to signalling docs 2023-11-17 10:23:35 +11:00
Matthew Cotton 7f54018320
Adding some new documentation for signalling 2023-11-17 10:22:29 +11:00
William Belcher 5bea564755
Update platform scripts so that the BASH_LOCATION works on Mac as well (#428) 2023-11-16 16:19:40 +10:00
mcottontensor 7320398ef3
Merge pull request #427 from mcottontensor/legacynamefix
Fixing missed function rename.
2023-11-15 11:42:00 +11:00
Matthew Cotton 082284b6e6
Fixing missed function rename. 2023-11-15 11:39:56 +11:00
mcottontensor a7afbc3d0c
Merge pull request #425 from mcottontensor/initial_bitrates
Bitrate negotiation on stream startup
2023-11-14 08:49:22 +11:00
Matthew Cotton ffcf7b4b3a
Typo 2023-11-10 16:23:01 +11:00
Matthew Cotton bbf812b9c2
More shuffling. 2023-11-10 16:09:00 +11:00
Matthew Cotton 8847ee7440
Small cleanup. 2023-11-10 15:49:57 +11:00
Matthew Cotton 72ebc33558
- Adding support for requesting min/max bitrates on initial negotiation
(offer/answer) from the player
- Fixing bitrate values units and parsing.
2023-11-09 11:01:30 +11:00
mcottontensor edef2915ee
Merge pull request #420 from mcottontensor/ps_expose
Exposing the pixelstreaming interface to the browser.
2023-11-02 12:43:10 +11:00
mcottontensor 1e051866c1
Merge pull request #419 from mcottontensor/fix_reconnect_flow
Rewriting a bunch of reconnect and disconnect handling.
2023-11-02 12:26:18 +11:00
mcottontensor 458e8aa9cb
Merge branch 'master' into fix_reconnect_flow 2023-11-02 09:27:34 +11:00
mcottontensor ea7b1ea030
Merge branch 'master' into ps_expose 2023-11-02 09:27:13 +11:00
Matthew Cotton 9cb4d2c572
Exposing the pixelstreaming interface to the browser. This allows automation to directly interact with the pixelstreaming features for testing etc. 2023-11-02 09:22:24 +11:00
mcottontensor 5cffec3b42
Merge pull request #414 from mcottontensor/remove_login
Removing authentication features.
2023-11-02 09:13:35 +11:00
Matthew Cotton ac6450fbbd
A few small tweaks to text and some comments. 2023-11-01 16:58:17 +11:00
Matthew Cotton 449079871c
Little name cleanup for ease of use 2023-11-01 16:48:08 +11:00
mcottontensor 3af6bff71b
Merge branch 'master' into remove_login 2023-11-01 13:29:06 +11:00
Matthew Cotton 84c8b96a66
Rewriting a bunch of reconnect and disconnect handling. Fixes #401 2023-11-01 13:16:56 +11:00
mcottontensor 44d6c4c829
Update RELEASE_VERSION
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2023-11-01 10:35:34 +11:00
Matthew Cotton 912265a6f8
Updating frontend library versions 2023-11-01 10:34:12 +11:00
Matthew Cotton 537acc2476
Updating package lock 2023-11-01 10:33:27 +11:00
Matthew Cotton c78be0404c
Updating frontend library versions 2023-11-01 10:32:46 +11:00
mcottontensor 41e7029e0c
Update package.json
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2023-11-01 10:30:44 +11:00
mcottontensor e90a685096
Merge pull request #417 from EpicGames/master
Merging master into 5.4
2023-11-01 10:29:09 +11:00
Matthew Cotton ea74d91658
Removing authentication features. 2023-10-31 09:44:41 +11:00
64 changed files with 1005 additions and 631 deletions

View File

@ -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**

View File

@ -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

View File

@ -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

View File

@ -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 Epics trademarks or registered trademarks in the US and elsewhere.
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epics trademarks or registered trademarks in the US and elsewhere.

View File

@ -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 Epics trademarks or registered trademarks in the US and elsewhere.
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epics trademarks or registered trademarks in the US and elsewhere.

View File

@ -87,4 +87,4 @@ The [/library](/Frontend/library) project has unit tests that test the Pixel Str
## Legal
Copyright &copy; 2023, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.
Copyright &copy; 2024, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.

View File

@ -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.**

View File

@ -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": [
{

View File

@ -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": {

View File

@ -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",

View File

@ -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>

View File

@ -1,2 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.

View File

@ -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;
}

View File

@ -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"

View File

@ -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",

View File

@ -156,10 +156,7 @@ export class Config {
constructor(config: ConfigParams = {}) {
const { initialSettings, useUrlParams } = config;
this._useUrlParams = !!useUrlParams;
this.populateDefaultSettings(this._useUrlParams);
if (initialSettings) {
this.setSettings(initialSettings);
}
this.populateDefaultSettings(this._useUrlParams, initialSettings);
}
/**
@ -173,7 +170,7 @@ export class Config {
/**
* Populate the default settings for a Pixel Streaming application
*/
private populateDefaultSettings(useUrlParams: boolean): void {
private populateDefaultSettings(useUrlParams: boolean, settings: Partial<AllSettings>): void {
/**
* Text Parameters
*/
@ -184,13 +181,15 @@ export class Config {
TextParameters.SignallingServerUrl,
'Signalling url',
'Url of the signalling server',
(location.protocol === 'https:' ? 'wss://' : 'ws://') +
window.location.hostname +
// for readability, we omit the port if it's 80
(window.location.port === '80' ||
window.location.port === ''
? ''
: `:${window.location.port}`),
settings && settings.hasOwnProperty(TextParameters.SignallingServerUrl) ?
settings[TextParameters.SignallingServerUrl] :
(location.protocol === 'https:' ? 'wss://' : 'ws://') +
window.location.hostname +
// for readability, we omit the port if it's 80
(window.location.port === '80' ||
window.location.port === ''
? ''
: `:${window.location.port}`),
useUrlParams
)
);
@ -201,7 +200,9 @@ export class Config {
OptionParameters.StreamerId,
'Streamer ID',
'The ID of the streamer to stream.',
'',
settings && settings.hasOwnProperty(OptionParameters.StreamerId) ?
settings[OptionParameters.StreamerId] :
'',
[],
useUrlParams
)
@ -217,29 +218,31 @@ export class Config {
'Preferred Codec',
'The preferred codec to be used during codec negotiation',
'H264 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
(function (): Array<string> {
const browserSupportedCodecs: Array<string> = [];
// Try get the info needed from the RTCRtpReceiver. This is only available on chrome
if (!RTCRtpReceiver.getCapabilities) {
browserSupportedCodecs.push('Only available on Chrome');
return browserSupportedCodecs;
}
const matcher = /(VP\d|H26\d|AV1).*/;
const codecs =
RTCRtpReceiver.getCapabilities('video').codecs;
codecs.forEach((codec) => {
const str =
codec.mimeType.split('/')[1] +
' ' +
(codec.sdpFmtpLine || '');
const match = matcher.exec(str);
if (match !== null) {
browserSupportedCodecs.push(str);
settings && settings.hasOwnProperty(OptionParameters.PreferredCodec) ?
[settings[OptionParameters.PreferredCodec]] :
(function (): Array<string> {
const browserSupportedCodecs: Array<string> = [];
// Try get the info needed from the RTCRtpReceiver. This is only available on chrome
if (!RTCRtpReceiver.getCapabilities) {
browserSupportedCodecs.push('Only available on Chrome');
return browserSupportedCodecs;
}
});
return browserSupportedCodecs;
})(),
const matcher = /(VP\d|H26\d|AV1).*/;
const codecs =
RTCRtpReceiver.getCapabilities('video').codecs;
codecs.forEach((codec) => {
const str =
codec.mimeType.split('/')[1] +
' ' +
(codec.sdpFmtpLine || '');
const match = matcher.exec(str);
if (match !== null) {
browserSupportedCodecs.push(str);
}
});
return browserSupportedCodecs;
})(),
useUrlParams
)
);
@ -254,7 +257,9 @@ export class Config {
Flags.AutoConnect,
'Auto connect to stream',
'Whether we should attempt to auto connect to the signalling server or show a click to start prompt.',
false,
settings && settings.hasOwnProperty(Flags.AutoConnect) ?
settings[Flags.AutoConnect] :
false,
useUrlParams
)
);
@ -265,7 +270,9 @@ export class Config {
Flags.AutoPlayVideo,
'Auto play video',
'When video is ready automatically start playing it as opposed to showing a play button.',
true,
settings && settings.hasOwnProperty(Flags.AutoPlayVideo) ?
settings[Flags.AutoPlayVideo] :
true,
useUrlParams
)
);
@ -276,7 +283,9 @@ export class Config {
Flags.BrowserSendOffer,
'Browser send offer',
'Browser will initiate the WebRTC handshake by sending the offer to the streamer',
false,
settings && settings.hasOwnProperty(Flags.BrowserSendOffer) ?
settings[Flags.BrowserSendOffer] :
false,
useUrlParams
)
);
@ -287,7 +296,9 @@ export class Config {
Flags.UseMic,
'Use microphone',
'Make browser request microphone access and open an input audio track.',
false,
settings && settings.hasOwnProperty(Flags.UseMic) ?
settings[Flags.UseMic] :
false,
useUrlParams
)
);
@ -298,7 +309,9 @@ export class Config {
Flags.StartVideoMuted,
'Start video muted',
'Video will start muted if true.',
false,
settings && settings.hasOwnProperty(Flags.StartVideoMuted) ?
settings[Flags.StartVideoMuted] :
false,
useUrlParams
)
);
@ -309,7 +322,9 @@ export class Config {
Flags.SuppressBrowserKeys,
'Suppress browser keys',
'Suppress certain browser keys that we use in UE, for example F5 to show shader complexity instead of refresh the page.',
true,
settings && settings.hasOwnProperty(Flags.SuppressBrowserKeys) ?
settings[Flags.SuppressBrowserKeys] :
true,
useUrlParams
)
);
@ -320,7 +335,9 @@ export class Config {
Flags.IsQualityController,
'Is quality controller?',
'True if this peer controls stream quality',
true,
settings && settings.hasOwnProperty(Flags.IsQualityController) ?
settings[Flags.IsQualityController] :
true,
useUrlParams
)
);
@ -331,7 +348,9 @@ export class Config {
Flags.ForceMonoAudio,
'Force mono audio',
'Force browser to request mono audio in the SDP',
false,
settings && settings.hasOwnProperty(Flags.ForceMonoAudio) ?
settings[Flags.ForceMonoAudio] :
false,
useUrlParams
)
);
@ -342,7 +361,9 @@ export class Config {
Flags.ForceTURN,
'Force TURN',
'Only generate TURN/Relayed ICE candidates.',
false,
settings && settings.hasOwnProperty(Flags.ForceTURN) ?
settings[Flags.ForceTURN] :
false,
useUrlParams
)
);
@ -353,7 +374,9 @@ export class Config {
Flags.AFKDetection,
'AFK if idle',
'Timeout the experience if user is AFK for a period.',
false,
settings && settings.hasOwnProperty(Flags.AFKDetection) ?
settings[Flags.AFKDetection] :
false,
useUrlParams
)
);
@ -364,7 +387,9 @@ export class Config {
Flags.MatchViewportResolution,
'Match viewport resolution',
'Pixel Streaming will be instructed to dynamically resize the video stream to match the size of the video element.',
false,
settings && settings.hasOwnProperty(Flags.MatchViewportResolution) ?
settings[Flags.MatchViewportResolution] :
false,
useUrlParams
)
);
@ -375,7 +400,9 @@ export class Config {
Flags.HoveringMouseMode,
'Control Scheme: Locked Mouse',
'Either locked mouse, where the pointer is consumed by the video and locked to it, or hovering mouse, where the mouse is not consumed.',
false,
settings && settings.hasOwnProperty(Flags.HoveringMouseMode) ?
settings[Flags.HoveringMouseMode] :
false,
useUrlParams,
(isHoveringMouse: boolean, setting: SettingBase) => {
setting.label = `Control Scheme: ${isHoveringMouse ? 'Hovering' : 'Locked'} Mouse`;
@ -389,7 +416,9 @@ export class Config {
Flags.FakeMouseWithTouches,
'Fake mouse with touches',
'A single finger touch is converted into a mouse event. This allows a non-touch application to be controlled partially via a touch device.',
false,
settings && settings.hasOwnProperty(Flags.FakeMouseWithTouches) ?
settings[Flags.FakeMouseWithTouches] :
true,
useUrlParams
)
);
@ -400,7 +429,9 @@ export class Config {
Flags.KeyboardInput,
'Keyboard input',
'If enabled, send keyboard events to streamer',
true,
settings && settings.hasOwnProperty(Flags.KeyboardInput) ?
settings[Flags.KeyboardInput] :
true,
useUrlParams
)
);
@ -411,7 +442,9 @@ export class Config {
Flags.MouseInput,
'Mouse input',
'If enabled, send mouse events to streamer',
true,
settings && settings.hasOwnProperty(Flags.MouseInput) ?
settings[Flags.MouseInput] :
true,
useUrlParams
)
);
@ -422,7 +455,9 @@ export class Config {
Flags.TouchInput,
'Touch input',
'If enabled, send touch events to streamer',
true,
settings && settings.hasOwnProperty(Flags.TouchInput) ?
settings[Flags.TouchInput] :
true,
useUrlParams
)
);
@ -433,7 +468,9 @@ export class Config {
Flags.GamepadInput,
'Gamepad input',
'If enabled, send gamepad events to streamer',
true,
settings && settings.hasOwnProperty(Flags.GamepadInput) ?
settings[Flags.GamepadInput] :
true,
useUrlParams
)
);
@ -444,7 +481,9 @@ export class Config {
Flags.XRControllerInput,
'XR controller input',
'If enabled, send XR controller events to streamer',
true,
settings && settings.hasOwnProperty(Flags.XRControllerInput) ?
settings[Flags.XRControllerInput] :
true,
useUrlParams
)
);
@ -455,7 +494,9 @@ export class Config {
Flags.WaitForStreamer,
'Wait for streamer',
'Will continue trying to connect to the first streamer available.',
true,
settings && settings.hasOwnProperty(Flags.WaitForStreamer) ?
settings[Flags.WaitForStreamer] :
true,
useUrlParams
)
);
@ -472,7 +513,9 @@ export class Config {
'The time (in seconds) it takes for the application to time out if AFK timeout is enabled.',
0 /*min*/,
600 /*max*/,
120 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.AFKTimeoutSecs) ?
settings[NumericParameters.AFKTimeoutSecs] :
120, /*value*/
useUrlParams
)
);
@ -485,7 +528,9 @@ export class Config {
'Maximum number of reconnects the application will attempt when a streamer disconnects.',
0 /*min*/,
999 /*max*/,
3 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.MaxReconnectAttempts) ?
settings[NumericParameters.MaxReconnectAttempts] :
3, /*value*/
useUrlParams
)
);
@ -498,7 +543,9 @@ export class Config {
'The lower bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
0 /*min*/,
51 /*max*/,
0 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.MinQP) ?
settings[NumericParameters.MinQP] :
0, /*value*/
useUrlParams
)
);
@ -511,7 +558,9 @@ export class Config {
'The upper bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
0 /*min*/,
51 /*max*/,
51 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.MaxQP) ?
settings[NumericParameters.MaxQP] :
51, /*value*/
useUrlParams
)
);
@ -524,7 +573,9 @@ export class Config {
'The maximum FPS that WebRTC will try to transmit frames at.',
1 /*min*/,
999 /*max*/,
60 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.WebRTCFPS) ?
settings[NumericParameters.WebRTCFPS] :
60, /*value*/
useUrlParams
)
);
@ -537,7 +588,9 @@ export class Config {
'The minimum bitrate that WebRTC should use.',
0 /*min*/,
500000 /*max*/,
0 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.WebRTCMinBitrate) ?
settings[NumericParameters.WebRTCMinBitrate] :
0, /*value*/
useUrlParams
)
);
@ -550,7 +603,9 @@ export class Config {
'The maximum bitrate that WebRTC should use.',
0 /*min*/,
500000 /*max*/,
0 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.WebRTCMaxBitrate) ?
settings[NumericParameters.WebRTCMaxBitrate] :
0, /*value*/
useUrlParams
)
);
@ -563,7 +618,9 @@ export class Config {
'Delay between retries when waiting for an available streamer.',
500 /*min*/,
900000 /*max*/,
3000 /*value*/,
settings && settings.hasOwnProperty(NumericParameters.StreamerAutoJoinInterval) ?
settings[NumericParameters.StreamerAutoJoinInterval] :
3000, /*value*/
useUrlParams
)
);
@ -742,7 +799,13 @@ export class Config {
`Cannot set text setting called ${id} - it does not exist in the Config.enumParameters map.`
);
} else {
this.optionParameters.get(id).selected = settingValue;
const optionSetting = this.optionParameters.get(id);
const existingOptions = optionSetting.options;
if (!existingOptions.includes(settingValue)) {
existingOptions.push(settingValue);
optionSetting.options = existingOptions;
}
optionSetting.selected = settingValue;
}
}
@ -762,24 +825,24 @@ export class Config {
}
}
/**
* Set a subset of all settings in one function call.
*
* @param settings A (partial) list of settings to set
*/
setSettings(settings: Partial<AllSettings>) {
for (const key of Object.keys(settings)) {
if (isFlagId(key)) {
this.setFlagEnabled(key, settings[key]);
} else if (isNumericId(key)) {
this.setNumericSetting(key, settings[key]);
} else if (isTextId(key)) {
this.setTextSetting(key, settings[key]);
} else if (isOptionId(key)) {
this.setOptionSettingValue(key, settings[key]);
/**
* Set a subset of all settings in one function call.
*
* @param settings A (partial) list of settings to set
*/
setSettings(settings: Partial<AllSettings>) {
for (const key of Object.keys(settings)) {
if (isFlagId(key)) {
this.setFlagEnabled(key, settings[key]);
} else if (isNumericId(key)) {
this.setNumericSetting(key, settings[key]);
} else if (isTextId(key)) {
this.setTextSetting(key, settings[key]);
} else if (isOptionId(key)) {
this.setOptionSettingValue(key, settings[key]);
}
}
}
}
/**
* Get all settings

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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' })
]

View File

@ -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;
}
}

View File

@ -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 {
/**

View File

@ -1,12 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
import { WebSocketController } from '../WebSockets/WebSocketController';
import { ExtraOfferParameters, ExtraAnswerParameters } from '../WebSockets/MessageSend';
import { StreamController } from '../VideoPlayer/StreamController';
import {
MessageAnswer,
MessageOffer,
MessageConfig,
MessageStreamerList
MessageStreamerList,
MessageStreamerIDChanged
} from '../WebSockets/MessageReceive';
import { FreezeFrameController } from '../FreezeFrame/FreezeFrameController';
import { AFKController } from '../AFK/AFKController';
@ -59,7 +61,8 @@ import {
PlayStreamErrorEvent,
PlayStreamEvent,
PlayStreamRejectedEvent,
StreamerListMessageEvent
StreamerListMessageEvent,
StreamerIDChangedMessageEvent
} from '../Util/EventEmitter';
import {
DataChannelLatencyTestRequest,
@ -104,16 +107,13 @@ export class WebRtcPlayerController {
preferredCodec: string;
peerConfig: RTCConfiguration;
videoAvgQp: number;
locallyClosed: boolean;
shouldReconnect: boolean;
isReconnecting: boolean;
reconnectAttempt: number;
subscribedStream: string | null;
disconnectMessage: string;
subscribedStream: string;
signallingUrlBuilder: () => string;
// if you override the disconnection message by calling the interface method setDisconnectMessageOverride
// it will use this property to store the override message string
disconnectMessageOverride: string;
autoJoinTimer: ReturnType<typeof setTimeout> = undefined;
/**
@ -139,10 +139,7 @@ export class WebRtcPlayerController {
this.onAfkTriggered.bind(this)
);
this.afkController.onAFKTimedOutCallback = () => {
this.setDisconnectMessageOverride(
'You have been disconnected due to inactivity'
);
this.closeSignalingServer();
this.closeSignalingServer('You have been disconnected due to inactivity');
};
this.freezeFrameController = new FreezeFrameController(
@ -202,14 +199,9 @@ export class WebRtcPlayerController {
this.webSocketController.onStreamerList = (
messageList: MessageReceive.MessageStreamerList
) => this.handleStreamerListMessage(messageList);
this.webSocketController.onWebSocketOncloseOverlayMessage = (event) => {
this.pixelStreaming._onDisconnect(
`Websocket disconnect (${event.code}) ${
event.reason != '' ? '- ' + event.reason : ''
}`
);
this.setVideoEncoderAvgQP(0);
};
this.webSocketController.onStreamerIDChanged = (
message: MessageReceive.MessageStreamerIDChanged
) => this.handleStreamerIDChangedMessage(message);
this.webSocketController.onPlayerCount = (playerCount: MessageReceive.MessagePlayerCount) => {
this.pixelStreaming._onPlayerCount(playerCount.count);
};
@ -223,6 +215,19 @@ export class WebRtcPlayerController {
}
});
this.webSocketController.onClose.addEventListener('close', (event : CustomEvent) => {
// when we refresh the page during a stream we get the going away code.
// in that case we don't want to reconnect since we're navigating away.
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
// lists all the codes.
const CODE_GOING_AWAY = 1001;
const willTryReconnect = this.shouldReconnect
&& event.detail.code != CODE_GOING_AWAY
&& this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts) > 0
const disconnectMessage = this.disconnectMessage ? this.disconnectMessage : event.detail.reason;
this.pixelStreaming._onDisconnect(disconnectMessage, !willTryReconnect && !this.isReconnecting);
this.afkController.stopAfkWarningTimer();
// stop sending stats on interval if we have closed our connection
@ -230,21 +235,22 @@ export class WebRtcPlayerController {
window.clearInterval(this.statsTimerHandle);
}
// reset the stream quality icon.
this.setVideoEncoderAvgQP(0);
// unregister all input device event handlers on disconnect
this.setTouchInputEnabled(false);
this.setMouseInputEnabled(false);
this.setKeyboardInputEnabled(false);
this.setGamePadInputEnabled(false);
// when we refresh the page during a stream we get the going away code.
// in that case we don't want to reconnect since we're navigating away.
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
// lists all the codes.
const CODE_GOING_AWAY = 1001;
if(this.shouldReconnect && event.detail.code != CODE_GOING_AWAY && this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts) > 0) {
this.isReconnecting = true;
this.reconnectAttempt++;
this.restartStreamAutomatically();
if (willTryReconnect) {
// need a small delay here to prevent reconnect spamming
setTimeout(() => {
this.isReconnecting = true;
this.reconnectAttempt++;
this.tryReconnect(event.detail.reason);
}, 2000);
}
});
@ -947,9 +953,9 @@ export class WebRtcPlayerController {
}
/**
* Restart the stream automatically without refreshing the page
* Attempt a reconnection to the signalling server
*/
restartStreamAutomatically() {
tryReconnect(message: string) {
// if there is no webSocketController return immediately or this will not work
if (!this.webSocketController) {
Logger.Log(
@ -959,33 +965,16 @@ export class WebRtcPlayerController {
return;
}
// if a websocket object has not been created connect normally without closing
if (
!this.webSocketController.webSocket ||
this.webSocketController.webSocket.readyState === WebSocket.CLOSED
) {
Logger.Log(
Logger.GetStackTrace(),
'A websocket connection has not been made yet so we will start the stream'
);
// if the connection is open, first close it. wait some time and try again.
this.isReconnecting = true;
if (this.webSocketController.webSocket && this.webSocketController.webSocket.readyState != WebSocket.CLOSED) {
this.closeSignalingServer(`${message} Restarting stream...`);
setTimeout(() => {
this.tryReconnect(message);
}, 3000);
} else {
this.pixelStreaming._onWebRtcAutoConnect();
this.connectToSignallingServer();
} else {
// set the replay status so we get a text overlay over an action overlay
this.pixelStreaming._showActionOrErrorOnDisconnect = false;
// set the disconnect message
this.setDisconnectMessageOverride('Restarting stream...');
// close the connection
this.closeSignalingServer();
// wait for the connection to close and restart the connection
const autoConnectTimeout = setTimeout(() => {
this.pixelStreaming._onWebRtcAutoConnect();
this.connectToSignallingServer();
clearTimeout(autoConnectTimeout);
}, 3000);
}
}
@ -1087,13 +1076,8 @@ export class WebRtcPlayerController {
);
Logger.Error(Logger.GetStackTrace(), message);
// set the disconnect message
this.setDisconnectMessageOverride(
'Stream not initialized correctly'
);
// close the connection
this.closeSignalingServer();
this.closeSignalingServer('Stream not initialized correctly');
return;
}
@ -1176,6 +1160,9 @@ export class WebRtcPlayerController {
* Connect to the Signaling server
*/
connectToSignallingServer() {
this.locallyClosed = false;
this.shouldReconnect = true;
this.disconnectMessage = null;
const signallingUrl = this.signallingUrlBuilder();
this.webSocketController.connect(signallingUrl);
}
@ -1198,10 +1185,7 @@ export class WebRtcPlayerController {
Logger.GetStackTrace(),
'No turn server was found in the Peer Connection Options. TURN cannot be forced, closing connection. Please use STUN instead'
);
this.setDisconnectMessageOverride(
'TURN cannot be forced, closing connection. Please use STUN instead.'
);
this.closeSignalingServer();
this.closeSignalingServer('TURN cannot be forced, closing connection. Please use STUN instead.');
return;
}
}
@ -1343,84 +1327,124 @@ export class WebRtcPlayerController {
6
);
if(this.isReconnecting) {
if(messageStreamerList.ids.includes(this.subscribedStream)) {
// If we're reconnecting and the previously subscribed stream has come back, resubscribe to it
this.isReconnecting = false;
this.reconnectAttempt = 0;
this.webSocketController.sendSubscribe(this.subscribedStream);
} else if(this.reconnectAttempt < this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts)) {
// Our previous stream hasn't come back, wait 2 seconds and request an updated stream list
this.reconnectAttempt++;
setTimeout(() => {
this.webSocketController.requestStreamerList();
}, 2000)
} else {
// We've exhausted our reconnect attempts, return to main screen
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.shouldReconnect = false;
this.webSocketController.close();
let wantedStreamerId: string = null;
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
""
);
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
[]
);
}
} else {
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
// get the current selected streamer id option
var streamerIDOption = this.config.getSettingOption(OptionParameters.StreamerId);
const existingSelection = streamerIDOption.selected.toString().trim();
if (!!existingSelection) {
// default to selected option if it exists
wantedStreamerId = streamerIDOption.selected;
}
// add the streamers to the UI
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
settingOptions
);
let autoSelectedStreamerId: string = null;
const waitForStreamer = this.config.isFlagEnabled(Flags.WaitForStreamer);
const reconnectLimit = this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts);
const reconnectDelay = this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval);
// first we figure out a wanted streamer id through various means
const useUrlParams = this.config.useUrlParams;
const urlParams = new URLSearchParams(window.location.search);
if (useUrlParams && urlParams.has(OptionParameters.StreamerId)) {
// if we've set the streamer id on the url we only want that streamer id
wantedStreamerId = urlParams.get(OptionParameters.StreamerId);
} else if (this.subscribedStream) {
// we were previously subscribed to a streamer, we want that
wantedStreamerId = this.subscribedStream;
}
// now lets see if we can pick it.
if (wantedStreamerId && messageStreamerList.ids.includes(wantedStreamerId)) {
// if the wanted stream is in the list. we pick that
autoSelectedStreamerId = wantedStreamerId;
} else if ((!wantedStreamerId || !waitForStreamer) && messageStreamerList.ids.length == 1) {
// otherwise, if we're not waiting for the wanted streamer and there's only one streamer, connect to it
autoSelectedStreamerId = messageStreamerList.ids[0];
}
// if we found a streamer id to auto select, select it
if (autoSelectedStreamerId) {
this.isReconnecting = false;
this.reconnectAttempt = 0;
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
settingOptions
autoSelectedStreamerId
);
const urlParams = new URLSearchParams(window.location.search);
let autoSelectedStreamerId: string | null = null;
if (messageStreamerList.ids.length == 1) {
// If there's only a single streamer, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = messageStreamerList.ids[0];
} else if (
urlParams.has(OptionParameters.StreamerId) &&
messageStreamerList.ids.includes(
urlParams.get(OptionParameters.StreamerId)
)
) {
// If there's a streamer ID in the URL and a streamer with this ID is connected, set it as the selected streamer
autoSelectedStreamerId = urlParams.get(OptionParameters.StreamerId);
}
if (autoSelectedStreamerId !== null) {
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
autoSelectedStreamerId
);
} else {
// no auto selected streamer
if (messageStreamerList.ids.length == 0 && this.config.isFlagEnabled(Flags.WaitForStreamer)) {
this.closeSignalingServer();
this.startAutoJoinTimer();
} else {
// no auto selected streamer.
// if we're waiting for a streamer then try reconnecting
if (waitForStreamer) {
if (this.reconnectAttempt < reconnectLimit) {
// still reconnects available
this.isReconnecting = true;
this.reconnectAttempt++;
setTimeout(() => {
this.webSocketController.requestStreamerList();
}, reconnectDelay);
} else {
// We've exhausted our reconnect attempts, return to main screen
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.shouldReconnect = false;
}
}
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId
})
);
}
// dispatch this event finally
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId,
wantedStreamerId
})
);
}
startAutoJoinTimer() {
clearTimeout(this.autoJoinTimer);
this.autoJoinTimer = setTimeout(() => this.tryAutoJoin(), this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval));
}
handleStreamerIDChangedMessage(streamerIDChangedMessage: MessageStreamerIDChanged) {
const newID = streamerIDChangedMessage.newID;
tryAutoJoin() {
this.connectToSignallingServer();
// need to edit the selected streamer in the settings list
var streamerListOptions = this.config.getSettingOption(OptionParameters.StreamerId);
// temporarily prevent onChange from firing (it would try to subscribe to the streamer again)
var oldOnChange = streamerListOptions.onChange;
streamerListOptions.onChange = ()=>{};
// change the selected entry.
var streamerList = streamerListOptions.options;
for (var i = 0; i < streamerList.length; ++i) {
if (streamerList[i] == this.subscribedStream) {
streamerList[i] = newID;
break;
}
}
// update the list
streamerListOptions.options = streamerList;
// update the selected entry
streamerListOptions.selected = newID;
// restore the old change notifier.
streamerListOptions.onChange = oldOnChange;
// remember which stream we're subscribe to
this.subscribedStream = streamerIDChangedMessage.newID;
// notify any listeners
this.pixelStreaming.dispatchEvent(
new StreamerIDChangedMessageEvent({
newID
})
);
}
/**
@ -1582,7 +1606,13 @@ export class WebRtcPlayerController {
'Sending the offer to the Server',
6
);
this.webSocketController.sendWebRtcOffer(offer);
const extraParams: ExtraOfferParameters = {
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
};
this.webSocketController.sendWebRtcOffer(offer, extraParams);
}
/**
@ -1595,7 +1625,13 @@ export class WebRtcPlayerController {
'Sending the answer to the Server',
6
);
this.webSocketController.sendWebRtcAnswer(answer);
const extraParams: ExtraAnswerParameters = {
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
};
this.webSocketController.sendWebRtcAnswer(answer, extraParams);
if (this.isUsingSFU) {
this.webSocketController.sendWebRtcDatachannelRequest();
@ -1617,9 +1653,11 @@ export class WebRtcPlayerController {
/**
* Close the Connection to the signaling server
*/
closeSignalingServer() {
closeSignalingServer(message: string) {
// We explicitly called close, therefore we don't want to trigger auto reconnect
this.locallyClosed = true;
this.shouldReconnect = false;
this.disconnectMessage = message;
this.webSocketController?.close();
}
@ -1634,7 +1672,7 @@ export class WebRtcPlayerController {
* Close all connections
*/
close() {
this.closeSignalingServer();
this.closeSignalingServer('');
this.closePeerConnection();
}
@ -2018,20 +2056,6 @@ export class WebRtcPlayerController {
this.videoPlayer.resizePlayerStyle();
}
/**
* Get the overridden disconnect message
*/
getDisconnectMessageOverride(): string {
return this.disconnectMessageOverride;
}
/**
* Set the override for the disconnect message
*/
setDisconnectMessageOverride(message: string): void {
this.disconnectMessageOverride = message;
}
setPreferredCodec(codec: string) {
this.preferredCodec = codec;
if (this.peerConnectionController) {

View File

@ -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
*/

View File

@ -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;
}
}
/**

View File

@ -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,

View File

@ -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
*/

View File

@ -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"

View File

@ -1,6 +1,6 @@
{
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4",
"version": "0.0.1",
"version": "0.0.3",
"description": "Reference frontend UI library for Unreal Engine 5.4 Pixel Streaming - gives the stock look and feel.",
"main": "dist/lib-pixelstreamingfrontend-ui.js",
"module": "dist/lib-pixelstreamingfrontend-ui.esm.js",
@ -16,7 +16,7 @@
"spellcheck": "cspell \"{README.md,.github/*.md,src/**/*.ts}\""
},
"devDependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1",
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"cspell": "^4.1.0",
@ -34,7 +34,7 @@
"jss-plugin-global": "^10.9.2"
},
"peerDependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.1"
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3"
},
"repository": {
"type": "git",
@ -46,4 +46,3 @@
"access": "public"
}
}

View File

@ -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);
}
}
}

View File

@ -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(

View File

@ -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);

View File

@ -32,7 +32,6 @@ module.exports = {
})
],
output: {
path: path.resolve(__dirname, 'dist'),
globalObject: 'this'
}
};

View File

@ -1,4 +1,4 @@
Copyright 2004-2022, Epic Games, Inc.
Copyright 2004-2024, Epic Games, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -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

View File

@ -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
View File

@ -1,107 +1,3 @@
| Branch | | | | |
| -------|--|--|--|--|
| UE5.4 | [![Publish frontend lib](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml/badge.svg?branch=UE5.4)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml) | [![Publish ui-lib](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml/badge.svg?branch=UE5.4)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml) | [![Publish cirrus container](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml/badge.svg?branch=UE5.4)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml) | [![Releases](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml/badge.svg?branch=UE5.4)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml) |
| UE5.3 | [![Publish frontend lib](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml/badge.svg?branch=UE5.3)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-library-to-npm.yml) | [![Publish ui-lib](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml/badge.svg?branch=UE5.3)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/publish-ui-library-to-npm.yml) | [![Publish cirrus container](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml/badge.svg?branch=UE5.3)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/container-images.yml) | [![Releases](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml/badge.svg?branch=UE5.3)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/create-gh-release.yml) |
| Master | [![Run library unit tests](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/run-library-unit-tests.yml/badge.svg?branch=master)](https://github.com/EpicGames/PixelStreamingInfrastructure/actions/workflows/run-library-unit-tests.yml) |
# PixelStreamingInfrastructure has moved [here!](https://github.com/EpicGamesExt/PixelStreamingInfrastructure)
# The official home for the Pixel Streaming servers and frontend!
The frontend and web server elements for Unreal Pixel Streaming (previously located in `Samples/PixelStreaming/WebServers`) are now in this repository, for all to contribute to. They are referred to as the **Pixel Streaming Infrastructure**.
## Goals
The goals of this repository are to:
- Increase the release cadence for the Pixel Streaming servers (to mitigate browser breaking changes sooner).
- Encourage easier contribution of these components by Unreal Engine licensees.
- Facilitate a more standard web release mechanism.
- Grant a permissive license to distribute and modify this code wherever you see fit (MIT licensed).
## Contributing
If you would like to contribute to our repository, please reference our [contribution guide](CONTRIBUTING.md). Thank you for your time and your efforts!
## Contents
The Pixel Streaming Infrastructure contains reference implementations for all the components needed to run a pixel streaming application. They are structured as separate projects, which work together, but are designed to be modular and interoperable with other implementations which use WebRTC technology. These implementations include:
- A signalling web server, called Cirrus, found in [`SignallingWebServer/`](SignallingWebServer/).
- An SFU (Selective Forwarding Unit), found in [`SFU/`](SFU/).
- A matchmaker, found in [`Matchmaker/`](Matchmaker/).
- Several frontend projects for the WebRTC player and input, found in [`Frontend/`](Frontend/):
- shared libraries for [communication](Frontend/library/) and [UI](Frontend/ui-library/) functionality
- separate [implementations](Frontend/implementations/) using different techologies such as TypeScript or React/JSX
For detailed information, see the [/frontend](/Frontend/).
## Releases
We release a number of different components under this repository, specifically:
- Container images for the signalling server
- NPM packages for the frontend
- Source releases of this repo with the reference frontend built as a minified js bundle
### Container images
The following container images are built from this repository:
- [ghcr.io/epicgames/pixel-streaming-signalling-server](https://github.com/orgs/EpicGames/packages/container/package/pixel-streaming-signalling-server) (since Unreal Engine 5.1)
( This link requires you to join Epic's Github org )
### NPM Packages
The following are `unofficial` NPM packages (official ones coming soon):
| Branch | Frontend library | Frontend reference ui |
|--------|------------------|-----------------------|
| UE5.3 |[lib-pixelstreamingfrontend-ue5.3](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3)|[lib-pixelstreamingfrontend-ui-ue5.3](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3)|
### NPM getting started
```bash
#frontend (core lib)
npm i @epicgames-ps/lib-pixelstreamingfrontend-ue5.3
#frontend ui
npm i @epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3
```
## Documentation
* [General Docs](/Docs/README.md)
* [Frontend Docs](/Frontend/)
* Signalling Server Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/255)
* Matchmaker Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/256)
* SFU Docs [TO DO](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/257)
### Tagged source releases + built typescript frontend
[Github releases](https://github.com/EpicGames/PixelStreamingInfrastructure/releases)
## Versions
We maintain versions of the servers and frontend that are compatible with existing and in-development version of Unreal Engine.
:warning: **There are breaking changes between UE versions - so make sure you get the right version**. :warning:
<ins>For a list of major changes between versions please refer to the [changelog](https://github.com/EpicGames/PixelStreamingInfrastructure/blob/master/CHANGELOG.md).</ins>
This repository contains the following in branches that track Unreal Engine versions:
| Branch | Status |
|--------|--------|
|[Master](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/master)| Dev |
|[UE5.4](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.4)| Pre-release |
|[UE5.3](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.3)| Current |
|[UE5.2](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.2)| Supported |
|[UE5.1](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.1)| End of life |
|[UE5.0](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.0)| Unsupported |
|[UE4.27](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.27)| Unsupported |
|[UE4.26](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.26)| Unsupported |
| Legend | Meaning |
|---------|-----------|
| Dev | This is our dev branch, intended to be paired with [ue5-main](https://github.com/EpicGames/UnrealEngine/tree/ue5-main) - experimental. |
|Pre-release| Code in here will be paired with the next UE release, we periodically update this branch from `master`. |
| Current | Supported and this is the branch tracking the **latest released** version of UE. |
| Supported | We will accept bugfixes/issues for this version. |
| End of life | Once the next UE version is released we will not support this version anymore. |
| Unsupported | We will not be supporting this version with bugfixes. |
## Legal
© 2004-2023, Epic Games, Inc. Unreal and its logo are Epics 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).

View File

@ -1 +1 @@
0.0.1
0.0.3

13
SFU/Dockerfile Normal file
View File

@ -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}

67
SFU/README.md Normal file
View File

@ -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&lt;WorkerLogTag&gt; | | 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&lt;RtpCodecCapability&gt; | | 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&lt;TransportListenIp|String&gt; | | 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```

46
SFU/package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -2,6 +2,7 @@ const config = require('./config');
const WebSocket = require('ws');
const mediasoup = require('mediasoup_prebuilt');
const mediasoupSdp = require('mediasoup-sdp-bridge');
const minimist = require('minimist');
if (!config.retrySubscribeDelaySecs) {
config.retrySubscribeDelaySecs = 10;
@ -351,6 +352,12 @@ async function createWebRtcTransport(identifier) {
}
async function main() {
var argv = minimist(process.argv.slice(2));
if ('signallingURL' in argv) {
config.signallingURL = argv['signallingURL'];
}
console.log('Starting Mediasoup...');
console.log("Config = ");
console.log(config);

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -20,7 +20,6 @@ const defaultConfig = {
UseHTTPS: false,
HTTPSCertFile: './certificates/client-cert.pem',
HTTPSKeyFile: './certificates/client-key.pem',
UseAuthentication: false,
LogToFile: true,
LogVerbose: true,
HomepageFile: 'player.html',
@ -60,18 +59,6 @@ if (config.UseHTTPS) {
var https = require('https').Server(options, app);
}
//If not using authetication then just move on to the next function/middleware
var isAuthenticated = redirectUrl => function (req, res, next) { return next(); }
if (config.UseAuthentication && config.UseHTTPS) {
var passport = require('passport');
require('./modules/authentication').init(app);
// Replace the isAuthenticated with the one setup on passport module
isAuthenticated = passport.authenticationMiddleware ? passport.authenticationMiddleware : isAuthenticated
} else if (config.UseAuthentication && !config.UseHTTPS) {
console.error('Trying to use authentication without using HTTPS, this is not allowed and so authentication will NOT be turned on, please turn on HTTPS to turn on authentication');
}
const helmet = require('helmet');
var hsts = require('hsts');
var net = require('net');
@ -205,44 +192,19 @@ var limiter = RateLimit({
// apply rate limiter to all requests
app.use(limiter);
//Setup the login page if we are using authentication
if(config.UseAuthentication){
if(config.EnableWebserver) {
app.get('/login', function(req, res){
res.sendFile(path.join(__dirname, '/Public', '/login.html'));
});
}
// create application/x-www-form-urlencoded parser
var urlencodedParser = bodyParser.urlencoded({ extended: false })
//login page form data is posted here
app.post('/login',
urlencodedParser,
passport.authenticate('local', { failureRedirect: '/login' }),
function(req, res){
//On success try to redirect to the page that they originally tired to get to, default to '/' if no redirect was found
var redirectTo = req.session.redirectTo ? req.session.redirectTo : '/';
delete req.session.redirectTo;
console.log(`Redirecting to: '${redirectTo}'`);
res.redirect(redirectTo);
}
);
}
if(config.EnableWebserver) {
//Setup folders
app.use(express.static(path.join(__dirname, '/Public')))
app.use('/images', express.static(path.join(__dirname, './images')))
app.use('/scripts', [isAuthenticated('/login'),express.static(path.join(__dirname, '/scripts'))]);
app.use('/', [isAuthenticated('/login'), express.static(path.join(__dirname, '/custom_html'))])
app.use('/scripts', express.static(path.join(__dirname, '/scripts')));
app.use('/', express.static(path.join(__dirname, '/custom_html')))
}
try {
for (var property in config.AdditionalRoutes) {
if (config.AdditionalRoutes.hasOwnProperty(property)) {
console.log(`Adding additional routes "${property}" -> "${config.AdditionalRoutes[property]}"`)
app.use(property, [isAuthenticated('/login'), express.static(path.join(__dirname, config.AdditionalRoutes[property]))]);
app.use(property, express.static(path.join(__dirname, config.AdditionalRoutes[property])));
}
}
} catch (err) {
@ -252,7 +214,7 @@ try {
if(config.EnableWebserver) {
// Request has been sent to site root, send the homepage file
app.get('/', isAuthenticated('/login'), function (req, res) {
app.get('/', function (req, res) {
homepageFile = (typeof config.HomepageFile != 'undefined' && config.HomepageFile != '') ? config.HomepageFile.toString() : defaultConfig.HomepageFile;
let pathsToTry = [ path.join(__dirname, homepageFile), path.join(__dirname, '/Public', homepageFile), path.join(__dirname, '/custom_html', homepageFile), homepageFile ];
@ -529,7 +491,7 @@ function requestStreamerId(streamer) {
streamer.idTimer = setTimeout(function() {
// streamer did not respond in time. give it a legacy id.
const newLegacyId = getUniqueLegacyId();
const newLegacyId = getUniqueLegacyStreamerId();
if (newLegacyId.length == 0) {
const error = `Ran out of legacy ids.`;
console.error(error);
@ -562,6 +524,20 @@ function sanitizeStreamerId(id) {
}
function registerStreamer(id, streamer) {
// remove any existing streamer id
if (!!streamer.id) {
// notify any connected peers of rename
const renameMessage = { type: "streamerIDChanged", newID: id };
let clone = new Map(players);
for (let player of clone.values()) {
if (player.streamerId == streamer.id) {
logOutgoing(player.id, renameMessage);
player.sendTo(renameMessage);
player.streamerId = id; // reassign the subscription
}
}
streamers.delete(streamer.id);
}
// make sure the id is unique
const uniqueId = sanitizeStreamerId(id);
streamer.commitId(uniqueId);
@ -574,6 +550,10 @@ function registerStreamer(id, streamer) {
}
function onStreamerDisconnected(streamer) {
if (!!streamer.idTimer) {
clearTimeout(streamer.idTimer);
}
if (!streamer.id || !streamers.has(streamer.id)) {
return;
}
@ -683,13 +663,16 @@ streamerServer.on('connection', function (ws, req) {
console.error(`streamer ${streamer.id} connection error: ${error}`);
onStreamerDisconnected(streamer);
try {
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `streamer ${streamer.id} connection error: ${error}`);
} catch(err) {
console.error(`ERROR: ws.on error: ${err.message}`);
}
});
ws.send(JSON.stringify(clientConfig));
const configStr = JSON.stringify(clientConfig);
logOutgoing(streamer.id, configStr)
ws.send(configStr);
requestStreamerId(streamer);
});
@ -847,7 +830,7 @@ sfuServer.on('connection', function (ws, req) {
console.error(`SFU connection error: ${error}`);
onSFUDisconnected(playerComponent);
try {
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `SFU connection error: ${error}`);
} catch(err) {
console.error(`ERROR: ws.on error: ${err.message}`);
}
@ -975,7 +958,7 @@ playerServer.on('connection', function (ws, req) {
ws.on('error', function(error) {
console.error(`player ${playerId} connection error: ${error}`);
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `player ${playerId} connection error: ${error}`);
onPlayerDisconnected(playerId);
console.logColor(logging.Red, `Trying to reconnect...`);
@ -984,7 +967,11 @@ playerServer.on('connection', function (ws, req) {
sendPlayerConnectedToFrontend();
sendPlayerConnectedToMatchmaker();
player.ws.send(JSON.stringify(clientConfig));
const configStr = JSON.stringify(clientConfig);
logOutgoing(player.id, configStr)
player.ws.send(configStr);
sendPlayersCount();
});
@ -992,7 +979,7 @@ function disconnectAllPlayers(streamerId) {
console.log(`unsubscribing all players on ${streamerId}`);
let clone = new Map(players);
for (let player of clone.values()) {
if (player.streamerId == streamerId) {
if (player.streamerId == streamerId) {
// disconnect players but just unsubscribe the SFU
const sfuPlayer = getSFUForStreamer(streamerId);
if (sfuPlayer && player.id == sfuPlayer.id) {

View File

@ -2,7 +2,6 @@
"UseFrontend": false,
"UseMatchmaker": false,
"UseHTTPS": false,
"UseAuthentication": false,
"LogToFile": true,
"LogVerbose": true,
"HomepageFile": "player.html",

View File

@ -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'.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -1,14 +1,14 @@
#!/bin/bash
# Copyright Epic Games, Inc. All Rights Reserved.
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
BASH_LOCATION="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
pushd "${BASH_LOCATION}" > /dev/null
source common_utils.sh
set_start_default_values "n" "n" # No server specific defaults
use_args "$@"
call_setup_sh
use_args "$*"
call_setup_sh $*
print_parameters
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
@ -34,3 +34,4 @@ start_process $process $arguments
popd > /dev/null # ../..
popd > /dev/null # BASH_SOURCE

View File

@ -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