├── examples └── typescript │ ├── .env.example │ ├── index.d.ts │ ├── src │ ├── assets │ │ ├── images │ │ │ ├── favicon.png │ │ │ └── favicon.svg │ │ └── css │ │ │ └── index.css │ ├── index.html │ └── index.ts │ ├── tsconfig.json │ ├── webpack.prod.js │ ├── webpack.dev.js │ ├── package.json │ └── webpack.common.js ├── .gitignore ├── dockerfiles └── sps-frontend.dockerfile ├── library ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js ├── package.json └── src │ ├── Messages.ts │ ├── LoadingOverlay.ts │ ├── index.ts │ ├── SPSApplication.ts │ └── SignallingExtension.ts ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── release-workflow.yaml │ ├── tag-workflow.yaml │ └── test-pull-request.yaml ├── LICENSE ├── docs ├── api_transition_guide.md ├── sps_frontend_reference_guide.md └── frontend_utilisation_guide.md └── README.md /examples/typescript/.env.example: -------------------------------------------------------------------------------- 1 | WEBSOCKET_URL=ws://example.com/your/ws -------------------------------------------------------------------------------- /examples/typescript/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.svg'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | types/ 4 | .vscode 5 | !.env.example 6 | *.env 7 | -------------------------------------------------------------------------------- /examples/typescript/src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalablePixelStreaming/Frontend/HEAD/examples/typescript/src/assets/images/favicon.png -------------------------------------------------------------------------------- /dockerfiles/sps-frontend.dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.6 2 | 3 | RUN mkdir www 4 | WORKDIR /www 5 | 6 | ADD examples/typescript/dist/. /www/ 7 | 8 | RUN rm /etc/nginx/nginx.conf 9 | RUN ln -s /etc/nginx/sps/nginx.conf /etc/nginx/nginx.conf 10 | 11 | EXPOSE 80 -------------------------------------------------------------------------------- /library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./types", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "esModuleInterop": true, 7 | "target": "ES6", 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "declaration": true 12 | }, 13 | "lib": [ 14 | "es2015" 15 | ], 16 | "include": [ 17 | "./src/*.ts" 18 | ], 19 | } -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "esModuleInterop": true, 7 | "target": "ES6", 8 | "moduleResolution": "node", 9 | "sourceMap": false, 10 | "allowJs": true, 11 | "declaration": false 12 | }, 13 | "lib": [ 14 | "es2015" 15 | ], 16 | "include": [ 17 | "./src/*.ts" 18 | ], 19 | } -------------------------------------------------------------------------------- /examples/typescript/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | optimization: { 8 | usedExports: true, 9 | minimize: true 10 | }, 11 | stats: 'errors-only', 12 | performance: { 13 | hints: false 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | WEBSOCKET_URL: undefined 18 | }), 19 | ] 20 | }); 21 | -------------------------------------------------------------------------------- /examples/typescript/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'development', 8 | devtool: 'source-map', 9 | plugins: [ 10 | new webpack.DefinePlugin({ 11 | WEBSOCKET_URL: JSON.stringify((process.env.WEBSOCKET_URL !== undefined) ? process.env.WEBSOCKET_URL : undefined) 12 | }), 13 | ] 14 | }); 15 | -------------------------------------------------------------------------------- /library/webpack.common.js: -------------------------------------------------------------------------------- 1 | const package = require('./package.json'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: { 7 | index: './src/index.ts' 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | loader: 'ts-loader', 14 | exclude: [/node_modules/] 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: ['.tsx', '.ts', '.js'] 20 | }, 21 | plugins: [], 22 | output: { 23 | path: path.resolve(__dirname, 'dist'), 24 | globalObject: 'this' 25 | } 26 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Relevant components: 2 | - [ ] Scalable Pixel Streaming Frontend library 3 | - [ ] Examples 4 | - [ ] Docs 5 | 6 | ## Problem statement: 7 | What problem does this PR address? 8 | 9 | ## Solution 10 | How does this PR solve the problem? 11 | 12 | ## Documentation 13 | If you added a new feature or changed an existing feature please add some documentation here. 14 | 15 | Or better yet, link to the relevant `/Docs` update in your PR. 16 | 17 | ## Test Plan and Compatibility 18 | What steps have you taken to ensure this PR maintains compataibility with the existing functionality? 19 | 20 | Or better yet, link to the unit tests that accompany this PR. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Scalable Pixel Streaming Frontend Release Workflow Pipeline 2 | 3 | # Start our pipeline when we create a new tag on the master branch 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | # Build NPM Package 10 | build-npm: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./library 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: "16.x" 20 | registry-url: "https://registry.npmjs.org" 21 | - run: npm ci 22 | - run: npm run build-prod 23 | - run: npm publish --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /library/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | const devCommon = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | devServer: { 8 | static: './dist', 9 | } 10 | }; 11 | 12 | module.exports = [ 13 | merge(common, devCommon, { 14 | output: { 15 | filename: 'libspsfrontend.js', 16 | library: { 17 | name: 'libspsfrontend', // exposed variable that will provide access to the library classes 18 | type: 'umd' 19 | }, 20 | }, 21 | }), 22 | merge(common, devCommon, { 23 | output: { 24 | filename: 'libspsfrontend.esm.js', 25 | library: { 26 | type: 'module' 27 | }, 28 | }, 29 | experiments: { 30 | outputModule: true 31 | } 32 | }) 33 | ]; 34 | -------------------------------------------------------------------------------- /library/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | const prodCommon = { 5 | mode: 'production', 6 | optimization: { 7 | usedExports: true, 8 | minimize: true 9 | }, 10 | stats: 'errors-only', 11 | }; 12 | 13 | module.exports = [ 14 | merge(common, prodCommon, { 15 | output: { 16 | filename: 'libspsfrontend.min.js', 17 | library: { 18 | name: 'libspsfrontend', // exposed variable that will provide access to the library classes 19 | type: 'umd' 20 | }, 21 | }, 22 | }), 23 | merge(common, prodCommon, { 24 | output: { 25 | filename: 'libspsfrontend.esm.js', 26 | library: { 27 | type: 'module' 28 | }, 29 | }, 30 | experiments: { 31 | outputModule: true 32 | } 33 | }) 34 | ]; 35 | -------------------------------------------------------------------------------- /library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tensorworks/libspsfrontend", 3 | "version": "1.0.0", 4 | "description": "The Scalable Pixel Streaming Frontend Library consuming Epic Games' Pixel Streaming Frontend", 5 | "main": "dist/libspsfrontend.min.js", 6 | "module": "dist/libspsfrontend.esm.js", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "build-prod": "npx webpack --config webpack.prod.js", 10 | "build-dev": "npx webpack --config webpack.dev.js", 11 | "watch": "npx webpack --watch", 12 | "serve": "webpack serve" 13 | }, 14 | "author": "TensorWorks Pty Ltd", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5": "^0.1.4", 18 | "@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5": "^0.1.0" 19 | }, 20 | "devDependencies": { 21 | "css-loader": "^6.7.3", 22 | "html-loader": "^4.2.0", 23 | "html-webpack-plugin": "^5.5.0", 24 | "path": "^0.12.7", 25 | "ts-loader": "^9.4.2", 26 | "webpack": "^5.76.1", 27 | "webpack-cli": "^5.0.1", 28 | "webpack-dev-server": "^4.11.1", 29 | "typescript": "^4.9.4" 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 - 2024 TensorWorks Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library/src/Messages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregatedStats, 3 | CandidatePairStats, 4 | CandidateStat, 5 | DataChannelStats, 6 | InboundAudioStats, 7 | InboundVideoStats, 8 | BaseMessage, 9 | OutBoundVideoStats 10 | } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 11 | 12 | /** 13 | * Aggregated Stats Message Wrapper 14 | */ 15 | export class MessageStats implements BaseMessage { 16 | type: string; 17 | inboundVideoStats: InboundVideoStats; 18 | inboundAudioStats: InboundAudioStats; 19 | candidatePair: CandidatePairStats; 20 | dataChannelStats: DataChannelStats; 21 | localCandidates: Array; 22 | remoteCandidates: Array; 23 | outboundVideoStats: OutBoundVideoStats; 24 | 25 | /** 26 | * @param aggregatedStats - Aggregated Stats 27 | */ 28 | constructor(aggregatedStats: AggregatedStats) { 29 | this.type = "stats"; 30 | this.inboundVideoStats = aggregatedStats.inboundVideoStats; 31 | this.inboundAudioStats = aggregatedStats.inboundAudioStats; 32 | this.candidatePair = aggregatedStats.getActiveCandidatePair(); 33 | this.dataChannelStats = aggregatedStats.DataChannelStats 34 | this.localCandidates = aggregatedStats.localCandidates; 35 | this.remoteCandidates = aggregatedStats.remoteCandidates; 36 | this.outboundVideoStats = aggregatedStats.outBoundVideoStats; 37 | } 38 | } -------------------------------------------------------------------------------- /examples/typescript/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* CUSTOM SPS STYLING */ 2 | /* Loading overlay */ 3 | #loadingOverlay { 4 | z-index: 30; 5 | position: absolute; 6 | color: var(--color2); 7 | font-size: 1.8em; 8 | width: 100%; 9 | height: 100%; 10 | background-color: var(--color1); 11 | justify-content: center; 12 | text-transform: uppercase; 13 | overflow: hidden; 14 | } 15 | 16 | #loadingOverlayText { 17 | align-self: flex-end; 18 | padding-bottom: 1%; 19 | display: inline-block; 20 | text-align: center; 21 | } 22 | 23 | /* Loading spinner */ 24 | .loading-spinner { 25 | display: inline-block; 26 | position: relative; 27 | } 28 | 29 | .loading-spinner div { 30 | box-sizing: border-box; 31 | display: block; 32 | position: absolute; 33 | margin: 12.5%; 34 | border: 1px solid var(--color2); 35 | border-radius: 50%; 36 | animation: loading-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 37 | border-color: var(--color2) transparent transparent transparent; 38 | } 39 | 40 | .loading-spinner div:nth-child(1) { 41 | animation-delay: -0.45s; 42 | } 43 | 44 | .loading-spinner div:nth-child(2) { 45 | animation-delay: -0.3s; 46 | } 47 | 48 | .loading-spinner div:nth-child(3) { 49 | animation-delay: -0.15s; 50 | } 51 | 52 | @keyframes loading-spinner { 53 | 0% { 54 | transform: rotate(0deg); 55 | } 56 | 57 | 100% { 58 | transform: rotate(360deg); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tensorworks/spstypescriptexample", 3 | "version": "1.0.0", 4 | "description": "The typescript example for consuming the Scalable Pixel Streaming Frontend", 5 | "main": "./src/index.ts", 6 | "scripts": { 7 | "build-dev": "npx webpack --config webpack.dev.js", 8 | "build-prod": "npx webpack --config webpack.prod.js", 9 | "watch": "npx webpack --watch", 10 | "start": "npx webpack && open-cli ./dist/index.html", 11 | "serve-dev": "webpack serve --config webpack.dev.js", 12 | "serve-prod": "webpack serve --config webpack.prod.js", 13 | "symlink": "npm link ../../library", 14 | "build-all-dev": "cd ../../library && npm install && npm run build-dev && cd ../examples/typescript && npm run symlink && npm run build-dev", 15 | "build-all-prod": "cd ../../library && npm install && npm run build-prod && cd ../examples/typescript && npm run symlink && npm run build-prod" 16 | }, 17 | "author": "TensorWorks Pty Ltd", 18 | "keywords": [], 19 | "license": "MIT", 20 | "dependencies": { 21 | "dotenv": "^16.0.3" 22 | }, 23 | "devDependencies": { 24 | "webpack-cli": "^5.0.1", 25 | "webpack-dev-server": "^4.11.1", 26 | "css-loader": "^6.7.3", 27 | "html-loader": "^4.2.0", 28 | "html-webpack-plugin": "^5.5.0", 29 | "path": "^0.12.7", 30 | "ts-loader": "^9.4.2", 31 | "typescript": "^4.9.4", 32 | "webpack": "^5.76.0" 33 | } 34 | } -------------------------------------------------------------------------------- /examples/typescript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Scalable Pixel Streaming 22 | 23 | 24 | 25 |
27 | Scalable Pixel Streaming by TensorWorks 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/api_transition_guide.md: -------------------------------------------------------------------------------- 1 | # Migrating from `libspsfrontend` predating `v0.1.4` 2 | 3 | SPS frontend changed to use the [Epic Games Pixel Streaming frontend](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/master/Frontend) since version `0.1.4`, which involved modifications both to our API and NPM packages. 4 | 5 | Below are some common usages of the SPS frontend API that have changed. Note that this list is not exhaustive, if you encounter more differences, please open an issue on this repository to report them. 6 | 7 | ### Listening for UE messages 8 | 9 | Refer to [this PR](https://github.com/EpicGames/PixelStreamingInfrastructure/pull/132) for more details. 10 | 11 | Before: 12 | ```js 13 | iWebRtcController.dataChannelController.onResponse = (messageBuffer) => { 14 | /* whatever */ 15 | } 16 | ``` 17 | 18 | Now: 19 | ```js 20 | pixelstreaming.addResponseEventListener(name, funct) 21 | 22 | // or 23 | 24 | pixelstreaming.removeResponseEventListener(name) 25 | ``` 26 | 27 | ### Sending messages to UE 28 | 29 | Refer to [this PR](https://github.com/EpicGames/PixelStreamingInfrastructure/pull/132) for more details. 30 | 31 | Before: 32 | ```js 33 | iWebRtcController.sendUeUiDescriptor(JSON.stringify({ /* whatever */ } )) 34 | ``` 35 | 36 | Now: 37 | ```js 38 | pixelstreaming.emitUIInteraction(data: object | string) 39 | ``` 40 | 41 | ### Listening for WebRTC stream start 42 | 43 | Refer to [this PR](https://github.com/EpicGames/PixelStreamingInfrastructure/pull/110) for more details. 44 | 45 | Before: 46 | ```js 47 | override onVideoInitialised() 48 | ``` 49 | 50 | Now: 51 | ```js 52 | pixelStreaming.addEventListener("videoInitialized", ()=> { /* Do something */ }); 53 | ``` 54 | -------------------------------------------------------------------------------- /.github/workflows/tag-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Scalable Pixel Streaming Frontend Tag Workflow Pipeline 2 | 3 | # Start our pipeline when we create a new tag on the master branch 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | # Assign an environment variable that we can use for the version of the Scalable Pixel Streaming Frontend that we are tagging 10 | env: 11 | VERSION: ${{ github.ref_name }} 12 | DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} 13 | DOCKER_TOKEN: ${{ secrets.DOCKER_ACCESS_KEY }} 14 | 15 | jobs: 16 | 17 | # Build Library 18 | build-library: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | 24 | # remove unneeded pieces from runner to free up space 25 | - name: Free up some space 26 | run: | 27 | sudo rm -rf /usr/local/lib/android # will release about 10 GB if you don't need Android 28 | sudo rm -rf /usr/share/dotnet # will release about 20GB if you don't need .NET 29 | 30 | # Clone the source code on the runner 31 | - name: Clone repository 32 | uses: actions/checkout@v3 33 | 34 | # Login to Docker 35 | - name: Docker login 36 | run: docker login -u $DOCKER_USER -p $DOCKER_TOKEN 37 | 38 | # Set up Node js for npm 39 | - name: Setup Nodejs 40 | uses: actions/setup-node@v3 41 | 42 | # Build the Scalable Pixel Streaming Frontend Library and Example 43 | - name: Build SPS Frontend 44 | run: | 45 | cd ./examples/typescript 46 | npm run build-all-prod 47 | 48 | # Build the Scalable Pixel Streaming Frontend Docker image from the dist directories of the packages 49 | - name: Build the Scalable Pixel Streaming Frontend Docker image and push to Docker 50 | run: | 51 | docker build -t "tensorworks/sps-frontend:$VERSION" -f ./dockerfiles/sps-frontend.dockerfile . 52 | docker push "tensorworks/sps-frontend:$VERSION" -------------------------------------------------------------------------------- /.github/workflows/test-pull-request.yaml: -------------------------------------------------------------------------------- 1 | # The following workflow will run when a pull request is opened, synced or reopened 2 | # It's intent is to run a build of the package against the PR to ensure that it passes a code build check 3 | 4 | name: test-build 5 | on: pull_request 6 | 7 | jobs: 8 | 9 | # This job will checkout the repo, install dependencies and then build the frontend package 10 | test-build-frontend-and-lib: 11 | runs-on: ubuntu-latest 12 | 13 | # set out working directory 14 | defaults: 15 | run: 16 | working-directory: ./ 17 | 18 | # checkout, install node and run our npm commands to check code builds 19 | steps: 20 | 21 | - name: Checkout repo 22 | uses: actions/checkout@v3 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: "16.x" 28 | 29 | # install deps for Library and build for development 30 | - name: Install and build library for development 31 | working-directory: ./library 32 | run: | 33 | npm ci 34 | npm run build-dev 35 | 36 | # install deps for Library and build for production 37 | - name: Install and build library for production 38 | working-directory: ./library 39 | run: | 40 | npm ci 41 | npm run build-prod 42 | 43 | # install deps for Frontend and Library and build both for development 44 | - name: Install and build library and Frontend for development 45 | working-directory: ./examples/typescript 46 | run: | 47 | npm ci 48 | npm run build-all-dev 49 | 50 | # install deps for Frontend and Library for and build both for production 51 | - name: Install and build library and Frontend for production 52 | working-directory: ./examples/typescript 53 | run: | 54 | npm ci 55 | npm run build-all-prod -------------------------------------------------------------------------------- /examples/typescript/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | require('dotenv').config({ path: './.env' }); 4 | 5 | module.exports = { 6 | entry: { 7 | index: './src/index.ts', 8 | }, 9 | plugins: [ 10 | new HtmlWebpackPlugin({ 11 | title: 'Scalable Pixel Streaming Frontend', 12 | template: './src/index.html', 13 | filename: 'index.html' 14 | }), 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | loader: 'ts-loader', 21 | exclude: [ 22 | /node_modules/, 23 | ], 24 | }, 25 | { 26 | test: /\.html$/i, 27 | use: 'html-loader' 28 | }, 29 | { 30 | test: /\.css$/, 31 | type: 'asset/resource', 32 | generator: { 33 | filename: '[name][ext]' 34 | } 35 | }, 36 | { 37 | test: /\.(png|svg)$/i, 38 | type: 'asset/resource', 39 | generator: { 40 | filename: 'images/[name][ext]' 41 | } 42 | }, 43 | ], 44 | }, 45 | resolve: { 46 | extensions: ['.tsx', '.ts', '.js', '.svg'], 47 | }, 48 | output: { 49 | filename: '[name].js', 50 | library: 'spstypescriptexample', 51 | libraryTarget: 'umd', 52 | path: path.resolve(__dirname, 'dist'), 53 | clean: true, 54 | globalObject: 'this', 55 | hashFunction: 'xxhash64' 56 | }, 57 | experiments: { 58 | futureDefaults: true 59 | }, 60 | optimization: { 61 | minimize: false 62 | }, 63 | devServer: { 64 | static: { 65 | directory: path.join(__dirname, 'dist'), 66 | }, 67 | }, 68 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalable Pixel Streaming frontend 2 | 3 | This repository contains the web frontend for [Scalable Pixel Streaming (SPS)](https://scalablestreaming.io), which is a lightweight wrapper implementation of Epic Games [Pixel Streaming frontend](https://github.com/EpicGamesExt/PixelStreamingInfrastructure/tree/master/Frontend). 4 | 5 | ## Features of the SPS frontend 6 | 7 | - [Extensions](./library/src/SignallingExtension.ts) to the default signalling messages to communicate with our custom signalling server. 8 | - [Streaming statistics](./library/src/SPSApplication.ts#L38) are being sent to our signalling server. 9 | 10 | ## Documentation 11 | 12 | ### Utilising the Scalable Pixel Streaming Frontend 13 | 14 | Refer to our [Scalable Pixel Streaming frontend utilisation guide](./docs/frontend_utilisation_guide.md) for accessing the Scalable Pixel Streaming frontend, downloading the library, consuming it, and customising it for usage in different projects. 15 | 16 | ### Migrating legacy Scalable Pixel Streaming frontend 17 | 18 | All SPS versions since `v0.1.4` are using the current version of Epic Games Pixel Streaming frontend. Refer to [our migration guide](./docs/api_transition_guide.md) if your frontend predates this version. 19 | 20 | ### Scalable Pixel Streaming frontend reference 21 | 22 | The Scalable Pixel Streaming frontend is part of a complex system that abstracts a lot of its complexities behind the library. Refer to our [reference guide](./docs/sps_frontend_reference_guide.md) to gain a deeper understanding of how the SPS frontend fits within Scalable Pixel Streaming as a whole. 23 | 24 | ## Issues 25 | 26 | As the SPS frontend is an implementation of the Epic Games Pixel Streaming frontend, the majority of issues will pertain to the Epic Games frontend and should be reported on [their repository](https://github.com/EpicGamesExt/PixelStreamingInfrastructure/issues). 27 | 28 | If you encounter an issue specific to the SPS implementation, please report it [here](https://github.com/ScalablePixelStreaming/Frontend/issues). 29 | 30 | 31 | ## Legal 32 | 33 | Copyright © 2021 - 2024, TensorWorks Pty Ltd. Licensed under the MIT License, see the [license file](./LICENSE) for details. 34 | -------------------------------------------------------------------------------- /library/src/LoadingOverlay.ts: -------------------------------------------------------------------------------- 1 | import { TextOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 2 | 3 | export class LoadingOverlay extends TextOverlay { 4 | 5 | private static _rootElement: HTMLElement; 6 | private static _textElement: HTMLElement; 7 | private static _spinner: HTMLElement; 8 | 9 | /** 10 | * @returns The created root element of this overlay. 11 | */ 12 | public static rootElement(): HTMLElement { 13 | if (!LoadingOverlay._rootElement) { 14 | LoadingOverlay._rootElement = document.createElement('div'); 15 | LoadingOverlay._rootElement.id = "loadingOverlay"; 16 | LoadingOverlay._rootElement.className = "textDisplayState"; 17 | } 18 | return LoadingOverlay._rootElement; 19 | } 20 | 21 | /** 22 | * @returns The created content element of this overlay, which contain whatever content this element contains, like text or a button. 23 | */ 24 | public static textElement(): HTMLElement { 25 | if (!LoadingOverlay._textElement) { 26 | LoadingOverlay._textElement = document.createElement('div'); 27 | LoadingOverlay._textElement.id = 'loadingOverlayText'; 28 | } 29 | return LoadingOverlay._textElement; 30 | } 31 | 32 | 33 | public static spinner(): HTMLElement { 34 | if (!LoadingOverlay._spinner) { 35 | // build the spinner div 36 | const size = LoadingOverlay._rootElement.clientWidth * 0.03; 37 | LoadingOverlay._spinner = document.createElement('div'); 38 | LoadingOverlay._spinner.id = "loading-spinner" 39 | LoadingOverlay._spinner.className = "loading-spinner"; 40 | LoadingOverlay._spinner.setAttribute("style", "width: " + size + "px; height: " + size + "px;"); 41 | 42 | const SpinnerSectionOne = document.createElement("div"); 43 | SpinnerSectionOne.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); 44 | LoadingOverlay._spinner.appendChild(SpinnerSectionOne); 45 | 46 | const SpinnerSectionTwo = document.createElement("div"); 47 | SpinnerSectionTwo.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); 48 | LoadingOverlay._spinner.appendChild(SpinnerSectionTwo); 49 | 50 | const SpinnerSectionThree = document.createElement("div"); 51 | SpinnerSectionThree.setAttribute("style", "width: " + size * 0.8 + "px; height: " + size * 0.8 + "px; border-width: " + size * 0.125 + "px;"); 52 | LoadingOverlay._spinner.appendChild(SpinnerSectionThree); 53 | } 54 | return LoadingOverlay._spinner; 55 | } 56 | /** 57 | * Construct a connect overlay with a connection button. 58 | * @param parentElem the parent element this overlay will be inserted into. 59 | */ 60 | public constructor(parentElem: HTMLElement) { 61 | super(parentElem, LoadingOverlay.rootElement(), LoadingOverlay.textElement()); 62 | } 63 | 64 | /** 65 | * Update the text overlays inner text 66 | * @param text the update text to be inserted into the overlay 67 | */ 68 | public update(text: string): void { 69 | if (text != null || text != undefined) { 70 | this.textElement.innerHTML = ""; 71 | 72 | let textContainer = document.createElement("div"); 73 | textContainer.innerHTML = text; 74 | this.textElement.append(textContainer); 75 | 76 | this.textElement.append(LoadingOverlay.spinner()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /docs/sps_frontend_reference_guide.md: -------------------------------------------------------------------------------- 1 | # Scalable Pixel Streaming frontend reference 2 | 3 | There are several phases involved in the SPS lifecycle, described in detail below. 4 | 5 | ### Authentication Phase 6 | 7 | 1) The signalling server transmits a message to the frontend indicating that authentication is required. 8 | 9 | 2) The player controller responds with an authentication request containing either an empty authentication token (if we have not yet received a token from an identity provider), or with an authentication token that had been obtained by means of a redirect during a previous run of the event lifecycle. 10 | 11 | 3) The signalling server communicates with the authentication plugin to determine what to do next: 12 | 13 | - If the no-op authentication method is used, then the signalling server transmits a response indicating that authentication was successful. 14 | 15 | - If any other authentication plugin is used and we _do not_ have an authentication token, then the signalling server transmits a response indicating that the user should be redirected to the login page for the identity provider. 16 | 17 | - If any other authentication plugin is used and we _do_ have an authentication token, then the signalling server transmits a response indicating whether the token was accepted as valid by the authentication plugin. 18 | 19 | Note that the path taken in step three is largely transparent to the logic in the frontend. 20 | 21 | 4) If a redirect is required, then the Scalable Pixel Streaming frontend will trigger it immediately after it has finished notifying the Epic Games Pixel Streaming frontend of the authentication status. After a redirect occurs and the identity provider's login page subsequently redirects back to the Scalable Pixel Streaming frontend, the page is reset and the event lifecycle restarts from the beginning, except that there is now an authentication token specified in the page's URL parameters, which will lead down a different path in the previous step. 22 | 23 | 5) Once the user has been successfully authenticated, the signalling server will initiate the creation of an instance of the Pixel Streaming application, beginning the instance startup phase. 24 | 25 | ### Instance startup phase 26 | 27 | 1) As the Pixel Streaming application instance is created, the signalling server transmits status update messages to the Epic Games Pixel Streaming frontend. 28 | 29 | 2) The Epic Games Pixel Streaming frontend notifies the SPS frontend of the application instance status, so it can inform the user through the page's UI. 30 | 31 | 3) Once the Pixel Streaming application instance has started, it will connect to the signalling server and initiate the WebRTC connection phase. 32 | 33 | ### Determining the WebSocket endpoint URL 34 | 35 | Prior to deploying the Scalable Pixel Streaming frontend, you will need to determine the endpoint URL that will be used to establish WebSocket connections to the signalling server/servers for your Pixel Streaming application: 36 | 37 | - If you are deploying your Pixel Streaming application in a single geographic region on a single cloud platform, this will be the signalling server endpoint URL reported by the Scalable Pixel Streaming REST API. 38 | 39 | - If you are deploying your Pixel Streaming application in multiple geographic regions or across multiple cloud platforms, this will be the URL of a load balancer that you have configured to distribute requests to the signalling servers in the various regions and/or platforms. 40 | -------------------------------------------------------------------------------- /library/src/index.ts: -------------------------------------------------------------------------------- 1 | // Scalable Pixel Streaming Frontend exports 2 | export { SPSApplication } from "./SPSApplication"; 3 | export { LoadingOverlay } from "./LoadingOverlay"; 4 | export { MessageStats } from "./Messages"; 5 | export { MessageAuthRequest, InstanceState, MessageInstanceState, MessageAuthResponseOutcomeType, MessageAuthResponse, MessageRequestInstance, SPSSignalling } from "./SignallingExtension"; 6 | 7 | // Epic Games Pixel Streaming Frontend exports 8 | export { WebRtcPlayerController, WebRtcDisconnectedEvent } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 9 | export { WebXRController } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 10 | export { Config, ControlSchemeType, Flags, NumericParameters, TextParameters, OptionParameters, FlagsIds, NumericParametersIds, TextParametersIds, OptionParametersIds, AllSettings } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 11 | export { SettingBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 12 | export { SettingFlag } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 13 | export { SettingNumber } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 14 | export { SettingOption } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 15 | export { SettingText } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 16 | export { PixelStreaming } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 17 | export { AfkLogic } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 18 | export { LatencyTestResults } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 19 | export { EncoderSettings, InitialSettings, WebRTCSettings } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 20 | export { AggregatedStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 21 | export { Logger } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 22 | export { UnquantizedAndDenormalizeUnsigned } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 23 | export { BaseMessage, MessageRegistry } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 24 | export { SignallingProtocol } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 25 | export { CandidatePairStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 26 | export { CandidateStat } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 27 | export { DataChannelStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 28 | export { InboundAudioStats, InboundVideoStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 29 | export { OutBoundVideoStats } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 30 | 31 | // Epic Games Pixel Streaming Frontend UI exports 32 | export { Application, UIOptions } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 33 | export { PixelStreamingApplicationStyle } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 34 | export { AFKOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 35 | export { ActionOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 36 | export { OverlayBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 37 | export { ConnectOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 38 | export { DisconnectOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 39 | export { ErrorOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 40 | export { InfoOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 41 | export { PlayOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 42 | export { TextOverlay } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 43 | export { ConfigUI } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 44 | export { SettingUIBase } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 45 | export { SettingUIFlag } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 46 | export { SettingUINumber } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 47 | export { SettingUIOption } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 48 | export { SettingUIText } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 49 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Config, PixelStreaming, SPSApplication, PixelStreamingApplicationStyle, Flags, TextParameters, BaseMessage, WebRtcDisconnectedEvent, UIOptions } from "@tensorworks/libspsfrontend"; 2 | 3 | // Apply default styling from Epic Games Pixel Streaming Frontend 4 | export const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); 5 | PixelStreamingApplicationStyles.applyStyleSheet(); 6 | 7 | // Extend the default "Config" message supplied by PSInfra library to include the following: 8 | // - Engine version 9 | // - Platform 10 | // - FrontendSendOffer 11 | class MessageExtendedConfig implements BaseMessage { 12 | type: string; 13 | peerConnectionOptions: RTCConfiguration; 14 | engineVersion: string; 15 | platform: string; 16 | frontendToSendOffer: boolean; 17 | }; 18 | 19 | // Extend PixelStreaming to use our custom extended config that includes the engine version 20 | class ScalablePixelStreaming extends PixelStreaming { 21 | // Create a new method that retains original functionality 22 | public handleOnConfig(messageExtendedConfig: MessageExtendedConfig) { 23 | this._webRtcController.handleOnConfigMessage(messageExtendedConfig); 24 | } 25 | }; 26 | 27 | document.body.onload = function () { 28 | 29 | // Uncomment and rebuild for detailed logging 30 | // Logger.SetLoggerVerbosity(10); 31 | 32 | // Create a config object. We default to sending the WebRTC offer from the browser as false, TimeoutIfIdle to true, AutoConnect to false and MaxReconnectAttempts to 0 33 | const config = new Config({ useUrlParams: true, initialSettings: { OfferToReceive: false, TimeoutIfIdle: true, AutoConnect: false, MaxReconnectAttempts: 0 } }); 34 | 35 | // Handle setting custom signalling url from code or by querying url parameters (e.g. ?ss=ws://my.signaling.server). 36 | { 37 | // Replace with your custom signalling url if you need to. 38 | // Otherwise SPS will use ws|wss://window.location.host/signalling/window.location.pathname 39 | let YOUR_CUSTOM_SIGNALLING_URL_HERE: string = ""; // <-- replace here 40 | 41 | // Check the ?ss= url parameter for a custom signalling url. 42 | const urlParams = new URLSearchParams(window.location.search); 43 | if (urlParams.has(TextParameters.SignallingServerUrl)) { 44 | YOUR_CUSTOM_SIGNALLING_URL_HERE = urlParams.get(TextParameters.SignallingServerUrl); 45 | } 46 | config.setTextSetting(TextParameters.SignallingServerUrl, YOUR_CUSTOM_SIGNALLING_URL_HERE); 47 | } 48 | 49 | // Create stream and spsApplication instances that implement the Epic Games Pixel Streaming Frontend PixelStreaming and Application types 50 | const stream = new ScalablePixelStreaming(config); 51 | 52 | // Override the onConfig so we can determine if we need to send the WebRTC offer based on what is sent from the signalling server 53 | stream.signallingProtocol.removeAllListeners("config"); 54 | stream.signallingProtocol.addListener("config", (config: MessageExtendedConfig) => { 55 | if (config.frontendToSendOffer) { 56 | stream.config.setFlagEnabled(Flags.BrowserSendOffer, config.frontendToSendOffer); 57 | } 58 | stream.handleOnConfig(config); 59 | }); 60 | 61 | // override the _onDisconnect function to intercept webrtc disconnect events 62 | // and modify how the event is fired by always showing a click to reconnect overlay. 63 | // we also add a full stop to the AFK message. 64 | stream._onDisconnect = function (eventString: string) { 65 | 66 | // check if the eventString coming in is the inactivity string and add a full stop 67 | if (eventString == "You have been disconnected due to inactivity") { 68 | eventString += "." 69 | } 70 | 71 | this._eventEmitter.dispatchEvent( 72 | new WebRtcDisconnectedEvent({ 73 | eventString: eventString, 74 | allowClickToReconnect: true 75 | }) 76 | ); 77 | } 78 | 79 | // Create our SPS application and pass it some UI configuration options. 80 | // Note: There are more options than this if you need them (e.g. turning off certain UI elements). 81 | const uiOptions: UIOptions = { 82 | stream: stream, 83 | onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode) /* Light/Dark mode support. */ 84 | }; 85 | const spsApplication: SPSApplication = new SPSApplication(uiOptions); 86 | document.body.appendChild(spsApplication.rootElement); 87 | } 88 | -------------------------------------------------------------------------------- /library/src/SPSApplication.ts: -------------------------------------------------------------------------------- 1 | import { Application, SettingUIFlag, UIOptions } from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5'; 2 | import { AggregatedStats, StatsReceivedEvent, SettingFlag, TextParameters } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 3 | import { LoadingOverlay } from './LoadingOverlay'; 4 | import { SPSSignalling } from './SignallingExtension'; 5 | import { MessageStats } from './Messages'; 6 | 7 | // For local testing. Declare a websocket URL that can be imported via a .env file that will override 8 | // the signalling server URL builder. 9 | declare var WEBSOCKET_URL: string; 10 | 11 | 12 | export class SPSApplication extends Application { 13 | private loadingOverlay: LoadingOverlay; 14 | private signallingExtension: SPSSignalling; 15 | private sendStatsToServerSetting: SettingFlag; 16 | 17 | static Flags = class { 18 | static sendToServer = "sendStatsToServer" 19 | } 20 | 21 | constructor(config: UIOptions) { 22 | super(config); 23 | this.signallingExtension = new SPSSignalling(this.stream.signallingProtocol); 24 | this.signallingExtension.onAuthenticationResponse = this.handleSignallingResponse.bind(this); 25 | this.signallingExtension.onInstanceStateChanged = this.handleSignallingResponse.bind(this); 26 | 27 | this.enforceSpecialSignallingServerUrl(); 28 | 29 | // Add 'Send Stats to Server' checkbox 30 | const spsSettingsSection = this.configUI.buildSectionWithHeading(this.settingsPanel.settingsContentElement, "Scalable Pixel Streaming"); 31 | this.sendStatsToServerSetting = new SettingFlag( 32 | SPSApplication.Flags.sendToServer, 33 | "Send stats to server", 34 | "Send session stats to the server", 35 | false, 36 | this.stream.config.useUrlParams 37 | ); 38 | 39 | spsSettingsSection.appendChild(new SettingUIFlag(this.sendStatsToServerSetting).rootElement); 40 | this.loadingOverlay = new LoadingOverlay(this.stream.videoElementParent); 41 | 42 | this.stream.addEventListener('statsReceived', (statsReceived: StatsReceivedEvent) => { this.handleStatsReceived(statsReceived); }); 43 | } 44 | 45 | handleStatsReceived(statsReceivedEvent: StatsReceivedEvent) { 46 | if(statsReceivedEvent && statsReceivedEvent.data && statsReceivedEvent.data.aggregatedStats) { 47 | if (this.sendStatsToServerSetting.flag) { 48 | this.sendStatsToSignallingServer(statsReceivedEvent.data.aggregatedStats); 49 | } 50 | } 51 | } 52 | 53 | handleSignallingResponse(signallingResp: string, isError: boolean) { 54 | if (isError) { 55 | this.showErrorOverlay(signallingResp); 56 | } else { 57 | this.showLoadingOverlay(signallingResp); 58 | } 59 | } 60 | 61 | enforceSpecialSignallingServerUrl() { 62 | // SPS needs to build a specific signalling server url based on the application name so K8s can distinguish it 63 | this.stream.setSignallingUrlBuilder(() => { 64 | 65 | // If we have overriden the signalling server URL with a .env file use it here 66 | if (WEBSOCKET_URL !== undefined ) { 67 | return WEBSOCKET_URL as string; 68 | } 69 | 70 | // If there is signalling url specified, then use that. 71 | let customSignallingUrl = this.stream.config.getTextSettingValue(TextParameters.SignallingServerUrl); 72 | if(customSignallingUrl && customSignallingUrl !== "") { 73 | return customSignallingUrl; 74 | } 75 | 76 | // If neither environment used or customSignallingUrl specified, then build the URL using the domain we are on. 77 | 78 | // Construct the signalling url from the base url, prepend protocol, then append /signalling, then append /rest-of-path?myargs 79 | const urlProtocol: string = window.location.protocol === 'http:' ? 'ws://' : 'wss://'; 80 | const urlBase: string = window.location.host; 81 | const urlPath: string = window.location.pathname; 82 | // Build the signalling URL based on the existing window location, the result should be 'domain.com/signalling/app-name' 83 | const signallingUrl = urlProtocol + urlBase + "/signalling" + urlPath; 84 | return signallingUrl; 85 | }); 86 | } 87 | 88 | showLoadingOverlay(signallingResp: string) { 89 | this.hideCurrentOverlay(); 90 | this.loadingOverlay.show(); 91 | this.loadingOverlay.update(signallingResp); 92 | 93 | this.currentOverlay = this.loadingOverlay; 94 | } 95 | 96 | /** 97 | * Send Aggregated Stats to the Signaling Server 98 | * @param stats - Aggregated Stats 99 | */ 100 | sendStatsToSignallingServer(stats: AggregatedStats) { 101 | const data = new MessageStats(stats); 102 | this.stream.signallingProtocol.sendMessage(data); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /library/src/SignallingExtension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Logger, 3 | BaseMessage, 4 | SignallingProtocol, 5 | } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.5'; 6 | 7 | /** 8 | * Auth Request Message Wrapper 9 | */ 10 | export class MessageAuthRequest implements BaseMessage { 11 | type: string; 12 | token: string; 13 | provider: string; 14 | 15 | /** 16 | * @param token - Token Provided by the Auth Provider 17 | * @param provider - Name of the provider that is registered in the auth plugin 18 | */ 19 | constructor(token: string, provider: string) { 20 | this.type = "authenticationRequest"; 21 | this.token = token; 22 | this.provider = provider; 23 | } 24 | } 25 | 26 | /** 27 | * States of the UE Instance 28 | */ 29 | export enum InstanceState { 30 | UNALLOCATED = "UNALLOCATED", 31 | PENDING = "PENDING", 32 | FAILED = "FAILED", 33 | READY = "READY" 34 | } 35 | 36 | /** 37 | * Instance State Message wrapper 38 | */ 39 | export class MessageInstanceState { 40 | state: InstanceState; 41 | details: string; 42 | progress: number; 43 | } 44 | 45 | /** 46 | * Types of Authentication responses 47 | */ 48 | export enum MessageAuthResponseOutcomeType { 49 | REDIRECT = "REDIRECT", 50 | INVALID_TOKEN = "INVALID_TOKEN", 51 | AUTHENTICATED = "AUTHENTICATED", 52 | ERROR = "ERROR" 53 | } 54 | 55 | /** 56 | * Structure for auth responses 57 | */ 58 | export class MessageAuthResponse { 59 | outcome: MessageAuthResponseOutcomeType; 60 | redirect: string; 61 | error: string; 62 | } 63 | 64 | /** 65 | * Instance Request Message Wrapper 66 | */ 67 | export class MessageRequestInstance implements BaseMessage { 68 | 69 | type: string; 70 | 71 | // An opaque string representing optional configuration data to pass to the signalling server for instance customisation 72 | data: string; 73 | 74 | constructor() { 75 | this.type = "requestInstance"; 76 | } 77 | } 78 | 79 | /** 80 | * Specific signalling extensions required by SPS. 81 | * For example: authenticationRequired, instanceState, authenticationResponse 82 | */ 83 | export class SPSSignalling { 84 | 85 | onInstanceStateChanged: (stateChangedMsg: string, isError: boolean) => void; 86 | onAuthenticationResponse: (authRespMsg: string, isError: boolean) => void; 87 | 88 | constructor(signallingProtocol: SignallingProtocol) { 89 | this.extendSignallingProtocol(signallingProtocol); 90 | } 91 | 92 | /** 93 | * Extend the signalling protocol with SPS specific messages. 94 | */ 95 | extendSignallingProtocol(signallingProtocol: SignallingProtocol) { 96 | 97 | // authenticationRequired 98 | signallingProtocol.addListener("authenticationRequired", (authReqPayload: BaseMessage) => { 99 | Logger.Log(Logger.GetStackTrace(), "AUTHENTICATION_REQUIRED", 6); 100 | const url_string = window.location.href; 101 | const url = new URL(url_string); 102 | const authRequest = new MessageAuthRequest(url.searchParams.get("code"), url.searchParams.get("provider")); 103 | signallingProtocol.sendMessage(authRequest); 104 | }); 105 | 106 | // instanceState 107 | signallingProtocol.addListener("instanceState", (instanceState: MessageInstanceState) => { 108 | Logger.Log(Logger.GetStackTrace(), "INSTANCE_STATE", 6); 109 | this.handleInstanceStateChanged(instanceState); 110 | }); 111 | 112 | // authenticationResponse 113 | signallingProtocol.addListener("authenticationResponse", (authenticationResponse: MessageAuthResponse) => { 114 | Logger.Log(Logger.GetStackTrace(), "AUTHENTICATION_RESPONSE", 6); 115 | 116 | this.handleAuthenticationResponse(authenticationResponse); 117 | 118 | switch (authenticationResponse.outcome) { 119 | case MessageAuthResponseOutcomeType.REDIRECT: { 120 | window.location.href = authenticationResponse.redirect; 121 | break; 122 | } 123 | case MessageAuthResponseOutcomeType.AUTHENTICATED: { 124 | Logger.Log(Logger.GetStackTrace(), "User is authenticated and now requesting an instance", 6); 125 | signallingProtocol.sendMessage(new MessageRequestInstance()); 126 | break; 127 | } 128 | case MessageAuthResponseOutcomeType.INVALID_TOKEN: { 129 | Logger.Info(Logger.GetStackTrace(), "Authentication error : Invalid Token"); 130 | break; 131 | } 132 | case MessageAuthResponseOutcomeType.ERROR: { 133 | Logger.Info(Logger.GetStackTrace(), "Authentication Error from server Check what you are sending"); 134 | break; 135 | } 136 | default: { 137 | Logger.Error(Logger.GetStackTrace(), "The Outcome Message has not been handled : this is really bad"); 138 | break; 139 | } 140 | } 141 | 142 | }); 143 | } 144 | 145 | /** 146 | * Set up functionality to happen when an instance state change occurs and updates the info overlay with the response 147 | * @param instanceState - the message instance state 148 | */ 149 | handleInstanceStateChanged(instanceState: MessageInstanceState) { 150 | let instanceStateMessage = ""; 151 | let isInstancePending = false; 152 | let isError = false; 153 | 154 | // get the response type 155 | switch (instanceState.state) { 156 | case InstanceState.UNALLOCATED: 157 | instanceStateMessage = "Instance Unallocated: " + instanceState.details; 158 | break; 159 | case InstanceState.FAILED: 160 | instanceStateMessage = "UE Instance Failed: " + instanceState.details; 161 | isError = true; 162 | break; 163 | case InstanceState.PENDING: 164 | isInstancePending = true; 165 | if (instanceState.details == undefined || instanceState.details == null) { 166 | instanceStateMessage = "Pending"; 167 | } else { 168 | instanceStateMessage = instanceState.details; 169 | } 170 | instanceStateMessage = "Step 2/3: " + instanceStateMessage; 171 | break; 172 | case InstanceState.READY: 173 | if (instanceState.details == undefined || instanceState.details == null) { 174 | instanceStateMessage = "Ready"; 175 | } else { 176 | instanceStateMessage = "Ready: " + instanceState.details; 177 | } 178 | instanceStateMessage = "Step 3/3: " + instanceStateMessage; 179 | break; 180 | default: 181 | instanceStateMessage = "Unhandled Instance State" + instanceState.state + " " + instanceState.details; 182 | break; 183 | } 184 | 185 | if (isError) { 186 | this.onInstanceStateChanged(instanceStateMessage, true); 187 | } else { 188 | this.onInstanceStateChanged(instanceStateMessage, false); 189 | } 190 | } 191 | 192 | /** 193 | * Set up functionality to happen when receiving an auth response and updates an info overlay with the response 194 | * @param authResponse - the auth response message type 195 | */ 196 | handleAuthenticationResponse(authResponse: MessageAuthResponse) { 197 | let instanceStateMessage = ""; 198 | let isError = false; 199 | 200 | // get the response type 201 | switch (authResponse.outcome) { 202 | case MessageAuthResponseOutcomeType.AUTHENTICATED: 203 | instanceStateMessage = "Step 1/3: Requesting Instance"; 204 | break; 205 | case MessageAuthResponseOutcomeType.INVALID_TOKEN: 206 | instanceStateMessage = "Invalid Token: " + authResponse.error; 207 | isError = true; 208 | break; 209 | case MessageAuthResponseOutcomeType.REDIRECT: 210 | instanceStateMessage = "Redirecting to: " + authResponse.redirect; 211 | break; 212 | case MessageAuthResponseOutcomeType.ERROR: 213 | instanceStateMessage = "Error: " + authResponse.error; 214 | isError = true; 215 | break; 216 | default: 217 | instanceStateMessage = "Unhandled Auth Response: " + authResponse.outcome; 218 | break; 219 | } 220 | 221 | this.onAuthenticationResponse(instanceStateMessage, isError); 222 | } 223 | } -------------------------------------------------------------------------------- /docs/frontend_utilisation_guide.md: -------------------------------------------------------------------------------- 1 | # Utilising the frontend 2 | 3 | ## Overview 4 | The Scalable Pixel Streaming Frontend is a library of HTML, CSS and TypeScript code that runs in client web browsers to help users connect to Scalable Pixel Streaming applications and interact with them. It is able to achieve this by consuming the Pixel Streaming Frontend and UI libraries and by extending their signalling server and WebSocket packages the Pixel Streaming Frontend can be configured to work with Scalable Pixel Streaming signalling severs. 5 | 6 | ## Epic Games Pixel Streaming Frontend and UI Frontend 7 | For the base functionality for Pixel Streaming and its UI capabilities the Scalable Pixel Streaming Frontend consumes the Epic Games Pixel Streaming Frontend and UI Frontend: 8 | - [Pixel Streaming Frontend](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ue5.5) 9 | - [Pixel Streaming Frontend UI](https://www.npmjs.com/package/@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.5) 10 | 11 | ### Pixel Streaming Frontend 12 | The Pixel Streaming Frontend contains all the base functionality: 13 | - WebSocket handling. 14 | - Data channel handling. 15 | - UE message handling. 16 | - Mouse and keyboard interaction handling. 17 | - Video and audio stream handling. 18 | - Logic for: AFK, FreezeFrames, Mic, TURN, SDP. 19 | 20 | ### Pixel Streaming Frontend UI 21 | The Pixel Streaming Frontend UI contains all the functionality for UI components: 22 | - Text, Action and AFK Overlays. 23 | - CSS styling. 24 | - UI display settings. 25 | - UE stream data. 26 | 27 | --- 28 | 29 | ## SPS frontend packages 30 | 31 | ### SPS frontend library 32 | 33 | The library includes all of the custom signalling logic that Scalable Pixel Streaming signalling servers require to work. The library can be obtained either through [GitHub](https://github.com/ScalablePixelStreaming/Frontend) or [NPM](https://www.npmjs.com/package/@tensorworks/libspsfrontend). The library must be initialised via HTML and Javascript. It is written in TypeScript, but configured to export as a UMD module and can be consumed by plain JavaScript and most JavaScript frameworks. 34 | 35 | ### SPS frontend TypeScript example 36 | 37 | Our [TypeScript example](https://github.com/ScalablePixelStreaming/Frontend/tree/main/examples/typescript) is a simple HTML, CSS, and TypeScript implementation that initialises the SPS frontend library by instantiating the library components and starting a connection to the signalling server. 38 | 39 | ## Installing and consuming SPS packages 40 | 41 | Download the [SPS frontend source code from GitHub](https://github.com/ScalablePixelStreaming/Frontend). 42 | 43 | ### Building for development and production 44 | 45 | SPS frontend packages contain several NPM scripts that can build the library and example implementation for either development or production. When building for development, source maps for debugging will be enabled. When building for production, source maps will be disabled, reducing console output and minifying the distributed JavaScript files. Below is a list of NPM scripts for both the library and example implementation with their respective commands. 46 | 47 | #### Building library 48 | 49 | First, install all required dependencies by running this command from the `library` directory: 50 | 51 | - `npm install`: Install the frontend library dependencies 52 | 53 | The following build scripts must be executed from the same directory: 54 | 55 | - `npm run build-dev`: Build the library in development mode 56 | - `npm run build-prod`: Build the library in production mode 57 | 58 | #### Building example and linking the library 59 | 60 | The library must be installed before executing example scripts. All example scripts must be executed from the `examples/typescript` directory: 61 | 62 | - `npm run build-dev`: Build the library in development mode 63 | - `npm run build-prod`: Build the library in production mode 64 | - `npm run serve-dev`: Serve the example locally using the library in development mode 65 | - `npm run serve-prod`: Serve the example locally using the library in production mode 66 | - `npm run symlink`: Link the library to the example for consumption 67 | 68 | #### Building and linking library and example with a single command 69 | 70 | Alternatively, you can run the build all scripts from the `examples/typescript` directory to install and link the library and the example at the same time: 71 | 72 | - `npm run build-all-dev`: Build the library and the example in development mode and link the library to the example for consumption 73 | - `npm run build-all-prod`: Build the library and the example in production mode and link the library to the example for consumption 74 | 75 | ### Installing the Scalable Pixel Streaming Frontend through NPM 76 | 77 | 1) If your project includes a `package.json` file, run the following command in the same directory: 78 | 79 | - `npm i @tensorworks/libspsfrontend` 80 | 81 | 2) Import your desired components from the library package `"@tensorworks/libspsfrontend"` 82 | 83 | #### Initialising and consuming the library 84 | 85 | The following example for initialising the library is based on the TypeScript example provided on GitHub. 86 | 87 | 1) Import all the required objects, types, and packages from the SPS frontend library: 88 | 89 | ```typescript 90 | import {Config, PixelStreaming, SPSApplication, TextParameters, PixelStreamingApplicationStyle} from "@tensorworks/libspsfrontend"; 91 | ``` 92 | 93 | 2) Apply default styling from Epic Games Pixel Streaming frontend: 94 | 95 | ```typescript 96 | export const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); 97 | PixelStreamingApplicationStyles.applyStyleSheet(); 98 | ``` 99 | 100 | 3) Create a `webSocketAddress` variable, so that the WebSocket URL could be modified if a user wishes to inject their own WebSocket address at load time: 101 | 102 | ```typescript 103 | let webSocketAddress = ""; 104 | ``` 105 | 106 | 4) Create a `document.body.onload` function to automate the activation and creation of the remaining steps: 107 | 108 | ```typescript 109 | document.body.onload = function () { 110 | // steps 5-8 go in here 111 | } 112 | ``` 113 | 114 | 5) Create the Pixel Streaming `config` object and ensure that `useUrlParams` is true, and `initialSettings` contains `{ OfferToReceive: true, TimeoutIfIdle: true }`. This is important as the SPS signalling server can only receive the offer to connect. `TimeoutIfIdle` enables the AFK timeout by default, so that any unattended sessions close and stop consuming unnecessary cloud GPU resources: 115 | 116 | ```typescript 117 | const config = new Config({ useUrlParams: true, initialSettings: { OfferToReceive: true, TimeoutIfIdle: true } }); 118 | ``` 119 | 120 | 6) Create an if statement that will make use of the `webSocketAddress` variable if one is included: 121 | 122 | ```typescript 123 | if(webSocketAddress != ""){ 124 | config.setTextSetting(TextParameters.SignallingServerUrl, webSocketAddress) 125 | } 126 | ``` 127 | 128 | 7) Create an instance of the `PixelStreaming` object called `stream` and an instance of the `SPSApplication` object called `spsApplication`: 129 | 130 | ```typescript 131 | 132 | // Create stream and spsApplication instances that implement the Epic Games Pixel Streaming Frontend PixelStreaming and Application types 133 | const stream = new PixelStreaming(config); 134 | 135 | // Create our SPS application and pass it some UI configuration options. 136 | // Note: There are more options than this if you need them (e.g. turning off certain UI elements). 137 | const uiOptions: UIOptions = { 138 | stream: stream, 139 | onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode) /* Light/Dark mode support. */ 140 | }; 141 | 142 | // Create our application 143 | const spsApplication: SPSApplication = new SPSApplication(uiOptions); 144 | ``` 145 | 146 | 8) Append the `spsApplication.rootElement` inside a DOM element of your choice or inject directly into the body of the web page, like in the TypeScript example: 147 | 148 | ```typescript 149 | document.body.appendChild(spsApplication.rootElement); 150 | //OR 151 | document.getElementById("myElementId").appendChild(spsApplication.rootElement); 152 | ``` 153 | 154 | ### Default index implementation 155 | 156 | A default index implementation would look like this: 157 | 158 | ```typescript 159 | import {Config, PixelStreaming, SPSApplication, TextParameters, PixelStreamingApplicationStyle} from "@tensorworks/libspsfrontend"; 160 | export const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle(); 161 | PixelStreamingApplicationStyles.applyStyleSheet(); 162 | let webSocketAddress = ""; 163 | 164 | document.body.onload = function () { 165 | const config = new Config({ useUrlParams: true, initialSettings: { OfferToReceive: true, TimeoutIfIdle: true } }); 166 | if(webSocketAddress != ""){ 167 | config.setTextSetting(TextParameters.SignallingServerUrl, webSocketAddress) 168 | } 169 | const stream = new PixelStreaming(config); 170 | const spsApplication = new SPSApplication({ 171 | stream, 172 | onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode) /* Light/Dark mode support. */ 173 | }); 174 | document.body.appendChild(spsApplication.rootElement); 175 | } 176 | ``` 177 | 178 | ### Customising the WebSocket connection 179 | 180 | #### Using setTextSetting within Config to inject a custom WebSocket 181 | 182 | When serving the SPS frontend, it will build a default WebSocket address to connect to, based on the address of the current window of the webpage. If the WebSocket address matches what is created by default, then no further steps are required. Users can override the default by using the `setTextSetting` method on the `config` instance. Refer to [Basics to initialising and consuming the library](frontend_utilisation_guide.md#basics-to-initialising-and-consuming-the-library), steps 3 and 6. 183 | 184 | #### The .env file for the TypeScript example 185 | 186 | In the TypeScript example, there is a `.env.example` file containing a filler URL in the `WEBSOCKET_URL` line. This file can be used to hard code a WebSocket address that can be consumed by the example as shown above. This example is able to work with the help of the [dotenv NPM package](https://www.npmjs.com/package/dotenv) in the `webpack.common.js` file in the TypeScript example. To implement this example, follow these steps: 187 | 188 | 1) Rename the `.env.example` to `.env`. 189 | 2) Replace the placeholder URL with the WebSocket URL you wish to consume. 190 | 3) Rebuild the example with the `npm run build-dev` or `npm run build-prod` for the changes to take effect. 191 | 192 | If you wish to include this functionality in your project, you will need to include the following steps, which are also demonstrated in the TypeScript example: 193 | 194 | 1) Install `dotenv` via NPM `npm i dotenv --save-dev`. 195 | 2) Include `dotenv` in your webpack file and set your `.env` file path using `path:`: 196 | 197 | ```javascript 198 | require('dotenv').config({ path: './.env' }); 199 | ``` 200 | 201 | 3) Include a plugin in your webpack file with the environment variable's name. For this example, the name will be set to `WEBSOCKET_URL`: 202 | 203 | ```javascript 204 | new webpack.DefinePlugin({ 205 | WEBSOCKET_URL: JSON.stringify((process.env.WEBSOCKET_URL !== undefined) ? process.env.WEBSOCKET_URL : '') 206 | }), 207 | ``` 208 | 209 | 4) Create the `.env` file in the path you set in the previous step with the variable of your choice: 210 | 211 | ```bash 212 | WEBSOCKET_URL=ws://example.com/your/ws 213 | ``` 214 | 215 | 5) Declare your environment variable where you instantiate your SPS frontend library: 216 | 217 | ```typescript 218 | declare var WEBSOCKET_URL: string; 219 | ``` 220 | 221 | 6) Make use of the `setTextSetting` method within the `config` instance to set the `TextParameters.SignallingServerUrl` to a variable that makes use of `WEBSOCKET_URL`: 222 | 223 | ```typescript 224 | let webSocketAddress = WEBSOCKET_URL; 225 | if(webSocketAddress != ""){ 226 | config.setTextSetting(TextParameters.SignallingServerUrl, webSocketAddress) 227 | } 228 | ``` 229 | 230 | ## SPS Frontend UI element customisation 231 | Further customisation of UI elements like overlays or visual elements can also be achieved by utilising the Pixel Streaming Frontend UI and extending its types. For further information on how to utilise the Epic Games Pixel Streaming Frontend UI refer to the [Pixel Streaming Frontend UI documentation](https://github.com/EpicGames/PixelStreamingInfrastructure#readme). 232 | 233 | ## Building a frontend container 234 | This may be useful if you need to make modifications to the default SPS frontend and want to deploy it in your SPS installation. 235 | 236 | 1. Build the `examples/typescript` frontend using the instructions above. 237 | 2. Navigate to the root of this repository. 238 | 3. `docker build -t yourdockerhubaccount/my-custom-sps-frontend:latest -f dockerfiles/sps-frontend.dockerfile .` -------------------------------------------------------------------------------- /examples/typescript/src/assets/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 1690 | 1697 | 1698 | 1699 | 1700 | 1701 | 1751 | --------------------------------------------------------------------------------