updated ui 2

This commit is contained in:
DmitriyB
2023-02-02 15:55:59 +05:00
parent 7016d6c742
commit 9c1561567f
48 changed files with 355 additions and 5804 deletions
@@ -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,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

+208 -122
View File
@@ -1,20 +1,20 @@
/*Copyright Epic Games, Inc. All Rights Reserved.*/
:root {
/*Using colour scheme https://color.adobe.com/TD-Colors---Option-3-color-theme-10394433/*/
--colour1:#000000;
--colour2:#FFFFFF;
--colour3:#0585fe;
--colour4:#35b350;
--colour5:#ffab00;
--colour6:#1e1d22;
--colour7:#3c3b40;
--colour1: #000000;
--colour2: #FFFFFF;
--colour3: #0585fe;
--colour4: #35b350;
--colour5: #ffab00;
--colour6: #1e1d22;
--colour7: #3c3b40;
}
body{
margin: 0px;
background-color: black;
body {
margin: 0px;
background-color: black;
font-family: 'Montserrat', sans-serif;
}
}
#playerUI {
width: 100%;
@@ -22,8 +22,8 @@ body{
}
canvas {
image-rendering: crisp-edges;
position: absolute;
image-rendering: crisp-edges;
position: absolute;
}
video {
@@ -32,14 +32,14 @@ video {
height: 100%;
}
#player{
#player {
width: 100%;
height: 100%;
position: absolute;
background-color: #000;
}
#videoPlayOverlay{
#videoPlayOverlay {
position: absolute;
font-size: 1.8em;
width: 100%;
@@ -48,7 +48,7 @@ video {
}
/* State for element to be clickable */
.clickableState{
.clickableState {
align-items: center;
justify-content: center;
display: flex;
@@ -56,7 +56,7 @@ video {
}
/* State for element to show text, this is for informational use*/
.textDisplayState{
.textDisplayState {
align-items: center;
justify-content: center;
display: flex;
@@ -64,17 +64,68 @@ video {
}
/* State to hide overlay, WebRTC communication is in progress and or is playing */
.hiddenState{
.hiddenState {
display: none;
}
#playButton{
display: inline-block;
height: auto;
#playButton {
font-family: 'Inter', sans-serif;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 30;
background: #262626;
width: 494px;
height: 282px;
border: 1px solid #404040;
border-radius: 32px;
align-items: center;
justify-content: center;
}
img#playButton{
.caption {
font-style: normal;
font-weight: 400;
font-size: 22px;
line-height: 130%;
/* identical to box height, or 29px */
text-align: center;
/* Landing/White */
margin: 0;
color: #EBEBEB;
}
.caption1 {
margin: 0;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 130%;
/* or 18px */
text-align: center;
/* Landing/LightGray */
color: #888888;
}
.play-btn {
margin-top: 17px;
background: #454545;
border-radius: 50%;
outline: none;
border: none;
cursor: pointer;
width: 106px;
height: 106px;
}
img#playButton {
max-width: 241px;
width: 10%;
}
@@ -117,14 +168,14 @@ img#playButton{
border: 3px solid var(--colour3);
transition: 0.25s ease;
padding-left: 0.55rem;
padding-top: 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;
padding-top: 0.55rem;
}
#overlay img {
@@ -146,7 +197,7 @@ img#playButton{
position: absolute;
top: 0;
transform: translateY(25%);
left: 125%;
left: 125%;
z-index: 20;
}
@@ -158,19 +209,19 @@ img#playButton{
#connection .tooltiptext {
top: 125%;
transform: translateX(-25%);
left: 0;
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);
top: 125%;
transform: translateX(-50%);
left: 0;
z-index: 20;
padding: 5px 10px;
border: 3px solid var(--colour5);
width: max-content;
}
@@ -183,7 +234,7 @@ img#playButton{
display: block;
}
#controls > * {
#controls>* {
margin-bottom: 0.5rem;
border-radius: 50%;
display: block;
@@ -222,12 +273,18 @@ img#playButton{
}
.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
-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 */
}
@@ -261,10 +318,10 @@ img#playButton{
#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;
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;
}
@@ -281,7 +338,8 @@ img#playButton{
#close:after {
padding-left: 0.5rem;
display: inline-block;
content: "\00d7"; /* This will render the 'X' */
content: "\00d7";
/* This will render the 'X' */
}
#close:hover {
@@ -301,7 +359,7 @@ img#playButton{
margin: 0.5rem 0;
}
.settings-text{
.settings-text {
margin-right: 2rem;
display: flex;
}
@@ -310,24 +368,44 @@ img#playButton{
.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 {
}
.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 {
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 {
}
.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 {
}
.tgl+.tgl-slider {
outline: 0;
display: block;
width: 40px;
@@ -335,50 +413,53 @@ img#playButton{
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 {
-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 {
}
.tgl+.tgl-slider:after {
left: 0;
}
.tgl + .tgl-slider:before {
}
.tgl+.tgl-slider:before {
display: none;
}
.tgl-flat + .tgl-slider {
}
.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 {
}
.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 {
}
.tgl-flat:checked+.tgl-slider {
border: 3px solid var(--colour3);
}
.tgl-flat:checked + .tgl-slider:after {
}
.tgl-flat:checked+.tgl-slider:after {
left: 50%;
background: var(--colour3);
}
}
.subtitle-text {
margin: 0 0 0 1rem;
@@ -411,7 +492,8 @@ img#playButton{
#hiddenInput {
position: absolute;
left: -10%; /* Although invisible, push off-screen to prevent user interaction. */
left: -10%;
/* Although invisible, push off-screen to prevent user interaction. */
width: 0px;
opacity: 0;
}
@@ -428,47 +510,50 @@ img#playButton{
}
input {
text-align: right;
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;
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
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 2px;
background: currentColor;
left: 7px
}
.warning::after {
top: 2px;
height: 8px
top: 2px;
height: 8px
}
.warning::before {
height: 2px;
bottom: 2px
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;
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;
}
@@ -477,6 +562,7 @@ input[type="button"]:hover {
background-color: var(--colour3);
transition: ease 0.3s;
}
input[type="button"]:active {
background-color: transparent;
}
@@ -489,11 +575,11 @@ input[type="button"]:active {
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;
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 {
@@ -543,17 +629,17 @@ object {
#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;
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;
line-height: 1.5;
height: 100vh;
}
@@ -9,7 +9,7 @@
<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=Michroma&family=Montserrat:wght@600&display=swap" rel="stylesheet">
<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
+140 -146
View File
@@ -8,7 +8,7 @@ class TwoWayMap {
constructor(map = {}) {
this.map = map;
this.reverseMap = new Map();
for(const key in map) {
for (const key in map) {
const value = map[key];
this.reverseMap[value] = key;
}
@@ -76,7 +76,7 @@ let freezeFrame = {
let file = {
mimetype: "",
extension: "",
receiving: false,
receiving: false,
size: 0,
data: [],
valid: false,
@@ -329,7 +329,7 @@ function registerMessageHandlers() {
registerMessageHandler(MessageDirection.ToStreamer, "StopStreaming", sendMessageToStreamer);
registerMessageHandler(MessageDirection.ToStreamer, "LatencyTest", sendMessageToStreamer);
registerMessageHandler(MessageDirection.ToStreamer, "RequestInitialSettings", sendMessageToStreamer);
registerMessageHandler(MessageDirection.ToStreamer, "TestEcho", () => { /* Do nothing */});
registerMessageHandler(MessageDirection.ToStreamer, "TestEcho", () => { /* Do nothing */ });
registerMessageHandler(MessageDirection.ToStreamer, "UIInteraction", emitUIInteraction);
registerMessageHandler(MessageDirection.ToStreamer, "Command", emitCommand);
registerMessageHandler(MessageDirection.ToStreamer, "KeyDown", sendMessageToStreamer);
@@ -470,7 +470,7 @@ function onProtocolMessage(data) {
}
let direction = protocolJSON.Direction;
delete protocolJSON.Direction;
console.log(`Received new ${ direction == MessageDirection.FromStreamer ? "FromStreamer" : "ToStreamer" } protocol. Updating existing protocol...`);
console.log(`Received new ${direction == MessageDirection.FromStreamer ? "FromStreamer" : "ToStreamer"} protocol. Updating existing protocol...`);
Object.keys(protocolJSON).forEach((messageType) => {
let message = protocolJSON[messageType];
switch (direction) {
@@ -482,7 +482,7 @@ function onProtocolMessage(data) {
// return in a forEach is equivalent to a continue in a normal for loop
return;
}
if(message.byteLength > 0 && !message.hasOwnProperty("structure")) {
if (message.byteLength > 0 && !message.hasOwnProperty("structure")) {
// If we specify a bytelength, will must have a corresponding structure
console.error(`ToStreamer->${messageType} protocol definition was malformed as it specified a byteLength but no accompanying structure`);
// return in a forEach is equivalent to a continue in a normal for loop
@@ -632,72 +632,72 @@ function gamepadDisconnectHandler(e) {
function fullscreen() {
// if already full screen; exit
// else go fullscreen
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
let element;
//HTML elements controls
if(!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
// Chrome and FireFox on iOS can only fullscreen a <video>
element = document.getElementById("streamingVideo");
// else go fullscreen
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
// Everywhere else can fullscreen a <div>
element = document.getElementById("playerUI");
let element;
//HTML elements controls
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
// Chrome and FireFox on iOS can only fullscreen a <video>
element = document.getElementById("streamingVideo");
} else {
// Everywhere else can fullscreen a <div>
element = document.getElementById("playerUI");
}
if (!element) {
return;
}
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.webkitEnterFullscreen) {
element.webkitEnterFullscreen(); //for iphone this code worked
}
}
if(!element) {
return;
}
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.webkitEnterFullscreen) {
element.webkitEnterFullscreen(); //for iphone this code worked
}
}
onFullscreenChange()
onFullscreenChange()
}
function onFullscreenChange() {
isFullscreen = (document.webkitIsFullScreen
|| document.mozFullScreen
|| (document.msFullscreenElement && document.msFullscreenElement !== null)
|| (document.fullscreenElement && document.fullscreenElement !== null));
isFullscreen = (document.webkitIsFullScreen
|| document.mozFullScreen
|| (document.msFullscreenElement && document.msFullscreenElement !== null)
|| (document.fullscreenElement && document.fullscreenElement !== null));
let minimize = document.getElementById('minimize');
let minimize = document.getElementById('minimize');
let maximize = document.getElementById('maximize');
if(minimize && maximize){
if(isFullscreen) {
if (minimize && maximize) {
if (isFullscreen) {
minimize.style.display = 'inline';
maximize.style.display = 'none';
} else {
minimize.style.display = 'none';
maximize.style.display = 'inline';
}
}
}
}
function parseURLParams() {
let urlParams = new URLSearchParams(window.location.search);
inputOptions.controlScheme = (urlParams.has('hoveringMouse') ? ControlSchemeType.HoveringMouse : ControlSchemeType.LockedMouse);
inputOptions.controlScheme = (urlParams.has('hoveringMouse') ? ControlSchemeType.HoveringMouse : ControlSchemeType.LockedMouse);
let schemeToggle = document.getElementById("control-scheme-text");
switch (inputOptions.controlScheme) {
case ControlSchemeType.HoveringMouse:
@@ -712,12 +712,12 @@ function parseURLParams() {
break;
}
if(urlParams.has('noWatermark')) {
if (urlParams.has('noWatermark')) {
let watermark = document.getElementById("unrealengine");
watermark.style.display = 'none';
}
inputOptions.hideBrowserCursor = (urlParams.has('hideBrowserCursor') ? true : false);
inputOptions.hideBrowserCursor = (urlParams.has('hideBrowserCursor') ? true : false);
}
@@ -754,21 +754,21 @@ function setupHtmlEvents() {
let resizeCheckBox = document.getElementById('enlarge-display-to-fill-window-tgl');
if (resizeCheckBox !== null) {
resizeCheckBox.onchange = function(event) {
resizeCheckBox.onchange = function (event) {
resizePlayerStyle();
};
}
qualityControlOwnershipCheckBox = document.getElementById('quality-control-ownership-tgl');
if (qualityControlOwnershipCheckBox !== null) {
qualityControlOwnershipCheckBox.onchange = function(event) {
qualityControlOwnershipCheckBox.onchange = function (event) {
requestQualityControl();
};
}
let encoderParamsSubmit = document.getElementById('encoder-params-submit');
if (encoderParamsSubmit !== null) {
encoderParamsSubmit.onclick = function(event) {
encoderParamsSubmit.onclick = function (event) {
let minQP = document.getElementById('encoder-min-qp-text').value;
let maxQP = document.getElementById('encoder-max-qp-text').value;
@@ -780,7 +780,7 @@ function setupHtmlEvents() {
let webrtcParamsSubmit = document.getElementById('webrtc-params-submit');
if (webrtcParamsSubmit !== null) {
webrtcParamsSubmit.onclick = function(event) {
webrtcParamsSubmit.onclick = function (event) {
let FPS = document.getElementById('webrtc-fps-text').value;
let minBitrate = document.getElementById('webrtc-min-bitrate-text').value * 1000;
let maxBitrate = document.getElementById('webrtc-max-bitrate-text').value * 1000;
@@ -822,7 +822,7 @@ function setupHtmlEvents() {
let statsCheckBox = document.getElementById('show-stats-tgl');
if (statsCheckBox !== null) {
statsCheckBox.onchange = function(event) {
statsCheckBox.onchange = function (event) {
let stats = document.getElementById('statsContainer');
stats.style.display = event.target.checked ? "block" : "none";
};
@@ -848,7 +848,7 @@ function setupHtmlEvents() {
var streamSelector = document.getElementById('stream-select');
var trackSelector = document.getElementById('track-select');
if (streamSelector) {
streamSelector.onchange = function(event) {
streamSelector.onchange = function (event) {
const stream = webRtcPlayerObj.availableVideoStreams.get(streamSelector.value);
webRtcPlayerObj.video.srcObject = stream;
streamTrackSource = stream;
@@ -857,7 +857,7 @@ function setupHtmlEvents() {
}
if (trackSelector) {
trackSelector.onchange = function(event) {
trackSelector.onchange = function (event) {
if (!streamTrackSource) {
streamTrackSource = webRtcPlayerObj.availableVideoStreams.get(streamSelector.value);
}
@@ -876,9 +876,9 @@ function setupHtmlEvents() {
}
}
function setupToggleWithUrlParams(toggleId, urlParameterKey){
function setupToggleWithUrlParams(toggleId, urlParameterKey) {
let toggleElem = document.getElementById(toggleId);
if(toggleElem) {
if (toggleElem) {
toggleElem.checked = new URLSearchParams(window.location.search).has(urlParameterKey);
toggleElem.addEventListener('change', (event) => {
const urlParams = new URLSearchParams(window.location.search);
@@ -942,7 +942,7 @@ function sendStartLatencyTest() {
return;
}
let onTestStarted = function(StartTimeMs) {
let onTestStarted = function (StartTimeMs) {
let descriptor = {
StartTime: StartTimeMs
};
@@ -988,7 +988,7 @@ function setOverlay(htmlClass, htmlElement, onClickFunction) {
function showConnectOverlay() {
let startText = document.createElement('div');
startText.id = 'playButton';
startText.innerHTML = 'Click to start'.toUpperCase();
startText.innerHTML = '<p class="caption"> Демонстрация запущена <p class="caption1"> Нажмите, чтобы начать </p> <button class="play-btn"><img src="./Play.svg"></img</button>';
setOverlay('clickableState', startText, event => {
connect();
@@ -1004,8 +1004,8 @@ function showTextOverlay(text) {
}
function playStream() {
if(webRtcPlayerObj && webRtcPlayerObj.video) {
if(webRtcPlayerObj.audio.srcObject && autoPlayAudio) {
if (webRtcPlayerObj && webRtcPlayerObj.video) {
if (webRtcPlayerObj.audio.srcObject && autoPlayAudio) {
// Video and Audio are seperate tracks
webRtcPlayerObj.audio.play().then(() => {
// audio play has succeeded, start playing video
@@ -1026,7 +1026,7 @@ function playStream() {
function playVideo() {
webRtcPlayerObj.video.play().catch((onRejectedReason) => {
if(webRtcPlayerObj.audio.srcObject) {
if (webRtcPlayerObj.audio.srcObject) {
webRtcPlayerObj.audio.stop();
}
console.error(onRejectedReason);
@@ -1071,7 +1071,7 @@ function showAfkOverlay() {
document.exitPointerLock();
}
afk.countdownTimer = setInterval(function() {
afk.countdownTimer = setInterval(function () {
afk.countdown--;
if (afk.countdown == 0) {
// The user failed to click so disconnect them.
@@ -1290,7 +1290,7 @@ function setupWebRtcPlayer(htmlElement, config) {
htmlElement.appendChild(webRtcPlayerObj.audio);
htmlElement.appendChild(freezeFrameOverlay);
webRtcPlayerObj.onWebRtcOffer = function(offer) {
webRtcPlayerObj.onWebRtcOffer = function (offer) {
if (ws && ws.readyState === WS_OPEN_STATE) {
let offerStr = JSON.stringify(offer);
console.log("%c[Outbound SS message (offer)]", "background: lightgreen; color: black", offer);
@@ -1298,7 +1298,7 @@ function setupWebRtcPlayer(htmlElement, config) {
}
};
webRtcPlayerObj.onWebRtcCandidate = function(candidate) {
webRtcPlayerObj.onWebRtcCandidate = function (candidate) {
if (ws && ws.readyState === WS_OPEN_STATE) {
ws.send(JSON.stringify({
type: 'iceCandidate',
@@ -1322,7 +1322,7 @@ function setupWebRtcPlayer(htmlElement, config) {
}
};
webRtcPlayerObj.onSFURecvDataChannelReady = function() {
webRtcPlayerObj.onSFURecvDataChannelReady = function () {
if (webRtcPlayerObj.sfu) {
// Send SFU a message to let it know browser data channels are ready
const requestMsg = { type: "peerDataChannelsReady" };
@@ -1331,7 +1331,7 @@ function setupWebRtcPlayer(htmlElement, config) {
}
}
webRtcPlayerObj.onVideoInitialised = function() {
webRtcPlayerObj.onVideoInitialised = function () {
if (ws && ws.readyState === WS_OPEN_STATE) {
if (shouldShowPlayOverlay) {
showPlayOverlay();
@@ -1351,7 +1351,7 @@ function setupWebRtcPlayer(htmlElement, config) {
updateStreamList();
}
webRtcPlayerObj.onDataChannelMessage = function(data) {
webRtcPlayerObj.onDataChannelMessage = function (data) {
let view = new Uint8Array(data);
try {
let messageType = fromStreamerMessages.getFromValue(view[0]);
@@ -1375,8 +1375,8 @@ function setupWebRtcPlayer(htmlElement, config) {
return webRtcPlayerObj.video;
}
function setupStats(){
webRtcPlayerObj.aggregateStats(1 * 1000 /*Check every 1 second*/ );
function setupStats() {
webRtcPlayerObj.aggregateStats(1 * 1000 /*Check every 1 second*/);
let printInterval = 5 * 60 * 1000; /*Print every 5 minutes*/
let nextPrintDuration = printInterval;
@@ -1416,14 +1416,14 @@ function setupStats(){
let qualityStatus = document.getElementById("connectionStrength");
// "blinks" quality status element for 1 sec by making it transparent, speed = number of blinks
let blinkQualityStatus = function(speed) {
let blinkQualityStatus = function (speed) {
let iter = speed;
let opacity = 1; // [0..1]
let tickId = setInterval(
function() {
function () {
opacity -= 0.1;
// map `opacity` to [-0.5..0.5] range, decrement by 0.2 per step and take `abs` to make it blink: 1 -> 0 -> 1
qualityStatus.style.opacity = `${Math.abs((opacity - 0.5) * 2)}`;
qualityStatus.style.opacity = `${Math.abs((opacity - 0.5) * 2)}`;
if (opacity <= 0.1) {
if (--iter == 0) {
clearInterval(tickId);
@@ -1479,10 +1479,9 @@ function setupStats(){
statsText += `<div>Duration: ${timeFormat.format(runTimeHours)}:${timeFormat.format(runTimeMinutes)}:${timeFormat.format(runTimeSeconds)}</div>`;
statsText += `<div>Controls stream input: ${inputController === null ? "Not sent yet" : (inputController ? "true" : "false")}</div>`;
statsText += `<div>Audio codec: ${aggregatedStats.hasOwnProperty('audioCodec') ? aggregatedStats.audioCodec : "Not set" }</div>`;
statsText += `<div>Video codec: ${aggregatedStats.hasOwnProperty('videoCodec') ? aggregatedStats.videoCodec : "Not set" }</div>`;
statsText += `<div>Video Resolution: ${
aggregatedStats.hasOwnProperty('frameWidth') && aggregatedStats.frameWidth && aggregatedStats.hasOwnProperty('frameHeight') && aggregatedStats.frameHeight ?
statsText += `<div>Audio codec: ${aggregatedStats.hasOwnProperty('audioCodec') ? aggregatedStats.audioCodec : "Not set"}</div>`;
statsText += `<div>Video codec: ${aggregatedStats.hasOwnProperty('videoCodec') ? aggregatedStats.videoCodec : "Not set"}</div>`;
statsText += `<div>Video Resolution: ${aggregatedStats.hasOwnProperty('frameWidth') && aggregatedStats.frameWidth && aggregatedStats.hasOwnProperty('frameHeight') && aggregatedStats.frameHeight ?
aggregatedStats.frameWidth + 'x' + aggregatedStats.frameHeight : 'Chrome only'
}</div>`;
statsText += `<div>Received (${receivedBytesMeasurement}): ${numberFormat.format(receivedBytes)}</div>`;
@@ -1515,7 +1514,7 @@ function setupStats(){
}
};
webRtcPlayerObj.latencyTestTimings.OnAllLatencyTimingsReady = function(timings) {
webRtcPlayerObj.latencyTestTimings.OnAllLatencyTimingsReady = function (timings) {
if (!timings.BrowserReceiptTimeMs) {
return;
@@ -1563,7 +1562,7 @@ function onWebRtcSFUPeerDatachannels(webRTCData) {
}
function onWebRtcIce(iceCandidate) {
if (webRtcPlayerObj){
if (webRtcPlayerObj) {
webRtcPlayerObj.handleCandidateFromServer(iceCandidate);
}
}
@@ -1689,7 +1688,7 @@ function invalidateFreezeFrameOverlay() {
freezeFrame.valid = false;
freezeFrameOverlay.classList.remove("freezeframeBackground");
}, freezeFrameDelay);
if (webRtcPlayerObj) {
webRtcPlayerObj.setVideoEnabled(true);
}
@@ -1796,7 +1795,7 @@ function updateVideoStreamSize() {
return;
let descriptor = {
"Resolution.Width": playerElement.clientWidth,
"Resolution.Width": playerElement.clientWidth,
"Resolution.Height": playerElement.clientHeight
};
emitCommand(descriptor);
@@ -1815,14 +1814,14 @@ let _orientationChangeTimeout;
function onOrientationChange(event) {
clearTimeout(_orientationChangeTimeout);
_orientationChangeTimeout = setTimeout(function() {
_orientationChangeTimeout = setTimeout(function () {
resizePlayerStyle();
}, 500);
}
function sendMessageToStreamer(messageType, indata = []) {
messageFormat = toStreamerMessages.getFromKey(messageType);
if(messageFormat === undefined) {
if (messageFormat === undefined) {
console.error(`Attempted to send a message to the streamer with message type: ${messageType}, but the frontend hasn't been configured to send such a message. Check you've added the message type in your cpp`);
return;
}
@@ -1864,7 +1863,7 @@ function emitDescriptor(messageType, descriptor) {
// Convert the descriptor object into a JSON string.
let descriptorAsString = JSON.stringify(descriptor);
let messageFormat = toStreamerMessages.getFromKey(messageType);
if(messageFormat === undefined) {
if (messageFormat === undefined) {
console.error(`Attempted to emit descriptor with message type: ${messageType}, but the frontend hasn't been configured to send such a message. Check you've added the message type in your cpp`);
}
// Add the UTF-16 JSON string to the array byte buffer, going two bytes at
@@ -2102,7 +2101,7 @@ function createOnScreenKeyboardHelpers(htmlElement) {
// Hide the 'edit text' button.
editTextButton.classList.add('hiddenState');
editTextButton.addEventListener('click', function() {
editTextButton.addEventListener('click', function () {
// Show the on-screen keyboard.
hiddenInput.focus();
});
@@ -2126,7 +2125,7 @@ function showOnScreenKeyboard(command) {
}
function registerMouseEnterAndLeaveEvents(playerElement) {
playerElement.onmouseenter = function(e) {
playerElement.onmouseenter = function (e) {
if (print_inputs) {
console.log('mouse enter');
}
@@ -2134,7 +2133,7 @@ function registerMouseEnterAndLeaveEvents(playerElement) {
playerElement.pressMouseButtons(e);
};
playerElement.onmouseleave = function(e) {
playerElement.onmouseleave = function (e) {
if (print_inputs) {
console.log('mouse leave');
}
@@ -2155,7 +2154,7 @@ function registerLockedMouseEvents(playerElement) {
playerElement.requestPointerLock = playerElement.requestPointerLock || playerElement.mozRequestPointerLock;
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
playerElement.onclick = function() {
playerElement.onclick = function () {
playerElement.requestPointerLock();
};
@@ -2221,11 +2220,11 @@ function registerLockedMouseEvents(playerElement) {
toStreamerHandlers.MouseDown("MouseDouble", [e.button, coord.x, coord.y]);
};
playerElement.pressMouseButtons = function(e) {
playerElement.pressMouseButtons = function (e) {
pressMouseButtons(e.buttons, x, y);
};
playerElement.releaseMouseButtons = function(e) {
playerElement.releaseMouseButtons = function (e) {
releaseMouseButtons(e.buttons, x, y);
};
}
@@ -2277,11 +2276,11 @@ function registerHoveringMouseEvents(playerElement) {
toStreamerHandlers.MouseDown("MouseDouble", [e.button, coord.x, coord.y]);
};
playerElement.pressMouseButtons = function(e) {
playerElement.pressMouseButtons = function (e) {
pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
};
playerElement.releaseMouseButtons = function(e) {
playerElement.releaseMouseButtons = function (e) {
releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
};
}
@@ -2303,7 +2302,7 @@ function registerTouchEvents(playerElement) {
function forgetTouch(touch) {
fingers.push(fingerIds[touch.identifier]);
// Sort array back into descending order. This means if finger '1' were to lift after finger '0', we would ensure that 0 will be the first index to pop
fingers.sort(function(a, b){return b - a});
fingers.sort(function (a, b) { return b - a });
delete fingerIds[touch.identifier];
}
@@ -2317,8 +2316,8 @@ function registerTouchEvents(playerElement) {
console.log(`F${fingerIds[touch.identifier]}=(${x}, ${y})`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
switch(type) {
switch (type) {
case "TouchStart":
toStreamerHandlers.TouchStart("TouchStart", [numTouches, coord.x, coord.y, fingerIds[touch.identifier], MaxByteValue * touch.force, coord.inRange ? 1 : 0]);
break;
@@ -2336,7 +2335,7 @@ function registerTouchEvents(playerElement) {
let finger = undefined;
playerElement.ontouchstart = function(e) {
playerElement.ontouchstart = function (e) {
if (finger === undefined) {
let firstTouch = e.changedTouches[0];
finger = {
@@ -2354,7 +2353,7 @@ function registerTouchEvents(playerElement) {
e.preventDefault();
};
playerElement.ontouchend = function(e) {
playerElement.ontouchend = function (e) {
for (let t = 0; t < e.changedTouches.length; t++) {
let touch = e.changedTouches[t];
if (touch.identifier === finger.id) {
@@ -2371,7 +2370,7 @@ function registerTouchEvents(playerElement) {
e.preventDefault();
};
playerElement.ontouchmove = function(e) {
playerElement.ontouchmove = function (e) {
for (let t = 0; t < e.touches.length; t++) {
let touch = e.touches[t];
if (touch.identifier === finger.id) {
@@ -2388,7 +2387,7 @@ function registerTouchEvents(playerElement) {
e.preventDefault();
};
} else {
playerElement.ontouchstart = function(e) {
playerElement.ontouchstart = function (e) {
// Assign a unique identifier to each touch.
for (let t = 0; t < e.changedTouches.length; t++) {
rememberTouch(e.changedTouches[t]);
@@ -2401,7 +2400,7 @@ function registerTouchEvents(playerElement) {
e.preventDefault();
};
playerElement.ontouchend = function(e) {
playerElement.ontouchend = function (e) {
if (print_inputs) {
console.log('touch end');
}
@@ -2414,7 +2413,7 @@ function registerTouchEvents(playerElement) {
e.preventDefault();
};
playerElement.ontouchmove = function(e) {
playerElement.ontouchmove = function (e) {
if (print_inputs) {
console.log('touch move');
}
@@ -2556,9 +2555,9 @@ function getKeyCode(e) {
// If we don't have keyCode property because browser API is deprecated then use KeyboardEvent.code instead.
// See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#constants_for_keycode_value
if(!("keyCode" in e)) {
if (!("keyCode" in e)) {
// Convert KeyboardEvent.code string into integer-based key code for backwards compatibility reasons.
if(e.code in CodeToKeyCode) {
if (e.code in CodeToKeyCode) {
return CodeToKeyCode[e.code];
} else {
console.warn(`Keyboard code of ${e.code} is not supported in our mapping, ignoring this key.`);
@@ -2577,10 +2576,10 @@ function getKeyCode(e) {
function registerKeyboardEvents() {
document.onkeydown = function(e) {
document.onkeydown = function (e) {
const keyCode = getKeyCode(e);
if(!keyCode) { return; }
if (!keyCode) { return; }
if (print_inputs) {
console.log(`key down ${keyCode}, repeat = ${e.repeat}`);
@@ -2602,10 +2601,10 @@ function registerKeyboardEvents() {
};
document.onkeyup = function(e) {
document.onkeyup = function (e) {
const keyCode = getKeyCode(e);
if(!keyCode) { return; }
if (!keyCode) { return; }
if (print_inputs) {
console.log(`key up ${keyCode}`);
@@ -2618,8 +2617,8 @@ function registerKeyboardEvents() {
}
};
document.onkeypress = function(e){
if(!("charCode" in e)){
document.onkeypress = function (e) {
if (!("charCode" in e)) {
console.warn("KeyboardEvent.charCode is deprecated in this browser, cannot send key press.");
return;
}
@@ -2632,30 +2631,28 @@ function registerKeyboardEvents() {
};
}
function settingsClicked( /* e */ ) {
function settingsClicked( /* e */) {
/**
* Toggle settings panel. If stats panel is already open, close it and then open settings
*/
let settings = document.getElementById('settings-panel');
let stats = document.getElementById('stats-panel');
if(stats.classList.contains("panel-wrap-visible"))
{
if (stats.classList.contains("panel-wrap-visible")) {
stats.classList.toggle("panel-wrap-visible");
}
settings.classList.toggle("panel-wrap-visible");
}
function statsClicked( /* e */ ) {
function statsClicked( /* e */) {
/**
* Toggle stats panel. If settings panel is already open, close it and then open stats
*/
let settings = document.getElementById('settings-panel');
let stats = document.getElementById('stats-panel');
if(settings.classList.contains("panel-wrap-visible"))
{
if (settings.classList.contains("panel-wrap-visible")) {
settings.classList.toggle("panel-wrap-visible");
}
@@ -2703,21 +2700,21 @@ function connect() {
ws = new WebSocket(connectionUrl);
ws.attemptStreamReconnection = true;
ws.onmessagebinary = function(event) {
if(!event || !event.data) { return; }
ws.onmessagebinary = function (event) {
if (!event || !event.data) { return; }
event.data.text().then(function(messageString){
event.data.text().then(function (messageString) {
// send the new stringified event back into `onmessage`
ws.onmessage({ data: messageString });
}).catch(function(error){
}).catch(function (error) {
console.error(`Failed to parse binary blob from websocket, reason: ${error}`);
});
}
ws.onmessage = function(event) {
ws.onmessage = function (event) {
// Check if websocket message is binary, if so, stringify it.
if(event.data && event.data instanceof Blob) {
if (event.data && event.data instanceof Blob) {
ws.onmessagebinary(event);
return;
}
@@ -2738,7 +2735,7 @@ function connect() {
onWebRtcAnswer(msg);
} else if (msg.type === 'iceCandidate') {
onWebRtcIce(msg.candidate);
} else if(msg.type === 'warning' && msg.warning) {
} else if (msg.type === 'warning' && msg.warning) {
console.warn(msg.warning);
} else if (msg.type === 'peerDataChannels') {
onWebRtcSFUPeerDatachannels(msg);
@@ -2747,27 +2744,25 @@ function connect() {
}
};
ws.onerror = function(event) {
ws.onerror = function (event) {
console.log(`WS error: ${JSON.stringify(event)}`);
};
ws.onclose = function(event) {
ws.onclose = function (event) {
closeStream();
if(ws.attemptStreamReconnection === true){
if (ws.attemptStreamReconnection === true) {
console.log(`WS closed: ${JSON.stringify(event.code)} - ${event.reason}`);
if(event.reason !== "")
{
if (event.reason !== "") {
showTextOverlay(`DISCONNECTED: ${event.reason.toUpperCase()}`);
}
else
{
else {
showTextOverlay(`DISCONNECTED`);
}
let reclickToStart = setTimeout(function(){
let reclickToStart = setTimeout(function () {
start(true)
}, 4000);
}
@@ -2815,7 +2810,7 @@ function clearMouseEvents(playerElement) {
function toggleControlScheme() {
let schemeToggle = document.getElementById("control-scheme-text");
switch (inputOptions.controlScheme) {
case ControlSchemeType.HoveringMouse:
inputOptions.controlScheme = ControlSchemeType.LockedMouse;
@@ -2833,8 +2828,7 @@ function toggleControlScheme() {
}
console.log(`Updating control scheme to: ${inputOptions.controlScheme ? "Hovering Mouse" : "Locked Mouse"}`)
if(webRtcPlayerObj && webRtcPlayerObj.video)
{
if (webRtcPlayerObj && webRtcPlayerObj.video) {
registerMouse(webRtcPlayerObj.video);
}
}
@@ -2847,14 +2841,14 @@ function toggleBrowserCursorVisibility() {
}
function restartStream() {
if(!ws){
if (!ws) {
return;
}
ws.attemptStreamReconnection = false;
let existingOnClose = ws.onclose;
ws.onclose = function(event) {
ws.onclose = function (event) {
existingOnClose(event);
// this is how we restart
connect_on_load = true;
@@ -2870,7 +2864,7 @@ function closeStream() {
if (webRtcPlayerObj) {
// Remove video element from the page.
let playerDiv = document.getElementById('player');
if(playerDiv){
if (playerDiv) {
playerDiv.removeChild(webRtcPlayerObj.video);
}
let outer = document.getElementById("outer");
@@ -2895,6 +2889,6 @@ function load() {
setupFreezeFrameOverlay();
registerKeyboardEvents();
// Example response event listener that logs to console
addResponseEventListener('logListener', (response) => {console.log(`Received response message from streamer: "${response}"`)})
addResponseEventListener('logListener', (response) => { console.log(`Received response message from streamer: "${response}"`) })
start(false);
}
-123
View File
@@ -1,123 +0,0 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
logs/
node_modules/
**/platform_scripts/cmd/*/
**/platform_scripts/bash/*/
node.zip
-3
View File
@@ -1,3 +0,0 @@
# pixel-streaming-webrtc
pixel streaming webrtc server
@@ -1,5 +0,0 @@
logs/
node_modules/
**/platform_scripts/cmd/*/
**/platform_scripts/bash/*/
node.zip
@@ -1,7 +0,0 @@
Copyright 2004-2022, Epic Games, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,6 +0,0 @@
{
"HttpPort": 90,
"UseHTTPS": false,
"MatchmakerPort": 9999,
"LogToFile": true
}
@@ -1,295 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
var enableRedirectionLinks = true;
var enableRESTAPI = true;
const defaultConfig = {
// The port clients connect to the matchmaking service over HTTP
HttpPort: 80,
UseHTTPS: false,
// The matchmaking port the signaling service connects to the matchmaker
MatchmakerPort: 9999,
// Log to file
LogToFile: true
};
// Similar to the Signaling Server (SS) code, load in a config.json file for the MM parameters
const argv = require('yargs').argv;
var configFile = (typeof argv.configFile != 'undefined') ? argv.configFile.toString() : 'config.json';
console.log(`configFile ${configFile}`);
const config = require('./modules/config.js').init(configFile, defaultConfig);
console.log("Config: " + JSON.stringify(config, null, '\t'));
const express = require('express');
var cors = require('cors');
const app = express();
const http = require('http').Server(app);
const fs = require('fs');
const path = require('path');
const logging = require('./modules/logging.js');
logging.RegisterConsoleLogger();
if (config.LogToFile) {
logging.RegisterFileLogger('./logs');
}
// A list of all the Cirrus server which are connected to the Matchmaker.
var cirrusServers = new Map();
//
// Parse command line.
//
if (typeof argv.HttpPort != 'undefined') {
config.HttpPort = argv.HttpPort;
}
if (typeof argv.MatchmakerPort != 'undefined') {
config.MatchmakerPort = argv.MatchmakerPort;
}
http.listen(config.HttpPort, () => {
console.log('HTTP listening on *:' + config.HttpPort);
});
if (config.UseHTTPS) {
//HTTPS certificate details
const options = {
key: fs.readFileSync(path.join(__dirname, './certificates/client-key.pem')),
cert: fs.readFileSync(path.join(__dirname, './certificates/client-cert.pem'))
};
var https = require('https').Server(options, app);
//Setup http -> https redirect
console.log('Redirecting http->https');
app.use(function (req, res, next) {
if (!req.secure) {
if (req.get('Host')) {
var hostAddressParts = req.get('Host').split(':');
var hostAddress = hostAddressParts[0];
if (httpsPort != 443) {
hostAddress = `${hostAddress}:${httpsPort}`;
}
return res.redirect(['https://', hostAddress, req.originalUrl].join(''));
} else {
console.error(`unable to get host name from header. Requestor ${req.ip}, url path: '${req.originalUrl}', available headers ${JSON.stringify(req.headers)}`);
return res.status(400).send('Bad Request');
}
}
next();
});
https.listen(443, function () {
console.log('Https listening on 443');
});
}
// No servers are available so send some simple JavaScript to the client to make
// it retry after a short period of time.
function sendRetryResponse(res) {
res.send(`All ${cirrusServers.size} Cirrus servers are in use. Retrying in <span id="countdown">3</span> seconds.
<script>
var countdown = document.getElementById("countdown").textContent;
setInterval(function() {
countdown--;
if (countdown == 0) {
window.location.reload(1);
} else {
document.getElementById("countdown").textContent = countdown;
}
}, 1000);
</script>`);
}
// Get a Cirrus server if there is one available which has no clients connected.
function getAvailableCirrusServer() {
for (cirrusServer of cirrusServers.values()) {
if (cirrusServer.numConnectedClients === 0 && cirrusServer.ready === true) {
// Check if we had at least 10 seconds since the last redirect, avoiding the
// chance of redirecting 2+ users to the same SS before they click Play.
// In other words, give the user 10 seconds to click play button the claim the server.
if( cirrusServer.hasOwnProperty('lastRedirect')) {
if( ((Date.now() - cirrusServer.lastRedirect) / 1000) < 10 )
continue;
}
cirrusServer.lastRedirect = Date.now();
return cirrusServer;
}
}
console.log('WARNING: No empty Cirrus servers are available');
return undefined;
}
if(enableRESTAPI) {
// Handle REST signalling server only request.
app.options('/signallingserver', cors())
app.get('/signallingserver', cors(), (req, res) => {
cirrusServer = getAvailableCirrusServer();
if (cirrusServer != undefined) {
res.json({ signallingServer: `${cirrusServer.address}:${cirrusServer.port}`});
console.log(`Returning ${cirrusServer.address}:${cirrusServer.port}`);
} else {
res.json({ signallingServer: '', error: 'No signalling servers available'});
}
});
}
if(enableRedirectionLinks) {
// Handle standard URL.
app.get('/', (req, res) => {
cirrusServer = getAvailableCirrusServer();
if (cirrusServer != undefined) {
res.redirect(`http://${cirrusServer.address}:${cirrusServer.port}/`);
//console.log(req);
console.log(`Redirect to ${cirrusServer.address}:${cirrusServer.port}`);
} else {
sendRetryResponse(res);
}
});
// Handle URL with custom HTML.
app.get('/custom_html/:htmlFilename', (req, res) => {
cirrusServer = getAvailableCirrusServer();
if (cirrusServer != undefined) {
res.redirect(`http://${cirrusServer.address}:${cirrusServer.port}/custom_html/${req.params.htmlFilename}`);
console.log(`Redirect to ${cirrusServer.address}:${cirrusServer.port}`);
} else {
sendRetryResponse(res);
}
});
}
//
// Connection to Cirrus.
//
const net = require('net');
function disconnect(connection) {
console.log(`Ending connection to remote address ${connection.remoteAddress}`);
connection.end();
}
const matchmaker = net.createServer((connection) => {
connection.on('data', (data) => {
try {
message = JSON.parse(data);
if(message)
console.log(`Message TYPE: ${message.type}`);
} catch(e) {
console.log(`ERROR (${e.toString()}): Failed to parse Cirrus information from data: ${data.toString()}`);
disconnect(connection);
return;
}
if (message.type === 'connect') {
// A Cirrus server connects to this Matchmaker server.
cirrusServer = {
address: message.address,
port: message.port,
numConnectedClients: 0,
lastPingReceived: Date.now()
};
cirrusServer.ready = message.ready === true;
// Handles disconnects between MM and SS to not add dupes with numConnectedClients = 0 and redirect users to same SS
// Check if player is connected and doing a reconnect. message.playerConnected is a new variable sent from the SS to
// help track whether or not a player is already connected when a 'connect' message is sent (i.e., reconnect).
if(message.playerConnected == true) {
cirrusServer.numConnectedClients = 1;
}
// Find if we already have a ciruss server address connected to (possibly a reconnect happening)
let server = [...cirrusServers.entries()].find(([key, val]) => val.address === cirrusServer.address && val.port === cirrusServer.port);
// if a duplicate server with the same address isn't found -- add it to the map as an available server to send users to.
if (!server || server.size <= 0) {
console.log(`Adding connection for ${cirrusServer.address.split(".")[0]} with playerConnected: ${message.playerConnected}`)
cirrusServers.set(connection, cirrusServer);
} else {
console.log(`RECONNECT: cirrus server address ${cirrusServer.address.split(".")[0]} already found--replacing. playerConnected: ${message.playerConnected}`)
var foundServer = cirrusServers.get(server[0]);
// Make sure to retain the numConnectedClients from the last one before the reconnect to MM
if (foundServer) {
cirrusServers.set(connection, cirrusServer);
console.log(`Replacing server with original with numConn: ${cirrusServer.numConnectedClients}`);
cirrusServers.delete(server[0]);
} else {
cirrusServers.set(connection, cirrusServer);
console.log("Connection not found in Map() -- adding a new one");
}
}
} else if (message.type === 'streamerConnected') {
// The stream connects to a Cirrus server and so is ready to be used
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServer.ready = true;
console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} ready for use`);
} else {
disconnect(connection);
}
} else if (message.type === 'streamerDisconnected') {
// The stream connects to a Cirrus server and so is ready to be used
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServer.ready = false;
console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} no longer ready for use`);
} else {
disconnect(connection);
}
} else if (message.type === 'clientConnected') {
// A client connects to a Cirrus server.
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServer.numConnectedClients++;
console.log(`Client connected to Cirrus server ${cirrusServer.address}:${cirrusServer.port}`);
} else {
disconnect(connection);
}
} else if (message.type === 'clientDisconnected') {
// A client disconnects from a Cirrus server.
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServer.numConnectedClients--;
console.log(`Client disconnected from Cirrus server ${cirrusServer.address}:${cirrusServer.port}`);
if(cirrusServer.numConnectedClients === 0) {
// this make this server immediately available for a new client
cirrusServer.lastRedirect = 0;
}
} else {
disconnect(connection);
}
} else if (message.type === 'ping') {
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServer.lastPingReceived = Date.now();
} else {
disconnect(connection);
}
} else {
console.log('ERROR: Unknown data: ' + JSON.stringify(message));
disconnect(connection);
}
});
// A Cirrus server disconnects from this Matchmaker server.
connection.on('error', () => {
cirrusServer = cirrusServers.get(connection);
if(cirrusServer) {
cirrusServers.delete(connection);
console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} disconnected from Matchmaker`);
} else {
console.log(`Disconnected machine that wasn't a registered cirrus server, remote address: ${connection.remoteAddress}`);
}
});
});
matchmaker.listen(config.MatchmakerPort, () => {
console.log('Matchmaker listening on *:' + config.MatchmakerPort);
});
@@ -1,49 +0,0 @@
// 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}
// 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) {
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
}
@@ -1,108 +0,0 @@
// 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 = './';
if (!fs.existsSync(path))
fs.mkdirSync(path);
var output = fs.createWriteStream(`./logs/${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
}
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
{
"name": "cirrus-matchmaker",
"version": "0.0.1",
"description": "Cirrus servers connect to the Matchmaker which redirects a browser to the next available Cirrus server",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.16.2",
"socket.io": "4.4.1",
"yargs": "17.3.1"
}
}
@@ -1,57 +0,0 @@
#!/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
"
exit 1
}
function use_args() {
while(($#)) ; do
case "$1" in
--debug ) IS_DEBUG=1; shift;;
--nosudo ) NO_SUDO=1; shift;;
--verbose ) VERBOSE=1; shift;;
--help ) print_usage;;
* ) echo "Unknown command"; 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'
}
@@ -1,25 +0,0 @@
#!/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 "$@"
call_setup_sh
process="${BASH_LOCATION}/node/bin/node matchmaker.js"
pushd ../.. > /dev/null
echo ""
echo "Starting Matchmaker use ctrl-c to exit"
echo "-----------------------------------------"
echo ""
start_process $process
popd > /dev/null # ../..
popd > /dev/null # BASH_SOURCE
@@ -1,114 +0,0 @@
#!/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"
exit 1
fi
fi
}
echo "Checking Matchmaker dependencies..."
# navigate to Matchmaker 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 # Matchmaker
popd > /dev/null # BASH_SOURCE
echo "All Matchmaker dependencies up to date."
@@ -1,25 +0,0 @@
@Rem Copyright Epic Games, Inc. All Rights Reserved.
@echo off
@Rem Set script directory as working directory.
pushd "%~dp0"
title Matchmaker
@Rem Run setup to ensure we have node and matchmaker installed.
call setup.bat
@Rem Move to matchmaker.js directory.
pushd ..\..
@Rem Run node server and pass any argument along.
platform_scripts\cmd\node\node.exe matchmaker %*
@Rem Pop matchmaker.js directory.
popd
@Rem Pop script directory.
popd
pause
@@ -1,17 +0,0 @@
@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 Move to matchmaker.js directory and install its package.json
pushd %~dp0\..\..\
call platform_scripts\cmd\node\npm install --no-save
popd
@Rem Pop working directory
popd
@@ -1,35 +0,0 @@
@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
@@ -1,45 +0,0 @@
# The new home for the Pixel Streaming servers!
The Pixel Streaming servers and web frontend that was in `Samples/PixelStreaming/WebServers` is now here.
## Goals
The goals of this repository are to:
- Increase the release cadence for the Pixel Streaming servers (to mitigate browser breaking changes sooner).
- Encourage easier contribution of these components by Unreal Engine licensees.
- Facilitate a more standard web release mechanism (e.g. NPM packages or similar... coming soon).
- Grant a permissive license to distribute and modify this code wherever you see fit (MIT licensed).
## Repository contents
Reference implementations for the various pieces needed to support a PixelStreaming application:
- SignallingWebServer (Cirrus)
- SFU (Selective Forwarding Unit)
- Matchmaker
## Container images
The following container images are built from this repository:
- [ghcr.io/epicgames/pixel-streaming-signalling-server](https://github.com/orgs/EpicGames/packages/container/package/pixel-streaming-signalling-server) (since Unreal Engine 5.1)
## Versions
We maintain versions of the servers and frontend that are compatible with existing and in-development version of Unreal Engine.
:warning: **There are breaking changes between UE versions - so make sure you get the right version**. :warning:
We maintain the following in branches right now:
[Master](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/master) (This is our dev branch.)
[UE5.1](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.1)
[UE5.0](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.0)
[UE4.27](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.27)
[UE4.26](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.26)
## Legal
© 2004-2022, Epic Games, Inc. Unreal and its logo are Epics trademarks or registered trademarks in the US and elsewhere.
@@ -1 +0,0 @@
node_modules
@@ -1,108 +0,0 @@
// Parse passed arguments
let passedPublicIP = null;
for(let arg of process.argv){
if(arg && arg.startsWith("--PublicIP=")){
let splitArr = arg.split("=");
if(splitArr.length == 2){
passedPublicIP = splitArr[1];
console.log("--PublicIP=" + passedPublicIP);
}
}
}
const config = {
signallingURL: "ws://localhost:8889",
mediasoup: {
worker: {
rtcMinPort: 40000,
rtcMaxPort: 49999,
logLevel: "debug",
logTags: [
"info",
"ice",
"dtls",
"rtp",
"srtp",
"rtcp",
"sctp"
// 'rtx',
// 'bwe',
// 'score',
// 'simulcast',
// 'svc'
],
},
router: {
mediaCodecs: [
{
kind: "audio",
mimeType: "audio/opus",
clockRate: 48000,
channels: 2,
},
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {
"packetization-mode": 1,
"profile-level-id": "42e01f",
"level-asymmetry-allowed": 1
}
},
{
kind: "video",
mimeType: "video/h264",
clockRate: 90000,
parameters: {
"packetization-mode": 1,
"profile-level-id": "42e01f",
"level-asymmetry-allowed": 1
},
},
],
},
// here you must specify ip addresses to listen on
// some browsers have issues with connecting to ICE on
// localhost so you might have to specify a proper
// private or public ip here.
webRtcTransport: {
listenIps: passedPublicIP != null ? [{ ip: "0.0.0.0", announcedIp: passedPublicIP}] : getLocalListenIps(),
// 100 megabits
initialAvailableOutgoingBitrate: 100_000_000,
},
},
}
function getLocalListenIps() {
const listenIps = []
if (typeof window === 'undefined') {
const os = require('os')
const networkInterfaces = os.networkInterfaces()
const ips = []
if (networkInterfaces) {
for (const [key, addresses] of Object.entries(networkInterfaces)) {
addresses.forEach(address => {
if (address.family === 'IPv4') {
listenIps.push({ ip: address.address, announcedIp: null })
}
/* ignore link-local and other special ipv6 addresses.
* https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.xhtml
*/
else if (address.family === 'IPv6' && address.address[0] !== 'f') {
listenIps.push({ ip: address.address, announcedIp: null })
}
})
}
}
}
if (listenIps.length === 0) {
listenIps.push({ ip: '127.0.0.1', announcedIp: null })
}
return listenIps
}
module.exports = config;
@@ -1,15 +0,0 @@
ISC License
Copyright © 2020, Iñaki Baz Castillo <ibc@aliax.net>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -1,182 +0,0 @@
# mediasoup-sdp-bridge v3
[![][npm-shield-mediasoup-sdp-bridge]][npm-mediasoup-sdp-bridge]
[![][travis-ci-shield-mediasoup-sdp-bridge]][travis-ci-mediasoup-sdp-bridge]
Node.js library to allow integration of SDP based clients with [mediasoup][mediasoup-website].
## Website and Documentation
* [mediasoup.org][mediasoup-website]
## Support Forum
* [mediasoup.discourse.group][mediasoup-discourse]
## Use-Case Design Proposal
This section contains a use-case that can serve as usage example to guide design of the internal implementation.
Within the Node.js server app running mediasoup:
### mediasoup receiving media from a remote SDP endpoint
```typescript
import * as SdpBridge from "mediasoup-sdp-bridge";
import Signaling from "./my-signaling"; // Our own signaling stuff.
const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport.
// Create an SdpEndpoint to handle SDP negotiation with the remote endpoint.
const sdpEndpoint = await SdpBridge.createSdpEndpoint({
transport: transport,
});
// Upon receipt of an SDP Offer from the remote endpoint, apply it.
Signaling.on("sdp-offer", async (sdpOffer: string) => {
// For each media section in the SDP Offer, SdpEndpoint creates a new Producer
// on top of the Transport that was provided.
const producers: Producer[] = await sdpEndpoint.processOffer(sdpOffer);
// Generate an SDP Answer and reply to the remote endpoint with it.
const sdpAnswer: string = sdpEndpoint.createAnswer();
await Signaling.sendAnswer(sdpAnswer);
});
```
### mediasoup sending media to a remote SDP endpoint
```typescript
import * as SdpBridge from "mediasoup-sdp-bridge";
import Signaling from "./my-signaling"; // Our own signaling stuff.
const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport.
// Create an SdpEndpoint to send media to the remote endpoint.
const sdpEndpoint = await createSdpEndpoint({
transport: transport,
});
// Listen for the "negotiationneeded" event, to send an SDP Offer to the remote
// endpoint. This event is emitted when transport.consume() is called, or when
// a Producer being consumed is closed or paused/resumed.
sdpEndpoint.on("negotiationneeded", () => {
// For each Consumer present in the Transport that was provided,
// SdpEndpoint creates a new media section in the SDP Offer.
const sdpOffer: string = sdpEndpoint.createOffer();
// Send the SDP Offer to the remote endpoint.
await Signaling.sendOffer(sdpOffer);
});
// Upon receipt of an SDP Answer from the remote endpoint, apply it.
Signaling.on("sdp-answer", async (sdpAnswer: string) => {
await sdpEndpoint.processAnswer(sdpAnswer);
});
// Generate remote endpoint's RTP capabilities based on a remote SDP or based
// on handmade capabilities.
const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities(
router.rtpCapabilities,
remoteSdp
);
// or:
const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities(
router.rtpCapabilities,
handmadeRtpCapabilities
);
// If there were mediasoup Producers already created in the Router, or if a new
// one is created, and we want to consume them in the remote endpoint, tell the
// Transport to consume them. transport.consume() method will trigger the
// "negotiationneeded" event, handled above.
//
// NOTE: By calling consume() method in parallel (without waiting for the
// previous one to complete) we ensure that the "negotiationneeded" event will
// just be emitted once upon completion of all consume() calls, so a single
// SDP Offer/Answer roundtrip will be needed.
transport
.consume({
producerId: producer1.id,
rtpCapabilities: endpointRtpCapabilities,
})
.catch((error) => console.error("transport.consume() failed:", error));
transport
.consume({
producerId: producer2.id,
rtpCapabilities: endpointRtpCapabilities,
})
.catch((error) => console.error("transport.consume() failed:", error));
```
## Implementation Notes
### Design limitations
The initial Use-Case Design Proposal lacks an important detail: it uses an `endpointRtpCapabilities` object, which represents the WebRTC and RTP capabilities of the remote endpoint that will receive media from mediasoup. This *RtpCapabilities* object is assumed to be written either by hand, or obtained from a previous SDP message that somehow might have been obtained from the remote endpoint. It is only *after* having these *RtpCapabilities*, that the SDP Offer/Answer process starts.
All this, however, goes backwards with the normal flow of the SDP Offer/Answer model. **The remote capabilities should be obtained from the SDP Offer/Answer exchange itself**, not as an unspecified out-of-band mechanism. In theory, how the mediasoup application learns about remote capabilities should come from one of these sources:
1. An SDP Offer (with *recvonly* or *sendrecv* direction) from a remote endpoint that wants to receive media.
2. An SDP Answer (with *recvonly* or *sendrecv* direction) from a remote endpoint, in response to an SDP Offer (with *sendonly* or *sendrecv* direction) that the application had previously sent.
However, in practice both of these options conflict with the current design proposal:
* (1) is not being considered for now. mediasoup is designed around the assumption that the participant sending media should always be the one starting the connection; thus, the endpoint that will send media is also the one sending the SDP Offer.
* (2) is the ideal but *it's not possible* with the current design, because the remote capabilities must be already known by the time `sdpEndpoint.createOffer()` is called.
(Note: Additionally, the *sendrecv* direction is also not considered for now. Both the local application or the remote endpoints are be assumed to be either *sendonly* or *recvonly*.)
### Current implementation
The implementation found in this repo is enough to cover basic usage, but is not complete by any means. Also, it tries to work around the limitations described above, using alternatives that are far from ideal.
Some notes:
* Receiving media is the part that works best. It suffices to call `SdpEndpoint.processOffer()`, and this will return one Producer for each media section found in the SDP Offer.
* Sending media, on the other hand, suffers from the limitation described in (2) above. To work around this, the class `BrowserRtpCapabilities` contains predefined capabilities objects for some of the most common web browsers. These can be used by the application to provide `transport.consume()` with something to work with.
The obvious drawback to this solution is that the objects in `BrowserRtpCapabilities` must be kept up to date from time to time, in order to accurately represent the actual capabilities of web browsers. To help with this task, there is a handy tool that can be found in the [tools/browser-rtpcapabilities](./tools/browser-rtpcapabilities/) subdirectory.
* SDP renegotiation is not implemented. The local endpoint cannot make an SDP Re-Offer when the state of the Producers or Consumers changes.
* There are minor improvements to be done in the implementation.
- Right now the SdpEndpoint class exports an `SdpEndpoint.addConsumer()` method, which the application uses to provide all Consumers that are created from the corresponding Transport. However, chances are that this is unnecessary: the Transport class already provides an observer event `Transport.observer.on("newconsumer")`, which could be used by the SdpEndpoint to be notified of all new Consumers in its Transport, saving the application the need to provide them explicitly with `addConsumer()`.
- Some unexpected errors are not handled gracefully, and instead a "BUG" error is logged before the application is forced to exit. These should probably be replaced by a `throw new Error(...)`.
- Some debug messages are simply commented out to avoid causing too much noise. A proper logging library should be used to allow setting different levels and hiding the less interesting ones.
- The code hasn't been linted. The default linter rules were left as per the initial commit, but haven't been used yet. For now, the code has just been formatted with the default rules from *Prettier.js*.
## Contributors
* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)]
* Juan Navarro [[github](https://github.com/j1elo)]
## License
[ISC](./LICENSE)
[mediasoup-website]: https://mediasoup.org
[mediasoup-discourse]: https://mediasoup.discourse.group
[npm-shield-mediasoup-sdp-bridge]: https://img.shields.io/npm/v/mediasoup-sdp-bridge.svg
[npm-mediasoup-sdp-bridge]: https://npmjs.org/package/mediasoup-sdp-bridge
[travis-ci-shield-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge.svg?branch=master
[travis-ci-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge
@@ -1,89 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sdpToSendRtpParameters = exports.sdpToRecvRtpCapabilities = void 0;
const MsRtpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/unifiedPlanUtils"));
const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils"));
const MsOrtc = __importStar(require("mediasoup-client/lib/ortc"));
require("util").inspect.defaultOptions.depth = null;
function sdpToRecvRtpCapabilities(sdpObject, localCaps) {
const caps = MsSdpUtils.extractRtpCapabilities({
sdpObject,
});
try {
MsOrtc.validateRtpCapabilities(caps);
}
catch (err) {
console.error("FIXME BUG:", err);
process.exit(1);
}
const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps);
const recvCaps = MsOrtc.getRecvRtpCapabilities(extendedCaps);
{
}
return recvCaps;
}
exports.sdpToRecvRtpCapabilities = sdpToRecvRtpCapabilities;
function sdpToSendRtpParameters(sdpObject, sdpMediaObj, localCaps, kind) {
var _a;
const caps = MsSdpUtils.extractRtpCapabilities({
sdpObject,
});
try {
MsOrtc.validateRtpCapabilities(caps);
}
catch (err) {
console.error("FIXME BUG:", err);
process.exit(1);
}
const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps);
const sendParams = MsOrtc.getSendingRemoteRtpParameters(kind, extendedCaps);
// const sdpMediaObj = (sdpObject.media || []).find((m) => m.type === kind) ||
// {};
if ("mid" in sdpMediaObj) {
sendParams.mid = String(sdpMediaObj.mid);
}
else {
sendParams.mid = kind === "audio" ? "0" : "1";
}
if ("rids" in sdpMediaObj) {
for (const mediaRid of sdpMediaObj.rids) {
(_a = sendParams.encodings) === null || _a === void 0 ? void 0 : _a.push({ rid: mediaRid.id });
}
}
else {
// sendParams.encodings = MsRtpUtils.getRtpEncodings({
// sdpObject,
// kind,
// });
sendParams.encodings = MsRtpUtils.getRtpEncodings({ offerMediaObject: sdpMediaObj });
}
sendParams.rtcp = {
cname: MsSdpUtils.getCname({ offerMediaObject: sdpMediaObj }),
reducedSize: "rtcpRsize" in sdpMediaObj && sdpMediaObj.rtcpRsize,
mux: "rtcpMux" in sdpMediaObj && sdpMediaObj.rtcpMux,
};
{
}
return sendParams;
}
exports.sdpToSendRtpParameters = sdpToSendRtpParameters;
//# sourceMappingURL=SdpUtils.js.map
@@ -1,198 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateRtpCapabilities2 = exports.generateRtpCapabilities1 = exports.generateRtpCapabilities0 = exports.createSdpEndpoint = exports.SdpEndpoint = void 0;
const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils"));
const RemoteSdp_1 = require("mediasoup-client/lib/handlers/sdp/RemoteSdp");
const SdpTransform = __importStar(require("sdp-transform"));
const uuid_1 = require("uuid");
const BrowserRtpCapabilities = __importStar(require("./BrowserRtpCapabilities"));
const SdpUtils = __importStar(require("./SdpUtils"));
const MediaSection_1 = require("mediasoup-client/lib/handlers/sdp/MediaSection");
require("util").inspect.defaultOptions.depth = null;
class SdpEndpoint {
constructor(webRtcTransport, localCaps) {
this.producers = [];
this.producerMedias = [];
this.consumers = [];
this.webRtcTransport = webRtcTransport;
this.transport = webRtcTransport;
this.localCaps = localCaps;
this.sctpMedia = null;
this.consumeData = false;
}
async processOffer(sdpOffer) {
if (this.remoteSdp) {
console.error("[SdpEndpoint.processOffer] ERROR: A remote description was already set");
return [];
}
this.remoteSdp = sdpOffer;
const remoteSdpObj = SdpTransform.parse(sdpOffer);
await this.webRtcTransport.connect({
dtlsParameters: MsSdpUtils.extractDtlsParameters({
sdpObject: remoteSdpObj,
}),
});
for (const media of remoteSdpObj.media) {
if (media.type == "application") {
this.sctpMedia = media;
console.log("[SdpEndpoint.processOffer] SCTP association received");
}
else {
if (!("rtp" in media)) {
continue;
}
if (!("direction" in media)) {
continue;
}
if (media.direction !== "sendonly") {
continue;
}
const sendParams = SdpUtils.sdpToSendRtpParameters(remoteSdpObj, media, this.localCaps, media.type);
let producer;
try {
producer = await this.transport.produce({
kind: media.type,
rtpParameters: sendParams,
paused: false,
});
}
catch (err) {
console.error("FIXME BUG:", err);
process.exit(1);
}
this.producers.push(producer);
this.producerMedias.push(media);
console.log("[SdpEndpoint.processOffer] mediasoup Producer created, kind: %s, type: %s, paused: %s", producer.kind, producer.type, producer.paused);
}
}
return this.producers;
}
createAnswer() {
if (this.localSdp) {
console.error("[SdpEndpoint.createAnswer] ERROR: A local description was already set");
return "";
}
const sdpBuilder = new RemoteSdp_1.RemoteSdp({
iceParameters: this.webRtcTransport.iceParameters,
iceCandidates: this.webRtcTransport.iceCandidates,
dtlsParameters: this.webRtcTransport.dtlsParameters,
sctpParameters: this.webRtcTransport.sctpParameters,
planB: false,
});
console.log("[SdpEndpoint.createAnswer] Make 'recvonly' SDP Answer");
for (let i = 0; i < this.producers.length; i++) {
const sdpMediaObj = this.producerMedias[i];
const recvParams = this.producers[i].rtpParameters;
sdpBuilder.send({
offerMediaObject: sdpMediaObj,
reuseMid: undefined,
offerRtpParameters: recvParams,
answerRtpParameters: recvParams,
codecOptions: undefined,
extmapAllowMixed: false,
});
}
if (this.sctpMedia != null) {
sdpBuilder.sendSctpAssociation({offerMediaObject: this.sctpMedia});
}
this.localSdp = sdpBuilder.getSdp();
return this.localSdp;
}
addConsumer(consumer) {
this.consumers.push(consumer);
}
addConsumeData() {
this.consumeData = true;
}
createOffer() {
var _a;
if (this.localSdp) {
console.error("[SdpEndpoint.createOffer] ERROR: A local description was already set");
return "";
}
const sdpBuilder = new RemoteSdp_1.RemoteSdp({
iceParameters: this.webRtcTransport.iceParameters,
iceCandidates: this.webRtcTransport.iceCandidates,
dtlsParameters: this.webRtcTransport.dtlsParameters,
sctpParameters: this.webRtcTransport.sctpParameters,
planB: false,
});
const sendMsid = uuid_1.v4().substr(0, 8);
console.log("[SdpEndpoint.createOffer] Make 'sendonly' SDP Offer");
for (let i = 0; i < this.consumers.length; i++) {
const mid = (_a = this.consumers[i].rtpParameters.mid) !== null && _a !== void 0 ? _a : "nomid";
const kind = this.consumers[i].kind;
const sendParams = this.consumers[i].rtpParameters;
sdpBuilder.receive({
mid,
kind,
offerRtpParameters: sendParams,
streamId: sendMsid,
trackId: `${sendMsid}-${kind}`,
});
}
if (this.consumeData) {
sdpBuilder.receiveSctpAssociation();
}
this.localSdp = sdpBuilder.getSdp();
return this.localSdp;
}
async processAnswer(sdpAnswer) {
if (this.remoteSdp) {
console.error("[SdpEndpoint.processAnswer] ERROR: A remote description was already set");
return;
}
this.remoteSdp = sdpAnswer;
const remoteSdpObj = SdpTransform.parse(sdpAnswer);
await this.webRtcTransport.connect({
dtlsParameters: MsSdpUtils.extractDtlsParameters({
sdpObject: remoteSdpObj,
}),
});
{
}
}
}
exports.SdpEndpoint = SdpEndpoint;
function createSdpEndpoint(webRtcTransport, localCaps) {
return new SdpEndpoint(webRtcTransport, localCaps);
}
exports.createSdpEndpoint = createSdpEndpoint;
function generateRtpCapabilities0() {
return BrowserRtpCapabilities.chrome;
}
exports.generateRtpCapabilities0 = generateRtpCapabilities0;
function generateRtpCapabilities1(localCaps, remoteSdp) {
console.error("[SdpEndpoint.generateRtpCapabilities1] BUG: Not implemented");
process.exit(1);
let caps;
return caps;
}
exports.generateRtpCapabilities1 = generateRtpCapabilities1;
function generateRtpCapabilities2(localCaps, remoteCaps) {
console.error("[SdpEndpoint.generateRtpCapabilities2] BUG: Not implemented");
process.exit(1);
let caps;
return caps;
}
exports.generateRtpCapabilities2 = generateRtpCapabilities2;
//# sourceMappingURL=index.js.map
@@ -1,27 +0,0 @@
{
"name": "mediasoup-sdp-bridge",
"version": "3.6.5",
"description": "Node.js library to allow integration of SDP based clients with mediasoup",
"contributors": [
"Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
"Juan Navarro <juan.navarro@gmx.es> (https://github.com/j1elo)"
],
"homepage": "https://mediasoup.org",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/versatica/mediasoup-sdp-bridge.git"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mediasoup"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"engines": {
"node": ">=10"
},
"dependencies": {
"mediasoup-client": "^3.6.41"
}
}
-371
View File
@@ -1,371 +0,0 @@
{
"name": "pixelstreaming-sfu",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pixelstreaming-sfu",
"version": "1.0.0",
"dependencies": {
"mediasoup_prebuilt": "^3.8.4",
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
"ws": "^7.1.2"
}
},
"mediasoup-sdp-bridge": {
"version": "3.6.5",
"license": "ISC",
"dependencies": {
"mediasoup-client": "^3.6.41"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mediasoup"
}
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"node_modules/@types/node": {
"version": "16.11.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
"integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA=="
},
"node_modules/awaitqueue": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz",
"integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fake-mediastreamtrack": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz",
"integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==",
"dependencies": {
"event-target-shim": "^5.0.1",
"uuid": "^8.1.0"
}
},
"node_modules/h264-profile-level-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz",
"integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==",
"dependencies": {
"debug": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/mediasoup_prebuilt": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz",
"integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==",
"dependencies": {
"@types/node": "^16.9.1",
"awaitqueue": "^2.3.3",
"debug": "^4.3.2",
"h264-profile-level-id": "^1.0.1",
"netstring": "^0.3.0",
"random-number": "^0.0.9",
"supports-color": "^9.0.2",
"uuid": "^8.3.2"
}
},
"node_modules/mediasoup-client": {
"version": "3.6.46",
"resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz",
"integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==",
"dependencies": {
"@types/debug": "^4.1.7",
"@types/events": "^3.0.0",
"awaitqueue": "^2.3.3",
"bowser": "^2.11.0",
"debug": "^4.3.2",
"events": "^3.3.0",
"fake-mediastreamtrack": "^1.1.6",
"h264-profile-level-id": "^1.0.1",
"sdp-transform": "^2.14.1",
"supports-color": "^9.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mediasoup"
}
},
"node_modules/mediasoup-sdp-bridge": {
"resolved": "mediasoup-sdp-bridge",
"link": true
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/netstring": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz",
"integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI=",
"engines": {
"node": ">=0.6"
}
},
"node_modules/random-number": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz",
"integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw=="
},
"node_modules/sdp-transform": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/supports-color": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz",
"integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/ws": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
"requires": {
"@types/ms": "*"
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"@types/node": {
"version": "16.11.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
"integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA=="
},
"awaitqueue": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz",
"integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw=="
},
"bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"fake-mediastreamtrack": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz",
"integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==",
"requires": {
"event-target-shim": "^5.0.1",
"uuid": "^8.1.0"
}
},
"h264-profile-level-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz",
"integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==",
"requires": {
"debug": "^4.1.1"
}
},
"mediasoup_prebuilt": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz",
"integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==",
"requires": {
"@types/node": "^16.9.1",
"awaitqueue": "^2.3.3",
"debug": "^4.3.2",
"h264-profile-level-id": "^1.0.1",
"netstring": "^0.3.0",
"random-number": "^0.0.9",
"supports-color": "^9.0.2",
"uuid": "^8.3.2"
}
},
"mediasoup-client": {
"version": "3.6.46",
"resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz",
"integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==",
"requires": {
"@types/debug": "^4.1.7",
"@types/events": "^3.0.0",
"awaitqueue": "^2.3.3",
"bowser": "^2.11.0",
"debug": "^4.3.2",
"events": "^3.3.0",
"fake-mediastreamtrack": "^1.1.6",
"h264-profile-level-id": "^1.0.1",
"sdp-transform": "^2.14.1",
"supports-color": "^9.1.0"
}
},
"mediasoup-sdp-bridge": {
"version": "file:mediasoup-sdp-bridge",
"requires": {
"mediasoup-client": "^3.6.41"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"netstring": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz",
"integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI="
},
"random-number": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz",
"integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw=="
},
"sdp-transform": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw=="
},
"supports-color": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz",
"integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"ws": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
"requires": {}
}
}
}
@@ -1,23 +0,0 @@
{
"name": "pixelstreaming-sfu",
"version": "1.0.0",
"description": "Reference implementation for a PixelStreaming SFU",
"scripts": {
"start-local": "run-script-os --",
"start-local:windows": ".\\platform_scripts\\cmd\\run.bat",
"start-local:default": "./platform_scripts/bash/run_local.sh",
"start-cloud": "run-script-os --",
"start-cloud:windows": ".\\platform_scripts\\cmd\\run_cloud.bat",
"start-cloud:default": "./platform_scripts/bash/run_cloud.sh",
"start": "run-script-os",
"start:windows": "platform_scripts\\cmd\\node\\node.exe sfu_server.js",
"start:default": "if [ `id -u` -eq 0 ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node sfu_server.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node sfu_server.js\"\nfi\n$process "
},
"dependencies": {
"mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge",
"ws": "^7.1.2",
"mediasoup_prebuilt": "^3.8.4",
"run-script-os": "^1.1.6"
}
}
@@ -1,25 +0,0 @@
# Copyright Epic Games, Inc. All Rights Reserved.
FROM node:latest
# Make sure Mediasoup requirements are met
RUN apt -y update
RUN apt -y install python3-pip
# Copy the Selective Forwarding Unit (SFU) to the Docker build context
COPY . /opt/SFU
# Install the dependencies for the mediasoup server
WORKDIR /opt/SFU
RUN npm update
RUN npm install .
# Expose TCP port 80 for player WebSocket connections and web server HTTP access
EXPOSE 40000-49999
# Expose TCP port 8888 for streamer WebSocket connections
EXPOSE 8889
# Set the signalling server as the container's entrypoint
ENTRYPOINT ["/usr/local/bin/node", "/opt/SFU/sfu_server.js"]
@@ -1,80 +0,0 @@
#!/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>] [sfu 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 SFU
Other options: stored and passed to the SFU. All parameters printed once the script values are set.
"
exit 1
}
function print_parameters() {
echo ""
echo "${0} is running with the following parameters:"
echo "--------------------------------------"
echo "Public IP address : ${publicip}"
echo "SFU command line arguments: ${sfucmd}"
echo ""
}
function set_start_default_values() {
# publicip and sfucmd are always needed
publicip=$(curl -s https://api.ipify.org)
if [[ -z $publicip ]]; then
publicip="127.0.0.1"
fi
sfucmd=""
}
function use_args() {
while(($#)) ; do
case "$1" in
--debug ) IS_DEBUG=1; shift;;
--nosudo ) NO_SUDO=1; shift;;
--verbose ) VERBOSE=1; shift;;
--publicip ) publicip="$2"; shift 2;;
--help ) print_usage;;
* ) echo "Unknown command, adding to SFU command line: $1"; sfucmd+=" $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'
}
@@ -1,9 +0,0 @@
#!/bin/bash
# Copyright Epic Games, Inc. All Rights Reserved.
# Build docker image for the Selective Forwarding Unit (SFU)
# When run from SFU/platform_scripts/bash, this uses the SFU directory
# as the build context so the SFU files can be successfully copied into the container image
docker build -t 'mediasoup_sfu:latest' -f ./Dockerfile ../..
@@ -1,8 +0,0 @@
#!/bin/bash
# Copyright Epic Games, Inc. All Rights Reserved.
# Start docker container by name using host networking
docker run --name sfu_latest --network host --rm mediasoup_sfu
# Interactive start example
#docker run --name sfu_latest --network host --rm -it --entrypoint /bin/bash mediasoup_sfu
@@ -1,12 +0,0 @@
#!/bin/bash
# Copyright Epic Games, Inc. All Rights Reserved.
# Stop the docker container
PSID=$(docker ps -a -q --filter="name=sfu_latest")
if [ -z "$PSID" ]; then
echo "Docker SFU is not running, no stopping will be done"
exit 1;
fi
echo "Stopping Mediasoup SFU server ..."
docker stop sfu_latest
@@ -1,27 +0,0 @@
#!/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 # 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="--PublicIP=${publicip}"
# Add arguments passed to script to arguments for executable
arguments+=" ${sfucmd}"
pushd ../.. > /dev/null
echo "Running: $process $arguments"
PATH="${BASH_LOCATION}/node/bin:$PATH"
start_process $process $arguments
popd
popd
@@ -1,27 +0,0 @@
#!/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 # No server specific defaults
use_args "$@"
call_setup_sh
process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --"
pushd ../.. > /dev/null
echo ""
echo "Starting (S)elective (F)orwarding (U)nit use ctrl-c to exit"
echo "-----------------------------------------"
echo ""
PATH="${BASH_LOCATION}/node/bin:$PATH"
start_process $process
popd > /dev/null # ../..
popd > /dev/null # BASH_SOURCE
@@ -1,114 +0,0 @@
#!/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"
exit 1
fi
fi
}
echo "Checking Pixel Streaming SFU dependencies."
# navigate to SFU 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 # SFU
popd > /dev/null # BASH_SOURCE
echo "All Pixel Streaming SFU dependencies up to date."
@@ -1,19 +0,0 @@
@Rem Copyright Epic Games, Inc. All Rights Reserved.
@echo off
@Rem Set script directory as working directory.
pushd "%~dp0"
title SFU
@Rem Get our public IP if we are running this SFU on the cloud we will need this.
FOR /F "tokens=*" %%g IN ('curl -L -S -s https://api.ipify.org') do (SET PUBLICIP=%%g)
@Rem Call out run.bat and pass in the Public IP we grabbed earlier.
call run_local.bat --PublicIP=%PUBLICIP%
@Rem Pop script directory.
popd
pause
@@ -1,25 +0,0 @@
@Rem Copyright Epic Games, Inc. All Rights Reserved.
@echo off
@Rem Set script directory as working directory.
pushd "%~dp0"
title SFU
@Rem Run setup to ensure we have node and mediasoup installed.
call setup.bat
@Rem Move to sfu_server.js directory.
pushd ..\..
@Rem Run node server and pass any argument along.
platform_scripts\cmd\node\node.exe sfu_server %*
@Rem Pop sfu_server directory.
popd
@Rem Pop script directory.
popd
pause
@@ -1,17 +0,0 @@
@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 Move to sfu_server.js directory and install its package.json
pushd %~dp0\..\..\
call platform_scripts\cmd\node\npm install --no-save
popd
@Rem Pop working directory
popd
@@ -1,35 +0,0 @@
@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
@@ -1,321 +0,0 @@
const config = require('./config');
const WebSocket = require('ws');
const mediasoup = require('mediasoup_prebuilt');
const mediasoupSdp = require('mediasoup-sdp-bridge');
let signalServer = null;
let mediasoupRouter;
let streamer = null;
let peers = new Map();
function connectSignalling(server) {
console.log("Connecting to Signalling Server at %s", server);
signalServer = new WebSocket(server);
signalServer.addEventListener("open", _ => { console.log(`Connected to signalling server`); });
signalServer.addEventListener("error", result => { console.log(`Error: ${result.message}`); });
signalServer.addEventListener("message", result => onSignallingMessage(result.data));
signalServer.addEventListener("close", result => {
console.log(`Disconnected from signalling server: ${result.code} ${result.reason}`);
console.log("Attempting reconnect to signalling server...");
setTimeout(()=> {
connectSignalling(server);
}, 2000);
});
}
async function onStreamerOffer(sdp) {
console.log("Got offer from streamer");
if (streamer != null) {
signalServer.close(1013 /* Try again later */, 'Producer is already connected');
return;
}
const transport = await createWebRtcTransport("Streamer");
const sdpEndpoint = mediasoupSdp.createSdpEndpoint(transport, mediasoupRouter.rtpCapabilities);
const producers = await sdpEndpoint.processOffer(sdp);
const sdpAnswer = sdpEndpoint.createAnswer();
const answer = { type: "answer", sdp: sdpAnswer };
console.log("Sending answer to streamer.");
signalServer.send(JSON.stringify(answer));
streamer = { transport: transport, producers: producers };
}
function getNextStreamerSCTPId() {
if(!streamer){
throw new TypeError('Cannot generate an SCTP stream id - streamer was null.');
}
if (!streamer.transport || !streamer.transport.sctpParameters || typeof streamer.transport.sctpParameters.MIS !== 'number') {
throw new TypeError('Streamer was not setup with the following require properties: streamer.transport.sctpParameters.MIS');
}
const numStreams = streamer.transport.sctpParameters.MIS;
if (!streamer.dataStreamIds){
streamer.dataStreamIds = Buffer.alloc(numStreams, 0);
}
if (!streamer.nextDataStreamId) {
streamer.nextDataStreamId = 0;
}
let sctpStreamId;
for (let idx = streamer.nextDataStreamId; idx < streamer.dataStreamIds.length; ++idx) {
sctpStreamId = idx % streamer.dataStreamIds.length;
if (!streamer.dataStreamIds[sctpStreamId]) {
streamer.nextDataStreamId = sctpStreamId + 1;
return sctpStreamId;
}
}
console.error("No available SCTP ids, they are all allocated.");
return -1;
}
function onStreamerDisconnected() {
console.log("Streamer disconnected");
disconnectAllPeers();
if (streamer != null) {
for (const mediaProducer of streamer.producers) {
mediaProducer.close();
}
streamer.transport.close();
streamer = null;
}
}
async function onPeerConnected(peerId) {
console.log("Player %s joined", peerId);
if (streamer == null) {
console.log("No streamer connected, ignoring player.");
return;
}
const transport = await createWebRtcTransport("Peer " + peerId);
const sdpEndpoint = mediasoupSdp.createSdpEndpoint( transport, mediasoupRouter.rtpCapabilities );
sdpEndpoint.addConsumeData(); // adds the sctp 'application' section to the offer
// media consumers
let consumers = [];
try {
for (const mediaProducer of streamer.producers) {
const consumer = await transport.consume({ producerId: mediaProducer.id, rtpCapabilities: mediasoupRouter.rtpCapabilities });
consumer.observer.on("layerschange", function() { console.log("layer changed!", consumer.currentLayers); });
sdpEndpoint.addConsumer(consumer);
consumers.push(consumer);
}
} catch(err) {
console.error("transport.consume() failed:", err);
return;
}
const offerSignal = {
type: "offer",
playerId: peerId,
sdp: sdpEndpoint.createOffer(),
sfu: true // indicate we're offering from sfu
};
// send offer to peer
signalServer.send(JSON.stringify(offerSignal));
const newPeer = {
id: peerId,
transport: transport,
sdpEndpoint: sdpEndpoint,
consumers: consumers
};
// add the new peer
peers.set(peerId, newPeer);
}
async function setupPeerDataChannels(peerId) {
const peer = peers.get(peerId);
if (!peer) {
console.error(`Could not send browser any datachannels for peer=${peerId} because peer was not found.`);
return;
}
const nextStreamerSCTPStreamId = getNextStreamerSCTPId();
const nextPeerSCTPStreamId = getNextStreamerSCTPId();
console.log(`Attempting streamer SCTP id=${nextStreamerSCTPStreamId}`);
// streamer data producer (produces data for the peer)
peer.streamerDataProducer = await streamer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextStreamerSCTPStreamId, ordered: true}});
console.log(`Attempting peer SCTP id=${nextPeerSCTPStreamId}`);
// peer data producer (produces data for the streamer)
peer.peerDataProducer = await peer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextPeerSCTPStreamId, ordered: true}});
// peer data consumer (consumes streamer data)
peer.peerDataConsumer = await peer.transport.consumeData({ dataProducerId: peer.streamerDataProducer.id });
// streamer data consumer (consumes peer data)
peer.streamerDataConsumer = await streamer.transport.consumeData({ dataProducerId: peer.peerDataProducer.id });
const peerSignal = {
type: 'peerDataChannels',
playerId: peerId,
sendStreamId: peer.peerDataProducer.sctpStreamParameters.streamId,
recvStreamId: peer.peerDataConsumer.sctpStreamParameters.streamId
};
// Send browser a message with a send/recv data channel SCTP stream id
signalServer.send(JSON.stringify(peerSignal));
}
async function setupStreamerDataChannelsForPeer(peerId) {
const peer = peers.get(peerId);
if (!peer) {
console.error(`Could not send streamer any datachannels for peer=${peerId} because peer was not found.`);
return;
}
if(!peer.streamerDataProducer || !peer.streamerDataConsumer){
console.error(`There was no streamer data producer/consumer setup for peer=${peerId}. Did you make sure to send "dataChannelRequest" first?`);
return;
}
const streamerSignal = {
type: "streamerDataChannels",
playerId: peerId,
sendStreamId: peer.streamerDataProducer.sctpStreamParameters.streamId,
recvStreamId: peer.streamerDataConsumer.sctpStreamParameters.streamId
};
// send streamer a message with a send/recv data channel SCTP stream id
signalServer.send(JSON.stringify(streamerSignal));
}
async function onPeerAnswer(peerId, sdp) {
console.log("Got answer from player %s", peerId);
const consumer = peers.get(peerId);
if (!consumer){
console.error(`Unable to find player ${peerId}`);
}
else{
consumer.sdpEndpoint.processAnswer(sdp);
}
}
function onPeerDisconnected(peerId) {
console.log("Player %s disconnected", peerId);
const peer = peers.get(peerId);
if (peer != null) {
for (consumer of peer.consumers) {
consumer.close();
}
if (peer.peerDataConsumer) {
peer.peerDataConsumer.close();
peer.peerDataProducer.close();
}
if(peer.streamerDataConsumer){
// Set the streamer sctp id we generated back to zero indicating it can be reused.
if(streamer && streamer.dataStreamIds){
const allocatedStreamId = peer.streamerDataProducer.sctpStreamParameters.streamId;
const allocatedPeerStreamId = peer.peerDataProducer.sctpStreamParameters.streamId;
streamer.dataStreamIds[allocatedStreamId] = 0;
streamer.dataStreamIds[allocatedPeerStreamId] = 0;
}
peer.streamerDataConsumer.close();
peer.streamerDataProducer.close();
}
peer.transport.close();
}
peers.delete(peerId);
}
function disconnectAllPeers() {
console.log("Disconnected all players");
for (const [peerId, peer] of peers) {
onPeerDisconnected(peerId);
}
}
async function onSignallingMessage(message) {
//console.log(`Got MSG: ${message}`);
const msg = JSON.parse(message);
if (msg.type == 'offer') {
onStreamerOffer(msg.sdp);
}
else if (msg.type == 'answer') {
onPeerAnswer(msg.playerId, msg.sdp);
}
else if (msg.type == 'playerConnected') {
onPeerConnected(msg.playerId);
}
else if (msg.type == 'playerDisconnected') {
onPeerDisconnected(msg.playerId);
}
else if (msg.type == 'streamerDisconnected') {
onStreamerDisconnected();
}
else if (msg.type == 'dataChannelRequest') {
setupPeerDataChannels(msg.playerId);
}
else if (msg.type == 'peerDataChannelsReady') {
setupStreamerDataChannelsForPeer(msg.playerId);
}
// todo a new message type for force layer switch (for debugging)
// see: https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-setPreferredLayers
// preferredLayers for debugging to select a particular simulcast layer, looks like { spatialLayer: 2, temporalLayer: 0 }
}
async function startMediasoup() {
let worker = await mediasoup.createWorker({
logLevel: config.mediasoup.worker.logLevel,
logTags: config.mediasoup.worker.logTags,
rtcMinPort: config.mediasoup.worker.rtcMinPort,
rtcMaxPort: config.mediasoup.worker.rtcMaxPort,
});
worker.on('died', () => {
console.error('mediasoup worker died (this should never happen)');
process.exit(1);
});
const mediaCodecs = config.mediasoup.router.mediaCodecs;
const mediasoupRouter = await worker.createRouter({ mediaCodecs });
return mediasoupRouter;
}
async function createWebRtcTransport(identifier) {
const {
listenIps,
initialAvailableOutgoingBitrate
} = config.mediasoup.webRtcTransport;
const transport = await mediasoupRouter.createWebRtcTransport({
listenIps: listenIps,
enableUdp: true,
enableTcp: false,
preferUdp: true,
enableSctp: true, // datachannels
initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate
});
transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); });
transport.on("iceselectedtuplechange", (iceTuple) => { console.log("%s ICE selected tuple %s", identifier, JSON.stringify(iceTuple)); });
transport.on("sctpstatechange", (sctpState) => { console.log("%s SCTP state changed to %s", identifier, sctpState); });
return transport;
}
async function main() {
console.log('Starting Mediasoup...');
console.log("Config = ");
console.log(config);
mediasoupRouter = await startMediasoup();
connectSignalling(config.signallingURL);
}
main();
Submodule pixel-streaming-webrtc/WebServers/SignallingWebServer deleted from 5d19a497ad
@@ -1,110 +0,0 @@
@Rem Copyright Epic Games, Inc. All Rights Reserved.
@echo off
@Rem Set script location as working directory for commands.
pushd "%~dp0"
:arg_loop_start
SET ARG=%1
if DEFINED ARG (
if "%ARG%"=="/h" (
goto print_help
)
if "%ARG%"=="/v" (
SET UEVersion=%2
SHIFT
)
if "%ARG%"=="/b" (
SET PSInfraTagOrBranch=%2
SET IsTag=0
SHIFT
)
if "%ARG%"=="/t" (
SET PSInfraTagOrBranch=%2
SET IsTag=1
SHIFT
)
SHIFT
goto arg_loop_start
)
@Rem Name and version of ps-infra that we are downloading
SET PSInfraOrg=EpicGames
SET PSInfraRepo=PixelStreamingInfrastructure
@Rem If a UE version is supplied set the right branch or tag to fetch for that version of UE
if DEFINED UEVersion (
if "%UEVersion%"=="4.26" (
SET PSInfraTagOrBranch=UE4.26
SET IsTag=0
)
if "%UEVersion%"=="4.27" (
SET PSInfraTagOrBranch=UE4.27
SET IsTag=0
)
if "%UEVersion%"=="5.0" (
SET PSInfraTagOrBranch=UE5.0
SET IsTag=0
)
)
@Rem If no arguments select a specific version, fetch the appropriate default
if NOT DEFINED PSInfraTagOrBranch (
SET PSInfraTagOrBranch=master
SET IsTag=0
)
@Rem Whether the named reference is a tag or a branch affects the URL we fetch it on
if %IsTag%==1 (
SET RefType=tags
) else (
SET RefType=heads
)
@Rem Look for a SignallingWebServer directory next to this script
if exist SignallingWebServer\ (
echo SignallingWebServer directory found...skipping install.
) else (
echo SignallingWebServer directory not found...beginning ps-infra download.
@Rem Download ps-infra and follow redirects.
curl -L https://github.com/%PSInfraOrg%/%PSInfraRepo%/archive/refs/%RefType%/%PSInfraTagOrBranch%.zip > ps-infra.zip
@Rem Unarchive the .zip
tar -xmf ps-infra.zip || echo bad archive, contents: && type ps-infra.zip && exit 0
@Rem Rename the extracted, versioned, directory
for /d %%i in ("PixelStreamingInfrastructure-*") do (
for /d %%j in ("%%i/*") do (
echo "%%i\%%j"
move "%%i\%%j" .
)
for %%j in ("%%i/*") do (
echo "%%i\%%j"
move "%%i\%%j" .
)
echo "%%i"
rmdir /s /q "%%i"
)
@Rem Delete the downloaded zip
del ps-infra.zip
)
exit 0
:print_help
echo.
echo Tool for fetching PixelStreaming Infrastructure. If no flags are set specifying a version to fetch,
echo the recommended version will be chosen as a default.
echo.
echo Usage:
echo %~n0%~x0 [^/h] [^/v ^<UE version^>] [^/b ^<branch^>] [^/t ^<tag^>]
echo Where:
echo /v Specify a version of Unreal Engine to download the recommended release for
echo /b Specify a specific branch for the tool to download from repo
echo /t Specify a specific tag for the tool to download from repo
echo /h Display this help message
exit 1
@@ -1,93 +0,0 @@
#!/bin/bash
# Copyright Epic Games, Inc. All Rights Reserved.
BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
pushd "${BASH_LOCATION}" > /dev/null
print_help() {
echo "
Tool for fetching PixelStreaming Infrastructure. If no flags are set specifying a version to fetch,
the recommended version will be chosen as a default.
Usage:
${0} [-h] [-v <UE version>] [-b <branch>] [-t <tag>]
Where:
-v Specify a version of Unreal Engine to download the recommended
release for
-b Specify a specific branch for the tool to download from repo
-t Specify a specific tag for the tool to download from repo
-h Display this help message
"
exit 1
}
while(($#)) ; do
case "$1" in
-h ) print_help;;
-v ) UEVersion="$2"; shift 2;;
-b ) PSInfraTagOrBranch="$2"; IsTag=0; shift 2;;
-t ) PSInfraTagOrBranch="$2"; IsTag=1; shift 2;;
* ) echo "Unknown command: $1"; shift;;
esac
done
# Name and version of ps-infra that we are downloading
PSInfraOrg=EpicGames
PSInfraRepo=PixelStreamingInfrastructure
# If a UE version is supplied set the right branch or tag to fetch for that version of UE
if [ ! -z "$UEVersion" ]
then
if [ "$UEVersion" = "4.26" ]
then
PSInfraTagOrBranch=UE4.26
IsTag=0
fi
if [ "$UEVersion" = "4.27" ]
then
PSInfraTagOrBranch=UE4.27
IsTag=0
fi
if [ "$UEVersion" = "5.0" ]
then
PSInfraTagOrBranch=UE5.0
IsTag=0
fi
fi
# If no arguments select a specific version, fetch the appropriate default
if [ -z "$PSInfraTagOrBranch" ]
then
PSInfraTagOrBranch=master
IsTag=0
fi
# Whether the named reference is a tag or a branch affects the URL we fetch it on
if [ "$IsTag" -eq 1 ]
then
RefType=tags
else
RefType=heads
fi
# Look for a SignallingWebServer directory next to this script
if [ -d SignallingWebServer ]
then
echo "SignallingWebServer directory found...skipping install."
else
echo "SignallingWebServer directory not found...beginning ps-infra download."
# Download ps-infra and follow redirects.
curl -L https://github.com/$PSInfraOrg/$PSInfraRepo/archive/refs/$RefType/$PSInfraTagOrBranch.tar.gz > ps-infra.tar.gz
# Unarchive the .tar
tar -xmf ps-infra.tar.gz || $(echo "bad archive, contents:" && head --lines=20 ps-infra.tar.gz && exit 0)
# Move the server folders into the current directory (WebServers) and delete the original directory
mv PixelStreamingInfrastructure-*/* .
rm -rf PixelStreamingInfrastructure-*
# Delete the downloaded tar
rm ps-infra.tar.gz
fi