moved to root directory
@@ -0,0 +1,4 @@
|
||||
logs/*.log
|
||||
node_modules
|
||||
platform_scripts
|
||||
tps
|
||||
@@ -0,0 +1,25 @@
|
||||
# Use the current Long Term Support (LTS) version of Node.js
|
||||
FROM node:lts
|
||||
|
||||
# Copy the signalling server source code from the build context
|
||||
COPY . /opt/SignallingWebServer
|
||||
|
||||
# Install the dependencies for the signalling server
|
||||
WORKDIR /opt/SignallingWebServer
|
||||
RUN npm install .
|
||||
|
||||
# Expose TCP ports 80 and 443 for player WebSocket connections and web server HTTP(S) access
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
# Expose TCP port 8888 for streamer WebSocket connections
|
||||
EXPOSE 8888
|
||||
|
||||
# Expose TCP port 8889 for connections from the SFU
|
||||
EXPOSE 8889
|
||||
|
||||
# Expose TCP port 9999 for connections from the Matchmaker
|
||||
EXPOSE 9999
|
||||
|
||||
# Set the signalling server as the container's entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/node", "/opt/SignallingWebServer/cirrus.js"]
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54 35.9999L27.0001 53.9999L27.0001 18L54 35.9999Z" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
@@ -0,0 +1,49 @@
|
||||
/*Copyright Epic Games, Inc. All Rights Reserved.*/
|
||||
|
||||
:root {
|
||||
/*Using colour scheme https://color.adobe.com/TD-Colors---Option-3-color-theme-10394433/*/
|
||||
--colour1:#2B3A42;
|
||||
--colour2:#3F5765;
|
||||
--colour3:#BDD4DE;
|
||||
--colour4:#EFEFEF;
|
||||
--colour5:#FF5035;
|
||||
}
|
||||
|
||||
form{
|
||||
margin: 0px auto;
|
||||
padding: 1em;
|
||||
width: 350px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #CCC;
|
||||
background-color: var(--colour4)
|
||||
}
|
||||
|
||||
.entry{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input {
|
||||
text-indent: 5px;
|
||||
font-family: verdana,sans-serif;
|
||||
font-size: 1em;
|
||||
|
||||
width: 65%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0px auto;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
font-family: verdana,sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link type="text/css" rel="stylesheet" href="login.css">
|
||||
<title>Login</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
<div class="entry">
|
||||
<label for="name">Username:</label> <input type="text" id="username" name="username" autocomplete="username">
|
||||
</div>
|
||||
<div class="entry">
|
||||
<label for="mail">Password:</label> <input type="password" id="password" name="password" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="entry button">
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="106" height="106" viewBox="0 0 106 106" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="53" cy="53" r="53" fill="#454545"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 155 B |
@@ -0,0 +1,730 @@
|
||||
/*Copyright Epic Games, Inc. All Rights Reserved.*/
|
||||
@import url(./fonts/fonts.css);
|
||||
|
||||
:root {
|
||||
/*Using colour scheme https://color.adobe.com/TD-Colors---Option-3-color-theme-10394433/*/
|
||||
--colour1: #151619;
|
||||
;
|
||||
--colour2: #FFFFFF;
|
||||
--colour3: #0585fe;
|
||||
--colour4: #35b350;
|
||||
--colour5: #ffab00;
|
||||
--colour6: #1e1d22;
|
||||
--colour7: #3c3b40;
|
||||
}
|
||||
|
||||
#loader {
|
||||
width: 106px;
|
||||
height: 106px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: conic-gradient(from 135deg at 50% 50%,
|
||||
rgba(255, 255, 255, 0) -6.26deg,
|
||||
#ffffff 314.83deg,
|
||||
rgba(255, 255, 255, 0) 353.74deg,
|
||||
#ffffff 674.83deg);
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#loader::after {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: #151619;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
background-color: #151619;
|
||||
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "GilroyWebRegular";
|
||||
}
|
||||
|
||||
#playerUI {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: crisp-edges;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#videoPlayOverlay {
|
||||
position: absolute;
|
||||
font-size: 1.8em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--colour2)
|
||||
}
|
||||
|
||||
/* State for element to be clickable */
|
||||
.clickableState {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* State for element to show text, this is for informational use*/
|
||||
.textDisplayState {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* State to hide overlay, WebRTC communication is in progress and or is playing */
|
||||
.hiddenState {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#playButton {
|
||||
display: block;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
z-index: 30;
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 112px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#container {
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
/* Background */
|
||||
background: transparent;
|
||||
/* Button_1 */
|
||||
border-width: 0px 2px;
|
||||
border-style: solid;
|
||||
border-color: #23242A;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
padding: 40px 56px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
#container {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#playButton:hover {
|
||||
background: linear-gradient(180deg, #BC75FF 0%, #798FFF 100%);
|
||||
backdrop-filter: blur(10px)
|
||||
}
|
||||
|
||||
#title {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 38px;
|
||||
line-height: 100%;
|
||||
/* or 38px */
|
||||
/* White */
|
||||
color: #F2F2F2;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
#caption {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 140%;
|
||||
/* or 22px */
|
||||
text-align: center;
|
||||
/* Inactive */
|
||||
color: #C5C7CE;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
#link {
|
||||
font-family: 'Inter';
|
||||
cursor: pointer;
|
||||
background: #1C1D21;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 130%;
|
||||
/* identical to box height, or 16px */
|
||||
/* Inactive */
|
||||
color: #C5C7CE;
|
||||
}
|
||||
|
||||
#link:hover {
|
||||
background: #23242A;
|
||||
}
|
||||
|
||||
|
||||
#freezeFrameOverlay {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.freezeframeBackground {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
color: var(--colour2);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#overlay button {
|
||||
background-color: var(--colour7);
|
||||
border: 1px solid var(--colour7);
|
||||
color: var(--colour2);
|
||||
position: relative;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#fullscreen-btn {
|
||||
padding: 0.6rem !important;
|
||||
}
|
||||
|
||||
#overlay button:hover {
|
||||
background-color: var(--colour3);
|
||||
border: 3px solid var(--colour3);
|
||||
transition: 0.25s ease;
|
||||
padding-left: 0.55rem;
|
||||
padding-top: 0.55rem;
|
||||
}
|
||||
|
||||
#overlay button:active {
|
||||
border: 3px solid var(--colour3);
|
||||
background-color: var(--colour7);
|
||||
padding-left: 0.55rem;
|
||||
padding-top: 0.55rem;
|
||||
}
|
||||
|
||||
#overlay img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
color: var(--colour2);
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
padding: 0px 10px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.75px;
|
||||
/* Position the tooltip */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(25%);
|
||||
left: 125%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
background-color: var(--colour7);
|
||||
}
|
||||
|
||||
#connection .tooltiptext {
|
||||
top: 125%;
|
||||
transform: translateX(-25%);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
#settings-panel .tooltiptext {
|
||||
display: block;
|
||||
top: 125%;
|
||||
transform: translateX(-50%);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
padding: 5px 10px;
|
||||
border: 3px solid var(--colour5);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
top: 2%;
|
||||
left: 1%;
|
||||
font-family: 'Michroma', sans-serif;
|
||||
pointer-events: all;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#controls>* {
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 2rem;
|
||||
line-height: 1.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
#controls #additionalinfo {
|
||||
text-align: center;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
#unrealengine {
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
right: 10%;
|
||||
font-family: 'Michroma', sans-serif;
|
||||
pointer-events: all;
|
||||
visibility: hidden;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
#unrealengine p {
|
||||
visibility: hidden;
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
#connection {
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
left: 10%;
|
||||
font-family: 'Michroma', sans-serif;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none;
|
||||
/* iOS Safari */
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-khtml-user-select: none;
|
||||
/* Konqueror HTML */
|
||||
-moz-user-select: none;
|
||||
/* Old versions of Firefox */
|
||||
-ms-user-select: none;
|
||||
/* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
/* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
.panel-wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
min-width: 20vw;
|
||||
transform: translateX(100%);
|
||||
transition: .3s ease-out;
|
||||
pointer-events: all;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: rgba(30, 29, 34, 0.5)
|
||||
}
|
||||
|
||||
.panel-wrap-visible {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.panel {
|
||||
color: #eee;
|
||||
overflow-y: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#heading {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
margin-block-start: 0.67em;
|
||||
margin-block-end: 0.67em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
position: relative;
|
||||
padding: 0 0 0 2rem;
|
||||
}
|
||||
|
||||
#close {
|
||||
margin: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
font-size: 2em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#close:after {
|
||||
padding-left: 0.5rem;
|
||||
display: inline-block;
|
||||
content: "\00d7";
|
||||
/* This will render the 'X' */
|
||||
}
|
||||
|
||||
#close:hover {
|
||||
color: var(--colour3);
|
||||
transition: ease 0.3s;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.setting {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.settings-text {
|
||||
margin-right: 2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/*** Toggle Switch styles ***/
|
||||
.tgl-switch {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tgl-switch .tgl {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tgl,
|
||||
.tgl:after,
|
||||
.tgl:before,
|
||||
.tgl *,
|
||||
.tgl *:after,
|
||||
.tgl *:before,
|
||||
.tgl+.tgl-slider {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tgl::-moz-selection,
|
||||
.tgl:after::-moz-selection,
|
||||
.tgl:before::-moz-selection,
|
||||
.tgl *::-moz-selection,
|
||||
.tgl *:after::-moz-selection,
|
||||
.tgl *:before::-moz-selection,
|
||||
.tgl+.tgl-slider::-moz-selection {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tgl::selection,
|
||||
.tgl:after::selection,
|
||||
.tgl:before::selection,
|
||||
.tgl *::selection,
|
||||
.tgl *:after::selection,
|
||||
.tgl *:before::selection,
|
||||
.tgl+.tgl-slider::selection {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tgl+.tgl-slider {
|
||||
outline: 0;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tgl+.tgl-slider:after,
|
||||
.tgl+.tgl-slider:before {
|
||||
position: relative;
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tgl+.tgl-slider:after {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tgl+.tgl-slider:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tgl-flat+.tgl-slider {
|
||||
padding: 2px;
|
||||
-webkit-transition: all .2s ease;
|
||||
transition: all .2s ease;
|
||||
background: var(--colour6);
|
||||
border: 3px solid var(--colour7);
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
.tgl-flat+.tgl-slider:after {
|
||||
-webkit-transition: all .2s ease;
|
||||
transition: all .2s ease;
|
||||
background: var(--colour7);
|
||||
content: "";
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.tgl-flat:checked+.tgl-slider {
|
||||
border: 3px solid var(--colour3);
|
||||
}
|
||||
|
||||
.tgl-flat:checked+.tgl-slider:after {
|
||||
left: 50%;
|
||||
background: var(--colour3);
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
margin: 0 0 0 1rem;
|
||||
color: var(--colour5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding-top: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
row-gap: 4px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
color: var(--colour2);
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#stats {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#LatencyStats {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#hiddenInput {
|
||||
position: absolute;
|
||||
left: -10%;
|
||||
/* Although invisible, push off-screen to prevent user interaction. */
|
||||
width: 0px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#editTextButton {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-right: 2rem;
|
||||
min-width: 75%;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.warning {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transform: scale(var(--ggs, 1));
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid;
|
||||
border-radius: 40px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.warning::after,
|
||||
.warning::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
width: 2px;
|
||||
background: currentColor;
|
||||
left: 7px
|
||||
}
|
||||
|
||||
.warning::after {
|
||||
top: 2px;
|
||||
height: 8px
|
||||
}
|
||||
|
||||
.warning::before {
|
||||
height: 2px;
|
||||
bottom: 2px
|
||||
}
|
||||
|
||||
/* Flat buttons */
|
||||
input[type="button"] {
|
||||
background-color: transparent;
|
||||
color: var(--colour2);
|
||||
font-family: 'Montserrat';
|
||||
border: 3px solid var(--colour3);
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="button"]:hover {
|
||||
background-color: var(--colour3);
|
||||
transition: ease 0.3s;
|
||||
}
|
||||
|
||||
input[type="button"]:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#encoder-params-submit,
|
||||
#webrtc-params-submit {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="number"] {
|
||||
background-color: var(--colour7);
|
||||
color: var(--colour2);
|
||||
border: 1px solid var(--colour6);
|
||||
padding: 0.25rem;
|
||||
font-family: 'Montserrat';
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="number"]:disabled {
|
||||
padding-right: 0.5rem;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type=number]:disabled::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
#settingsBtn,
|
||||
#statsBtn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#streamingVideo {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
embed {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
g {
|
||||
fill: var(--colour2);
|
||||
}
|
||||
|
||||
object {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#connectionStrength {
|
||||
fill: var(--colour7);
|
||||
}
|
||||
|
||||
#minimize {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#afkOverlay {
|
||||
z-index: 999;
|
||||
background-color: rgba(30, 29, 34, 0.5);
|
||||
display: inline-block;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
line-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#afkOverlay center {
|
||||
display: inline-block;
|
||||
line-height: 1.5;
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
|
||||
<link type="text/css" rel="stylesheet" href="player.css">
|
||||
<script type="text/javascript">
|
||||
// This horrible hack is to make Fippo's adapter-latest.js work with Firefox when not on https
|
||||
// because FF does not let you access navigator.mediaDevices when on http.
|
||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
if (isFirefox) {
|
||||
window.navigator.mozGetUserMedia = function () { };
|
||||
if (window.navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
navigator.mediaDevices.getSupportedConstraints = function () { return {}; };
|
||||
navigator.mediaDevices.getUserMedia = function () { return {}; };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||
<script type="text/javascript" src="scripts/webRtcPlayer.js"></script>
|
||||
<script type="text/javascript" src="scripts/app.js"></script>
|
||||
<script>
|
||||
inputOptions.controlScheme = ControlSchemeType.HoveringMouse;
|
||||
inputOptions.fakeMouseWithTouches = true;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body onload="load()">
|
||||
<div id="playerUI" class="noselect">
|
||||
<div id="player"></div>
|
||||
<div id="overlay">
|
||||
<div id="controls">
|
||||
<button class="tooltip" onclick="fullscreen()" id="fullscreen-btn">
|
||||
<object data="images/Minimize.svg" type="image/svg+xml" id="minimize"></object>
|
||||
<object data="images/Maximize.svg" type="image/svg+xml" id="maximize"></object>
|
||||
<span class="tooltiptext">Fullscreen</span>
|
||||
</button>
|
||||
<button class="tooltip" id="settingsBtn">
|
||||
<object data="images/Settings.svg" type="image/svg+xml"></object>
|
||||
<span class="tooltiptext">Settings</span>
|
||||
</button>
|
||||
<button class="tooltip" id="statsBtn">
|
||||
<object data="images/Info.svg" type="image/svg+xml"></object>
|
||||
<span class="tooltiptext">Information</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div id="unrealengine">
|
||||
<p></p>
|
||||
<svg viewBox="0 0 1204.105 74.011" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="svgGroup" stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="#ffffff"
|
||||
stroke-width="0.25mm" fill="#ffffff" style="stroke:#ffffff;stroke-width:0.25mm;fill:#ffffff">
|
||||
<path
|
||||
d="M 894.605 74.006 L 921.305 74.006 C 942.427 74.006 953.413 73.292 956.313 60.527 A 37.848 37.848 0 0 0 957.105 52.206 L 957.105 31.606 L 907.305 31.606 L 907.305 46.606 L 934.905 46.606 L 934.905 48.706 C 934.905 53.442 932.251 55.385 928.738 56.147 A 26.156 26.156 0 0 1 923.305 56.606 L 899.605 56.606 A 39.408 39.408 0 0 1 894.638 56.344 C 888.593 55.565 886.231 52.853 885.654 45.747 A 52.88 52.88 0 0 1 885.505 41.506 L 885.505 32.506 A 48.028 48.028 0 0 1 885.738 27.377 C 886.556 19.865 889.695 17.689 897.793 17.433 A 57.321 57.321 0 0 1 899.605 17.406 L 923.705 17.406 A 38.323 38.323 0 0 1 927.889 17.588 C 932.328 18.085 934.835 19.818 934.904 25.273 A 18.461 18.461 0 0 1 934.905 25.506 L 956.705 25.506 L 956.705 23.206 A 36.235 36.235 0 0 0 955.47 12.587 C 953.178 5.214 946.987 0.624 931.363 0.064 A 93.813 93.813 0 0 0 928.005 0.006 L 894.605 0.006 C 874.839 0.006 862.985 4.89 862.71 25.469 A 55.259 55.259 0 0 0 862.705 26.206 L 862.705 47.806 A 39.6 39.6 0 0 0 863.741 57.358 C 866.906 70.059 877.192 73.722 892.685 73.99 A 111.289 111.289 0 0 0 894.605 74.006 Z M 323.405 73.206 L 403.905 73.206 L 403.905 55.806 L 345.605 55.806 L 345.605 44.206 L 399.605 44.206 L 399.605 29.206 L 345.605 29.206 L 345.605 17.606 L 402.605 17.606 L 402.605 0.806 L 323.405 0.806 L 323.405 73.206 Z M 656.905 73.206 L 737.405 73.206 L 737.405 55.806 L 679.105 55.806 L 679.105 44.206 L 733.105 44.206 L 733.105 29.206 L 679.105 29.206 L 679.105 17.606 L 736.105 17.606 L 736.105 0.806 L 656.905 0.806 L 656.905 73.206 Z M 1123.605 73.206 L 1204.105 73.206 L 1204.105 55.806 L 1145.805 55.806 L 1145.805 44.206 L 1199.805 44.206 L 1199.805 29.206 L 1145.805 29.206 L 1145.805 17.606 L 1202.805 17.606 L 1202.805 0.806 L 1123.605 0.806 L 1123.605 73.206 Z M 106.205 73.206 L 128.405 73.206 L 128.405 20.606 L 128.605 20.606 L 170.305 73.206 L 204.705 73.206 L 204.705 0.806 L 182.505 0.806 L 182.505 53.406 L 182.305 53.406 L 140.605 0.806 L 106.205 0.806 L 106.205 73.206 Z M 750.805 73.206 L 773.005 73.206 L 773.005 20.606 L 773.205 20.606 L 814.905 73.206 L 849.305 73.206 L 849.305 0.806 L 827.105 0.806 L 827.105 53.406 L 826.905 53.406 L 785.205 0.806 L 750.805 0.806 L 750.805 73.206 Z M 1010.105 73.206 L 1032.305 73.206 L 1032.305 20.606 L 1032.505 20.606 L 1074.205 73.206 L 1108.605 73.206 L 1108.605 0.806 L 1086.405 0.806 L 1086.405 53.406 L 1086.205 53.406 L 1044.505 0.806 L 1010.105 0.806 L 1010.105 73.206 Z M 29.705 74.006 L 61.505 74.006 A 51.732 51.732 0 0 0 72.484 72.955 C 82.691 70.728 88.531 64.939 90.473 55.381 A 40.134 40.134 0 0 0 91.205 47.406 L 91.205 0.806 L 69.005 0.806 L 69.005 42.606 C 69.005 51.405 65.987 55.082 57.762 55.385 A 31.364 31.364 0 0 1 56.605 55.406 L 34.605 55.406 A 22.943 22.943 0 0 1 30.082 55.013 C 24.736 53.93 22.488 50.538 22.231 43.95 A 34.441 34.441 0 0 1 22.205 42.606 L 22.205 0.806 L 0.005 0.806 L 0.005 47.406 C 0.005 64.841 8.73 73.457 27.816 73.981 A 68.853 68.853 0 0 0 29.705 74.006 Z M 219.705 73.206 L 241.905 73.206 L 241.905 54.406 L 276.805 54.406 A 18.122 18.122 0 0 1 280.084 54.663 C 284.425 55.469 285.905 58.272 285.905 64.506 L 285.905 73.206 L 308.105 73.206 L 308.105 60.906 C 308.105 49.006 302.605 46.106 296.005 44.906 L 296.005 44.706 A 22.978 22.978 0 0 0 300.937 43.003 C 305.503 40.745 307.44 37.183 308.108 32.151 A 40.124 40.124 0 0 0 308.405 26.906 L 308.405 22.306 A 33.46 33.46 0 0 0 307.248 12.853 C 305.018 5.341 299.196 0.806 286.705 0.806 L 219.705 0.806 L 219.705 73.206 Z M 410.205 73.206 L 435.005 73.206 L 441.505 60.406 L 486.505 60.406 L 493.405 73.206 L 518.105 73.206 L 478.305 0.806 L 449.405 0.806 L 410.205 73.206 Z M 526.005 73.206 L 598.105 73.206 L 598.105 54.606 L 548.205 54.606 L 548.205 0.806 L 526.005 0.806 L 526.005 73.206 Z M 971.705 73.206 L 993.905 73.206 L 993.905 0.806 L 971.705 0.806 L 971.705 73.206 Z M 241.905 18.806 L 276.605 18.806 A 34.237 34.237 0 0 1 280.146 18.959 C 283.795 19.344 285.876 20.558 286.556 23.711 A 12.861 12.861 0 0 1 286.805 26.406 L 286.805 28.806 A 12.128 12.128 0 0 1 286.485 31.774 C 285.685 34.926 283.316 36.406 278.605 36.406 L 241.905 36.406 L 241.905 18.806 Z M 463.805 17.606 L 478.205 44.806 L 449.805 44.806 L 463.805 17.606 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="connection" class="tooltip">
|
||||
<svg version="1.1" id="connectionStrength" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 494.45 494.45"
|
||||
style="enable-background:new 0 0 494.45 494.45;" xml:space="preserve">
|
||||
<circle id="dot" cx="247.125" cy="398.925" r="35.3" />
|
||||
|
||||
<path id="middle"
|
||||
d="M395.225,277.325c-6.8,0-13.5-2.6-18.7-7.8c-71.4-71.3-187.4-71.3-258.8,0c-10.3,10.3-27.1,10.3-37.4,0
|
||||
s-10.3-27.1,0-37.4c92-92,241.6-92,333.6,0c10.3,10.3,10.3,27.1,0,37.4C408.725,274.725,401.925,277.325,395.225,277.325z" />
|
||||
|
||||
<path id="outer" d="M467.925,204.625c-6.8,0-13.5-2.6-18.7-7.8c-111.5-111.4-292.7-111.4-404.1,0c-10.3,10.3-27.1,10.3-37.4,0
|
||||
s-10.3-27.1,0-37.4c64-64,149-99.2,239.5-99.2s175.5,35.2,239.5,99.2c10.3,10.3,10.3,27.1,0,37.4
|
||||
C481.425,202.025,474.625,204.625,467.925,204.625z" />
|
||||
|
||||
<path id="inner" d="M323.625,348.825c-6.8,0-13.5-2.6-18.7-7.8c-15.4-15.4-36-23.9-57.8-23.9s-42.4,8.5-57.8,23.9
|
||||
c-10.3,10.3-27.1,10.3-37.4,0c-10.3-10.3-10.3-27.1,0-37.4c25.4-25.4,59.2-39.4,95.2-39.4s69.8,14,95.2,39.5
|
||||
c10.3,10.3,10.3,27.1,0,37.4C337.225,346.225,330.425,348.825,323.625,348.825z" />
|
||||
|
||||
|
||||
|
||||
|
||||
</svg>
|
||||
<span class="tooltiptext" id="qualityText">Not connected</span>
|
||||
</div>
|
||||
<div class="panel-wrap" id="settings-panel">
|
||||
<div class="panel">
|
||||
<div id="heading">Settings</div>
|
||||
<div id="close" onclick="settingsClicked()"></div>
|
||||
<div id="content">
|
||||
<div id="fillWindow" class="setting">
|
||||
<div class="settings-text">Enlarge display to fill window</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="enlarge-display-to-fill-window-tgl" class="tgl tgl-flat" checked>
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="qualityControlOwnership" class="setting">
|
||||
<div class="settings-text">Is quality controller?</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="quality-control-ownership-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="matchViewportResolution" class="setting">
|
||||
<div class="settings-text">Match viewport resolution</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="match-viewport-res-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="offerToReceive" class="setting">
|
||||
<div class="settings-text">Offer To Receive</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="offer-receive-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="preferSFU" class="setting">
|
||||
<div class="settings-text">Prefer SFU</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="prefer-sfu-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="useMic" class="setting">
|
||||
<div class="settings-text">Use microphone</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="use-mic-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="forceMonoAudio" class="setting">
|
||||
<div class="settings-text">Force mono audio</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="force-mono-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="forceTURN" class="setting">
|
||||
<div class="settings-text">Force TURN</div>
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="force-turn-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="toggleControl" class="setting">
|
||||
<div class="settings-text" id="control-scheme-text">Control Scheme</div>
|
||||
<label class="btn-overlay">
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="control-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="toggleCursor" class="setting">
|
||||
<div class="settings-text" id="cursor-text">Hide Browser Cursor</div>
|
||||
<label class="btn-overlay">
|
||||
<label class="tgl-switch">
|
||||
<input type="checkbox" id="cursor-tgl" class="tgl tgl-flat">
|
||||
<div class="tgl-slider"></div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div id="showFPS" class="setting">
|
||||
<div class="settings-text">Show FPS</div>
|
||||
<label class="btn-overlay">
|
||||
<input type="button" id="show-fps-button" class="overlay-button btn-flat" value="Toggle">
|
||||
</label>
|
||||
</div>
|
||||
<div id="keyframeRequest" class="setting">
|
||||
<div class="settings-text">Request KeyFrame</div>
|
||||
<label class="btn-overlay">
|
||||
<input type="button" id="request-keyframe-button" class="overlay-button btn-flat" value="Request">
|
||||
</label>
|
||||
</div>
|
||||
<section id="encoderSettings">
|
||||
<div id="encoderSettingsHeader" class="settings-text">
|
||||
<div>Encoder Settings</div>
|
||||
</div>
|
||||
<div id="encoderParamsContainer" class="collapse">
|
||||
<div class="form-group">
|
||||
<label for="encoder-min-qp-text">Min QP</label>
|
||||
<input type="number" class="form-control" id="encoder-min-qp-text" value="0" min="0"
|
||||
max="51" />
|
||||
<label for="encoder-max-qp-text">Max QP</label>
|
||||
<input type="number" class="form-control" id="encoder-max-qp-text" value="51" min="0"
|
||||
max="51" />
|
||||
<br>
|
||||
<input id="encoder-params-submit" class="overlay-button btn-flat" type="button"
|
||||
value="Apply">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="webRTCSettings">
|
||||
<div id="webRTCSettingsHeader" class="settings-text">
|
||||
<div>WebRTC Settings</div>
|
||||
</div>
|
||||
<div id="webrtcParamsContainer" class="collapse">
|
||||
<div class="form-group">
|
||||
<label for="webrtc-fps-text">FPS</label>
|
||||
<input type="number" class="form-control" id="webrtc-fps-text" value="60" min="1"
|
||||
max="999" />
|
||||
<label for="webrtc-min-bitrate-text">Min bitrate (kbps)</label>
|
||||
<input type="number" class="form-control" id="webrtc-min-bitrate-text" value="0" min="0"
|
||||
max="100000" />
|
||||
<label for="webrtc-max-bitrate-text">Max bitrate (kbps)</label>
|
||||
<input type="number" class="form-control" id="webrtc-max-bitrate-text" value="0" min="0"
|
||||
max="100000" />
|
||||
<br>
|
||||
<input id="webrtc-params-submit" class="overlay-button btn-flat" type="button"
|
||||
value="Apply">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="streamSettings">
|
||||
<div id="streamSettingsHeader" class="settings-text">
|
||||
<div>Stream Settings</div>
|
||||
</div>
|
||||
<div id="streamSettingsContainer" class="collapse">
|
||||
<div class="form-group">
|
||||
<div class="settings-text">Player stream</div>
|
||||
<select class="form-control" id="stream-select"></select>
|
||||
<div class="settings-text">Player track</div>
|
||||
<select class="form-control" id="track-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<br>
|
||||
<section id="connectionSettings">
|
||||
<div id="connectionHeader" class="settings-text">
|
||||
<div>Stream Settings</div>
|
||||
</div>
|
||||
<div id="connectionContainer" class="collapse">
|
||||
<div class="setting">
|
||||
<div class="settings-text"></div>
|
||||
<label class="btn-overlay">
|
||||
<input type="button" id="restart-stream-button" class="overlay-button btn-flat" value="Restart stream">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-wrap" id="stats-panel">
|
||||
<div class="panel">
|
||||
<div id="heading">Information</div>
|
||||
<div id="close" onclick="statsClicked()"></div>
|
||||
<div id="content">
|
||||
<section id="statsPanel">
|
||||
<div class="setting settings-text">
|
||||
<div>Session Stats</div>
|
||||
</div>
|
||||
<div id="statsContainer" class="statsContainer">
|
||||
<div id="stats" class="stats"></div>
|
||||
</div>
|
||||
</section>
|
||||
<br>
|
||||
<section id="latencyTest">
|
||||
<div class="setting">
|
||||
<div class="settings-text">
|
||||
<div>Latency Report</div>
|
||||
</div>
|
||||
<label class="btn-overlay">
|
||||
<input type="button" id="test-latency-button" class="overlay-button btn-flat"
|
||||
value="Get Report">
|
||||
</label>
|
||||
</div>
|
||||
<div id="latencyStatsContainer" class="statsContainer">
|
||||
<div id=LatencyStats class="stats">No report yet</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<script type="text/javascript" src="scripts/stressTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="stressTest()">
|
||||
<button id="playPause">Pause</button>
|
||||
<div>Total streams: <span id="nStreamsLabel">0</span></div>
|
||||
<div>
|
||||
<span>Max peers: </span>
|
||||
<span id="nPeerLabel">5</span>
|
||||
<input type="range" min="1" max="8" value="5" class="slider" id="nPeersSlider">
|
||||
</div>
|
||||
<div>
|
||||
<span>Peer creation interval (seconds): </span>
|
||||
<input type="number" id="creationIntervalInput" min="0" max="10" step="1" value="1">
|
||||
</div>
|
||||
<div>
|
||||
<span>Peer deletion interval (seconds): </span>
|
||||
<input type="number" id="deletionIntervalInput" min="0" max="10" step="1" value="2">
|
||||
</div>
|
||||
<div id="streamsContainer"></div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"UseFrontend": false,
|
||||
"UseMatchmaker": false,
|
||||
"UseHTTPS": false,
|
||||
"UseAuthentication": false,
|
||||
"LogToFile": true,
|
||||
"LogVerbose": true,
|
||||
"HomepageFile": "player.html",
|
||||
"AdditionalRoutes": {},
|
||||
"EnableWebserver": true,
|
||||
"MatchmakerAddress": "",
|
||||
"MatchmakerPort": "9999",
|
||||
"PublicIp": "localhost",
|
||||
"HttpPort": 47500,
|
||||
"HttpsPort": 443,
|
||||
"StreamerPort": 8888,
|
||||
"SFUPort": 8889,
|
||||
"MaxPlayerCount": -1,
|
||||
"peerConnectionOptions": "{\"iceServers\": [{\"urls\": [\"stun:stun1.l.google.com:19302\",\"stun:stun2.l.google.com:19302\",\"stun:stun3.l.google.com:19302\",\"stun:stun4.l.google.com:19302\"]},{\"urls\":[\"turn:13.250.13.83:3478?transport=udp\"],\"username\":\"YzYNCouZM1mhqhmseWk6\",\"credential\":\"YzYNCouZM1mhqhmseWk6\"},{\"urls\": [\"turn:192.158.29.39:3478?transport=tcp\"],\"credential\": \"JZEOEt2V3Qb0y27GRntt2u2PAYA=\",\"username\": \"28224511:1379330808\"}]}"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
url('./inter-v12-latin_cyrillic-regular.woff2') format('woff2'),
|
||||
url('./inter-v12-latin_cyrillic-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
url('./inter-v12-latin_cyrillic-300.woff2') format('woff2'),
|
||||
url('./inter-v12-latin_cyrillic-300.woff') format('woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
url('./inter-v12-latin_cyrillic-500.woff2') format('woff2'),
|
||||
url('./inter-v12-latin_cyrillic-500.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
url('./inter-v12-latin_cyrillic-600.woff2') format('woff2'),
|
||||
url('./inter-v12-latin_cyrillic-600.woff') format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
url('./inter-v12-latin_cyrillic-700.woff2') format('woff2'),
|
||||
url('./inter-v12-latin_cyrillic-700.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'GilroyWebRegular';
|
||||
src: url('./Gilroy_Regular.woff'),
|
||||
url('./Gilroy_Regular.woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
|
||||
<defs>
|
||||
<link href="../player.css" type="text/css" rel="stylesheet"
|
||||
xmlns="http://www.w3.org/1999/xhtml"/>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M165,0.008C74.019,0.008,0,74.024,0,164.999c0,90.977,74.019,164.992,165,164.992s165-74.015,165-164.992
|
||||
C330,74.024,255.981,0.008,165,0.008z M165,299.992c-74.439,0-135-60.557-135-134.992S90.561,30.008,165,30.008
|
||||
s135,60.557,135,134.991C300,239.436,239.439,299.992,165,299.992z"/>
|
||||
<path d="M165,130.008c-8.284,0-15,6.716-15,15v99.983c0,8.284,6.716,15,15,15s15-6.716,15-15v-99.983
|
||||
C180,136.725,173.284,130.008,165,130.008z"/>
|
||||
<path d="M165,70.011c-3.95,0-7.811,1.6-10.61,4.39c-2.79,2.79-4.39,6.66-4.39,10.61s1.6,7.81,4.39,10.61
|
||||
c2.79,2.79,6.66,4.39,10.61,4.39s7.81-1.6,10.609-4.39c2.79-2.8,4.391-6.66,4.391-10.61s-1.601-7.82-4.391-10.61
|
||||
C172.81,71.61,168.95,70.011,165,70.011z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg version="1.1" id="maximize" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 384.97 384.97" style="enable-background:new 0 0 384.97 384.97;" xml:space="preserve">
|
||||
<defs>
|
||||
<link href="../player.css" type="text/css" rel="stylesheet" xmlns="http://www.w3.org/1999/xhtml"/>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M384.97,12.03c0-6.713-5.317-12.03-12.03-12.03H264.847c-6.833,0-11.922,5.39-11.934,12.223
|
||||
c0,6.821,5.101,11.838,11.934,11.838h96.062l-0.193,96.519c0,6.833,5.197,12.03,12.03,12.03c6.833-0.012,12.03-5.197,12.03-12.03
|
||||
l0.193-108.369c0-0.036-0.012-0.06-0.012-0.084C384.958,12.09,384.97,12.066,384.97,12.03z"/>
|
||||
<path d="M120.496,0H12.403c-0.036,0-0.06,0.012-0.096,0.012C12.283,0.012,12.247,0,12.223,0C5.51,0,0.192,5.317,0.192,12.03
|
||||
L0,120.399c0,6.833,5.39,11.934,12.223,11.934c6.821,0,11.838-5.101,11.838-11.934l0.192-96.339h96.242
|
||||
c6.833,0,12.03-5.197,12.03-12.03C132.514,5.197,127.317,0,120.496,0z"/>
|
||||
<path d="M120.123,360.909H24.061v-96.242c0-6.833-5.197-12.03-12.03-12.03S0,257.833,0,264.667v108.092
|
||||
c0,0.036,0.012,0.06,0.012,0.084c0,0.036-0.012,0.06-0.012,0.096c0,6.713,5.317,12.03,12.03,12.03h108.092
|
||||
c6.833,0,11.922-5.39,11.934-12.223C132.057,365.926,126.956,360.909,120.123,360.909z"/>
|
||||
<path d="M372.747,252.913c-6.833,0-11.85,5.101-11.838,11.934v96.062h-96.242c-6.833,0-12.03,5.197-12.03,12.03
|
||||
s5.197,12.03,12.03,12.03h108.092c0.036,0,0.06-0.012,0.084-0.012c0.036-0.012,0.06,0.012,0.096,0.012
|
||||
c6.713,0,12.03-5.317,12.03-12.03V264.847C384.97,258.014,379.58,252.913,372.747,252.913z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 385.331 385.331" style="enable-background:new 0 0 385.331 385.331;" xml:space="preserve">
|
||||
<defs>
|
||||
<link href="../player.css" type="text/css" rel="stylesheet" xmlns="http://www.w3.org/1999/xhtml"/>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M264.943,156.665h108.273c6.833,0,11.934-5.39,11.934-12.211c0-6.833-5.101-11.85-11.934-11.838h-96.242V36.181
|
||||
c0-6.833-5.197-12.03-12.03-12.03s-12.03,5.197-12.03,12.03v108.273c0,0.036,0.012,0.06,0.012,0.084
|
||||
c0,0.036-0.012,0.06-0.012,0.096C252.913,151.347,258.23,156.677,264.943,156.665z"/>
|
||||
<path d="M120.291,24.247c-6.821,0-11.838,5.113-11.838,11.934v96.242H12.03c-6.833,0-12.03,5.197-12.03,12.03
|
||||
c0,6.833,5.197,12.03,12.03,12.03h108.273c0.036,0,0.06-0.012,0.084-0.012c0.036,0,0.06,0.012,0.096,0.012
|
||||
c6.713,0,12.03-5.317,12.03-12.03V36.181C132.514,29.36,127.124,24.259,120.291,24.247z"/>
|
||||
<path d="M120.387,228.666H12.115c-6.833,0.012-11.934,5.39-11.934,12.223c0,6.833,5.101,11.85,11.934,11.838h96.242v96.423
|
||||
c0,6.833,5.197,12.03,12.03,12.03c6.833,0,12.03-5.197,12.03-12.03V240.877c0-0.036-0.012-0.06-0.012-0.084
|
||||
c0-0.036,0.012-0.06,0.012-0.096C132.418,233.983,127.1,228.666,120.387,228.666z"/>
|
||||
<path d="M373.3,228.666H265.028c-0.036,0-0.06,0.012-0.084,0.012c-0.036,0-0.06-0.012-0.096-0.012
|
||||
c-6.713,0-12.03,5.317-12.03,12.03v108.273c0,6.833,5.39,11.922,12.223,11.934c6.821,0.012,11.838-5.101,11.838-11.922v-96.242
|
||||
H373.3c6.833,0,12.03-5.197,12.03-12.03S380.134,228.678,373.3,228.666z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,31 @@
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 478.703 478.703" style="enable-background:new 0 0 478.703 478.703;" xml:space="preserve">
|
||||
<defs>
|
||||
<link href="../player.css" type="text/css" rel="stylesheet" xmlns="http://www.w3.org/1999/xhtml"/>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M454.2,189.101l-33.6-5.7c-3.5-11.3-8-22.2-13.5-32.6l19.8-27.7c8.4-11.8,7.1-27.9-3.2-38.1l-29.8-29.8
|
||||
c-5.6-5.6-13-8.7-20.9-8.7c-6.2,0-12.1,1.9-17.1,5.5l-27.8,19.8c-10.8-5.7-22.1-10.4-33.8-13.9l-5.6-33.2
|
||||
c-2.4-14.3-14.7-24.7-29.2-24.7h-42.1c-14.5,0-26.8,10.4-29.2,24.7l-5.8,34c-11.2,3.5-22.1,8.1-32.5,13.7l-27.5-19.8
|
||||
c-5-3.6-11-5.5-17.2-5.5c-7.9,0-15.4,3.1-20.9,8.7l-29.9,29.8c-10.2,10.2-11.6,26.3-3.2,38.1l20,28.1
|
||||
c-5.5,10.5-9.9,21.4-13.3,32.7l-33.2,5.6c-14.3,2.4-24.7,14.7-24.7,29.2v42.1c0,14.5,10.4,26.8,24.7,29.2l34,5.8
|
||||
c3.5,11.2,8.1,22.1,13.7,32.5l-19.7,27.4c-8.4,11.8-7.1,27.9,3.2,38.1l29.8,29.8c5.6,5.6,13,8.7,20.9,8.7c6.2,0,12.1-1.9,17.1-5.5
|
||||
l28.1-20c10.1,5.3,20.7,9.6,31.6,13l5.6,33.6c2.4,14.3,14.7,24.7,29.2,24.7h42.2c14.5,0,26.8-10.4,29.2-24.7l5.7-33.6
|
||||
c11.3-3.5,22.2-8,32.6-13.5l27.7,19.8c5,3.6,11,5.5,17.2,5.5l0,0c7.9,0,15.3-3.1,20.9-8.7l29.8-29.8c10.2-10.2,11.6-26.3,3.2-38.1
|
||||
l-19.8-27.8c5.5-10.5,10.1-21.4,13.5-32.6l33.6-5.6c14.3-2.4,24.7-14.7,24.7-29.2v-42.1
|
||||
C478.9,203.801,468.5,191.501,454.2,189.101z M451.9,260.401c0,1.3-0.9,2.4-2.2,2.6l-42,7c-5.3,0.9-9.5,4.8-10.8,9.9
|
||||
c-3.8,14.7-9.6,28.8-17.4,41.9c-2.7,4.6-2.5,10.3,0.6,14.7l24.7,34.8c0.7,1,0.6,2.5-0.3,3.4l-29.8,29.8c-0.7,0.7-1.4,0.8-1.9,0.8
|
||||
c-0.6,0-1.1-0.2-1.5-0.5l-34.7-24.7c-4.3-3.1-10.1-3.3-14.7-0.6c-13.1,7.8-27.2,13.6-41.9,17.4c-5.2,1.3-9.1,5.6-9.9,10.8l-7.1,42
|
||||
c-0.2,1.3-1.3,2.2-2.6,2.2h-42.1c-1.3,0-2.4-0.9-2.6-2.2l-7-42c-0.9-5.3-4.8-9.5-9.9-10.8c-14.3-3.7-28.1-9.4-41-16.8
|
||||
c-2.1-1.2-4.5-1.8-6.8-1.8c-2.7,0-5.5,0.8-7.8,2.5l-35,24.9c-0.5,0.3-1,0.5-1.5,0.5c-0.4,0-1.2-0.1-1.9-0.8l-29.8-29.8
|
||||
c-0.9-0.9-1-2.3-0.3-3.4l24.6-34.5c3.1-4.4,3.3-10.2,0.6-14.8c-7.8-13-13.8-27.1-17.6-41.8c-1.4-5.1-5.6-9-10.8-9.9l-42.3-7.2
|
||||
c-1.3-0.2-2.2-1.3-2.2-2.6v-42.1c0-1.3,0.9-2.4,2.2-2.6l41.7-7c5.3-0.9,9.6-4.8,10.9-10c3.7-14.7,9.4-28.9,17.1-42
|
||||
c2.7-4.6,2.4-10.3-0.7-14.6l-24.9-35c-0.7-1-0.6-2.5,0.3-3.4l29.8-29.8c0.7-0.7,1.4-0.8,1.9-0.8c0.6,0,1.1,0.2,1.5,0.5l34.5,24.6
|
||||
c4.4,3.1,10.2,3.3,14.8,0.6c13-7.8,27.1-13.8,41.8-17.6c5.1-1.4,9-5.6,9.9-10.8l7.2-42.3c0.2-1.3,1.3-2.2,2.6-2.2h42.1
|
||||
c1.3,0,2.4,0.9,2.6,2.2l7,41.7c0.9,5.3,4.8,9.6,10,10.9c15.1,3.8,29.5,9.7,42.9,17.6c4.6,2.7,10.3,2.5,14.7-0.6l34.5-24.8
|
||||
c0.5-0.3,1-0.5,1.5-0.5c0.4,0,1.2,0.1,1.9,0.8l29.8,29.8c0.9,0.9,1,2.3,0.3,3.4l-24.7,34.7c-3.1,4.3-3.3,10.1-0.6,14.7
|
||||
c7.8,13.1,13.6,27.2,17.4,41.9c1.3,5.2,5.6,9.1,10.8,9.9l42,7.1c1.3,0.2,2.2,1.3,2.2,2.6v42.1H451.9z"/>
|
||||
<path d="M239.4,136.001c-57,0-103.3,46.3-103.3,103.3s46.3,103.3,103.3,103.3s103.3-46.3,103.3-103.3S296.4,136.001,239.4,136.001
|
||||
z M239.4,315.601c-42.1,0-76.3-34.2-76.3-76.3s34.2-76.3,76.3-76.3s76.3,34.2,76.3,76.3S281.5,315.601,239.4,315.601z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 959 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,2 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
exports.users = require('./users');
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
//
|
||||
// Usage: npm run store_password -- --username <USERNAME> --password <PASSWORD>
|
||||
// or from ./modules/authentication/db dir: node store_password.js --username <USERNAME> --password <PASSWORD>
|
||||
//
|
||||
// --usersFile is an optional parameter that can be used to specify a different location for the users database file
|
||||
// use this if running the command from a different working dir. The default location is './users.json'
|
||||
// e.g. If running from the SignallingWebServer dir use: --usersFile ./modules/authentication/db/users.json
|
||||
|
||||
const argv = require('yargs').argv;
|
||||
const fs = require('fs');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
var username, password;
|
||||
var usersFile = './users.json'
|
||||
|
||||
const STORE_PLAINTEXT_PASSWORD = false;
|
||||
|
||||
try {
|
||||
if(typeof argv.username != 'undefined'){
|
||||
username = argv.username.toString();
|
||||
}
|
||||
|
||||
if(typeof argv.password != 'undefined'){
|
||||
password = argv.password;
|
||||
}
|
||||
|
||||
if(typeof argv.usersFile != 'undefined'){
|
||||
usersFile = argv.usersFile;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if(username && password){
|
||||
let existingAccounts = [];
|
||||
if (fs.existsSync(usersFile)) {
|
||||
console.log(`File '${usersFile}' exists, reading file`)
|
||||
var content = fs.readFileSync(usersFile, 'utf8');
|
||||
try{
|
||||
existingAccounts = JSON.parse(content);
|
||||
}
|
||||
catch(e){
|
||||
console.error(`Existing file '${usersFile}', has invalid JSON: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
var existingUser = existingAccounts.find( u => u.username == username)
|
||||
if(existingUser){
|
||||
console.log(`User '${username}', already exists, updating password`)
|
||||
existingUser.passwordHash = generatePasswordHash(password)
|
||||
if(STORE_PLAINTEXT_PASSWORD)
|
||||
existingUser.password = password;
|
||||
else if (existingUser.password)
|
||||
delete existingUser.password;
|
||||
|
||||
} else {
|
||||
console.log(`Adding new user '${username}'`)
|
||||
let newUser = {
|
||||
id: existingAccounts.length + 1,
|
||||
username: username,
|
||||
passwordHash: generatePasswordHash(password)
|
||||
}
|
||||
if(STORE_PLAINTEXT_PASSWORD)
|
||||
newUser.password = password;
|
||||
|
||||
existingAccounts.push(newUser);
|
||||
}
|
||||
|
||||
console.log(`Writing updated users to '${usersFile}'`);
|
||||
var newContent = JSON.stringify(existingAccounts);
|
||||
fs.writeFileSync(usersFile, newContent);
|
||||
} else {
|
||||
console.log(`Please pass in both username (${username}) and password (${password}) please`);
|
||||
}
|
||||
|
||||
function generatePasswordHash(pass){
|
||||
return bcrypt.hashSync(pass, 12)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read in users from file
|
||||
let records = [];
|
||||
let usersFile = path.join(__dirname, './users.json');
|
||||
if (fs.existsSync(usersFile)) {
|
||||
console.log(`Reading users from '${usersFile}'`)
|
||||
var content = fs.readFileSync(usersFile, 'utf8');
|
||||
try {
|
||||
records = JSON.parse(content);
|
||||
} catch(e) {
|
||||
console.log(`ERROR: Failed to parse users from file '${usersFile}'`)
|
||||
}
|
||||
}
|
||||
|
||||
exports.findById = function(id, cb) {
|
||||
var idx = id - 1;
|
||||
if (records[idx]) {
|
||||
cb(null, records[idx]);
|
||||
} else {
|
||||
cb(new Error('User ' + id + ' does not exist'));
|
||||
}
|
||||
}
|
||||
|
||||
exports.findByUsername = function(username, cb) {
|
||||
for (var i = 0, len = records.length; i < len; i++) {
|
||||
var record = records[i];
|
||||
if (record.username === username) {
|
||||
return cb(null, record);
|
||||
}
|
||||
}
|
||||
return cb(null, null);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
module.exports = {
|
||||
init: require('./init')
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// Adapted from
|
||||
// * https://blog.risingstack.com/node-hero-node-js-authentication-passport-js/
|
||||
// * https://github.com/RisingStack/nodehero-authentication/tree/master/app
|
||||
// * https://github.com/passport/express-4.x-local-example
|
||||
|
||||
|
||||
const passport = require('passport');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
var db = require('./db');
|
||||
|
||||
function initPassport (app) {
|
||||
|
||||
// Generate session secret if it doesn't already exist and save it to file for use next time
|
||||
let config = {};
|
||||
let configPath = path.join(__dirname, './config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
let content = fs.readFileSync(configPath, 'utf8');
|
||||
try {
|
||||
config = JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.log(`Error with config file '${configPath}': ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
if(!config.sessionSecret){
|
||||
config.sessionSecret = bcrypt.genSaltSync(12);
|
||||
let content = JSON.stringify(config);
|
||||
fs.writeFileSync(configPath, content);
|
||||
}
|
||||
|
||||
// Setup session id settings
|
||||
app.use(session({
|
||||
secret: config.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 /* 1 day */
|
||||
//maxAge: 5 * 1000 /* 5 seconds */
|
||||
}
|
||||
}));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
passport.serializeUser(function(user, cb) {
|
||||
cb(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(id, cb) {
|
||||
db.users.findById(id, function (err, user) {
|
||||
if (err) { return cb(err); }
|
||||
cb(null, user);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Setting up auth');
|
||||
passport.use(new LocalStrategy(
|
||||
(username, password, callback) => {
|
||||
db.users.findByUsername(username, (err, user) => {
|
||||
if (err) {
|
||||
console.log(`Unable to login '${username}', error ${err}`);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// User not found
|
||||
if (!user) {
|
||||
console.log(`User '${username}' not found`);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
// Always use hashed passwords and fixed time comparison
|
||||
bcrypt.compare(password, user.passwordHash, (err, isValid) => {
|
||||
if (err) {
|
||||
console.log(`Error comparing password for user '${username}': ${err}`);
|
||||
return callback(err);
|
||||
}
|
||||
if (!isValid) {
|
||||
console.log(`Password incorrect for user '${username}'`)
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
console.log(`User '${username}' logged in`);
|
||||
return callback(null, user);
|
||||
});
|
||||
})
|
||||
}
|
||||
));
|
||||
|
||||
passport.authenticationMiddleware = function authenticationMiddleware (redirectUrl) {
|
||||
return function (req, res, next) {
|
||||
if (req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Set redirectTo property so that user can be redirected back there after logging in
|
||||
//console.log(`Original request path '${req.originalUrl}'`);
|
||||
req.session.redirectTo = req.originalUrl;
|
||||
res.redirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = initPassport;
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
//-- Provides configuration information from file and combines it with default values and command line arguments --//
|
||||
//-- Hierachy of values: Default Values < Config File < Command Line arguments --//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const argv = require('yargs').argv;
|
||||
|
||||
function initConfig(configFile, defaultConfig){
|
||||
defaultConfig = defaultConfig || {};
|
||||
|
||||
// Using object spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals
|
||||
let config = {...defaultConfig};
|
||||
try {
|
||||
let configData = fs.readFileSync(configFile, 'UTF8');
|
||||
fileConfig = JSON.parse(configData);
|
||||
config = {...config, ...fileConfig}
|
||||
|
||||
try {
|
||||
accessSync('configFile', constants.W_OK);
|
||||
// Update config file with any additional defaults (does not override existing values if default has changed)
|
||||
fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8');
|
||||
} catch (err) {
|
||||
console.log("Config file is readonly, skipping writing config...");
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log("No config file found, writing defaults to log file " + configFile);
|
||||
fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8');
|
||||
} else if (err instanceof SyntaxError) {
|
||||
console.log(`ERROR: Invalid JSON in ${configFile}, ignoring file config, ${err}`)
|
||||
} else {
|
||||
console.log(`ERROR: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
//Make a copy of the command line args and remove the unneccessary ones
|
||||
//The _ value is an array of any elements without a key
|
||||
let commandLineConfig = {...argv}
|
||||
delete commandLineConfig._;
|
||||
delete commandLineConfig.help;
|
||||
delete commandLineConfig.version;
|
||||
delete commandLineConfig['$0'];
|
||||
config = {...config, ...commandLineConfig}
|
||||
} catch(err) {
|
||||
console.log(`ERROR: ${err}`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: initConfig
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
var querystring = require('querystring')
|
||||
const https = require('https');
|
||||
const assert = require('assert');
|
||||
|
||||
function cleanUrl(aUrl){
|
||||
let url = aUrl;
|
||||
if(url.startsWith("https://"))
|
||||
url = url.substring("https://".length);
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
function createOptions(requestType, url){
|
||||
let index = url.indexOf('/');
|
||||
|
||||
let urlParts = url.split('/', 2)
|
||||
|
||||
return {
|
||||
hostname: (index === -1) ? url.substring(0) : url.substring(0, index),
|
||||
port: 443,
|
||||
path: (index === -1) ? '' : url.substring(index),
|
||||
method: requestType,
|
||||
timeout: 30000,
|
||||
};
|
||||
}
|
||||
|
||||
function makeHttpsCall(options, aCallback, aError){
|
||||
//console.log(JSON.stringify(options));
|
||||
const req = https.request(options, function(response){
|
||||
let data = '';
|
||||
|
||||
//console.log('statusCode:', response.statusCode);
|
||||
//console.log('headers:', response.headers);
|
||||
|
||||
// A chunk of data has been received.
|
||||
response.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
// The whole response has been received. Print out the result.
|
||||
response.on('end', () => {
|
||||
if(typeof aCallback != "undefined")
|
||||
aCallback(response, data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', function () {
|
||||
console.log("Request timed out. " + (options.timeout / 1000) + " seconds expired");
|
||||
|
||||
// Source: https://github.com/nodejs/node/blob/master/test/parallel/test-http-client-timeout-option.js#L27
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
if(typeof aError != "undefined") {
|
||||
aError(err);
|
||||
} else {
|
||||
console.log("Error: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
module.exports = class HttpClient {
|
||||
get(aUrl, aCallback, aError) {
|
||||
let url = cleanUrl(aUrl);
|
||||
|
||||
let options = createOptions('GET', url);
|
||||
|
||||
const req = makeHttpsCall(options, aCallback, aError);
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
post(aUrl, body, aCallback, aError) {
|
||||
let url = cleanUrl(aUrl);
|
||||
|
||||
let options = createOptions('POST', url);
|
||||
|
||||
let postBody = querystring.stringify(body);
|
||||
|
||||
//Add extra options for POST request type
|
||||
options.headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': postBody.length
|
||||
};
|
||||
|
||||
const req = makeHttpsCall(options, aCallback, aError);
|
||||
|
||||
req.write(postBody);
|
||||
req.end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
const fs = require('fs');
|
||||
const { Console } = require('console');
|
||||
|
||||
var loggers=[];
|
||||
var logFunctions=[];
|
||||
var logColorFunctions=[];
|
||||
|
||||
console.log = function(msg, ...args) {
|
||||
logFunctions.forEach((logFunction) => {
|
||||
logFunction(msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
console.logColor = function(color, msg, ...args) {
|
||||
logColorFunctions.forEach((logColorFunction) => {
|
||||
logColorFunction(color, msg, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
const AllAttributesOff = '\x1b[0m';
|
||||
const BoldOn = '\x1b[1m';
|
||||
const Black = '\x1b[30m';
|
||||
const Red = '\x1b[31m';
|
||||
const Green = '\x1b[32m';
|
||||
const Yellow = '\x1b[33m';
|
||||
const Blue = '\x1b[34m';
|
||||
const Magenta = '\x1b[35m';
|
||||
const Cyan = '\x1b[36m';
|
||||
const White = '\x1b[37m';
|
||||
|
||||
/**
|
||||
* Pad the start of the given number with zeros so it takes up the number of digits.
|
||||
* e.g. zeroPad(5, 3) = '005' and zeroPad(23, 2) = '23'.
|
||||
*/
|
||||
function zeroPad(number, digits) {
|
||||
let string = number.toString();
|
||||
while (string.length < digits) {
|
||||
string = '0' + string;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string of the form 'YEAR.MONTH.DATE.HOURS.MINUTES.SECONDS'.
|
||||
*/
|
||||
function dateTimeToString() {
|
||||
let date = new Date();
|
||||
return `${date.getFullYear()}.${zeroPad(date.getMonth(), 2)}.${zeroPad(date.getDate(), 2)}.${zeroPad(date.getHours(), 2)}.${zeroPad(date.getMinutes(), 2)}.${zeroPad(date.getSeconds(), 2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string of the form 'HOURS.MINUTES.SECONDS.MILLISECONDS'.
|
||||
*/
|
||||
function timeToString() {
|
||||
let date = new Date();
|
||||
return `${zeroPad(date.getHours(), 2)}:${zeroPad(date.getMinutes(), 2)}:${zeroPad(date.getSeconds(), 2)}.${zeroPad(date.getMilliseconds(), 3)}`;
|
||||
}
|
||||
|
||||
function RegisterFileLogger(path) {
|
||||
if(path == null)
|
||||
path = './logs/';
|
||||
|
||||
if (!fs.existsSync(path))
|
||||
fs.mkdirSync(path);
|
||||
|
||||
var output = fs.createWriteStream(`${path}${dateTimeToString()}.log`);
|
||||
var fileLogger = new Console(output);
|
||||
logFunctions.push(function(msg, ...args) {
|
||||
fileLogger.log(`${timeToString()} ${msg}`, ...args);
|
||||
});
|
||||
|
||||
logColorFunctions.push(function(color, msg, ...args) {
|
||||
fileLogger.log(`${timeToString()} ${msg}`, ...args);
|
||||
});
|
||||
loggers.push(fileLogger);
|
||||
}
|
||||
|
||||
function RegisterConsoleLogger() {
|
||||
var consoleLogger = new Console(process.stdout, process.stderr)
|
||||
logFunctions.push(function(msg, ...args) {
|
||||
consoleLogger.log(`${timeToString()} ${msg}`, ...args);
|
||||
});
|
||||
|
||||
logColorFunctions.push(function(color, msg, ...args) {
|
||||
consoleLogger.log(`${BoldOn}${color}${timeToString()} ${msg}${AllAttributesOff}`, ...args);
|
||||
});
|
||||
loggers.push(consoleLogger);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
//Functions
|
||||
RegisterFileLogger,
|
||||
RegisterConsoleLogger,
|
||||
|
||||
//Variables
|
||||
AllAttributesOff,
|
||||
BoldOn,
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "cirrus-webserver",
|
||||
"version": "0.0.1",
|
||||
"description": "cirrus web server",
|
||||
"scripts": {
|
||||
"store_password": "run-script-os",
|
||||
"store_password:default": "./platform_scripts/bash/node/bin/node ./modules/authentication/db/store_password.js --usersFile=./modules/authentication/db/users.json",
|
||||
"store_password:windows": "platform_scripts\\cmd\\node\\node.exe ./modules/authentication/db/store_password.js --usersFile=./modules/authentication/db/users.json",
|
||||
"start-local": "run-script-os --",
|
||||
"start-local:default": "./platform_scripts/bash/Start_Local.sh",
|
||||
"start-local:windows": ".\\platform_scripts\\cmd\\Start_Local.bat",
|
||||
"start-signalling-server": "run-script-os --",
|
||||
"start-signalling-server:default": "./platform_scripts/bash/Start_SignallingServer.sh",
|
||||
"start-signalling-server:windows": ".\\platform_scripts\\cmd\\Start_SignallingServer.bat",
|
||||
"start-with-turn-signalling-server": "run-script-os --",
|
||||
"start-with-turn-signalling-server:default": "./platform_scripts/bash/Start_WithTurn_SignallingServer.sh",
|
||||
"start-wiht-turn-signalling-server:windows": ".\\platform_scripts\\cmd\\Start_WithTurn_SignallingServer.bat",
|
||||
"start": "run-script-os",
|
||||
"start:default": "if [ `id -u` -eq 0 ] || [ ! -z $NO_SUDO ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node cirrus.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node cirrus.js\"\nfi\n$process ",
|
||||
"start:windows": "platform_scripts\\cmd\\node\\node.exe cirrus.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"helmet": "^3.21.3",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"run-script-os": "^1.1.6",
|
||||
"ws": "^7.1.2",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs": "^15.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
FROM node:latest
|
||||
|
||||
# Copy the signalling server source code to the Docker build context
|
||||
COPY . /opt/SignallingWebServer
|
||||
|
||||
# Install the dependencies for the signalling server
|
||||
WORKDIR /opt/SignallingWebServer
|
||||
RUN npm install .
|
||||
|
||||
# Expose TCP port 80 for player WebSocket connections and web server HTTP access
|
||||
EXPOSE 80
|
||||
|
||||
# Expose TCP port 8888 for streamer WebSocket connections
|
||||
EXPOSE 8888
|
||||
EXPOSE 8888/udp
|
||||
|
||||
# Expose port for SFU connections
|
||||
EXPOSE 8889
|
||||
|
||||
# Google stun
|
||||
EXPOSE 19302
|
||||
|
||||
# Matchmaker
|
||||
EXPOSE 9999
|
||||
|
||||
# Turn coturn
|
||||
EXPOSE 3478
|
||||
EXPOSE 3479
|
||||
|
||||
# Set the signalling server as the container's entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/node", "/opt/SignallingWebServer/cirrus.js"]
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
How to use files in this directory:
|
||||
- Make sure that all of your dependencies are installed. Use ./setup.sh what will install whatever is missing as long as you are on a supported operating system. Please note that setup.sh is called from every script designed to run
|
||||
|
||||
- Run a local instance of the Cirrus server by using the ./run_local.sh script
|
||||
|
||||
- Use the following scripts to run locally or in your cloud instance:
|
||||
- Start_SignallingServer.sh - Start only the Signalling (STUN) server
|
||||
- Start_TURNServer.sh - Start only the TURN server
|
||||
- Start_WithTURN_SignallingServer.sh - Start a TURN server and the Cirrus server together
|
||||
|
||||
- Please note that scripts intended to run need to be executable: $ chmod +x *.sh will do that job.
|
||||
- The local/cloud Start_*.sh shell scripts can be invoked with the --help command line option to see how those can be configured. The following options can be supplied: --publicip, --turn, --stun. Please read the --help
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values "n" "y" # Set STUN server defaults only
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
print_parameters
|
||||
|
||||
peerconnectionoptions='{\"iceServers\":[{\"urls\":[\"stun:${stunserver}\"]}]}'
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
arguments=""
|
||||
|
||||
if [ ! -z $IS_DEBUG ]; then
|
||||
arguments+=" --inspect"
|
||||
fi
|
||||
|
||||
arguments+=" --peerConnectionOptions=\"${peerconnectionoptions}\" --PublicIp=${publicip}"
|
||||
# Add arguments passed to script to arguments for executable
|
||||
arguments+=" ${cirruscmd}"
|
||||
|
||||
pushd ../..
|
||||
echo "Running: $process $arguments"
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process $arguments
|
||||
popd
|
||||
|
||||
popd
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source turn_user_pwd.sh
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values "y" "n" # TURN server defaults only
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
print_parameters
|
||||
|
||||
localip=$(hostname -I | awk '{print $1}')
|
||||
echo "Private IP: $localip"
|
||||
|
||||
turnport="${turnserver##*:}"
|
||||
if [ -z "${turnport}" ]; then
|
||||
turnport=3478
|
||||
fi
|
||||
echo "TURN port: ${turnport}"
|
||||
echo ""
|
||||
|
||||
|
||||
# Hmm, plain text
|
||||
realm="PixelStreaming"
|
||||
process="turnserver"
|
||||
arguments="-p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -n -u ${turnusername}:${turnpassword}"
|
||||
|
||||
# Add arguments passed to script to arguments for executable
|
||||
arguments+=" ${cirruscmd}"
|
||||
|
||||
pushd ../.. >/dev/null
|
||||
echo "Running: $process $arguments"
|
||||
# pause
|
||||
start_process $process $arguments &
|
||||
popd >/dev/null # ../..
|
||||
|
||||
popd >/dev/null # BASH_SOURCE
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values "y" "y" # Set both TURN and STUN server defaults
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
print_parameters
|
||||
|
||||
bash Start_TURNServer.sh --turn "${turnserver}"
|
||||
|
||||
peerconnectionoptions='{\"iceServers\":[{\"urls\":[\"stun:$stunserver\",\"turn:$turnserver\"],\"username\":\"PixelStreamingUser\",\"credential\":\"AnotherTURNintheroad\"}]}'
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
arguments=""
|
||||
|
||||
if [ ! -z $IS_DEBUG ]; then
|
||||
arguments+=" --inspect"
|
||||
fi
|
||||
|
||||
arguments+=" --peerConnectionOptions=\"$peerconnectionoptions\" --PublicIp=$publicip"
|
||||
# Add arguments passed to script to arguments for executable
|
||||
arguments+=" ${cirruscmd}"
|
||||
|
||||
pushd ../..
|
||||
echo "Running: $process $arguments"
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process $arguments
|
||||
popd
|
||||
|
||||
popd
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
function log_msg() { #message
|
||||
if [ ! -z $VERBOSE ]; then
|
||||
echo $1
|
||||
fi
|
||||
}
|
||||
|
||||
function print_usage() {
|
||||
echo "
|
||||
Usage:
|
||||
${0} [--help] [--publicip <IP Address>] [--turn <turn server>] [--stun <stun server>] [cirrus options...]
|
||||
Where:
|
||||
--help will print this message and stop this script.
|
||||
--debug will run all scripts with --inspect
|
||||
--nosudo will run all scripts without \`sudo\` command useful for when run in containers.
|
||||
--verbose will enable additional logging
|
||||
--package-manager <package manager name> specify an alternative package manager to apt-get
|
||||
--publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for
|
||||
default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the Cirrus server and the default IP address of the TURN server
|
||||
--turn defines what TURN server to be used, syntax: --turn 127.0.0.1:19303
|
||||
default value: as above, IP address downloaded from https://api.ipify.org; in case if download failure it is set to 127.0.0.1
|
||||
--stun defined what STUN server to be used, syntax: --stun stun.l.google.com:19302
|
||||
default value as above
|
||||
Other options: stored and passed to the Cirrus server. All parameters printed once the script values are set.
|
||||
Command line options might be omitted to run with defaults and it is a good practice to omit specific ones when just starting the TURN or the STUN server alone, not the whole set of scripts.
|
||||
"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function print_parameters() {
|
||||
echo ""
|
||||
echo "${0} is running with the following parameters:"
|
||||
echo "--------------------------------------"
|
||||
if [[ -n "${stunserver}" ]]; then echo "STUN server : ${stunserver}" ; fi
|
||||
if [[ -n "${turnserver}" ]]; then echo "TURN server : ${turnserver}" ; fi
|
||||
echo "Public IP address : ${publicip}"
|
||||
echo "Cirrus server command line arguments: ${cirruscmd}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function set_start_default_values() {
|
||||
# publicip and cirruscmd are always needed
|
||||
publicip=$(curl -s https://api.ipify.org)
|
||||
if [[ -z $publicip ]]; then
|
||||
publicip="127.0.0.1"
|
||||
fi
|
||||
cirruscmd=""
|
||||
|
||||
if [ "$1" = "y" ]; then
|
||||
turnserver="${publicip}:19303"
|
||||
fi
|
||||
|
||||
if [ "$2" = "y" ]; then
|
||||
stunserver="stun.l.google.com:19302"
|
||||
fi
|
||||
}
|
||||
|
||||
function use_args() {
|
||||
while(($#)) ; do
|
||||
case "$1" in
|
||||
--debug ) IS_DEBUG=1; shift;;
|
||||
--nosudo ) NO_SUDO=1; shift;;
|
||||
--verbose ) VERBOSE=1; shift;;
|
||||
--stun ) stunserver="$2"; shift 2;;
|
||||
--turn ) turnserver="$2"; shift 2;;
|
||||
--publicip ) publicip="$2"; turnserver="${publicip}:19303"; shift 2;;
|
||||
--help ) print_usage;;
|
||||
* ) echo "Unknown command, adding to cirrus command line: $1"; cirruscmd+=" $1"; shift;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function call_setup_sh() {
|
||||
bash "setup.sh"
|
||||
}
|
||||
|
||||
function start_process() {
|
||||
if [ ! -z $NO_SUDO ]; then
|
||||
log_msg "running with sudo removed"
|
||||
eval $(echo "$@" | sed 's/sudo//g')
|
||||
else
|
||||
eval $@
|
||||
fi
|
||||
}
|
||||
|
||||
function get_version() {
|
||||
local version=$1
|
||||
|
||||
if command -v $version; then
|
||||
version=$($@)
|
||||
fi
|
||||
|
||||
echo $version | sed -E 's/[^0-9.]//g'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# When run from SignallingWebServer/platform_scripts/bash, this uses the SignallingWebServer directory
|
||||
# as the build context so the Cirrus files can be successfully copied into the container image
|
||||
docker build --network=host -t 'cirrus-webserver:latest' -f ./Dockerfile ../..
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Start docker container by name using host networking
|
||||
docker run --name cirrus_latest --network host --rm cirrus-webserver
|
||||
|
||||
# Interactive start example
|
||||
#docker run --name cirrus_latest --network host --rm -it --entrypoint /bin/bash cirrus-webserver
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Suppress printing of directory stack
|
||||
pushd () {
|
||||
command pushd "$@" > /dev/null
|
||||
}
|
||||
popd () {
|
||||
command popd "$@" > /dev/null
|
||||
}
|
||||
|
||||
# Stop both stun and turn
|
||||
pushd "$(dirname ${BASH_SOURCE[0]})"
|
||||
./docker-start-cirrus.sh --with-turn &
|
||||
popd
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
source turn_user_pwd.sh
|
||||
|
||||
USETURN="false"
|
||||
|
||||
for arg do
|
||||
shift
|
||||
[ "${arg}" = "--with-turn" ] && USETURN="true" && continue
|
||||
set -- "$@" "${arg}"
|
||||
done
|
||||
|
||||
# Get stun server data for passing to the container
|
||||
source common_utils.sh
|
||||
if [ "${USETURN}" = "true" ]; then
|
||||
set_start_default_values "y" "y" # Both TURN and STUN server defaults
|
||||
else
|
||||
set_start_default_values "n" "y" # Only STUN server defaults
|
||||
fi
|
||||
use_args "$@"
|
||||
|
||||
# Start docker container by name using host networking
|
||||
if [ "${USETURN}" = "true" ]; then
|
||||
peerConnectionOptions="{\""iceServers\"":[{\""urls\"":[\""stun:"${stunserver}"\"",\""turn":"${turnserver}\""],\""username\"":\""${turnusername}\"",\""credential\"":\""${turnpassword}\""}]}"
|
||||
else
|
||||
peerConnectionOptions="{\""iceServers\"":[{\""urls\"":[\""stun:"${stunserver}"\""]}]}"
|
||||
fi
|
||||
|
||||
docker run --name cirrus_latest --network host --rm --entrypoint /usr/local/bin/node cirrus-webserver /opt/SignallingWebServer/cirrus.js --peerConnectionOptions="${peerConnectionOptions}" --publicIp="${publicip}"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Get stun server data for passing to the container
|
||||
source common_utils.sh
|
||||
set_start_default_values "n" "y" # Only STUN server defaults
|
||||
use_args "$@"
|
||||
|
||||
localip=$(hostname -I | awk '{print $1}')
|
||||
echo "Private IP: $localip"
|
||||
|
||||
turnport="${turnserver##*:}"
|
||||
if [ -z "${turnport}" ]; then
|
||||
turnport=3478
|
||||
fi
|
||||
echo "TURN port: ${turnport}"
|
||||
echo ""
|
||||
|
||||
turnusername="PixelStreamingUser"
|
||||
turnpassword="AnotherTURNintheroad"
|
||||
realm="PixelStreaming"
|
||||
process="turnserver"
|
||||
arguments="-p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -n -u ${turnusername}:${turnpassword}"
|
||||
|
||||
# Add arguments passed to script to arguments for executable
|
||||
arguments+=" ${cirruscmd}"
|
||||
|
||||
# Start docker container by name using host networking
|
||||
echo "Running: ${process} ${arguments}"
|
||||
|
||||
# Get the docker image
|
||||
docker pull coturn/coturn
|
||||
|
||||
# Start the TURN server
|
||||
#docker run --name coturn_latest --network host -it --entrypoint /bin/bash coturn/coturn
|
||||
#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "sudo mkdir -p /var/run" coturn/coturn ""
|
||||
#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "/bin/ls" coturn/coturn "/var/"
|
||||
|
||||
docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "${process}" coturn/coturn "${arguments}"
|
||||
|
||||
#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "/bin/bash" coturn/coturn "ls -latr /var/run/"
|
||||
#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "sudo chown ubuntu:ubuntu /var/run/turnserver.pid | sudo chmod +x /var/run/turnserver.pid | ${process}" coturn/coturn "${arguments}"
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Suppress printing of directory stack
|
||||
pushd () {
|
||||
command pushd "$@" > /dev/null
|
||||
}
|
||||
popd () {
|
||||
command popd "$@" > /dev/null
|
||||
}
|
||||
|
||||
# Stop both stun and turn
|
||||
pushd "$(dirname ${BASH_SOURCE[0]})"
|
||||
./docker-stop-cirrus.sh
|
||||
./docker-stop-turn.sh
|
||||
popd
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Stop the docker container
|
||||
PSID=$(docker ps -a -q --filter="name=cirrus_latest")
|
||||
if [ -z "$PSID" ]; then
|
||||
echo "Docker stun is not running, no stopping will be done"
|
||||
exit 1;
|
||||
fi
|
||||
echo "Stopping stun server ..."
|
||||
docker stop cirrus_latest
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Stop the docker container
|
||||
PSID=$(docker ps -a -q --filter="name=coturn_latest")
|
||||
if [ -z "$PSID" ]; then
|
||||
echo "Docker turn is not running, no stopping will be done"
|
||||
exit 1;
|
||||
fi
|
||||
echo "Stopping turn server..."
|
||||
docker stop coturn_latest
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
set_start_default_values "n" "n" # No server specific defaults
|
||||
use_args "$@"
|
||||
call_setup_sh
|
||||
print_parameters
|
||||
|
||||
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
|
||||
arguments=""
|
||||
|
||||
if [ ! -z $IS_DEBUG ]; then
|
||||
arguments+=" --inspect"
|
||||
fi
|
||||
|
||||
arguments+=" --publicIp=${publicip}"
|
||||
arguments+=" ${cirruscmd}"
|
||||
|
||||
pushd ../.. > /dev/null
|
||||
|
||||
echo ""
|
||||
echo "Starting Cirrus server use ctrl-c to exit"
|
||||
echo "-----------------------------------------"
|
||||
echo ""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
start_process $process $arguments
|
||||
|
||||
popd > /dev/null # ../..
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
pushd "${BASH_LOCATION}" > /dev/null
|
||||
|
||||
source common_utils.sh
|
||||
|
||||
use_args $@
|
||||
# Azure specific fix to allow installing NodeJS from NodeSource
|
||||
if test -f "/etc/apt/sources.list.d/azure-cli.list"; then
|
||||
sudo touch /etc/apt/sources.list.d/nodesource.list
|
||||
sudo touch /usr/share/keyrings/nodesource.gpg
|
||||
sudo chmod 644 /etc/apt/sources.list.d/nodesource.list
|
||||
sudo chmod 644 /usr/share/keyrings/nodesource.gpg
|
||||
sudo chmod 644 /etc/apt/sources.list.d/azure-cli.list
|
||||
fi
|
||||
|
||||
function check_version() { #current_version #min_version
|
||||
#check if same string
|
||||
if [ -z "$2" ] || [ "$1" = "$2" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local i current minimum
|
||||
|
||||
IFS="." read -r -a current <<< $1
|
||||
IFS="." read -r -a minimum <<< $2
|
||||
|
||||
# fill empty fields in current with zeros
|
||||
for ((i=${#current[@]}; i<${#minimum[@]}; i++))
|
||||
do
|
||||
current[i]=0
|
||||
done
|
||||
|
||||
for ((i=0; i<${#current[@]}; i++))
|
||||
do
|
||||
if [[ -z ${minimum[i]} ]]; then
|
||||
# fill empty fields in minimum with zeros
|
||||
minimum[i]=0
|
||||
fi
|
||||
|
||||
if ((10#${current[i]} > 10#${minimum[i]})); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ((10#${current[i]} < 10#${minimum[i]})); then
|
||||
return 2
|
||||
fi
|
||||
done
|
||||
|
||||
# if got this far string is the same once we added missing 0
|
||||
return 0
|
||||
}
|
||||
|
||||
function check_and_install() { #dep_name #get_version_string #version_min #install_command
|
||||
local is_installed=0
|
||||
|
||||
log_msg "Checking for required $1 install"
|
||||
|
||||
local current=$(echo $2 | sed -E 's/[^0-9.]//g')
|
||||
local minimum=$(echo $3 | sed -E 's/[^0-9.]//g')
|
||||
|
||||
if [ $# -ne 4 ]; then
|
||||
log_msg "check_and_install expects 4 args (dep_name get_version_string version_min install_command) got $#"
|
||||
return -1
|
||||
fi
|
||||
|
||||
if [ ! -z $current ]; then
|
||||
log_msg "Current version: $current checking >= $minimum"
|
||||
check_version "$current" "$minimum"
|
||||
if [ "$?" -lt 2 ]; then
|
||||
log_msg "$1 is installed."
|
||||
return 0
|
||||
else
|
||||
log_msg "Required install of $1 not found installing"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $is_installed -ne 1 ]; then
|
||||
echo "$1 installation not found installing..."
|
||||
|
||||
start_process $4
|
||||
|
||||
if [ $? -ge 1 ]; then
|
||||
echo "Installation of $1 failed try running `export VERBOSE=1` then run this script again for more details"
|
||||
fi
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking Pixel Streaming Server dependencies."
|
||||
|
||||
# navigate to SignallingWebServer root
|
||||
pushd ../.. > /dev/null
|
||||
|
||||
node_version=""
|
||||
if [[ -f "${BASH_LOCATION}/node/bin/node" ]]; then
|
||||
node_version=$("${BASH_LOCATION}/node/bin/node" --version)
|
||||
fi
|
||||
check_and_install "node" "$node_version" "v16.4.2" "curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz --output node.tar.xz
|
||||
&& tar -xf node.tar.xz
|
||||
&& rm node.tar.xz
|
||||
&& mv node-v*-linux-x64 \"${BASH_LOCATION}/node\""
|
||||
|
||||
PATH="${BASH_LOCATION}/node/bin:$PATH"
|
||||
"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install
|
||||
|
||||
popd > /dev/null # SignallingWebServer
|
||||
|
||||
popd > /dev/null # BASH_SOURCE
|
||||
|
||||
#command #dep_name #get_version_string #version_min #install command
|
||||
coturn_version=$(if command -v turnserver &> /dev/null; then echo 1; else echo 0; fi)
|
||||
if [ $coturn_version -eq 0 ]; then
|
||||
if ! command -v apt-get &> /dev/null; then
|
||||
echo "Setup for the scripts is designed for use with distros that use the apt-get package manager" \
|
||||
"if you are seeing this message you will have to update \"${BASH_LOCATION}/setup.sh\" with\n" \
|
||||
"a package manger and the equivalent packages for your distribution. Please follow the\n" \
|
||||
"instructions found at https://pkgs.org/search/?q=coturn to install Coturn for your specific distribution"
|
||||
exit 1
|
||||
else
|
||||
if [ `id -u` -eq 0 ]; then
|
||||
check_and_install "coturn" "$coturn_version" "1" "apt-get install -y coturn"
|
||||
else
|
||||
check_and_install "coturn" "$coturn_version" "1" "sudo apt-get install -y coturn"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Plain text TURN setup
|
||||
turnusername="PixelStreamingUser"
|
||||
turnpassword="AnotherTURNintheroad"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
How to use files in this directory:
|
||||
- Files with .ps1 extension can be run with PowerShell[.exe] in Windows. Powershell needs to be started as Administrator to run setup.ps1 so it can run installation / installation check steps
|
||||
- Make sure that all of your dependencies are installed. Use .\setup.ps1 what will install whatever is missing as long as you are on a supported operating system
|
||||
|
||||
- Run a local instance of the Cirrus server by using the .\run_local.ps1 script
|
||||
|
||||
- Use the following scripts to run locally or in your cloud instance:
|
||||
- Start_SignallingServer.ps1 - Start only the Signalling (STUN) server
|
||||
- Start_TURNServer.ps1 - Start only the TURN server
|
||||
- Start_WithTURN_SignallingServer.ps1 - Start a TURN server and the Cirrus server together
|
||||
- The Start_Common.ps1 file contains shared functions for other Start_*.ps1 scripts and it is not supposed to run alone
|
||||
|
||||
- The local/cloud Start_*.ps1 powershell scripts can be invoked with the --help command line option to see how those can be configured. The following options can be supplied: --publicip, --turn, --stun. Please read the --help
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
# Do setup as a common task, it is smart and will not reinstall if not required.
|
||||
Start-Process -FilePath "$PSScriptRoot\setup.bat" -Wait -NoNewWindow
|
||||
|
||||
$global:ScriptName = $MyInvocation.MyCommand.Name
|
||||
$global:PublicIP = $null
|
||||
$global:StunServer = $null
|
||||
$global:TurnServer = $null
|
||||
$global:CirrusCmd = $null
|
||||
|
||||
function print_usage {
|
||||
echo "
|
||||
Usage (in MS Windows Power Shell):
|
||||
$global:ScriptName [--help] [--publicip <IP Address>] [--turn <turn server>] [--stun <stun server>] [cirrus options...]
|
||||
Where:
|
||||
--help will print this message and stop this script.
|
||||
--publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for
|
||||
default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the Cirrus server and the default IP address of the TURN server
|
||||
--turn defines what TURN server to be used, syntax: --turn 127.0.0.1:19303
|
||||
default value: as above, IP address downloaded from https://api.ipify.org; in case if download failure it is set to 127.0.0.1
|
||||
--stun defined what STUN server to be used, syntax: --stun stun.l.google.com:19302
|
||||
default value as above
|
||||
Other options: stored and passed to the Cirrus server. All parameters printed once the script values are set.
|
||||
Command line options might be omitted to run with defaults and it is a good practice to omit specific ones when just starting the TURN or the STUN server alone, not the whole set of scripts.
|
||||
"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function print_parameters {
|
||||
echo ""
|
||||
echo "$scriptname is running with the following parameters:"
|
||||
echo "--------------------------------------"
|
||||
if ($global:StunServer -ne $null) { echo "STUN server : $global:StunServer" }
|
||||
if ($global:TurnServer -ne $null) { echo "TURN server : $global:TurnServer" }
|
||||
echo "Public IP address : $global:PublicIP"
|
||||
echo "Cirrus server command line arguments: $global:CirrusCmd"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function set_start_default_values($SetTurnServerVar, $SetStunServerVar) {
|
||||
# publicip and cirruscmd are always needed
|
||||
$global:PublicIP = Invoke-WebRequest -Uri "https://api.ipify.org" -UseBasicParsing
|
||||
if ($global:PublicIP -eq $null -Or $global:PublicIP.length -eq 0) {
|
||||
$global:PublicIP = "127.0.0.1"
|
||||
} else {
|
||||
$global:PublicIP = ($global:PublicIP).Content
|
||||
}
|
||||
$global:cirruscmd = ""
|
||||
|
||||
if ($SetTurnServerVar -eq "y") {
|
||||
$global:TurnServer = $global:PublicIP + ":19303"
|
||||
}
|
||||
if ($SetStunServerVar -eq "y") {
|
||||
$global:StunServer = "stun.l.google.com:19302"
|
||||
}
|
||||
}
|
||||
|
||||
function use_args($arg) {
|
||||
$CmdArgs = $arg -split (" ")
|
||||
while($CmdArgs.count -gt 0) {
|
||||
$Cmd, $CmdArgs = $CmdArgs
|
||||
if ($Cmd -eq "--stun") {
|
||||
$global:StunServer, $CmdArgs = $CmdArgs
|
||||
} elseif ($Cmd -eq "--turn") {
|
||||
$global:TurnServer, $CmdArgs = $CmdArgs
|
||||
} elseif ($Cmd -eq "--publicip") {
|
||||
$global:PublicIP, $CmdArgs = $CmdArgs
|
||||
$global:TurnServer = $global:publicip + ":19303"
|
||||
} elseif ($Cmd -eq "--help") {
|
||||
print_usage
|
||||
} else {
|
||||
echo "Unknown command, adding to cirrus command line: $Cmd"
|
||||
$global:CirrusCmd += " $Cmd"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
. "$PSScriptRoot\Start_Common.ps1"
|
||||
|
||||
set_start_default_values "n" "y" # Set both TURN and STUN server defaults
|
||||
use_args($args)
|
||||
print_parameters
|
||||
|
||||
$peerConnectionOptions = "{ \""iceServers\"": [{\""urls\"": [\""stun:" + $global:StunServer + "\""]}] }"
|
||||
|
||||
$ProcessExe = "platform_scripts\cmd\node\node.exe"
|
||||
$Arguments = @("cirrus", "--peerConnectionOptions=""$peerConnectionOptions""", "--PublicIp=$global:PublicIp")
|
||||
# Add arguments passed to script to Arguments for executable
|
||||
$Arguments += $global:CirrusCmd
|
||||
|
||||
Push-Location $PSScriptRoot\..\..\
|
||||
Write-Output "Running: $ProcessExe $Arguments"
|
||||
Start-Process -FilePath $ProcessExe -ArgumentList "$Arguments" -Wait -NoNewWindow
|
||||
Pop-Location
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
. "$PSScriptRoot\Start_Common.ps1"
|
||||
|
||||
set_start_default_values "y" "n" # Set both TURN and STUN server defaults
|
||||
use_args($args)
|
||||
print_parameters
|
||||
#$LocalIp = Invoke-WebRequest -Uri "http://169.254.169.254/latest/meta-data/local-ipv4"
|
||||
$LocalIP = (Test-Connection -ComputerName (hostname) -Count 1 | Select IPV4Address).IPV4Address.IPAddressToString
|
||||
|
||||
Write-Output "Private IP: $LocalIp"
|
||||
|
||||
$TurnPort="19303"
|
||||
$Pos = $global:TurnServer.LastIndexOf(":")
|
||||
if ($Pos -ne -1) {
|
||||
$TurnPort = $global:TurnServer.Substring($Pos+ 1)
|
||||
}
|
||||
echo "TURN port: ${turnport}"
|
||||
echo ""
|
||||
|
||||
Push-Location $PSScriptRoot
|
||||
|
||||
$TurnUsername = "PixelStreamingUser"
|
||||
$TurnPassword = "AnotherTURNintheroad"
|
||||
$Realm = "PixelStreaming"
|
||||
$ProcessExe = ".\turnserver.exe"
|
||||
$Arguments = "-p $TurnPort -r $Realm -X $PublicIP -E $LocalIP -L $LocalIP --no-cli --no-tls --no-dtls --pidfile `"C:\coturn.pid`" -f -a -v -n -u $TurnUsername`:$TurnPassword"
|
||||
|
||||
# Add arguments passed to script to Arguments for executable
|
||||
$Arguments += $args
|
||||
|
||||
Push-Location $PSScriptRoot\coturn\
|
||||
Write-Output "Running: $ProcessExe $Arguments"
|
||||
# pause
|
||||
Start-Process -FilePath $ProcessExe -ArgumentList $Arguments -NoNewWindow
|
||||
Pop-Location
|
||||
|
||||
Pop-Location
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
. "$PSScriptRoot\Start_Common.ps1"
|
||||
|
||||
set_start_default_values "y" "y" # Set both TURN and STUN server defaults
|
||||
use_args($args)
|
||||
print_parameters
|
||||
|
||||
Push-Location $PSScriptRoot
|
||||
|
||||
Start-Process -FilePath "PowerShell" -ArgumentList ".\Start_TURNServer.ps1" -WorkingDirectory "$PSScriptRoot"
|
||||
|
||||
$peerConnectionOptions = "{ \""iceServers\"": [{\""urls\"": [\""stun:" + $global:StunServer + "\"",\""turn:" + $global:TurnServer + "\""], \""username\"": \""PixelStreamingUser\"", \""credential\"": \""AnotherTURNintheroad\""}] }"
|
||||
|
||||
$ProcessExe = "platform_scripts\cmd\node\node.exe"
|
||||
$Arguments = @("cirrus", "--peerConnectionOptions=""$peerConnectionOptions""", "--PublicIp=$global:PublicIp")
|
||||
# Add arguments passed to script to Arguments for executable
|
||||
$Arguments += $args
|
||||
|
||||
Push-Location $PSScriptRoot\..\..\
|
||||
Write-Output "Running: $ProcessExe $Arguments"
|
||||
Start-Process -FilePath $ProcessExe -ArgumentList $Arguments -Wait -NoNewWindow
|
||||
Pop-Location
|
||||
|
||||
Pop-Location
|
||||
@@ -0,0 +1,39 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script directory as working directory.
|
||||
pushd "%~dp0"
|
||||
|
||||
title Building Cirrus.exe
|
||||
|
||||
@Rem Run setup to ensure we have node and cirrus installed.
|
||||
call setup.bat
|
||||
|
||||
@Rem Look for a `nexe` directory next to this script
|
||||
if exist nexe\ (
|
||||
echo nexe directory found...skipping install.
|
||||
) else (
|
||||
echo nexe directory not found...beginning nexe install.
|
||||
|
||||
@Rem Make `nexe directory`
|
||||
mkdir nexe
|
||||
|
||||
@Rem npm init and install nexe
|
||||
pushd nexe
|
||||
call ..\node\npm init -y
|
||||
call ..\node\npm install nexe --save
|
||||
popd
|
||||
)
|
||||
|
||||
@Rem Move to cirrus directory.
|
||||
pushd ..\..
|
||||
|
||||
@Rem Build cirrus.exe using `nexe` using node 14.5.0 (as that is one of the latest prebuilts node versions in the nexe repo)
|
||||
call platform_scripts\cmd\node\npx nexe cirrus.js --target "x64-14.15.3" -r "Public/*" -r "scripts/*" -r "images/*" -r "config.json"
|
||||
|
||||
@Rem Pop cirrus directory.
|
||||
popd ..\..
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,25 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script directory as working directory.
|
||||
pushd "%~dp0"
|
||||
|
||||
title Cirrus
|
||||
|
||||
@Rem Run setup to ensure we have node and cirrus installed.
|
||||
call setup.bat
|
||||
|
||||
@Rem Move to cirrus directory.
|
||||
pushd ..\..
|
||||
|
||||
@Rem Run node server and pass any argument along.
|
||||
platform_scripts\cmd\node\node.exe cirrus %*
|
||||
|
||||
@Rem Pop cirrus directory.
|
||||
popd
|
||||
|
||||
@Rem Pop script directory.
|
||||
popd
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,20 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script location as working directory for commands.
|
||||
pushd "%~dp0"
|
||||
|
||||
@Rem Ensure we have NodeJs available for calling.
|
||||
call setup_node.bat
|
||||
|
||||
@Rem Ensure we have CoTURN available for calling.
|
||||
call setup_coturn.bat
|
||||
|
||||
@Rem Move to cirrus.js directory and install its package.json
|
||||
pushd %~dp0\..\..\
|
||||
call platform_scripts\cmd\node\npm install --no-save
|
||||
popd
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,25 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script location as working directory for commands.
|
||||
pushd "%~dp0"
|
||||
|
||||
@Rem Look for CoTURN directory next to this script
|
||||
if exist coturn\ (
|
||||
echo CoTURN directory found...skipping install.
|
||||
) else (
|
||||
echo CoTURN directory not found...beginning CoTURN download for Windows.
|
||||
|
||||
@Rem Download nodejs and follow redirects.
|
||||
curl -L -o ./turnserver.zip "https://github.com/mcottontensor/coturn/releases/download/v4.5.2-windows/turnserver.zip"
|
||||
|
||||
@Rem Unarchive the .zip to a directory called "turnserver"
|
||||
mkdir coturn & tar -xf turnserver.zip -C coturn
|
||||
|
||||
@Rem Delete the downloaded turnserver.zip
|
||||
del turnserver.zip
|
||||
)
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,35 @@
|
||||
@Rem Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
@echo off
|
||||
|
||||
@Rem Set script location as working directory for commands.
|
||||
pushd "%~dp0"
|
||||
|
||||
@Rem Name and version of node that we are downloading
|
||||
SET NodeVersion=v16.4.2
|
||||
SET NodeName=node-%NodeVersion%-win-x64
|
||||
|
||||
@Rem Look for a node directory next to this script
|
||||
if exist node\ (
|
||||
echo Node directory found...skipping install.
|
||||
) else (
|
||||
echo Node directory not found...beginning NodeJS download for Windows.
|
||||
|
||||
@Rem Download nodejs and follow redirects.
|
||||
curl -L -o ./node.zip "https://nodejs.org/dist/%NodeVersion%/%NodeName%.zip"
|
||||
|
||||
@Rem Unarchive the .zip
|
||||
tar -xf node.zip
|
||||
|
||||
@Rem Rename the extracted, versioned, directory that contains the NodeJS binaries to simply "node".
|
||||
ren "%NodeName%\" "node"
|
||||
|
||||
@Rem Delete the downloaded node.zip
|
||||
del node.zip
|
||||
)
|
||||
|
||||
@Rem Print node version
|
||||
echo Node version: & node\node.exe -v
|
||||
|
||||
@Rem Pop working directory
|
||||
popd
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
// This is the entrypoint to the stress test, all setup happens here
|
||||
function stressTest() {
|
||||
|
||||
|
||||
// This stress test creates a number of Pixel Streaming pages on the same page
|
||||
// using iframes and then tries to auto-connect them.
|
||||
|
||||
// The purpose of the stress test is to automate testing a large number of peers
|
||||
// connecting and disconnecting regularly from a single Unreal Engine streaming instance.
|
||||
|
||||
let self = this;
|
||||
this.play = true;
|
||||
this.maxPeers = 2;
|
||||
this.totalStreams = 0;
|
||||
this.streamCreationIntervalMs = 200;
|
||||
this.streamDeletionIntervalMs = 2000;
|
||||
this.pixelStreamingFrames = [];
|
||||
this.creationIntervalHandle = null;
|
||||
this.deletionIntervalHandle = null;
|
||||
|
||||
// Create a container to put the "Pixel Streaming" pages in.
|
||||
let streamsContainer = document.getElementById("streamsContainer");
|
||||
|
||||
function startStressTest() {
|
||||
setupNumPeersSlider();
|
||||
startStreamCreation();
|
||||
startStreamDeletion();
|
||||
setupPlayPause();
|
||||
|
||||
document.getElementById("creationIntervalInput").addEventListener("change", function(){
|
||||
self.streamCreationIntervalMs = document.getElementById("creationIntervalInput").value * 1000.0;
|
||||
startStreamCreation();
|
||||
});
|
||||
|
||||
document.getElementById("deletionIntervalInput").addEventListener("change", function(){
|
||||
self.streamDeletionIntervalMs = document.getElementById("deletionIntervalInput").value * 1000.0;
|
||||
startStreamDeletion();
|
||||
});
|
||||
}
|
||||
|
||||
function startStreamCreation() {
|
||||
|
||||
if(self.creationIntervalHandle) {
|
||||
clearInterval(self.creationIntervalHandle);
|
||||
}
|
||||
|
||||
// Create iframes of Pixel Streaming as a given interval (up to the max nPeers)
|
||||
self.creationIntervalHandle = setInterval(function(){
|
||||
|
||||
if(self.play) {
|
||||
let curNPeers = self.pixelStreamingFrames.length;
|
||||
if(curNPeers >= self.maxPeers) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a random amount of peers between 0 and up to max peers.
|
||||
let maxPeersToCreate = self.maxPeers - curNPeers;
|
||||
let nPeersToCreate = Math.ceil(Math.random() * maxPeersToCreate);
|
||||
|
||||
for(let i = 0; i < nPeersToCreate; i++) {
|
||||
let frame = createPixelStreamingFrame();
|
||||
let n = self.pixelStreamingFrames.length;
|
||||
frame.id = "PixelStreamingFrame_" + (n + 1);
|
||||
streamsContainer.append(frame);
|
||||
self.pixelStreamingFrames.push(frame);
|
||||
self.totalStreams += 1;
|
||||
updateTotalStreams();
|
||||
}
|
||||
}
|
||||
}, self.streamCreationIntervalMs);
|
||||
}
|
||||
|
||||
function startStreamDeletion() {
|
||||
|
||||
if(self.deletionIntervalHandle) {
|
||||
clearInterval(self.deletionIntervalHandle);
|
||||
}
|
||||
|
||||
self.deletionIntervalHandle = setInterval(function(){
|
||||
if(self.play) {
|
||||
let curNPeers = self.pixelStreamingFrames.length;
|
||||
if(curNPeers == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete a random amount of peers up to current number of peers
|
||||
let nPeersToDelete = Math.ceil(Math.random() * curNPeers);
|
||||
|
||||
for(let i = 0; i < nPeersToDelete; i++) {
|
||||
let frame = self.pixelStreamingFrames.shift();
|
||||
frame.parentNode.removeChild(frame);
|
||||
}
|
||||
}
|
||||
}, self.streamDeletionIntervalMs);
|
||||
|
||||
}
|
||||
|
||||
function updateTotalStreams() {
|
||||
let nStreamsLabel = document.getElementById("nStreamsLabel");
|
||||
nStreamsLabel.innerHTML = self.totalStreams;
|
||||
}
|
||||
|
||||
|
||||
function setupPlayPause() {
|
||||
let playPauseBtn = document.getElementById("playPause");
|
||||
playPauseBtn.addEventListener("click", () => {
|
||||
if(self.play) {
|
||||
self.play = false;
|
||||
playPauseBtn.innerHTML = "Play"
|
||||
} else {
|
||||
self.play = true;
|
||||
playPauseBtn.innerHTML = "Pause"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupNumPeersSlider() {
|
||||
// Tie number of peers to the slider
|
||||
let nPeersSlider = document.getElementById("nPeersSlider");
|
||||
nPeersSlider.value = self.maxPeers;
|
||||
|
||||
let nPeersLabel = document.getElementById("nPeerLabel");
|
||||
nPeersLabel.innerHTML = self.maxPeers;
|
||||
|
||||
// When the slide changes update the nPeers variable
|
||||
nPeersSlider.addEventListener("change", function(){
|
||||
self.maxPeers = nPeersSlider.value;
|
||||
nPeersLabel.innerHTML = nPeersSlider.value;
|
||||
});
|
||||
}
|
||||
|
||||
function createPixelStreamingFrame() {
|
||||
// Create an iframe that holds the Pixel Streaming page
|
||||
let streamIFrame = document.createElement("iframe");
|
||||
streamIFrame.src = "player.html";
|
||||
streamIFrame.onload = function(){
|
||||
|
||||
let pixelStreamingJS = streamIFrame.contentWindow;
|
||||
|
||||
// Don't show the play button
|
||||
pixelStreamingJS.connect_on_load = true;
|
||||
pixelStreamingJS.shouldShowPlayOverlay = false;
|
||||
|
||||
// Create a hook for when webRTCPlayer is setup
|
||||
let existingSetupPlayerFunc = pixelStreamingJS.setupWebRtcPlayer;
|
||||
let newSetupPlayerFunc = function(htmlElement, config){
|
||||
config.startVideoMuted = true;
|
||||
config.autoPlayAudio = false;
|
||||
let webrtcPlayer = existingSetupPlayerFunc(htmlElement, config);
|
||||
return webrtcPlayer;
|
||||
}
|
||||
pixelStreamingJS.setupWebRtcPlayer = newSetupPlayerFunc;
|
||||
|
||||
|
||||
pixelStreamingJS.connect();
|
||||
}
|
||||
return streamIFrame;
|
||||
}
|
||||
|
||||
// Start here
|
||||
startStressTest();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
function webRtcPlayer(parOptions) {
|
||||
parOptions = typeof parOptions !== 'undefined' ? parOptions : {};
|
||||
|
||||
var self = this;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
//**********************
|
||||
//Config setup
|
||||
//**********************
|
||||
this.cfg = typeof parOptions.peerConnectionOptions !== 'undefined' ? parOptions.peerConnectionOptions : {};
|
||||
this.cfg.sdpSemantics = 'unified-plan';
|
||||
|
||||
// If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
|
||||
// However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
|
||||
// tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
|
||||
this.cfg.offerExtmapAllowMixed = false;
|
||||
|
||||
this.forceTURN = urlParams.has('ForceTURN');
|
||||
if(this.forceTURN)
|
||||
{
|
||||
console.log("Forcing TURN usage by setting ICE Transport Policy in peer connection config.");
|
||||
this.cfg.iceTransportPolicy = "relay";
|
||||
}
|
||||
|
||||
this.cfg.bundlePolicy = "balanced";
|
||||
this.forceMaxBundle = urlParams.has('ForceMaxBundle');
|
||||
if(this.forceMaxBundle)
|
||||
{
|
||||
this.cfg.bundlePolicy = "max-bundle";
|
||||
}
|
||||
|
||||
//**********************
|
||||
//Variables
|
||||
//**********************
|
||||
this.pcClient = null;
|
||||
this.dcClient = null;
|
||||
this.tnClient = null;
|
||||
this.sfu = false;
|
||||
|
||||
this.sdpConstraints = {
|
||||
offerToReceiveAudio: 1, //Note: if you don't need audio you can get improved latency by turning this off.
|
||||
offerToReceiveVideo: 1,
|
||||
voiceActivityDetection: false
|
||||
};
|
||||
|
||||
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
||||
this.dataChannelOptions = {ordered: true};
|
||||
|
||||
// This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
|
||||
this.startVideoMuted = typeof parOptions.startVideoMuted !== 'undefined' ? parOptions.startVideoMuted : false;
|
||||
this.autoPlayAudio = typeof parOptions.autoPlayAudio !== 'undefined' ? parOptions.autoPlayAudio : true;
|
||||
|
||||
// To force mono playback of WebRTC audio
|
||||
this.forceMonoAudio = urlParams.has('ForceMonoAudio');
|
||||
if(this.forceMonoAudio){
|
||||
console.log("Will attempt to force mono audio by munging the sdp in the browser.")
|
||||
}
|
||||
|
||||
// To enable mic in browser use SSL/localhost and have ?useMic in the query string.
|
||||
this.useMic = urlParams.has('useMic');
|
||||
if(!this.useMic){
|
||||
console.log("Microphone access is not enabled. Pass ?useMic in the url to enable it.");
|
||||
}
|
||||
|
||||
// When ?useMic check for SSL or localhost
|
||||
let isLocalhostConnection = location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
||||
let isHttpsConnection = location.protocol === 'https:';
|
||||
if(this.useMic && !isLocalhostConnection && !isHttpsConnection)
|
||||
{
|
||||
this.useMic = false;
|
||||
console.error("Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.");
|
||||
console.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
||||
}
|
||||
|
||||
// Prefer SFU or P2P connection
|
||||
this.preferSFU = true//urlParams.has('preferSFU');
|
||||
console.log('USING SFU: ', this.preferSFU)
|
||||
console.log(this.preferSFU ?
|
||||
"The browser will signal it would prefer an SFU connection. Remove ?preferSFU from the url to signal for P2P usage." :
|
||||
"The browser will signal for a P2P connection. Pass ?preferSFU in the url to signal for SFU usage.");
|
||||
|
||||
// Latency tester
|
||||
this.latencyTestTimings =
|
||||
{
|
||||
TestStartTimeMs: null,
|
||||
UEReceiptTimeMs: null,
|
||||
UEEncodeMs: null,
|
||||
UECaptureToSendMs: null,
|
||||
UETransmissionTimeMs: null,
|
||||
BrowserReceiptTimeMs: null,
|
||||
FrameDisplayDeltaTimeMs: null,
|
||||
Reset: function()
|
||||
{
|
||||
this.TestStartTimeMs = null;
|
||||
this.UEReceiptTimeMs = null;
|
||||
this.UEEncodeMs = null,
|
||||
this.UECaptureToSendMs = null,
|
||||
this.UETransmissionTimeMs = null;
|
||||
this.BrowserReceiptTimeMs = null;
|
||||
this.FrameDisplayDeltaTimeMs = null;
|
||||
},
|
||||
SetUETimings: function(UETimings)
|
||||
{
|
||||
this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;
|
||||
this.UEEncodeMs = UETimings.EncodeMs,
|
||||
this.UECaptureToSendMs = UETimings.CaptureToSendMs,
|
||||
this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;
|
||||
this.BrowserReceiptTimeMs = Date.now();
|
||||
this.OnAllLatencyTimingsReady(this);
|
||||
},
|
||||
SetFrameDisplayDeltaTime: function(DeltaTimeMs)
|
||||
{
|
||||
if(this.FrameDisplayDeltaTimeMs == null)
|
||||
{
|
||||
this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);
|
||||
this.OnAllLatencyTimingsReady(this);
|
||||
}
|
||||
},
|
||||
OnAllLatencyTimingsReady: function(Timings){}
|
||||
}
|
||||
|
||||
//**********************
|
||||
//Functions
|
||||
//**********************
|
||||
|
||||
//Create Video element and expose that as a parameter
|
||||
this.createWebRtcVideo = function() {
|
||||
var video = document.createElement('video');
|
||||
|
||||
video.id = "streamingVideo";
|
||||
video.playsInline = true;
|
||||
video.disablePictureInPicture = true;
|
||||
video.muted = self.startVideoMuted;;
|
||||
|
||||
video.addEventListener('loadedmetadata', function(e){
|
||||
if(self.onVideoInitialised){
|
||||
self.onVideoInitialised();
|
||||
}
|
||||
}, true);
|
||||
|
||||
video.addEventListener('pause', function(e) {
|
||||
video.play();
|
||||
})
|
||||
|
||||
// Check if request video frame callback is supported
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
// The API is supported!
|
||||
|
||||
const onVideoFrameReady = (now, metadata) => {
|
||||
|
||||
if(metadata.receiveTime && metadata.expectedDisplayTime)
|
||||
{
|
||||
const receiveToCompositeMs = metadata.presentationTime - metadata.receiveTime;
|
||||
self.aggregatedStats.receiveToCompositeMs = receiveToCompositeMs;
|
||||
}
|
||||
|
||||
|
||||
// Re-register the callback to be notified about the next frame.
|
||||
video.requestVideoFrameCallback(onVideoFrameReady);
|
||||
};
|
||||
|
||||
// Initially register the callback to be notified about the first frame.
|
||||
video.requestVideoFrameCallback(onVideoFrameReady);
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
this.createWebRtcAudio = function() {
|
||||
var audio = document.createElement('audio');
|
||||
audio.id = 'streamingAudio';
|
||||
|
||||
return audio;
|
||||
}
|
||||
|
||||
this.video = this.createWebRtcVideo();
|
||||
this.audio = this.createWebRtcAudio();
|
||||
this.availableVideoStreams = new Map();
|
||||
|
||||
onsignalingstatechange = function(state) {
|
||||
console.info('Signaling state change. |', state.srcElement.signalingState, "|")
|
||||
};
|
||||
|
||||
oniceconnectionstatechange = function(state) {
|
||||
console.info('Browser ICE connection |', state.srcElement.iceConnectionState, '|')
|
||||
};
|
||||
|
||||
onicegatheringstatechange = function(state) {
|
||||
console.info('Browser ICE gathering |', state.srcElement.iceGatheringState, '|')
|
||||
};
|
||||
|
||||
handleOnTrack = function(e) {
|
||||
if (e.track)
|
||||
{
|
||||
console.log('Got track. | Kind=' + e.track.kind + ' | Id=' + e.track.id + ' | readyState=' + e.track.readyState + ' |');
|
||||
}
|
||||
|
||||
if(e.track.kind == "audio")
|
||||
{
|
||||
handleOnAudioTrack(e.streams[0]);
|
||||
return;
|
||||
}
|
||||
else if(e.track.kind == "video")
|
||||
{
|
||||
for (const s of e.streams) {
|
||||
if (!self.availableVideoStreams.has(s.id)) {
|
||||
self.availableVideoStreams.set(s.id, s);
|
||||
}
|
||||
}
|
||||
|
||||
self.video.srcObject = e.streams[0];
|
||||
|
||||
// All tracks are added "muted" by WebRTC/browser and become unmuted when media is being sent
|
||||
e.track.onunmute = () => {
|
||||
self.video.srcObject = e.streams[0];
|
||||
self.onNewVideoTrack(e.streams);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleOnAudioTrack = function(audioMediaStream)
|
||||
{
|
||||
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
||||
if(self.video.srcObject == audioMediaStream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// video element has some other media stream that is not associated with this audio track
|
||||
else if(self.video.srcObject && self.video.srcObject !== audioMediaStream)
|
||||
{
|
||||
self.audio.srcObject = audioMediaStream;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onDataChannel = function(dataChannelEvent){
|
||||
// This is the primary data channel code path when we are "receiving"
|
||||
console.log("Data channel created for us by browser as we are a receiving peer.");
|
||||
self.dcClient = dataChannelEvent.channel;
|
||||
setupDataChannelCallbacks(self.dcClient);
|
||||
}
|
||||
|
||||
createDataChannel = function(pc, label, options){
|
||||
// This is the primary data channel code path when we are "offering"
|
||||
let datachannel = pc.createDataChannel(label, options);
|
||||
console.log(`Created datachannel (${label})`);
|
||||
setupDataChannelCallbacks(datachannel);
|
||||
return datachannel;
|
||||
}
|
||||
|
||||
setupDataChannelCallbacks = function(datachannel) {
|
||||
try {
|
||||
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
||||
datachannel.binaryType = "arraybuffer";
|
||||
|
||||
datachannel.addEventListener('open', e => {
|
||||
console.log(`Data channel connected: ${datachannel.label}(${datachannel.id})`);
|
||||
if(self.onDataChannelConnected){
|
||||
self.onDataChannelConnected();
|
||||
}
|
||||
});
|
||||
|
||||
datachannel.addEventListener('close', e => {
|
||||
console.log(`Data channel disconnected: ${datachannel.label}(${datachannel.id}`, e);
|
||||
});
|
||||
|
||||
datachannel.addEventListener('message', e => {
|
||||
if (self.onDataChannelMessage){
|
||||
self.onDataChannelMessage(e.data);
|
||||
}
|
||||
});
|
||||
|
||||
datachannel.addEventListener('error', e => {
|
||||
console.error(`Data channel error: ${datachannel.label}(${datachannel.id}`, e);
|
||||
});
|
||||
|
||||
return datachannel;
|
||||
} catch (e) {
|
||||
console.warn('Datachannel setup caused an exception: ', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onicecandidate = function (e) {
|
||||
let candidate = e.candidate;
|
||||
if (candidate && candidate.candidate) {
|
||||
console.log("%c[Browser ICE candidate]", "background: violet; color: black", "| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|");
|
||||
self.onWebRtcCandidate(candidate);
|
||||
}
|
||||
};
|
||||
|
||||
handleCreateOffer = function (pc) {
|
||||
pc.createOffer(self.sdpConstraints).then(function (offer) {
|
||||
|
||||
// Munging is where we modifying the sdp string to set parameters that are not exposed to the browser's WebRTC API
|
||||
mungeSDP(offer);
|
||||
|
||||
// Set our munged SDP on the local peer connection so it is "set" and will be send across
|
||||
pc.setLocalDescription(offer);
|
||||
if (self.onWebRtcOffer) {
|
||||
self.onWebRtcOffer(offer);
|
||||
}
|
||||
},
|
||||
function () { console.warn("Couldn't create offer") });
|
||||
}
|
||||
|
||||
mungeSDP = function (offer) {
|
||||
|
||||
let audioSDP = '';
|
||||
|
||||
// set max bitrate to highest bitrate Opus supports
|
||||
audioSDP += 'maxaveragebitrate=510000;';
|
||||
|
||||
if(self.useMic){
|
||||
// set the max capture rate to 48khz (so we can send high quality audio from mic)
|
||||
audioSDP += 'sprop-maxcapturerate=48000;';
|
||||
}
|
||||
|
||||
// Force mono or stereo based on whether ?forceMono was passed or not
|
||||
audioSDP += self.forceMonoAudio ? 'sprop-stereo=0;stereo=0;' : 'sprop-stereo=1;stereo=1;';
|
||||
|
||||
// enable in-band forward error correction for opus audio
|
||||
audioSDP += 'useinbandfec=1';
|
||||
|
||||
// We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
|
||||
offer.sdp = offer.sdp.replace('useinbandfec=1', audioSDP);
|
||||
}
|
||||
|
||||
setupPeerConnection = function (pc) {
|
||||
//Setup peerConnection events
|
||||
pc.onsignalingstatechange = onsignalingstatechange;
|
||||
pc.oniceconnectionstatechange = oniceconnectionstatechange;
|
||||
pc.onicegatheringstatechange = onicegatheringstatechange;
|
||||
|
||||
pc.ontrack = handleOnTrack;
|
||||
pc.onicecandidate = onicecandidate;
|
||||
pc.ondatachannel = onDataChannel;
|
||||
};
|
||||
|
||||
generateAggregatedStatsFunction = function(){
|
||||
if(!self.aggregatedStats)
|
||||
self.aggregatedStats = {};
|
||||
|
||||
return function(stats){
|
||||
|
||||
let newStat = {};
|
||||
|
||||
// store each type of codec we can get stats on
|
||||
newStat.codecs = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
|
||||
// Get the inbound-rtp for video
|
||||
if (stat.type === 'inbound-rtp'
|
||||
&& !stat.isRemote
|
||||
&& (stat.mediaType === 'video' || stat.id.toLowerCase().includes('video'))) {
|
||||
|
||||
newStat.timestamp = stat.timestamp;
|
||||
newStat.bytesReceived = stat.bytesReceived;
|
||||
newStat.framesDecoded = stat.framesDecoded;
|
||||
newStat.packetsLost = stat.packetsLost;
|
||||
newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
|
||||
newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
|
||||
newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
|
||||
|
||||
if(self.aggregatedStats && self.aggregatedStats.timestamp){
|
||||
|
||||
// Get the mimetype of the video codec being used
|
||||
if(stat.codecId && self.aggregatedStats.codecs && self.aggregatedStats.codecs.hasOwnProperty(stat.codecId)){
|
||||
newStat.videoCodec = self.aggregatedStats.codecs[stat.codecId];
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.bytesReceived){
|
||||
// bitrate = bits received since last time / number of ms since last time
|
||||
//This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
|
||||
newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
|
||||
newStat.bitrate = Math.floor(newStat.bitrate);
|
||||
newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
|
||||
newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.bytesReceivedStart){
|
||||
newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
|
||||
newStat.avgBitrate = Math.floor(newStat.avgBitrate);
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.framesDecoded){
|
||||
// framerate = frames decoded since last time / number of seconds since last time
|
||||
newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
|
||||
newStat.framerate = Math.floor(newStat.framerate);
|
||||
newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
|
||||
newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.framesDecodedStart){
|
||||
newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
|
||||
newStat.avgframerate = Math.floor(newStat.avgframerate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get inbound-rtp for audio
|
||||
if (stat.type === 'inbound-rtp'
|
||||
&& !stat.isRemote
|
||||
&& (stat.mediaType === 'audio' || stat.id.toLowerCase().includes('audio'))) {
|
||||
|
||||
// Get audio bytes received
|
||||
if(stat.bytesReceived){
|
||||
newStat.audioBytesReceived = stat.bytesReceived;
|
||||
}
|
||||
|
||||
// As we loop back through we may wish to compute some stats based on a delta of the previous time we recorded the stat
|
||||
if(self.aggregatedStats && self.aggregatedStats.timestamp){
|
||||
|
||||
// Get the mimetype of the audio codec being used
|
||||
if(stat.codecId && self.aggregatedStats.codecs && self.aggregatedStats.codecs.hasOwnProperty(stat.codecId)){
|
||||
newStat.audioCodec = self.aggregatedStats.codecs[stat.codecId];
|
||||
}
|
||||
|
||||
// Determine audio bitrate delta over the time period
|
||||
if(self.aggregatedStats.audioBytesReceived){
|
||||
newStat.audioBitrate = 8 * (newStat.audioBytesReceived - self.aggregatedStats.audioBytesReceived) / (stat.timestamp - self.aggregatedStats.timestamp);
|
||||
newStat.audioBitrate = Math.floor(newStat.audioBitrate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Read video track stats
|
||||
if(stat.type === 'track' && (stat.trackIdentifier === 'video_label' || stat.kind === 'video')) {
|
||||
newStat.framesDropped = stat.framesDropped;
|
||||
newStat.framesReceived = stat.framesReceived;
|
||||
newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
|
||||
newStat.frameHeight = stat.frameHeight;
|
||||
newStat.frameWidth = stat.frameWidth;
|
||||
newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
|
||||
newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
|
||||
}
|
||||
|
||||
if(stat.type ==='candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0){
|
||||
newStat.currentRoundTripTime = stat.currentRoundTripTime;
|
||||
}
|
||||
|
||||
// Store mimetype of each codec
|
||||
if(newStat.hasOwnProperty('codecs') && stat.type === 'codec' && stat.mimeType && stat.id){
|
||||
const codecId = stat.id;
|
||||
const codecType = stat.mimeType.replace("video/", "").replace("audio/", "");
|
||||
newStat.codecs[codecId] = codecType;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if(self.aggregatedStats.receiveToCompositeMs)
|
||||
{
|
||||
newStat.receiveToCompositeMs = self.aggregatedStats.receiveToCompositeMs;
|
||||
self.latencyTestTimings.SetFrameDisplayDeltaTime(self.aggregatedStats.receiveToCompositeMs);
|
||||
}
|
||||
|
||||
self.aggregatedStats = newStat;
|
||||
|
||||
if(self.onAggregatedStats)
|
||||
self.onAggregatedStats(newStat)
|
||||
}
|
||||
};
|
||||
|
||||
setupTransceiversAsync = async function(pc){
|
||||
|
||||
let hasTransceivers = pc.getTransceivers().length > 0;
|
||||
|
||||
// Setup a transceiver for getting UE video
|
||||
pc.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
||||
if(!self.useMic)
|
||||
{
|
||||
pc.addTransceiver("audio", { direction: "recvonly" });
|
||||
}
|
||||
else
|
||||
{
|
||||
let audioSendOptions = self.useMic ?
|
||||
{
|
||||
autoGainControl: false,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
latency: 0,
|
||||
noiseSuppression: false,
|
||||
sampleRate: 48000,
|
||||
sampleSize: 16,
|
||||
volume: 1.0
|
||||
} : false;
|
||||
|
||||
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
||||
const stream = await navigator.mediaDevices.getUserMedia({video: false, audio: audioSendOptions});
|
||||
if(stream)
|
||||
{
|
||||
if(hasTransceivers){
|
||||
for(let transceiver of pc.getTransceivers()){
|
||||
if(transceiver && transceiver.receiver && transceiver.receiver.track && transceiver.receiver.track.kind === "audio")
|
||||
{
|
||||
for (const track of stream.getTracks()) {
|
||||
if(track.kind && track.kind == "audio")
|
||||
{
|
||||
transceiver.sender.replaceTrack(track);
|
||||
transceiver.direction = "sendrecv";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const track of stream.getTracks()) {
|
||||
if(track.kind && track.kind == "audio")
|
||||
{
|
||||
pc.addTransceiver(track, { direction: "sendrecv" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pc.addTransceiver("audio", { direction: "recvonly" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//**********************
|
||||
//Public functions
|
||||
//**********************
|
||||
|
||||
this.setVideoEnabled = function(enabled) {
|
||||
self.video.srcObject.getTracks().forEach(track => track.enabled = enabled);
|
||||
}
|
||||
|
||||
this.startLatencyTest = function(onTestStarted) {
|
||||
// Can't start latency test without a video element
|
||||
if(!self.video)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.latencyTestTimings.Reset();
|
||||
self.latencyTestTimings.TestStartTimeMs = Date.now();
|
||||
onTestStarted(self.latencyTestTimings.TestStartTimeMs);
|
||||
}
|
||||
|
||||
//This is called when revceiving new ice candidates individually instead of part of the offer
|
||||
this.handleCandidateFromServer = function(iceCandidate) {
|
||||
let candidate = new RTCIceCandidate(iceCandidate);
|
||||
|
||||
console.log("%c[Unreal ICE candidate]", "background: pink; color: black" ,"| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|");
|
||||
|
||||
// if forcing TURN, reject any candidates not relay
|
||||
if(self.forceTURN)
|
||||
{
|
||||
// check if no relay address is found, if so, we are assuming it means no TURN server
|
||||
if(candidate.candidate.indexOf("relay") < 0) {
|
||||
console.warn("Dropping candidate because it was not TURN relay.", "| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|")
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.pcClient.addIceCandidate(candidate).catch(function(e){
|
||||
console.error("Failed to add ICE candidate", e);
|
||||
});
|
||||
};
|
||||
|
||||
//Called externaly to create an offer for the server
|
||||
this.createOffer = function() {
|
||||
if(self.pcClient){
|
||||
console.log("Closing existing PeerConnection")
|
||||
self.pcClient.close();
|
||||
self.pcClient = null;
|
||||
}
|
||||
self.pcClient = new RTCPeerConnection(self.cfg);
|
||||
setupPeerConnection(self.pcClient);
|
||||
|
||||
setupTransceiversAsync(self.pcClient).finally(function()
|
||||
{
|
||||
self.dcClient = createDataChannel(self.pcClient, 'cirrus', self.dataChannelOptions);
|
||||
handleCreateOffer(self.pcClient);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
//Called externaly when an offer is received from the server
|
||||
this.receiveOffer = function(offer) {
|
||||
if (offer.sfu) {
|
||||
this.sfu = true;
|
||||
delete offer.sfu;
|
||||
}
|
||||
|
||||
if (!self.pcClient){
|
||||
console.log("Creating a new PeerConnection in the browser.")
|
||||
self.pcClient = new RTCPeerConnection(self.cfg);
|
||||
setupPeerConnection(self.pcClient);
|
||||
|
||||
// Put things here that happen post transceiver setup
|
||||
self.pcClient.setRemoteDescription(offer)
|
||||
.then(() =>
|
||||
{
|
||||
setupTransceiversAsync(self.pcClient).finally(function(){
|
||||
self.pcClient.createAnswer()
|
||||
.then(answer => {
|
||||
mungeSDP(answer);
|
||||
return self.pcClient.setLocalDescription(answer);
|
||||
})
|
||||
.then(() => {
|
||||
if (self.onWebRtcAnswer) {
|
||||
self.onWebRtcAnswer(self.pcClient.currentLocalDescription);
|
||||
}
|
||||
})
|
||||
.then(()=> {
|
||||
let receivers = self.pcClient.getReceivers();
|
||||
for(let receiver of receivers)
|
||||
{
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("createAnswer() failed:", error));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//Called externaly when an answer is received from the server
|
||||
this.receiveAnswer = function(answer) {
|
||||
self.pcClient.setRemoteDescription(answer);
|
||||
};
|
||||
|
||||
this.receiveSFUPeerDataChannelRequest = function(channelData) {
|
||||
const sendOptions = {
|
||||
ordered: true,
|
||||
negotiated: true,
|
||||
id: channelData.sendStreamId
|
||||
};
|
||||
const unidirectional = channelData.sendStreamId != channelData.recvStreamId;
|
||||
const sendDataChannel = self.pcClient.createDataChannel(unidirectional ? 'send-datachannel' : 'datachannel', sendOptions);
|
||||
setupDataChannelCallbacks(sendDataChannel);
|
||||
|
||||
if (unidirectional) {
|
||||
const recvOptions = {
|
||||
ordered: true,
|
||||
negotiated: true,
|
||||
id: channelData.recvStreamId
|
||||
};
|
||||
const recvDataChannel = self.pcClient.createDataChannel('recv-datachannel', recvOptions);
|
||||
|
||||
// when recv data channel is "open" we want to let SFU know so it can tell streamer
|
||||
recvDataChannel.addEventListener('open', e => {
|
||||
if(self.onSFURecvDataChannelReady) {
|
||||
self.onSFURecvDataChannelReady();
|
||||
}
|
||||
});
|
||||
|
||||
setupDataChannelCallbacks(recvDataChannel);
|
||||
}
|
||||
this.dcClient = sendDataChannel;
|
||||
}
|
||||
|
||||
this.close = function(){
|
||||
if(self.pcClient){
|
||||
console.log("Closing existing peerClient")
|
||||
self.pcClient.close();
|
||||
self.pcClient = null;
|
||||
}
|
||||
if(self.aggregateStatsIntervalId){
|
||||
clearInterval(self.aggregateStatsIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
//Sends data across the datachannel
|
||||
this.send = function(data){
|
||||
if(self.dcClient && self.dcClient.readyState == 'open'){
|
||||
//console.log('Sending data on dataconnection', self.dcClient)
|
||||
self.dcClient.send(data);
|
||||
}
|
||||
};
|
||||
|
||||
this.getStats = function(onStats){
|
||||
if(self.pcClient && onStats){
|
||||
self.pcClient.getStats(null).then((stats) => {
|
||||
onStats(stats);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.aggregateStats = function(checkInterval){
|
||||
let calcAggregatedStats = generateAggregatedStatsFunction();
|
||||
let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
|
||||
self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>bootstrap v3.x, v4.x</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>This is a requirement to using Bootstrap, providing better UI elements for the client web pages created for demoing pixelstreaming</Function>
|
||||
<Eula>https://github.com/twitter/bootstrap/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/Bootstrap_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Express v4.16.2</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Express is a web framework for Node.js.</Function>
|
||||
<Eula>https://github.com/expressjs/express/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Plugins/Experimental/PixelStreaming/Source/Express_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>FontAwesome Free v5.1</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Provides a consistent icon style to use in the sites for demoing pixelstreaming.</Function>
|
||||
<Eula>https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>None</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Helmet v.3.21.3</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/SignallingWebServer</Location>
|
||||
<Function>Helmet helps you secure your Express apps by setting various HTTP headers.</Function>
|
||||
<Eula>https://github.com/helmetjs/helmet/blob/v3.21.3/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>NONE (but keep license with code)</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>JQuery</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>This is a requirement to using Bootstrap, providing access to the DOM in the browser for easier and more advanced client side interactions and UI. Used for Project Cirrus.</Function>
|
||||
<Eula>https://github.com/jquery/jquery/blob/master/LICENSE.txt; https://js.foundation/pdf/ip-policy.pdf</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/JQuery_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Popper.js v1.14.3</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>A requirement to using Bootstrap.</Function>
|
||||
<Eula>https://github.com/FezVrasta/popper.js/blob/master/LICENSE.md</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/Popper.js_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Socket.io v2.0.4</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Enables real-time bidirectional event-based communication.</Function>
|
||||
<Eula>https://github.com/socketio/socket.io/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Plugins/Experimental/PixelStreaming/Source/Socket.io_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>WS</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/SignallingWebServer/</Location>
|
||||
<Function>It's used by SignallingWebServer (based on Node.js web-server) that is part of our PixelStreaming project.We add a dependency to WS library to Node.js configuration and it's downloaded automatically.</Function>
|
||||
<Eula>https://github.com/websockets/ws/blob/HEAD/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>//depot/UE4/Engine/Source/ThirdParty/Licenses/WS_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>WebRTC adapter (adapter.js) v6.3.2</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Used as a cross browser interface for WebRTC.</Function>
|
||||
<Eula>https://github.com/webrtc/adapter/blob/master/LICENSE.md</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/WebRTCadapter_License.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Yargs v15.3.0</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/SignallingWebServer/</Location>
|
||||
<Function>A module for Node.js, used to parse command line arguments, which is downloaded automatically by Node Package Manager.</Function>
|
||||
<Eula>https://github.com/yargs/yargs/blob/v15.3.0/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>None</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Bcrypt.js</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/SignallingWebServer/</Location>
|
||||
<Function>This is used to verify passwords match the ones stored using the bcrypt algorithm. The passwords are always stored using bcrypt and so we never know the unencrypted password. This allows us to implement a authentication system on the web server so that only people we give accounts to can access the web server. This use is only for prototype stage, production will use the Epic unreal account system</Function>
|
||||
<Eula>https://github.com/dcodeIO/bcrypt.js/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/UE4/Main/Engine/Source/ThirdParty/Licenses</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>express-session v1.15.6</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Used to create session id's used to remember a person who has logged into a server across page loads so that they don't have to log in every time the reload or navigate to a different page hosted on a webserver</Function>
|
||||
<Eula>https://github.com/expressjs/session/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/express-session_license.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>passport-local v1.0.0</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>This is a implementation for the Passport middleware that allows you to store user credentials locally on the machine (passwords are stored with bcrypt and not reversible) to be used to authenticate users on a node.js webserver. This use is only for prototype stage, production will use the Epic unreal account system.</Function>
|
||||
<Eula>https://github.com/jaredhanson/passport-local/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/passport-local_license.txt</LicenseFolder>
|
||||
</TpsData>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>passport v0.4.0</Name>
|
||||
<Location>/Samples/PixelStreaming/WebServers/</Location>
|
||||
<Function>Is the authentication middleware that adds the ability to securely log in a user to the webserver. This is a generic framework that you add specific implementation frameworks (separate TPS's will be provided for these) to to provide authentication on a node.js webserver.</Function>
|
||||
<Eula>https://github.com/jaredhanson/passport/blob/master/LICENSE</Eula>
|
||||
<RedistributeTo>
|
||||
<EndUserGroup>Licensees</EndUserGroup>
|
||||
<EndUserGroup>Git</EndUserGroup>
|
||||
<EndUserGroup>P4</EndUserGroup>
|
||||
</RedistributeTo>
|
||||
<LicenseFolder>/Engine/Source/ThirdParty/Licenses/passport_license.txt</LicenseFolder>
|
||||
</TpsData>
|
||||