Compare commits
319 Commits
UE5.3-0.0.
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
cb741add4f | |
|
|
88d82e1112 | |
|
|
caddb00ed3 | |
|
|
f0fd72768c | |
|
|
7cbe5227dd | |
|
|
672397e6a8 | |
|
|
fa75e9d8c5 | |
|
|
4969548535 | |
|
|
e91781b6fa | |
|
|
eea0faff60 | |
|
|
664d40b801 | |
|
|
5cf2735c5f | |
|
|
2544b717ae | |
|
|
b2cdcf10f3 | |
|
|
e44a9ff734 | |
|
|
8734e5bb56 | |
|
|
f272cefb93 | |
|
|
d61b989bff | |
|
|
4aded6b752 | |
|
|
dee225c03a | |
|
|
ce2a493114 | |
|
|
44367dfa8a | |
|
|
23e562dfdf | |
|
|
20987e006e | |
|
|
a0f9b34dde | |
|
|
f4380ab2c0 | |
|
|
556ecc5df3 | |
|
|
cb58c2c068 | |
|
|
6d59c44126 | |
|
|
04c2bc5e70 | |
|
|
d355d6685a | |
|
|
1b00462e1c | |
|
|
ce299ebd37 | |
|
|
7315292918 | |
|
|
8b838f48fe | |
|
|
87b9c9aaed | |
|
|
898504d3a6 | |
|
|
6ac47fffcd | |
|
|
606298d40d | |
|
|
d8a4e118a6 | |
|
|
eb7d4c6262 | |
|
|
4ebc522437 | |
|
|
c6ad00f2ca | |
|
|
c05f3b18ce | |
|
|
db6273513a | |
|
|
e5f2ff8dd1 | |
|
|
2c5628124a | |
|
|
05821c7656 | |
|
|
92de29dbc9 | |
|
|
28e7c0c1f3 | |
|
|
649d6b2dcb | |
|
|
a3b76a3acc | |
|
|
5a734f2204 | |
|
|
6fe29baa95 | |
|
|
5e71c6ff97 | |
|
|
6462905b76 | |
|
|
7eba788faa | |
|
|
e9b860428d | |
|
|
4fd2948d99 | |
|
|
78640671d4 | |
|
|
cfbe5a0f86 | |
|
|
ae4b5b7a6d | |
|
|
a8de900166 | |
|
|
12a08e1624 | |
|
|
a9b4f31a7d | |
|
|
d8ac1b62d8 | |
|
|
e66dacf750 | |
|
|
8080d2aebb | |
|
|
4fd080675e | |
|
|
bbba5e0a67 | |
|
|
c37095d229 | |
|
|
f94039dac8 | |
|
|
76bdc5e683 | |
|
|
0f5e2d9e12 | |
|
|
1aad4a6e58 | |
|
|
65ebcfe03e | |
|
|
d5efa38acf | |
|
|
7d3a6b64c4 | |
|
|
32d6e2b0b1 | |
|
|
7f54018320 | |
|
|
5bea564755 | |
|
|
7320398ef3 | |
|
|
082284b6e6 | |
|
|
a7afbc3d0c | |
|
|
ffcf7b4b3a | |
|
|
bbf812b9c2 | |
|
|
8847ee7440 | |
|
|
72ebc33558 | |
|
|
edef2915ee | |
|
|
1e051866c1 | |
|
|
458e8aa9cb | |
|
|
ea7b1ea030 | |
|
|
9cb4d2c572 | |
|
|
5cffec3b42 | |
|
|
ac6450fbbd | |
|
|
449079871c | |
|
|
3af6bff71b | |
|
|
84c8b96a66 | |
|
|
44d6c4c829 | |
|
|
912265a6f8 | |
|
|
537acc2476 | |
|
|
c78be0404c | |
|
|
41e7029e0c | |
|
|
e90a685096 | |
|
|
da0a871234 | |
|
|
b16bb6c75c | |
|
|
22de24e61a | |
|
|
fdb6d6d8a0 | |
|
|
3d7bfb0723 | |
|
|
63f8a320e1 | |
|
|
78d68a8a8b | |
|
|
e88871c226 | |
|
|
48b256753a | |
|
|
ddb4c4776e | |
|
|
f8de08ab61 | |
|
|
1e5d075d5f | |
|
|
25024f6848 | |
|
|
7f07f4b29e | |
|
|
f2cde5176a | |
|
|
5c65721da3 | |
|
|
ea74d91658 | |
|
|
b97dcb11cd | |
|
|
339aa088cc | |
|
|
403fe39f4b | |
|
|
adfca6c42d | |
|
|
bbcfe8a6b5 | |
|
|
2ce53023ee | |
|
|
090cc89b08 | |
|
|
c0e715ca9d | |
|
|
01d8056bee | |
|
|
127feac2e4 | |
|
|
1478eceb9b | |
|
|
d8007a3530 | |
|
|
23eb2601e1 | |
|
|
52f8a17e48 | |
|
|
ac6cafae85 | |
|
|
df2ef8ba04 | |
|
|
8a6a5bbfc5 | |
|
|
c76284041e | |
|
|
2a21ee6566 | |
|
|
bf6dcade68 | |
|
|
ee82bd398c | |
|
|
952b309c71 | |
|
|
8ba410154d | |
|
|
16d80e27e1 | |
|
|
af5339bec8 | |
|
|
1301fde89a | |
|
|
ad85b02b5e | |
|
|
81c3f52f84 | |
|
|
828123c6c8 | |
|
|
55b771e633 | |
|
|
1a749cea8a | |
|
|
75cd975400 | |
|
|
4c3eda6e9b | |
|
|
20a409526d | |
|
|
354a97e11d | |
|
|
bb388fec27 | |
|
|
aa12e8967a | |
|
|
2902bdc22b | |
|
|
830e1440b4 | |
|
|
e858b809b7 | |
|
|
fef026bfad | |
|
|
3816f4b535 | |
|
|
eb9a665a0a | |
|
|
55ab66076e | |
|
|
f6d724a3fe | |
|
|
cabf32a879 | |
|
|
24b8082f35 | |
|
|
f7bde64c42 | |
|
|
66fc4bc68e | |
|
|
f7c4fd2f3c | |
|
|
e2ccbfb28b | |
|
|
bc2e34821b | |
|
|
e6797144d4 | |
|
|
6b8f4f6324 | |
|
|
d6b55fccec | |
|
|
292b75e9f3 | |
|
|
0cde16d497 | |
|
|
759cb9ae3f | |
|
|
57c895482d | |
|
|
a0f874bcc0 | |
|
|
e1d0904417 | |
|
|
eb5313b734 | |
|
|
9fd9c0618c | |
|
|
8b25630852 | |
|
|
c21f4c5b97 | |
|
|
3dcc00db4d | |
|
|
2a34f838e2 | |
|
|
19d1ac8d90 | |
|
|
4722f86aab | |
|
|
54eb5b73b5 | |
|
|
9e802dde92 | |
|
|
ca70087ce0 | |
|
|
10d61a2ea1 | |
|
|
7f8a86435e | |
|
|
53bf8deece | |
|
|
c0b941acf7 | |
|
|
301b624806 | |
|
|
2159bb4272 | |
|
|
dbb54d14eb | |
|
|
75cb711348 | |
|
|
a907d2b926 | |
|
|
1468459f93 | |
|
|
aeddc8fe74 | |
|
|
3dc7591f7f | |
|
|
8d0d498310 | |
|
|
0dcef11533 | |
|
|
d8ba9e5e95 | |
|
|
24577cdfd8 | |
|
|
bcc6bc7d08 | |
|
|
65238c6c47 | |
|
|
efcbf7be9f | |
|
|
e6093213fc | |
|
|
fc1c98cd66 | |
|
|
ff10231cb0 | |
|
|
81e065f84e | |
|
|
6e14dbb34b | |
|
|
e4946abba2 | |
|
|
081b66b25d | |
|
|
b158ebdada | |
|
|
b6f63fc30e | |
|
|
d1afc80bba | |
|
|
7368039b50 | |
|
|
33b675f09b | |
|
|
1c7ecb67aa | |
|
|
ae7861a69f | |
|
|
d319891abb | |
|
|
b3c5c82d25 | |
|
|
6b742d3577 | |
|
|
22489b6e1e | |
|
|
158ca0a909 | |
|
|
4806a96bf2 | |
|
|
ce3bf3e080 | |
|
|
19438d8654 | |
|
|
b0294701d5 | |
|
|
21abd3892d | |
|
|
2b73af42f7 | |
|
|
2ff5a8aa0f | |
|
|
20dd81f606 | |
|
|
5b250b7148 | |
|
|
6051ea52fc | |
|
|
1373f41257 | |
|
|
4078871c1c | |
|
|
5fcd53614e | |
|
|
333c5fbab8 | |
|
|
11387d642a | |
|
|
1947760e2b | |
|
|
7ca3114698 | |
|
|
f20b38bb30 | |
|
|
c6bfe08766 | |
|
|
5f7ebe1471 | |
|
|
42dedc698b | |
|
|
bc845a0d99 | |
|
|
af6fc4bd18 | |
|
|
b3f966bc8e | |
|
|
aa13c254fd | |
|
|
8914e616db | |
|
|
4e370e5439 | |
|
|
2a952a11b3 | |
|
|
930c396aeb | |
|
|
c6c3e4cd87 | |
|
|
80d165950f | |
|
|
2dea212048 | |
|
|
d92e46e301 | |
|
|
149e526360 | |
|
|
3bcffb2b68 | |
|
|
503b565bc5 | |
|
|
6852ed39c0 | |
|
|
9598623648 | |
|
|
d5d8db1bd9 | |
|
|
2ad3674a56 | |
|
|
6bdbf509eb | |
|
|
4124bb759f | |
|
|
8646e47653 | |
|
|
10d6cd699c | |
|
|
fc4872f464 | |
|
|
1e11c6a78d | |
|
|
d6ee7301e7 | |
|
|
f56cb1089f | |
|
|
f5993e7f5c | |
|
|
5be8187e71 | |
|
|
36631072a3 | |
|
|
530cf9e804 | |
|
|
6c20889572 | |
|
|
c24549366d | |
|
|
c4d6ddb204 | |
|
|
3a480aa540 | |
|
|
1f3644eff7 | |
|
|
039354b151 | |
|
|
8d6ebc6910 | |
|
|
0fd71b6887 | |
|
|
4b5660cd0c | |
|
|
31d363455c | |
|
|
a6eb05993b | |
|
|
bfd1e66973 | |
|
|
daf9a3fd35 | |
|
|
551c399555 | |
|
|
6d33562d4c | |
|
|
69f8c7befb | |
|
|
a4362d52c2 | |
|
|
c28b466385 | |
|
|
71598b4503 | |
|
|
338c22e26b | |
|
|
fc3af1dcdd | |
|
|
001170f0e2 | |
|
|
900f45ce4d | |
|
|
daa4024dac | |
|
|
322fc69206 | |
|
|
c89193e46d | |
|
|
357a665994 | |
|
|
d9dfa12b62 | |
|
|
e7cd55a303 | |
|
|
63227f6b1c | |
|
|
124fc6658d | |
|
|
21f5685ae7 | |
|
|
5811f32d1c | |
|
|
f76abf3e33 | |
|
|
252b958bb7 | |
|
|
89d8505181 |
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"autoMerge": true,
|
||||
"autoMergeMethod": "squash"
|
||||
}
|
||||
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ name: Publish Cirrus container
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: ['UE5.3']
|
||||
workflow_dispatch:
|
||||
branches: ['UE5.4']
|
||||
paths: ['SignallingWebServer/**']
|
||||
|
||||
jobs:
|
||||
|
|
@ -27,6 +28,15 @@ jobs:
|
|||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
tags: 'ghcr.io/epicgames/pixel-streaming-signalling-server:5.3'
|
||||
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
|
||||
|
||||
|
|
@ -2,7 +2,7 @@ name: Releases
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: ['UE5.3']
|
||||
branches: ['UE5.4']
|
||||
paths: ['RELEASE_VERSION']
|
||||
|
||||
jobs:
|
||||
|
|
@ -48,16 +48,16 @@ jobs:
|
|||
working-directory: ./Frontend/ui-library
|
||||
run: npm run build-all
|
||||
|
||||
- name: Install implementations/EpicGames deps
|
||||
working-directory: ./Frontend/implementations/EpicGames
|
||||
- name: Install implementations/typescript deps
|
||||
working-directory: ./Frontend/implementations/typescript
|
||||
run: npm ci
|
||||
|
||||
- name: NPM link frontend and ui-library into implementations/EpicGames
|
||||
working-directory: ./Frontend/implementations/EpicGames
|
||||
- name: NPM link frontend and ui-library into implementations/typescript
|
||||
working-directory: ./Frontend/implementations/typescript
|
||||
run: npm link ../../library ../../ui-library
|
||||
|
||||
- name: Build implementations/EpicGames
|
||||
working-directory: ./Frontend/implementations/EpicGames
|
||||
- name: Build implementations/typescript
|
||||
working-directory: ./Frontend/implementations/typescript
|
||||
run: npm run build-all
|
||||
|
||||
- name: Move all content into output directory for archiving
|
||||
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
path: 'PixelStreamingInfrastructure-${{ github.ref_name }}-${{ steps.getversion.outputs.version }}'
|
||||
type: 'tar'
|
||||
filename: '${{ github.ref_name }}-${{ steps.getversion.outputs.version }}.tar.gz'
|
||||
exclusions: '.git .github output Frontend/Docs Frontend/library/dist Frontend/library/types Frontend/library/node_modules Frontend/ui-library/dist Frontend/ui-library/types Frontend/ui-library/node_modules Frontend/implementations/EpicGames/node_modules'
|
||||
exclusions: '.git .github output Frontend/Docs Frontend/library/dist Frontend/library/types Frontend/library/node_modules Frontend/ui-library/dist Frontend/ui-library/types Frontend/ui-library/node_modules Frontend/implementations/typescript/node_modules'
|
||||
|
||||
- name: Archive Release .zip
|
||||
uses: thedoctor0/zip-release@0.7.1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
name: Publish frontend lib
|
||||
on:
|
||||
push:
|
||||
branches: ['UE5.3']
|
||||
workflow_dispatch:
|
||||
branches: ['UE5.4']
|
||||
paths: ['Frontend/library/package.json']
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -13,7 +14,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.17.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
name: Publish frontend-ui lib
|
||||
on:
|
||||
push:
|
||||
branches: ['UE5.3']
|
||||
workflow_dispatch:
|
||||
branches: ['UE5.4']
|
||||
paths: ['Frontend/ui-library/package.json']
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -13,7 +14,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '18.17.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
name: Mark and close stale issues and pull requests
|
||||
|
||||
on:
|
||||
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Issues go stale after 30 days of inactivity. Please comment or re-open the issue if you are still interested in getting this issue fixed.'
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'enhancement'
|
||||
|
||||
stale-pr-message: 'PRs go stale after 30 days of inactivity. Please comment or re-open the PR if you are still working on this PR.'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'awaiting-approval'
|
||||
|
||||
days-before-stale: 30
|
||||
days-before-close: 0
|
||||
|
||||
60
CHANGELOG.md
|
|
@ -4,10 +4,46 @@ The changelog is a summary of commits between releases of Unreal Engine.
|
|||
|
||||
As a reminder each UE-X branch/tag in this repository corresponds to a version of Unreal Engine.
|
||||
|
||||
## [UE 5.3 (Coming soon)](https://github.com/EpicGames/PixelStreamingInfrastructure/commits/UE5.3)
|
||||
Coming soon...
|
||||
## [UE 5.3 (Current)](https://github.com/EpicGames/PixelStreamingInfrastructure/commits/UE5.3)
|
||||
|
||||
## [UE 5.2 (Current)](https://github.com/EpicGames/PixelStreamingInfrastructure/commits/UE5.2)
|
||||
### Features
|
||||
- Protocol structures can now contain strings by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/336
|
||||
- Added the ability for the frontend peer to auto connect when a new streamer is available by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/358
|
||||
|
||||
### Enhancements
|
||||
- Upgrade 5.2 to 5.3 in libraries, docs, log messages, build pipelines by @lukehb in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/262
|
||||
- Include create, reconnect, and update events (with associated tests) by @jibranabsarulislam in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/273
|
||||
- Add github action for PR and Issue management by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/304
|
||||
- Update stale.yml by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/305
|
||||
- Update stale workflow to also auto close stale issues and PRs by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/306
|
||||
- Show player count in stats panel by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/303
|
||||
- Change implementations/EpicGames to implementations/typescript #166 by @gunsha in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/177
|
||||
- Refactor SignallingWebServer to a single docker file by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/337
|
||||
- Update LatencyTest handler to accept input data by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/340
|
||||
- Add contribution guideline `CONTRIBUTING.md` by @DenisTensorWorks in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/352
|
||||
- New matchmaker queue screen with easy customization by @kasp1 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/211
|
||||
- Updated CONTRIBUTING.md with backport rules by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/357
|
||||
|
||||
### Documentation
|
||||
- Signaling message reference doc by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/271
|
||||
- Update SignallingProtocol.md by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/288
|
||||
|
||||
### Bug fixes
|
||||
- Fixed auto reconnect to not reconnect when the page is refreshed. by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/285
|
||||
- Fixed iOS touch when settings panel is open by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/274
|
||||
- Fixed Firefox console errors `TypeError: this.preferredCodec.split is not a function` by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/310
|
||||
- Fixed ensuring touch is relative to absolute location of parent rect by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/316
|
||||
- Fixed injecting new params into SDP to get stereo back on Chrome by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/332
|
||||
- Fixed matchmaker asking for OS authentication instead of erroring out with EACCESS by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/333
|
||||
- Fixed consuming the context menu event instead of sending a mouse up by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/354
|
||||
|
||||
### Security
|
||||
- Bump tough-cookie from 4.1.2 to 4.1.3 in /Frontend/library by @dependabot in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/290
|
||||
- Bump word-wrap from 1.2.3 to 1.2.4 in /Frontend/library by @dependabot in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/320
|
||||
- Bump word-wrap from 1.2.3 to 1.2.5 in /Frontend/ui-library by @dependabot in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/327
|
||||
- Bump Node.js to latest LTS by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/321
|
||||
|
||||
## [UE 5.2](https://github.com/EpicGames/PixelStreamingInfrastructure/commits/UE5.2)
|
||||
|
||||
### Features
|
||||
- Added minimal sample React implementation by @hmuurine in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/159
|
||||
|
|
@ -18,6 +54,12 @@ Coming soon...
|
|||
- Added experimental support for WebXR based experiences by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/85
|
||||
|
||||
### Docs
|
||||
- New general docs page/ToC + new security page. by @MWillWallT in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/254
|
||||
- Update README to mention container images require being part of Epic's Github org by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/248
|
||||
- Update platform_scripts readme.md to explain the different scripts by @lukehb in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/224
|
||||
- Improve signalling Server readme @DenisTensorWorks in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/223
|
||||
- Adding microphone feature documentation for UE5.2 by @DenisTensorWorks in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/228
|
||||
- Adding microphone feature documentation by @DenisTensorWorks in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/208
|
||||
- Added new general docs page/ToC + new security page. by @MWillWallT in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/254
|
||||
- Settings Panel Documentation by @MWillWallT in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/135
|
||||
- Customised Pixel Streaming Player Page by @MWillWallT in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/90
|
||||
|
|
@ -29,6 +71,14 @@ Coming soon...
|
|||
- Update Docs to remove broken links by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/122
|
||||
|
||||
### Enhancements
|
||||
- Add repository health status in the form of Github badges table on readme.md by @lukehb in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/265
|
||||
- Re-enable iOS and iPadOS fullscreen by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/266
|
||||
- Changed forwarded logs to Cyan, added warning for missing playerId by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/253
|
||||
- Added "media-playout" to prevent spam in Aggregated Stats by @chasse20 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/232
|
||||
- Added 'stat PixelStreamingGraphs' to showcase frontend #229 by @devrajgadhvi in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/238
|
||||
- Bump socket.io-parser from 4.2.2 to 4.2.4 in /Matchmaker by @dependabot in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/244
|
||||
- Bump engine.io from 6.4.0 to 6.4.2 in /Matchmaker by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/220
|
||||
- Allow inheritance of webrtcPlayerController and webXrController by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/209
|
||||
- Pass command line args when calling run_local.bat by @lukehb in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/109
|
||||
- Customize frontend styles through UI API by @hmuurine in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/133
|
||||
- Force URL param settings when receiving initial application settings by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/134
|
||||
|
|
@ -57,6 +107,10 @@ Coming soon...
|
|||
- Replaced hardcoded log path with given parameter path by @Mirmidion in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/39
|
||||
|
||||
### Bug fixes
|
||||
- Fixed viewport resizing not always working due to improperly calling timer. by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/247
|
||||
- Fixed hovering mouse mode set in URL being overridden on refresh. by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/246
|
||||
- Fixed matchmaker directing users to http when the signalling server is using https by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/245
|
||||
- Fixed reconnects will be attempted even when a disconnect is triggered by afk timeout by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/215
|
||||
- Fixed datachannels not working when using the SFU by @mcottontensor in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/137
|
||||
- Fixed SFU having clashing datachannel/stream ids, now using mediasoup's internal stream ids for SCTP by @StomyPX in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/129
|
||||
- Fixed controller indices from multiple peers would clash by @Belchy06 in https://github.com/EpicGames/PixelStreamingInfrastructure/pull/165
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
# Welcome to Pixel Streaming contribution guide
|
||||
|
||||
First and foremost, thank you for your time and contribution to Pixel Streaming!
|
||||
|
||||
We are proud and excited to be a part of a passionate community that continuously helps us improve Pixel Streaming 🎉
|
||||
|
||||
If you are not familiar with contributing on GitHub, have a look at the [official documentation](https://docs.github.com/get-started) to learn more about repositories structure, forks, branches, commits, issues, and PRs.
|
||||
|
||||
### Code of conduct
|
||||
|
||||
Please remain patient, courteous, and professional at all times. Any form of spam, abuse, or discrimination will not be tolerated.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Creating issues
|
||||
|
||||
If you have encountered a bug, have suggestions for our documentation or infrastructure, or would like to propose a feature that could enhance Pixel Streaming in various use case scenarios, you can raise this with us by creating a new issue.
|
||||
1. First, search all open and closed issues [here](https://github.com/EpicGames/PixelStreamingInfrastructure/issues?q=is%3Aissue+) - your issue may have already been discussed or addressed.
|
||||
2. If your issue doesn't exist, open a new issue [here](https://github.com/EpicGames/PixelStreamingInfrastructure/issues/new/choose) by selecting a bug or feature request.
|
||||
3. Make sure to fill in the template as much as possible; any information you can provide, such as repro steps, crash stacks, screenshots, etc., can help us triage and fix the problem as quickly as possible.
|
||||
4. Keep an eye on the status of your issue; our developers or other users might reach out with requests for more information. If this happens, issues that have not received a response in over 30 days will be automatically closed.
|
||||
5. Be patient while waiting on a resolution; we prioritize the issues internally and some less critical features (however much we'd love to implement them!) will take a backseat to more pressing priorities, so some issues can take a while to get resolved.
|
||||
|
||||
### Creating pull requests (PR)
|
||||
|
||||
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 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.
|
||||
6. If your PR is good to go, we will merge it in. Woohoo! Thank you for your time and efforts! 🎉
|
||||
7. Keep a close eye on your PR - quite often, our developers will review your PR and leave comments; we might request some minor code changes and modifications, style unification, or leave any general comments and questions that are preventing us from merging the PR.
|
||||
8. If we do not hear from you after requesting more information within 30 days, the PR will auto-close. In this case, we might elect to open our own PR and re-use some of the changes that you proposed, supplemented with anything else that was required to be added in your original PR.
|
||||
9. If your PR fixes a problem in the previous [still-supported UE branches](https://github.com/EpicGames/PixelStreamingInfrastructure#versions), feel free to add the `auto-backport` and `auto-backport-to-UEX.X` labels. You'll need to add a `auto-backport-to-UEX.X` label for each branch you wish your change to be merged back to. Note that if a change to any of the previous branches is not trivial and requires a lot of testing and compatibility checks, we might elect to close it if we do not think that it brings enough value to the branch.
|
||||
|
||||
### Other ways to contribute
|
||||
|
||||
- Keep an eye on our repo and stay active on existing issues and PRs; you can help by adding informative comments to the discussions, additional repro steps, repros in different environments, or any suggestions as to what could be causing the issue and how it could be solved.
|
||||
- Work on [issues labeled for community](https://github.com/EpicGames/PixelStreamingInfrastructure/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). We specifically created this label to mark issues that we would love the community to help us with.
|
||||
- Create documentation for undocumented features. Please open an issue first, so our developers can provide you with some guidance.
|
||||
- Write more unit test coverage.
|
||||
- Document functions in the public API that are not documented.
|
||||
- Write new frontend implementations using another web framework, e.g. Angular, Vue, etc.
|
||||
- Perform QA on different engine versions, particularly previews, and create issues based on the bugs that you have found.
|
||||
|
||||
## Coding style
|
||||
- TypeScript should be used over JavaScript.
|
||||
- All TypeScript should adhere to the following [linting rules](https://github.com/EpicGames/PixelStreamingInfrastructure/blob/master/Frontend/library/.eslintrc.js).
|
||||
- Names should follow US English spelling.
|
||||
- All public functions/API should have comments.
|
||||
- Code formatting should adhere to the following [whitespace and indentation rules](https://github.com/EpicGames/PixelStreamingInfrastructure/blob/master/Frontend/library/.prettierrc.json).
|
||||
- All new features should have accompanying unit tests and documentation when they are submitted.
|
||||
- Prefer early returns in `if` statements to decrease indentation.
|
||||
- Prefer functions to not exceed ~20 lines.
|
||||
- Prefer comments in longer functions.
|
||||
- Prefer verbosity over syntactic sugar.
|
||||
- Prefer exporting a minimal public API surface for iteration and support reasons.
|
||||
- Try not to exceed three levels of nesting in a function.
|
||||
|
||||
## Documentation style
|
||||
All documentation should be written in US English and follow correct grammar and spelling. Endeavour to lay out the document in a logical fashion with headings, lists, and bullet points where appropriate.
|
||||
|
||||
Documentation should be broken up into separate `.md` files per directory, ideally with a `readme.md` file in the root of each top-level directory for a component to explain it. Where appropriate, these documentation pages should be linked to a table of contents in the relevant part of the repository.
|
||||
|
||||
## Legal
|
||||
|
||||
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
|
|
@ -9,4 +9,4 @@ Welcome to the general documentation page for Pixel Streaming. This page serves
|
|||
|
||||
|
||||
## Legal
|
||||
© 2004-2023, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
© 2004-2024, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ The following options are available in the frontend library to customize input:
|
|||
| HoveringMouseMode | false | Determines whether or not the video element captures and locks the mouse when the player interacts with the widget. When enabled, the mouse cursor hovers over the player widget without interacting with it. In order to send the mouse movements to the input controller of the Unreal Engine application, the user needs to click and hold the left button of the mouse. Otherwise, clicking on the player widget causes it to capture and lock the mouse cursor. Any further movements of the mouse are passed immediately to the input controller in the Unreal Engine application. This typically allows the user to move and rotate the camera by simply dragging the mouse. To release the cursor from the control of the player widget, the user can press the **Esc** key. |
|
||||
| SuppressBrowserKeys | true | When this setting is enabled, the player widget will intercept function keys (**F1** to **F12**) and the **Tab** key, and pass those keypress events through to the Unreal Engine application rather than allowing the browser to process them normally.| This means, for example, that while this setting is active, pressing **F5** will not refresh the player page in the browser. Instead, that event is passed through to the Unreal Engine application, and has its usual function of switching the view to visualize shader complexity.
|
||||
| FakeMouseWithTouches | false | When this option is enabled and the user is viewing the stream on a device with a touch screen such as a smartphone or tablet, this setting causes single-finger touch events to be interpreted by the Unreal Engine application as mouse clicks and drag events. Enabling this setting can provide users on mobile devices with the ability to partially control your Unreal Engine application, even when the application's input controller does not specifically handle touch input events. |
|
||||
| UseMic | false | Indicates whether or not the stream should be created with a microphone track which is sent to the UE application. This microphone track can be listened to using the [`UPixelStreamingAudioComponent`](https://docs.unrealengine.com/5.0/en-US/API/Plugins/PixelStreaming/UPixelStreamingAudioComponent/). If this flag is enabled, a microphone audio track is created (if the browser settings allow for it) and is actively sending audio. For saving bandwidth or other functionality, it's possible to mute the microphone track by calling `PixelStreaming`'s `muteMicrophone`, and later re-enable it with `unmuteMicrophone`. If the `PixelStreaming` object was created without this flag, it's possible to enable it later by calling `unmuteMicrophone(true)`, which will use the `forceEnable` parameter to add the track and trigger a full reconnection (this is a heavier operation that takes a while). |
|
||||
|
||||
When creating a frontend implementation, these options are visible via the [`Config`](/Frontend/library/src/Config/Config.ts) object required in order to create a [`PixelStreaming`](/Frontend/library/src/PixelStreaming/PixelStreaming.ts) stream for your frontend application. Simply set the values you want before initializing the stream object.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
## Recommended Reading
|
||||
We recommend starting with the [sample implementations](/Frontend/implementations/EpicGames/src) in order to judge how to put a new player page together and integrate it with your Unreal Engine application. Additionally, if you have cloned the Pixel Streaming Infrastructure repository and made upstream changes, you can fork the repo and make a pull request.
|
||||
We recommend starting with the [sample implementations](/Frontend/implementations/typescript/src) in order to judge how to put a new player page together and integrate it with your Unreal Engine application. Additionally, if you have cloned the Pixel Streaming Infrastructure repository and made upstream changes, you can fork the repo and make a pull request.
|
||||
|
||||
## Using the default Player Webpage
|
||||
The Pixel Streaming Signalling and Web Server provides a sample player page that is already set up to stream in media from your Unreal Engine application and to send mouse, keyboard, and touch events back to the application. You can use this default player page as-is, if it meets your needs.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## HTML Page Requirements
|
||||
|
||||
Most of the HTML that will end up on the final page will actually be introduced by the Pixel Streaming application itself. Several example HTML pages are provided in the [sample implementations](/Frontend/implementations/EpicGames/src) where you can see the base page is very minimal, only serving as a space for the application to attach to and fill. The only concrete requirements are for ensuring there's sufficient space taken up by the element being attached to for the viewport to be visible on screen. In the sample implementations this is simply a `<body>` tag set to fill the screen without scrolling.
|
||||
Most of the HTML that will end up on the final page will actually be introduced by the Pixel Streaming application itself. Several example HTML pages are provided in the [sample implementations](/Frontend/implementations/typescript/src) where you can see the base page is very minimal, only serving as a space for the application to attach to and fill. The only concrete requirements are for ensuring there's sufficient space taken up by the element being attached to for the viewport to be visible on screen. In the sample implementations this is simply a `<body>` tag set to fill the screen without scrolling.
|
||||
|
||||
```html
|
||||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
|
|
@ -20,14 +20,14 @@ Most of the HTML that will end up on the final page will actually be introduced
|
|||
</html>
|
||||
```
|
||||
|
||||
As can be seen in the [sample implementations](/Frontend/implementations/EpicGames/src/player.ts), you must specify which element on the page the Pixel Streaming viewport is to be appended to. In the sample implementations this is typically done in the `document.body.onload` event listener and in this case appended to the `document.body` element in the DOM, causing it to fill the whole page.
|
||||
As can be seen in the [sample implementations](/Frontend/implementations/typescript/src/player.ts), you must specify which element on the page the Pixel Streaming viewport is to be appended to. In the sample implementations this is typically done in the `document.body.onload` event listener and in this case appended to the `document.body` element in the DOM, causing it to fill the whole page.
|
||||
|
||||
[//]: # (This has yet to be done)
|
||||
### Player File Location and URL
|
||||
|
||||
You have a few options for where you can place your custom HTML player page, and how client browsers can access it.
|
||||
|
||||
* You can create a new implementation page and place it in [`/Frontend/implementations/EpicGames/src/`](/Frontend/implementations/EpicGames/src) alongside the sample implementations. This must consist of both a base `.html` page and the `.ts` source for your application's entrypoint. This will then be accessible by appending the name of the `html` file to IP address or hostname of the computer running the Signalling Server.
|
||||
* You can create a new implementation page and place it in [`/Frontend/implementations/typescript/src/`](/Frontend/implementations/typescript/src) alongside the sample implementations. This must consist of both a base `.html` page and the `.ts` source for your application's entrypoint. This will then be accessible by appending the name of the `html` file to IP address or hostname of the computer running the Signalling Server.
|
||||
For example, the sample `stresstest` page can be accessed on a locally-running infrastructure at `http:/127.0.0.1/stresstest.html`.
|
||||
* You can customize the `HomepageFile` parameter for the Signaling and Web Server, and set the path to the filename of your custom HTML player page relative to the [Frontend implementations source folder](/Frontend/implementations/src). It will then be accessible when you access the IP address or hostname of the computer running the Signaling and Web Server.
|
||||
* You can also use the **AdditionalRoutes** parameter for the Signaling and Web Server to customize the mapping between URL paths and local folders on your computer.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ The **frontend** refers to the HTML, CSS, images, and JavaScript/TypeScript code
|
|||
The frontend consists of two packages:
|
||||
|
||||
1. [lib-pixelstreamingfrontend](/Frontend/library/): the core Pixel Streaming frontend for WebRTC, settings, input, and general functionality.
|
||||
2. [lib-pixelstreamingfrontend-ui](/Frontend/implementations/EpicGames): the reference UI that users can either optionally apply on top of the core library or build on top of.
|
||||
2. [lib-pixelstreamingfrontend-ui](/Frontend/implementations/typescript): the reference UI that users can either optionally apply on top of the core library or build on top of.
|
||||
|
||||
|
||||
## Docs
|
||||
|
|
@ -29,7 +29,7 @@ The TypeScript libraries are provided as both an [NPM](https://www.npmjs.com/set
|
|||
## Usage from source
|
||||
|
||||
When developing your own Pixel Streaming experience the intent is you will start with this library and extend it through the use of
|
||||
its public API. We have provided an example of this workflow in our [implementations/EpicGames](/Frontend/implementations/EpicGames), which is an implementation of this library.
|
||||
its public API. We have provided an example of this workflow in our [implementations/typescript](/Frontend/implementations/typescript), which is an implementation of this library.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
@ -37,6 +37,14 @@ If part of the library is not exposed and you wish to extend it, please do so in
|
|||
|
||||
## Developing
|
||||
|
||||
⚠️ Only NodeJS LTS 18.17.0 is officially supported, some newer versions on NodeJS **WILL BREAK YOUR BUILD** ⚠️
|
||||
|
||||
### Prerequisites
|
||||
- Install NodeJS LTS 18.17.0 on your system.
|
||||
- Install npm globally using: `npm install npm -g` (yes this is required)
|
||||
|
||||
### Building the Library
|
||||
|
||||
Changes to the library occur in the [/library](/Frontend/library) directory and require you to have NodeJS installed as part of your development environment.
|
||||
Once you have NodeJS installed:
|
||||
|
||||
|
|
@ -44,16 +52,28 @@ Once you have NodeJS installed:
|
|||
- `npm install`
|
||||
- `npm run build`
|
||||
|
||||
The default user interface is provided in [/ui-library](/Frontend/ui-library) directory. You can either use it or provide your own user interface. To build the default UI, run:
|
||||
### Building the UI-Library
|
||||
|
||||
The user interface library is provided in [/ui-library](/Frontend/ui-library) directory. You can either use it or provide your own user interface. To build run:
|
||||
- Follow the steps to build the library first
|
||||
- `cd ui-library`
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
- `npm run build-all`
|
||||
|
||||
This will produce `player.js` under the `SignallingWebServer/Public` directory - this is the default UI.
|
||||
### Building the default UI
|
||||
|
||||
The default user interface is provided under [/implementations/typescript](/Frontend/implementations/typescript). To build run:
|
||||
|
||||
- Follow the steps to build the libary and ui-library first
|
||||
- `cd implementations/typescript`
|
||||
- `npm install`
|
||||
- `npm run build-all`
|
||||
|
||||
This will produce `player.html` and `player.js` under the `SignallingWebServer/Public` directory - this is the default UI.
|
||||
|
||||
### Making your own UI
|
||||
|
||||
We recommend studying [/ui-library](/Frontend/ui-library) and [player.ts](/Frontend/implementations/EpicGames/src/player.ts)/[player.html](/Frontend/implementations/EpicGames/src/player.html), or alternatively the sample React implementation in [implementations/react](/Frontend/implementations/react), then once you have copied and modified the [package.json](/Frontend/implementations/EpicGames/package.json) and `.ts` into your own `implementation/your_implementation` directory, the process is similar:
|
||||
We recommend studying [/ui-library](/Frontend/ui-library) and [player.ts](/Frontend/implementations/typescript/src/player.ts)/[player.html](/Frontend/implementations/typescript/src/player.html), or alternatively the sample React implementation in [implementations/react](/Frontend/implementations/react), then once you have copied and modified the [package.json](/Frontend/implementations/typescript/package.json) and `.ts` into your own `implementation/your_implementation` directory, the process is similar:
|
||||
|
||||
- `cd implementation/your_implementation`
|
||||
- `npm build-all`
|
||||
|
|
@ -67,4 +87,4 @@ The [/library](/Frontend/library) project has unit tests that test the Pixel Str
|
|||
|
||||
## Legal
|
||||
|
||||
Copyright © 2023, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.
|
||||
Copyright © 2024, Epic Games. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# Configuration of the Frontend UI
|
||||
|
||||
Todo
|
||||
|
Before Width: | Height: | Size: 55 KiB |
|
|
@ -1 +0,0 @@
|
|||
The images directory
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%; height: 100%">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Michroma&family=Montserrat:wght@600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Required - the Login style sheet -->
|
||||
<link rel="stylesheet" type="text/css" href="./assets/css/login.css">
|
||||
|
||||
<!-- Optional: set some favicons -->
|
||||
<link id="favPng" rel="icon" type="image/png" href="./assets/images/favicon.png">
|
||||
|
||||
<!-- Optional: set a title for your page -->
|
||||
<title>Pixel Streaming Login</title>
|
||||
</head>
|
||||
|
||||
<body style="width: 100vw; height: 100vh; min-height: -webkit-fill-available; font-family: 'Montserrat'; margin: 0px">
|
||||
<form action="/login" method="post">
|
||||
<div class="entry">
|
||||
<input type="text" id="username" name="username" placeholder="Username"
|
||||
autocomplete="username">
|
||||
</div>
|
||||
<div class="entry">
|
||||
<input type="password" id="password" name="password" placeholder="Password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<div class="entry button">
|
||||
<button type="submit">LOGIN</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Angular Implementations
|
||||
|
||||
Here are a selection of community contributed implementations of Angular frontends.
|
||||
|
||||
- [cheikhnadiouf](https://github.com/cheikhnadiouf)'s implementation - [LINK](https://github.com/cheikhnadiouf/PixelStreamingInfrastructure/tree/AngularImplementations/Frontend/implementations/angular)
|
||||
|
||||
If you wish to contribute your own example frontend, please open an issue/PR.
|
||||
|
||||
**Disclaimer: We do not warrant these for any fitness of purpose, nor do we maintain them.**
|
||||
|
|
@ -1576,9 +1576,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2500,10 +2500,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
|
@ -2831,9 +2837,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.21",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2843,10 +2849,14 @@
|
|||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@epicgames-ps/react-pixelstreamingfrontend-react-ue5.3",
|
||||
"name": "@epicgames-ps/react-pixelstreamingfrontend-react-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "./src/index.tsx",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export const App = () => {
|
|||
AutoConnect: true,
|
||||
ss: 'ws://localhost:80',
|
||||
StartVideoMuted: true,
|
||||
HoveringMouse: true
|
||||
HoveringMouse: true,
|
||||
WaitForStreamer: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
Config,
|
||||
AllSettings,
|
||||
PixelStreaming
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
|
||||
export interface PixelStreamingWrapperProps {
|
||||
initialSettings?: Partial<AllSettings>;
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
@ -2207,9 +2208,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
|
@ -2505,8 +2513,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.21",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2516,10 +2525,14 @@
|
|||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
|
|
@ -4886,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": {
|
||||
|
|
@ -5463,8 +5477,9 @@
|
|||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"dev": true
|
||||
},
|
||||
"negotiator": {
|
||||
|
|
@ -5680,11 +5695,12 @@
|
|||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.21",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.3",
|
||||
"name": "@epicgames-ps/reference-pixelstreamingfrontend-ue5.4",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "./src/player.ts",
|
||||
|
|
@ -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/EpicGames && 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/EpicGames && 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",
|
||||
|
|
@ -4,7 +4,7 @@ A plugin library that can be optionally applied on top of the core library to cr
|
|||
|
||||
**This is great starting point for building your UI or studying the Pixel Streaming feature set.**
|
||||
|
||||

|
||||

|
||||
|
||||
### Key features
|
||||
- An info panel (screen right) that provides a UI for displaying live statistics to the user.
|
||||
|
|
@ -14,4 +14,4 @@ A plugin library that can be optionally applied on top of the core library to cr
|
|||
|
||||
|
||||
### Adding it to your project
|
||||
`npm i @epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3`
|
||||
`npm i @epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4`
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 959 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
|
@ -1,11 +1,16 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3';
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4';
|
||||
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);
|
||||
|
|
@ -15,10 +20,13 @@ document.body.onload = function() {
|
|||
|
||||
// Create a Native DOM delegate instance that implements the Delegate interface class
|
||||
const stream = new PixelStreaming(config);
|
||||
|
||||
const application = new Application({
|
||||
stream,
|
||||
onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode)
|
||||
});
|
||||
// document.getElementById("centrebox").appendChild(application.rootElement);
|
||||
document.body.appendChild(application.rootElement);
|
||||
|
||||
window.pixelStreaming = stream;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3';
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4';
|
||||
export const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle();
|
||||
PixelStreamingApplicationStyles.applyStyleSheet();
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Config, Flags, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3';
|
||||
import { Config, Flags, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { Application, PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4';
|
||||
const PixelStreamingApplicationStyles =
|
||||
new PixelStreamingApplicationStyle();
|
||||
PixelStreamingApplicationStyles.applyStyleSheet();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Config, PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
|
||||
document.body.onload = function() {
|
||||
// Example of how to set the logger level
|
||||
|
|
@ -13,6 +13,7 @@ document.body.onload = function() {
|
|||
AutoConnect: true,
|
||||
ss: "ws://localhost:80",
|
||||
StartVideoMuted: true,
|
||||
WaitForStreamer: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3",
|
||||
"version": "0.0.1",
|
||||
"description": "Frontend library for Unreal Engine 5.3 Pixel Streaming",
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4",
|
||||
"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",
|
||||
"types": "types/pixelstreamingfrontend.d.ts",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The core library for the browser/client side of Pixel Streaming experiences. **This library contains no UI.**
|
||||
|
||||
See [lib-pixelstreamingfrontend-ui](/Frontend/implementations/EpicGames) for an example of how to build UI on top of this library.
|
||||
See [lib-pixelstreamingfrontend-ui](/Frontend/implementations/typescript) for an example of how to build UI on top of this library.
|
||||
|
||||
### Key features
|
||||
- Create a websocket connection to communicate with the signalling server.
|
||||
|
|
@ -11,5 +11,5 @@ See [lib-pixelstreamingfrontend-ui](/Frontend/implementations/EpicGames) for an
|
|||
- Opens a datachannel connection sending and receiving custom data (in addition to input).
|
||||
|
||||
### Adding it to your project
|
||||
`npm i @epicgames-ps/lib-pixelstreamingfrontend-ue5.3`
|
||||
`npm i @epicgames-ps/lib-pixelstreamingfrontend-ue5.4`
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -32,6 +31,7 @@ export class Flags {
|
|||
static TouchInput = 'TouchInput' as const;
|
||||
static GamepadInput = 'GamepadInput' as const;
|
||||
static XRControllerInput = 'XRControllerInput' as const;
|
||||
static WaitForStreamer = "WaitForStreamer" as const;
|
||||
}
|
||||
|
||||
export type FlagsKeys = Exclude<keyof typeof Flags, 'prototype'>;
|
||||
|
|
@ -54,6 +54,7 @@ export class NumericParameters {
|
|||
static WebRTCMinBitrate = 'WebRTCMinBitrate' as const;
|
||||
static WebRTCMaxBitrate = 'WebRTCMaxBitrate' as const;
|
||||
static MaxReconnectAttempts = 'MaxReconnectAttempts' as const;
|
||||
static StreamerAutoJoinInterval = 'StreamerAutoJoinInterval' as const;
|
||||
}
|
||||
|
||||
export type NumericParametersKeys = Exclude<
|
||||
|
|
@ -155,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,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
|
||||
*/
|
||||
|
|
@ -183,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
|
||||
)
|
||||
);
|
||||
|
|
@ -200,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
|
||||
)
|
||||
|
|
@ -216,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
|
||||
)
|
||||
);
|
||||
|
|
@ -253,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
|
||||
)
|
||||
);
|
||||
|
|
@ -264,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
|
||||
)
|
||||
);
|
||||
|
|
@ -275,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
|
||||
)
|
||||
);
|
||||
|
|
@ -286,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
|
||||
)
|
||||
);
|
||||
|
|
@ -297,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
|
||||
)
|
||||
);
|
||||
|
|
@ -308,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
|
||||
)
|
||||
);
|
||||
|
|
@ -330,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
|
||||
)
|
||||
);
|
||||
|
|
@ -341,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
|
||||
)
|
||||
);
|
||||
|
|
@ -352,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
|
||||
)
|
||||
);
|
||||
|
|
@ -363,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
|
||||
)
|
||||
);
|
||||
|
|
@ -374,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
|
||||
)
|
||||
);
|
||||
|
|
@ -385,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`;
|
||||
|
|
@ -399,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
|
||||
)
|
||||
);
|
||||
|
|
@ -410,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
|
||||
)
|
||||
);
|
||||
|
|
@ -421,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
|
||||
)
|
||||
);
|
||||
|
|
@ -432,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
|
||||
)
|
||||
);
|
||||
|
|
@ -443,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
|
||||
)
|
||||
);
|
||||
|
|
@ -454,7 +481,22 @@ 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
|
||||
)
|
||||
);
|
||||
|
||||
this.flags.set(
|
||||
Flags.WaitForStreamer,
|
||||
new SettingFlag(
|
||||
Flags.WaitForStreamer,
|
||||
'Wait for streamer',
|
||||
'Will continue trying to connect to the first streamer available.',
|
||||
settings && settings.hasOwnProperty(Flags.WaitForStreamer) ?
|
||||
settings[Flags.WaitForStreamer] :
|
||||
true,
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -471,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
|
||||
)
|
||||
);
|
||||
|
|
@ -484,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
|
||||
)
|
||||
);
|
||||
|
|
@ -497,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
|
||||
)
|
||||
);
|
||||
|
|
@ -510,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
|
||||
)
|
||||
);
|
||||
|
|
@ -523,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
|
||||
)
|
||||
);
|
||||
|
|
@ -536,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
|
||||
)
|
||||
);
|
||||
|
|
@ -549,7 +603,24 @@ 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
|
||||
)
|
||||
);
|
||||
|
||||
this.numericParameters.set(
|
||||
NumericParameters.StreamerAutoJoinInterval,
|
||||
new SettingNumber(
|
||||
NumericParameters.StreamerAutoJoinInterval,
|
||||
'Streamer Auto Join Interval (ms)',
|
||||
'Delay between retries when waiting for an available streamer.',
|
||||
500 /*min*/,
|
||||
900000 /*max*/,
|
||||
settings && settings.hasOwnProperty(NumericParameters.StreamerAutoJoinInterval) ?
|
||||
settings[NumericParameters.StreamerAutoJoinInterval] :
|
||||
3000, /*value*/
|
||||
useUrlParams
|
||||
)
|
||||
);
|
||||
|
|
@ -728,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -748,24 +825,24 @@ export class Config {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a subset of all settings in one function call.
|
||||
*
|
||||
* @param settings A (partial) list of settings to set
|
||||
*/
|
||||
setSettings(settings: Partial<AllSettings>) {
|
||||
for (const key of Object.keys(settings)) {
|
||||
if (isFlagId(key)) {
|
||||
this.setFlagEnabled(key, settings[key]);
|
||||
} else if (isNumericId(key)) {
|
||||
this.setNumericSetting(key, settings[key]);
|
||||
} else if (isTextId(key)) {
|
||||
this.setTextSetting(key, settings[key]);
|
||||
} else if (isOptionId(key)) {
|
||||
this.setOptionSettingValue(key, settings[key]);
|
||||
/**
|
||||
* Set a subset of all settings in one function call.
|
||||
*
|
||||
* @param settings A (partial) list of settings to set
|
||||
*/
|
||||
setSettings(settings: Partial<AllSettings>) {
|
||||
for (const key of Object.keys(settings)) {
|
||||
if (isFlagId(key)) {
|
||||
this.setFlagEnabled(key, settings[key]);
|
||||
} else if (isNumericId(key)) {
|
||||
this.setNumericSetting(key, settings[key]);
|
||||
} else if (isTextId(key)) {
|
||||
this.setTextSetting(key, settings[key]);
|
||||
} else if (isOptionId(key)) {
|
||||
this.setOptionSettingValue(key, settings[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class SettingNumber<
|
|||
if (!useUrlParams || !urlParams.has(this.id)) {
|
||||
this.number = defaultNumber;
|
||||
} else {
|
||||
const parsedValue = Number.parseInt(urlParams.get(this.id));
|
||||
const parsedValue = Number.parseFloat(urlParams.get(this.id));
|
||||
this.number = Number.isNaN(parsedValue)
|
||||
? defaultNumber
|
||||
: parsedValue;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class SettingOption<
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
defaultOnChangeListener: (changedValue: unknown, setting: SettingBase) => void = () => { /* Do nothing, to be overridden. */ }
|
||||
) {
|
||||
super(id, label, description, [defaultTextValue, defaultTextValue], defaultOnChangeListener);
|
||||
super(id, label, description, defaultTextValue, defaultOnChangeListener);
|
||||
|
||||
this.options = options;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
@ -103,11 +103,22 @@ export class SettingOption<
|
|||
public set selected(value: string) {
|
||||
// A user may not specify the full possible value so we instead use the closest match.
|
||||
// eg ?xxx=H264 would select 'H264 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f'
|
||||
const filteredList = this.options.filter(
|
||||
let filteredList = this.options.filter(
|
||||
(option: string) => option.indexOf(value) !== -1
|
||||
);
|
||||
if (filteredList.length) {
|
||||
this.value = filteredList[0];
|
||||
return;
|
||||
}
|
||||
|
||||
// A user has specified a codec with a fmtp string but this codec + fmtp line isn't available.
|
||||
// in that case, just use the codec
|
||||
filteredList = this.options.filter(
|
||||
(option: string) => option.indexOf(value.split(' ')[0]) !== -1
|
||||
);
|
||||
if (filteredList.length) {
|
||||
this.value = filteredList[0];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Logger } from '../Logger/Logger';
|
||||
import {
|
||||
DataChannelLatencyTestRecord,
|
||||
DataChannelLatencyTestRequest,
|
||||
DataChannelLatencyTestResponse,
|
||||
DataChannelLatencyTestResult,
|
||||
DataChannelLatencyTestSeq,
|
||||
DataChannelLatencyTestTimestamp
|
||||
} from "./DataChannelLatencyTestResults";
|
||||
|
||||
export type DataChannelLatencyTestConfig = {
|
||||
// test duration in milliseconds
|
||||
duration: number;
|
||||
//requests per second
|
||||
rps: number;
|
||||
//request filler size
|
||||
requestSize: number;
|
||||
//response filler size
|
||||
responseSize: number;
|
||||
}
|
||||
|
||||
export type DataChannelLatencyTestSink = (request: DataChannelLatencyTestRequest) => void;
|
||||
export type DataChannelLatencyTestResultCallback = (result: DataChannelLatencyTestResult) => void;
|
||||
|
||||
export class DataChannelLatencyTestController {
|
||||
startTime: DataChannelLatencyTestTimestamp;
|
||||
sink: DataChannelLatencyTestSink;
|
||||
callback: DataChannelLatencyTestResultCallback;
|
||||
records: Map<DataChannelLatencyTestSeq, DataChannelLatencyTestRecord>;
|
||||
seq: DataChannelLatencyTestSeq;
|
||||
interval: NodeJS.Timer;
|
||||
|
||||
constructor(sink: DataChannelLatencyTestSink, callback: DataChannelLatencyTestResultCallback) {
|
||||
this.sink = sink;
|
||||
this.callback = callback;
|
||||
this.records = new Map();
|
||||
this.seq = 0;
|
||||
}
|
||||
|
||||
start(config: DataChannelLatencyTestConfig) {
|
||||
if (this.isRunning()) {
|
||||
return false;
|
||||
}
|
||||
this.startTime = Date.now();
|
||||
this.records.clear();
|
||||
this.interval = setInterval((() => {
|
||||
if (Date.now() - this.startTime >= config.duration) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.sendRequest(config.requestSize, config.responseSize);
|
||||
}
|
||||
}).bind(this), Math.floor(1000/config.rps));
|
||||
return true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
this.callback(this.produceResult());
|
||||
}
|
||||
}
|
||||
|
||||
produceResult(): DataChannelLatencyTestResult {
|
||||
const resultRecords = new Map(this.records);
|
||||
return {
|
||||
records: resultRecords,
|
||||
dataChannelRtt: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => {
|
||||
return acc + (next.playerReceivedTimestamp - next.playerSentTimestamp);
|
||||
}, 0) / this.records.size),
|
||||
playerToStreamerTime: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => {
|
||||
return acc + (next.streamerReceivedTimestamp - next.playerSentTimestamp);
|
||||
}, 0) / this.records.size),
|
||||
streamerToPlayerTime: Math.ceil(Array.from(this.records.values()).reduce((acc, next) => {
|
||||
return acc + (next.playerReceivedTimestamp - next.streamerSentTimestamp);
|
||||
}, 0) / this.records.size),
|
||||
exportLatencyAsCSV: () => {
|
||||
let csv = "Timestamp;RTT;PlayerToStreamer;StreamerToPlayer;\n";
|
||||
resultRecords.forEach((record) => {
|
||||
csv += record.playerSentTimestamp + ";";
|
||||
csv += (record.playerReceivedTimestamp - record.playerSentTimestamp) + ";";
|
||||
csv += (record.streamerReceivedTimestamp - record.playerSentTimestamp) + ";";
|
||||
csv += (record.playerReceivedTimestamp - record.streamerSentTimestamp) + ";";
|
||||
csv += "\n";
|
||||
})
|
||||
return csv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return !!this.interval;
|
||||
}
|
||||
|
||||
receive(response: DataChannelLatencyTestResponse) {
|
||||
if (!this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (!response) {
|
||||
Logger.Error(
|
||||
Logger.GetStackTrace(),
|
||||
"Undefined response from server"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let record = this.records.get(response.Seq);
|
||||
if (record) {
|
||||
record.update(response);
|
||||
}
|
||||
}
|
||||
|
||||
sendRequest(requestSize: number, responseSize: number) {
|
||||
let request = this.createRequest(requestSize, responseSize);
|
||||
let record = new DataChannelLatencyTestRecord(request);
|
||||
this.records.set(record.seq, record);
|
||||
this.sink(request);
|
||||
}
|
||||
|
||||
createRequest(requestSize: number, responseSize: number): DataChannelLatencyTestRequest {
|
||||
return {
|
||||
Seq: this.seq++,
|
||||
FillResponseSize: responseSize,
|
||||
Filler: requestSize ? "A".repeat(requestSize) : ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
/**
|
||||
* Data Channel Latency Test types
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Unix epoch
|
||||
*/
|
||||
export type DataChannelLatencyTestTimestamp = number;
|
||||
|
||||
/**
|
||||
* Sequence number represented by unsigned int
|
||||
*/
|
||||
export type DataChannelLatencyTestSeq = number;
|
||||
|
||||
/**
|
||||
* Request sent to Streamer
|
||||
*/
|
||||
export type DataChannelLatencyTestRequest = {
|
||||
Seq: DataChannelLatencyTestSeq;
|
||||
FillResponseSize: number;
|
||||
Filler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the Streamer
|
||||
*/
|
||||
export type DataChannelLatencyTestResponse = {
|
||||
Seq: DataChannelLatencyTestSeq;
|
||||
Filler: string;
|
||||
ReceivedTimestamp: DataChannelLatencyTestTimestamp;
|
||||
SentTimestamp: DataChannelLatencyTestTimestamp;
|
||||
}
|
||||
|
||||
export type DataChannelLatencyTestResult = {
|
||||
records: Map<DataChannelLatencyTestSeq, DataChannelLatencyTestRecord>
|
||||
dataChannelRtt: number,
|
||||
playerToStreamerTime: number,
|
||||
streamerToPlayerTime: number,
|
||||
exportLatencyAsCSV: () => string
|
||||
}
|
||||
|
||||
export class DataChannelLatencyTestRecord {
|
||||
seq: DataChannelLatencyTestSeq;
|
||||
playerSentTimestamp: DataChannelLatencyTestTimestamp;
|
||||
playerReceivedTimestamp: DataChannelLatencyTestTimestamp;
|
||||
streamerReceivedTimestamp: DataChannelLatencyTestTimestamp;
|
||||
streamerSentTimestamp: DataChannelLatencyTestTimestamp;
|
||||
requestFillerSize: number;
|
||||
responseFillerSize: number;
|
||||
|
||||
constructor(request: DataChannelLatencyTestRequest) {
|
||||
this.seq = request.Seq;
|
||||
this.playerSentTimestamp = Date.now();
|
||||
this.requestFillerSize = request.Filler ? request.Filler.length : 0;
|
||||
}
|
||||
|
||||
update(response: DataChannelLatencyTestResponse) {
|
||||
this.playerReceivedTimestamp = Date.now();
|
||||
this.streamerReceivedTimestamp = response.ReceivedTimestamp;
|
||||
this.streamerSentTimestamp = response.SentTimestamp;
|
||||
this.responseFillerSize = response.Filler ? response.Filler.length : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touch - the activating touch event
|
||||
*/
|
||||
onTouchStart(touch: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || touch.target !== this.videoElementProvider.getVideoElement()) {
|
||||
return;
|
||||
}
|
||||
if (this.fakeTouchFinger == null) {
|
||||
|
|
@ -108,7 +108,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touchEvent - the activating touch event
|
||||
*/
|
||||
onTouchEnd(touchEvent: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
|
||||
return;
|
||||
}
|
||||
const videoElementParent =
|
||||
|
|
@ -144,7 +144,7 @@ export class FakeTouchController implements ITouchController {
|
|||
* @param touchEvent - the activating touch event
|
||||
*/
|
||||
onTouchMove(touchEvent: TouchEvent): void {
|
||||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
if (!this.videoElementProvider.isVideoReady() || this.fakeTouchFinger == null) {
|
||||
return;
|
||||
}
|
||||
const toStreamerHandlers =
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ export class GamePadController {
|
|||
window.requestAnimationFrame
|
||||
).bind(window);
|
||||
const browserWindow = window as Window;
|
||||
|
||||
const onBeforeUnload = (ev: Event) =>
|
||||
this.onBeforeUnload(ev);
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
|
||||
if ('GamepadEvent' in browserWindow) {
|
||||
const onGamePadConnected = (ev: GamepadEvent) =>
|
||||
this.gamePadConnectHandler(ev);
|
||||
|
|
@ -197,7 +202,8 @@ export class GamePadController {
|
|||
} else {
|
||||
toStreamerHandlers.get('GamepadButtonReleased')([
|
||||
controllerIndex,
|
||||
i
|
||||
i,
|
||||
0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +259,14 @@ export class GamePadController {
|
|||
onGamepadDisconnected(controllerIdx: number) {
|
||||
// Default Functionality: Do Nothing
|
||||
}
|
||||
|
||||
onBeforeUnload(ev: Event) {
|
||||
// When a user navigates away from the page, we need to inform UE of all the disconnecting
|
||||
// controllers
|
||||
for(const controller of this.controllers) {
|
||||
this.onGamepadDisconnected(controller.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export class HoveringMouseEvents implements IMouseEvents {
|
|||
if (!this.mouseController.videoElementProvider.isVideoReady()) {
|
||||
return;
|
||||
}
|
||||
Logger.Log(Logger.GetStackTrace(), 'onMouse Up', 6);
|
||||
const coord =
|
||||
this.mouseController.coordinateConverter.normalizeAndQuantizeUnsigned(
|
||||
mouseEvent.offsetX,
|
||||
|
|
@ -102,25 +103,13 @@ export class HoveringMouseEvents implements IMouseEvents {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle the mouse context menu event, sends the mouse data to the UE Instance
|
||||
* Consumes the mouse context event. The UE instance has no equivalent and doesn't need to be informed.
|
||||
* @param mouseEvent - Mouse Event
|
||||
*/
|
||||
handleContextMenu(mouseEvent: MouseEvent) {
|
||||
if (!this.mouseController.videoElementProvider.isVideoReady()) {
|
||||
return;
|
||||
}
|
||||
const coord =
|
||||
this.mouseController.coordinateConverter.normalizeAndQuantizeUnsigned(
|
||||
mouseEvent.offsetX,
|
||||
mouseEvent.offsetY
|
||||
);
|
||||
const toStreamerHandlers =
|
||||
this.mouseController.toStreamerMessagesProvider.toStreamerHandlers;
|
||||
toStreamerHandlers.get('MouseUp')([
|
||||
mouseEvent.button,
|
||||
coord.x,
|
||||
coord.y
|
||||
]);
|
||||
mouseEvent.preventDefault();
|
||||
}
|
||||
|
||||
|
|
@ -177,6 +166,7 @@ export class HoveringMouseEvents implements IMouseEvents {
|
|||
if (!this.mouseController.videoElementProvider.isVideoReady()) {
|
||||
return;
|
||||
}
|
||||
Logger.Log(Logger.GetStackTrace(), 'onMouse press', 6);
|
||||
this.mouseController.pressMouseButtons(
|
||||
mouseEvent.buttons,
|
||||
mouseEvent.offsetX,
|
||||
|
|
@ -192,6 +182,7 @@ export class HoveringMouseEvents implements IMouseEvents {
|
|||
if (!this.mouseController.videoElementProvider.isVideoReady()) {
|
||||
return;
|
||||
}
|
||||
Logger.Log(Logger.GetStackTrace(), 'onMouse release', 6);
|
||||
this.mouseController.releaseMouseButtons(
|
||||
mouseEvent.buttons,
|
||||
mouseEvent.offsetX,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -232,10 +237,7 @@ export class KeyboardController {
|
|||
Logger.Log(Logger.GetStackTrace(), `key up ${keyCode}`, 6);
|
||||
const toStreamerHandlers =
|
||||
this.toStreamerMessagesProvider.toStreamerHandlers;
|
||||
toStreamerHandlers.get('KeyUp')([
|
||||
keyCode,
|
||||
keyboardEvent.repeat ? 1 : 0
|
||||
]);
|
||||
toStreamerHandlers.get('KeyUp')([ keyCode ]);
|
||||
|
||||
if (
|
||||
this.config.isFlagEnabled(Flags.SuppressBrowserKeys) &&
|
||||
|
|
@ -266,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
|
||||
|
|
|
|||
|
|
@ -152,16 +152,15 @@ export class TouchController implements ITouchController {
|
|||
if (!this.videoElementProvider.isVideoReady()) {
|
||||
return;
|
||||
}
|
||||
const videoElementParent =
|
||||
this.videoElementProvider.getVideoParentElement();
|
||||
const offset = this.videoElementProvider.getVideoParentElement().getBoundingClientRect();
|
||||
const toStreamerHandlers =
|
||||
this.toStreamerMessagesProvider.toStreamerHandlers;
|
||||
|
||||
for (let t = 0; t < touches.length; t++) {
|
||||
const numTouches = 1; // the number of touches to be sent this message
|
||||
const touch = touches[t];
|
||||
const x = touch.clientX - videoElementParent.offsetLeft;
|
||||
const y = touch.clientY - videoElementParent.offsetTop;
|
||||
const x = touch.clientX - offset.left;
|
||||
const y = touch.clientY - offset.top;
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
`F${this.fingerIds.get(touch.identifier)}=(${x}, ${y})`,
|
||||
|
|
@ -179,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;
|
||||
|
|
@ -199,7 +198,7 @@ export class TouchController implements ITouchController {
|
|||
coord.x,
|
||||
coord.y,
|
||||
this.fingerIds.get(touch.identifier),
|
||||
this.maxByteValue * touch.force,
|
||||
this.maxByteValue * (touch.force > 0 ? touch.force : 1),
|
||||
coord.inRange ? 1 : 0
|
||||
]);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export class AggregatedStats {
|
|||
inboundAudioStats: InboundAudioStats;
|
||||
lastVideoStats: InboundVideoStats;
|
||||
lastAudioStats: InboundAudioStats;
|
||||
candidatePair: CandidatePairStats;
|
||||
candidatePairs: Array<CandidatePairStats>;
|
||||
DataChannelStats: DataChannelStats;
|
||||
localCandidates: Array<CandidateStat>;
|
||||
remoteCandidates: Array<CandidateStat>;
|
||||
|
|
@ -37,7 +37,6 @@ export class AggregatedStats {
|
|||
constructor() {
|
||||
this.inboundVideoStats = new InboundVideoStats();
|
||||
this.inboundAudioStats = new InboundAudioStats();
|
||||
this.candidatePair = new CandidatePairStats();
|
||||
this.DataChannelStats = new DataChannelStats();
|
||||
this.outBoundVideoStats = new OutBoundVideoStats();
|
||||
this.sessionStats = new SessionStats();
|
||||
|
|
@ -52,6 +51,7 @@ export class AggregatedStats {
|
|||
processStats(rtcStatsReport: RTCStatsReport) {
|
||||
this.localCandidates = new Array<CandidateStat>();
|
||||
this.remoteCandidates = new Array<CandidateStat>();
|
||||
this.candidatePairs = new Array<CandidatePairStats>();
|
||||
|
||||
rtcStatsReport.forEach((stat) => {
|
||||
const type: RTCStatsTypePS = stat.type;
|
||||
|
|
@ -120,16 +120,10 @@ export class AggregatedStats {
|
|||
* @param stat - the stats coming in from ice candidates
|
||||
*/
|
||||
handleCandidatePair(stat: CandidatePairStats) {
|
||||
this.candidatePair.bytesReceived = stat.bytesReceived;
|
||||
this.candidatePair.bytesSent = stat.bytesSent;
|
||||
this.candidatePair.localCandidateId = stat.localCandidateId;
|
||||
this.candidatePair.remoteCandidateId = stat.remoteCandidateId;
|
||||
this.candidatePair.nominated = stat.nominated;
|
||||
this.candidatePair.readable = stat.readable;
|
||||
this.candidatePair.selected = stat.selected;
|
||||
this.candidatePair.writable = stat.writable;
|
||||
this.candidatePair.state = stat.state;
|
||||
this.candidatePair.currentRoundTripTime = stat.currentRoundTripTime;
|
||||
|
||||
// Add the candidate pair to the candidate pair array
|
||||
this.candidatePairs.push(stat)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,6 +156,8 @@ export class AggregatedStats {
|
|||
localCandidate.protocol = stat.protocol;
|
||||
localCandidate.candidateType = stat.candidateType;
|
||||
localCandidate.id = stat.id;
|
||||
localCandidate.relayProtocol = stat.relayProtocol;
|
||||
localCandidate.transportId = stat.transportId;
|
||||
this.localCandidates.push(localCandidate);
|
||||
}
|
||||
|
||||
|
|
@ -171,12 +167,14 @@ export class AggregatedStats {
|
|||
*/
|
||||
handleRemoteCandidate(stat: CandidateStat) {
|
||||
const RemoteCandidate = new CandidateStat();
|
||||
RemoteCandidate.label = 'local-candidate';
|
||||
RemoteCandidate.label = 'remote-candidate';
|
||||
RemoteCandidate.address = stat.address;
|
||||
RemoteCandidate.port = stat.port;
|
||||
RemoteCandidate.protocol = stat.protocol;
|
||||
RemoteCandidate.id = stat.id;
|
||||
RemoteCandidate.candidateType = stat.candidateType;
|
||||
RemoteCandidate.relayProtocol = stat.relayProtocol;
|
||||
RemoteCandidate.transportId = stat.transportId
|
||||
this.remoteCandidates.push(RemoteCandidate);
|
||||
}
|
||||
|
||||
|
|
@ -308,4 +306,12 @@ export class AggregatedStats {
|
|||
isNumber(value: unknown): boolean {
|
||||
return typeof value === 'number' && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return the active candidate pair
|
||||
* @returns The candidate pair that is currently receiving data
|
||||
*/
|
||||
public getActiveCandidatePair(): CandidatePairStats | null {
|
||||
return this.candidatePairs.find((candidatePair) => candidatePair.bytesReceived > 0, null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,19 @@
|
|||
export class CandidatePairStats {
|
||||
bytesReceived: number;
|
||||
bytesSent: number;
|
||||
currentRoundTripTime: number;
|
||||
id: string;
|
||||
lastPacketReceivedTimestamp: number;
|
||||
lastPacketSentTimestamp: number;
|
||||
localCandidateId: string;
|
||||
remoteCandidateId: string;
|
||||
nominated: boolean;
|
||||
priority: number;
|
||||
readable: boolean;
|
||||
writable: boolean;
|
||||
remoteCandidateId: string;
|
||||
selected: boolean;
|
||||
state: string;
|
||||
currentRoundTripTime: number;
|
||||
timestamp: number;
|
||||
transportId: string;
|
||||
type: string;
|
||||
writable: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
* ICE Candidate Stat collected from the RTC Stats Report
|
||||
*/
|
||||
export class CandidateStat {
|
||||
label: string;
|
||||
id: string;
|
||||
address: string;
|
||||
candidateType: string;
|
||||
id: string;
|
||||
label: string;
|
||||
port: number;
|
||||
protocol: 'tcp' | 'udp';
|
||||
relayProtocol: 'tcp' | 'udp' | 'tls';
|
||||
transportId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Logger } from '../Logger/Logger';
|
|||
import { Config, OptionParameters, Flags } from '../Config/Config';
|
||||
import { AggregatedStats } from './AggregatedStats';
|
||||
import { parseRtpParameters, splitSections } from 'sdp';
|
||||
import { RTCUtils } from '../Util/RTCUtils';
|
||||
|
||||
/**
|
||||
* Handles the Peer Connection
|
||||
|
|
@ -179,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(
|
||||
|
|
@ -207,16 +208,13 @@ export class PeerConnectionController {
|
|||
* @returns A modified Session Descriptor
|
||||
*/
|
||||
mungeSDP(sdp: string, useMic: boolean) {
|
||||
const mungedSDP = sdp;
|
||||
mungedSDP.replace(
|
||||
let mungedSDP = sdp.replace(
|
||||
/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm,
|
||||
'$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n'
|
||||
);
|
||||
|
||||
let audioSDP = '';
|
||||
|
||||
// set max bitrate to highest bitrate Opus supports
|
||||
audioSDP += 'maxaveragebitrate=510000;';
|
||||
let audioSDP = 'maxaveragebitrate=510000;';
|
||||
|
||||
if (useMic) {
|
||||
// set the max capture rate to 48khz (so we can send high quality audio from mic)
|
||||
|
|
@ -232,7 +230,7 @@ export class PeerConnectionController {
|
|||
audioSDP += 'useinbandfec=1';
|
||||
|
||||
// We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
|
||||
mungedSDP.replace('useinbandfec=1', audioSDP);
|
||||
mungedSDP = mungedSDP.replace('useinbandfec=1', audioSDP);
|
||||
|
||||
return mungedSDP;
|
||||
}
|
||||
|
|
@ -375,7 +373,9 @@ export class PeerConnectionController {
|
|||
transceiver &&
|
||||
transceiver.receiver &&
|
||||
transceiver.receiver.track &&
|
||||
transceiver.receiver.track.kind === 'video'
|
||||
transceiver.receiver.track.kind === 'video' &&
|
||||
// As of 06/2023, FireFox has added RTCRtpReceiver.getCapabilities, but hasn't added the ability to set codec preferences
|
||||
transceiver.setCodecPreferences
|
||||
) {
|
||||
const preferredRTPCodec = this.preferredCodec.split(' ');
|
||||
const codecs = [
|
||||
|
|
@ -426,18 +426,16 @@ export class PeerConnectionController {
|
|||
});
|
||||
} else {
|
||||
// set the audio options based on mic usage
|
||||
const audioOptions = useMic
|
||||
? {
|
||||
autoGainControl: false,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
latency: 0,
|
||||
noiseSuppression: false,
|
||||
sampleRate: 48000,
|
||||
sampleSize: 16,
|
||||
volume: 1.0
|
||||
}
|
||||
: false;
|
||||
const audioOptions = {
|
||||
autoGainControl: false,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
latency: 0,
|
||||
noiseSuppression: false,
|
||||
sampleRate: 48000,
|
||||
sampleSize: 16,
|
||||
volume: 1.0
|
||||
}
|
||||
|
||||
// set the media send options
|
||||
const mediaSendOptions: MediaStreamConstraints = {
|
||||
|
|
@ -452,12 +450,7 @@ export class PeerConnectionController {
|
|||
if (stream) {
|
||||
if (hasTransceivers) {
|
||||
for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
|
||||
if (
|
||||
transceiver &&
|
||||
transceiver.receiver &&
|
||||
transceiver.receiver.track &&
|
||||
transceiver.receiver.track.kind === 'audio'
|
||||
) {
|
||||
if (RTCUtils.canTransceiverReceiveAudio(transceiver)) {
|
||||
for (const track of stream.getTracks()) {
|
||||
if (track.kind && track.kind == 'audio') {
|
||||
transceiver.sender.replaceTrack(track);
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
]
|
||||
|
|
|
|||
|
|
@ -25,10 +25,25 @@ import {
|
|||
WebRtcConnectingEvent,
|
||||
WebRtcDisconnectedEvent,
|
||||
WebRtcFailedEvent,
|
||||
WebRtcSdpEvent
|
||||
WebRtcSdpEvent,
|
||||
DataChannelLatencyTestResponseEvent,
|
||||
DataChannelLatencyTestResultEvent,
|
||||
PlayerCountEvent,
|
||||
WebRtcTCPRelayDetectedEvent
|
||||
} from '../Util/EventEmitter';
|
||||
import { MessageOnScreenKeyboard } from '../WebSockets/MessageReceive';
|
||||
import { WebXRController } from '../WebXR/WebXRController';
|
||||
import { MessageDirection } from '../UeInstanceMessage/StreamMessageController';
|
||||
import {
|
||||
DataChannelLatencyTestConfig,
|
||||
DataChannelLatencyTestController
|
||||
} from "../DataChannel/DataChannelLatencyTestController";
|
||||
import {
|
||||
DataChannelLatencyTestResponse,
|
||||
DataChannelLatencyTestResult
|
||||
} from "../DataChannel/DataChannelLatencyTestResults";
|
||||
import { RTCUtils } from '../Util/RTCUtils';
|
||||
|
||||
|
||||
export interface PixelStreamingOverrides {
|
||||
/** The DOM elment where Pixel Streaming video and user input event handlers are attached to.
|
||||
|
|
@ -47,6 +62,8 @@ export interface PixelStreamingOverrides {
|
|||
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.
|
||||
|
|
@ -55,7 +72,6 @@ export class PixelStreaming {
|
|||
|
||||
private _videoElementParent: HTMLElement;
|
||||
|
||||
_showActionOrErrorOnDisconnect = true;
|
||||
private allowConsoleCommands = false;
|
||||
|
||||
private onScreenKeyboardHelper: OnScreenKeyboard;
|
||||
|
|
@ -102,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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -338,7 +363,7 @@ export class PixelStreaming {
|
|||
*/
|
||||
public reconnect() {
|
||||
this._eventEmitter.dispatchEvent(new StreamReconnectEvent());
|
||||
this._webRtcController.restartStreamAutomatically();
|
||||
this._webRtcController.tryReconnect("Reconnecting...");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -369,12 +394,62 @@ export class PixelStreaming {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will unmute the microphone track which is sent to Unreal Engine.
|
||||
* By default, will only unmute an existing mic track.
|
||||
*
|
||||
* @param forceEnable Can be used for cases when this object wasn't initialized with a mic track.
|
||||
* If this parameter is true, the connection will be restarted with a microphone.
|
||||
* Warning: this takes some time, as a full renegotiation and reconnection will happen.
|
||||
*/
|
||||
public unmuteMicrophone(forceEnable = false) : void {
|
||||
// If there's an existing mic track, we just set muted state
|
||||
if (this.config.isFlagEnabled('UseMic')) {
|
||||
this.setMicrophoneMuted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no pre-existing mic track, and caller is ok with full reset, we enable and reset
|
||||
if (forceEnable) {
|
||||
this.config.setFlagEnabled("UseMic", true);
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we prefer not to force a reconnection, just warn the user that this operation didn't happen
|
||||
Logger.Warning(
|
||||
Logger.GetStackTrace(),
|
||||
'Trying to unmute mic, but PixelStreaming was initialized with no microphone track. Call with forceEnable == true to re-connect with a mic track.'
|
||||
);
|
||||
}
|
||||
|
||||
public muteMicrophone() : void {
|
||||
if (this.config.isFlagEnabled('UseMic')) {
|
||||
this.setMicrophoneMuted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there wasn't a mic track, just let user know there's nothing to mute
|
||||
Logger.Info(
|
||||
Logger.GetStackTrace(),
|
||||
'Trying to mute mic, but PixelStreaming has no microphone track, so sending sound is already disabled.'
|
||||
);
|
||||
}
|
||||
|
||||
private setMicrophoneMuted(mute: boolean) : void
|
||||
{
|
||||
for (const transceiver of this._webRtcController?.peerConnectionController?.peerConnection?.getTransceivers() ?? []) {
|
||||
if (RTCUtils.canTransceiverSendAudio(transceiver)) {
|
||||
transceiver.sender.track.enabled = !mute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event on auto connecting
|
||||
*/
|
||||
_onWebRtcAutoConnect() {
|
||||
this._eventEmitter.dispatchEvent(new WebRtcAutoConnectEvent());
|
||||
this._showActionOrErrorOnDisconnect = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -394,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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -459,6 +520,12 @@ export class PixelStreaming {
|
|||
);
|
||||
}
|
||||
|
||||
_onDataChannelLatencyTestResponse(response: DataChannelLatencyTestResponse) {
|
||||
this._eventEmitter.dispatchEvent(
|
||||
new DataChannelLatencyTestResponseEvent({ response })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up functionality to happen when receiving video statistics
|
||||
* @param videoStats - video statistics as a aggregate stats object
|
||||
|
|
@ -510,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
|
||||
);
|
||||
|
||||
|
|
@ -523,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
|
||||
);
|
||||
}
|
||||
|
|
@ -531,20 +602,20 @@ export class PixelStreaming {
|
|||
this.config.setNumericSetting(
|
||||
NumericParameters.WebRTCMinBitrate,
|
||||
(useUrlParams && urlParams.has(NumericParameters.WebRTCMinBitrate))
|
||||
? Number.parseInt(urlParams.get(NumericParameters.WebRTCMinBitrate)) / 1000 /* bps to kbps */
|
||||
: 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)) / 1000 /* bps to kbps */
|
||||
: 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
|
||||
);
|
||||
}
|
||||
|
|
@ -561,6 +632,34 @@ export class PixelStreaming {
|
|||
);
|
||||
}
|
||||
|
||||
_onPlayerCount(playerCount: number) {
|
||||
this._eventEmitter.dispatchEvent(
|
||||
new PlayerCountEvent({ count: playerCount })
|
||||
);
|
||||
}
|
||||
|
||||
// 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!
|
||||
|
|
@ -574,6 +673,30 @@ export class PixelStreaming {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a data channel latency test.
|
||||
* NOTE: There are plans to refactor all request* functions. Expect changes if you use this!
|
||||
*/
|
||||
public requestDataChannelLatencyTest(config: DataChannelLatencyTestConfig) {
|
||||
if (!this._webRtcController.videoPlayer.isVideoReady()) {
|
||||
return false;
|
||||
}
|
||||
if (!this._dataChannelLatencyTestController) {
|
||||
this._dataChannelLatencyTestController = new DataChannelLatencyTestController(
|
||||
this._webRtcController.sendDataChannelLatencyTest.bind(this._webRtcController),
|
||||
(result: DataChannelLatencyTestResult) => {
|
||||
this._eventEmitter.dispatchEvent(new DataChannelLatencyTestResultEvent( { result }))
|
||||
});
|
||||
this.addEventListener(
|
||||
"dataChannelLatencyTestResponse",
|
||||
({data: {response} }) => {
|
||||
this._dataChannelLatencyTestController.receive(response);
|
||||
}
|
||||
)
|
||||
}
|
||||
return this._dataChannelLatencyTestController.start(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for the UE application to show FPS counter.
|
||||
* NOTE: There are plans to refactor all request* functions. Expect changes if you use this!
|
||||
|
|
@ -725,4 +848,37 @@ export class PixelStreaming {
|
|||
public get webXrController() {
|
||||
return this._webXrController;
|
||||
}
|
||||
|
||||
public registerMessageHandler(name: string, direction: MessageDirection, handler?: (data: ArrayBuffer | Array<number | string>) => void) {
|
||||
if(direction === MessageDirection.FromStreamer && typeof handler === 'undefined') {
|
||||
Logger.Warning(Logger.GetStackTrace(), `Unable to register an undefined handler for ${name}`)
|
||||
return;
|
||||
}
|
||||
|
||||
if(direction === MessageDirection.ToStreamer && typeof handler === 'undefined') {
|
||||
this._webRtcController.streamMessageController.registerMessageHandler(
|
||||
direction,
|
||||
name,
|
||||
(data: Array<number | string>) =>
|
||||
this._webRtcController.sendMessageController.sendMessageToStreamer(
|
||||
name,
|
||||
data
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this._webRtcController.streamMessageController.registerMessageHandler(
|
||||
direction,
|
||||
name,
|
||||
(data: ArrayBuffer) => handler(data)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get toStreamerHandlers() {
|
||||
return this._webRtcController.streamMessageController.toStreamerHandlers;
|
||||
}
|
||||
|
||||
public isReconnecting() {
|
||||
return this._webRtcController.isReconnecting;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { DataChannelSender } from '../DataChannel/DataChannelSender';
|
||||
import { Logger } from '../Logger/Logger';
|
||||
import { StreamMessageController } from './StreamMessageController';
|
||||
|
||||
export class SendDescriptorController {
|
||||
toStreamerMessagesMapProvider: StreamMessageController;
|
||||
dataChannelSender: DataChannelSender;
|
||||
|
||||
constructor(
|
||||
dataChannelSender: DataChannelSender,
|
||||
toStreamerMessagesMapProvider: StreamMessageController
|
||||
) {
|
||||
this.dataChannelSender = dataChannelSender;
|
||||
this.toStreamerMessagesMapProvider = toStreamerMessagesMapProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Latency Test to the UE Instance
|
||||
* @param descriptor - the descriptor for a latency test
|
||||
*/
|
||||
sendLatencyTest(descriptor: object) {
|
||||
this.sendDescriptor('LatencyTest', descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Latency Test to the UE Instance
|
||||
* @param descriptor - the descriptor for a command
|
||||
*/
|
||||
emitCommand(descriptor: object) {
|
||||
this.sendDescriptor('Command', descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Latency Test to the UE Instance
|
||||
* @param descriptor - the descriptor for a UI Interaction
|
||||
*/
|
||||
emitUIInteraction(descriptor: object | string) {
|
||||
this.sendDescriptor('UIInteraction', descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Descriptor to the UE Instances
|
||||
* @param messageType - UE Message Type
|
||||
* @param descriptor - Descriptor Message as JSON
|
||||
*/
|
||||
sendDescriptor(messageType: string, descriptor: object | string) {
|
||||
// Convert the descriptor object into a JSON string.
|
||||
const descriptorAsString = JSON.stringify(descriptor);
|
||||
const toStreamerMessages =
|
||||
this.toStreamerMessagesMapProvider.toStreamerMessages;
|
||||
const messageFormat = toStreamerMessages.getFromKey(messageType);
|
||||
if (messageFormat === undefined) {
|
||||
Logger.Error(
|
||||
Logger.GetStackTrace(),
|
||||
`Attempted to emit descriptor with message type: ${messageType}, but the frontend hasn't been configured to send such a message. Check you've added the message type in your cpp`
|
||||
);
|
||||
}
|
||||
|
||||
Logger.Log(Logger.GetStackTrace(), 'Sending: ' + descriptor, 6);
|
||||
// Add the UTF-16 JSON string to the array byte buffer, going two bytes at
|
||||
// a time.
|
||||
const data = new DataView(
|
||||
new ArrayBuffer(1 + 2 + 2 * descriptorAsString.length)
|
||||
);
|
||||
let byteIdx = 0;
|
||||
data.setUint8(byteIdx, messageFormat.id);
|
||||
byteIdx++;
|
||||
data.setUint16(byteIdx, descriptorAsString.length, true);
|
||||
byteIdx += 2;
|
||||
for (let i = 0; i < descriptorAsString.length; i++) {
|
||||
data.setUint16(byteIdx, descriptorAsString.charCodeAt(i), true);
|
||||
byteIdx += 2;
|
||||
}
|
||||
|
||||
if (!this.dataChannelSender.canSend()) {
|
||||
Logger.Info(
|
||||
Logger.GetStackTrace(),
|
||||
`Data channel cannot send yet, skipping sending descriptor message: ${messageType} - ${descriptorAsString}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataChannelSender.sendData(data.buffer);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,14 +26,14 @@ export class SendMessageController {
|
|||
* @param messageData - the message data we are sending over the data channel
|
||||
* @returns - nil
|
||||
*/
|
||||
sendMessageToStreamer(messageType: string, messageData?: Array<number>) {
|
||||
sendMessageToStreamer(messageType: string, messageData?: Array<number | string>) {
|
||||
if (messageData === undefined) {
|
||||
messageData = [];
|
||||
}
|
||||
|
||||
const toStreamerMessages =
|
||||
this.toStreamerMessagesMapProvider.toStreamerMessages;
|
||||
const messageFormat = toStreamerMessages.getFromKey(messageType);
|
||||
const messageFormat = toStreamerMessages.get(messageType);
|
||||
if (messageFormat === undefined) {
|
||||
Logger.Error(
|
||||
Logger.GetStackTrace(),
|
||||
|
|
@ -42,39 +42,100 @@ export class SendMessageController {
|
|||
return;
|
||||
}
|
||||
|
||||
const data = new DataView(
|
||||
new ArrayBuffer(messageFormat.byteLength + 1)
|
||||
);
|
||||
data.setUint8(0, messageFormat.id);
|
||||
let byteOffset = 1;
|
||||
if(messageFormat.structure && messageData && messageFormat.structure.length !== messageData.length) {
|
||||
Logger.Error(
|
||||
Logger.GetStackTrace(),
|
||||
`Provided message data doesn't match expected layout. Expected [ ${messageFormat.structure.map((element: string) => {
|
||||
switch (element) {
|
||||
case 'uint8':
|
||||
case 'uint16':
|
||||
case 'int16':
|
||||
case 'float':
|
||||
case 'double':
|
||||
return 'number';
|
||||
case 'string':
|
||||
return 'string';
|
||||
}
|
||||
}).toString() } ] but received [ ${messageData.map((element: number | string) => typeof element).toString()} ]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
messageData.forEach((element: number, idx: number) => {
|
||||
let byteLength = 0;
|
||||
const textEncoder = new TextEncoder();
|
||||
// One loop to calculate the length in bytes of all of the provided data
|
||||
messageData.forEach((element: number | string, idx: number) => {
|
||||
const type = messageFormat.structure[idx];
|
||||
switch (type) {
|
||||
case 'uint8':
|
||||
data.setUint8(byteOffset, element);
|
||||
byteLength += 1;
|
||||
break;
|
||||
|
||||
case 'uint16':
|
||||
byteLength += 2;
|
||||
break;
|
||||
|
||||
case 'int16':
|
||||
byteLength += 2;
|
||||
break;
|
||||
|
||||
case 'float':
|
||||
byteLength += 4;
|
||||
break;
|
||||
|
||||
case 'double':
|
||||
byteLength += 8;
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
// 2 bytes for string length
|
||||
byteLength += 2;
|
||||
// 2 bytes per characters
|
||||
byteLength += 2 * textEncoder.encode(element as string).length;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const data = new DataView(new ArrayBuffer(byteLength + 1));
|
||||
data.setUint8(0, messageFormat.id);
|
||||
let byteOffset = 1;
|
||||
|
||||
messageData.forEach((element: number | string, idx: number) => {
|
||||
const type = messageFormat.structure[idx];
|
||||
switch (type) {
|
||||
case 'uint8':
|
||||
data.setUint8(byteOffset, element as number);
|
||||
byteOffset += 1;
|
||||
break;
|
||||
|
||||
case 'uint16':
|
||||
data.setUint16(byteOffset, element, true);
|
||||
data.setUint16(byteOffset, element as number, true);
|
||||
byteOffset += 2;
|
||||
break;
|
||||
|
||||
case 'int16':
|
||||
data.setInt16(byteOffset, element, true);
|
||||
data.setInt16(byteOffset, element as number, true);
|
||||
byteOffset += 2;
|
||||
break;
|
||||
|
||||
case 'float':
|
||||
data.setFloat32(byteOffset, element, true);
|
||||
data.setFloat32(byteOffset, element as number, true);
|
||||
byteOffset += 4;
|
||||
break;
|
||||
|
||||
case 'double':
|
||||
data.setFloat64(byteOffset, element, true);
|
||||
data.setFloat64(byteOffset, element as number, true);
|
||||
byteOffset += 8;
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
data.setUint16(byteOffset, (element as string).length, true);
|
||||
byteOffset += 2;
|
||||
for (let i = 0; i < (element as string).length; i++) {
|
||||
data.setUint16(byteOffset, (element as string).charCodeAt(i), true);
|
||||
byteOffset += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -86,8 +147,8 @@ export class SendMessageController {
|
|||
)}`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
this.dataChannelSender.sendData(data.buffer);
|
||||
}
|
||||
|
||||
this.dataChannelSender.sendData(data.buffer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,31 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { TwoWayMap } from './TwoWayMap';
|
||||
import { Logger } from '../Logger/Logger';
|
||||
|
||||
export class ToStreamerMessage {
|
||||
id: number;
|
||||
byteLength: number;
|
||||
structure?: Array<string>;
|
||||
}
|
||||
|
||||
export class StreamMessageController {
|
||||
toStreamerHandlers: Map<
|
||||
string,
|
||||
(messageData?: Array<number> | undefined) => void
|
||||
(messageData?: Array<number | string> | undefined) => void
|
||||
>;
|
||||
fromStreamerHandlers: Map<
|
||||
string,
|
||||
(messageType: string, messageData?: ArrayBuffer | undefined) => void
|
||||
>;
|
||||
// Type Format
|
||||
toStreamerMessages: TwoWayMap<string, ToStreamerMessage>;
|
||||
// Type ID
|
||||
fromStreamerMessages: TwoWayMap<string, number>;
|
||||
// Type Format
|
||||
toStreamerMessages: Map<string, ToStreamerMessage>;
|
||||
// ID Type
|
||||
fromStreamerMessages: Map<number, string>;
|
||||
|
||||
constructor() {
|
||||
this.toStreamerHandlers = new Map();
|
||||
this.fromStreamerHandlers = new Map();
|
||||
this.toStreamerMessages = new TwoWayMap();
|
||||
this.fromStreamerMessages = new TwoWayMap();
|
||||
this.toStreamerMessages = new Map();
|
||||
this.fromStreamerMessages = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,190 +35,166 @@ export class StreamMessageController {
|
|||
/*
|
||||
* Control Messages. Range = 0..49.
|
||||
*/
|
||||
this.toStreamerMessages.add('IFrameRequest', {
|
||||
this.toStreamerMessages.set('IFrameRequest', {
|
||||
id: 0,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('RequestQualityControl', {
|
||||
this.toStreamerMessages.set('RequestQualityControl', {
|
||||
id: 1,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('FpsRequest', {
|
||||
this.toStreamerMessages.set('FpsRequest', {
|
||||
id: 2,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('AverageBitrateRequest', {
|
||||
this.toStreamerMessages.set('AverageBitrateRequest', {
|
||||
id: 3,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('StartStreaming', {
|
||||
this.toStreamerMessages.set('StartStreaming', {
|
||||
id: 4,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('StopStreaming', {
|
||||
this.toStreamerMessages.set('StopStreaming', {
|
||||
id: 5,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('LatencyTest', {
|
||||
this.toStreamerMessages.set('LatencyTest', {
|
||||
id: 6,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
structure: ['string']
|
||||
});
|
||||
this.toStreamerMessages.add('RequestInitialSettings', {
|
||||
this.toStreamerMessages.set('RequestInitialSettings', {
|
||||
id: 7,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('TestEcho', {
|
||||
this.toStreamerMessages.set('TestEcho', {
|
||||
id: 8,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.set('DataChannelLatencyTest', {
|
||||
id: 9,
|
||||
structure: []
|
||||
});
|
||||
/*
|
||||
* Input Messages. Range = 50..89.
|
||||
*/
|
||||
// Generic Input Messages. Range = 50..59.
|
||||
this.toStreamerMessages.add('UIInteraction', {
|
||||
this.toStreamerMessages.set('UIInteraction', {
|
||||
id: 50,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
structure: ['string']
|
||||
});
|
||||
this.toStreamerMessages.add('Command', {
|
||||
this.toStreamerMessages.set('Command', {
|
||||
id: 51,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
structure: ['string']
|
||||
});
|
||||
// Keyboard Input Message. Range = 60..69.
|
||||
this.toStreamerMessages.add('KeyDown', {
|
||||
this.toStreamerMessages.set('KeyDown', {
|
||||
id: 60,
|
||||
byteLength: 2,
|
||||
// keyCode isRepeat
|
||||
structure: ['uint8', 'uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('KeyUp', {
|
||||
this.toStreamerMessages.set('KeyUp', {
|
||||
id: 61,
|
||||
byteLength: 1,
|
||||
// keyCode
|
||||
structure: ['uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('KeyPress', {
|
||||
this.toStreamerMessages.set('KeyPress', {
|
||||
id: 62,
|
||||
byteLength: 2,
|
||||
// charcode
|
||||
structure: ['uint16']
|
||||
});
|
||||
// Mouse Input Messages. Range = 70..79.
|
||||
this.toStreamerMessages.add('MouseEnter', {
|
||||
this.toStreamerMessages.set('MouseEnter', {
|
||||
id: 70,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('MouseLeave', {
|
||||
this.toStreamerMessages.set('MouseLeave', {
|
||||
id: 71,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('MouseDown', {
|
||||
this.toStreamerMessages.set('MouseDown', {
|
||||
id: 72,
|
||||
byteLength: 5,
|
||||
// button x y
|
||||
structure: ['uint8', 'uint16', 'uint16']
|
||||
});
|
||||
this.toStreamerMessages.add('MouseUp', {
|
||||
this.toStreamerMessages.set('MouseUp', {
|
||||
id: 73,
|
||||
byteLength: 5,
|
||||
// button x y
|
||||
structure: ['uint8', 'uint16', 'uint16']
|
||||
});
|
||||
this.toStreamerMessages.add('MouseMove', {
|
||||
this.toStreamerMessages.set('MouseMove', {
|
||||
id: 74,
|
||||
byteLength: 8,
|
||||
// x y deltaX deltaY
|
||||
structure: ['uint16', 'uint16', 'int16', 'int16']
|
||||
});
|
||||
this.toStreamerMessages.add('MouseWheel', {
|
||||
this.toStreamerMessages.set('MouseWheel', {
|
||||
id: 75,
|
||||
byteLength: 6,
|
||||
// delta x y
|
||||
structure: ['int16', 'uint16', 'uint16']
|
||||
});
|
||||
this.toStreamerMessages.add('MouseDouble', {
|
||||
this.toStreamerMessages.set('MouseDouble', {
|
||||
id: 76,
|
||||
byteLength: 5,
|
||||
// button x y
|
||||
structure: ['uint8', 'uint16', 'uint16']
|
||||
});
|
||||
// Touch Input Messages. Range = 80..89.
|
||||
this.toStreamerMessages.add('TouchStart', {
|
||||
this.toStreamerMessages.set('TouchStart', {
|
||||
id: 80,
|
||||
byteLength: 8,
|
||||
// numtouches(1) x y idx force valid
|
||||
structure: ['uint8', 'uint16', 'uint16', 'uint8', 'uint8', 'uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('TouchEnd', {
|
||||
this.toStreamerMessages.set('TouchEnd', {
|
||||
id: 81,
|
||||
byteLength: 8,
|
||||
// numtouches(1) x y idx force valid
|
||||
structure: ['uint8', 'uint16', 'uint16', 'uint8', 'uint8', 'uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('TouchMove', {
|
||||
this.toStreamerMessages.set('TouchMove', {
|
||||
id: 82,
|
||||
byteLength: 8,
|
||||
// numtouches(1) x y idx force valid
|
||||
structure: ['uint8', 'uint16', 'uint16', 'uint8', 'uint8', 'uint8']
|
||||
});
|
||||
// Gamepad Input Messages. Range = 90..99
|
||||
this.toStreamerMessages.add('GamepadConnected', {
|
||||
this.toStreamerMessages.set('GamepadConnected', {
|
||||
id: 93,
|
||||
byteLength: 0,
|
||||
structure: []
|
||||
});
|
||||
this.toStreamerMessages.add('GamepadButtonPressed', {
|
||||
this.toStreamerMessages.set('GamepadButtonPressed', {
|
||||
id: 90,
|
||||
byteLength: 3,
|
||||
// ctrlerId button isRepeat
|
||||
// ctrlerId button isRepeat
|
||||
structure: ['uint8', 'uint8', 'uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('GamepadButtonReleased', {
|
||||
this.toStreamerMessages.set('GamepadButtonReleased', {
|
||||
id: 91,
|
||||
byteLength: 3,
|
||||
// ctrlerId button isRepeat(0)
|
||||
// ctrlerId button isRepeat(0)
|
||||
structure: ['uint8', 'uint8', 'uint8']
|
||||
});
|
||||
this.toStreamerMessages.add('GamepadAnalog', {
|
||||
this.toStreamerMessages.set('GamepadAnalog', {
|
||||
id: 92,
|
||||
byteLength: 10,
|
||||
// ctrlerId button analogValue
|
||||
// ctrlerId button analogValue
|
||||
structure: ['uint8', 'uint8', 'double']
|
||||
});
|
||||
this.toStreamerMessages.add('GamepadDisconnected', {
|
||||
this.toStreamerMessages.set('GamepadDisconnected', {
|
||||
id: 94,
|
||||
byteLength: 1,
|
||||
// ctrlerId
|
||||
structure: ['uint8']
|
||||
});
|
||||
|
||||
this.fromStreamerMessages.add('QualityControlOwnership', 0);
|
||||
this.fromStreamerMessages.add('Response', 1);
|
||||
this.fromStreamerMessages.add('Command', 2);
|
||||
this.fromStreamerMessages.add('FreezeFrame', 3);
|
||||
this.fromStreamerMessages.add('UnfreezeFrame', 4);
|
||||
this.fromStreamerMessages.add('VideoEncoderAvgQP', 5);
|
||||
this.fromStreamerMessages.add('LatencyTest', 6);
|
||||
this.fromStreamerMessages.add('InitialSettings', 7);
|
||||
this.fromStreamerMessages.add('FileExtension', 8);
|
||||
this.fromStreamerMessages.add('FileMimeType', 9);
|
||||
this.fromStreamerMessages.add('FileContents', 10);
|
||||
this.fromStreamerMessages.add('TestEcho', 11);
|
||||
this.fromStreamerMessages.add('InputControlOwnership', 12);
|
||||
this.fromStreamerMessages.add('GamepadResponse', 13);
|
||||
this.fromStreamerMessages.add('Protocol', 255);
|
||||
this.fromStreamerMessages.set(0, 'QualityControlOwnership');
|
||||
this.fromStreamerMessages.set(1, 'Response');
|
||||
this.fromStreamerMessages.set(2, 'Command');
|
||||
this.fromStreamerMessages.set(3, 'FreezeFrame');
|
||||
this.fromStreamerMessages.set(4, 'UnfreezeFrame');
|
||||
this.fromStreamerMessages.set(5, 'VideoEncoderAvgQP');
|
||||
this.fromStreamerMessages.set(6, 'LatencyTest');
|
||||
this.fromStreamerMessages.set(7, 'InitialSettings');
|
||||
this.fromStreamerMessages.set(8, 'FileExtension');
|
||||
this.fromStreamerMessages.set(9, 'FileMimeType');
|
||||
this.fromStreamerMessages.set(10, 'FileContents');
|
||||
this.fromStreamerMessages.set(11, 'TestEcho');
|
||||
this.fromStreamerMessages.set(12, 'InputControlOwnership');
|
||||
this.fromStreamerMessages.set(13, 'GamepadResponse');
|
||||
this.fromStreamerMessages.set(14, 'DataChannelLatencyTest');
|
||||
this.fromStreamerMessages.set(255, 'Protocol');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
export class TwoWayMap<KeyType, ValueType> {
|
||||
map: Map<KeyType, ValueType>;
|
||||
reverseMap: Map<ValueType, KeyType>;
|
||||
|
||||
/**
|
||||
* @param map - an optional map of parameters
|
||||
*/
|
||||
constructor() {
|
||||
this.map = new Map();
|
||||
this.reverseMap = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from the map by key
|
||||
* @param key - the key we are searching by
|
||||
* @returns - the value associated with the key
|
||||
*/
|
||||
getFromKey(key: KeyType) {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reverse key from the map by searching by value
|
||||
* @param value - the key we are searching by
|
||||
* @returns - they key associated with the value
|
||||
*/
|
||||
getFromValue(value: ValueType) {
|
||||
return this.reverseMap.get(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key and value to both the map and reverse map
|
||||
* @param key - the indexing key
|
||||
* @param value - the value associated with the key
|
||||
*/
|
||||
add(key: KeyType, value: ValueType) {
|
||||
this.map.set(key, value);
|
||||
this.reverseMap.set(value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key and value from both the map and reverse map
|
||||
* @param key - the indexing key
|
||||
* @param value - the value associated with the key
|
||||
*/
|
||||
remove(key: KeyType, value: ValueType) {
|
||||
this.map.delete(key);
|
||||
this.reverseMap.delete(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,10 @@ import { SettingFlag } from '../Config/SettingFlag';
|
|||
import { SettingNumber } from '../Config/SettingNumber';
|
||||
import { SettingText } from '../Config/SettingText';
|
||||
import { SettingOption } from '../Config/SettingOption';
|
||||
import {
|
||||
DataChannelLatencyTestResponse,
|
||||
DataChannelLatencyTestResult
|
||||
} from "../DataChannel/DataChannelLatencyTestResults";
|
||||
|
||||
/**
|
||||
* An event that is emitted when AFK disconnect is about to happen.
|
||||
|
|
@ -140,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');
|
||||
|
|
@ -343,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');
|
||||
|
|
@ -351,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.
|
||||
*/
|
||||
|
|
@ -366,6 +387,37 @@ export class LatencyTestResultEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when receiving data channel latency test response from server.
|
||||
* This event is handled by DataChannelLatencyTestController
|
||||
*/
|
||||
export class DataChannelLatencyTestResponseEvent extends Event {
|
||||
readonly type: 'dataChannelLatencyTestResponse';
|
||||
readonly data: {
|
||||
/** Latency test result object */
|
||||
response: DataChannelLatencyTestResponse
|
||||
};
|
||||
constructor(data: DataChannelLatencyTestResponseEvent['data']) {
|
||||
super('dataChannelLatencyTestResponse');
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when data channel latency test results are ready.
|
||||
*/
|
||||
export class DataChannelLatencyTestResultEvent extends Event {
|
||||
readonly type: 'dataChannelLatencyTestResult';
|
||||
readonly data: {
|
||||
/** Latency test result object */
|
||||
result: DataChannelLatencyTestResult
|
||||
};
|
||||
constructor(data: DataChannelLatencyTestResultEvent['data']) {
|
||||
super('dataChannelLatencyTestResult');
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when receiving initial settings from UE.
|
||||
*/
|
||||
|
|
@ -470,6 +522,31 @@ export class XrFrameEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is emitted when receiving a player count from the signalling server
|
||||
*/
|
||||
export class PlayerCountEvent extends Event {
|
||||
readonly type: 'playerCount';
|
||||
readonly data: {
|
||||
/** count object */
|
||||
count: number
|
||||
};
|
||||
constructor(data: PlayerCountEvent['data']) {
|
||||
super('playerCount');
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -497,12 +574,17 @@ export type PixelStreamingEvent =
|
|||
| HideFreezeFrameEvent
|
||||
| StatsReceivedEvent
|
||||
| StreamerListMessageEvent
|
||||
| StreamerIDChangedMessageEvent
|
||||
| LatencyTestResultEvent
|
||||
| DataChannelLatencyTestResponseEvent
|
||||
| DataChannelLatencyTestResultEvent
|
||||
| InitialSettingsEvent
|
||||
| SettingsChangedEvent
|
||||
| XrSessionStartedEvent
|
||||
| XrSessionEndedEvent
|
||||
| XrFrameEvent;
|
||||
| XrFrameEvent
|
||||
| PlayerCountEvent
|
||||
| WebRtcTCPRelayDetectedEvent;
|
||||
|
||||
export class EventEmitter extends EventTarget {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
export class RTCUtils {
|
||||
static isVideoTransciever(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return this.canTransceiverReceiveVideo(transceiver) || this.canTransceiverSendVideo(transceiver);
|
||||
}
|
||||
|
||||
static canTransceiverReceiveVideo(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return !!transceiver &&
|
||||
(transceiver.direction === 'sendrecv' || transceiver.direction === 'recvonly') &&
|
||||
transceiver.receiver &&
|
||||
transceiver.receiver.track &&
|
||||
transceiver.receiver.track.kind === 'video';
|
||||
}
|
||||
|
||||
static canTransceiverSendVideo(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return !!transceiver &&
|
||||
(transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly') &&
|
||||
transceiver.sender &&
|
||||
transceiver.sender.track &&
|
||||
transceiver.sender.track.kind === 'video';
|
||||
}
|
||||
|
||||
static isAudioTransciever(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return this.canTransceiverReceiveAudio(transceiver) || this.canTransceiverSendAudio(transceiver);
|
||||
}
|
||||
|
||||
static canTransceiverReceiveAudio(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return !!transceiver &&
|
||||
(transceiver.direction === 'sendrecv' || transceiver.direction === 'recvonly') &&
|
||||
transceiver.receiver &&
|
||||
transceiver.receiver.track &&
|
||||
transceiver.receiver.track.kind === 'audio';
|
||||
}
|
||||
|
||||
static canTransceiverSendAudio(transceiver : RTCRtpTransceiver | undefined) : boolean {
|
||||
return !!transceiver &&
|
||||
(transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly') &&
|
||||
transceiver.sender &&
|
||||
transceiver.sender.track &&
|
||||
transceiver.sender.track.kind === 'audio';
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export class StreamController {
|
|||
constructor(videoElementProvider: VideoPlayer) {
|
||||
this.videoElementProvider = videoElementProvider;
|
||||
this.audioElement = document.createElement('Audio') as HTMLAudioElement;
|
||||
this.videoElementProvider.setAudioElement(this.audioElement);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ declare global {
|
|||
export class VideoPlayer {
|
||||
private config: Config;
|
||||
private videoElement: HTMLVideoElement;
|
||||
private audioElement?: HTMLAudioElement;
|
||||
private orientationChangeTimeout: number;
|
||||
private lastTimeResized = new Date().getTime();
|
||||
|
||||
|
|
@ -52,8 +53,11 @@ export class VideoPlayer {
|
|||
);
|
||||
};
|
||||
|
||||
// set play for video
|
||||
// set play for video (and audio)
|
||||
this.videoElement.onclick = () => {
|
||||
if (this.audioElement != undefined && this.audioElement.paused) {
|
||||
this.audioElement.play();
|
||||
}
|
||||
if (this.videoElement.paused) {
|
||||
this.videoElement.play();
|
||||
}
|
||||
|
|
@ -70,6 +74,10 @@ export class VideoPlayer {
|
|||
);
|
||||
}
|
||||
|
||||
public setAudioElement(audioElement: HTMLAudioElement) : void {
|
||||
this.audioElement = audioElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the video element with any application config and plays the video element.
|
||||
* @returns A promise for if playing the video was successful or not.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
export enum MessageRecvTypes {
|
||||
CONFIG = 'config',
|
||||
STREAMER_LIST = 'streamerList',
|
||||
STREAMER_ID_CHANGED = 'streamerIDChanged',
|
||||
PLAYER_COUNT = 'playerCount',
|
||||
OFFER = 'offer',
|
||||
ANSWER = 'answer',
|
||||
|
|
@ -42,6 +43,13 @@ export class MessageStreamerList extends MessageRecv {
|
|||
ids: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Streamer ID Changed Message Wrapper
|
||||
*/
|
||||
export class MessageStreamerIDChanged extends MessageRecv {
|
||||
newID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player Count Message wrapper
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,20 @@ export class MessageSend implements Send {
|
|||
type: string;
|
||||
peerConnectionOptions: object;
|
||||
|
||||
/**
|
||||
* A filter for controlling what parameters to actually send.
|
||||
* Good for excluding default values or hidden internals.
|
||||
* Example for including everything but zero bitrate fields...
|
||||
* sendFilter(key: string, value: any) {
|
||||
* if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
* return value;
|
||||
* }
|
||||
* Return undefined to exclude the property completely.
|
||||
*/
|
||||
sendFilter(key: string, value: any) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the wrapper into a JSON String
|
||||
* @returns - JSON String of the Message to send
|
||||
|
|
@ -31,10 +45,10 @@ export class MessageSend implements Send {
|
|||
payload() {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
'Sending => \n' + JSON.stringify(this, undefined, 4),
|
||||
'Sending => \n' + JSON.stringify(this, this.sendFilter, 4),
|
||||
6
|
||||
);
|
||||
return JSON.stringify(this);
|
||||
return JSON.stringify(this, this.sendFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,24 +97,45 @@ export class MessagePong extends MessageSend {
|
|||
}
|
||||
}
|
||||
|
||||
export type ExtraOfferParameters = {
|
||||
minBitrateBps: number;
|
||||
maxBitrateBps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web RTC Offer message wrapper
|
||||
*/
|
||||
export class MessageWebRTCOffer extends MessageSend {
|
||||
sdp: string;
|
||||
minBitrate: number;
|
||||
maxBitrate: number;
|
||||
|
||||
/**
|
||||
* @param offer - Generated Web RTC Offer
|
||||
*/
|
||||
constructor(offer?: RTCSessionDescriptionInit) {
|
||||
constructor(offer: RTCSessionDescriptionInit, extraParams: ExtraOfferParameters) {
|
||||
super();
|
||||
this.type = MessageSendTypes.OFFER;
|
||||
this.minBitrate = 0;
|
||||
this.maxBitrate = 0;
|
||||
|
||||
if (offer) {
|
||||
this.type = offer.type as MessageSendTypes;
|
||||
this.sdp = offer.sdp;
|
||||
this.minBitrate = extraParams.minBitrateBps;
|
||||
this.maxBitrate = extraParams.maxBitrateBps;
|
||||
}
|
||||
}
|
||||
|
||||
sendFilter(key: string, value: any) {
|
||||
if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtraAnswerParameters = {
|
||||
minBitrateBps: number;
|
||||
maxBitrateBps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,19 +143,30 @@ export class MessageWebRTCOffer extends MessageSend {
|
|||
*/
|
||||
export class MessageWebRTCAnswer extends MessageSend {
|
||||
sdp: string;
|
||||
minBitrate: number;
|
||||
maxBitrate: number;
|
||||
|
||||
/**
|
||||
* @param answer - Generated Web RTC Offer
|
||||
*/
|
||||
constructor(answer?: RTCSessionDescriptionInit) {
|
||||
constructor(answer: RTCSessionDescriptionInit, extraParams: ExtraAnswerParameters) {
|
||||
super();
|
||||
this.type = MessageSendTypes.ANSWER;
|
||||
this.minBitrate = 0;
|
||||
this.maxBitrate = 0;
|
||||
|
||||
if (answer) {
|
||||
this.type = answer.type as MessageSendTypes;
|
||||
this.sdp = answer.sdp;
|
||||
this.minBitrate = extraParams.minBitrateBps;
|
||||
this.maxBitrate = extraParams.maxBitrateBps;
|
||||
}
|
||||
}
|
||||
|
||||
sendFilter(key: string, value: any) {
|
||||
if ((key == "minBitrate" || key == "maxBitrate") && value <= 0) return undefined;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
MessageRecvTypes,
|
||||
MessageConfig,
|
||||
MessageStreamerList,
|
||||
MessageStreamerIDChanged,
|
||||
MessagePlayerCount,
|
||||
MessageAnswer,
|
||||
MessageOffer,
|
||||
|
|
@ -92,6 +93,21 @@ export class SignallingProtocol {
|
|||
}
|
||||
);
|
||||
|
||||
// STREAMER_ID_CHANGED
|
||||
websocketController.signallingProtocol.addMessageHandler(
|
||||
MessageRecvTypes.STREAMER_ID_CHANGED,
|
||||
(idPayload: string) => {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
MessageRecvTypes.STREAMER_ID_CHANGED,
|
||||
6
|
||||
);
|
||||
const streamerIdMessage: MessageStreamerIDChanged =
|
||||
JSON.parse(idPayload);
|
||||
websocketController.onStreamerIDChanged(streamerIdMessage);
|
||||
}
|
||||
);
|
||||
|
||||
// PLAYER_COUNT
|
||||
websocketController.signallingProtocol.addMessageHandler(
|
||||
MessageRecvTypes.PLAYER_COUNT,
|
||||
|
|
@ -108,6 +124,7 @@ export class SignallingProtocol {
|
|||
'Player Count: ' + playerCount.count,
|
||||
6
|
||||
);
|
||||
websocketController.onPlayerCount(playerCount)
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ' +
|
||||
|
|
@ -142,7 +141,7 @@ export class WebSocketController {
|
|||
' - ' +
|
||||
event.reason
|
||||
);
|
||||
this.onClose.dispatchEvent(new Event('close'));
|
||||
this.onClose.dispatchEvent(new CustomEvent('close', { 'detail': event }));
|
||||
}
|
||||
|
||||
requestStreamerList() {
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
@ -247,7 +249,12 @@ export class WebSocketController {
|
|||
* @param messageDataChannels - The data channels details
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
onWebRtcPeerDataChannels(
|
||||
messageDataChannels: MessageReceive.MessagePeerDataChannels
|
||||
) {}
|
||||
onWebRtcPeerDataChannels(messageDataChannels: MessageReceive.MessagePeerDataChannels) {}
|
||||
|
||||
/**
|
||||
* Event is fired when the websocket receives the an updated player count from cirrus
|
||||
* @param MessagePlayerCount - The new player count
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
onPlayerCount(playerCount: MessageReceive.MessagePlayerCount) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export {
|
|||
export { AggregatedStats } from './PeerConnectionController/AggregatedStats';
|
||||
export { Logger } from './Logger/Logger';
|
||||
export { UnquantizedDenormalizedUnsignedCoord as UnquantizedAndDenormalizeUnsigned } from './Util/CoordinateConverter';
|
||||
export { MessageDirection } from './UeInstanceMessage/StreamMessageController';
|
||||
export { MessageSend } from './WebSockets/MessageSend';
|
||||
export { MessageRecv, MessageStreamerList } from './WebSockets/MessageReceive';
|
||||
export { WebSocketController } from './WebSockets/WebSocketController';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3",
|
||||
"version": "0.0.1",
|
||||
"description": "Reference frontend UI library for Unreal Engine 5.3 Pixel Streaming - gives the stock look and feel.",
|
||||
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.4",
|
||||
"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",
|
||||
"types": "types/pixelstreamingfrontend-ui.d.ts",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"spellcheck": "cspell \"{README.md,.github/*.md,src/**/*.ts}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^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.3": "^0.0.1"
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.4": "^0.0.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
LatencyTestResults,
|
||||
InitialSettings,
|
||||
MessageStreamerList
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { OverlayBase } from '../Overlay/BaseOverlay';
|
||||
import { ActionOverlay } from '../Overlay/ActionOverlay';
|
||||
import { TextOverlay } from '../Overlay/TextOverlay';
|
||||
|
|
@ -31,6 +31,9 @@ import {
|
|||
UIElementConfig
|
||||
} from '../UI/UIConfigurationTypes'
|
||||
import { FullScreenIconBase, FullScreenIconExternal } from '../UI/FullscreenIcon';
|
||||
import {
|
||||
DataChannelLatencyTestResult
|
||||
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/types/DataChannel/DataChannelLatencyTestResults";
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -195,7 +198,7 @@ export class Application {
|
|||
// Or use the one created by the Controls initializer earlier
|
||||
: controls.fullscreenIcon;
|
||||
if (fullScreenButton) {
|
||||
fullScreenButton.fullscreenElement = /iPhone|iPod/.test(navigator.userAgent) ? this.stream.videoElementParent.getElementsByTagName("video")[0] : this.rootElement;
|
||||
fullScreenButton.fullscreenElement = /iPad|iPhone|iPod/.test(navigator.userAgent) ? this.stream.videoElementParent.getElementsByTagName("video")[0] : this.rootElement;
|
||||
}
|
||||
|
||||
// Add settings button to controls
|
||||
|
|
@ -321,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()
|
||||
|
|
@ -356,15 +359,33 @@ export class Application {
|
|||
({ data: { latencyTimings } }) =>
|
||||
this.onLatencyTestResults(latencyTimings)
|
||||
);
|
||||
this.stream.addEventListener(
|
||||
'dataChannelLatencyTestResult',
|
||||
({data: { result } }) =>
|
||||
this.onDataChannelLatencyTestResults(result)
|
||||
)
|
||||
this.stream.addEventListener(
|
||||
'streamerListMessage',
|
||||
({ data: { messageStreamerList, autoSelectedStreamerId } }) =>
|
||||
this.handleStreamerListMessage(messageStreamerList, autoSelectedStreamerId)
|
||||
({ data: { messageStreamerList, autoSelectedStreamerId, wantedStreamerId } }) =>
|
||||
this.handleStreamerListMessage(messageStreamerList, autoSelectedStreamerId, wantedStreamerId)
|
||||
);
|
||||
this.stream.addEventListener(
|
||||
'settingsChanged',
|
||||
(event) => this.configUI.onSettingsChanged(event)
|
||||
);
|
||||
this.stream.addEventListener(
|
||||
'playerCount',
|
||||
({ 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.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -474,7 +495,7 @@ export class Application {
|
|||
* Shows or hides the settings panel if clicked
|
||||
*/
|
||||
settingsClicked() {
|
||||
this.statsPanel.hide();
|
||||
this.statsPanel?.hide();
|
||||
this.settingsPanel.toggleVisibility();
|
||||
}
|
||||
|
||||
|
|
@ -482,7 +503,7 @@ export class Application {
|
|||
* Shows or hides the stats panel if clicked
|
||||
*/
|
||||
statsClicked() {
|
||||
this.settingsPanel.hide();
|
||||
this.settingsPanel?.hide();
|
||||
this.statsPanel.toggleVisibility();
|
||||
}
|
||||
|
||||
|
|
@ -560,19 +581,17 @@ 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 check
|
||||
this.statsPanel.latencyTest.latencyTestButton.onclick = () => {
|
||||
// do nothing
|
||||
};
|
||||
// disable starting a latency checks
|
||||
this.statsPanel?.onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -619,11 +638,7 @@ export class Application {
|
|||
if (!this.stream.config.isFlagEnabled(Flags.AutoPlayVideo)) {
|
||||
this.showPlayOverlay();
|
||||
}
|
||||
|
||||
// starting a latency check
|
||||
this.statsPanel.latencyTest.latencyTestButton.onclick = () => {
|
||||
this.stream.requestLatencyTest();
|
||||
};
|
||||
this.statsPanel?.onVideoInitialized(this.stream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -639,39 +654,62 @@ export class Application {
|
|||
|
||||
onInitialSettings(settings: InitialSettings) {
|
||||
if (settings.PixelStreamingSettings) {
|
||||
const disableLatencyTest =
|
||||
settings.PixelStreamingSettings.DisableLatencyTest;
|
||||
if (disableLatencyTest) {
|
||||
this.statsPanel.latencyTest.latencyTestButton.disabled = true;
|
||||
this.statsPanel.latencyTest.latencyTestButton.title =
|
||||
'Disabled by -PixelStreamingDisableLatencyTester=true';
|
||||
Logger.Info(
|
||||
Logger.GetStackTrace(),
|
||||
'-PixelStreamingDisableLatencyTester=true, requesting latency report from the the browser to UE is disabled.'
|
||||
);
|
||||
}
|
||||
this.statsPanel?.configure(settings.PixelStreamingSettings);
|
||||
}
|
||||
}
|
||||
|
||||
onStatsReceived(aggregatedStats: AggregatedStats) {
|
||||
// Grab all stats we can off the aggregated stats
|
||||
this.statsPanel.handleStats(aggregatedStats);
|
||||
this.statsPanel?.handleStats(aggregatedStats);
|
||||
}
|
||||
|
||||
onLatencyTestResults(latencyTimings: LatencyTestResults) {
|
||||
this.statsPanel.latencyTest.handleTestResult(latencyTimings);
|
||||
this.statsPanel?.latencyTest.handleTestResult(latencyTimings);
|
||||
}
|
||||
|
||||
handleStreamerListMessage(messageStreamingList: MessageStreamerList, autoSelectedStreamerId: string | null) {
|
||||
if (autoSelectedStreamerId === null) {
|
||||
if(messageStreamingList.ids.length === 0) {
|
||||
this.showDisconnectOverlay(
|
||||
'No streamers connected. <div class="clickableState">Click To Restart</div>'
|
||||
);
|
||||
onDataChannelLatencyTestResults(result: DataChannelLatencyTestResult) {
|
||||
this.statsPanel?.dataChannelLatencyTest.handleTestResult(result);
|
||||
}
|
||||
|
||||
onPlayerCount(playerCount: number) {
|
||||
this.statsPanel?.handlePlayerCount(playerCount);
|
||||
}
|
||||
|
||||
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 {
|
||||
this.showTextOverlay(
|
||||
'Multiple streamers detected. Use the dropdown in the settings menu to select the streamer'
|
||||
);
|
||||
message = `Multiple streamers available. Select one from the settings menu.`;
|
||||
allowRestart = false;
|
||||
}
|
||||
|
||||
if (allowRestart) {
|
||||
this.showDisconnectOverlay(message);
|
||||
} else {
|
||||
this.showTextOverlay(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
SettingOption,
|
||||
Logger,
|
||||
SettingBase
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { SettingUIFlag } from './SettingUIFlag';
|
||||
import { SettingUINumber } from './SettingUINumber';
|
||||
import { SettingUIText } from './SettingUIText';
|
||||
|
|
@ -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)
|
||||
|
|
@ -198,6 +194,10 @@ export class ConfigUI {
|
|||
psSettingsSection,
|
||||
this.flagsUi.get(Flags.AFKDetection)
|
||||
);
|
||||
this.addSettingFlag(
|
||||
psSettingsSection,
|
||||
this.flagsUi.get(Flags.WaitForStreamer)
|
||||
);
|
||||
this.addSettingNumeric(
|
||||
psSettingsSection,
|
||||
this.numericParametersUi.get(NumericParameters.AFKTimeoutSecs)
|
||||
|
|
@ -206,6 +206,10 @@ export class ConfigUI {
|
|||
psSettingsSection,
|
||||
this.numericParametersUi.get(NumericParameters.MaxReconnectAttempts)
|
||||
);
|
||||
this.addSettingNumeric(
|
||||
psSettingsSection,
|
||||
this.numericParametersUi.get(NumericParameters.StreamerAutoJoinInterval)
|
||||
);
|
||||
|
||||
/* Setup all view/ui related settings under this section */
|
||||
const viewSettingsSection = this.buildSectionWithHeading(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { SettingBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { SettingBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
|
||||
/**
|
||||
* Base class for a setting that has a text label, an arbitrary setting value it stores, an a HTML element that represents this setting.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type {
|
||||
FlagsIds,
|
||||
SettingFlag
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { SettingUIBase } from './SettingUIBase';
|
||||
|
||||
export class SettingUIFlag<
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import type {
|
||||
NumericParametersIds,
|
||||
SettingNumber
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { SettingUIBase } from './SettingUIBase';
|
||||
|
||||
/**
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type {
|
||||
OptionParametersIds,
|
||||
SettingOption
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { SettingUIBase } from './SettingUIBase';
|
||||
|
||||
export class SettingUIOption<
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type {
|
||||
SettingText,
|
||||
TextParametersIds
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
} from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { SettingUIBase } from './SettingUIBase';
|
||||
|
||||
export class SettingUIText<
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
|
||||
import { OverlayBase } from './BaseOverlay';
|
||||
|
||||
|
|
|
|||
|
|
@ -526,14 +526,16 @@ export class PixelStreamingApplicationStyle {
|
|||
customStyles?: Partial<Styles>;
|
||||
lightModePalette?: ColorPalette;
|
||||
darkModePalette?: ColorPalette;
|
||||
jssInsertionPoint?: string | HTMLElement;
|
||||
}) {
|
||||
const { customStyles, lightModePalette, darkModePalette } =
|
||||
const { customStyles, lightModePalette, darkModePalette, jssInsertionPoint } =
|
||||
options ?? {};
|
||||
// One time setup with default plugins and settings.
|
||||
const jssOptions = {
|
||||
// JSS has many interesting plugins we may wish to turn on
|
||||
//plugins: [functions(), template(), global(), extend(), nested(), compose(), camelCase(), defaultUnit(options.defaultUnit), expand(), vendorPrefixer(), propsSort()]
|
||||
plugins: [global(), camelCase()]
|
||||
plugins: [global(), camelCase()],
|
||||
insertionPoint: jssInsertionPoint
|
||||
};
|
||||
|
||||
jss.setup(jssOptions);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FullScreenIcon } from './FullscreenIcon';
|
|||
import { SettingsIcon } from './SettingsIcon';
|
||||
import { StatsIcon } from './StatsIcon';
|
||||
import { XRIcon } from './XRIcon';
|
||||
import { WebXRController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { WebXRController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { UIElementConfig, UIElementCreationMode } from '../UI/UIConfigurationTypes'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import {
|
||||
DataChannelLatencyTestResult
|
||||
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/types/DataChannel/DataChannelLatencyTestResults";
|
||||
|
||||
/**
|
||||
* DataChannel Latency test UI elements and results handling.
|
||||
*/
|
||||
export class DataChannelLatencyTest {
|
||||
_rootElement: HTMLElement;
|
||||
_latencyTestButton: HTMLInputElement;
|
||||
_latencyTestResultsElement: HTMLElement;
|
||||
|
||||
/**
|
||||
* Get the button containing the stats icon.
|
||||
*/
|
||||
public get rootElement(): HTMLElement {
|
||||
if (!this._rootElement) {
|
||||
this._rootElement = document.createElement('section');
|
||||
this._rootElement.classList.add('settingsContainer');
|
||||
|
||||
// make heading
|
||||
const heading = document.createElement('div');
|
||||
heading.id = 'dataChannelLatencyTestHeader';
|
||||
heading.classList.add('settings-text');
|
||||
heading.classList.add('settingsHeader');
|
||||
this._rootElement.appendChild(heading);
|
||||
|
||||
const headingText = document.createElement('div');
|
||||
headingText.innerHTML = 'Data Channel Latency Test';
|
||||
heading.appendChild(headingText);
|
||||
heading.appendChild(this.latencyTestButton);
|
||||
|
||||
// make test results element
|
||||
const resultsParentElem = document.createElement('div');
|
||||
resultsParentElem.id = 'dataChannelLatencyTestContainer';
|
||||
resultsParentElem.classList.add('d-none');
|
||||
this._rootElement.appendChild(resultsParentElem);
|
||||
|
||||
resultsParentElem.appendChild(this.latencyTestResultsElement);
|
||||
}
|
||||
return this._rootElement;
|
||||
}
|
||||
|
||||
public get latencyTestResultsElement(): HTMLElement {
|
||||
if (!this._latencyTestResultsElement) {
|
||||
this._latencyTestResultsElement = document.createElement('div');
|
||||
this._latencyTestResultsElement.id = 'dataChannelLatencyStatsResults';
|
||||
this._latencyTestResultsElement.classList.add('StatsResult');
|
||||
}
|
||||
return this._latencyTestResultsElement;
|
||||
}
|
||||
|
||||
public get latencyTestButton(): HTMLInputElement {
|
||||
if (!this._latencyTestButton) {
|
||||
this._latencyTestButton = document.createElement('input');
|
||||
this._latencyTestButton.type = 'button';
|
||||
this._latencyTestButton.value = 'Run Test';
|
||||
this._latencyTestButton.id = 'btn-start-data-channel-latency-test';
|
||||
this._latencyTestButton.classList.add('streamTools-button');
|
||||
this._latencyTestButton.classList.add('btn-flat');
|
||||
}
|
||||
return this._latencyTestButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the UI based on the latency test's results.
|
||||
* @param result The latency test results.
|
||||
*/
|
||||
public handleTestResult(result: DataChannelLatencyTestResult) {
|
||||
Logger.Log(
|
||||
Logger.GetStackTrace(),
|
||||
result.toString(),
|
||||
6
|
||||
);
|
||||
/**
|
||||
* Check we have results, NaN would mean that UE version we talk to doesn't support our test
|
||||
*/
|
||||
if (isNaN(result.dataChannelRtt)) {
|
||||
this.latencyTestResultsElement.innerHTML = '<div>Not supported</div>';
|
||||
return;
|
||||
}
|
||||
let latencyStatsInnerHTML = '';
|
||||
latencyStatsInnerHTML +=
|
||||
'<div>Data channel RTT (ms): ' +
|
||||
result.dataChannelRtt +
|
||||
'</div>';
|
||||
/**
|
||||
* Separate path time discovery works only when UE and Player clocks have been synchronized.
|
||||
*/
|
||||
if (result.playerToStreamerTime >= 0 && result.streamerToPlayerTime >= 0) {
|
||||
latencyStatsInnerHTML +=
|
||||
'<div>Player to Streamer path (ms): ' + result.playerToStreamerTime + '</div>';
|
||||
latencyStatsInnerHTML +=
|
||||
'<div>Streamer to Player path (ms): ' +
|
||||
result.streamerToPlayerTime +
|
||||
'</div>';
|
||||
}
|
||||
this.latencyTestResultsElement.innerHTML = latencyStatsInnerHTML;
|
||||
//setup button to download the detailed results
|
||||
let downloadButton: HTMLInputElement = document.createElement('input');
|
||||
downloadButton.type = 'button';
|
||||
downloadButton.value = 'Download';
|
||||
downloadButton.classList.add('streamTools-button');
|
||||
downloadButton.classList.add('btn-flat');
|
||||
downloadButton.onclick = () => {
|
||||
let file = new Blob([result.exportLatencyAsCSV()], {type: 'text/plain'});
|
||||
let a = document.createElement("a"),
|
||||
url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = "data_channel_latency_test_results.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
this.latencyTestResultsElement.appendChild(downloadButton);
|
||||
}
|
||||
|
||||
public handleTestStart() {
|
||||
this.latencyTestResultsElement.innerHTML =
|
||||
'<div>Test in progress</div>';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4';
|
||||
|
||||
/**
|
||||
* Latency test UI elements and results handling.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import { LatencyTest } from './LatencyTest';
|
||||
import { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
import { AggregatedStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3';
|
||||
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";
|
||||
import {PixelStreamingSettings} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.4/types/DataChannel/InitialSettings";
|
||||
|
||||
/**
|
||||
* A stat structure, an id, the stat string, and the element where it is rendered.
|
||||
|
|
@ -26,12 +28,14 @@ export class StatsPanel {
|
|||
_statsResult: HTMLElement;
|
||||
|
||||
latencyTest: LatencyTest;
|
||||
dataChannelLatencyTest: DataChannelLatencyTest;
|
||||
|
||||
/* A map stats we are storing/rendering */
|
||||
statsMap = new Map<string, Stat>();
|
||||
|
||||
constructor() {
|
||||
this.latencyTest = new LatencyTest();
|
||||
this.dataChannelLatencyTest = new DataChannelLatencyTest();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,6 +95,7 @@ export class StatsPanel {
|
|||
statistics.appendChild(this.statisticsContainer);
|
||||
|
||||
controlStats.appendChild(this.latencyTest.rootElement);
|
||||
controlStats.appendChild(this.dataChannelLatencyTest.rootElement);
|
||||
}
|
||||
return this._statsContentElement;
|
||||
}
|
||||
|
|
@ -122,6 +127,48 @@ export class StatsPanel {
|
|||
return this._statsCloseButton;
|
||||
}
|
||||
|
||||
public onDisconnect(): void {
|
||||
this.latencyTest.latencyTestButton.onclick = () => {
|
||||
// do nothing
|
||||
}
|
||||
this.dataChannelLatencyTest.latencyTestButton.onclick = () => {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
public onVideoInitialized(stream: PixelStreaming): void {
|
||||
// starting a latency check
|
||||
this.latencyTest.latencyTestButton.onclick = () => {
|
||||
stream.requestLatencyTest();
|
||||
};
|
||||
this.dataChannelLatencyTest.latencyTestButton.onclick = () => {
|
||||
let started = stream.requestDataChannelLatencyTest({
|
||||
duration: 1000,
|
||||
rps: 10,
|
||||
requestSize: 200,
|
||||
responseSize: 200
|
||||
});
|
||||
if (started) {
|
||||
this.dataChannelLatencyTest.handleTestStart();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public configure(settings: PixelStreamingSettings): void {
|
||||
if (settings.DisableLatencyTest) {
|
||||
this.latencyTest.latencyTestButton.disabled = true;
|
||||
this.latencyTest.latencyTestButton.title =
|
||||
'Disabled by -PixelStreamingDisableLatencyTester=true';
|
||||
this.dataChannelLatencyTest.latencyTestButton.disabled = true;
|
||||
this.dataChannelLatencyTest.latencyTestButton.title =
|
||||
'Disabled by -PixelStreamingDisableLatencyTester=true';
|
||||
Logger.Info(
|
||||
Logger.GetStackTrace(),
|
||||
'-PixelStreamingDisableLatencyTester=true, requesting latency report from the the browser to UE is disabled.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show stats panel.
|
||||
*/
|
||||
|
|
@ -147,6 +194,14 @@ export class StatsPanel {
|
|||
}
|
||||
}
|
||||
|
||||
public handlePlayerCount(playerCount: number) {
|
||||
this.addOrUpdateStat(
|
||||
'PlayerCountStat',
|
||||
'Players',
|
||||
playerCount.toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stats coming in from browser/UE
|
||||
* @param stats the stats structure
|
||||
|
|
@ -263,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);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ module.exports = {
|
|||
extensions: ['.tsx', '.ts', '.js']
|
||||
},
|
||||
externals: {
|
||||
'@epicgames-ps/lib-pixelstreamingfrontend-ue5.3': '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3',
|
||||
'@epicgames-ps/lib-pixelstreamingfrontend-ue5.4': '@epicgames-ps/lib-pixelstreamingfrontend-ue5.4',
|
||||
jss: 'jss',
|
||||
'jss-plugin-camel-case': 'jss-plugin-camel-case',
|
||||
'jss-plugin-global': 'jss-plugin-global'
|
||||
|
|
@ -32,7 +32,6 @@ module.exports = {
|
|||
})
|
||||
],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
globalObject: 'this'
|
||||
}
|
||||
};
|
||||