Compare commits

...

154 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
mcottontensor da0a871234
Merge branch 'UE5.4' into master 2023-11-01 10:26:11 +11:00
mcottontensor b16bb6c75c
Merge pull request #416 from EpicGames/backport/UE5.4/pr-411
[UE5.4] Merge pull request #411 from mcottontensor/streamer_ids_fix
2023-11-01 10:05:25 +11:00
Matthew Cotton 22de24e61a Fixing sfu forwarding.
(cherry picked from commit 7f07f4b29e)
2023-10-31 22:57:16 +00:00
Matthew Cotton fdb6d6d8a0 Catching case where we're sanitizing a streamer id that is numeric. Updating docs to remove PreferSFU (SFU is just selected as a streamer now).
(cherry picked from commit 339aa088cc)
2023-10-31 22:57:16 +00:00
Matthew Cotton 3d7bfb0723 Updating the handling of generating new legacy streamer and sfu ids.
(cherry picked from commit 403fe39f4b)
2023-10-31 22:57:16 +00:00
Matthew Cotton 63f8a320e1 Cleanup and fixing sfu behaviour.
(cherry picked from commit adfca6c42d)
2023-10-31 22:57:16 +00:00
Matthew Cotton 78d68a8a8b Removing PreferSFU option since this is now handled with the stream selection option. Fixing browser behaviour when multiple streamers detected (previous failed tests).
(cherry picked from commit bbcfe8a6b5)
2023-10-31 22:57:16 +00:00
Matthew Cotton e88871c226 Just some small cleanup
(cherry picked from commit 2ce53023ee)
2023-10-31 22:57:16 +00:00
Matthew Cotton 48b256753a Fixing the windows build script nuking the PATH env variable.
(cherry picked from commit 090cc89b08)
2023-10-31 22:57:16 +00:00
Matthew Cotton ddb4c4776e Allowing SFU to work with multiple streamers.
(cherry picked from commit c0e715ca9d)
2023-10-31 22:57:16 +00:00
Matthew Cotton f8de08ab61 working on handling multiple sfus gracefully
(cherry picked from commit 01d8056bee)
2023-10-31 22:57:16 +00:00
Matthew Cotton 1e5d075d5f Better handling of streamer ids. Specifically legacy ids.
(cherry picked from commit 127feac2e4)
2023-10-31 22:57:16 +00:00
mcottontensor 25024f6848
Merge pull request #411 from mcottontensor/streamer_ids_fix
Fixes for Streamers changing IDs when connecting & Updates to SFU behaviour relating to multi streamer changes.
2023-11-01 09:56:47 +11:00
Matthew Cotton 7f07f4b29e
Fixing sfu forwarding. 2023-10-31 12:54:41 +11:00
Luke Bermingham f2cde5176a
Update link to frontend docs to be /frontend
Signed-off-by: Luke Bermingham <1215582+lukehb@users.noreply.github.com>
2023-10-31 09:07:28 +10:00
Luke Bermingham 5c65721da3
Update link to frontend docs to go to /frontend
Signed-off-by: Luke Bermingham <1215582+lukehb@users.noreply.github.com>
2023-10-31 09:05:57 +10:00
Matthew Cotton ea74d91658
Removing authentication features. 2023-10-31 09:44:41 +11:00
mcottontensor b97dcb11cd
Merge pull request #410 from timbotimbo/faketouch-ui
[Frontend] Fix faketouch capturing touches on UI.
2023-10-31 09:28:19 +11:00
Matthew Cotton 339aa088cc
Catching case where we're sanitizing a streamer id that is numeric. Updating docs to remove PreferSFU (SFU is just selected as a streamer now). 2023-10-31 09:17:26 +11:00
Matthew Cotton 403fe39f4b
Updating the handling of generating new legacy streamer and sfu ids. 2023-10-30 16:52:26 +11:00
Matthew Cotton adfca6c42d
Cleanup and fixing sfu behaviour. 2023-10-27 15:15:41 +11:00
Matthew Cotton bbcfe8a6b5
Removing PreferSFU option since this is now handled with the stream selection option. Fixing browser behaviour when multiple streamers detected (previous failed tests). 2023-10-26 10:01:31 +11:00
Matthew Cotton 2ce53023ee
Just some small cleanup 2023-10-25 11:59:45 +11:00
Matthew Cotton 090cc89b08
Fixing the windows build script nuking the PATH env variable. 2023-10-25 11:50:33 +11:00
Matthew Cotton c0e715ca9d
Allowing SFU to work with multiple streamers. 2023-10-25 11:49:55 +11:00
Matthew Cotton 01d8056bee
working on handling multiple sfus gracefully 2023-10-24 15:01:34 +11:00
Matthew Cotton 127feac2e4
Better handling of streamer ids. Specifically legacy ids. 2023-10-24 11:42:30 +11:00
timbotimbo 1478eceb9b
Fix faketouch capturing touches on UI.
Signed-off-by: timbotimbo <timbotimbo@users.noreply.github.com>
2023-10-23 13:46:50 +02:00
mcottontensor d8007a3530
Merge pull request #409 from EpicGames/backport/UE5.4/pr-381
[UE5.4] Merge pull request #381 from New-Game-Plus/fix-video-autoplay
2023-10-23 15:35:18 +11:00
Bramford Horton 23eb2601e1 Fix/allow video autoplay without click
(cherry picked from commit 75cd975400)
2023-10-23 03:32:47 +00:00
mcottontensor 52f8a17e48
Merge pull request #381 from New-Game-Plus/fix-video-autoplay
Fix/allow video autoplay without click
2023-10-23 14:32:22 +11:00
mcottontensor ac6cafae85
Merge pull request #406 from EpicGames/backport/UE5.4/pr-403
[UE5.4] Merge pull request #403 from mcottontensor/mm_linux_fix
2023-10-23 14:13:21 +11:00
Matthew Cotton df2ef8ba04 Small fix to allow the matchmaker start scripts to find the custom install of node.
(cherry picked from commit c76284041e)
2023-10-23 03:04:14 +00:00
mcottontensor 8a6a5bbfc5
Merge pull request #403 from mcottontensor/mm_linux_fix
Allowing the Matchmaker to run on linux.
2023-10-23 14:03:47 +11:00
Matthew Cotton c76284041e
Small fix to allow the matchmaker start scripts to find the custom install of node. 2023-10-23 11:29:27 +11:00
William Belcher 2a21ee6566
Ensure that we have a non-null codecId when we try to update the preferred codec (#400) 2023-10-19 16:17:37 +10:00
William Belcher bf6dcade68
Update SignallingWebServer bash platform scripts to default to Linux (#399) 2023-10-19 14:55:10 +10:00
William Belcher ee82bd398c
Update SignallingWebServer platform scripts to support Mac (#389)
* Update SignallingWebServer platform scripts to support Mac x86_64 and Arm64

* Update bash scripts to default to Linux

* Update coturn URLs to use the binaries provided by the PixelStreamingInfrastructure
2023-10-19 14:40:52 +10:00
github-actions[bot] 952b309c71
Expose JSS InsertionPoint (#397)
Signed-off-by: timbotimbo <timbotimbo@users.noreply.github.com>
Co-authored-by: William Belcher <william.belcher@xa.epicgames.com>
(cherry picked from commit 8ba410154d)

Co-authored-by: timbotimbo <timbotimbo@users.noreply.github.com>
2023-10-19 10:48:19 +10:00
github-actions[bot] 16d80e27e1
Handle statsPanel or settingsPanel being undefined. (#394)
Signed-off-by: timbotimbo <timbotimbo@users.noreply.github.com>
(cherry picked from commit af5339bec8)

Co-authored-by: timbotimbo <timbotimbo@users.noreply.github.com>
2023-10-19 10:44:18 +10:00
github-actions[bot] 1301fde89a
Bump @babel/traverse from 7.21.3 to 7.23.2 in /Frontend/library (#388)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.3 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 81c3f52f84)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William Belcher <william.belcher@xa.epicgames.com>
2023-10-17 14:26:44 +10:00
github-actions[bot] ad85b02b5e
Bump postcss from 8.4.21 to 8.4.31 in /Frontend/implementations/react (#386)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 55b771e633)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William Belcher <william.belcher@xa.epicgames.com>
2023-10-17 14:24:19 +10:00
github-actions[bot] 828123c6c8
Bump postcss in /Frontend/implementations/typescript (#383)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1a749cea8a)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 14:10:42 +10:00
Bramford Horton 75cd975400 Fix/allow video autoplay without click 2023-10-10 14:42:38 +13:00
github-actions[bot] 3816f4b535
Remove unit conversion for bitrate from URL. URL is already in kbps (#369) (#372)
(cherry picked from commit eb9a665a0a)

Co-authored-by: William Belcher <william.belcher@xa.epicgames.com>
2023-09-14 10:47:07 +10:00
mcottontensor f6d724a3fe
Update RELEASE_VERSION
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2023-09-13 12:56:26 +10:00
mcottontensor cabf32a879
Touching ui-library to trigger build action.
Signed-off-by: mcottontensor <80377552+mcottontensor@users.noreply.github.com>
2023-09-13 12:55:25 +10:00
mcottontensor 24b8082f35
Merge pull request #368 from EpicGames/master
Updating workflows.
2023-09-13 12:53:41 +10:00
mcottontensor f7c4fd2f3c
Merge pull request #366 from EpicGames/master
Merging in master for some missed 5.3 references.
2023-09-13 12:49:01 +10:00
mcottontensor e6797144d4
Merge pull request #364 from EpicGames/master
Merging in master to generate npm packages.
2023-09-13 12:21:09 +10:00
77 changed files with 1425 additions and 933 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

@ -19,7 +19,6 @@ This page will be updated with new features and commands as they become availabl
| **Browser send offer** | The browser will start the WebRTC handshake instead of the Unreal Engine application. This is an advanced setting for users customising the frontend. Primarily for backwards compatibility for 4.x versions of the engine. |
| **Use microphone** | Will start receiving audio input from your microphone and transmit it to the Unreal Engine. |
| **Start video muted** | Muted audio when the stream starts. |
| **Prefer SFU** | Will attempt to use the Selective Forwarding Unit (SFU), if you have one running. |
| **Is quality controller?** | Makes the encoder of the Pixel Streaming Plugin use the current browser connection to determine the bandwidth available, and therefore the quality of the stream encoding. **See notes below** |
| **Force mono audio** | Force the browser to request mono audio in the SDP. |
| **Force TURN** | Will attempt to connect exclusively via the TURN server. Will not work without an active TURN server. |

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

@ -1,11 +1,11 @@
{
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3",
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3",
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4",
"version": "0.0.1",
"devDependencies": {
"css-loader": "^6.7.3",
@ -1390,8 +1390,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"dev": true,
"funding": [
{
@ -4898,8 +4899,9 @@
}
},
"follow-redirects": {
"version": "1.15.2",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"dev": true
},
"forwarded": {

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

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

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

@ -72,7 +72,7 @@ export class FakeTouchController implements ITouchController {
* @param touch - the activating touch event
*/
onTouchStart(touch: TouchEvent): void {
if (!this.videoElementProvider.isVideoReady()) {
if (!this.videoElementProvider.isVideoReady() || touch.target !== this.videoElementProvider.getVideoElement()) {
return;
}
if (this.fakeTouchFinger == null) {
@ -108,7 +108,7 @@ export class FakeTouchController implements ITouchController {
* @param touchEvent - the activating touch event
*/
onTouchEnd(touchEvent: TouchEvent): void {
if (!this.videoElementProvider.isVideoReady()) {
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
return;
}
const videoElementParent =
@ -144,7 +144,7 @@ export class FakeTouchController implements ITouchController {
* @param touchEvent - the activating touch event
*/
onTouchMove(touchEvent: TouchEvent): void {
if (!this.videoElementProvider.isVideoReady()) {
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
return;
}
const toStreamerHandlers =

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

@ -180,7 +180,7 @@ export class PeerConnectionController {
this.onVideoStats(this.aggregatedStats);
// Update the preferred codec selection based on what was actually negotiated
if (this.updateCodecSelection) {
if (this.updateCodecSelection && !!this.aggregatedStats.inboundVideoStats.codecId) {
this.config.setOptionSettingValue(
OptionParameters.PreferredCodec,
this.aggregatedStats.codecs.get(
@ -370,7 +370,7 @@ export class PeerConnectionController {
if (RTCRtpReceiver.getCapabilities && this.preferredCodec != '') {
for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
if (
transceiver &&
transceiver &&
transceiver.receiver &&
transceiver.receiver.track &&
transceiver.receiver.track.kind === 'video' &&

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

@ -18,6 +18,7 @@ export class StreamController {
constructor(videoElementProvider: VideoPlayer) {
this.videoElementProvider = videoElementProvider;
this.audioElement = document.createElement('Audio') as HTMLAudioElement;
this.videoElementProvider.setAudioElement(this.audioElement);
}
/**

View File

@ -18,6 +18,7 @@ declare global {
export class VideoPlayer {
private config: Config;
private videoElement: HTMLVideoElement;
private audioElement?: HTMLAudioElement;
private orientationChangeTimeout: number;
private lastTimeResized = new Date().getTime();
@ -52,8 +53,11 @@ export class VideoPlayer {
);
};
// set play for video
// set play for video (and audio)
this.videoElement.onclick = () => {
if (this.audioElement != undefined && this.audioElement.paused) {
this.audioElement.play();
}
if (this.videoElement.paused) {
this.videoElement.play();
}
@ -70,6 +74,10 @@ export class VideoPlayer {
);
}
public setAudioElement(audioElement: HTMLAudioElement) : void {
this.audioElement = audioElement;
}
/**
* Sets up the video element with any application config and plays the video element.
* @returns A promise for if playing the video was successful or not.

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;
}
@ -1109,26 +1093,30 @@ export class WebRtcPlayerController {
this.pixelStreaming.dispatchEvent(new PlayStreamEvent());
if (this.streamController.audioElement.srcObject) {
this.streamController.audioElement.muted =
this.config.isFlagEnabled(Flags.StartVideoMuted);
const startMuted = this.config.isFlagEnabled(Flags.StartVideoMuted)
this.streamController.audioElement.muted = startMuted;
this.streamController.audioElement
.play()
.then(() => {
this.playVideo();
})
.catch((onRejectedReason) => {
Logger.Log(Logger.GetStackTrace(), onRejectedReason);
Logger.Log(
Logger.GetStackTrace(),
'Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.'
);
this.pixelStreaming.dispatchEvent(
new PlayStreamRejectedEvent({
reason: onRejectedReason
})
);
});
if (startMuted) {
this.playVideo();
} else {
this.streamController.audioElement
.play()
.then(() => {
this.playVideo();
})
.catch((onRejectedReason) => {
Logger.Log(Logger.GetStackTrace(), onRejectedReason);
Logger.Log(
Logger.GetStackTrace(),
'Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.'
);
this.pixelStreaming.dispatchEvent(
new PlayStreamRejectedEvent({
reason: onRejectedReason
})
);
});
}
} else {
this.playVideo();
}
@ -1172,6 +1160,9 @@ export class WebRtcPlayerController {
* Connect to the Signaling server
*/
connectToSignallingServer() {
this.locallyClosed = false;
this.shouldReconnect = true;
this.disconnectMessage = null;
const signallingUrl = this.signallingUrlBuilder();
this.webSocketController.connect(signallingUrl);
}
@ -1194,10 +1185,7 @@ export class WebRtcPlayerController {
Logger.GetStackTrace(),
'No turn server was found in the Peer Connection Options. TURN cannot be forced, closing connection. Please use STUN instead'
);
this.setDisconnectMessageOverride(
'TURN cannot be forced, closing connection. Please use STUN instead.'
);
this.closeSignalingServer();
this.closeSignalingServer('TURN cannot be forced, closing connection. Please use STUN instead.');
return;
}
}
@ -1339,89 +1327,124 @@ export class WebRtcPlayerController {
6
);
if(this.isReconnecting) {
if(messageStreamerList.ids.includes(this.subscribedStream)) {
// If we're reconnecting and the previously subscribed stream has come back, resubscribe to it
this.isReconnecting = false;
this.reconnectAttempt = 0;
this.webSocketController.sendSubscribe(this.subscribedStream);
} else if(this.reconnectAttempt < this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts)) {
// Our previous stream hasn't come back, wait 2 seconds and request an updated stream list
this.reconnectAttempt++;
setTimeout(() => {
this.webSocketController.requestStreamerList();
}, 2000)
} else {
// We've exhausted our reconnect attempts, return to main screen
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.shouldReconnect = false;
this.webSocketController.close();
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
""
);
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
[]
);
}
} else {
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
settingOptions
);
let wantedStreamerId: string = null;
const urlParams = new URLSearchParams(window.location.search);
let autoSelectedStreamerId: string | null = null;
if (messageStreamerList.ids.length == 1) {
// If there's only a single streamer, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = messageStreamerList.ids[0];
} else if (
this.config.isFlagEnabled(Flags.PreferSFU) &&
messageStreamerList.ids.includes('SFU')
) {
// If the SFU toggle is on and there's an SFU connected, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = 'SFU';
} else if (
urlParams.has(OptionParameters.StreamerId) &&
messageStreamerList.ids.includes(
urlParams.get(OptionParameters.StreamerId)
)
) {
// If there's a streamer ID in the URL and a streamer with this ID is connected, set it as the selected streamer
autoSelectedStreamerId = urlParams.get(OptionParameters.StreamerId);
}
if (autoSelectedStreamerId !== null) {
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
autoSelectedStreamerId
);
} else {
// no auto selected streamer
if (this.config.isFlagEnabled(Flags.WaitForStreamer)) {
this.startAutoJoinTimer()
// get the current selected streamer id option
var streamerIDOption = this.config.getSettingOption(OptionParameters.StreamerId);
const existingSelection = streamerIDOption.selected.toString().trim();
if (!!existingSelection) {
// default to selected option if it exists
wantedStreamerId = streamerIDOption.selected;
}
// add the streamers to the UI
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
settingOptions
);
let autoSelectedStreamerId: string = null;
const waitForStreamer = this.config.isFlagEnabled(Flags.WaitForStreamer);
const reconnectLimit = this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts);
const reconnectDelay = this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval);
// first we figure out a wanted streamer id through various means
const useUrlParams = this.config.useUrlParams;
const urlParams = new URLSearchParams(window.location.search);
if (useUrlParams && urlParams.has(OptionParameters.StreamerId)) {
// if we've set the streamer id on the url we only want that streamer id
wantedStreamerId = urlParams.get(OptionParameters.StreamerId);
} else if (this.subscribedStream) {
// we were previously subscribed to a streamer, we want that
wantedStreamerId = this.subscribedStream;
}
// now lets see if we can pick it.
if (wantedStreamerId && messageStreamerList.ids.includes(wantedStreamerId)) {
// if the wanted stream is in the list. we pick that
autoSelectedStreamerId = wantedStreamerId;
} else if ((!wantedStreamerId || !waitForStreamer) && messageStreamerList.ids.length == 1) {
// otherwise, if we're not waiting for the wanted streamer and there's only one streamer, connect to it
autoSelectedStreamerId = messageStreamerList.ids[0];
}
// if we found a streamer id to auto select, select it
if (autoSelectedStreamerId) {
this.isReconnecting = false;
this.reconnectAttempt = 0;
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
autoSelectedStreamerId
);
} else {
// no auto selected streamer.
// if we're waiting for a streamer then try reconnecting
if (waitForStreamer) {
if (this.reconnectAttempt < reconnectLimit) {
// still reconnects available
this.isReconnecting = true;
this.reconnectAttempt++;
setTimeout(() => {
this.webSocketController.requestStreamerList();
}, reconnectDelay);
} else {
// We've exhausted our reconnect attempts, return to main screen
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.shouldReconnect = false;
}
}
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId
})
);
}
// dispatch this event finally
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId,
wantedStreamerId
})
);
}
startAutoJoinTimer() {
clearTimeout(this.autoJoinTimer);
this.autoJoinTimer = setTimeout(() => this.tryAutoJoin(), this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval));
}
handleStreamerIDChangedMessage(streamerIDChangedMessage: MessageStreamerIDChanged) {
const newID = streamerIDChangedMessage.newID;
tryAutoJoin() {
this.connectToSignallingServer();
// need to edit the selected streamer in the settings list
var streamerListOptions = this.config.getSettingOption(OptionParameters.StreamerId);
// temporarily prevent onChange from firing (it would try to subscribe to the streamer again)
var oldOnChange = streamerListOptions.onChange;
streamerListOptions.onChange = ()=>{};
// change the selected entry.
var streamerList = streamerListOptions.options;
for (var i = 0; i < streamerList.length; ++i) {
if (streamerList[i] == this.subscribedStream) {
streamerList[i] = newID;
break;
}
}
// update the list
streamerListOptions.options = streamerList;
// update the selected entry
streamerListOptions.selected = newID;
// restore the old change notifier.
streamerListOptions.onChange = oldOnChange;
// remember which stream we're subscribe to
this.subscribedStream = streamerIDChangedMessage.newID;
// notify any listeners
this.pixelStreaming.dispatchEvent(
new StreamerIDChangedMessageEvent({
newID
})
);
}
/**
@ -1583,7 +1606,13 @@ export class WebRtcPlayerController {
'Sending the offer to the Server',
6
);
this.webSocketController.sendWebRtcOffer(offer);
const extraParams: ExtraOfferParameters = {
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
};
this.webSocketController.sendWebRtcOffer(offer, extraParams);
}
/**
@ -1596,7 +1625,13 @@ export class WebRtcPlayerController {
'Sending the answer to the Server',
6
);
this.webSocketController.sendWebRtcAnswer(answer);
const extraParams: ExtraAnswerParameters = {
minBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMinBitrate),
maxBitrateBps: 1000 * this.config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
};
this.webSocketController.sendWebRtcAnswer(answer, extraParams);
if (this.isUsingSFU) {
this.webSocketController.sendWebRtcDatachannelRequest();
@ -1618,9 +1653,11 @@ export class WebRtcPlayerController {
/**
* Close the Connection to the signaling server
*/
closeSignalingServer() {
closeSignalingServer(message: string) {
// We explicitly called close, therefore we don't want to trigger auto reconnect
this.locallyClosed = true;
this.shouldReconnect = false;
this.disconnectMessage = message;
this.webSocketController?.close();
}
@ -1635,7 +1672,7 @@ export class WebRtcPlayerController {
* Close all connections
*/
close() {
this.closeSignalingServer();
this.closeSignalingServer('');
this.closePeerConnection();
}
@ -2019,20 +2056,6 @@ export class WebRtcPlayerController {
this.videoPlayer.resizePlayerStyle();
}
/**
* Get the overridden disconnect message
*/
getDisconnectMessageOverride(): string {
return this.disconnectMessageOverride;
}
/**
* Set the override for the disconnect message
*/
setDisconnectMessageOverride(message: string): void {
this.disconnectMessageOverride = message;
}
setPreferredCodec(codec: string) {
this.preferredCodec = codec;
if (this.peerConnectionController) {

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

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

@ -174,10 +174,6 @@ export class ConfigUI {
psSettingsSection,
this.flagsUi.get(Flags.StartVideoMuted)
);
this.addSettingFlag(
psSettingsSection,
this.flagsUi.get(Flags.PreferSFU)
);
this.addSettingFlag(
psSettingsSection,
this.flagsUi.get(Flags.IsQualityController)

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,7 +1,7 @@
Copyright 2004-2022, Epic Games, Inc.
Copyright 2004-2024, Epic Games, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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
@ -18,6 +18,7 @@ echo "Starting Matchmaker use ctrl-c to exit"
echo "-----------------------------------------"
echo ""
PATH="${BASH_LOCATION}/node/bin:$PATH"
start_process $process
popd > /dev/null # ../..

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

View File

@ -11,8 +11,18 @@ for(let arg of process.argv){
}
const config = {
// The URL of the signalling server to connect to
signallingURL: "ws://localhost:8889",
// The ID for this SFU to use. This will show up as a streamer ID on the signalling server
SFUId: "SFU",
// The ID of the streamer to subscribe to. If you leave this blank it will subscribe to the first streamer it sees.
subscribeStreamerId: "DefaultStreamer",
// Delay between list requests when looking for a specifc streamer.
retrySubscribeDelaySecs: 10,
mediasoup: {
worker: {
rtcMinPort: 40000,

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,11 @@ const config = require('./config');
const WebSocket = require('ws');
const mediasoup = require('mediasoup_prebuilt');
const mediasoupSdp = require('mediasoup-sdp-bridge');
const minimist = require('minimist');
if (!config.retrySubscribeDelaySecs) {
config.retrySubscribeDelaySecs = 10;
}
let signalServer = null;
let mediasoupRouter;
@ -24,6 +29,35 @@ function connectSignalling(server) {
});
}
async function onStreamerList(msg) {
let success = false;
// subscribe to either the configured streamer, or if not configured, just grab the first id
if (msg.ids.length > 0) {
if (!!config.subscribeStreamerId && config.subscribeStreamerId.length != 0) {
if (msg.ids.includes(config.subscribeStreamerId)) {
signalServer.send(JSON.stringify({type: 'subscribe', streamerId: config.subscribeStreamerId}));
success = true;
}
} else {
signalServer.send(JSON.stringify({type: 'subscribe', streamerId: msg.ids[0]}));
success = true;
}
}
if (!success) {
// did not subscribe to anything
setTimeout(function() {
signalServer.send(JSON.stringify({type: 'listStreamers'}));
}, config.retrySubscribeDelaySecs * 1000);
}
}
async function onIdentify(msg) {
signalServer.send(JSON.stringify({type: 'endpointId', id: config.SFUId}));
signalServer.send(JSON.stringify({type: 'listStreamers'}));
}
async function onStreamerOffer(sdp) {
console.log("Got offer from streamer");
@ -57,6 +91,11 @@ function onStreamerDisconnected() {
}
streamer.transport.close();
streamer = null;
signalServer.send(JSON.stringify({type: 'stopStreaming'}));
setTimeout(function() {
signalServer.send(JSON.stringify({type: 'listStreamers'}));
}, config.retrySubscribeDelaySecs * 1000);
}
}
@ -228,7 +267,7 @@ function onLayerPreference(msg) {
}
async function onSignallingMessage(message) {
//console.log(`Got MSG: ${message}`);
//console.log(`Got MSG: ${message}`);
const msg = JSON.parse(message);
if (msg.type == 'offer') {
@ -255,6 +294,12 @@ async function onSignallingMessage(message) {
else if (msg.type == 'layerPreference') {
onLayerPreference(msg);
}
else if (msg.type == 'streamerList') {
onStreamerList(msg);
}
else if (msg.type == 'identify') {
onIdentify(msg);
}
}
async function startMediasoup() {
@ -276,6 +321,14 @@ async function startMediasoup() {
return mediasoupRouter;
}
async function onICEStateChange(identifier, iceState) {
console.log("%s ICE state changed to %s", identifier, iceState);
if (identifier == 'Streamer' && iceState == 'completed') {
signalServer.send(JSON.stringify({type: 'startStreaming'}));
}
}
async function createWebRtcTransport(identifier) {
const {
listenIps,
@ -291,7 +344,7 @@ async function createWebRtcTransport(identifier) {
initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate
});
transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); });
transport.on("icestatechange", (iceState) => onICEStateChange(identifier, iceState));
transport.on("iceselectedtuplechange", (iceTuple) => { console.log("%s ICE selected tuple %s", identifier, JSON.stringify(iceTuple)); });
transport.on("sctpstatechange", (sctpState) => { console.log("%s SCTP state changed to %s", identifier, sctpState); });
@ -299,6 +352,12 @@ async function createWebRtcTransport(identifier) {
}
async function main() {
var argv = minimist(process.argv.slice(2));
if ('signallingURL' in argv) {
config.signallingURL = argv['signallingURL'];
}
console.log('Starting Mediasoup...');
console.log("Config = ");
console.log(config);

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 ];
@ -288,14 +250,83 @@ console.logColor(logging.Cyan, `Running Cirrus - The Pixel Streaming reference i
let nextPlayerId = 1;
const StreamerType = { Regular: 0, SFU: 1 };
class Streamer {
constructor(initialId, ws, type) {
this.id = initialId;
this.ws = ws;
this.type = type;
this.idCommitted = false;
}
// registers this streamers id
commitId(id) {
this.id = id;
this.idCommitted = true;
}
// returns true if we have a valid id
isIdCommitted() {
return this.idCommitted;
}
// links this streamer to a subscribed SFU player (player component of an SFU)
addSFUPlayer(sfuPlayerId) {
if (!!this.SFUPlayerId && this.SFUPlayerId != sfuPlayerId) {
console.error(`Streamer ${this.id} already has an SFU ${this.SFUPlayerId}. Trying to add ${sfuPlayerId} as SFU.`);
return;
}
this.SFUPlayerId = sfuPlayerId;
}
// removes the previously subscribed SFU player
removeSFUPlayer() {
delete this.SFUPlayerId;
}
// gets the player id of the subscribed SFU if any
getSFUPlayerId() {
return this.SFUPlayerId;
}
// returns true if this streamer is forwarding another streamer
isSFU() {
return this.type == StreamerType.SFU;
}
// links this streamer to a player, used for SFU connections since they have both components
setSFUPlayerComponent(playerComponent) {
if (!this.isSFU()) {
console.error(`Trying to add an SFU player component ${playerComponent.id} to streamer ${this.id} but it is not an SFU type.`);
return;
}
this.sfuPlayerComponent = playerComponent;
}
// gets the player component for this sfu
getSFUPlayerComponent() {
if (!this.isSFU()) {
console.error(`Trying to get an SFU player component from streamer ${this.id} but it is not an SFU type.`);
return null;
}
return this.sfuPlayerComponent;
}
}
const PlayerType = { Regular: 0, SFU: 1 };
const WhoSendsOffer = { Streamer: 0, Browser: 1 };
class Player {
constructor(id, ws, type, browserSendOffer) {
constructor(id, ws, type, whoSendsOffer) {
this.id = id;
this.ws = ws;
this.type = type;
this.browserSendOffer = browserSendOffer;
this.whoSendsOffer = whoSendsOffer;
}
isSFU() {
return this.type == PlayerType.SFU;
}
subscribe(streamerId) {
@ -304,13 +335,25 @@ class Player {
return;
}
this.streamerId = streamerId;
const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: !this.browserSendOffer };
if (this.type == PlayerType.SFU) {
let streamer = streamers.get(this.streamerId);
streamer.addSFUPlayer(this.id);
}
const msg = { type: 'playerConnected', playerId: this.id, dataChannel: true, sfu: this.type == PlayerType.SFU, sendOffer: this.whoSendsOffer == WhoSendsOffer.Streamer };
logOutgoing(this.streamerId, msg);
this.sendFrom(msg);
}
unsubscribe() {
if (this.streamerId && streamers.has(this.streamerId)) {
if (this.type == PlayerType.SFU) {
let streamer = streamers.get(this.streamerId);
if (streamer.getSFUPlayerId() != this.id) {
console.error(`Trying to unsibscribe SFU player ${this.id} from streamer ${streamer.id} but the current SFUId does not match (${streamer.getSFUPlayerId()}).`)
} else {
streamer.removeSFUPlayer();
}
}
const msg = { type: 'playerDisconnected', playerId: this.id };
logOutgoing(this.streamerId, msg);
this.sendFrom(msg);
@ -348,20 +391,41 @@ class Player {
const msgString = JSON.stringify(message);
this.ws.send(msgString);
}
setSFUStreamerComponent(streamerComponent) {
if (!this.isSFU()) {
console.error(`Trying to add an SFU streamer component ${streamerComponent.id} to player ${this.id} but it is not an SFU type.`);
return;
}
this.sfuStreamerComponent = streamerComponent;
}
getSFUStreamerComponent() {
if (!this.isSFU()) {
console.error(`Trying to get an SFU streamer component from player ${this.id} but it is not an SFU type.`);
return null;
}
return this.sfuStreamerComponent;
}
};
let streamers = new Map(); // streamerId <-> streamer socket
let players = new Map(); // playerId <-> player, where player is either a web-browser or a native webrtc player
const SFUPlayerId = "SFU";
const LegacyStreamerId = "__LEGACY__"; // old streamers that dont know how to ID will be assigned this id.
let streamers = new Map(); // streamerId <-> streamer
let players = new Map(); // playerId <-> player/peer/viewer
const LegacyStreamerPrefix = "__LEGACY_STREAMER__"; // old streamers that dont know how to ID will be assigned this id prefix.
const LegacySFUPrefix = "__LEGACY_SFU__"; // same as streamer version but for SFUs
const streamerIdTimeoutSecs = 5;
function sfuIsConnected() {
const sfuPlayer = players.get(SFUPlayerId);
return sfuPlayer && sfuPlayer.ws && sfuPlayer.ws.readyState == 1;
}
function getSFU() {
return players.get(SFUPlayerId);
// gets the SFU subscribed to this streamer if any.
function getSFUForStreamer(streamerId) {
if (!streamers.has(streamerId)) {
return null;
}
const streamer = streamers.get(streamerId);
const sfuPlayerId = streamer.getSFUPlayerId();
if (!sfuPlayerId) {
return null;
}
return players.get(sfuPlayerId);
}
function logIncoming(sourceName, msg) {
@ -401,30 +465,109 @@ function getPlayerIdFromMessage(msg) {
return sanitizePlayerId(msg.playerId);
}
let uniqueLegacyStreamerPostfix = 0;
function getUniqueLegacyStreamerId() {
const finalId = LegacyStreamerPrefix + uniqueLegacyStreamerPostfix;
++uniqueLegacyStreamerPostfix;
return finalId;
}
let uniqueLegacySFUPostfix = 0;
function getUniqueLegacySFUId() {
const finalId = LegacySFUPrefix + uniqueLegacySFUPostfix;
++uniqueLegacySFUPostfix;
return finalId;
}
function requestStreamerId(streamer) {
// first we ask the streamer to id itself.
// if it doesnt reply within a time limit we assume it's an older streamer
// and assign it an id.
// request id
const msg = { type: "identify" };
logOutgoing(streamer.id, msg);
streamer.ws.send(JSON.stringify(msg));
streamer.idTimer = setTimeout(function() {
// streamer did not respond in time. give it a legacy id.
const newLegacyId = getUniqueLegacyStreamerId();
if (newLegacyId.length == 0) {
const error = `Ran out of legacy ids.`;
console.error(error);
streamer.ws.close(1008, error);
} else {
registerStreamer(newLegacyId, streamer);
}
}, streamerIdTimeoutSecs * 1000);
}
function sanitizeStreamerId(id) {
let maxPostfix = -1;
for (let [streamerId, streamer] of streamers) {
const idMatchRegex = /^(.*?)(\d*)$/;
const [, baseId, postfix] = streamerId.match(idMatchRegex);
// if the id is numeric then base id will be empty and we need to compare with the postfix
if ((baseId != '' && baseId != id) || (baseId == '' && postfix != id)) {
continue;
}
const numPostfix = Number(postfix);
if (numPostfix > maxPostfix) {
maxPostfix = numPostfix
}
}
if (maxPostfix >= 0) {
return id + (maxPostfix + 1);
}
return id;
}
function registerStreamer(id, streamer) {
streamer.id = id;
streamers.set(streamer.id, streamer);
// remove any existing streamer id
if (!!streamer.id) {
// notify any connected peers of rename
const renameMessage = { type: "streamerIDChanged", newID: id };
let clone = new Map(players);
for (let player of clone.values()) {
if (player.streamerId == streamer.id) {
logOutgoing(player.id, renameMessage);
player.sendTo(renameMessage);
player.streamerId = id; // reassign the subscription
}
}
streamers.delete(streamer.id);
}
// make sure the id is unique
const uniqueId = sanitizeStreamerId(id);
streamer.commitId(uniqueId);
if (!!streamer.idTimer) {
clearTimeout(streamer.idTimer);
delete streamer.idTimer;
}
streamers.set(uniqueId, streamer);
console.logColor(logging.Green, `Registered new streamer: ${streamer.id}`);
}
function onStreamerDisconnected(streamer) {
if (!streamer.id) {
if (!!streamer.idTimer) {
clearTimeout(streamer.idTimer);
}
if (!streamer.id || !streamers.has(streamer.id)) {
return;
}
if (!streamers.has(streamer.id)) {
console.error(`Disconnecting streamer ${streamer.id} does not exist.`);
} else {
sendStreamerDisconnectedToMatchmaker();
let sfuPlayer = getSFU();
if (sfuPlayer) {
const msg = { type: "streamerDisconnected" };
logOutgoing(sfuPlayer.id, msg);
sfuPlayer.sendTo(msg);
disconnectAllPlayers(sfuPlayer.id);
}
disconnectAllPlayers(streamer.id);
streamers.delete(streamer.id);
sendStreamerDisconnectedToMatchmaker();
let sfuPlayer = getSFUForStreamer(streamer.id);
if (sfuPlayer) {
const msg = { type: "streamerDisconnected" };
logOutgoing(sfuPlayer.id, msg);
sfuPlayer.sendTo(msg);
disconnectAllPlayers(sfuPlayer.id);
}
disconnectAllPlayers(streamer.id);
streamers.delete(streamer.id);
}
function onStreamerMessageId(streamer, msg) {
@ -432,15 +575,6 @@ function onStreamerMessageId(streamer, msg) {
let streamerId = msg.id;
registerStreamer(streamerId, streamer);
// subscribe any sfu to the latest connected streamer
const sfuPlayer = getSFU();
if (sfuPlayer) {
sfuPlayer.subscribe(streamer.id);
}
// if any streamer id's assume the legacy streamer is not needed.
streamers.delete(LegacyStreamerId);
}
function onStreamerMessagePing(streamer, msg) {
@ -461,7 +595,7 @@ function onStreamerMessageDisconnectPlayer(streamer, msg) {
}
function onStreamerMessageLayerPreference(streamer, msg) {
let sfuPlayer = getSFU();
let sfuPlayer = getSFUForStreamer(streamer.id);
if (sfuPlayer) {
logOutgoing(sfuPlayer.id, msg);
sfuPlayer.sendTo(msg);
@ -495,7 +629,8 @@ streamerServer.on('connection', function (ws, req) {
console.logColor(logging.Green, `Streamer connected: ${req.connection.remoteAddress}`);
sendStreamerConnectedToMatchmaker();
let streamer = { ws: ws };
const temporaryId = req.connection.remoteAddress;
let streamer = new Streamer(temporaryId, ws, StreamerType.Regular);
ws.on('message', (msgRaw) => {
var msg;
@ -528,76 +663,134 @@ streamerServer.on('connection', function (ws, req) {
console.error(`streamer ${streamer.id} connection error: ${error}`);
onStreamerDisconnected(streamer);
try {
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `streamer ${streamer.id} connection error: ${error}`);
} catch(err) {
console.error(`ERROR: ws.on error: ${err.message}`);
}
});
ws.send(JSON.stringify(clientConfig));
const configStr = JSON.stringify(clientConfig);
logOutgoing(streamer.id, configStr)
ws.send(configStr);
// request id
const msg = { type: "identify" };
logOutgoing("unknown", msg);
ws.send(JSON.stringify(msg));
registerStreamer(LegacyStreamerId, streamer);
requestStreamerId(streamer);
});
function forwardSFUMessageToPlayer(msg) {
function forwardSFUMessageToPlayer(sfuPlayer, msg) {
const playerId = getPlayerIdFromMessage(msg);
const player = players.get(playerId);
if (player) {
logForward(SFUPlayerId, playerId, msg);
logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg);
player.sendTo(msg);
}
}
function forwardSFUMessageToStreamer(msg) {
const sfuPlayer = getSFU();
if (sfuPlayer) {
logForward(SFUPlayerId, sfuPlayer.streamerId, msg);
msg.sfuId = SFUPlayerId;
sfuPlayer.sendFrom(msg);
}
function forwardSFUMessageToStreamer(sfuPlayer, msg) {
logForward(sfuPlayer.getSFUStreamerComponent().id, sfuPlayer.streamerId, msg);
msg.sfuId = sfuPlayer.id;
sfuPlayer.sendFrom(msg);
}
function onPeerDataChannelsSFUMessage(msg) {
function onPeerDataChannelsSFUMessage(sfuPlayer, msg) {
// sfu is telling a peer what stream id to use for a data channel
const playerId = getPlayerIdFromMessage(msg);
const player = players.get(playerId);
if (player) {
logForward(SFUPlayerId, playerId, msg);
logForward(sfuPlayer.getSFUStreamerComponent().id, playerId, msg);
player.sendTo(msg);
player.datachannel = true;
}
}
function onSFUDisconnected() {
console.log("disconnecting SFU from streamer");
disconnectAllPlayers(SFUPlayerId);
const sfuPlayer = getSFU();
if (sfuPlayer) {
sfuPlayer.unsubscribe();
sfuPlayer.ws.close(4000, "SFU Disconnected");
}
players.delete(SFUPlayerId);
streamers.delete(SFUPlayerId);
// basically a duplicate of the streamer id request but this one does not register the streamer
function requestSFUStreamerId(sfuPlayer) {
// request id
const msg = { type: "identify" };
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
logOutgoing(sfuStreamerComponent.id, msg);
sfuStreamerComponent.ws.send(JSON.stringify(msg));
sfuStreamerComponent.idTimer = setTimeout(function() {
// streamer did not respond in time. give it a legacy id.
const newLegacyId = getUniqueSFUId();
if (newLegacyId.length == 0) {
const error = `Ran out of legacy ids.`;
console.error(error);
sfuPlayer.ws.close(1008, error);
} else {
sfuStreamerComponent.id = newLegacyId;
}
}, streamerIdTimeoutSecs * 1000);
}
function onSFUMessageId(sfuPlayer, msg) {
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
logIncoming(sfuStreamerComponent.id, msg);
sfuStreamerComponent.id = msg.id;
if (!!sfuStreamerComponent.idTimer) {
clearTimeout(sfuStreamerComponent.idTimer);
delete sfuStreamerComponent.idTimer;
}
}
function onSFUMessageStartStreaming(sfuPlayer, msg) {
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
logIncoming(sfuStreamerComponent.id, msg);
if (streamers.has(sfuStreamerComponent.id)) {
console.error(`SFU ${sfuStreamerComponent.id} is already registered as a streamer and streaming.`)
return;
}
registerStreamer(sfuStreamerComponent.id, sfuStreamerComponent);
}
function onSFUMessageStopStreaming(sfuPlayer, msg) {
const sfuStreamerComponent = sfuPlayer.getSFUStreamerComponent();
logIncoming(sfuStreamerComponent.id, msg);
if (!streamers.has(sfuStreamerComponent.id)) {
console.error(`SFU ${sfuStreamerComponent.id} is not registered as a streamer or streaming.`)
return;
}
onStreamerDisconnected(sfuStreamerComponent);
}
function onSFUDisconnected(sfuPlayer) {
console.log("disconnecting SFU from streamer");
disconnectAllPlayers(sfuPlayer.id);
onStreamerDisconnected(sfuPlayer.getSFUStreamerComponent());
sfuPlayer.unsubscribe();
sfuPlayer.ws.close(4000, "SFU Disconnected");
players.delete(sfuPlayer.id);
streamers.delete(sfuPlayer.id);
}
sfuMessageHandlers.set('listStreamers', onPlayerMessageListStreamers);
sfuMessageHandlers.set('subscribe', onPlayerMessageSubscribe);
sfuMessageHandlers.set('unsubscribe', onPlayerMessageUnsubscribe);
sfuMessageHandlers.set('offer', forwardSFUMessageToPlayer);
sfuMessageHandlers.set('answer', forwardSFUMessageToStreamer);
sfuMessageHandlers.set('streamerDataChannels', forwardSFUMessageToStreamer);
sfuMessageHandlers.set('peerDataChannels', onPeerDataChannelsSFUMessage);
sfuMessageHandlers.set('endpointId', onSFUMessageId);
sfuMessageHandlers.set('startStreaming', onSFUMessageStartStreaming);
sfuMessageHandlers.set('stopStreaming', onSFUMessageStopStreaming);
console.logColor(logging.Green, `WebSocket listening for SFU connections on :${sfuPort}`);
let sfuServer = new WebSocket.Server({ port: sfuPort });
sfuServer.on('connection', function (ws, req) {
// reject if we already have an sfu
if (sfuIsConnected()) {
ws.close(1013, 'Already have an SFU');
return;
}
let playerId = sanitizePlayerId(nextPlayerId++);
console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `);
let streamerComponent = new Streamer(req.connection.remoteAddress, ws, StreamerType.SFU);
let playerComponent = new Player(playerId, ws, PlayerType.SFU, WhoSendsOffer.Streamer);
streamerComponent.setSFUPlayerComponent(playerComponent);
playerComponent.setSFUStreamerComponent(streamerComponent);
players.set(playerId, playerComponent);
ws.on('message', (msgRaw) => {
var msg;
@ -609,45 +802,41 @@ sfuServer.on('connection', function (ws, req) {
return;
}
let sfuPlayer = players.get(playerId);
if (!sfuPlayer) {
console.error(`Received a message from an SFU not in the player list ${playerId}`);
ws.close(1001, 'Broken');
return;
}
let handler = sfuMessageHandlers.get(msg.type);
if (!handler || (typeof handler != 'function')) {
if (config.LogVerbose) {
console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", SFUPlayerId, msgRaw);
console.logColor(logging.White, "\x1b[37m-> %s\x1b[34m: %s", sfuPlayer.id, msgRaw);
}
console.error(`unsupported SFU message type: ${msg.type}`);
ws.close(1008, 'Unsupported message type');
return;
}
handler(msg);
handler(sfuPlayer, msg);
});
ws.on('close', function(code, reason) {
console.error(`SFU disconnected: ${code} - ${reason}`);
onSFUDisconnected();
onSFUDisconnected(playerComponent);
});
ws.on('error', function(error) {
console.error(`SFU connection error: ${error}`);
onSFUDisconnected();
onSFUDisconnected(playerComponent);
try {
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `SFU connection error: ${error}`);
} catch(err) {
console.error(`ERROR: ws.on error: ${err.message}`);
}
});
let sfuPlayer = new Player(SFUPlayerId, ws, PlayerType.SFU, false);
players.set(SFUPlayerId, sfuPlayer);
console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `);
// TODO subscribe it to one of any of the streamers for now
for (let [streamerId, streamer] of streamers) {
sfuPlayer.subscribe(streamerId);
break;
}
// sfu also acts as a streamer
registerStreamer(SFUPlayerId, { ws: ws });
requestStreamerId(playerComponent.getSFUStreamerComponent());
});
let playerCount = 0;
@ -718,7 +907,7 @@ playerServer.on('connection', function (ws, req) {
var url = require('url');
const parsedUrl = url.parse(req.url);
const urlParams = new URLSearchParams(parsedUrl.search);
const browserSendOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false';
const whoSendsOffer = urlParams.has('OfferToReceive') && urlParams.get('OfferToReceive') !== 'false' ? WhoSendsOffer.Browser : WhoSendsOffer.Streamer;
if (playerCount + 1 > maxPlayerCount && maxPlayerCount !== -1)
{
@ -730,7 +919,7 @@ playerServer.on('connection', function (ws, req) {
++playerCount;
let playerId = sanitizePlayerId(nextPlayerId++);
console.logColor(logging.Green, `player ${playerId} (${req.connection.remoteAddress}) connected`);
let player = new Player(playerId, ws, PlayerType.Regular, browserSendOffer);
let player = new Player(playerId, ws, PlayerType.Regular, whoSendsOffer);
players.set(playerId, player);
ws.on('message', (msgRaw) =>{
@ -769,7 +958,7 @@ playerServer.on('connection', function (ws, req) {
ws.on('error', function(error) {
console.error(`player ${playerId} connection error: ${error}`);
ws.close(1006 /* abnormal closure */, error);
ws.close(1006 /* abnormal closure */, `player ${playerId} connection error: ${error}`);
onPlayerDisconnected(playerId);
console.logColor(logging.Red, `Trying to reconnect...`);
@ -778,7 +967,11 @@ playerServer.on('connection', function (ws, req) {
sendPlayerConnectedToFrontend();
sendPlayerConnectedToMatchmaker();
player.ws.send(JSON.stringify(clientConfig));
const configStr = JSON.stringify(clientConfig);
logOutgoing(player.id, configStr)
player.ws.send(configStr);
sendPlayersCount();
});
@ -786,11 +979,11 @@ function disconnectAllPlayers(streamerId) {
console.log(`unsubscribing all players on ${streamerId}`);
let clone = new Map(players);
for (let player of clone.values()) {
if (player.streamerId == streamerId) {
if (player.streamerId == streamerId) {
// disconnect players but just unsubscribe the SFU
if (player.id == SFUPlayerId) {
// because we're working on a clone here we have to access directly
getSFU().unsubscribe();
const sfuPlayer = getSFUForStreamer(streamerId);
if (sfuPlayer && player.id == sfuPlayer.id) {
sfuPlayer.unsubscribe();
} else {
player.ws.close();
}

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
@ -28,11 +28,8 @@ realm="PixelStreaming"
process=""
if [ "$(uname)" == "Darwin" ]; then
process="${BASH_LOCATION}/coturn/bin/turnserver"
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
process="turnserver"
else
echo 'Incorrect host OS for use with Start_TURNServer.sh'
exit -1
process="turnserver"
fi
arguments="-c turnserver.conf --allowed-peer-ip=$localip -p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -u ${turnusername}:${turnpassword}"

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 --"
@ -33,4 +33,5 @@ start_process $process $arguments
popd > /dev/null # ../..
popd > /dev/null # BASH_SOURCE
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
@ -141,16 +141,13 @@ if [ "$(uname)" == "Darwin" ]; then
echo 'Incompatible architecture. Only x86_64 and ARM64 are supported'
exit -1
fi
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
node_url="https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz"
else
echo 'Incorrect OS for use with setup.sh'
exit -1
node_url="https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz"
fi
check_and_install "node" "$node_version" "$NODE_VERSION" "curl $node_url --output node.tar.xz
&& tar -xf node.tar.xz
&& rm node.tar.xz
&& mv node-v*-*-* \"${BASH_LOCATION}/node\""
&& tar -xf node.tar.xz
&& rm node.tar.xz
&& mv node-v*-*-* \"${BASH_LOCATION}/node\""
PATH="${BASH_LOCATION}/node/bin:$PATH"
"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install
@ -167,19 +164,19 @@ if [ "$(uname)" == "Darwin" ]; then
if [ -d "${BASH_LOCATION}/coturn" ]; then
echo 'CoTURN directory found...skipping install.'
else
echo 'CoTURN directory not found...beginning CoTURN download for Mac.'
echo 'CoTURN directory not found...beginning CoTURN download for Mac.'
coturn_url=""
if [[ $arch == x86_64* ]]; then
coturn_url="https://github.com/Belchy06/coturn/releases/download/v4.6.2-mac-x84_64/turnserver.zip"
coturn_url="https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.6.2-coturn-mac-x86_64/turnserver.zip"
elif [[ $arch == arm* ]]; then
coturn_url="https://github.com/Belchy06/coturn/releases/download/v4.6.2-mac-arm64/turnserver.zip"
coturn_url="https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.6.2-coturn-mac-arm64/turnserver.zip"
fi
curl -L -o ./turnserver.zip "$coturn_url"
mkdir "${BASH_LOCATION}/coturn"
tar -xf turnserver.zip -C "${BASH_LOCATION}/coturn"
rm turnserver.zip
fi
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
else
#command #dep_name #get_version_string #version_min #install command
coturn_version=$(if command -v turnserver &> /dev/null; then echo 1; else echo 0; fi)
if [ $coturn_version -eq 0 ]; then
@ -198,4 +195,3 @@ elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
fi
fi
fi

View File

@ -1,66 +0,0 @@
::
:: RefreshEnv.cmd
::
:: Batch file to read environment variables from registry and
:: set session variables to these values.
::
:: With this batch file, there should be no need to reload command
:: environment every time you want environment changes to propagate
::echo "RefreshEnv.cmd only works from cmd.exe, please install the Chocolatey Profile to take advantage of refreshenv from PowerShell"
echo | set /p dummy="Refreshing environment variables from registry for cmd.exe. Please wait..."
goto main
:: Set one environment variable from registry key
:SetFromReg
"%WinDir%\System32\Reg" QUERY "%~1" /v "%~2" > "%TEMP%\_envset.tmp" 2>NUL
for /f "usebackq skip=2 tokens=2,*" %%A IN ("%TEMP%\_envset.tmp") do (
echo/set "%~3=%%B"
)
goto :EOF
:: Get a list of environment variables from registry
:GetRegEnv
"%WinDir%\System32\Reg" QUERY "%~1" > "%TEMP%\_envget.tmp"
for /f "usebackq skip=2" %%A IN ("%TEMP%\_envget.tmp") do (
if /I not "%%~A"=="Path" (
call :SetFromReg "%~1" "%%~A" "%%~A"
)
)
goto :EOF
:main
echo/@echo off >"%TEMP%\_env.cmd"
:: Slowly generating final file
call :GetRegEnv "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" >> "%TEMP%\_env.cmd"
call :GetRegEnv "HKCU\Environment">>"%TEMP%\_env.cmd" >> "%TEMP%\_env.cmd"
:: Special handling for PATH - mix both User and System
call :SetFromReg "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" Path Path_HKLM >> "%TEMP%\_env.cmd"
call :SetFromReg "HKCU\Environment" Path Path_HKCU >> "%TEMP%\_env.cmd"
:: Caution: do not insert space-chars before >> redirection sign
echo/set "Path=%%Path_HKLM%%;%%Path_HKCU%%" >> "%TEMP%\_env.cmd"
:: Cleanup
del /f /q "%TEMP%\_envset.tmp" 2>nul
del /f /q "%TEMP%\_envget.tmp" 2>nul
:: capture user / architecture
SET "OriginalUserName=%USERNAME%"
SET "OriginalArchitecture=%PROCESSOR_ARCHITECTURE%"
:: Set these variables
call "%TEMP%\_env.cmd"
:: Cleanup
del /f /q "%TEMP%\_env.cmd" 2>nul
:: reset user / architecture
SET "USERNAME=%OriginalUserName%"
SET "PROCESSOR_ARCHITECTURE=%OriginalArchitecture%"
echo | set /p dummy="Finished."
echo ...

View File

@ -1,24 +0,0 @@
License
-------
Copyright (C) 1999-2008 - Jonathan Wilkes
http://www.xanya.net
Installing and using this software (or source code) signifies acceptance of these terms and the conditions of the license.
This license applies to everything in this package (Including any supplied Source Code), except where otherwise noted.
License Agreement
-----------------
This software is provided 'as-is', without any express or implied warranty.
In no event will the author be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software/source code.
(If you use the supplied source code (if any) in a product, then an acknowledgment in the product documentation would be appreciated but is not required.)
2. If you have downloaded the Source Code for this application (where available) then altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any distribution of the software.
(If you use the supplied source code (if any) in a product, including commercial applications, then you do NOT need to distribute this license with your product.)

View File

@ -1,46 +0,0 @@
SetEnv
Version 1.09 - ( For Windows 9x/NT/2000/XP/S2K3/Vista )
Copyright (C) 2005-2008 - Jonathan Wilkes - All Rights Reserved.
http://www.xanya.net
================================================================================
1. Installation
Simply download and run the Setup_SetEnv.exe application to install SetEnv.
2. Using SetEnv
The SetEnv is a free tool for setting/updating/deleting System Environment Variables.
Type the following at a command prompt (assumes SetEnv.exe is in current path), for command line usage information.
setenv -?
See our website for full usage details, http://www.xanya.net/site/utils/setenv.php
3. Version History
1.09 [Fix] - (Feb 9, 2008) - Fixed a problem on Windows 98 where it sometimes failed to open the Autoexec.bat file.
1.08 [New] - (May 31, 2007) - Added how to delete a USER environment variable to the usage information.
1.07 [Fix] - (Jan 25, 2007) - Fixed a bug found by depaolim.
1.06 [New] - (Jan 14, 2007) - Added dynamic expansion support (same as using ~ with setx)
- Originally requested by Andre Amaral, further Request by Synetech
1.05 [New] - (Sep 06, 2006) - Added support to prepend (rather than append) a value to an expanded string
- Requested by Masuia
1.04 [New] - (May 30, 2006) - Added support for User environment variables.
1.03 [Fix] - (Apr 20, 2006) - Bug fix in ProcessWinXP() discovered by attiasr
1.01 [Fix] - (Nov 15, 2005) - Bug fix in IsWinME() discovered by frankd
1.00 [New] - (Oct 29, 2005) - Initial Public Release.
4. License and Terms of Use
Please see the License.txt file for licensing information.
5. Reporting Problems
If you encounter any problems whilst using SetEnv, please try downloading the latest version from http://www.xanya.net to see if the problem has already been resolved.
If this does not help, then please send an e-mail to darka@xanya.net with details describing the problem.
================================================================================

View File

@ -12,7 +12,7 @@ if exist coturn\ (
echo CoTURN directory not found...beginning CoTURN download for Windows.
@Rem Download nodejs and follow redirects.
curl -L -o ./turnserver.zip "https://github.com/mcottontensor/coturn/releases/download/v4.5.2-windows/turnserver.zip"
curl -L -o ./turnserver.zip "https://github.com/EpicGames/PixelStreamingInfrastructure/releases/download/v4.5.2-coturn-windows/turnserver.zip"
@Rem Unarchive the .zip to a directory called "turnserver"
mkdir coturn & tar -xf turnserver.zip -C coturn

View File

@ -43,10 +43,10 @@
@Rem Save our current directory (the NodeJS dir) in a variable
set "NodeDir=%CD%\SignallingWebServer\platform_scripts\cmd\node"
@Rem Prepend NodeDir to PATH temporarily using a custom tool called SetEnv
call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -uap PATH %%%%"%NodeDir%"
@Rem Refresh the cmd session with new PATH
call %~dp0\refreshenv.cmd
@rem Save the old path variable
set OLDPATH=%PATH%
@Rem Prepend NodeDir to PATH temporarily
set PATH=%PATH%;%NodeDir%
@Rem Do npm install in the Frontend\lib directory (note we use start because that loads PATH)
echo ----------------------------
@ -73,7 +73,7 @@
echo End of build reference frontend step.
echo ----------------------------
@Rem Remove our NodeJS from the PATH
call SignallingWebServer\platform_scripts\cmd\setenv\SetEnv.exe -ud PATH %%%%"%NodeDir%"
@Rem Restore path
set PATH=%OLDPATH%
goto :eof