Commit 3a88e44c by Christoph

added awrtc

parent 128d3c11
build/apps
build/awrtc
build/bundle
node_modules
\ No newline at end of file
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2019, because-why-not Copyright (c) 2019, because-why-not.com Limited
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
......
# awrtc_browser # awrtc_browser
\ No newline at end of file npm install
npm run build
After that you can try the file ./build/callapp.html for the typesript based example or ./build/callapp_js.html for javascript example.
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.4.1</title>
<link rel="shortcut icon" type="image/png" href="node_modules/jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine_favicon.png">
<link rel="stylesheet" href="node_modules/jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="node_modules/jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine/node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<!-- include source files here... -->
<script src="./build/bundle/test.js"></script>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="./bundle/apps.js"></script>
</head>
<body>
<div id="callapp1">
<h1>Callapp1:</h1>
URL to connect:
<p class="callapp_url">
</p>
<input type="checkbox" name="audio" class="callapp_send_audio" checked autocomplete="off"> Audio
<input type="checkbox" name="video" class="callapp_send_video" checked autocomplete="off"> Video
<input type= "text" class="callapp_address" autocomplete="off">
<button class="callapp_button"> Start / Stop </button>
<div class="callapp_local_video">local video</div>
<div class="callapp_remote_video">remote video</div>
</div>
<!--
<div id="callapp2">
<h1>Callapp2:</h1>
URL to connect:
<p class="callapp_url">
</p>
<input type="checkbox" name="audio" class="callapp_send_audio" checked autocomplete="off"> Audio
<input type="checkbox" name="video" class="callapp_send_video" checked autocomplete="off"> Video
<input type= "text" class="callapp_address" autocomplete="off">
<button class="callapp_button"> Start / Stop </button>
<div class="callapp_local_video">local video</div>
<div class="callapp_remote_video">remote video</div>
</div>
-->
<script>
apps.callapp(document.querySelector("#callapp1"));
//apps.callapp(document.querySelector("#callapp2"));
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="./bundle/awrtc.js"></script>
</head>
<body>
<div id="callapp1">
<h1>Callapp1:</h1>
URL to connect:
<p class="callapp_url">
</p>
<input type="checkbox" name="audio" class="callapp_send_audio" checked autocomplete="off"> Audio
<input type="checkbox" name="video" class="callapp_send_video" checked autocomplete="off"> Video
<input type= "text" class="callapp_address" autocomplete="off">
<button class="callapp_button"> Start / Stop </button>
<div class="callapp_local_video">local video</div>
<div class="callapp_remote_video">remote video</div>
</div>
<script>
//everything below is just the callapp.ts compiled to js without the "export" key word
//the inclusion of the ./bundle/awrtc.js above will make the interface available via
//the global variable "awrtc"
class CallApp {
constructor() {
this.mNetConfig = new awrtc.NetworkConfig();
this.mCall = null;
//update loop
this.mIntervalId = -1;
this.mLocalVideo = null;
this.mRemoteVideo = {};
this.mIsRunning = false;
this.OnStartStopButtonClicked = () => {
if (this.mIsRunning) {
this.Stop();
}
else {
this.Start(this.mAddress, this.mVideo, this.mAudio);
}
};
this.OnUiUpdate = () => {
console.debug("OnUiUpdate");
this.mAddress = this.mUiAddress.value;
this.mAudio = this.mUiAudio.checked;
this.mVideo = this.mUiVideo.checked;
this.mUiUrl.innerHTML = this.GetUrl();
};
this.mNetConfig.IceServers = [
{ urls: "stun:stun.because-why-not.com:443" },
{ urls: "stun:stun.l.google.com:19302" }
];
//use for testing conferences
//this.mNetConfig.IsConference = true;
//this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/testshared";
this.mNetConfig.IsConference = false;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/callapp";
}
setupUi(parent) {
this.mUiAddress = parent.querySelector(".callapp_address");
this.mUiAudio = parent.querySelector(".callapp_send_audio");
this.mUiVideo = parent.querySelector(".callapp_send_video");
this.mUiUrl = parent.querySelector(".callapp_url");
this.mUiButton = parent.querySelector(".callapp_button");
this.mUiLocalVideoParent = parent.querySelector(".callapp_local_video");
this.mUiRemoteVideoParent = parent.querySelector(".callapp_remote_video");
this.mUiAudio.onclick = this.OnUiUpdate;
this.mUiVideo.onclick = this.OnUiUpdate;
this.mUiAddress.onkeyup = this.OnUiUpdate;
this.mUiButton.onclick = this.OnStartStopButtonClicked;
//set default value + make string "true"/"false" to proper booleans
this.mAudio = this.GetParameterByName("audio");
this.mAudio = this.tobool(this.mAudio, true);
this.mVideo = this.GetParameterByName("video");
this.mVideo = this.tobool(this.mVideo, true);
this.mAutostart = this.GetParameterByName("autostart");
this.mAutostart = this.tobool(this.mAutostart, false);
this.mAddress = this.GetParameterByName("a");
//if autostart is set but no address is given -> create one and reopen the page
if (this.mAddress === null && this.mAutostart == true) {
this.mAddress = this.GenerateRandomKey();
window.location.href = this.GetUrlParams();
}
else {
if (this.mAddress === null)
this.mAddress = this.GenerateRandomKey();
this.UpdateUi();
}
//used for interacting with the Unity CallApp
//current hack to get the html element delivered. by default this
//just the image is copied and given as array
//Lazy frames will be the default soon though
if (this.mAutostart) {
console.log("Starting automatically ... ");
this.Start(this.mAddress, this.mAudio, this.mVideo);
}
console.log("address: " + this.mAddress + " audio: " + this.mAudio + " video: " + this.mVideo + " autostart: " + this.mAutostart);
}
GetParameterByName(name) {
var url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
if (!results)
return null;
if (!results[2])
return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
tobool(value, defaultval) {
if (value === true || value === "true")
return true;
if (value === false || value === "false")
return false;
return defaultval;
}
GenerateRandomKey() {
var result = "";
for (var i = 0; i < 7; i++) {
result += String.fromCharCode(65 + Math.round(Math.random() * 25));
}
return result;
}
GetUrlParams() {
return "?a=" + this.mAddress + "&audio=" + this.mAudio + "&video=" + this.mVideo + "&" + "autostart=" + true;
}
GetUrl() {
return location.protocol + '//' + location.host + location.pathname + this.GetUrlParams();
}
UpdateUi() {
console.log("UpdateUi");
this.mUiAddress.value = this.mAddress;
this.mUiAudio.checked = this.mAudio;
this.mUiVideo.checked = this.mVideo;
this.mUiUrl.innerHTML = this.GetUrl();
}
Start(address, audio, video) {
if (this.mCall != null)
this.Stop();
console.log("start");
console.log("Using signaling server url: " + this.mNetConfig.SignalingUrl);
//create media configuration
var config = new awrtc.MediaConfig();
config.Audio = audio;
config.Video = video;
config.IdealWidth = 640;
config.IdealHeight = 480;
config.IdealFps = 30;
config.FrameUpdates = false;
console.log("requested config:" + JSON.stringify(config));
//setup our high level call class.
this.mCall = new awrtc.BrowserWebRtcCall(this.mNetConfig);
//handle events (get triggered after Configure / Listen call)
//+ugly lahmda to avoid loosing "this" reference
this.mCall.addEventListener((sender, args) => {
this.OnNetworkEvent(sender, args);
});
//As the system is designed for realtime graphics we have to call the Update method. Events are only
//triggered during this Update call!
this.mIntervalId = setInterval(() => {
this.Update();
}, 50);
//configure media. This will request access to media and can fail if the user doesn't have a proper device or
//blocks access
this.mCall.Configure(config);
//Try to listen to the address
//Conference mode = everyone listening will connect to each other
//Call mode -> If the address is free it will wait for someone else to connect
// -> If the address is used then it will fail to listen and then try to connect via Call(address);
this.mCall.Listen(address);
}
Stop() {
if (this.mCall != null) {
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
}
}
Update() {
if (this.mCall != null)
this.mCall.Update();
}
OnNetworkEvent(sender, args) {
//User gave access to requiested camera/ micrphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete) {
console.log("configuration complete");
}
/*
else if (args.Type == awrtc.CallEventType.FrameUpdate) {
//frame updated. For unity this needs to be called every frame to deliver
//the byte array but for the browser we get the internally used video element
//for now. (this system will probably change in the future)
let frameUpdateArgs = args as awrtc.FrameUpdateEventArgs;
var lazyFrame = frameUpdateArgs.Frame as LazyFrame;
if (this.mLocalVideo == null && frameUpdateArgs.ConnectionId == awrtc.ConnectionId.INVALID) {
let videoElement = lazyFrame.FrameGenerator.VideoElement;
this.mLocalVideo = videoElement;
this.mUiLocalVideoParent.appendChild(videoElement);
console.log("local video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
else if (frameUpdateArgs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] == null) {
var videoElement = lazyFrame.FrameGenerator.VideoElement;
this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] = videoElement;
this.mUiRemoteVideoParent.appendChild(videoElement);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}
*/
else if (args.Type == awrtc.CallEventType.MediaUpdate) {
let margs = args;
if (this.mLocalVideo == null && margs.ConnectionId == awrtc.ConnectionId.INVALID) {
var videoElement = margs.VideoElement;
this.mLocalVideo = videoElement;
this.mUiLocalVideoParent.appendChild(videoElement);
console.log("local video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
else if (margs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[margs.ConnectionId.id] == null) {
var videoElement = margs.VideoElement;
this.mRemoteVideo[margs.ConnectionId.id] = videoElement;
this.mUiRemoteVideoParent.appendChild(videoElement);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}
else if (args.Type == awrtc.CallEventType.ListeningFailed) {
//First attempt of this example is to try to listen on a certain address
//for confernce calls this should always work (expect the internet is dead)
if (this.mNetConfig.IsConference == false) {
//no conference call and listening failed? someone might have claimed the address.
//Try to connect to existing call
this.mCall.Call(this.mAddress);
}
else {
console.error("Listening failed. Server dead?");
}
}
else if (args.Type == awrtc.CallEventType.ConnectionFailed) {
//Outgoing call failed entirely. This can mean there is no address to connect to,
//server is offline, internet is dead, firewall blocked access, ...
alert("connection failed");
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call endet or was disconnected
var callEndedEvent = args;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
//document.body.removeChild(mRemoteVideo[callEndedEvent.ConnectionId.id]);
//remove properly
this.mRemoteVideo[callEndedEvent.ConnectionId.id] = null;
}
else if (args.Type == awrtc.CallEventType.Message) {
let messageArgs = args;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
let messageArgs = args;
this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else {
console.log("Unhandled event: " + args.Type);
}
}
}
function callapp(parent) {
let callApp;
console.log("init callapp");
if (parent == null) {
console.log("parent was null");
parent = document.body;
}
awrtc.SLog.SetLogLevel(awrtc.SLogLevel.Info);
callApp = new CallApp();
callApp.setupUi(parent);
}
callapp(document.querySelector("#callapp1"));
//examples.CAPIWebRtcNetwork_minimal();
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="./bundle/apps.js"></script>
<button onclick="apps.WebRtcNetwork_minimal()">WebRtcNetwork_minimal</button><br>
<button onclick="apps.BrowserWebRtcCall_minimal()">BrowserWebRtcCall_minimal</button><br>
</head>
<body>
<script>
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="./bundle/apps.js"></script>
<button onclick="apps.BrowserMediaNetwork_TestLocalCamera()">BrowserMediaNetwork_TestLocalCamera</button><br>
<button onclick="apps.BrowserMediaNetwork_frameaccess()">BrowserMediaNetwork_frameaccess</button><br>
<button onclick="apps.WebsocketNetwork_sharedaddress()">WebsocketNetwork_sharedaddress</button><br>
<button onclick="apps.WebsocketNetwork_test1()">WebsocketNetwork_test1</button><br>
<button onclick="apps.CAPIWebRtcNetwork_testapp()">CAPIWebRtcNetwork_testapp</button><br>
<button onclick="apps.CAPIMediaNetwork_testapp()">CAPIMediaNetwork_testapp</button><br>
<button onclick="apps.CAPIMediaStreamAPI()">CAPIMediaStreamAPI</button><br>
</head>
<body>
<script>
</script>
</body>
</html>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "awrtc_browser",
"version": "0.98.3",
"description": "",
"author": "because-why-not.com Limited",
"license": "BSD-3-Clause",
"dependencies": {},
"scripts": {
"tsc": "tsc",
"webpack": "webpack",
"build": "webpack && tsc -p ./src/awrtc",
"clean": "shx rm -rf ./build/awrtc ./build/bundle"
},
"devDependencies": {
"@types/jasmine": "^2.8.9",
"jasmine": "^2.4.1",
"shx": "^0.3.2",
"source-map-loader": "^0.2.4",
"ts-loader": "^5.2.2",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.1.3",
"uglify-js": "^2.7.0",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",
"webrtc-adapter": "^6.4.6"
}
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as awrtc from "../awrtc";
/**
* Contains default values / servers used for example and test apps.
*
* Note that these servers might not be online forever. Feel free to
* run your own servers and update the url's below.
*/
export class DefaultValues
{
private static SignalingUrl= "ws://signaling.because-why-not.com";
private static SecureSignalingUrl= "wss://signaling.because-why-not.com";
private static get SignalingBase():string
{
if (window.location.protocol != "https:") {
return DefaultValues.SignalingUrl;
} else
{
return DefaultValues.SecureSignalingUrl;
}
}
/**
* Returns the signaling server URL using ws for http pages and
* wss for https. The server url here ends with "test" to avoid
* clashes with existing callapp.
*/
public static get Signaling():string
{
return DefaultValues.SignalingBase + "/test";
}
/**
* Returns the signaling server URL using ws for http pages and
* wss for https. The server url here ends with "testshared" to avoid
* clashes with existing conference app.
* This url of the server usually allows shared addresses for
* n to n connections / conference calls.
*/
public static get SignalingShared():string
{
return DefaultValues.SignalingBase + "/testshared";
}
private static get StunServer() : RTCIceServer
{
let res : RTCIceServer = {
urls: "stun:stun.l.google.com:19302"
};
return res;
}
/**
* Returns ice servers used for testing.
* Might only return the free google stun server. Without an
* additional turn server connections might fail due to firewall.
* Server might be unavailable in China.
*/
public static get IceServers(): RTCIceServer[]
{
return [DefaultValues.StunServer];
}
}
//
export function GetParameterByName(name : string, url?:string) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
//Returns a random string
export function GetRandomKey(): string {
var result = "";
for (var i = 0; i < 7; i++) {
result += String.fromCharCode(65 + Math.round(Math.random() * 25));
}
return result;
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as awrtc from "../awrtc/index"
export class CallApp
{
private mAddress;
private mAudio;
private mVideo;
private mAutostart;
private mNetConfig = new awrtc.NetworkConfig();
private mCall : awrtc.BrowserWebRtcCall = null;
//update loop
private mIntervalId:any = -1;
private mLocalVideo = null;
private mRemoteVideo = {};
private mUiAddress: HTMLInputElement;
private mUiAudio: HTMLInputElement;
private mUiVideo: HTMLInputElement;
private mUiButton: HTMLButtonElement;
private mUiUrl: HTMLElement;
private mUiLocalVideoParent: HTMLElement;
private mUiRemoteVideoParent: HTMLElement;
private mIsRunning = false;
public constructor()
{
this.mNetConfig.IceServers = [
{urls: "stun:stun.because-why-not.com:443"},
{urls: "stun:stun.l.google.com:19302"}
];
//use for testing conferences
//this.mNetConfig.IsConference = true;
//this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/testshared";
this.mNetConfig.IsConference = false;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/callapp";
}
public setupUi(parent : HTMLElement)
{
this.mUiAddress = parent.querySelector<HTMLInputElement>(".callapp_address");
this.mUiAudio = parent.querySelector<HTMLInputElement>(".callapp_send_audio");
this.mUiVideo = parent.querySelector<HTMLInputElement>(".callapp_send_video");
this.mUiUrl = parent.querySelector<HTMLParagraphElement>(".callapp_url");
this.mUiButton = parent.querySelector<HTMLInputElement>(".callapp_button");
this.mUiLocalVideoParent = parent.querySelector<HTMLParagraphElement>(".callapp_local_video");
this.mUiRemoteVideoParent = parent.querySelector<HTMLParagraphElement>(".callapp_remote_video");
this.mUiAudio.onclick = this.OnUiUpdate;
this.mUiVideo.onclick = this.OnUiUpdate;
this.mUiAddress.onkeyup = this.OnUiUpdate;
this.mUiButton.onclick = this.OnStartStopButtonClicked;
//set default value + make string "true"/"false" to proper booleans
this.mAudio = this.GetParameterByName("audio");
this.mAudio = this.tobool(this.mAudio , true)
this.mVideo = this.GetParameterByName("video");
this.mVideo = this.tobool(this.mVideo , true);
this.mAutostart = this.GetParameterByName("autostart");
this.mAutostart = this.tobool(this.mAutostart, false);
this.mAddress = this.GetParameterByName("a");
//if autostart is set but no address is given -> create one and reopen the page
if (this.mAddress === null && this.mAutostart == true) {
this.mAddress = this.GenerateRandomKey();
window.location.href = this.GetUrlParams();
}
else
{
if(this.mAddress === null)
this.mAddress = this.GenerateRandomKey();
this.UpdateUi();
}
//used for interacting with the Unity CallApp
//current hack to get the html element delivered. by default this
//just the image is copied and given as array
//Lazy frames will be the default soon though
if(this.mAutostart)
{
console.log("Starting automatically ... ")
this.Start(this.mAddress, this.mAudio , this.mVideo );
}
console.log("address: " + this.mAddress + " audio: " + this.mAudio + " video: " + this.mVideo + " autostart: " + this.mAutostart);
}
private GetParameterByName(name) {
var url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
if (!results)
return null;
if (!results[2])
return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
private tobool(value, defaultval)
{
if(value === true || value === "true")
return true;
if(value === false || value === "false")
return false;
return defaultval;
}
private GenerateRandomKey() {
var result = "";
for (var i = 0; i < 7; i++) {
result += String.fromCharCode(65 + Math.round(Math.random() * 25));
}
return result;
}
private GetUrlParams() {
return "?a=" + this.mAddress + "&audio=" + this.mAudio + "&video=" + this.mVideo + "&" + "autostart=" + true;
}
private GetUrl() {
return location.protocol + '//' + location.host + location.pathname + this.GetUrlParams();
}
public OnStartStopButtonClicked = ()=>{
if(this.mIsRunning) {
this.Stop();
}else{
this.Start(this.mAddress, this.mVideo , this.mAudio );
}
}
public OnUiUpdate = ()=>
{
console.debug("OnUiUpdate");
this.mAddress = this.mUiAddress.value;
this.mAudio = this.mUiAudio.checked;
this.mVideo = this.mUiVideo.checked;
this.mUiUrl.innerHTML = this.GetUrl();
}
public UpdateUi() : void
{
console.log("UpdateUi");
this.mUiAddress.value = this.mAddress;
this.mUiAudio.checked = this.mAudio ;
this.mUiVideo.checked = this.mVideo ;
this.mUiUrl.innerHTML = this.GetUrl();
}
public Start(address, audio, video) : void
{
if(this.mCall != null)
this.Stop();
console.log("start");
console.log("Using signaling server url: " + this.mNetConfig.SignalingUrl);
//create media configuration
var config = new awrtc.MediaConfig();
config.Audio = audio;
config.Video = video;
config.IdealWidth = 640;
config.IdealHeight = 480;
config.IdealFps = 30;
config.FrameUpdates = false;
console.log("requested config:" + JSON.stringify(config));
//setup our high level call class.
this.mCall = new awrtc.BrowserWebRtcCall(this.mNetConfig);
//handle events (get triggered after Configure / Listen call)
//+ugly lahmda to avoid loosing "this" reference
this.mCall.addEventListener((sender, args)=>{
this.OnNetworkEvent(sender, args);
});
//As the system is designed for realtime graphics we have to call the Update method. Events are only
//triggered during this Update call!
this.mIntervalId = setInterval(()=>{
this.Update();
}, 50);
//configure media. This will request access to media and can fail if the user doesn't have a proper device or
//blocks access
this.mCall.Configure(config);
//Try to listen to the address
//Conference mode = everyone listening will connect to each other
//Call mode -> If the address is free it will wait for someone else to connect
// -> If the address is used then it will fail to listen and then try to connect via Call(address);
this.mCall.Listen(address);
}
public Stop(): void
{
if(this.mCall != null)
{
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
}
}
private Update():void
{
if(this.mCall != null)
this.mCall.Update();
}
private OnNetworkEvent(sender: any, args: awrtc.CallEventArgs):void{
//User gave access to requiested camera/ micrphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete){
console.log("configuration complete");
}
/*
else if (args.Type == awrtc.CallEventType.FrameUpdate) {
//frame updated. For unity this needs to be called every frame to deliver
//the byte array but for the browser we get the internally used video element
//for now. (this system will probably change in the future)
let frameUpdateArgs = args as awrtc.FrameUpdateEventArgs;
var lazyFrame = frameUpdateArgs.Frame as LazyFrame;
if (this.mLocalVideo == null && frameUpdateArgs.ConnectionId == awrtc.ConnectionId.INVALID) {
let videoElement = lazyFrame.FrameGenerator.VideoElement;
this.mLocalVideo = videoElement;
this.mUiLocalVideoParent.appendChild(videoElement);
console.log("local video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
else if (frameUpdateArgs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] == null) {
var videoElement = lazyFrame.FrameGenerator.VideoElement;
this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] = videoElement;
this.mUiRemoteVideoParent.appendChild(videoElement);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}
*/
else if (args.Type == awrtc.CallEventType.MediaUpdate) {
let margs = args as awrtc.MediaUpdatedEventArgs;
if (this.mLocalVideo == null && margs.ConnectionId == awrtc.ConnectionId.INVALID) {
var videoElement = margs.VideoElement;
this.mLocalVideo = videoElement;
this.mUiLocalVideoParent.appendChild(videoElement);
console.log("local video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
else if (margs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[margs.ConnectionId.id] == null) {
var videoElement = margs.VideoElement;
this.mRemoteVideo[margs.ConnectionId.id] = videoElement;
this.mUiRemoteVideoParent.appendChild(videoElement);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}
else if (args.Type == awrtc.CallEventType.ListeningFailed) {
//First attempt of this example is to try to listen on a certain address
//for confernce calls this should always work (expect the internet is dead)
if (this.mNetConfig.IsConference == false) {
//no conference call and listening failed? someone might have claimed the address.
//Try to connect to existing call
this.mCall.Call(this.mAddress);
}
else {
console.error("Listening failed. Server dead?");
}
}
else if (args.Type == awrtc.CallEventType.ConnectionFailed) {
//Outgoing call failed entirely. This can mean there is no address to connect to,
//server is offline, internet is dead, firewall blocked access, ...
alert("connection failed");
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call endet or was disconnected
var callEndedEvent = args as awrtc.CallEndedEventArgs;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
//document.body.removeChild(mRemoteVideo[callEndedEvent.ConnectionId.id]);
//remove properly
this.mRemoteVideo[callEndedEvent.ConnectionId.id] = null;
}
else if (args.Type == awrtc.CallEventType.Message) {
let messageArgs = args as awrtc.MessageEventArgs;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
let messageArgs = args as awrtc.DataMessageEventArgs;
this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else {
console.log("Unhandled event: " + args.Type);
}
}
}
export function callapp(parent: HTMLElement)
{
let callApp : CallApp;
console.log("init callapp");
if(parent == null)
{
console.log("parent was null");
parent = document.body;
}
awrtc.SLog.SetLogLevel(awrtc.SLogLevel.Info);
callApp = new CallApp();
callApp.setupUi(parent);
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as apps from "./index"
import * as awrtc from "../awrtc/index"
(window as any).awrtc = awrtc;
(window as any).apps = apps;
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as awrtc from "../awrtc/index"
import { DefaultValues, GetRandomKey, GetParameterByName } from "./apphelpers";
import { WebsocketNetwork } from "../awrtc/index";
//Creates two WebRtcNetwork objects and connects them
//directly + sends test messages
export function WebRtcNetwork_minimal() {
console.log("test1");
var testMessage = "test1234";
var websocketurl: string = DefaultValues.Signaling;
let rtcConfig: RTCConfiguration = { iceServers: [{ urls: ["stun:stun.l.google.com:19302"] } as RTCIceServer] };
var srv = new awrtc.WebRtcNetwork(new awrtc.SignalingConfig(new WebsocketNetwork(websocketurl)), rtcConfig);
srv.StartServer();
var clt = new awrtc.WebRtcNetwork(new awrtc.SignalingConfig(new WebsocketNetwork(websocketurl)), rtcConfig);
setInterval(() => {
srv.Update();
var evt: awrtc.NetworkEvent = null;
while (evt = srv.Dequeue()) {
console.log("server inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.ServerInitialized) {
console.log("server started. Address " + evt.Info);
clt.Connect(evt.Info);
} else if (evt.Type == awrtc.NetEventType.ServerInitFailed) {
console.error("server start failed");
} else if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("server new incoming connection");
} else if (evt.Type == awrtc.NetEventType.Disconnected) {
console.log("server peer disconnected");
console.log("server shutdown");
srv.Shutdown();
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
srv.SendData(evt.ConnectionId, evt.MessageData, true);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
srv.SendData(evt.ConnectionId, evt.MessageData, false);
}
}
srv.Flush();
clt.Update();
while (evt = clt.Dequeue()) {
console.log("client inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("client connection established");
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
clt.SendData(evt.ConnectionId, buff, true);
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
//check last message
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
clt.SendData(evt.ConnectionId, buff, false);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
console.log("client disconnecting");
clt.Disconnect(evt.ConnectionId);
console.log("client shutting down");
clt.Shutdown();
}
}
clt.Flush();
}, 100);
}
interface IRemoteVideoDict {
[connectionId: number]: HTMLVideoElement;
}
class MinimalCall
{
//just a number we give each local call to
//identify the output of each indivudal call
mId:number = -1;
mCall: awrtc.BrowserWebRtcCall = null;
mLocalVideo: HTMLVideoElement = null;
mRemoteVideo: IRemoteVideoDict = {};
mNetConfig: awrtc.NetworkConfig;
mMediaConfig: awrtc.MediaConfig;
mAddress: string;
mDiv:HTMLElement;
constructor( id, netConfig:awrtc.NetworkConfig, mediaConfig: awrtc.MediaConfig)
{
this.mId = id;
this.mNetConfig = netConfig;
this.mMediaConfig = mediaConfig;
}
public Start(address:string): void
{
this.mDiv = document.createElement("div");
document.body.appendChild(this.mDiv);
this.mDiv.innerHTML += "<h1>Call " + this.mId + "</h1>";
this.mAddress = address;
this.mCall = new awrtc.BrowserWebRtcCall(this.mNetConfig);
this.mCall.addEventListener((sender: any, args: awrtc.CallEventArgs) => {
this.OnCallEvent(sender, args);
});
setInterval(() => {
this.Update();
}, 50);
this.mCall.Configure(this.mMediaConfig);
}
private OnCallEvent(sender: any, args: awrtc.CallEventArgs)
{
if (args.Type == awrtc.CallEventType.ConfigurationComplete) {
console.log("configuration complete");
this.mCall.Listen(this.mAddress);
} else if (args.Type == awrtc.CallEventType.FrameUpdate) {
let frameUpdateArgs = args as awrtc.FrameUpdateEventArgs;
if (this.mLocalVideo == null && frameUpdateArgs.ConnectionId == awrtc.ConnectionId.INVALID) {
this.mDiv.innerHTML += "local video: " + "<br>";
console.log(this.mId + ":local video added");
let lazyFrame = frameUpdateArgs.Frame as awrtc.LazyFrame;
this.mLocalVideo = lazyFrame.FrameGenerator.VideoElement;
this.mDiv.appendChild(this.mLocalVideo);
} else if (frameUpdateArgs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] == null) {
console.log(this.mId + ":remote video added");
let lazyFrame = frameUpdateArgs.Frame as awrtc.LazyFrame;
this.mDiv.innerHTML += "remote " + this.mId + "<br>";
this.mRemoteVideo[frameUpdateArgs.ConnectionId.id] = lazyFrame.FrameGenerator.VideoElement;
this.mDiv.appendChild(this.mRemoteVideo[frameUpdateArgs.ConnectionId.id]);
}
} else if (args.Type == awrtc.CallEventType.ListeningFailed) {
if (this.mNetConfig.IsConference == false) {
//in 1 to 1 calls there is a listener and a caller
//if we try to listen first and it fails it likely means
//the other side is waiting for an incoming call
this.mCall.Call(this.mAddress);
} else {
//in conference mode there is no "caller" as everyone
//just joins a single call via Listen call. if it fails
//there is likely a network fault / configuration error
console.error(this.mId + ":Listening failed. Server dead?");
}
} else if (args.Type == awrtc.CallEventType.ConnectionFailed) {
alert(this.mId + ":connection failed");
} else if (args.Type == awrtc.CallEventType.CallEnded) {
let callEndedEvent = args as awrtc.CallEndedEventArgs;
console.log(this.mId + ":call ended with id " + callEndedEvent.ConnectionId.id);
//document.body.removeChild(mRemoteVideo[callEndedEvent.ConnectionId.id]);
//remove properly
this.mRemoteVideo[callEndedEvent.ConnectionId.id] = null;
} else {
console.log(args.Type);
}
}
private Update(): void
{
this.mCall.Update();
}
}
//Example that creates two calls within the same
//browser window and streams from one end to the
//other.
export function BrowserWebRtcCall_minimal() {
awrtc.BrowserMediaStream.sUseLazyFrames = true;
let netConfig = new awrtc.NetworkConfig();
netConfig.IsConference = false;
netConfig.SignalingUrl = DefaultValues.Signaling;
let mediaConfigSender = new awrtc.MediaConfig();
mediaConfigSender.Video = true;
mediaConfigSender.Audio = true;
let mediaConfigReceiver = new awrtc.MediaConfig();
mediaConfigReceiver.Video = false;
mediaConfigReceiver.Audio = false;
//random key so we don't mistakenly connect
//to another user
//replace with fixed passphrase to connect multiple browser windows
var address = GetRandomKey();
let numberOfCalls = 2;
//creates a call that sends audio and video to the other side
let sender = new MinimalCall(1, netConfig, mediaConfigSender);
sender.Start(address);
//will create a call that is just receiving
let receiver = new MinimalCall(2, netConfig, mediaConfigReceiver);
receiver.Start(address);
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from "./apphelpers"
export * from "./testapps"
export * from "./examples"
export * from "./callapp"
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as awrtc from "../awrtc/index"
import {DefaultValues, GetRandomKey} from "./apphelpers"
import { DeviceApi, DeviceInfo } from "../awrtc/index";
//This file only contains badly maintained
//test apps. Use only experimentation.
//For proper examples look at examples.ts
//testapp to run a full connection test using the CAPI
//which is used by the unity WebGL plugin
export function CAPIWebRtcNetwork_testapp() {
console.log("test1");
var testMessage = "test1234";
//var configuration = "{ \"signaling\" : { \"class\": \"WebsocketNetwork\", \"param\" : \"ws://localhost:12776\"}, \"iceServers\":[\"stun:stun.l.google.com:19302\"]}";
var configuration = "{ \"signaling\" : { \"class\": \"LocalNetwork\", \"param\" : null}, \"iceServers\":[{\"urls\": \"stun:stun.l.google.com:19302\"}]}";
var srv = awrtc.CAPIWebRtcNetworkCreate(configuration);
awrtc.CAPIWebRtcNetworkStartServer(srv, "Room1");
var clt = awrtc.CAPIWebRtcNetworkCreate(configuration);
setInterval(() => {
awrtc.CAPIWebRtcNetworkUpdate(srv);
var evt = null;
while (evt = awrtc.CAPIWebRtcNetworkDequeue(srv)) {
console.log("server inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.ServerInitialized) {
console.log("server started. Address " + evt.Info);
awrtc.CAPIWebRtcNetworkConnect(clt, evt.Info);
} else if (evt.Type == awrtc.NetEventType.ServerInitFailed) {
console.error("server start failed");
} else if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("server new incoming connection");
} else if (evt.Type == awrtc.NetEventType.Disconnected) {
console.log("server peer disconnected");
console.log("server shutdown");
awrtc.CAPIWebRtcNetworkShutdown(srv);
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
//srv.SendData(evt.ConnectionId, evt.MessageData, true);
awrtc.CAPIWebRtcNetworkSendData(srv, evt.ConnectionId.id, evt.MessageData, true);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
//srv.SendData(evt.ConnectionId, evt.MessageData, false);
awrtc.CAPIWebRtcNetworkSendData(srv, evt.ConnectionId.id, evt.MessageData, false);
}
}
//srv.Flush();
awrtc.CAPIWebRtcNetworkFlush(srv);
//clt.Update();
awrtc.CAPIWebRtcNetworkUpdate(clt);
while (evt = awrtc.CAPIWebRtcNetworkDequeue(clt)) {
console.log("client inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("client connection established");
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
//clt.SendData(evt.ConnectionId, buff, true);
awrtc.CAPIWebRtcNetworkSendData(clt, evt.ConnectionId.id, buff, true);
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
//check last message
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
//clt.SendData(evt.ConnectionId, buff, false);
awrtc.CAPIWebRtcNetworkSendData(clt, evt.ConnectionId.id, buff, false);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
console.log("client disconnecting");
//clt.Disconnect(evt.ConnectionId);
awrtc.CAPIWebRtcNetworkDisconnect(clt, evt.ConnectionId.id);
console.log("client shutting down");
//clt.Shutdown();
awrtc.CAPIWebRtcNetworkShutdown(clt);
}
}
//clt.Flush();
awrtc.CAPIWebRtcNetworkFlush(clt);
}, 100);
}
//for testing the media API used by the unity plugin
export function CAPIMediaNetwork_testapp()
{
awrtc.BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
var signalingUrl : string = DefaultValues.Signaling;
let lIndex = awrtc.CAPIMediaNetwork_Create("{\"IceUrls\":[\"stun:stun.l.google.com:19302\"], \"SignalingUrl\":\"ws://because-why-not.com:12776\"}");
let configDone = false;
awrtc.CAPIMediaNetwork_Configure(lIndex, true, true, 160, 120, 640, 480, 640, 480, -1, -1, -1);
console.log(awrtc.CAPIMediaNetwork_GetConfigurationState(lIndex));
let startTime = new Date().getTime();
let mainLoop = function () {
awrtc.CAPIWebRtcNetworkUpdate(lIndex);
if (awrtc.CAPIMediaNetwork_GetConfigurationState(lIndex) == (awrtc.MediaConfigurationState.Successful as number) && configDone == false) {
configDone = true;
console.log("configuration done");
}
if (awrtc.CAPIMediaNetwork_GetConfigurationState(lIndex) == (awrtc.MediaConfigurationState.Failed as number)) {
alert("configuration failed");
}
if (configDone == false)
console.log(awrtc.CAPIMediaNetwork_GetConfigurationState(lIndex));
if ((new Date().getTime() - startTime) < 15000) {
window.requestAnimationFrame(mainLoop);
} else {
console.log("shutting down");
awrtc.CAPIWebRtcNetworkRelease(lIndex);
}
}
window.requestAnimationFrame(mainLoop);
}
//Tests shared address feature of the WebsocketNetwork
export function WebsocketNetwork_sharedaddress() {
console.log("WebsocketNetwork shared address test");
var testMessage = "test1234";
var local = true;
var allowUnsafe = true;
var url : string = DefaultValues.SignalingShared;
let address = "sharedaddresstest";
var network1 = new awrtc.WebsocketNetwork(url);
var network2 = new awrtc.WebsocketNetwork(url);
var network3 = new awrtc.WebsocketNetwork(url);
let network1Greeting = awrtc.Encoding.UTF16.GetBytes("network1 says hi");
let network2Greeting = awrtc.Encoding.UTF16.GetBytes("network2 says hi");
let network3Greeting = awrtc.Encoding.UTF16.GetBytes("network3 says hi");
//
network1.StartServer(address);
network2.StartServer(address);
network3.StartServer(address);
function UpdateNetwork(network: awrtc.IBasicNetwork, name: string) {
network.Update();
var evt: awrtc.NetworkEvent = null;
while (evt = network.Dequeue()) {
if (evt.Type == awrtc.NetEventType.ServerInitFailed
|| evt.Type == awrtc.NetEventType.ConnectionFailed
|| evt.Type == awrtc.NetEventType.ServerClosed) {
console.error(name + "inc: " + evt.toString());
}
else {
console.log(name + "inc: " + evt.toString());
}
if (evt.Type == awrtc.NetEventType.ServerInitialized) {
} else if (evt.Type == awrtc.NetEventType.ServerInitFailed) {
} else if (evt.Type == awrtc.NetEventType.NewConnection) {
let greeting = awrtc.Encoding.UTF16.GetBytes(name + "says hi!");
network.SendData(evt.ConnectionId, greeting, true);
} else if (evt.Type == awrtc.NetEventType.Disconnected) {
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData)
console.log(name + " received: " + str)
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
}
}
network.Flush();
}
let time = 0;
setInterval(() => {
UpdateNetwork(network1, "network1 ");
UpdateNetwork(network2, "network2 ");
UpdateNetwork(network3, "network3 ");
time += 100;
if (time == 10000) {
console.log("network1 shutdown");
network1.Shutdown();
}
if (time == 15000) {
console.log("network2 shutdown");
network2.Shutdown();
}
if (time == 20000) {
console.log("network3 shutdown");
network3.Shutdown();
}
}, 100);
}
export function WebsocketNetwork_test1()
{
var testMessage = "test1234";
var url : string = DefaultValues.Signaling;
var srv = new awrtc.WebsocketNetwork(url);
srv.StartServer();
var clt = new awrtc.WebsocketNetwork(url);
setInterval(() => {
srv.Update();
var evt : awrtc.NetworkEvent= null;
while (evt = srv.Dequeue()) {
console.log("server inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.ServerInitialized) {
console.log("server started. Address " + evt.Info);
clt.Connect(evt.Info);
} else if (evt.Type == awrtc.NetEventType.ServerInitFailed) {
console.error("server start failed");
} else if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("server new incoming connection");
} else if (evt.Type == awrtc.NetEventType.Disconnected) {
console.log("server peer disconnected");
console.log("server shutdown");
srv.Shutdown();
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
srv.SendData(evt.ConnectionId, evt.MessageData, true);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
srv.SendData(evt.ConnectionId, evt.MessageData, false);
}
}
srv.Flush();
clt.Update();
while (evt = clt.Dequeue()) {
console.log("client inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.NewConnection) {
console.log("client connection established");
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
clt.SendData(evt.ConnectionId, buff, true);
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
//check last message
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
let buff = awrtc.Encoding.UTF16.GetBytes(testMessage);
clt.SendData(evt.ConnectionId, buff, false);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
let str = awrtc.Encoding.UTF16.GetString(evt.MessageData);
if (str != testMessage) {
console.error("Test failed sent string %s but received string %s", testMessage, str);
}
console.log("client disconnecting");
clt.Disconnect(evt.ConnectionId);
console.log("client shutting down");
clt.Shutdown();
}
}
clt.Flush();
}, 100);
}
export function BrowserMediaNetwork_TestLocalCamera() {
//first get the device names
let handler : awrtc.DeviceApiOnChanged;
handler = ()=>{
awrtc.DeviceApi.RemOnChangedHandler(handler);
BrowserMediaNetwork_TestLocalCameraInternal();
};
awrtc.DeviceApi.AddOnChangedHandler(handler);
awrtc.DeviceApi.Update();
}
function BrowserMediaNetwork_TestLocalCameraInternal() {
awrtc.BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
let networkConfig = new awrtc.NetworkConfig();
networkConfig.SignalingUrl = null;
let network = new awrtc.BrowserMediaNetwork(networkConfig);
let mediaConfig = new awrtc.MediaConfig();
mediaConfig.Audio = true;
mediaConfig.Video = true;
//test setting a specifid device here
let keys = Object.keys(awrtc.DeviceApi.Devices);
mediaConfig.VideoDeviceName = "";//awrtc.DeviceApi.Devices[keys[0]].label;
network.Configure(mediaConfig);
setInterval(() => {
network.Update();
let frame = network.TryGetFrame(awrtc.ConnectionId.INVALID);
if(frame != null)
console.log("width" + frame.Width + " height:" + frame.Height + " data:" + frame.Buffer[0]);
network.Flush();
}, 50);
}
class FpsCounter
{
lastRefresh = 0;
fps = 0;
counter = 0;
public get Fps()
{
return Math.round(this.fps);
}
public get Counter()
{
return this.counter;
}
Update():void
{
this.counter++;
let diff = new Date().getTime() - this.lastRefresh;
if(diff > 1000)
{
this.fps = this.counter / (diff / 1000);
this.counter = 0;
this.lastRefresh = new Date().getTime();
}
}
}
//Sends video data between two peers within the same browser window
//and accesses the resultung frame data directly
export function BrowserMediaNetwork_frameaccess() {
//awrtc.BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
let address = GetRandomKey();
let networkConfig = new awrtc.NetworkConfig();
networkConfig.SignalingUrl = DefaultValues.Signaling;
let network1 = new awrtc.BrowserMediaNetwork(networkConfig);
let network2 = new awrtc.BrowserMediaNetwork(networkConfig);
let mediaConfig1 = new awrtc.MediaConfig();
mediaConfig1.Audio = true;
mediaConfig1.Video = true;
let mediaConfig2 = new awrtc.MediaConfig();
mediaConfig2.Audio = false;
mediaConfig2.Video = false;
let localFps = new FpsCounter();
let remoteFps = new FpsCounter();
setTimeout(() => {
network1.Configure(mediaConfig1);
}, 5000);
setTimeout(() => {
console.log("connecting network1");
network1.StartServer(address);
//if (network2 != null)
//network2.Configure(mediaConfig);
}, 10000);
setTimeout(() => {
if (network2 != null) {
console.log("connecting network2");
network2.Connect(address);
}
}, 15000);
var remoteConId1: awrtc.ConnectionId = null;
var remoteConId2: awrtc.ConnectionId = null;
setInterval(() => {
network1.Update();
let frame1: awrtc.IFrameData = null;
let frame2: awrtc.IFrameData = null;
frame1 = network1.TryGetFrame(awrtc.ConnectionId.INVALID);
if (frame1 != null)
{
localFps.Update();
if(localFps.Counter % 30 == 0)
console.log("local1 width" + frame1.Width + " height:" + frame1.Height + "fps: " + localFps.Fps + " data:" + frame1.Buffer[0]);
}
var evt: awrtc.NetworkEvent;
while ((evt = network1.Dequeue()) != null) {
console.log("network1: " + evt.toString());
if (evt.Type == awrtc.NetEventType.NewConnection) {
remoteConId1 = evt.ConnectionId;
}
}
if (remoteConId1 != null) {
frame1 = network1.TryGetFrame(remoteConId1);
if (frame1 != null)
console.log("remote1 width" + frame1.Width + " height:" + frame1.Height + " data:" + frame1.Buffer[0]);
}
network1.Flush();
if (network2 == null)
return;
network2.Update();
frame2 = network2.TryGetFrame(awrtc.ConnectionId.INVALID);
if (frame2 != null)
console.log("local2 width" + frame2.Width + " height:" + frame2.Height + " data:" + frame2.Buffer[0]);
while ((evt = network2.Dequeue()) != null) {
console.log("network2: " + evt.toString());
if (evt.Type == awrtc.NetEventType.NewConnection) {
remoteConId2 = evt.ConnectionId;
}
}
if (remoteConId2 != null) {
frame2 = network2.TryGetFrame(remoteConId2);
if (frame2 != null)
{
remoteFps.Update();
if(remoteFps.Counter % 30 == 0)
console.log("remote2 width" + frame2.Width + " height:" + frame2.Height + "fps: " + remoteFps.Fps + " data:" + frame2.Buffer[0]);
}
}
network2.Flush();
}, 10);
}
\ No newline at end of file
{
"extends": "../awrtc/tsconfig_base",
"compilerOptions": {
"declaration": false,
"target": "es6",
"module": "es2015",
"outDir": "../../build/apps",
"lib": [
"es2015",
"dom"
]
},
"files": [
"callapp.ts"
]
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//obsolete. not needed for unity build anymore
//special entry point only needed for backwards compatibility
//it will merge awrtc namespace into window so old code still works
//that accesses objects directly instead using the global awrtc object
//the index will include all external modules
import * as awrtc from "./index"
//we merge awrtc into the global namespace
Object.assign(window, awrtc);
//for less risky global access
(window as any).awrtc = awrtc;
console.debug("loading awrtc modules completed");
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
console.debug("loading awrtc modules ...");
//this should trigger webpack to include
//the webrtc adapter.js. It changes the
//global WebRTC calls and adds backwards
//and browser compatibility.
declare function require(moduleName: string)
let adapter = require("webrtc-adapter");
export * from "./network/index"
export * from "./media/index"
//for simplicity browser and unity are merged here
//it could as well be built and deployed separately
export * from "./media_browser/index"
export * from "./unity/index"
console.debug("loading awrtc modules completed");
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { ICall } from "./ICall";
import { IMediaNetwork, MediaConfigurationState, MediaEvent } from "./IMediaNetwork";
import { CallEventHandler, CallAcceptedEventArgs, CallEndedEventArgs, ErrorEventArgs, CallEventType, WaitForIncomingCallEventArgs, CallErrorType, DataMessageEventArgs, FrameUpdateEventArgs, CallEventArgs, MessageEventArgs, MediaUpdatedEventArgs } from "./CallEventArgs";
import { SLog, Encoding } from "../network/Helper";
import { NetworkConfig } from "./NetworkConfig";
import { MediaConfig } from "./MediaConfig";
import { ConnectionId, NetworkEvent, NetEventType } from "../network/index";
import { BrowserMediaNetwork } from "../media_browser/BrowserMediaNetwork";
import { IFrameData } from "./RawFrame";
class CallException {
private mErrorMsg: string;
public ErrorMsg() {
}
public constructor(errorMsg: string) {
this.mErrorMsg = errorMsg;
}
}
class InvalidOperationException extends CallException
{
}
/// <summary>
/// State of the call. Mainly used to check for bugs / invalid states.
/// </summary>
enum CallState {
/// <summary>
/// Not yet initialized / bug
/// </summary>
Invalid = 0,
/// <summary>
/// Object is initialized but local media not yet configured
/// </summary>
Initialized = 1,
/// <summary>
/// In process of accessing the local media devices.
/// </summary>
Configuring = 2,
/// <summary>
/// Configured. Video/Audio can be accessed and call is ready to start
/// </summary>
Configured = 3,
/// <summary>
/// In process of requesting an address from the server to then listen and wait for
/// an incoming call.
/// </summary>
RequestingAddress = 4,
/// <summary>
/// Call is listening on an address and waiting for an incoming call
/// </summary>
WaitingForIncomingCall = 5,
/// <summary>
/// Call is in the process of connecting to another call object.
/// </summary>
WaitingForOutgoingCall = 6,
/// <summary>
/// Indicating that the call object is at least connected to another object
/// </summary>
InCall = 7,
//CallAcceptedIncoming,
//CallAcceptedOutgoing,
/// <summary>
/// Call ended / conference room closed
/// </summary>
Closed = 8
}
/*
class ConnectionMetaData
{
}
*/
class ConnectionInfo{
private mConnectionIds = new Array<number>();
//private mConnectionMeta: { [id: number]: ConnectionMetaData } = {};
public AddConnection(id: ConnectionId, incoming:boolean)
{
this.mConnectionIds.push(id.id);
//this.mConnectionMeta[id.id] = new ConnectionMetaData();
}
public RemConnection(id:ConnectionId)
{
let index = this.mConnectionIds.indexOf(id.id);
if(index >= 0){
this.mConnectionIds.splice(index, 1);
}
else{
SLog.LE("tried to remove an unknown connection with id " + id.id);
}
//delete this.mConnectionMeta[id.id];
}
public HasConnection(id:ConnectionId)
{
return this.mConnectionIds.indexOf(id.id) != -1;
}
public GetIds()
{
return this.mConnectionIds;
}
//public GetMeta(id:ConnectionId) : ConnectionMetaData
//{
// return this.mConnectionMeta[id.id];
//}
}
/**This class wraps an implementation of
* IMediaStream and converts its polling system
* to an easier to use event based system.
*
* Ideally use only features defined by
* ICall to avoid having to deal with internal changes
* in future updates.
*/
export class AWebRtcCall implements ICall {
private MESSAGE_TYPE_INVALID : number = 0;
private MESSAGE_TYPE_DATA : number = 1;
private MESSAGE_TYPE_STRING : number = 2;
private MESSAGE_TYPE_CONTROL : number = 3;
protected mNetworkConfig = new NetworkConfig();
private mMediaConfig: MediaConfig = null;
private mCallEventHandlers: Array<CallEventHandler> = [];
public addEventListener(listener: CallEventHandler): void {
this.mCallEventHandlers.push(listener);
}
public removeEventListener(listener: CallEventHandler): void {
this.mCallEventHandlers = this.mCallEventHandlers.filter(h => h !== listener);
}
protected mNetwork: IMediaNetwork = null
private mConnectionInfo = new ConnectionInfo();
private mConferenceMode = false;
private mState = CallState.Invalid;
public get State(): CallState {
return this.mState;
}
private mIsDisposed = false;
private mServerInactive = true;
private mPendingListenCall = false;
private mPendingCallCall = false;
private mPendingAddress = null;
constructor(config: NetworkConfig = null) {
if (config != null) {
this.mNetworkConfig = config;
this.mConferenceMode = config.IsConference;
}
}
protected Initialize(network: IMediaNetwork): void {
this.mNetwork = network;
this.mState = CallState.Initialized;
}
public Configure(config: MediaConfig): void {
this.CheckDisposed();
if (this.mState != CallState.Initialized) {
throw new InvalidOperationException("Method can't be used in state " + this.mState);
}
this.mState = CallState.Configuring;
SLog.Log("Enter state CallState.Configuring");
this.mMediaConfig = config;
this.mNetwork.Configure(this.mMediaConfig);
}
public Call(address: string): void {
this.CheckDisposed();
if (this.mState != CallState.Initialized
&& this.mState != CallState.Configuring
&& this.mState != CallState.Configured) {
throw new InvalidOperationException("Method can't be used in state " + this.mState);
}
if (this.mConferenceMode) {
throw new InvalidOperationException("Method can't be used in conference calls.");
}
SLog.Log("Call to " + address);
this.EnsureConfiguration();
if (this.mState == CallState.Configured) {
this.ProcessCall(address);
}
else {
this.PendingCall(address);
}
}
public Listen(address: string): void {
this.CheckDisposed();
if (this.mState != CallState.Initialized
&& this.mState != CallState.Configuring
&& this.mState != CallState.Configured) {
throw new InvalidOperationException("Method can't be used in state " + this.mState);
}
this.EnsureConfiguration();
if (this.mState == CallState.Configured) {
this.ProcessListen(address);
}
else {
this.PendingListen(address);
}
}
public Send(message: string, reliable?:boolean, id? :ConnectionId): void
{
this.CheckDisposed();
if(reliable == null)
reliable = true;
if(id) {
this.InternalSendTo(message, reliable, id);
} else{
this.InternalSendToAll(message, reliable);
}
}
private InternalSendToAll(message: string, reliable:boolean): void {
let data = this.PackStringMsg(message);;
for (let id of this.mConnectionInfo.GetIds()) {
SLog.L("Send message to " + id + "! " + message);
this.InternalSendRawTo(data, new ConnectionId(id), reliable);
}
}
private InternalSendTo(message: string, reliable:boolean, id :ConnectionId): void {
let data = this.PackStringMsg(message);
this.InternalSendRawTo(data, id, reliable);
}
public SendData(message: Uint8Array, reliable:boolean, id :ConnectionId): void {
this.CheckDisposed();
let data = this.PackDataMsg(message);
this.InternalSendRawTo(data, id, reliable);
}
private PackStringMsg(message: string): Uint8Array {
let data = Encoding.UTF16.GetBytes(message);
let buff = new Uint8Array(data.length + 1);
buff[0] = this.MESSAGE_TYPE_STRING;
for(let i = 0; i < data.length; i++){
buff[i + 1] = data[i];
}
return buff;
}
private UnpackStringMsg(message: Uint8Array): string
{
let buff = new Uint8Array(message.length - 1);
for(let i = 0; i < buff.length; i++){
buff[i] = message[i + 1];
}
let res = Encoding.UTF16.GetString(buff);
return res;
}
private PackDataMsg(data: Uint8Array): Uint8Array {
let buff = new Uint8Array(data.length + 1);
buff[0] = this.MESSAGE_TYPE_DATA;
for(let i = 0; i < data.length; i++){
buff[i + 1] = data[i];
}
return buff;
}
private UnpackDataMsg(message: Uint8Array): Uint8Array
{
let buff = new Uint8Array(message.length - 1);
for(let i = 0; i < buff.length; i++){
buff[i] = message[i + 1];
}
return buff;
}
private InternalSendRawTo(rawdata: Uint8Array, id :ConnectionId, reliable: boolean) {
this.mNetwork.SendData(id, rawdata, reliable);
}
public Update(): void {
if (this.mIsDisposed)
return;
if (this.mNetwork == null)
return;
this.mNetwork.Update();
//waiting for the media configuration?
if (this.mState == CallState.Configuring) {
var configState = this.mNetwork.GetConfigurationState();
if (configState == MediaConfigurationState.Failed) {
this.OnConfigurationFailed(this.mNetwork.GetConfigurationError());
//bugfix: user might dispose the call during the event above
if (this.mIsDisposed)
return;
if (this.mNetwork != null)
this.mNetwork.ResetConfiguration();
}
else if (configState == MediaConfigurationState.Successful) {
this.OnConfigurationComplete();
if (this.mIsDisposed)
return;
}
}
let evt: NetworkEvent;
while ((evt = this.mNetwork.Dequeue()) != null) {
switch (evt.Type) {
case NetEventType.NewConnection:
if (this.mState == CallState.WaitingForIncomingCall
|| (this.mConferenceMode && this.mState == CallState.InCall)) //keep accepting connections after
{
//remove ability to accept incoming connections
if (this.mConferenceMode == false)
this.mNetwork.StopServer();
this.mState = CallState.InCall;
this.mConnectionInfo.AddConnection(evt.ConnectionId, true);
this.TriggerCallEvent(new CallAcceptedEventArgs(evt.ConnectionId));
if (this.mIsDisposed)
return;
}
else if (this.mState == CallState.WaitingForOutgoingCall) {
this.mConnectionInfo.AddConnection(evt.ConnectionId, false);
//only possible in 1 on 1 calls
this.mState = CallState.InCall;
this.TriggerCallEvent(new CallAcceptedEventArgs(evt.ConnectionId));
if (this.mIsDisposed)
return;
}
else {
//Debug.Assert(mState == CallState.WaitingForIncomingCall || mState == CallState.WaitingForOutgoingCall);
SLog.LogWarning("Received incoming connection during invalid state " + this.mState);
}
break;
case NetEventType.ConnectionFailed:
//call failed
if (this.mState == CallState.WaitingForOutgoingCall) {
this.TriggerCallEvent(new ErrorEventArgs(CallEventType.ConnectionFailed));
if (this.mIsDisposed)
return;
this.mState = CallState.Configured;
}
else {
//Debug.Assert(mState == CallState.WaitingForOutgoingCall);
SLog.LogError("Received ConnectionFailed during " + this.mState);
}
break;
case NetEventType.Disconnected:
if (this.mConnectionInfo.HasConnection(evt.ConnectionId)) {
this.mConnectionInfo.RemConnection(evt.ConnectionId);
//call ended
if (this.mConferenceMode == false && this.mConnectionInfo.GetIds().length == 0) {
this.mState = CallState.Closed;
}
this.TriggerCallEvent(new CallEndedEventArgs(evt.ConnectionId));
if (this.mIsDisposed)
return;
}
break;
case NetEventType.ServerInitialized:
//incoming calls possible
this.mServerInactive = false;
this.mState = CallState.WaitingForIncomingCall;
this.TriggerCallEvent(new WaitForIncomingCallEventArgs(evt.Info));
if (this.mIsDisposed)
return;
break;
case NetEventType.ServerInitFailed:
this.mServerInactive = true;
//reset state to the earlier state which is Configured (as without configuration no
//listening possible). Local camera/audio will keep running
this.mState = CallState.Configured;
this.TriggerCallEvent(new ErrorEventArgs(CallEventType.ListeningFailed));
if (this.mIsDisposed)
return;
break;
case NetEventType.ServerClosed:
this.mServerInactive = true;
//no incoming calls possible anymore
if (this.mState == CallState.WaitingForIncomingCall || this.mState == CallState.RequestingAddress) {
this.mState = CallState.Configured;
//might need to be handled as a special timeout event?
this.TriggerCallEvent(new ErrorEventArgs(CallEventType.ListeningFailed, CallErrorType.Unknown, "Server closed the connection while waiting for incoming calls."));
if (this.mIsDisposed)
return;
}
else {
//event is normal during other states as the server connection will be closed after receiving a call
}
break;
case NetEventType.ReliableMessageReceived:
case NetEventType.UnreliableMessageReceived:
let reliable = evt.Type === NetEventType.ReliableMessageReceived;
//chat message received
if(evt.MessageData.length >= 2)
{
if(evt.MessageData[0] == this.MESSAGE_TYPE_STRING)
{
let message = this.UnpackStringMsg(evt.MessageData);
this.TriggerCallEvent(new MessageEventArgs(evt.ConnectionId, message, reliable));
}else if(evt.MessageData[0] == this.MESSAGE_TYPE_DATA)
{
let message = this.UnpackDataMsg(evt.MessageData);
this.TriggerCallEvent(new DataMessageEventArgs(evt.ConnectionId, message, reliable));
}else{
//invalid message?
}
}else{
//invalid message?
}
if (this.mIsDisposed)
return;
break;
}
}
let handleLocalFrames = true;
let handleRemoteFrames = true;
if (this.mMediaConfig.FrameUpdates && handleLocalFrames)
{
let localFrame = this.mNetwork.TryGetFrame(ConnectionId.INVALID);
if (localFrame != null) {
this.FrameToCallEvent(ConnectionId.INVALID, localFrame);
if (this.mIsDisposed)
return;
}
}
if (this.mMediaConfig.FrameUpdates && handleRemoteFrames)
{
for (var id of this.mConnectionInfo.GetIds())
{
let conId = new ConnectionId(id);
let remoteFrame = this.mNetwork.TryGetFrame(conId);
if (remoteFrame != null) {
this.FrameToCallEvent(conId, remoteFrame);
if (this.mIsDisposed)
return;
}
}
}
let mediaEvent : MediaEvent= null;
while((mediaEvent = this.mNetwork.DequeueMediaEvent()) != null)
{
this.MediaEventToCallEvent(mediaEvent);
}
this.mNetwork.Flush();
}
private FrameToCallEvent(id:ConnectionId, frame:IFrameData)
{
let args = new FrameUpdateEventArgs(id, frame);
this.TriggerCallEvent(args);
}
private MediaEventToCallEvent(evt: MediaEvent)
{
let videoElement : HTMLVideoElement = null;
if(evt.EventType == evt.EventType)
{
let args = new MediaUpdatedEventArgs(evt.ConnectionId, evt.Args as HTMLVideoElement);
this.TriggerCallEvent(args);
}
}
private PendingCall(address: string): void {
this.mPendingAddress = address;
this.mPendingCallCall = true;
this.mPendingListenCall = false;
}
private ProcessCall(address: string): void {
this.mState = CallState.WaitingForOutgoingCall;
this.mNetwork.Connect(address);
this.ClearPending();
}
private PendingListen(address: string): void {
this.mPendingAddress = address;
this.mPendingCallCall = false;
this.mPendingListenCall = true;
}
private ProcessListen(address: string): void{
SLog.Log("Listen at " + address);
this.mServerInactive = false;
this.mState = CallState.RequestingAddress;
this.mNetwork.StartServer(address);
this.ClearPending();
}
private DoPending(): void
{
if (this.mPendingCallCall) {
this.ProcessCall(this.mPendingAddress);
} else if (this.mPendingListenCall) {
this.ProcessListen(this.mPendingAddress);
}
this.ClearPending();
}
private ClearPending(): void {
this.mPendingAddress = null;
this.mPendingCallCall = null;
this.mPendingListenCall = null;
}
private CheckDisposed():void
{
if (this.mIsDisposed)
throw new InvalidOperationException("Object is disposed. No method calls possible.");
}
private EnsureConfiguration(): void {
if (this.mState == CallState.Initialized) {
SLog.Log("Use default configuration");
this.Configure(new MediaConfig());
}
else {
}
}
private TriggerCallEvent(args: CallEventArgs): void {
let arr = this.mCallEventHandlers.slice();
for (let callback of arr) {
callback(this, args);
}
}
private OnConfigurationComplete(): void {
if (this.mIsDisposed)
return;
this.mState = CallState.Configured;
SLog.Log("Enter state CallState.Configured");
this.TriggerCallEvent(new CallEventArgs(CallEventType.ConfigurationComplete));
if (this.mIsDisposed == false)
this.DoPending();
}
private OnConfigurationFailed(error: string): void {
SLog.LogWarning("Configuration failed: " + error);
if (this.mIsDisposed)
return;
this.mState = CallState.Initialized;
this.TriggerCallEvent(new ErrorEventArgs(CallEventType.ConfigurationFailed, CallErrorType.Unknown, error));
//bugfix: user might dispose the call during the event above
if (this.mIsDisposed == false)
this.ClearPending();
}
protected DisposeInternal(disposing: boolean): void {
//nothing to dispose but subclasses overwrite this
if (!this.mIsDisposed) {
if (disposing) {
}
this.mIsDisposed = true;
}
}
public Dispose() : void
{
this.DisposeInternal(true);
}
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { ConnectionId } from "../network/index";
import { IFrameData } from "./RawFrame";
export interface CallEventHandler {
(sender: any, args: CallEventArgs): void;
}
/// <summary>
/// Type of the event.
/// </summary>
export enum CallEventType {
/// <summary>
/// Used if the event value wasn't initialized
/// </summary>
Invalid = 0,
/// <summary>
/// The call object is successfully connected to the server waiting for another user
/// to connect.
/// </summary>
WaitForIncomingCall = 1,
/// <summary>
/// A call was accepted
/// </summary>
CallAccepted = 2,
/// <summary>
/// The call ended
/// </summary>
CallEnded = 3,
/**
* Backwards compatibility. Use MediaUpdate
*/
FrameUpdate = 4,
/// <summary>
/// Text message arrived
/// </summary>
Message = 5,
/// <summary>
/// Connection failed. Might be due to an server, network error or the address didn't exist
/// Using ErrorEventArgs
/// </summary>
ConnectionFailed = 6,
/// <summary>
/// Listening failed. Address might be in use or due to server/network error
/// Using ErrorEventArgs
/// </summary>
ListeningFailed = 7,
/// <summary>
/// Event triggered after the local media was successfully configured.
/// If requested the call object will have access to the users camera and/or audio now and
/// the local camera frames can be received in events.
/// </summary>
ConfigurationComplete = 8,
/// <summary>
/// Configuration failed. This happens if the configuration requested features
/// the system doesn't support e.g. no camera, camera doesn't support the requested resolution
/// or the user didn't allow the website to access the camera/microphone in WebGL mode.
/// </summary>
ConfigurationFailed = 9,
/// <summary>
/// Reliable or unreliable data msg arrived
/// </summary>
DataMessage = 10,
/**
*
*/
MediaUpdate = 20,
}
export class CallEventArgs {
private mType = CallEventType.Invalid;
public get Type(): CallEventType {
return this.mType;
}
public constructor(type: CallEventType) {
this.mType = type;
}
}
export class CallAcceptedEventArgs extends CallEventArgs
{
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
public constructor(connectionId: ConnectionId) {
super(CallEventType.CallAccepted);
this.mConnectionId = connectionId;
}
}
export class CallEndedEventArgs extends CallEventArgs {
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId{
return this.mConnectionId;
}
public constructor(connectionId: ConnectionId) {
super(CallEventType.CallEnded);
this.mConnectionId = connectionId;
}
}
export enum CallErrorType {
Unknown
}
export class ErrorEventArgs extends CallEventArgs {
private mErrorMessage: string;
public get ErrorMessage(): string {
return this.mErrorMessage;
}
private mErrorType: CallErrorType = CallErrorType.Unknown;
public get ErrorType(): CallErrorType {
return this.mErrorType;
}
public constructor(eventType: CallEventType, type?: CallErrorType, errorMessage?: string) {
super(eventType);
this.mErrorType = type;
this.mErrorMessage = errorMessage;
if (this.mErrorMessage == null) {
switch (eventType) {
//use some generic error messages as the underlaying system doesn't report the errors yet.
case CallEventType.ConnectionFailed:
this.mErrorMessage = "Connection failed.";
break;
case CallEventType.ListeningFailed:
this.mErrorMessage = "Failed to allow incoming connections. Address already in use or server connection failed.";
break;
default:
this.mErrorMessage = "Unknown error.";
break;
}
}
}
}
export class WaitForIncomingCallEventArgs extends CallEventArgs
{
private mAddress: string;
public get Address(): string {
return this.mAddress;
}
public constructor(address: string) {
super(CallEventType.WaitForIncomingCall);
this.mAddress = address;
}
}
export class MessageEventArgs extends CallEventArgs {
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
private mContent: string;
public get Content(): string {
return this.mContent;
}
private mReliable: boolean;
public get Reliable(): boolean {
return this.mReliable;
}
public constructor(id: ConnectionId, message: string, reliable: boolean) {
super(CallEventType.Message);
this.mConnectionId = id;
this.mContent = message;
this.mReliable = reliable;
}
}
export class DataMessageEventArgs extends CallEventArgs {
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
private mContent: Uint8Array;
public get Content(): Uint8Array {
return this.mContent;
}
private mReliable: boolean;
public get Reliable(): boolean {
return this.mReliable;
}
public constructor(id: ConnectionId, message: Uint8Array, reliable: boolean) {
super(CallEventType.DataMessage);
this.mConnectionId = id;
this.mContent = message;
this.mReliable = reliable;
}
}
/**
* Replaces the FrameUpdateEventArgs. Instead of
* giving access to video frames only this gives access to
* video html tag once it is created.
* TODO: Add audio + video tracks + flag that indicates added, updated or removed
* after renegotiation is added.
*/
export class MediaUpdatedEventArgs extends CallEventArgs
{
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
/// <summary>
/// False if the frame is from a local camera. True if it is received from
/// via network.
/// </summary>
public get IsRemote(): boolean {
return this.mConnectionId.id != ConnectionId.INVALID.id;
}
private mVideoElement:HTMLVideoElement;
public get VideoElement():HTMLVideoElement
{
return this.mVideoElement;
}
public constructor(conId: ConnectionId, videoElement:HTMLVideoElement)
{
super(CallEventType.MediaUpdate);
this.mConnectionId = conId;
this.mVideoElement = videoElement;
}
}
/// <summary>
/// Will be replaced with MediaUpdatedEventArgs.
/// It doesn't make a lot of sense in HTML only
/// </summary>
export class FrameUpdateEventArgs extends CallEventArgs {
private mFrame: IFrameData;
/// <summary>
/// Raw image data. Note that the byte array contained in RawFrame will be reused
/// for the next frames received. Only valid until the next call of ICall.Update
/// </summary>
public get Frame(): IFrameData {
return this.mFrame;
}
private mConnectionId: ConnectionId = ConnectionId.INVALID;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
/// <summary>
/// False if the frame is from a local camera. True if it is received from
/// via network.
/// </summary>
public get IsRemote(): boolean {
return this.mConnectionId.id != ConnectionId.INVALID.id;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="conId"></param>
/// <param name="frame"></param>
public constructor(conId: ConnectionId, frame: IFrameData)
{
super(CallEventType.FrameUpdate);
this.mConnectionId = conId;
this.mFrame = frame;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { CallEventHandler } from "./CallEventArgs";
import { ConnectionId } from "../network/index";
import { MediaConfig } from "./MediaConfig";
/** Mostly the same as the C# side ICall interface.
*
* Usage of this interface usually follows a specific pattern:
* 1. Create a platform specific instance via a factory with a specific
* NetworkConfig
* 2. Register an event handler at CallEvent and call Update regularly
* (ideally once for each frame shown to the user in realtime
* applcations so 30-60 times per second)
* 3. Call configure with your own MediaConfig instance defining what
* features you need.
* 4. Wait for a ConfigurationComplete (or failed) event. During this
* time the platform might ask the user the allow access to the devices.
* 5. Either call Listen with an address to wait for an incoming connection
* or use Call to conect another ICall that already listens on that address.
* 6. Wait for CallAccepted and other events. The call is now active and
* you can use Send messages, change volume, ...
* 7. Call Dispose to cleanup
*
* Do not forget to call Dispose method after you finished the call or the connection
* might run forever in the background!
*
* See example apps and guides for more information.
*/
export interface ICall {
addEventListener(listener: CallEventHandler): void;
removeEventListener(listener: CallEventHandler): void;
Call(address: string): void;
Configure(config: MediaConfig): void;
Listen(address: string): void;
Send(message: string, reliable?:boolean, id? :ConnectionId): void
SendData(message: Uint8Array, reliable: boolean, id: ConnectionId): void
Update(): void;
Dispose(): void;
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { ConnectionId, IWebRtcNetwork } from "../network/index";
import { MediaConfig } from "./MediaConfig";
import { IFrameData } from "./RawFrame";
export enum MediaConfigurationState {
Invalid = 0,
NoConfiguration = 1,
InProgress = 2,
Successful = 3,
Failed = 4
}
export enum MediaEventType
{
Invalid = 0,
StreamAdded = 20
}
/**
* Will replace frame event / configuration system in the future.
*
* So far it only delivers HTMLVideoElements once connection and
* all tracks are ready and it plays.
*
* This is all temporary and will be updated soon to handle
* all events from configuration of local streams to frame updates and
* renegotation.
*
*/
export class MediaEvent
{
private mEventType = MediaEventType.Invalid;
public get EventType()
{
return this.mEventType;
}
private mConnectionId = ConnectionId.INVALID;
public get ConnectionId()
{
return this.mConnectionId;
}
private mArgs:any;
public get Args():any
{
return this.mArgs;
}
public constructor(type:MediaEventType, id: ConnectionId, args:any)
{
this.mEventType = type;
this.mConnectionId = id;
this.mArgs = args;
}
}
/**Interface adds media functionality to IWebRtcNetwork.
* It is used to ensure compatibility to all other platforms.
*/
export interface IMediaNetwork extends IWebRtcNetwork
{
Configure(config: MediaConfig): void;
GetConfigurationState(): MediaConfigurationState;
GetConfigurationError(): string;
ResetConfiguration(): void;
TryGetFrame(id: ConnectionId): IFrameData;
PeekFrame(id: ConnectionId): IFrameData;
SetVolume(volume: number, id: ConnectionId): void;
HasAudioTrack(id: ConnectionId): boolean;
HasVideoTrack(id: ConnectionId): boolean;
//Only used for browser specific events for now
//Not part of the C# api yet.
DequeueMediaEvent(): MediaEvent;
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/// <summary>
/// Configuration for the WebRtcCall class.
///
/// Allows to turn on / off video and audio + configure the used servers to initialize the connection and
/// avoid firewalls.
/// </summary>
export class MediaConfig {
private mAudio: boolean = true;
public get Audio(): boolean {
return this.mAudio;
}
public set Audio(value: boolean) {
this.mAudio = value;
}
private mVideo: boolean = true;
public get Video(): boolean {
return this.mVideo;
}
public set Video(value: boolean) {
this.mVideo = value;
}
private mVideoDeviceName : string = "";
public get VideoDeviceName(): string {
return this.mVideoDeviceName;
}
public set VideoDeviceName(value: string) {
this.mVideoDeviceName = value;
}
private mMinWidth = -1;
public get MinWidth(): number {
return this.mMinWidth;
}
public set MinWidth(value: number) {
this.mMinWidth = value;
}
private mMinHeight = -1;
public get MinHeight(): number {
return this.mMinHeight;
}
public set MinHeight(value: number) {
this.mMinHeight = value;
}
private mMaxWidth = -1;
public get MaxWidth(): number {
return this.mMaxWidth;
}
public set MaxWidth(value: number) {
this.mMaxWidth = value;
}
private mMaxHeight = -1;
public get MaxHeight(): number {
return this.mMaxHeight;
}
public set MaxHeight(value: number) {
this.mMaxHeight = value;
}
private mIdealWidth = -1;
public get IdealWidth(): number {
return this.mIdealWidth;
}
public set IdealWidth(value: number) {
this.mIdealWidth = value;
}
private mIdealHeight = -1;
public get IdealHeight(): number {
return this.mIdealHeight;
}
public set IdealHeight(value: number) {
this.mIdealHeight = value;
}
private mMinFps = -1;
public get MinFps(): number {
return this.mMinFps;
}
public set MinFps(value: number) {
this.mMinFps = value;
}
private mMaxFps = -1;
public get MaxFps(): number {
return this.mMaxFps;
}
public set MaxFps(value: number) {
this.mMaxFps = value;
}
private mIdealFps = -1;
public get IdealFps(): number {
return this.mIdealFps;
}
public set IdealFps(value: number) {
this.mIdealFps = value;
}
private mFrameUpdates = false;
/** false - frame updates aren't generated. Useful for browser mode
* true - library will deliver frames as ByteArray
*/
public get FrameUpdates(): boolean {
return this.mFrameUpdates;
}
public set FrameUpdates(value: boolean) {
this.mFrameUpdates = value;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export class NetworkConfig {
private mIceServers = new Array<RTCIceServer>();
public get IceServers(): RTCIceServer[] {
return this.mIceServers;
}
public set IceServers(value: RTCIceServer[]) {
this.mIceServers = value;
}
private mSignalingUrl = "ws://because-why-not.com:12776";
public get SignalingUrl() {
return this.mSignalingUrl;
}
public set SignalingUrl(value: string) {
this.mSignalingUrl = value;
}
private mIsConference = false;
public get IsConference(): boolean {
return this.mIsConference;
}
public set IsConference(value:boolean) {
this.mIsConference = value;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { SLog } from "../network/Helper";
import { BrowserMediaStream } from "../media_browser/index";
export enum FramePixelFormat {
Invalid = 0,
Format32bppargb
}
//replace with interface after typescript 2.0 update (properties in interfaces aren't supported yet)
export class IFrameData {
public get Format(): FramePixelFormat{
return FramePixelFormat.Format32bppargb;
}
public get Buffer(): Uint8Array {
return null;
}
public get Width(): number {
return -1;
}
public get Height(): number {
return -1;
}
public constructor() { }
}
//Container for the raw bytes of the current frame + height and width.
//Format is currently fixed based on the browser getImageData format
export class RawFrame extends IFrameData{
private mBuffer: Uint8Array = null;
public get Buffer(): Uint8Array {
return this.mBuffer;
}
private mWidth: number;
public get Width(): number {
return this.mWidth;
}
private mHeight: number;
public get Height(): number {
return this.mHeight;
}
constructor(buffer: Uint8Array, width: number, height: number) {
super();
this.mBuffer = buffer;
this.mWidth = width;
this.mHeight = height;
}
}
/**
* This class is suppose to increase the speed of the java script implementation.
* Instead of creating RawFrames every Update call (because the real fps are unknown currently) it will
* only create a lazy frame which will delay the creation of the RawFrame until the user actually tries
* to access any data.
* Thus if the game slows down or the user doesn't access any data the expensive copy is avoided.
*/
export class LazyFrame extends IFrameData{
private mFrameGenerator: BrowserMediaStream;
public get FrameGenerator() {
return this.mFrameGenerator;
}
private mRawFrame: RawFrame;
public get Buffer(): Uint8Array {
this.GenerateFrame();
if (this.mRawFrame == null)
return null;
return this.mRawFrame.Buffer;
}
public get Width(): number {
this.GenerateFrame();
if (this.mRawFrame == null)
return -1;
return this.mRawFrame.Width;
}
public get Height(): number {
this.GenerateFrame();
if (this.mRawFrame == null)
return -1;
return this.mRawFrame.Height;
}
constructor(frameGenerator: BrowserMediaStream) {
super();
this.mFrameGenerator = frameGenerator;
}
//Called before access of any frame data triggering the creation of the raw frame data
private GenerateFrame() {
if (this.mRawFrame == null) {
try {
this.mRawFrame = this.mFrameGenerator.CreateFrame();
} catch (exception) {
this.mRawFrame = null;
SLog.LogWarning("frame skipped in GenerateFrame due to exception: " + JSON.stringify(exception));
}
}
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from './AWebRtcCall'
export * from './CallEventArgs'
export * from './ICall'
export * from './IMediaNetwork'
export * from './MediaConfig'
export * from './NetworkConfig'
export * from './RawFrame'
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { WebRtcNetwork, SLog, ConnectionId, SignalingConfig, IBasicNetwork, LocalNetwork, WebsocketNetwork, WebRtcDataPeer, Queue }
from "../network/index";
import { IMediaNetwork, MediaConfigurationState, MediaEvent, MediaEventType } from "../media/IMediaNetwork";
import { NetworkConfig } from "../media/NetworkConfig";
import { MediaConfig } from "../media/MediaConfig";
import { IFrameData } from "../media/RawFrame";
import { MediaPeer } from "./MediaPeer";
import { BrowserMediaStream } from "./BrowserMediaStream";
import { DeviceApi } from "./DeviceApi";
/**Avoid using this class directly whenever possible. Use BrowserWebRtcCall instead.
* BrowserMediaNetwork might be subject to frequent changes to keep up with changes
* in all other platforms.
*
* IMediaNetwork implementation for the browser. The class is mostly identical with the
* C# version. Main goal is to have an interface that can easily be wrapped to other
* programming languages and gives access to basic WebRTC features such as receiving
* and sending audio and video + signaling via websockets.
*
* BrowserMediaNetwork can be used to stream a local audio and video track to a group of
* multiple peers and receive remote tracks. The handling of the peers itself
* remains the same as WebRtcNetwork.
* Local tracks are created after calling Configure. This will request access from the
* user. After the user allowed access GetConfigurationState will return Configured.
* Every incoming and outgoing peer that is established after this will receive
* the local audio and video track.
* So far Configure can only be called once before any peers are connected.
*
*
*/
export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork {
//media configuration set by the user
private mMediaConfig: MediaConfig = null;
//keeps track of audio / video tracks based on local devices
//will be shared with all connected peers.
private mLocalStream: BrowserMediaStream = null;
private mConfigurationState: MediaConfigurationState = MediaConfigurationState.Invalid;
private mConfigurationError: string = null;
private mMediaEvents: Queue<MediaEvent> = new Queue<MediaEvent>();
constructor(config: NetworkConfig) {
super(BrowserMediaNetwork.BuildSignalingConfig(config.SignalingUrl),
BrowserMediaNetwork.BuildRtcConfig(config.IceServers));
this.mConfigurationState = MediaConfigurationState.NoConfiguration;
}
/**Triggers the creation of a local audio and video track. After this
* call the user might get a request to allow access to the requested
* devices.
*
* @param config Detail configuration for audio/video devices.
*/
public Configure(config: MediaConfig): void {
this.mMediaConfig = config;
this.mConfigurationError = null;
this.mConfigurationState = MediaConfigurationState.InProgress;
if (config.Audio || config.Video) {
//ugly part starts -> call get user media data (no typescript support)
//different browsers have different calls...
//check getSupportedConstraints()???
//see https://w3c.github.io/mediacapture-main/getusermedia.html#constrainable-interface
//set default ideal to very common low 320x240 to avoid overloading weak computers
var constraints = {
audio: config.Audio
} as any;
let width = {} as any;
let height = {} as any;
let video = {} as any;
let fps = {} as any;
if (config.MinWidth != -1)
width.min = config.MinWidth;
if (config.MaxWidth != -1)
width.max = config.MaxWidth;
if (config.IdealWidth != -1)
width.ideal = config.IdealWidth;
if (config.MinHeight != -1)
height.min = config.MinHeight;
if (config.MaxHeight != -1)
height.max = config.MaxHeight;
if (config.IdealHeight != -1)
height.ideal = config.IdealHeight;
if (config.MinFps != -1)
fps.min = config.MinFps;
if (config.MaxFps != -1)
fps.max = config.MaxFps;
if (config.IdealFps != -1)
fps.ideal = config.IdealFps;
//user requested specific device? get it now to properly add it to the
//constraints alter
let deviceId:string = null;
if(config.Video && config.VideoDeviceName && config.VideoDeviceName !== "")
{
deviceId = DeviceApi.GetDeviceId(config.VideoDeviceName);
SLog.L("using device " + config.VideoDeviceName);
if(deviceId !== null)
{
//SLog.L("using device id " + deviceId);
}
else{
SLog.LE("Failed to find deviceId for label " + config.VideoDeviceName);
}
}
//watch out: unity changed behaviour and will now
//give 0 / 1 instead of false/true
//using === won't work
if(config.Video == false)
{
//video is off
video = false;
}else {
if(Object.keys(width).length > 0){
video.width = width;
}
if(Object.keys(height).length > 0){
video.height = height;
}
if(Object.keys(fps).length > 0){
video.frameRate = fps;
}
if(deviceId !== null){
video.deviceId = {"exact":deviceId};
}
//if we didn't add anything we need to set it to true
//at least (I assume?)
if(Object.keys(video).length == 0){
video = true;
}
}
constraints.video = video;
SLog.L("calling GetUserMedia. Media constraints: " + JSON.stringify(constraints));
let promise = navigator.mediaDevices.getUserMedia(constraints);
promise.then((stream) => { //user gave permission
//totally unrelated -> user gave access to devices. use this
//to get the proper names for our DeviceApi
DeviceApi.Update();
//call worked -> setup a frame buffer that deals with the rest
this.mLocalStream = new BrowserMediaStream(stream as MediaStream);
this.mLocalStream.InternalStreamAdded = (stream)=>{
this.EnqueueMediaEvent(MediaEventType.StreamAdded, ConnectionId.INVALID, this.mLocalStream.VideoElement);
};
//unlike native version this one will happily play the local sound causing an echo
//set to mute
this.mLocalStream.SetMute(true);
this.OnConfigurationSuccess();
});
promise.catch((err)=> {
//failed due to an error or user didn't give permissions
SLog.LE(err.name + ": " + err.message);
this.OnConfigurationFailed(err.message);
});
} else {
this.OnConfigurationSuccess();
}
}
/**Call this every time a new frame is shown to the user in realtime
* applications.
*
*/
public Update(): void {
super.Update();
if (this.mLocalStream != null)
this.mLocalStream.Update();
}
private EnqueueMediaEvent(type: MediaEventType, id:ConnectionId, args: HTMLVideoElement)
{
let evt = new MediaEvent(type, id, args);
this.mMediaEvents.Enqueue(evt);
}
public DequeueMediaEvent(): MediaEvent
{
return this.mMediaEvents.Dequeue();
}
/**
* Call this every frame after interacting with this instance.
*
* This call might flush buffered messages in the future and clear
* events that the user didn't process to avoid buffer overflows.
*
*/
public Flush():void{
super.Flush();
this.mMediaEvents.Clear();
}
/**Poll this after Configure is called to get the result.
* Won't change after state is Configured or Failed.
*
*/
public GetConfigurationState(): MediaConfigurationState {
return this.mConfigurationState;
}
/**Returns the error message if the configure process failed.
* This usally either happens because the user refused access
* or no device fulfills the configuration given
* (e.g. device doesn't support the given resolution)
*
*/
public GetConfigurationError(): string {
return this.mConfigurationError;
}
/**Resets the configuration state to allow multiple attempts
* to call Configure.
*
*/
public ResetConfiguration(): void {
this.mConfigurationState = MediaConfigurationState.NoConfiguration;
this.mMediaConfig = new MediaConfig();
this.mConfigurationError = null;
}
private OnConfigurationSuccess(): void {
this.mConfigurationState = MediaConfigurationState.Successful;
}
private OnConfigurationFailed(error: string): void {
this.mConfigurationError = error;
this.mConfigurationState = MediaConfigurationState.Failed;
}
/**Allows to peek at the current frame.
* Added to allow the emscripten C / C# side to allocate memory before
* actually getting the frame.
*
* @param id
*/
public PeekFrame(id: ConnectionId): IFrameData {
if (id == null)
return;
if (id.id == ConnectionId.INVALID.id) {
if (this.mLocalStream != null) {
return this.mLocalStream.PeekFrame();
}
} else {
let peer = this.IdToConnection[id.id] as MediaPeer;
if (peer != null) {
return peer.PeekFrame();
}
//TODO: iterate over media peers and do the same as above
}
return null;
}
public TryGetFrame(id: ConnectionId): IFrameData {
if (id == null)
return;
if (id.id == ConnectionId.INVALID.id) {
if (this.mLocalStream != null) {
return this.mLocalStream.TryGetFrame();
}
} else {
let peer = this.IdToConnection[id.id] as MediaPeer;
if (peer != null) {
return peer.TryGetRemoteFrame();
}
//TODO: iterate over media peers and do the same as above
}
return null;
}
/**
* Remote audio control for each peer.
*
* @param volume 0 - mute and 1 - max volume
* @param id peer id
*/
public SetVolume(volume: number, id: ConnectionId): void {
SLog.L("SetVolume called. Volume: " + volume + " id: " + id.id);
let peer = this.IdToConnection[id.id] as MediaPeer;
if (peer != null) {
return peer.SetVolume(volume);
}
}
/** Allows to check if a specific peer has a remote
* audio track attached.
*
* @param id
*/
public HasAudioTrack(id: ConnectionId): boolean {
let peer = this.IdToConnection[id.id] as MediaPeer;
if (peer != null) {
return peer.HasAudioTrack();
}
return false;
}
/** Allows to check if a specific peer has a remote
* video track attached.
*
* @param id
*/
public HasVideoTrack(id: ConnectionId): boolean {
let peer = this.IdToConnection[id.id] as MediaPeer;
if (peer != null) {
return peer.HasVideoTrack();
}
return false;
}
/**Returns true if no local audio available or it is muted.
* False if audio is available (could still not work due to 0 volume, hardware
* volume control or a dummy audio input device is being used)
*/
public IsMute(): boolean {
if(this.mLocalStream != null && this.mLocalStream.Stream != null)
{
var stream = this.mLocalStream.Stream;
var tracks = stream.getAudioTracks();
if(tracks.length > 0)
{
if(tracks[0].enabled)
return false;
}
}
return true;
}
/**Sets the local audio device to mute / unmute it.
*
* @param value
*/
public SetMute(value: boolean){
if(this.mLocalStream != null && this.mLocalStream.Stream != null)
{
var stream = this.mLocalStream.Stream;
var tracks = stream.getAudioTracks();
if(tracks.length > 0)
{
tracks[0].enabled = !value;
}
}
}
protected CreatePeer(peerId: ConnectionId, lRtcConfig: RTCConfiguration): WebRtcDataPeer {
let peer = new MediaPeer(peerId, lRtcConfig);
peer.InternalStreamAdded = this.MediaPeer_InternalMediaStreamAdded;
if (this.mLocalStream != null)
peer.AddLocalStream(this.mLocalStream.Stream);
return peer;
}
private MediaPeer_InternalMediaStreamAdded = (peer: MediaPeer, stream:BrowserMediaStream):void =>
{
this.EnqueueMediaEvent(MediaEventType.StreamAdded, peer.ConnectionId, stream.VideoElement);
}
protected DisposeInternal(): void
{
super.DisposeInternal();
this.DisposeLocalStream();
}
private DisposeLocalStream(): void
{
if (this.mLocalStream != null) {
this.mLocalStream.Dispose();
this.mLocalStream = null;
SLog.L("local buffer disposed");
}
}
private static BuildSignalingConfig(signalingUrl: string): SignalingConfig {
let signalingNetwork: IBasicNetwork;
if (signalingUrl == null || signalingUrl == "") {
signalingNetwork = new LocalNetwork();
} else {
signalingNetwork = new WebsocketNetwork(signalingUrl);
}
return new SignalingConfig(signalingNetwork);
}
private static BuildRtcConfig(servers: RTCIceServer[]): RTCConfiguration{
let rtcConfig: RTCConfiguration = { iceServers: servers};
return rtcConfig;
}
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { IFrameData, RawFrame, LazyFrame } from "../media/RawFrame";
import { SLog } from "../network/Helper";
/**Internal use only.
* Bundles all functionality related to MediaStream, Tracks and video processing.
* It creates two HTML elements: Video and Canvas to interact with the video stream
* and convert the visible frame data to Uint8Array for compatibility with the
* unity plugin and all other platforms.
*
*/
export class BrowserMediaStream {
//no double buffering in java script as it forces us to create a new frame each time
//for debugging. Will attach the HTMLVideoElement used to play the local and remote
//video streams to the document.
public static DEBUG_SHOW_ELEMENTS = false;
//TODO: remove this flag. it is now always using lazy frames
public static sUseLazyFrames = true;
//Gives each FrameBuffer and its HTMLVideoElement a fixed id for debugging purposes.
public static sNextInstanceId = 1;
private mStream: MediaStream;
public get Stream() {
return this.mStream;
}
private mBufferedFrame: IFrameData = null;
private mInstanceId = 0;
private mVideoElement: HTMLVideoElement;
public get VideoElement() {
return this.mVideoElement;
}
private mCanvasElement: HTMLCanvasElement = null;
private mIsActive = false;
//Framerate used as a workaround if
//the actual framerate is unknown due to browser restrictions
public static DEFAULT_FRAMERATE = 25;
private mMsPerFrame = 1.0 / BrowserMediaStream.DEFAULT_FRAMERATE * 1000;
private mFrameRateKnown = false;
//Time the last frame was generated
private mLastFrameTime = 0;
/** Number of the last frame (not yet supported in all browsers)
* if it remains at <= 0 then we just generate frames based on
* the timer above
*/
private mLastFrameNumber = 0;
private mHasVideo: boolean = false;
public InternalStreamAdded: (stream: BrowserMediaStream) => void = null;
constructor(stream: MediaStream) {
this.mStream = stream;
this.mInstanceId = BrowserMediaStream.sNextInstanceId;
BrowserMediaStream.sNextInstanceId++;
if (this.mStream.getVideoTracks().length > 0)
{
this.mHasVideo = true;
let vtrack = this.mStream.getVideoTracks()[0];
let settings = vtrack.getSettings();
let fps = settings.frameRate;
if(fps)
{
this.mMsPerFrame = 1.0 / fps * 1000;
this.mFrameRateKnown = true;
}
}
this.SetupElements();
}
private CheckFrameRate():void
{
//in chrome the track itself might miss the framerate but
//we still know when it updates trough webkitDecodedFrameCount
if(this.mVideoElement && typeof (this.mVideoElement as any).webkitDecodedFrameCount !== "undefined")
{
this.mFrameRateKnown = true;
}
if(this.mFrameRateKnown === false)
{
//firefox and co won't tell us the FPS for remote stream
SLog.LW("Framerate unknown. Using default framerate of " + BrowserMediaStream.DEFAULT_FRAMERATE);
}
}
public SetupElements() {
this.mVideoElement = this.SetupVideoElement();
//TOOD: investigate bug here
//In some cases onloadedmetadata is never called. This might happen due to a
//bug in firefox or might be related to a device / driver error
//So far it only happens randomly (maybe 1 in 10 tries) on a single test device and only
//with 720p. (video device "BisonCam, NB Pro" on MSI laptop)
SLog.L("video element created. video tracks: " + this.mStream.getVideoTracks().length);
this.mVideoElement.onloadedmetadata = (e) => {
this.mVideoElement.play();
if(this.InternalStreamAdded != null)
this.InternalStreamAdded(this);
this.CheckFrameRate();
SLog.L("Resolution: " + this.mVideoElement.videoWidth + "x" + this.mVideoElement.videoHeight);
//now create canvas after the meta data of the video are known
if (this.mHasVideo) {
this.mCanvasElement = this.SetupCanvas();
//canvas couldn't be created. set video to false
if (this.mCanvasElement == null)
this.mHasVideo = false;
} else {
this.mCanvasElement = null;
}
this.mIsActive = true;
};
//set the src value and trigger onloadedmetadata above
try {
//newer method. not yet supported everywhere
let element : any = this.mVideoElement;
element.srcObject = this.mStream;
}
catch (error)
{
//old way of doing it. won't work anymore in firefox and possibly other browsers
this.mVideoElement.src = window.URL.createObjectURL(this.mStream);
}
}
/** Returns the current frame number.
* Treat a return value of 0 or smaller as unknown.
* (Browsers might have the property but
* always return 0)
*/
private GetFrameNumber() : number
{
let frameNumber;
if(this.mVideoElement)
{
//to find out if we got a new frame
//chrome has webkitDecodedFrameCount
//firefox mozDecodedFrames, mozParsedFrames, mozPresentedFrames seems to be always 0 so far
//mozPaintedFrames turned out useless as it only updates if the tag is visible
//no idea about all others
//
frameNumber = (this.mVideoElement as any).webkitDecodedFrameCount
//|| this.mVideoElement.currentTime can't be used updates every call
|| -1;
}else{
frameNumber = -1;
}
return frameNumber;
}
//TODO: Buffering
public TryGetFrame(): IFrameData
{
//make sure we get the newest frame
this.EnsureLatestFrame();
//remove the buffered frame if any
var result = this.mBufferedFrame;
this.mBufferedFrame = null;
return result;
}
public SetMute(mute: boolean): void {
this.mVideoElement.muted = mute;
}
public PeekFrame(): IFrameData {
this.EnsureLatestFrame();
return this.mBufferedFrame;
}
/** Ensures we have the latest frame ready
* for the next PeekFrame / TryGetFrame calls
*/
private EnsureLatestFrame():boolean
{
if (this.HasNewerFrame()) {
this.FrameToBuffer();
return true;
}
return false;
}
/** checks if the html tag has a newer frame available
* (or if 1/30th of a second passed since last frame if
* this info isn't available)
*/
private HasNewerFrame():boolean
{
if (this.mIsActive
&& this.mHasVideo
&& this.mCanvasElement != null)
{
if(this.mLastFrameNumber > 0)
{
//we are getting frame numbers. use those to
//check if we have a new one
if(this.GetFrameNumber() > this.mLastFrameNumber)
{
return true;
}
}
else
{
//many browsers do not share the frame info
//so far we just generate 30 FPS as a work around
let now = new Date().getTime();
let div = now - this.mLastFrameTime;
if (div >= this.mMsPerFrame) {
{
return true;
}
}
}
}
return false;
}
public Update(): void {
//moved to avoid creating buffered frames if not needed
//this.EnsureLatestFrame();
}
public DestroyCanvas(): void {
if (this.mCanvasElement != null && this.mCanvasElement.parentElement != null) {
this.mCanvasElement.parentElement.removeChild(this.mCanvasElement);
}
}
public Dispose(): void {
this.mIsActive = false;
this.DestroyCanvas();
if (this.mVideoElement != null && this.mVideoElement.parentElement != null) {
this.mVideoElement.parentElement.removeChild(this.mVideoElement);
}
//track cleanup is probably not needed but
//it might help ensure it properly stops
//in case there are other references out there
var tracks = this.mStream.getTracks();
for (var i = 0; i < tracks.length; i++) {
tracks[i].stop();
}
this.mStream = null;
this.mVideoElement = null;
this.mCanvasElement = null;
}
public CreateFrame(): RawFrame {
this.mCanvasElement.width = this.mVideoElement.videoWidth;
this.mCanvasElement.height = this.mVideoElement.videoHeight;
let ctx = this.mCanvasElement.getContext("2d");
var fillBackgroundFirst = true;
if (fillBackgroundFirst) {
ctx.clearRect(0, 0, this.mCanvasElement.width, this.mCanvasElement.height);
}
ctx.drawImage(this.mVideoElement, 0, 0);
try {
//risk of security exception in firefox
let imgData = ctx.getImageData(0, 0, this.mCanvasElement.width, this.mCanvasElement.height);
var imgRawData = imgData.data;
var array = new Uint8Array(imgRawData.buffer);
return new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height);
} catch (exception) {
//show white frame for now
var array = new Uint8Array(this.mCanvasElement.width * this.mCanvasElement.height * 4);
array.fill(255, 0, array.length - 1);
let res = new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height);
//attempted workaround for firefox bug / suspected cause:
// * root cause seems to be an internal origin-clean flag within the canvas. If set to false reading from the
// canvas triggers a security exceptions. This is usually used if the canvas contains data that isn't
// suppose to be accessible e.g. a picture from another domain
// * while moving the image to the canvas the origin-clean flag seems to be set to false but only
// during the first few frames. (maybe a race condition within firefox? A higher CPU workload increases the risk)
// * the canvas will work and look just fine but calling getImageData isn't allowed anymore
// * After a few frames the video is back to normal but the canvas will still have the flag set to false
//
//Solution:
// * Recreate the canvas if the exception is triggered. During the next few frames firefox should get its flag right
// and then stop causing the error. It might recreate the canvas multiple times until it finally works as we
// can't detect if the video element will trigger the issue until we tried to access the data
SLog.LogWarning("Firefox workaround: Refused access to the remote video buffer. Retrying next frame...");
this.DestroyCanvas();
this.mCanvasElement = this.SetupCanvas();
return res;
}
}
private FrameToBuffer(): void
{
this.mLastFrameTime = new Date().getTime();
this.mLastFrameNumber = this.GetFrameNumber();
this.mBufferedFrame = new LazyFrame(this);
}
private SetupVideoElement(): HTMLVideoElement {
var videoElement: HTMLVideoElement= document.createElement("video");
//width/doesn't seem to be important
videoElement.width = 320;
videoElement.height = 240;
videoElement.controls = true;
videoElement.id = "awrtc_mediastream_video_" + this.mInstanceId;
//videoElement.muted = true;
if (BrowserMediaStream.DEBUG_SHOW_ELEMENTS)
document.body.appendChild(videoElement);
return videoElement;
}
private SetupCanvas(): HTMLCanvasElement {
if (this.mVideoElement == null || this.mVideoElement.videoWidth <= 0 ||
this.mVideoElement.videoHeight <= 0)
return null;
var canvas: HTMLCanvasElement= document.createElement("canvas");
canvas.width = this.mVideoElement.videoWidth;
canvas.height = this.mVideoElement.videoHeight;
canvas.id = "awrtc_mediastream_canvas_" + this.mInstanceId;
if (BrowserMediaStream.DEBUG_SHOW_ELEMENTS)
document.body.appendChild(canvas);
return canvas;
}
public SetVolume(volume: number): void {
if (this.mVideoElement == null) {
return;
}
if (volume < 0)
volume = 0;
if (volume > 1)
volume = 1;
this.mVideoElement.volume = volume;
}
public HasAudioTrack(): boolean {
if (this.mStream != null && this.mStream.getAudioTracks() != null
&& this.mStream.getAudioTracks().length > 0) {
return true;
}
return false;
}
public HasVideoTrack(): boolean {
if (this.mStream != null && this.mStream.getVideoTracks() != null
&& this.mStream.getVideoTracks().length > 0) {
return true;
}
return false;
}
//for debugging purposes this is in here
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { AWebRtcCall } from "../media/AWebRtcCall";
import { NetworkConfig } from "../media/NetworkConfig";
import { IMediaNetwork } from "../media/IMediaNetwork";
import { BrowserMediaNetwork } from "./BrowserMediaNetwork";
/**Browser version of the C# version of WebRtcCall.
*
* See ICall interface for detailed documentation.
* BrowserWebRtcCall mainly exists to allow other versions
* in the future that might build on a different IMediaNetwork
* interface (Maybe something running inside Webassembly?).
*/
export class BrowserWebRtcCall extends AWebRtcCall {
public constructor(config: NetworkConfig) {
super(config);
this.Initialize(this.CreateNetwork());
}
private CreateNetwork(): IMediaNetwork {
return new BrowserMediaNetwork(this.mNetworkConfig);
}
protected DisposeInternal(disposing: boolean): void {
super.DisposeInternal(disposing);
if (disposing) {
if (this.mNetwork != null)
this.mNetwork.Dispose();
this.mNetwork = null;
}
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { SLog } from "../network/index";
export class DeviceInfo
{
public deviceId:string = null;
public defaultLabel:string = null;
public label:string = null;
public isLabelGuessed:boolean = true;
}
export interface DeviceApiOnChanged {
(): void;
}
export class DeviceApi
{
private static sLastUpdate = 0;
public static get LastUpdate() :number
{
return DeviceApi.sLastUpdate;
}
public static get HasInfo()
{
return DeviceApi.sLastUpdate > 0;
}
private static sDeviceInfo: { [id: string] : DeviceInfo; } = {};
private static sVideoDeviceCounter = 1;
private static sAccessStream:MediaStream = null;
private static sUpdateEvents: Array<DeviceApiOnChanged> = [];
public static AddOnChangedHandler(evt: DeviceApiOnChanged)
{
DeviceApi.sUpdateEvents.push(evt);
}
public static RemOnChangedHandler(evt: DeviceApiOnChanged)
{
let index = DeviceApi.sUpdateEvents.indexOf(evt);
if(index >= 0)
DeviceApi.sUpdateEvents.splice(index, 1);
}
private static TriggerChangedEvent()
{
for(let v of DeviceApi.sUpdateEvents)
{
try{
v();
}catch(e)
{
SLog.LE("Error in DeviceApi user event handler: " + e);
console.exception(e);
}
}
}
private static InternalOnEnum = (devices:MediaDeviceInfo[])=>
{
DeviceApi.sLastUpdate = new Date().getTime();
let newDeviceInfo: { [id: string] : DeviceInfo; } = {};
for(let info of devices)
{
if(info.kind != "videoinput")
continue;
let newInfo = new DeviceInfo();
newInfo.deviceId = info.deviceId;
let knownInfo:DeviceInfo= null;
if(newInfo.deviceId in DeviceApi.Devices)
{
//known device. reuse the default label
knownInfo = DeviceApi.Devices[newInfo.deviceId];
}
//check if we gave this device a default label already
//this is used to identify it via a user readable name in case
//we update multiple times with proper labels / default labels
if(knownInfo != null)
{
newInfo.defaultLabel = knownInfo.defaultLabel;
}else
{
newInfo.defaultLabel = info.kind + " " + DeviceApi.sVideoDeviceCounter;;
DeviceApi.sVideoDeviceCounter++;
}
//check if we know a proper label or got one this update
if(knownInfo != null && knownInfo.isLabelGuessed == false)
{
//already have one
newInfo.label = knownInfo.label;
newInfo.isLabelGuessed = false;
}else if(info.label)
{
//got a new one
newInfo.label = info.label;
newInfo.isLabelGuessed = false;
}else{
//no known label -> just use the default one
newInfo.label = newInfo.defaultLabel;
newInfo.isLabelGuessed = true;
}
newDeviceInfo[newInfo.deviceId] = newInfo;
}
DeviceApi.sDeviceInfo = newDeviceInfo;
if(DeviceApi.sAccessStream)
{
DeviceApi.sAccessStream.stop();
DeviceApi.sAccessStream = null;
}
DeviceApi.TriggerChangedEvent();
}
public static get Devices()
{
return DeviceApi.sDeviceInfo;
}
public static Reset()
{
DeviceApi.sUpdateEvents = [];
DeviceApi.sLastUpdate = 0;
DeviceApi.sDeviceInfo = {};
DeviceApi.sVideoDeviceCounter = 1;
DeviceApi.sAccessStream = null;
}
private static InternalOnError = (err:DOMError)=>
{
SLog.LE(err);
}
private static InternalOnStream = (stream:MediaStream)=>
{
DeviceApi.sAccessStream = stream;
DeviceApi.Update();
}
/**Updates the device list based on the current
* access. Given devices numbers if the name isn't known.
*/
public static Update():void
{
navigator.mediaDevices.enumerateDevices()
.then(DeviceApi.InternalOnEnum)
.catch(DeviceApi.InternalOnError);
}
/**Asks the user for access first to get the full
* device names.
*/
public static RequestUpdate():void
{
let constraints = {video:true};
navigator.mediaDevices.getUserMedia(constraints)
.then(DeviceApi.InternalOnStream)
.catch(DeviceApi.InternalOnError);
}
public static GetDeviceId(label:string):string {
let devs = DeviceApi.Devices;
for (var key in devs) {
let dev = devs[key];
if(dev.label == label || dev.defaultLabel == label || dev.deviceId == label){
return dev.deviceId;
}
}
return null;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { WebRtcDataPeer, SLog } from "../network/index";
import { BrowserMediaStream } from "./BrowserMediaStream";
import { IFrameData } from "../media/RawFrame";
//TODO: Not part of typescript as it is obsolete but
//chrome might still need this
export interface RTCMediaStreamEvent extends Event {
stream: MediaStream;
}
export interface RTCPeerConnectionObsolete extends RTCPeerConnection
{
onaddstream: ((this: RTCPeerConnection, streamEvent: RTCMediaStreamEvent) => any) | null;
addStream(stream: MediaStream): void;
}
export class MediaPeer extends WebRtcDataPeer
{
private mRemoteStream: BrowserMediaStream = null;
//quick workaround to allow html user to get the HTMLVideoElement once it is
//created. Might be done via events later to make wrapping to unity/emscripten possible
public InternalStreamAdded: (peer:MediaPeer, stream: BrowserMediaStream) => void = null;
//true - will use obsolete onstream / add stream
//false - will use ontrack / addtrack (seems to work fine now even on chrome)
public static sUseObsolete = false;
protected OnSetup(): void {
super.OnSetup();
//TODO: test in different browsers if boolean works now
//this is unclear in the API. according to typescript they are boolean, in native code they are int
//and some browser failed in the past if boolean was used ...
this.mOfferOptions = { "offerToReceiveAudio": true, "offerToReceiveVideo": true };
if(MediaPeer.sUseObsolete) {
SLog.LW("Using obsolete onaddstream as not all browsers support ontrack");
(this.mPeer as RTCPeerConnectionObsolete).onaddstream = (streamEvent: RTCMediaStreamEvent) => { this.OnAddStream(streamEvent); };
}
else{
this.mPeer.ontrack = (ev:RTCTrackEvent)=>{this.OnTrack(ev);}
}
}
protected OnCleanup() {
super.OnCleanup();
if (this.mRemoteStream != null) {
this.mRemoteStream.Dispose();
this.mRemoteStream = null;
}
}
private OnAddStream(streamEvent: RTCMediaStreamEvent) {
this.SetupStream(streamEvent.stream);
}
private OnTrack(ev:RTCTrackEvent){
if(ev && ev.streams && ev.streams.length > 0)
{
//this is getting called twice if audio and video is active
if(this.mRemoteStream == null)
this.SetupStream(ev.streams[0]);
}else{
SLog.LE("Unexpected RTCTrackEvent: " + JSON.stringify(ev));
}
}
private SetupStream(stream:MediaStream)
{
this.mRemoteStream = new BrowserMediaStream(stream);
//trigger events once the stream has its meta data available
this.mRemoteStream.InternalStreamAdded = (stream) =>{
if(this.InternalStreamAdded != null)
{
this.InternalStreamAdded(this, stream);
}
};
}
public TryGetRemoteFrame(): IFrameData
{
if (this.mRemoteStream == null)
return null;
return this.mRemoteStream.TryGetFrame();
}
public PeekFrame(): IFrameData {
if (this.mRemoteStream == null)
return null;
return this.mRemoteStream.PeekFrame();
}
public AddLocalStream(stream: MediaStream) {
if(MediaPeer.sUseObsolete) {
(this.mPeer as RTCPeerConnectionObsolete).addStream(stream);
}
else{
for(let v of stream.getTracks())
this.mPeer.addTrack(v, stream);
}
}
public Update() {
super.Update();
if (this.mRemoteStream != null) {
this.mRemoteStream.Update();
}
}
public SetVolume(volume: number): void {
if (this.mRemoteStream != null)
this.mRemoteStream.SetVolume(volume);
}
public HasAudioTrack(): boolean {
if (this.mRemoteStream != null)
return this.mRemoteStream.HasAudioTrack();
return false;
}
public HasVideoTrack(): boolean {
if (this.mRemoteStream != null)
return this.mRemoteStream.HasVideoTrack();
return false;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from './BrowserMediaNetwork'
export * from './BrowserWebRtcCall'
export * from './BrowserMediaStream'
export * from './MediaPeer'
export * from './DeviceApi'
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**Contains some helper classes to keep the typescript implementation
* similar to the C# implementation.
*
*/
export class Queue<T> {
private mArr: Array<T> = new Array<T>();
constructor() {
}
public Enqueue(val: T) {
this.mArr.push(val);
}
public TryDequeue(outp: Output<T> ): boolean{
var res = false
if (this.mArr.length > 0) {
outp.val = this.mArr.shift();
res = true;
}
return res;
}
public Dequeue(): T {
if (this.mArr.length > 0) {
return this.mArr.shift();
} else {
return null;
}
}
public Peek(): T {
if (this.mArr.length > 0) {
return this.mArr[0];
} else {
return null;
}
}
public Count(): number{
return this.mArr.length;
}
public Clear():void
{
this.mArr = new Array<T>();
}
}
export class List<T> {
private mArr: Array<T> = new Array<T>();
public get Internal() : Array<T>
{
return this.mArr;
}
constructor() {
}
public Add(val: T) {
this.mArr.push(val);
}
public get Count(): number {
return this.mArr.length;
}
}
export class Output<T>
{
public val : T;
}
export class Debug {
public static Log(s: any) {
SLog.Log(s);
}
public static LogError(s: any) {
SLog.LogError(s);
}
public static LogWarning(s: any) {
SLog.LogWarning(s);
}
}
export abstract class Encoder {
public abstract GetBytes(text: string): Uint8Array;
public abstract GetString(buffer: Uint8Array): string;
}
export class UTF16Encoding extends Encoder{
constructor() {
super();
}
public GetBytes(text: string): Uint8Array {
return this.stringToBuffer(text);
}
public GetString(buffer: Uint8Array): string {
return this.bufferToString(buffer);
}
private bufferToString(buffer: Uint8Array): string {
let arr = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2);
return String.fromCharCode.apply(null, arr);
}
private stringToBuffer(str: string): Uint8Array {
let buf = new ArrayBuffer(str.length * 2);
let bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
let result = new Uint8Array(buf);
return result;
}
}
export class Encoding {
public static get UTF16() {
return new UTF16Encoding();
}
constructor() {
}
}
export class Random {
public static getRandomInt(min, max): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
}
export class Helper {
public static tryParseInt(value : string): number {
try {
if (/^(\-|\+)?([0-9]+)$/.test(value)) {
let result = Number(value);
if (isNaN(result) == false)
return result;
}
} catch ( e) {
}
return null;
}
}
export enum SLogLevel
{
None = 0,
Errors = 1,
Warnings = 2,
Info = 3
}
//Simplified logger
export class SLog {
private static sLogLevel: SLogLevel = SLogLevel.Warnings;
public static SetLogLevel(level: SLogLevel)
{
SLog.sLogLevel = level;
}
public static L(msg: any): void {
SLog.Log(msg);
}
public static LW(msg: any): void {
SLog.LogWarning(msg);
}
public static LE(msg: any): void {
SLog.LogError(msg);
}
public static Log(msg: any): void {
if(SLog.sLogLevel >= SLogLevel.Info)
console.log(msg);
}
public static LogWarning(msg: any): void {
if(SLog.sLogLevel >= SLogLevel.Warnings)
console.warn(msg);
}
public static LogError(msg: any) {
if(SLog.sLogLevel >= SLogLevel.Errors)
console.error(msg);
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** Abstract interfaces and serialization to keep different
* versions compatible to each other.
*
* Watch out before changing anything in this file. Content is reused
* between webclient, signaling server and needs to remain compatible to
* the C# implementation.
*/
import { SLog } from "./Helper";
export enum NetEventType {
Invalid = 0,
UnreliableMessageReceived = 1,
ReliableMessageReceived = 2,
ServerInitialized = 3,//confirmation that the server was started. other people will be able to connect
ServerInitFailed = 4,//server couldn't be started
ServerClosed = 5,//server was closed. no new incoming connections
NewConnection = 6,//new incoming or outgoing connection established
ConnectionFailed = 7,//outgoing connection failed
Disconnected = 8,//a connection was disconnected
FatalError = 100, //not yet used
Warning = 101,//not yet used
Log = 102 //not yet used
}
export enum NetEventDataType {
Null = 0,
ByteArray = 1, //leading 32 bit byte length + byte array
UTF16String = 2, //leading 32 bit length (in utf16 chunks) + UTF 16
}
export class NetworkEvent {
private type: NetEventType;
private connectionId: ConnectionId;
private data: any;
constructor(t: NetEventType, conId: ConnectionId, data: any) {
this.type = t;
this.connectionId = conId;
this.data = data;
}
public get RawData(): any {
return this.data;
}
public get MessageData(): Uint8Array {
if (typeof this.data != "string")
return this.data;
return null;
}
public get Info(): string {
if (typeof this.data == "string")
return this.data;
return null;
}
public get Type(): NetEventType {
return this.type;
}
public get ConnectionId(): ConnectionId {
return this.connectionId;
}
//for debugging only
public toString(): string {
let output = "NetworkEvent[";
output += "NetEventType: (";
output += NetEventType[this.type];
output += "), id: (";
output += this.connectionId.id;
output += "), Data: (";
if (typeof this.data == "string") {
output += this.data;
}
output += ")]";
return output;
}
public static parseFromString(str: string): NetworkEvent {
let values = JSON.parse(str);
let data: any;
if (values.data == null) {
data = null;
} else if (typeof values.data == "string") {
data = values.data;
} else if (typeof values.data == "object") {
//json represents the array as an object containing each index and the
//value as string number ... improve that later
let arrayAsObject = values.data;
var length = 0;
for (var prop in arrayAsObject) {
//if (arrayAsObject.hasOwnProperty(prop)) { //shouldnt be needed
length++;
//}
}
let buffer = new Uint8Array(Object.keys(arrayAsObject).length);
for (let i = 0; i < buffer.length; i++)
buffer[i] = arrayAsObject[i];
data = buffer;
} else {
SLog.LogError("network event can't be parsed: " + str);
}
var evt = new NetworkEvent(values.type, values.connectionId, data);
return evt;
}
public static toString(evt: NetworkEvent): string {
return JSON.stringify(evt);
}
public static fromByteArray(arrin: Uint8Array): NetworkEvent {
//old node js versions seem to not return proper Uint8Arrays but
//buffers -> make sure it is a Uint8Array
let arr : Uint8Array = new Uint8Array(arrin)
let type: NetEventType = arr[0]; //byte
let dataType: NetEventDataType = arr[1]; //byte
let id: number = new Int16Array(arr.buffer, arr.byteOffset + 2, 1)[0]; //short
let data: any = null;
if (dataType == NetEventDataType.ByteArray) {
let length: number = new Uint32Array(arr.buffer, arr.byteOffset + 4, 1)[0]; //uint
let byteArray = new Uint8Array(arr.buffer, arr.byteOffset + 8, length);
data = byteArray;
} else if (dataType == NetEventDataType.UTF16String) {
let length: number = new Uint32Array(arr.buffer, arr.byteOffset + 4, 1)[0]; //uint
let uint16Arr = new Uint16Array(arr.buffer, arr.byteOffset + 8, length);
let str: string = "";
for (let i = 0; i < uint16Arr.length; i++) {
str += String.fromCharCode(uint16Arr[i]);
}
data = str;
} else if (dataType == NetEventDataType.Null) {
//message has no data
}
else
{
throw new Error('Message has an invalid data type flag: ' + dataType);
}
let conId: ConnectionId = new ConnectionId(id);
let result: NetworkEvent = new NetworkEvent(type, conId, data);
return result;
}
public static toByteArray(evt: NetworkEvent): Uint8Array {
let dataType: NetEventDataType;
let length = 4; //4 bytes are always needed
//getting type and length
if (evt.data == null) {
dataType = NetEventDataType.Null;
} else if (typeof evt.data == "string") {
dataType = NetEventDataType.UTF16String;
let str: string = evt.data;
length += str.length * 2 + 4;
} else {
dataType = NetEventDataType.ByteArray;
let byteArray: Uint8Array = evt.data;
length += 4 + byteArray.length;
}
//creating the byte array
let result = new Uint8Array(length);
result[0] = evt.type;;
result[1] = dataType;
let conIdField = new Int16Array(result.buffer, result.byteOffset + 2, 1);
conIdField[0] = evt.connectionId.id;
if (dataType == NetEventDataType.ByteArray) {
let byteArray: Uint8Array = evt.data;
let lengthField = new Uint32Array(result.buffer, result.byteOffset + 4, 1);
lengthField[0] = byteArray.length;
for (let i = 0; i < byteArray.length; i++) {
result[8 + i] = byteArray[i];
}
} else if (dataType == NetEventDataType.UTF16String) {
let str: string = evt.data;
let lengthField = new Uint32Array(result.buffer, result.byteOffset + 4, 1);
lengthField[0] = str.length;
let dataField = new Uint16Array(result.buffer, result.byteOffset + 8, str.length);
for (let i = 0; i < dataField.length; i++) {
dataField[i] = str.charCodeAt(i);
}
}
return result;
}
}
export class ConnectionId {
public static INVALID: ConnectionId = new ConnectionId(-1);
id: number;
constructor(nid: number) {
this.id = nid;
}
}
/// <summary>
/// Interface to a network that doesn't enforce storing any states.
///
/// Anything more is reusable between multiple different networks.
/// </summary>
export interface INetwork {
/// <summary>
/// This will return the incoming network events. Call this method and handle the incommen events until it returns false.
/// </summary>
/// <param name="evt"></param>
/// <returns>Returns true if the parameter evt contains a new event. False if there are no events to process left.</returns>
Dequeue(): NetworkEvent;
Peek(): NetworkEvent;
/// <summary>
/// Sends buffered data.
/// Might also clear all unused events from the queue!
/// </summary>
Flush(): void;
/// <summary>
/// Sends the content if a byte array to the given connection.
/// </summary>
/// <param name="id">The id of the recipient</param>
/// <param name="data">Byte array containing the data to send</param>
/// <param name="offset">The index in data where the network should start to send</param>
/// <param name="length">Length in bytes you want to send</param>
/// <param name="reliable">True to send a reliable message(tcp style) and false to send unreliable (udp style)</param>
SendData(id: ConnectionId, data: Uint8Array, /*offset: number, length: number,*/ reliable: boolean): boolean
/// <summary>
/// Disconnects the given connection
/// </summary>
/// <param name="id">Id of the connection to disconnect.</param>
Disconnect(id: ConnectionId): void;
/// <summary>
/// Disconnects all connection and shutsdown the server if started.
/// Dequeue will still return the confirmation messages such as Disconnected event for each connection.
///
/// </summary>
Shutdown(): void;
/// <summary>
/// Call this every frame if you intend to read incoming messages using Dequeue. This will make
/// sure all data is read received by the network.
/// </summary>
Update(): void;
Dispose(): void;
}
/// <summary>
/// Shared interface for WebRtcNetwork and UnityNetwork.
///
/// Keep in mind that in the current version the network can only act as a server (StartServer method) or
/// as a client (via Connect method).
/// </summary>
export interface IBasicNetwork extends INetwork {
/// <summary>
/// Starts a new server. After the server is started the Dequeue method will return a
/// ServerInitialized event with the address in the Info field.
///
/// If the server fails to start it will return a ServerInitFailed event. If the
/// server is closed due to an error or the Shutdown method a ServerClosed event
/// will be triggered.
/// </summary>
StartServer(address?: string): void;
StopServer(): void
/// <summary>
/// Connects to a given address or roomname.
///
/// This call will result in one of those 2 events in response:
/// * NewConnection if the connection was established
/// * ConnectionFailed if the connection failed.
///
///
/// </summary>
/// <param name="address">A string that identifies the target.</param>
/// <returns>Returns the Connection id the established connection will have (only supported by WebRtcNetwork).</returns>
Connect(address: string): ConnectionId;
}
export interface IWebRtcNetwork extends IBasicNetwork {
}
//export {NetEventType, NetworkEvent, ConnectionId, INetwork, IBasicNetwork};
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {ConnectionId, NetworkEvent, NetEventType} from "./index"
import { Queue } from "./Helper";
interface IIdNetworkDictionary {
[id: number]: LocalNetwork;
}
interface IAddressNetworkDictionary {
[address: string]: LocalNetwork;
}
/**Helper to simulate the WebsocketNetwork or WebRtcNetwork
* within a local application without
* any actual network components.
*
* This implementation might lack some features.
*/
export class LocalNetwork{
private static sNextId:number = 1;
private static mServers = {} as IAddressNetworkDictionary;
private mId:number;
private mNextNetworkId = new ConnectionId(1);
private mServerAddress : string = null;
private mEvents = new Queue<NetworkEvent>();
private mConnectionNetwork = {} as IIdNetworkDictionary;
private mIsDisposed = false;
public constructor() {
this.mId = LocalNetwork.sNextId;
LocalNetwork.sNextId++;
}
public get IsServer() {
return this.mServerAddress != null;
}
public StartServer(serverAddress: string = null): void
{
if (serverAddress == null)
serverAddress = "" + this.mId;
if (serverAddress in LocalNetwork.mServers) {
this.Enqueue(NetEventType.ServerInitFailed, ConnectionId.INVALID, serverAddress);
return;
}
LocalNetwork.mServers[serverAddress] = this;
this.mServerAddress = serverAddress;
this.Enqueue(NetEventType.ServerInitialized, ConnectionId.INVALID, serverAddress);
}
public StopServer() : void
{
if (this.IsServer) {
this.Enqueue(NetEventType.ServerClosed, ConnectionId.INVALID, this.mServerAddress);
delete LocalNetwork.mServers[this.mServerAddress];
this.mServerAddress = null;
}
}
public Connect(address: string): ConnectionId
{
var connectionId = this.NextConnectionId();
var sucessful = false;
if (address in LocalNetwork.mServers) {
let server = LocalNetwork.mServers[address];
if (server != null) {
server.ConnectClient(this);
//add the server as local connection
this.mConnectionNetwork[connectionId.id] = LocalNetwork.mServers[address];
this.Enqueue(NetEventType.NewConnection, connectionId, null);
sucessful = true;
}
}
if (sucessful == false) {
this.Enqueue(NetEventType.ConnectionFailed, connectionId, "Couldn't connect to the given server with id " + address);
}
return connectionId;
}
public Shutdown() : void
{
for(var id in this.mConnectionNetwork) //can be changed while looping?
{
this.Disconnect(new ConnectionId(+id));
}
//this.mConnectionNetwork.Clear();
this.StopServer();
}
public Dispose() : void{
if (this.mIsDisposed == false) {
this.Shutdown();
}
}
public SendData(userId: ConnectionId, data: Uint8Array, reliable: boolean): boolean
{
if (userId.id in this.mConnectionNetwork)
{
let net = this.mConnectionNetwork[userId.id];
net.ReceiveData(this, data, reliable);
return true;
}
return false;
}
public Update(): void
{
//work around for the GarbageCollection bug
//usually weak references are removed during garbage collection but that
//fails sometimes as others weak references get null to even though
//the objects still exist!
this.CleanupWreakReferences();
}
public Dequeue(): NetworkEvent
{
return this.mEvents.Dequeue();
}
public Peek(): NetworkEvent
{
return this.mEvents.Peek();
}
public Flush(): void
{
}
public Disconnect(id: ConnectionId): void
{
if (id.id in this.mConnectionNetwork) {
let other = this.mConnectionNetwork[id.id];
if (other != null) {
other.InternalDisconnectNetwork(this);
this.InternalDisconnect(id);
}
else {
//this is suppose to never happen but it does
//if a server is destroyed by the garbage collector the client
//weak reference appears to be NULL even though it still exists
//bug?
this.CleanupWreakReferences();
}
}
}
private FindConnectionId(network: LocalNetwork): ConnectionId
{
for(var kvp in this.mConnectionNetwork)
{
let network = this.mConnectionNetwork[kvp];
if (network != null) {
return new ConnectionId(+kvp);
}
}
return ConnectionId.INVALID;
}
private NextConnectionId(): ConnectionId
{
let res = this.mNextNetworkId;
this.mNextNetworkId = new ConnectionId(res.id + 1);
return res;
}
private ConnectClient(client: LocalNetwork): void
{
//if (this.IsServer == false)
// throw new InvalidOperationException();
let nextId = this.NextConnectionId();
//server side only
this.mConnectionNetwork[nextId.id] = client;
this.Enqueue(NetEventType.NewConnection, nextId, null);
}
private Enqueue(type: NetEventType, id: ConnectionId, data: any): void
{
let ev = new NetworkEvent(type, id, data);
this.mEvents.Enqueue(ev);
}
private ReceiveData(network: LocalNetwork, data: Uint8Array, reliable : boolean): void
{
let userId = this.FindConnectionId(network);
let buffer = new Uint8Array(data.length);
for (let i = 0; i < buffer.length; i++) {
buffer[i] = data[i];
}
let type = NetEventType.UnreliableMessageReceived;
if (reliable)
type = NetEventType.ReliableMessageReceived;
this.Enqueue(type, userId, buffer);
}
private InternalDisconnect(id: ConnectionId): void
{
if (id.id in this.mConnectionNetwork) {
this.Enqueue(NetEventType.Disconnected, id, null);
delete this.mConnectionNetwork[id.id];
}
}
private InternalDisconnectNetwork(ln: LocalNetwork): void
{
//if it can't be found it will return invalid which is ignored in internal disconnect
this.InternalDisconnect(this.FindConnectionId(ln));
}
private CleanupWreakReferences(): void
{
//foreach(var kvp in mConnectionNetwork.Keys.ToList())
//{
// var val = mConnectionNetwork[kvp];
// if (val.Get() == null) {
// InternalDisconnect(kvp);
// }
//}
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//import {ConnectionId, NetworkEvent, NetEventType, IBasicNetwork} from './INetwork'
import {
SignalingInfo, SignalingConfig, WebRtcPeerState, WebRtcDataPeer,
NetworkEvent, NetEventType, ConnectionId, IBasicNetwork} from "./index"
import { Queue, SLog, Output } from "./Helper";
export enum WebRtcNetworkServerState {
Invalid,
Offline,
Starting,
Online
}
/// <summary>
/// Native version of WebRtc
///
/// Make sure to use Shutdown before unity quits! (unity will probably get stuck without it)
///
///
/// </summary>
export class WebRtcNetwork implements IBasicNetwork {
private mTimeout = 60000;
private mInSignaling: { [id: number]: WebRtcDataPeer } = {}
private mNextId: ConnectionId = new ConnectionId(1);
private mSignaling: SignalingConfig = null;
private mEvents: Queue<NetworkEvent> = new Queue<NetworkEvent>();
private mIdToConnection: { [id: number]: WebRtcDataPeer } = {};
protected get IdToConnection() {
return this.mIdToConnection;
}
//must be the same as the hashmap and later returned read only (avoids copies)
private mConnectionIds: Array<ConnectionId> = new Array<ConnectionId>();
//only for internal use
public GetConnections(): Array<ConnectionId> {
return this.mConnectionIds;
}
private mServerState = WebRtcNetworkServerState.Offline;
private mRtcConfig: RTCConfiguration;
private mSignalingNetwork: IBasicNetwork;
private mLogDelegate: (s: string) => void;
private mIsDisposed = false;
//just for debugging / testing
public SetLog(logDel: (s: string) => void) {
this.mLogDelegate = logDel;
}
//public
public constructor(signalingConfig: SignalingConfig, lRtcConfig: RTCConfiguration) {
this.mSignaling = signalingConfig;
this.mSignalingNetwork = this.mSignaling.GetNetwork();
this.mRtcConfig = lRtcConfig;
}
public StartServer(): void;
public StartServer(address: string): void;
public StartServer(address?: string): void {
this.StartServerInternal(address);
}
protected StartServerInternal(address?: string): void {
this.mServerState = WebRtcNetworkServerState.Starting;
this.mSignalingNetwork.StartServer(address);
}
public StopServer(): void {
if (this.mServerState == WebRtcNetworkServerState.Starting) {
this.mSignalingNetwork.StopServer();
//removed. the underlaying sygnaling network should set those values
//this.mServerState = WebRtcNetworkServerState.Offline;
//this.mEvents.Enqueue(new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, null));
}
else if (this.mServerState == WebRtcNetworkServerState.Online) {
//dont wait for confirmation
this.mSignalingNetwork.StopServer();
//removed. the underlaying sygnaling network should set those values
//this.mServerState = WebRtcNetworkServerState.Offline;
//this.mEvents.Enqueue(new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null));
}
}
public Connect(address: string): ConnectionId {
return this.AddOutgoingConnection(address);
}
public Update(): void {
this.CheckSignalingState();
this.UpdateSignalingNetwork();
this.UpdatePeers();
}
public Dequeue(): NetworkEvent {
if (this.mEvents.Count() > 0)
return this.mEvents.Dequeue();
return null;
}
public Peek(): NetworkEvent {
if (this.mEvents.Count() > 0)
return this.mEvents.Peek();
return null;
}
public Flush(): void {
this.mSignalingNetwork.Flush();
}
public SendData(id: ConnectionId, data: Uint8Array/*, offset : number, length : number*/, reliable: boolean): boolean {
if (id == null || data == null || data.length == 0)
return;
let peer = this.mIdToConnection[id.id];
if (peer) {
return peer.SendData(data,/* offset, length,*/ reliable);
} else {
SLog.LogWarning("unknown connection id");
return false;
}
}
public Disconnect(id: ConnectionId): void {
let peer = this.mIdToConnection[id.id];
if (peer) {
this.HandleDisconnect(id);
}
}
public Shutdown(): void {
for (var id of this.mConnectionIds) {
this.Disconnect(id);
}
this.StopServer();
this.mSignalingNetwork.Shutdown();
}
protected DisposeInternal() {
if (this.mIsDisposed == false) {
this.Shutdown();
this.mIsDisposed = true;
}
}
public Dispose(): void {
this.DisposeInternal();
}
//protected
protected CreatePeer(peerId: ConnectionId, rtcConfig: RTCConfiguration): WebRtcDataPeer
{
let peer = new WebRtcDataPeer(peerId, rtcConfig);
return peer;
}
//private
private CheckSignalingState() {
let connected = new Array<ConnectionId>();
let failed = new Array<ConnectionId>();
//update the signaling channels
for (let key in this.mInSignaling) {
let peer = this.mInSignaling[key];
peer.Update();
let timeAlive = peer.SignalingInfo.GetCreationTimeMs();
let msg = new Output<string>();
while (peer.DequeueSignalingMessage(msg)) {
let buffer = this.StringToBuffer(msg.val);
this.mSignalingNetwork.SendData(new ConnectionId(+key), buffer, true);
}
if (peer.GetState() == WebRtcPeerState.Connected) {
connected.push(peer.SignalingInfo.ConnectionId);
}
else if (peer.GetState() == WebRtcPeerState.SignalingFailed || timeAlive > this.mTimeout) {
failed.push(peer.SignalingInfo.ConnectionId);
}
}
for (var v of connected) {
this.ConnectionEstablished(v);
}
for (var v of failed) {
this.SignalingFailed(v);
}
}
private UpdateSignalingNetwork(): void {
//update the signaling system
this.mSignalingNetwork.Update();
let evt: NetworkEvent;
while ((evt = this.mSignalingNetwork.Dequeue()) != null) {
if (evt.Type == NetEventType.ServerInitialized) {
this.mServerState = WebRtcNetworkServerState.Online;
this.mEvents.Enqueue(new NetworkEvent(NetEventType.ServerInitialized, ConnectionId.INVALID, evt.RawData));
} else if (evt.Type == NetEventType.ServerInitFailed) {
this.mServerState = WebRtcNetworkServerState.Offline;
this.mEvents.Enqueue(new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, evt.RawData));
} else if (evt.Type == NetEventType.ServerClosed) {
this.mServerState = WebRtcNetworkServerState.Offline;
this.mEvents.Enqueue(new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, evt.RawData));
} else if (evt.Type == NetEventType.NewConnection) {
//check if new incoming connection or an outgoing was established
let peer = this.mInSignaling[evt.ConnectionId.id];
if (peer) {
peer.StartSignaling();
} else {
this.AddIncomingConnection(evt.ConnectionId);
}
} else if (evt.Type == NetEventType.ConnectionFailed) {
//Outgoing connection failed
this.SignalingFailed(evt.ConnectionId);
} else if (evt.Type == NetEventType.Disconnected) {
let peer = this.mInSignaling[evt.ConnectionId.id];
if (peer) {
peer.SignalingInfo.SignalingDisconnected();
}
//if signaling was completed this isn't a problem
//SignalingDisconnected(evt.ConnectionId);
//do nothing. either webrtc has enough information to connect already
//or it will wait forever for the information -> after 30 sec we give up
} else if (evt.Type == NetEventType.ReliableMessageReceived) {
let peer = this.mInSignaling[evt.ConnectionId.id];
if (peer) {
let msg = this.BufferToString(evt.MessageData);
peer.AddSignalingMessage(msg);
} else {
SLog.LogWarning("Signaling message from unknown connection received");
}
}
}
}
private UpdatePeers(): void {
//every peer has a queue storing incoming messages to avoid multi threading problems -> handle it now
let disconnected = new Array<ConnectionId>();
for (var key in this.mIdToConnection) {
var peer: WebRtcDataPeer = this.mIdToConnection[key];
peer.Update();
let ev = new Output<NetworkEvent>();
while (peer.DequeueEvent(/*out*/ ev)) {
this.mEvents.Enqueue(ev.val);
}
if (peer.GetState() == WebRtcPeerState.Closed) {
disconnected.push(peer.ConnectionId);
}
}
for (let key of disconnected) {
this.HandleDisconnect(key);
}
}
private AddOutgoingConnection(address: string): ConnectionId {
let signalingConId = this.mSignalingNetwork.Connect(address);
SLog.L("new outgoing connection");
let info = new SignalingInfo(signalingConId, false, Date.now());
let peer = this.CreatePeer(this.NextConnectionId(), this.mRtcConfig);
peer.SetSignalingInfo(info);
this.mInSignaling[signalingConId.id] = peer;
return peer.ConnectionId;
}
private AddIncomingConnection(signalingConId: ConnectionId): ConnectionId {
SLog.L("new incoming connection");
let info = new SignalingInfo(signalingConId, true, Date.now());
let peer = this.CreatePeer(this.NextConnectionId(), this.mRtcConfig);
peer.SetSignalingInfo(info);
this.mInSignaling[signalingConId.id] = peer;
//passive way of starting signaling -> send out random number. if the other one does the same
//the one with the highest number starts signaling
peer.NegotiateSignaling();
return peer.ConnectionId;
}
private ConnectionEstablished(signalingConId: ConnectionId): void {
let peer = this.mInSignaling[signalingConId.id];
delete this.mInSignaling[signalingConId.id];
this.mSignalingNetwork.Disconnect(signalingConId);
this.mConnectionIds.push(peer.ConnectionId);
this.mIdToConnection[peer.ConnectionId.id] = peer;
this.mEvents.Enqueue(new NetworkEvent(NetEventType.NewConnection, peer.ConnectionId, null));
}
private SignalingFailed(signalingConId: ConnectionId): void {
let peer = this.mInSignaling[signalingConId.id];
if (peer) {
//connection was still believed to be in signaling -> notify the user of the event
delete this.mInSignaling[signalingConId.id];
this.mEvents.Enqueue(new NetworkEvent(NetEventType.ConnectionFailed, peer.ConnectionId, null));
if (peer.SignalingInfo.IsSignalingConnected()) {
this.mSignalingNetwork.Disconnect(signalingConId);
}
peer.Dispose();
}
}
private HandleDisconnect(id: ConnectionId): void {
let peer = this.mIdToConnection[id.id];
if (peer) {
peer.Dispose();
}
//??? this looks buggy. the connection id could be a reference with the same id and would not be recognized
let index = this.mConnectionIds.indexOf(id);
if (index != -1) {
this.mConnectionIds.splice(index, 1);
}
delete this.mIdToConnection[id.id];
let ev = new NetworkEvent(NetEventType.Disconnected, id, null);
this.mEvents.Enqueue(ev);
}
private NextConnectionId(): ConnectionId {
let id = new ConnectionId(this.mNextId.id);
this.mNextId.id++;
return id;
}
private StringToBuffer(str: string): Uint8Array {
let buf = new ArrayBuffer(str.length * 2);
let bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
let result = new Uint8Array(buf);
return result;
}
private BufferToString(buffer: Uint8Array): string {
let arr = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2);
return String.fromCharCode.apply(null, arr);
}
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {IBasicNetwork, ConnectionId, NetworkEvent, NetEventType} from "./index"
import { Queue, Helper, SLog, Debug, Output, Random } from "./Helper";
export class SignalingConfig {
private mNetwork: IBasicNetwork;
constructor(network: IBasicNetwork) {
this.mNetwork = network;
}
public GetNetwork(): IBasicNetwork {
return this.mNetwork;
}
}
export class SignalingInfo {
private mSignalingConnected: boolean;
public IsSignalingConnected(): boolean
{
return this.mSignalingConnected;
}
private mConnectionId: ConnectionId;
public get ConnectionId() : ConnectionId
{
return this.mConnectionId;
}
private mIsIncoming: boolean;
public IsIncoming(): boolean
{
return this.mIsIncoming;
}
private mCreationTime: number;
public GetCreationTimeMs(): number
{
return Date.now() - this.mCreationTime;
}
public constructor(id: ConnectionId, isIncoming: boolean, timeStamp : number)
{
this.mConnectionId = id;
this.mIsIncoming = isIncoming;
this.mCreationTime = timeStamp;
this.mSignalingConnected = true;
}
public SignalingDisconnected(): void
{
this.mSignalingConnected = false;
}
}
export enum WebRtcPeerState {
Invalid,
Created, //freshly created peer. didn't start to connect yet but can receive message to trigger it
Signaling, //webrtc started the process of connecting 2 peers
SignalingFailed, //connection failed to be established -> either cleanup/close or try again (not yet possible)
Connected, //connection is running
Closing, //Used before Close call to block reaction to webrtc events coming back
Closed //either Closed call finished or closed remotely or Cleanup/Dispose finished -> peer connection is destroyed and all resources are released
}
export enum WebRtcInternalState {
None, //nothing happened yet
Signaling, //Create Offer or CreateAnswer successfully called (after create callbacks)
SignalingFailed, //Signaling failed
Connected, //all channels opened
Closed //at least one channel was closed
}
export abstract class AWebRtcPeer {
private mState = WebRtcPeerState.Invalid;
public GetState(): WebRtcPeerState {
return this.mState;
}
//only written during webrtc callbacks
private mRtcInternalState = WebRtcInternalState.None;
protected mPeer: RTCPeerConnection;
private mIncomingSignalingQueue: Queue<string> = new Queue<string>();
private mOutgoingSignalingQueue: Queue<string> = new Queue<string>();
//Used to negotiate who starts the signaling if 2 peers listening
//at the same time
private mDidSendRandomNumber = false;
private mRandomNumerSent = 0;
protected mOfferOptions: RTCOfferOptions = { "offerToReceiveAudio": false, "offerToReceiveVideo": false };
constructor(rtcConfig: RTCConfiguration) {
this.SetupPeer(rtcConfig);
//remove this. it will trigger this call before the subclasses
//are initialized
this.OnSetup();
this.mState = WebRtcPeerState.Created;
}
protected abstract OnSetup(): void;
protected abstract OnStartSignaling(): void;
protected abstract OnCleanup(): void;
private SetupPeer(rtcConfig: RTCConfiguration): void {
this.mPeer = new RTCPeerConnection(rtcConfig);
this.mPeer.onicecandidate = (ev: RTCPeerConnectionIceEvent) => { this.OnIceCandidate(ev); };
this.mPeer.oniceconnectionstatechange = (ev: Event) => { this.OnIceConnectionChange(); };
this.mPeer.onnegotiationneeded = (ev: Event) => { this.OnRenegotiationNeeded(); };
this.mPeer.onsignalingstatechange = (ev: Event) => { this.OnSignalingChange(); };
}
protected DisposeInternal(): void {
this.Cleanup();
}
public Dispose(): void {
if (this.mPeer != null) {
this.DisposeInternal();
}
}
private Cleanup(): void {
//closing webrtc could cause old events to flush out -> make sure we don't call cleanup
//recursively
if (this.mState == WebRtcPeerState.Closed || this.mState == WebRtcPeerState.Closing) {
return;
}
this.mState = WebRtcPeerState.Closing;
this.OnCleanup();
if (this.mPeer != null)
this.mPeer.close();
//js version still receives callbacks after this. would make it
//impossible to get the state
//this.mReliableDataChannel = null;
//this.mUnreliableDataChannel = null;
//this.mPeer = null;
this.mState = WebRtcPeerState.Closed;
}
public Update(): void {
if (this.mState != WebRtcPeerState.Closed && this.mState != WebRtcPeerState.Closing && this.mState != WebRtcPeerState.SignalingFailed)
this.UpdateState();
if (this.mState == WebRtcPeerState.Signaling || this.mState == WebRtcPeerState.Created)
this.HandleIncomingSignaling();
}
private UpdateState(): void {
//will only be entered if the current state isn't already one of the ending states (closed, closing, signalingfailed)
if (this.mRtcInternalState == WebRtcInternalState.Closed) {
//if webrtc switched to the closed state -> make sure everything is destroyed.
//webrtc closed the connection. update internal state + destroy the references
//to webrtc
this.Cleanup();
//mState will be Closed now as well
} else if (this.mRtcInternalState == WebRtcInternalState.SignalingFailed) {
//if webrtc switched to a state indicating the signaling process failed -> set the whole state to failed
//this step will be ignored if the peers are destroyed already to not jump back from closed state to failed
this.mState = WebRtcPeerState.SignalingFailed;
} else if (this.mRtcInternalState == WebRtcInternalState.Connected) {
this.mState = WebRtcPeerState.Connected;
}
}
public HandleIncomingSignaling(): void {
//handle the incoming messages all at once
while (this.mIncomingSignalingQueue.Count() > 0) {
let msgString: string = this.mIncomingSignalingQueue.Dequeue();
let randomNumber = Helper.tryParseInt(msgString);
if (randomNumber != null) {
//was a random number for signaling negotiation
//if this peer uses negotiation as well then
//this would be true
if (this.mDidSendRandomNumber) {
//no peer is set to start signaling -> the one with the bigger number starts
if (randomNumber < this.mRandomNumerSent) {
//own diced number was bigger -> start signaling
SLog.L("Signaling negotiation complete. Starting signaling.");
this.StartSignaling();
} else if (randomNumber == this.mRandomNumerSent) {
//same numbers. restart the process
this.NegotiateSignaling();
} else {
//wait for other peer to start signaling
SLog.L("Signaling negotiation complete. Waiting for signaling.");
}
} else {
//ignore. this peer starts signaling automatically and doesn't use this
//negotiation
}
}
else {
//must be a webrtc signaling message using default json formatting
let msg: any = JSON.parse(msgString);
if (msg.sdp) {
let sdp: RTCSessionDescription = new RTCSessionDescription(msg as RTCSessionDescriptionInit);
if (sdp.type == 'offer') {
this.CreateAnswer(sdp);
//setTimeout(() => { }, 5000);
}
else {
//setTimeout(() => { }, 5000);
this.RecAnswer(sdp);
}
} else {
let ice: RTCIceCandidate = new RTCIceCandidate(msg);
if (ice != null) {
let promise = this.mPeer.addIceCandidate(ice);
promise.then(() => {/*success*/ });
promise.catch((error: DOMError) => { Debug.LogError(error); });
}
}
}
}
}
public AddSignalingMessage(msg: string): void {
Debug.Log("incoming Signaling message " + msg);
this.mIncomingSignalingQueue.Enqueue(msg);
}
public DequeueSignalingMessage(/*out*/ msg: Output<string>): boolean {
//lock might be not the best way to deal with this
//lock(mOutgoingSignalingQueue)
{
if (this.mOutgoingSignalingQueue.Count() > 0) {
msg.val = this.mOutgoingSignalingQueue.Dequeue();
return true;
}
else {
msg.val = null;
return false;
}
}
}
private EnqueueOutgoing(msg: string): void {
//lock(mOutgoingSignalingQueue)
{
Debug.Log("Outgoing Signaling message " + msg);
this.mOutgoingSignalingQueue.Enqueue(msg);
}
}
public StartSignaling(): void {
this.OnStartSignaling();
this.CreateOffer();
}
public NegotiateSignaling(): void {
let nb = Random.getRandomInt(0, 2147483647);
this.mRandomNumerSent = nb;
this.mDidSendRandomNumber = true;
this.EnqueueOutgoing("" + nb);
}
private CreateOffer(): void {
Debug.Log("CreateOffer");
let createOfferPromise = this.mPeer.createOffer(this.mOfferOptions);
createOfferPromise.then((desc: RTCSessionDescription) => {
let msg: string = JSON.stringify(desc);
let setDescPromise = this.mPeer.setLocalDescription(desc);
setDescPromise.then(() => {
this.RtcSetSignalingStarted();
this.EnqueueOutgoing(msg);
});
setDescPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
});
createOfferPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
}
private CreateAnswer(offer: RTCSessionDescription): void {
Debug.Log("CreateAnswer");
let remoteDescPromise = this.mPeer.setRemoteDescription(offer);
remoteDescPromise.then(() => {
let createAnswerPromise = this.mPeer.createAnswer();
createAnswerPromise.then((desc: RTCSessionDescription) => {
let msg: string = JSON.stringify(desc);
let localDescPromise = this.mPeer.setLocalDescription(desc);
localDescPromise.then(() => {
this.RtcSetSignalingStarted();
this.EnqueueOutgoing(msg);
});
localDescPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
});
createAnswerPromise.catch( (error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
});
remoteDescPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
}
private RecAnswer(answer: RTCSessionDescription): void {
Debug.Log("RecAnswer");
let remoteDescPromise = this.mPeer.setRemoteDescription(answer);
remoteDescPromise.then(() => {
//all done
});
remoteDescPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
}
private RtcSetSignalingStarted(): void {
if (this.mRtcInternalState == WebRtcInternalState.None) {
this.mRtcInternalState = WebRtcInternalState.Signaling;
}
}
protected RtcSetSignalingFailed(): void {
this.mRtcInternalState = WebRtcInternalState.SignalingFailed;
}
protected RtcSetConnected(): void {
if (this.mRtcInternalState == WebRtcInternalState.Signaling)
this.mRtcInternalState = WebRtcInternalState.Connected;
}
protected RtcSetClosed(): void {
if (this.mRtcInternalState == WebRtcInternalState.Connected)
this.mRtcInternalState = WebRtcInternalState.Closed;
}
private OnIceCandidate(ev: RTCPeerConnectionIceEvent): void {
if (ev && ev.candidate) {
let candidate = ev.candidate;
let msg: string = JSON.stringify(candidate);
this.EnqueueOutgoing(msg);
}
}
private OnIceConnectionChange(): void {
Debug.Log(this.mPeer.iceConnectionState);
if (this.mPeer.iceConnectionState == "failed") {
if(this.mState == WebRtcPeerState.Signaling)
{
this.RtcSetSignalingFailed();
}else if(this.mState == WebRtcPeerState.Connected)
{
this.RtcSetClosed();
}
}
}
private OnIceGatheringChange(/*new_state: RTCIceGatheringState*/): void {
Debug.Log(this.mPeer.iceGatheringState);
}
private OnRenegotiationNeeded(): void
{ }
private OnSignalingChange(/*new_state: RTCSignalingState*/): void {
Debug.Log(this.mPeer.signalingState);
if (this.mPeer.signalingState == "closed") {
this.RtcSetClosed();
}
}
}
export class WebRtcDataPeer extends AWebRtcPeer {
private mConnectionId: ConnectionId;
public get ConnectionId(): ConnectionId {
return this.mConnectionId;
}
private mInfo: SignalingInfo = null;
public get SignalingInfo(): SignalingInfo {
return this.mInfo;
}
public SetSignalingInfo(info: SignalingInfo) {
this.mInfo = info;
}
private mEvents: Queue<NetworkEvent> = new Queue<NetworkEvent>();
private static sLabelReliable: string = "reliable";
private static sLabelUnreliable: string = "unreliable";
private mReliableDataChannelReady: boolean = false;
private mUnreliableDataChannelReady: boolean = false;
private mReliableDataChannel: RTCDataChannel;
private mUnreliableDataChannel: RTCDataChannel;
public constructor(id: ConnectionId, rtcConfig: RTCConfiguration) {
super(rtcConfig);
this.mConnectionId = id;
}
protected OnSetup():void {
this.mPeer.ondatachannel = (ev: Event) => { this.OnDataChannel((ev as any).channel); };
}
protected OnStartSignaling(): void {
let configReliable: RTCDataChannelInit = {} as RTCDataChannelInit;
this.mReliableDataChannel = this.mPeer.createDataChannel(WebRtcDataPeer.sLabelReliable, configReliable);
this.RegisterObserverReliable();
let configUnreliable: RTCDataChannelInit = {} as RTCDataChannelInit;
configUnreliable.maxRetransmits = 0;
configUnreliable.ordered = false;
this.mUnreliableDataChannel = this.mPeer.createDataChannel(WebRtcDataPeer.sLabelUnreliable, configUnreliable);
this.RegisterObserverUnreliable();
}
protected OnCleanup(): void {
if (this.mReliableDataChannel != null)
this.mReliableDataChannel.close();
if (this.mUnreliableDataChannel != null)
this.mUnreliableDataChannel.close();
//dont set to null. handlers will be called later
}
private RegisterObserverReliable(): void {
this.mReliableDataChannel.onmessage = (event: MessageEvent) => { this.ReliableDataChannel_OnMessage(event); };
this.mReliableDataChannel.onopen = (event: Event) => { this.ReliableDataChannel_OnOpen(); };
this.mReliableDataChannel.onclose = (event: Event) => { this.ReliableDataChannel_OnClose(); };
this.mReliableDataChannel.onerror = (event: Event) => { this.ReliableDataChannel_OnError(""); }; //should the event just be a string?
}
private RegisterObserverUnreliable(): void {
this.mUnreliableDataChannel.onmessage = (event: MessageEvent) => { this.UnreliableDataChannel_OnMessage(event); };
this.mUnreliableDataChannel.onopen = (event: Event) => { this.UnreliableDataChannel_OnOpen(); };
this.mUnreliableDataChannel.onclose = (event: Event) => { this.UnreliableDataChannel_OnClose(); };
this.mUnreliableDataChannel.onerror = (event: Event) => { this.UnreliableDataChannel_OnError(""); };//should the event just be a string?
}
public SendData(data: Uint8Array,/* offset : number, length : number,*/ reliable: boolean): boolean {
//let buffer: ArrayBufferView = data.subarray(offset, offset + length) as ArrayBufferView;
let buffer: ArrayBufferView = data as ArrayBufferView;
let MAX_SEND_BUFFER = 1024 * 1024;
//chrome bug: If the channels is closed remotely trough disconnect
//then the local channel can appear open but will throw an exception
//if send is called
let sentSuccessfully = false;
try {
if (reliable) {
if (this.mReliableDataChannel.readyState === "open")
{
//bugfix: WebRTC seems to simply close the data channel if we send
//too much at once. avoid this from now on by returning false
//if the buffer gets too full
if((this.mReliableDataChannel.bufferedAmount + buffer.byteLength) < MAX_SEND_BUFFER)
{
this.mReliableDataChannel.send(buffer);
sentSuccessfully = true;
}
}
}
else {
if (this.mUnreliableDataChannel.readyState === "open")
{
if((this.mUnreliableDataChannel.bufferedAmount + buffer.byteLength) < MAX_SEND_BUFFER)
{
this.mUnreliableDataChannel.send(buffer);
sentSuccessfully = true;
}
}
}
} catch (e) {
SLog.LogError("Exception while trying to send: " + e);
}
return sentSuccessfully;
}
public DequeueEvent(/*out*/ ev: Output<NetworkEvent>): boolean {
//lock(mEvents)
{
if (this.mEvents.Count() > 0) {
ev.val = this.mEvents.Dequeue();
return true;
}
}
return false;
}
private Enqueue(ev: NetworkEvent): void {
//lock(mEvents)
{
this.mEvents.Enqueue(ev);
}
}
public OnDataChannel(data_channel: RTCDataChannel): void {
let newChannel = data_channel;
if (newChannel.label == WebRtcDataPeer.sLabelReliable) {
this.mReliableDataChannel = newChannel;
this.RegisterObserverReliable();
}
else if (newChannel.label == WebRtcDataPeer.sLabelUnreliable) {
this.mUnreliableDataChannel = newChannel;
this.RegisterObserverUnreliable();
}
else {
Debug.LogError("Datachannel with unexpected label " + newChannel.label);
}
}
private RtcOnMessageReceived(event: MessageEvent, reliable: boolean): void {
let eventType = NetEventType.UnreliableMessageReceived;
if (reliable) {
eventType = NetEventType.ReliableMessageReceived;
}
//async conversion to blob/arraybuffer here
if (event.data instanceof ArrayBuffer) {
let buffer = new Uint8Array(event.data);
this.Enqueue(new NetworkEvent(eventType, this.mConnectionId, buffer));
} else if (event.data instanceof Blob) {
var connectionId = this.mConnectionId;
var fileReader = new FileReader();
var self: WebRtcDataPeer = this;
fileReader.onload = function () {
//need to use function as this pointer is needed to reference to the data
let data = this.result as ArrayBuffer;
let buffer = new Uint8Array(data);
self.Enqueue(new NetworkEvent(eventType, self.mConnectionId, buffer));
};
fileReader.readAsArrayBuffer(event.data);
} else {
Debug.LogError("Invalid message type. Only blob and arraybuffer supported: " + event.data);
}
}
private ReliableDataChannel_OnMessage(event: MessageEvent): void {
Debug.Log("ReliableDataChannel_OnMessage ");
this.RtcOnMessageReceived(event, true);
}
private ReliableDataChannel_OnOpen(): void {
Debug.Log("mReliableDataChannelReady");
this.mReliableDataChannelReady = true;
if (this.IsRtcConnected()) {
this.RtcSetConnected();
Debug.Log("Fully connected");
}
}
private ReliableDataChannel_OnClose(): void {
this.RtcSetClosed();
}
private ReliableDataChannel_OnError(errorMsg: string) : void
{
Debug.LogError(errorMsg);
this.RtcSetClosed();
}
private UnreliableDataChannel_OnMessage(event: MessageEvent): void {
Debug.Log("UnreliableDataChannel_OnMessage ");
this.RtcOnMessageReceived(event, false);
}
private UnreliableDataChannel_OnOpen(): void {
Debug.Log("mUnreliableDataChannelReady");
this.mUnreliableDataChannelReady = true;
if (this.IsRtcConnected()) {
this.RtcSetConnected();
Debug.Log("Fully connected");
}
}
private UnreliableDataChannel_OnClose(): void {
this.RtcSetClosed();
}
private UnreliableDataChannel_OnError(errorMsg: string): void {
Debug.LogError(errorMsg);
this.RtcSetClosed();
}
private IsRtcConnected(): boolean {
return this.mReliableDataChannelReady && this.mUnreliableDataChannelReady;
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {ConnectionId, NetworkEvent, NetEventType, IBasicNetwork} from './INetwork'
import { SLog } from './Helper';
export enum WebsocketConnectionStatus {
Uninitialized,
NotConnected,
Connecting,
Connected,
Disconnecting //server will shut down, all clients disconnect, ...
}
export enum WebsocketServerStatus {
Offline,
Starting,
Online,
ShuttingDown
}
//TODO: handle errors if the socket connection failed
//+ send back failed events for connected / serverstart events that are buffered
export class WebsocketNetwork implements IBasicNetwork {
//websocket.
private mSocket: WebSocket;
//currents status. will be updated based on update call
private mStatus = WebsocketConnectionStatus.Uninitialized;
public getStatus() { return this.mStatus;};
//queue to hold buffered outgoing messages
private mOutgoingQueue = new Array<NetworkEvent>();
//buffer for incoming messages
private mIncomingQueue = new Array<NetworkEvent>();
//Status of the server for incoming connections
private mServerStatus = WebsocketServerStatus.Offline;
//outgoing connections (just need to be stored to allow to send out a failed message
//if the whole signaling connection fails
private mConnecting = new Array<number>();
private mConnections = new Array<number>();
//next free connection id
private mNextOutgoingConnectionId = new ConnectionId(1);
private mUrl: string = null;
private mIsDisposed = false;
public constructor(url: string) {
this.mUrl = url;
this.mStatus = WebsocketConnectionStatus.NotConnected;
}
private WebsocketConnect(): void {
this.mStatus = WebsocketConnectionStatus.Connecting;
this.mSocket = new WebSocket(this.mUrl);
this.mSocket.binaryType = "arraybuffer";
this.mSocket.onopen = () => { this.OnWebsocketOnOpen(); }
this.mSocket.onerror = (error) => { this.OnWebsocketOnError(error); };
this.mSocket.onmessage = (e) => { this.OnWebsocketOnMessage(e); };
this.mSocket.onclose = (e) => { this.OnWebsocketOnClose(e); };
//js websockets connect automatically after creation?
}
private WebsocketCleanup() : void {
this.mSocket.onopen = null;
this.mSocket.onerror = null;
this.mSocket.onmessage = null;
this.mSocket.onclose = null;
if (this.mSocket.readyState == this.mSocket.OPEN
|| this.mSocket.readyState == this.mSocket.CONNECTING) {
this.mSocket.close();
}
this.mSocket = null;
}
private EnsureServerConnection(): void
{
if (this.mStatus == WebsocketConnectionStatus.NotConnected) {
//no server
//no connection about to be established
//no current connections
//-> disconnect the server connection
this.WebsocketConnect();
}
}
private CheckSleep() : void
{
if (this.mStatus == WebsocketConnectionStatus.Connected
&& this.mServerStatus == WebsocketServerStatus.Offline
&& this.mConnecting.length == 0
&& this.mConnections.length == 0) {
//no server
//no connection about to be established
//no current connections
//-> disconnect the server connection
this.Cleanup();
}
}
private OnWebsocketOnOpen() {
SLog.L('onWebsocketOnOpen');
this.mStatus = WebsocketConnectionStatus.Connected;
}
private OnWebsocketOnClose(event: CloseEvent) {
SLog.L('Closed: ' + JSON.stringify(event));
if(event.code != 1000)
{
SLog.LE("Websocket closed with code: " + event.code + " " + event.reason);
}
//ignore closed event if it was caused due to a shutdown (as that means we cleaned up already)
if (this.mStatus == WebsocketConnectionStatus.Disconnecting
|| this.mStatus == WebsocketConnectionStatus.NotConnected)
return;
this.Cleanup();
this.mStatus = WebsocketConnectionStatus.NotConnected;
}
private OnWebsocketOnMessage(event) {
if (this.mStatus == WebsocketConnectionStatus.Disconnecting
|| this.mStatus == WebsocketConnectionStatus.NotConnected)
return;
//browsers will have ArrayBuffer in event.data -> change to byte array
let evt = NetworkEvent.fromByteArray(new Uint8Array(event.data));
this.HandleIncomingEvent(evt);
}
private OnWebsocketOnError(error) {
//the error event doesn't seem to have any useful information?
//browser is expected to call OnClose after this
SLog.LE('WebSocket Error ' + error);
}
/// <summary>
/// called during Disconnecting state either trough server connection failed or due to Shutdown
///
/// Also used to switch to sleeping mode. In this case there connection isn't used as
/// server and doesn't have any connections (established or connecting) thus
/// only WebsocketCleanup is in effect.
///
/// WebsocketNetwork has to be still usable after this call like a newly
/// created connections (except with events in the message queue)
/// </summary>
private Cleanup(): void {
//check if this was done already (or we are in the process of cleanup already)
if (this.mStatus == WebsocketConnectionStatus.Disconnecting
|| this.mStatus == WebsocketConnectionStatus.NotConnected)
return;
this.mStatus = WebsocketConnectionStatus.Disconnecting;
//throw connection failed events for each connection in mConnecting
for (let conId of this.mConnecting) {
//all connection it tries to establish right now fail due to shutdown
this.EnqueueIncoming(
new NetworkEvent(NetEventType.ConnectionFailed, new ConnectionId(conId), null));
}
this.mConnecting = new Array<number>();
//throw disconnect events for all NewConnection events in the outgoing queue
//ignore messages and everything else
for (let conId of this.mConnections) {
//all connection it tries to establish right now fail due to shutdown
this.EnqueueIncoming(
new NetworkEvent(NetEventType.Disconnected, new ConnectionId(conId), null));
}
this.mConnections = new Array<number>();
if (this.mServerStatus == WebsocketServerStatus.Starting) {
//if server was Starting -> throw failed event
this.EnqueueIncoming(
new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, null));
} else if (this.mServerStatus == WebsocketServerStatus.Online) {
//if server was Online -> throw close event
this.EnqueueIncoming(
new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null));
} else if (this.mServerStatus == WebsocketServerStatus.ShuttingDown) {
//if server was ShuttingDown -> throw close event (don't think this can happen)
this.EnqueueIncoming(
new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null));
}
this.mServerStatus = WebsocketServerStatus.Offline;
this.mOutgoingQueue = new Array<NetworkEvent>();
this.WebsocketCleanup();
this.mStatus = WebsocketConnectionStatus.NotConnected;
}
private EnqueueOutgoing(evt: NetworkEvent): void {
this.mOutgoingQueue.push(evt);
}
private EnqueueIncoming(evt: NetworkEvent): void {
this.mIncomingQueue.push(evt);
}
private TryRemoveConnecting(id: ConnectionId): void {
var index = this.mConnecting.indexOf(id.id);
if (index != -1) {
this.mConnecting.splice(index, 1);
}
}
private TryRemoveConnection(id: ConnectionId): void {
var index = this.mConnections.indexOf(id.id);
if (index != -1) {
this.mConnections.splice(index, 1);
}
}
private HandleIncomingEvent(evt: NetworkEvent) {
if (evt.Type == NetEventType.NewConnection) {
//removing connecting info
this.TryRemoveConnecting(evt.ConnectionId);
//add connection
this.mConnections.push(evt.ConnectionId.id);
} else if (evt.Type == NetEventType.ConnectionFailed) {
//remove connecting info
this.TryRemoveConnecting(evt.ConnectionId);
} else if (evt.Type == NetEventType.Disconnected) {
//remove from connections
this.TryRemoveConnection(evt.ConnectionId);
} else if (evt.Type == NetEventType.ServerInitialized)
{
this.mServerStatus = WebsocketServerStatus.Online;
} else if (evt.Type == NetEventType.ServerInitFailed)
{
this.mServerStatus = WebsocketServerStatus.Offline;
} else if (evt.Type == NetEventType.ServerClosed)
{
this.mServerStatus = WebsocketServerStatus.ShuttingDown;
//any cleaning up to do?
this.mServerStatus = WebsocketServerStatus.Offline;
}
this.EnqueueIncoming(evt);
}
private HandleOutgoingEvents(): void {
while (this.mOutgoingQueue.length > 0) {
var evt = this.mOutgoingQueue.shift();
//var msg = NetworkEvent.toString(evt);
var msg = NetworkEvent.toByteArray(evt);
this.mSocket.send(msg);
}
}
private NextConnectionId(): ConnectionId {
var result = this.mNextOutgoingConnectionId;
this.mNextOutgoingConnectionId = new ConnectionId(this.mNextOutgoingConnectionId.id + 1);
return result;
}
private GetRandomKey(): string {
var result = "";
for (var i = 0; i < 7; i++) {
result += String.fromCharCode(65 + Math.round(Math.random() * 25));
}
return result;
}
//interface implementation
public Dequeue(): NetworkEvent {
if (this.mIncomingQueue.length > 0)
return this.mIncomingQueue.shift();
return null;
}
public Peek(): NetworkEvent {
if (this.mIncomingQueue.length > 0)
return this.mIncomingQueue[0];
return null;
}
public Update(): void {
this.CheckSleep();
}
public Flush(): void {
//ideally we buffer everything and then flush when it is connected as
//websockets aren't suppose to be used for realtime communication anyway
if (this.mStatus == WebsocketConnectionStatus.Connected)
this.HandleOutgoingEvents();
}
public SendData(id: ConnectionId, data: Uint8Array, /*offset: number, length: number,*/ reliable: boolean): boolean {
if (id == null || data == null || data.length == 0)
return;
var evt: NetworkEvent;
if (reliable) {
evt = new NetworkEvent(NetEventType.ReliableMessageReceived, id, data);
} else {
evt = new NetworkEvent(NetEventType.UnreliableMessageReceived, id, data);
}
this.EnqueueOutgoing(evt);
return true;
}
public Disconnect(id: ConnectionId): void {
var evt = new NetworkEvent(NetEventType.Disconnected, id, null);
this.EnqueueOutgoing(evt);
}
public Shutdown(): void {
this.Cleanup();
this.mStatus = WebsocketConnectionStatus.NotConnected;
}
public Dispose() {
if (this.mIsDisposed == false) {
this.Shutdown();
this.mIsDisposed = true;
}
}
public StartServer(): void;
public StartServer(address: string): void;
public StartServer(address?: string): void {
if (address == null) {
address = "" + this.GetRandomKey();
}
if (this.mServerStatus == WebsocketServerStatus.Offline) {
this.EnsureServerConnection();
this.mServerStatus = WebsocketServerStatus.Starting;
//TODO: address is a string but ubytearray is defined. will fail if binary
this.EnqueueOutgoing(new NetworkEvent(NetEventType.ServerInitialized, ConnectionId.INVALID, address));
} else {
this.EnqueueIncoming(new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, address));
}
}
public StopServer(): void {
this.EnqueueOutgoing(new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null));
}
public Connect(address: string): ConnectionId {
this.EnsureServerConnection();
var newConId = this.NextConnectionId();
this.mConnecting.push(newConId.id);
var evt = new NetworkEvent(NetEventType.NewConnection, newConId, address);
this.EnqueueOutgoing(evt);
return newConId;
}
}
//Below tests only. Move out later
function bufferToString(buffer: Uint8Array): string {
let arr = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2);
return String.fromCharCode.apply(null, arr);
}
function stringToBuffer(str: string): Uint8Array {
let buf = new ArrayBuffer(str.length * 2);
let bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
let result = new Uint8Array(buf);
return result;
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from './INetwork'
export * from './Helper'
export * from './WebRtcPeer'
export * from './WebRtcNetwork'
export * from './WebsocketNetwork'
export * from './LocalNetwork'
\ No newline at end of file
{
"extends": "./tsconfig_base",
"compilerOptions": {
"declaration": true,
"target": "es5",
"module": "es2015",
"outDir": "../../build/awrtc"
}
}
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"declaration": false,
"baseUrl": ".",
"esModuleInterop": true
},
"exclude": [
"node_modules",
"wwwroot"
],
"files": [
"./network/Helper.ts",
"./network/INetwork.ts",
"./network/WebRtcNetwork.ts",
"./network/WebRtcPeer.ts",
"./network/WebsocketNetwork.ts",
"./network/LocalNetwork.ts",
"./network/index.ts",
"./media/CallEventArgs.ts",
"./media/ICall.ts",
"./media/IMediaNetwork.ts",
"./media/MediaConfig.ts",
"./media/NetworkConfig.ts",
"./media/RawFrame.ts",
"./media/AWebRtcCall.ts",
"./media/index.ts",
"./media_browser/BrowserMediaNetwork.ts",
"./media_browser/BrowserWebRtcCall.ts",
"./media_browser/BrowserMediaStream.ts",
"./media_browser/MediaPeer.ts",
"./media_browser/index.ts",
"./unity/CAPI.ts",
"./unity/index.ts",
"./index.ts"
]
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**This file contains the mapping between the awrtc_browser library and
* Unitys WebGL support. Not needed for regular use.
*/
import {SLog, WebRtcNetwork, SignalingConfig, NetworkEvent, ConnectionId, LocalNetwork, WebsocketNetwork} from "../network/index"
import { MediaConfigurationState, NetworkConfig, MediaConfig } from "../media/index";
import { BrowserMediaStream, BrowserMediaNetwork, DeviceApi } from "../media_browser/index";
var gCAPIWebRtcNetworkInstances: { [id: number]: WebRtcNetwork }= {};
var gCAPIWebRtcNetworkInstancesNextIndex = 1;
export function CAPIWebRtcNetworkIsAvailable() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
return true;
SLog.LE("Browser doesn't seem to support a modern WebRTC API");
return false;
}
export function CAPIWebRtcNetworkCreate(lConfiguration: string) {
var lIndex = gCAPIWebRtcNetworkInstancesNextIndex;
gCAPIWebRtcNetworkInstancesNextIndex++;
var signaling_class = "LocalNetwork";
var signaling_param: any = null;
var iceServers: RTCIceServer[];
if (lConfiguration == null || typeof lConfiguration !== 'string' || lConfiguration.length === 0) {
SLog.LogError("invalid configuration. Returning -1! Config: " + lConfiguration);
return -1;
}
else {
var conf = JSON.parse(lConfiguration);
if (conf) {
if (conf.signaling) {
signaling_class = conf.signaling.class;
signaling_param = conf.signaling.param;
}
if (conf.iceServers) {
iceServers = conf.iceServers;
}
SLog.L(signaling_class);
//this seems to be broken after switch to modules
//let signalingNetworkClass = window[signaling_class];
//let signalingNetworkClass = new (<any>window)["awrtc.LocalNetwork"];
//console.debug(signalingNetworkClass);
let signalingNetworkClass : any;
if(signaling_class === "LocalNetwork")
{
signalingNetworkClass = LocalNetwork;
}else{
signalingNetworkClass = WebsocketNetwork;
}
let signalingConfig = new SignalingConfig(new signalingNetworkClass(signaling_param));
let rtcConfiguration: RTCConfiguration = { iceServers: iceServers };
gCAPIWebRtcNetworkInstances[lIndex] = new WebRtcNetwork(signalingConfig, rtcConfiguration);
} else {
SLog.LogWarning("Parsing configuration failed. Configuration: " + lConfiguration);
return -1;
}
}
//gCAPIWebRtcNetworkInstances[lIndex].OnLog = function (lMsg) {
// console.debug(lMsg);
//};
return lIndex;
}
export function CAPIWebRtcNetworkRelease(lIndex: number) {
if (lIndex in gCAPIWebRtcNetworkInstances) {
gCAPIWebRtcNetworkInstances[lIndex].Dispose();
delete gCAPIWebRtcNetworkInstances[lIndex];
}
}
export function CAPIWebRtcNetworkConnect(lIndex: number, lRoom: string) {
return gCAPIWebRtcNetworkInstances[lIndex].Connect(lRoom);
}
export function CAPIWebRtcNetworkStartServer(lIndex: number, lRoom: string) {
gCAPIWebRtcNetworkInstances[lIndex].StartServer(lRoom);
}
export function CAPIWebRtcNetworkStopServer(lIndex: number) {
gCAPIWebRtcNetworkInstances[lIndex].StopServer();
}
export function CAPIWebRtcNetworkDisconnect(lIndex: number, lConnectionId: number) {
gCAPIWebRtcNetworkInstances[lIndex].Disconnect(new ConnectionId(lConnectionId));
}
export function CAPIWebRtcNetworkShutdown(lIndex: number) {
gCAPIWebRtcNetworkInstances[lIndex].Shutdown();
}
export function CAPIWebRtcNetworkUpdate(lIndex: number) {
gCAPIWebRtcNetworkInstances[lIndex].Update();
}
export function CAPIWebRtcNetworkFlush(lIndex: number) {
gCAPIWebRtcNetworkInstances[lIndex].Flush();
}
export function CAPIWebRtcNetworkSendData(lIndex: number, lConnectionId: number, lUint8ArrayData: Uint8Array, lReliable: boolean) {
gCAPIWebRtcNetworkInstances[lIndex].SendData(new ConnectionId(lConnectionId), lUint8ArrayData, lReliable);
}
//helper for emscripten
export function CAPIWebRtcNetworkSendDataEm(lIndex: number, lConnectionId: number, lUint8ArrayData: Uint8Array, lUint8ArrayDataOffset: number, lUint8ArrayDataLength: number, lReliable: boolean) {
//console.debug("SendDataEm: " + lReliable + " length " + lUint8ArrayDataLength + " to " + lConnectionId);
var arrayBuffer = new Uint8Array(lUint8ArrayData.buffer, lUint8ArrayDataOffset, lUint8ArrayDataLength);
return gCAPIWebRtcNetworkInstances[lIndex].SendData(new ConnectionId(lConnectionId), arrayBuffer, lReliable);
}
export function CAPIWebRtcNetworkDequeue(lIndex: number): NetworkEvent {
return gCAPIWebRtcNetworkInstances[lIndex].Dequeue();
}
export function CAPIWebRtcNetworkPeek(lIndex: number): NetworkEvent {
return gCAPIWebRtcNetworkInstances[lIndex].Peek();
}
/**Allows to peek into the next event to figure out its length and allocate
* the memory needed to store it before calling
* CAPIWebRtcNetworkDequeueEm
*
* @param {type} lIndex
* @returns {Number}
*/
export function CAPIWebRtcNetworkPeekEventDataLength(lIndex) {
var lNetEvent = gCAPIWebRtcNetworkInstances[lIndex].Peek();
return CAPIWebRtcNetworkCheckEventLength(lNetEvent);
}
//helper
export function CAPIWebRtcNetworkCheckEventLength(lNetEvent: NetworkEvent) {
if (lNetEvent == null) {
//invalid event
return -1;
} else if (lNetEvent.RawData == null) {
//no data
return 0;
} else if (typeof lNetEvent.RawData === "string") {
//no user strings are allowed thus we get away with counting the characters
//(ASCII only!)
return lNetEvent.RawData.length;
} else //message event types 1 and 2 only? check for it?
{
//its not null and not a string. can only be a Uint8Array if we didn't
//mess something up in the implementation
return lNetEvent.RawData.length;
}
}
export function CAPIWebRtcNetworkEventDataToUint8Array(data: any, dataUint8Array: Uint8Array, dataOffset: number, dataLength: number) {
//data can be null, string or Uint8Array
//return value will be the length of data we used
if (data == null) {
return 0;
} else if ((typeof data) === "string") {
//in case we don't get a large enough array we need to cut off the string
var i = 0;
for (i = 0; i < data.length && i < dataLength; i++) {
dataUint8Array[dataOffset + i] = data.charCodeAt(i);
}
return i;
}
else {
var i = 0;
//in case we don't get a large enough array we need to cut off the string
for (i = 0; i < data.length && i < dataLength; i++) {
dataUint8Array[dataOffset + i] = data[i];
}
return i;
}
}
//Version for emscripten or anything that doesn't have a garbage collector.
// The memory for everything needs to be allocated before the call.
export function CAPIWebRtcNetworkDequeueEm(lIndex: number, lTypeIntArray: Int32Array, lTypeIntIndex: number, lConidIntArray: Int32Array, lConidIndex: number, lDataUint8Array: Uint8Array, lDataOffset: number, lDataLength: number, lDataLenIntArray: Int32Array, lDataLenIntIndex: number) {
var nEvt = CAPIWebRtcNetworkDequeue(lIndex);
if (nEvt == null)
return false;
lTypeIntArray[lTypeIntIndex] = nEvt.Type;
lConidIntArray[lConidIndex] = nEvt.ConnectionId.id;
//console.debug("event" + nEvt.netEventType);
var length = CAPIWebRtcNetworkEventDataToUint8Array(nEvt.RawData, lDataUint8Array, lDataOffset, lDataLength);
lDataLenIntArray[lDataLenIntIndex] = length; //return the length if so the user knows how much of the given array is used
return true;
}
export function CAPIWebRtcNetworkPeekEm(lIndex: number, lTypeIntArray: Int32Array, lTypeIntIndex: number, lConidIntArray: Int32Array, lConidIndex: number, lDataUint8Array: Uint8Array, lDataOffset: number, lDataLength: number, lDataLenIntArray: Int32Array, lDataLenIntIndex: number) {
var nEvt = CAPIWebRtcNetworkPeek(lIndex);
if (nEvt == null)
return false;
lTypeIntArray[lTypeIntIndex] = nEvt.Type;
lConidIntArray[lConidIndex] = nEvt.ConnectionId.id;
//console.debug("event" + nEvt.netEventType);
var length = CAPIWebRtcNetworkEventDataToUint8Array(nEvt.RawData, lDataUint8Array, lDataOffset, lDataLength);
lDataLenIntArray[lDataLenIntIndex] = length; //return the length if so the user knows how much of the given array is used
return true;
}
export function CAPIMediaNetwork_IsAvailable() : boolean{
return true;
}
export function CAPIMediaNetwork_Create(lJsonConfiguration):number {
let config = new NetworkConfig();
config = JSON.parse(lJsonConfiguration);
let mediaNetwork = new BrowserMediaNetwork(config);
var lIndex = gCAPIWebRtcNetworkInstancesNextIndex;
gCAPIWebRtcNetworkInstancesNextIndex++;
gCAPIWebRtcNetworkInstances[lIndex] = mediaNetwork;
return lIndex;
}
//Configure(config: MediaConfig): void;
export function CAPIMediaNetwork_Configure(lIndex:number, audio: boolean, video: boolean,
minWidth: number, minHeight: number,
maxWidth: number, maxHeight: number,
idealWidth: number, idealHeight: number,
minFps: number, maxFps: number, idealFps: number, deviceName: string = ""){
let config: MediaConfig = new MediaConfig();
config.Audio = audio;
config.Video = video;
config.MinWidth = minWidth;
config.MinHeight = minHeight;
config.MaxWidth = maxWidth;
config.MaxHeight = maxHeight;
config.IdealWidth = idealWidth;
config.IdealHeight = idealHeight;
config.MinFps = minFps;
config.MaxFps = maxFps;
config.IdealFps = idealFps;
config.VideoDeviceName = deviceName;
config.FrameUpdates = true;
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
mediaNetwork.Configure(config);
}
//GetConfigurationState(): MediaConfigurationState;
export function CAPIMediaNetwork_GetConfigurationState(lIndex: number): number{
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.GetConfigurationState() as number;
}
//Note: not yet glued to the C# version!
//GetConfigurationError(): string;
export function CAPIMediaNetwork_GetConfigurationError(lIndex: number): string {
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.GetConfigurationError();
}
//ResetConfiguration(): void;
export function CAPIMediaNetwork_ResetConfiguration(lIndex: number) : void {
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.ResetConfiguration();
}
//TryGetFrame(id: ConnectionId): RawFrame;
export function CAPIMediaNetwork_TryGetFrame(lIndex: number, lConnectionId: number,
lWidthInt32Array: Int32Array, lWidthIntArrayIndex: number,
lHeightInt32Array: Int32Array, lHeightIntArrayIndex: number,
lBufferUint8Array: Uint8Array, lBufferUint8ArrayOffset: number, lBufferUint8ArrayLength: number): boolean
{
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.TryGetFrame(new ConnectionId(lConnectionId));
if (frame == null || frame.Buffer == null) {
return false;
} else {
//TODO: copy frame over
lWidthInt32Array[lWidthIntArrayIndex] = frame.Width;
lHeightInt32Array[lHeightIntArrayIndex] = frame.Height;
for (let i = 0; i < lBufferUint8ArrayLength && i < frame.Buffer.length; i++) {
lBufferUint8Array[lBufferUint8ArrayOffset + i] = frame.Buffer[i];
}
return true;
}
}
//Returns the frame buffer size or -1 if no frame is available
export function CAPIMediaNetwork_TryGetFrameDataLength(lIndex: number, connectionId: number) : number {
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.PeekFrame(new ConnectionId(connectionId));
let length: number = -1;
//added frame.Buffer != null as the frame might be a LazyFrame just creating a copy of the html video element
//in the moment frame.Buffer is called. if this fails for any reasion it might return null despite
//the frame object itself being available
if (frame != null && frame.Buffer != null) {
length = frame.Buffer.length;
}
//SLog.L("data length:" + length);
return length;
}
export function CAPIMediaNetwork_SetVolume(lIndex: number, volume: number, connectionId: number) : void {
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
mediaNetwork.SetVolume(volume, new ConnectionId(connectionId));
}
export function CAPIMediaNetwork_HasAudioTrack(lIndex: number, connectionId: number): boolean
{
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.HasAudioTrack(new ConnectionId(connectionId));
}
export function CAPIMediaNetwork_HasVideoTrack(lIndex: number, connectionId: number): boolean {
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.HasVideoTrack(new ConnectionId(connectionId));
}
export function CAPIMediaNetwork_SetMute(lIndex: number, value: boolean)
{
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
mediaNetwork.SetMute(value);
}
export function CAPIMediaNetwork_IsMute(lIndex: number)
{
let mediaNetwork = gCAPIWebRtcNetworkInstances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.IsMute();
}
export function CAPI_DeviceApi_Update():void
{
DeviceApi.Update();
}
export function CAPI_DeviceApi_RequestUpdate():void
{
DeviceApi.RequestUpdate();
}
export function CAPI_DeviceApi_LastUpdate():number
{
return DeviceApi.LastUpdate;
}
export function CAPI_DeviceApi_Devices_Length():number{
return Object.keys(DeviceApi.Devices).length;
}
export function CAPI_DeviceApi_Devices_Get(index:number):string{
let keys = Object.keys(DeviceApi.Devices);
if(keys.length > index)
{
let key = keys[index];
return DeviceApi.Devices[key].label;
}
else
{
SLog.LE("Requested device with index " + index + " does not exist.");
return "";
}
}
/**
*
* @param loglevel
* None = 0,
* Errors = 1,
* Warnings = 2,
* Verbose = 3
*/
export function CAPI_SLog_SetLogLevel(loglevel:number)
{
if(loglevel < 0 || loglevel > 3)
{
SLog.LogError("Invalid log level " + loglevel);
return;
}
SLog.SetLogLevel(loglevel);
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from "./CAPI"
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//current setup needs to load everything as a module
export function some_random_export_1()
{
}
describe("BrowserApiTest_MediaStreamApi", () => {
beforeEach(()=>{
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
});
it("devices", (done) => {
navigator.mediaDevices.enumerateDevices()
.then(function(devices)
{
expect(devices).not.toBeNull();
devices.forEach(function(device) {
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
});
done();
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
fail();
});
});
it("devices2", (done) => {
let gStream;
let constraints = {video:{deviceId:undefined}, audio:{deviceId:undefined}} as MediaStreamConstraints;
navigator.mediaDevices.getUserMedia(constraints)
.then((stream)=>{
//if this stream stops the access to labels disapears again after
//a few ms (tested in firefox)
gStream = stream;
navigator.mediaDevices.enumerateDevices()
.then(function(devices)
{
expect(devices).not.toBeNull();
devices.forEach(function(device) {
expect(device.label).not.toBeNull();
expect(device.label).not.toBe("");
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
});
gStream.stop();
done();
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
fail();
});
})
.catch((err)=>{
console.log(err.name + ": " + err.message);
fail();
});
});
it("devices3", (done) => {
let gStream;
let constraints = {video: true, audio:false} as MediaStreamConstraints;
navigator.mediaDevices.getUserMedia(constraints)
.then((stream)=>{
//if this stream stops the access to labels disapears again after
//a few ms (tested in firefox)
gStream = stream;
navigator.mediaDevices.enumerateDevices()
.then(function(devices)
{
expect(devices).not.toBeNull();
devices.forEach(function(device) {
expect(device.label).not.toBeNull();
expect(device.label).not.toBe("");
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
});
gStream.stop();
done();
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
fail();
});
})
.catch((err)=>{
console.log(err.name + ": " + err.message);
fail();
});
});
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {ICall, NetworkConfig, ConnectionId,
MediaConfig, BrowserWebRtcCall, CallEventType,
DataMessageEventArgs, MessageEventArgs,
CallAcceptedEventArgs, CallEventArgs } from "../awrtc/index";
export class CallTestHelper
{
static CreateCall(video:boolean, audio: boolean) : ICall {
var nconfig = new NetworkConfig();
nconfig.SignalingUrl = "wss://signaling.because-why-not.com:443/test";
var call = new BrowserWebRtcCall(nconfig);
return call;
}
}
describe("CallTest", () => {
var originalTimeout;
beforeEach(() => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
});
afterEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL =originalTimeout;
});
it("CallTest normal", () => {
expect(true).toBe(true);
});
it("CallTest async", (done) => {
setTimeout(()=>{
expect(true).toBe(true);
done();
}, 1000);
});
it("Send test", (done) => {
var call1 : ICall = null;
var call2 : ICall = null;
let call1ToCall2:ConnectionId;
let call2ToCall1:ConnectionId;
var address = "webunittest";
var teststring1 = "teststring1";
var teststring2 = "teststring2";
var testdata1 = new Uint8Array([1, 2]);
var testdata2 = new Uint8Array([3, 4]);
call1 = CallTestHelper.CreateCall(false, false);
expect(call1).not.toBeNull();
call2 = CallTestHelper.CreateCall(false, false);
expect(call2).not.toBeNull();
expect(true).toBe(true);
var mconfig = new MediaConfig();
mconfig.Audio = false;
mconfig.Video = false;
call1.addEventListener((sender: any, args: CallEventArgs)=>{
if(args.Type == CallEventType.ConfigurationComplete)
{
console.debug("call1 ConfigurationComplete");
call2.Configure(mconfig);
}else if(args.Type == CallEventType.WaitForIncomingCall)
{
console.debug("call1 WaitForIncomingCall");
call2.Call(address);
}else if(args.Type == CallEventType.CallAccepted)
{
let ar = args as CallAcceptedEventArgs;
call1ToCall2 = ar.ConnectionId;
//wait for message
}else if(args.Type == CallEventType.Message)
{
console.debug("call1 Message");
var margs = args as MessageEventArgs;
expect(margs.Content).toBe(teststring1);
expect(margs.Reliable).toBe(true);
call1.Send(teststring2, false, call1ToCall2);
}else if(args.Type == CallEventType.DataMessage)
{
console.debug("call1 DataMessage");
var dargs = args as DataMessageEventArgs;
expect(dargs.Reliable).toBe(true);
var recdata = dargs.Content;
expect(testdata1[0]).toBe(recdata[0]);
expect(testdata1[1]).toBe(recdata[1]);
console.debug("call1 send DataMessage");
call1.SendData(testdata2, false, call1ToCall2)
}else{
console.error("unexpected event: " + args.Type);
expect(true).toBe(false);
}
});
call2.addEventListener((sender: any, args: CallEventArgs)=>{
if(args.Type == CallEventType.ConfigurationComplete)
{
console.debug("call2 ConfigurationComplete");
call1.Listen(address);
}else if(args.Type == CallEventType.CallAccepted)
{
let ar = args as CallAcceptedEventArgs;
call2ToCall1 = ar.ConnectionId;
expect(call2ToCall1).toBeDefined();
call2.Send(teststring1);
}else if(args.Type == CallEventType.Message)
{
console.debug("call2 Message");
var margs = args as MessageEventArgs;
expect(margs.Content).toBe(teststring2);
expect(margs.Reliable).toBe(false);
console.debug("call2 send DataMessage " + call2ToCall1.id);
call2.SendData(testdata1, true, call2ToCall1)
}else if(args.Type == CallEventType.DataMessage)
{
console.debug("call2 DataMessage");
var dargs = args as DataMessageEventArgs;
expect(dargs.Reliable).toBe(false);
var recdata = dargs.Content;
expect(testdata2[0]).toBe(recdata[0]);
expect(testdata2[1]).toBe(recdata[1]);
done();
}else{
console.error("unexpected event: " + args.Type);
expect(true).toBe(false);
}
});
setInterval(()=>{
call1.Update();
call2.Update();
}, 50);
call1.Configure(mconfig);
});
});
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//current setup needs to load everything as a module
import {DeviceApi, CAPI_DeviceApi_Update,
CAPI_DeviceApi_RequestUpdate, CAPI_DeviceApi_Devices_Length,
CAPI_DeviceApi_Devices_Get} from "../awrtc/index"
export function DeviceApiTest_export()
{
}
describe("DeviceApiTest", () => {
beforeEach(()=>{
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
DeviceApi.Reset();
});
function printall()
{
console.log("current DeviceApi.Devices:");
for(let k in DeviceApi.Devices)
{
let v = DeviceApi.Devices[k];
console.log(v.deviceId + " defaultLabel:" + v.defaultLabel + " label:" + v.label + " guessed:" + v.isLabelGuessed);
}
}
it("update", (done) => {
let update1complete = false;
let update2complete = false;
let deviceCount = 0;
expect(Object.keys(DeviceApi.Devices).length).toBe(0);
//first without device labels
let updatecall1 = ()=>{
expect(update1complete).toBe(false);
expect(update2complete).toBe(false);
console.debug("updatecall1");
printall();
let devices1 = DeviceApi.Devices;
deviceCount = Object.keys(devices1).length;
expect(deviceCount).toBeGreaterThan(0);
let key1 = Object.keys(devices1)[0];
expect(devices1[key1].label).toBe("videoinput 1");
expect(devices1[key1].isLabelGuessed).toBe(true);
if(deviceCount > 1)
{
let key2 = Object.keys(devices1)[1];
expect(devices1[key2].label).toBe("videoinput 2");
expect(devices1[key2].isLabelGuessed).toBe(true);
}
DeviceApi.RemOnChangedHandler(updatecall1);
//second call with device labels
let updatecall2 = ()=>{
console.debug("updatecall2");
printall();
//check if the handler work properly
expect(update1complete).toBe(true);
expect(update2complete).toBe(false);
//sadly can't simulate fixed device names for testing
let devices2 = DeviceApi.Devices;
expect(Object.keys(devices2).length).toBe(deviceCount);
let key2 = Object.keys(devices2)[0];
//should have original label now
expect(devices2[key1].label).not.toBe("videodevice 1");
//and not be guessed anymore
expect(devices2[key1].isLabelGuessed).toBe(false);
update2complete = true;
DeviceApi.Reset();
expect(Object.keys(DeviceApi.Devices).length).toBe(0);
done();
}
update1complete = true;
DeviceApi.AddOnChangedHandler(updatecall2);
DeviceApi.RequestUpdate();
};
DeviceApi.AddOnChangedHandler(updatecall1);
DeviceApi.Update();
});
it("capi_update", (done) => {
let update1complete = false;
let update2complete = false;
let deviceCount = 0;
expect(CAPI_DeviceApi_Devices_Length()).toBe(0);
CAPI_DeviceApi_Update();
setTimeout(()=>{
expect(CAPI_DeviceApi_Devices_Length()).not.toBe(0);
expect(CAPI_DeviceApi_Devices_Length()).toBe(Object.keys(DeviceApi.Devices).length);
let keys = Object.keys(DeviceApi.Devices);
let counter = 0;
for(let k of keys)
{
let expectedVal = DeviceApi.Devices[k].label;
let actual = CAPI_DeviceApi_Devices_Get(counter);
expect(actual).toBe(expectedVal);
counter++;
}
done();
}, 100);
});
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { LocalNetwork, IBasicNetwork } from "../awrtc/index";
import { IBasicNetworkTest } from "helper/IBasicNetworkTest";
export class LocalNetworkTest extends IBasicNetworkTest {
public setup(): void {
super.setup();
//special tests
}
public _CreateNetworkImpl(): IBasicNetwork {
return new LocalNetwork();
}
}
describe("LocalNetworkTest", () => {
it("TestEnvironment", () => {
expect(null).toBeNull();
});
var test = new LocalNetworkTest();
test.setup();
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { BrowserMediaNetwork, NetworkConfig, MediaConfig,
ConnectionId, MediaEvent, MediaEventType,
MediaConfigurationState, NetEventType } from "../awrtc/index";
export class MediaNetworkTest{
createdNetworks:Array<BrowserMediaNetwork> = [];
createDefault() : BrowserMediaNetwork
{
let netConfig = new NetworkConfig();
netConfig.SignalingUrl = null;
let createdNetwork = new BrowserMediaNetwork(netConfig);
this.createdNetworks.push(createdNetwork);
return createdNetwork;
}
public setup(): void {
beforeEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
})
afterEach(() => {
for(let net of this.createdNetworks)
net.Dispose();
this.createdNetworks = new Array<BrowserMediaNetwork>();
})
it("FrameUpdates", (done) => {
let mediaConfig = new MediaConfig();
let network = this.createDefault();
network.Configure(mediaConfig);
setInterval(()=>{
network.Update();
let localFrame = network.TryGetFrame(ConnectionId.INVALID);
if(localFrame != null)
{
expect(localFrame.Height).toBeGreaterThan(0);
expect(localFrame.Width).toBeGreaterThan(0);
expect(localFrame.Buffer).not.toBeNull();
done();
}
network.Flush();
}, 10);
});
it("MediaEvent", (done) => {
let mediaConfig = new MediaConfig();
let network = this.createDefault();
network.Configure(mediaConfig);
setInterval(()=>{
network.Update();
let evt : MediaEvent = null;
while((evt = network.DequeueMediaEvent()) != null)
{
expect(evt.EventType).toBe(MediaEventType.StreamAdded);
expect(evt.Args.videoHeight).toBeGreaterThan(0);
expect(evt.Args.videoWidth).toBeGreaterThan(0);
done();
}
network.Flush();
}, 10);
});
it("MediaEventRemote", (done) => {
let testaddress = "testaddress" + Math.random();
let sender = this.createDefault();
let receiver = this.createDefault();
let configureComplete = false;
let senderFrame = false;
let receiverFrame = false;
sender.Configure(new MediaConfig());
setInterval(()=>{
sender.Update();
receiver.Update();
if(configureComplete == false)
{
let state = sender.GetConfigurationState();
if(state == MediaConfigurationState.Successful)
{
configureComplete = true;
sender.StartServer(testaddress);
}else if(state == MediaConfigurationState.Failed)
{
fail();
}
}
let sndEvt = sender.Dequeue();
if(sndEvt != null)
{
console.log("sender event: " + sndEvt);
if(sndEvt.Type == NetEventType.ServerInitialized)
{
receiver.Connect(testaddress);
}
}
let recEvt = receiver.Dequeue();
if(recEvt != null)
{
console.log("receiver event: " + recEvt);
}
let evt : MediaEvent = null;
while((evt = sender.DequeueMediaEvent()) != null)
{
expect(evt.EventType).toBe(MediaEventType.StreamAdded);
expect(evt.Args.videoHeight).toBeGreaterThan(0);
expect(evt.Args.videoWidth).toBeGreaterThan(0);
senderFrame = true;
console.log("sender received first frame");
}
while((evt = receiver.DequeueMediaEvent()) != null)
{
expect(evt.EventType).toBe(MediaEventType.StreamAdded);
expect(evt.Args.videoHeight).toBeGreaterThan(0);
expect(evt.Args.videoWidth).toBeGreaterThan(0);
receiverFrame = true;
console.log("receiver received first frame");
}
sender.Flush();
receiver.Flush();
if(senderFrame && receiverFrame)
done();
}, 10);
});
}
}
describe("MediaNetworkTest", () => {
it("TestEnvironment", () => {
expect(null).toBeNull();
});
var test = new MediaNetworkTest();
test.setup();
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { WebsocketTest } from "WebsocketNetworkTest";
import { IBasicNetworkTest } from "helper/IBasicNetworkTest";
import { NetworkEvent, IBasicNetwork, NetEventType, WebsocketNetwork,
ConnectionId, SignalingConfig, LocalNetwork, WebRtcNetwork }
from "../awrtc/index";
export class WebRtcNetworkTest extends IBasicNetworkTest {
public static sUrl = 'ws://localhost:12776/test';
public static sUrlShared = 'ws://localhost:12776/testshared';
public static sDefaultIceServer = { urls: ["stun:stun.l.google.com:19302"] } as RTCIceServer;
private mUrl = WebsocketTest.sUrl;
//allows each test to overwrite the default behaviour
private mUseWebsockets = false;
//will set use websocket flag for each test
public static mAlwaysUseWebsockets = false;
public setup(): void {
beforeEach(() => {
this.mUrl = WebsocketTest.sUrl;
this.mUseWebsockets = WebRtcNetworkTest.mAlwaysUseWebsockets;
})
it("SharedAddress", (done) => {
//turn off websockets and use shared websockets for this test as local network doesn't support shared mode
this.mUseWebsockets = true;
this.mUrl = WebsocketTest.sUrlShared;
var sharedAddress = "sharedtestaddress";
var evt: NetworkEvent;
var net1: IBasicNetwork;
var net2: IBasicNetwork;
this.thenAsync((finished) => {
net1 = this._CreateNetwork();
net1.StartServer(sharedAddress);
this.waitForEvent(net1, finished);
});
this.thenAsync((finished) => {
evt = net1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
net2 = this._CreateNetwork() as WebsocketNetwork;
net2.StartServer(sharedAddress);
this.waitForEvent(net2, finished);
});
this.thenAsync((finished) => {
evt = net2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
this.waitForEvent(net1, finished);
});
this.thenAsync((finished) => {
evt = net1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
this.waitForEvent(net2, finished);
});
this.then(() => {
evt = net2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
done();
});
this.start();
});
//connect using only direct local connections (give no ice servers)
it("ConnectLocalOnly", (done) => {
var srv: IBasicNetwork;
var address: string;
var clt: IBasicNetwork;
var cltId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
srv = this._CreateNetwork();
this._CreateServerNetwork((rsrv, raddress) => {
srv = rsrv;
address = raddress;
finished();
});
});
this.thenAsync((finished) => {
clt = this._CreateNetwork();
cltId = clt.Connect(address);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).toBe(cltId.id);
});
this.thenAsync((finished) => {
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).not.toBe(ConnectionId.INVALID.id);
done();
});
this.start();
});
super.setup();
//special tests
}
public _CreateNetworkImpl(): IBasicNetwork {
let rtcConfig: RTCConfiguration = { iceServers: [WebRtcNetworkTest.sDefaultIceServer]};
var sigConfig: SignalingConfig;
if (this.mUseWebsockets) {
sigConfig = new SignalingConfig(new WebsocketNetwork(this.mUrl));
}
else {
sigConfig = new SignalingConfig(new LocalNetwork());
}
return new WebRtcNetwork(sigConfig, rtcConfig);
}
}
describe("WebRtcNetworkTest", () => {
it("TestEnvironment", () => {
expect(null).toBeNull();
});
var test = new WebRtcNetworkTest();
test.mDefaultWaitTimeout = 5000;
test.setup();
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { NetworkEvent, WebsocketNetwork, NetEventType,
WebsocketConnectionStatus, ConnectionId, IBasicNetwork }
from "../awrtc/index";
import { IBasicNetworkTest } from "helper/IBasicNetworkTest";
export class WebsocketTest extends IBasicNetworkTest {
//replace with valid url that has a server behind it
//public static sUrl = 'ws://localhost:12776/test';
//public static sUrlShared = 'ws://localhost:12776/testshared';
public static sUrl = 'ws://signaling.because-why-not.com';
public static sUrlShared = 'ws://signaling.because-why-not.com/testshared';
//any url to simulate offline server
public static sBadUrl = 'ws://localhost:13776';
private mUrl;
public setup(): void {
super.setup();
//special tests
beforeEach(() => {
this.mUrl = WebsocketTest.sUrl;
});
it("SharedAddress", (done) => {
this.mUrl = WebsocketTest.sUrlShared;
var sharedAddress = "sharedtestaddress";
var evt: NetworkEvent;
var net1: WebsocketNetwork;
var net2: WebsocketNetwork;
this.thenAsync((finished) => {
net1 = this._CreateNetwork() as WebsocketNetwork;
net1.StartServer(sharedAddress);
this.waitForEvent(net1, finished);
});
this.thenAsync((finished) => {
evt = net1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
net2 = this._CreateNetwork() as WebsocketNetwork;
net2.StartServer(sharedAddress);
this.waitForEvent(net2, finished);
});
this.thenAsync((finished) => {
evt = net2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
this.waitForEvent(net1, finished);
});
this.thenAsync((finished) => {
evt = net1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
this.waitForEvent(net2, finished);
});
this.then(() => {
evt = net2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
done();
});
this.start();
});
it("BadUrlStartServer", (done) => {
this.mUrl = WebsocketTest.sBadUrl;
var evt: NetworkEvent;
var srv: WebsocketNetwork;
this.thenAsync((finished) => {
srv = this._CreateNetwork() as WebsocketNetwork;
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
srv.StartServer();
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.Connecting);
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitFailed);
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
done();
});
this.start();
});
it("BadUrlConnect", (done) => {
this.mUrl = WebsocketTest.sBadUrl;
var evt: NetworkEvent;
var clt: WebsocketNetwork;
var cltId: ConnectionId;
this.thenAsync((finished) => {
clt = this._CreateNetwork() as WebsocketNetwork;
expect(clt.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
cltId = clt.Connect("invalid address");
expect(clt.getStatus()).toBe(WebsocketConnectionStatus.Connecting);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(clt.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
done();
});
this.start();
});
it("WebsocketState", (done) => {
var srv: WebsocketNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: WebsocketNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv as WebsocketNetwork;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt as WebsocketNetwork;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
//both should be connected
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.Connected);
expect(clt.getStatus()).toBe(WebsocketConnectionStatus.Connected);
srv.Disconnect(srvToCltId);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
this.waitForEvent(clt, finished);
});
this.thenAsync((finished) => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
//after disconnect the client doesn't have any active connections -> expect disconnected
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.Connected);
expect(clt.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
srv.StopServer();
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerClosed);
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
srv.StartServer(address);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
this._Connect(srv, address, clt, (srvToCltIdOut, cltToSrvIdOut) => {
finished();
});
});
this.then(() => {
done();
});
this.start();
});
}
public _CreateNetworkImpl(): IBasicNetwork {
//let url = 'ws://because-why-not.com:12776';
return new WebsocketNetwork(this.mUrl);
}
}
describe("WebsocketNetworkTest", () => {
it("TestEnvironment", () => {
expect(null).toBeNull();
});
beforeEach(() => {
});
var test = new WebsocketTest();
test.setup();
});
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { IBasicNetwork, NetworkEvent, ConnectionId, NetEventType }
from "../../awrtc/network/index";
export interface Task {
(): void;
}
export interface AsyncTask {
(finished: Task): void;
}
export class TestTaskRunner {
public _toDoList = new Array<AsyncTask>();
public then(syncTask: Task) {
var wrap = (finished: Task) => {
syncTask();
finished();
};
this._toDoList.push(wrap);
}
public thenAsync(task: AsyncTask) {
this._toDoList.push(task);
}
public start() {
var task = this._toDoList.shift();
this._run(task);
}
public stop() {
}
private _run(task: AsyncTask): void {
task(() => {
if (this._toDoList.length > 0) {
setTimeout(() => {
this._run(this._toDoList.shift());
}, 1);
}
});
}
}
export abstract class BasicNetworkTestBase {
private mTestRunner = new TestTaskRunner();
private mCreatedNetworks = new Array<IBasicNetwork>();
public mDefaultWaitTimeout = 5000;
public abstract _CreateNetworkImpl(): IBasicNetwork;
public setup(): void {
beforeEach(() => {
this.mTestRunner.stop();
this.mTestRunner = new TestTaskRunner();
this.mCreatedNetworks = new Array<IBasicNetwork>();
});
}
public _CreateNetwork(): IBasicNetwork {
let net = this._CreateNetworkImpl();
this.mCreatedNetworks.push(net);
return net;
}
public then(syncTask: Task) {
this.mTestRunner.then(syncTask);
}
public thenAsync(task: AsyncTask) {
this.mTestRunner.thenAsync(task);
}
public start() {
this.mTestRunner.start();
}
//public waitForEvent(net: IBasicNetwork) {
// var wrap = (finished: Task) => {
// var timeout = 1000;
// var interval = 100;
// var intervalHandle;
// intervalHandle = setInterval(() => {
// this.UpdateAll();
// this.FlushAll();
// timeout -= interval;
// if (net.Peek() != null) {
// clearInterval(intervalHandle);
// finished();
// } else if (timeout <= 0) {
// clearInterval(intervalHandle);
// finished();
// }
// }, interval);
// };
// this.mTestRunner.thenAsync(wrap);
//}
public waitForEvent(net: IBasicNetwork, finished : Task, timeout?:number) {
if (timeout == null)
timeout = this.mDefaultWaitTimeout;
var interval = 50;
var intervalHandle;
intervalHandle = setInterval(() => {
this.UpdateAll();
this.FlushAll();
timeout -= interval;
if (net.Peek() != null) {
clearInterval(intervalHandle);
finished();
} else if (timeout <= 0) {
clearInterval(intervalHandle);
finished();
}
}, interval);
}
public UpdateAll(): void {
for (let v of this.mCreatedNetworks) {
v.Update();
}
}
public FlushAll(): void {
for (let v of this.mCreatedNetworks) {
v.Flush();
}
}
public ShutdownAll(): void {
for (let v of this.mCreatedNetworks) {
v.Shutdown();
}
this.mCreatedNetworks = new Array<IBasicNetwork>();
}
public _CreateServerNetwork(result: (IBasicNetwork, string) => void)
{
var srv = this._CreateNetwork();
srv.StartServer();
this.waitForEvent(srv, () => {
var evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
expect(evt.Info).not.toBeNull();
var address = evt.Info;
result(srv, address);
});
}
public _Connect(srv: IBasicNetwork, address: string, clt: IBasicNetwork, result: (srvToCltId: ConnectionId, cltToSrvId: ConnectionId) => void) {
var evt: NetworkEvent;
var cltToSrvId = clt.Connect(address);
var srvToCltId: ConnectionId;
this.waitForEvent(clt, () => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).toBe(cltToSrvId.id);
this.waitForEvent(srv, () => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).not.toBe(ConnectionId.INVALID.id);
srvToCltId = evt.ConnectionId;
result(srvToCltId, cltToSrvId);
});
});
}
public _CreateServerClient(result: (srv: IBasicNetwork, address: string, srvToCltId: ConnectionId, clt: IBasicNetwork, cltToSrvId: ConnectionId) => void) {
let srv: IBasicNetwork;
let address: string;
let srvToCltId: ConnectionId;
let clt: IBasicNetwork;
let cltToSrvId: ConnectionId;
this._CreateServerNetwork((rsrv, raddress) => {
srv = rsrv;
address = raddress;
clt = this._CreateNetwork();
this._Connect(srv, address, clt, (rsrvToCltId, rcltToSrvId) => {
srvToCltId = rsrvToCltId;
cltToSrvId = rcltToSrvId;
result(srv, address, srvToCltId, clt, cltToSrvId);
});
});
}
}
\ No newline at end of file
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { BasicNetworkTestBase, Task } from "./BasicNetworkTestBase";
import { IBasicNetwork, NetworkEvent, NetEventType, ConnectionId, Encoding } from "../../awrtc/network/index";
export abstract class IBasicNetworkTest extends BasicNetworkTestBase {
public setup(): void {
super.setup();
let originalTimeout = 5000;
beforeEach(() => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = this.mDefaultWaitTimeout + 5000;
});
afterEach(() => {
this.ShutdownAll();
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = this.mDefaultWaitTimeout + 5000;
});
//add all reusable tests here
//TODO: check how to find the correct line where it failed
it("TestEnvironmentAsync", (done) => {
let value1 = false;
let value2 = false;
this.then(() => {
expect(value1).toBe(false);
expect(value2).toBe(false);
value1 = true;
});
this.thenAsync((finished: Task) => {
expect(value1).toBe(true);
expect(value2).toBe(false);
value2 = true;
finished();
});
this.then(() => {
expect(value1).toBe(true);
expect(value2).toBe(true);
done();
});
this.start();
});
it("Create", () => {
let clt: IBasicNetwork;
clt = this._CreateNetwork();
expect(clt).not.toBe(null);
});
it("StartServer", (done) => {
var evt: NetworkEvent;
var srv: IBasicNetwork;
this.thenAsync((finished) => {
srv = this._CreateNetwork();
srv.StartServer();
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
expect(evt.Info).not.toBe(null);
done();
});
this.start();
});
it("StartServerNamed", (done) => {
var name = "StartServerNamedTest";
var evt: NetworkEvent;
var srv1: IBasicNetwork;
var srv2: IBasicNetwork;
srv1 = this._CreateNetwork();
srv2 = this._CreateNetwork();
this.thenAsync((finished) => {
srv1.StartServer(name);
this.waitForEvent(srv1, finished);
});
this.then(() => {
evt = srv1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
expect(evt.Info).toBe(name);
});
this.thenAsync((finished) => {
srv2.StartServer(name);
this.waitForEvent(srv2, finished);
});
this.thenAsync((finished) => {
//expect the server start to fail because the address is in use
evt = srv2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitFailed);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
expect(evt.Info).toBe(name);
//stop the other server to free the address
srv1.StopServer();
this.waitForEvent(srv1, finished);
});
this.thenAsync((finished) => {
//expect the server start to fail because the address is in use
evt = srv1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerClosed);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
//stop the other server to free the address
srv2.StartServer(name);
this.waitForEvent(srv2, finished);
});
this.thenAsync((finished) => {
//expect the server start to fail because the address is in use
evt = srv2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
done();
});
this.start();
});
it("StopServer", (done) => {
var evt: NetworkEvent;
var srv: IBasicNetwork;
this.thenAsync((finished) => {
srv = this._CreateNetwork();
srv.StopServer();
this.waitForEvent(srv, finished, 100);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).toBeNull();
done();
});
this.start();
});
it("StopServer2", (done) => {
var evt: NetworkEvent;
var srv: IBasicNetwork;
this.thenAsync((finished) => {
srv = this._CreateNetwork();
srv.StartServer();
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerInitialized);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
expect(evt.Info).not.toBe(null);
});
this.thenAsync((finished) => {
srv.StopServer();
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerClosed);
expect(evt.ConnectionId.id).toBe(ConnectionId.INVALID.id);
//enforce address in this to prepare having multiple addresses?
//expect(evt.Info).not.toBe(null);
done();
});
this.start();
});
it("_CreateServerNetwork", (done) => {
var srv: IBasicNetwork;
var address: string;
this.thenAsync((finished) => {
this._CreateServerNetwork((rsrv, raddress) => {
srv = rsrv;
address = raddress;
finished();
});
});
this.then(() => {
expect(srv).not.toBeNull();
expect(address).not.toBeNull();
done();
});
this.start();
});
it("ConnectFail", (done) => {
var evt: NetworkEvent;
var clt: IBasicNetwork;
var cltId: ConnectionId;
this.thenAsync((finished) => {
clt = this._CreateNetwork();
cltId = clt.Connect("invalid address");
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId.id);
done();
});
this.start();
});
it("ConnectTwo", (done) => {
var srv: IBasicNetwork;
var address: string;
var clt: IBasicNetwork;
var cltId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
srv = this._CreateNetwork();
this._CreateServerNetwork((rsrv, raddress) => {
srv = rsrv;
address = raddress;
finished();
});
});
this.thenAsync((finished) => {
clt = this._CreateNetwork();
cltId = clt.Connect(address);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).toBe(cltId.id);
});
this.thenAsync((finished) => {
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.NewConnection);
expect(evt.ConnectionId.id).not.toBe(ConnectionId.INVALID.id);
done();
});
this.start();
});
it("ConnectHelper", (done) => {
var srv: IBasicNetwork;
var address: string;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var srvToCltId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv: IBasicNetwork, raddress: string, rsrvToCltId: ConnectionId, rclt: IBasicNetwork, rcltToSrvId: ConnectionId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
done();
});
});
this.start();
});
it("Peek", (done) => {
var evt: NetworkEvent;
var net = this._CreateNetwork();
var cltId1 = net.Connect("invalid address1");
var cltId2 = net.Connect("invalid address2");
var cltId3 = net.Connect("invalid address3");
this.thenAsync((finished) => {
this.waitForEvent(net, finished);
});
this.thenAsync((finished) => {
evt = net.Peek();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId1.id);
evt = net.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId1.id);
this.waitForEvent(net, finished);
});
this.thenAsync((finished) => {
evt = net.Peek();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId2.id);
evt = net.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId2.id);
this.waitForEvent(net, finished);
});
this.thenAsync((finished) => {
evt = net.Peek();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId3.id);
evt = net.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ConnectionFailed);
expect(evt.ConnectionId.id).toBe(cltId3.id);
done();
});
this.start();
});
it("Disconnect", (done) => {
var evt: NetworkEvent;
var clt = this._CreateNetwork();
this.thenAsync((finished) => {
clt.Disconnect(ConnectionId.INVALID);
this.waitForEvent(clt, finished, 100);
});
this.thenAsync((finished) => {
evt = clt.Dequeue();
expect(evt).toBeNull();
clt.Disconnect(new ConnectionId(1234));
this.waitForEvent(clt, finished, 100);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).toBeNull();
done();
});
this.start();
});
it("DisconnectClient", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
clt.Disconnect(cltToSrvId);
this.waitForEvent(clt, finished);
});
this.thenAsync((finished) => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
done();
});
this.start();
});
it("DisconnectServer", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
srv.Disconnect(srvToCltId);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
done();
});
this.start();
});
it("DisconnectServerMulti", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToClt1Id: ConnectionId;
var srvToClt2Id: ConnectionId;
var clt1: IBasicNetwork;
var clt1ToSrvId: ConnectionId;
var clt2: IBasicNetwork;
var clt2ToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToClt1Id = rsrvToCltId;
clt1 = rclt;
clt1ToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
clt2 = this._CreateNetwork();
this._Connect(srv, address, clt2, (rsrvToCltId, rcltToSrvId) => {
srvToClt2Id = rsrvToCltId;
clt2ToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
srv.Disconnect(srvToClt1Id);
srv.Disconnect(srvToClt2Id);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToClt1Id.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToClt2Id.id).toBe(evt.ConnectionId.id);
this.waitForEvent(clt1, finished);
});
this.thenAsync((finished) => {
evt = clt1.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(clt1ToSrvId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(clt2, finished);
});
this.then(() => {
evt = clt2.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(clt2ToSrvId.id).toBe(evt.ConnectionId.id);
done();
});
this.start();
});
it("ShutdownEmpty", (done) => {
var net: IBasicNetwork;
var evt: NetworkEvent;
net = this._CreateNetwork();
this.thenAsync((finished) => {
net.Shutdown();
this.waitForEvent(net, finished);
});
this.then(() => {
evt = net.Dequeue();
expect(evt).toBeNull();
done();
});
this.start();
});
it("ShutdownServer", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
srv.Shutdown();
this.waitForEvent(clt, finished);
});
this.thenAsync((finished) => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerClosed);
expect(evt.ConnectionId).toBe(ConnectionId.INVALID);
this.waitForEvent(srv, finished, 100);
});
this.then(() => {
//no further events are suppose to be triggered after shutdown
evt = srv.Dequeue();
expect(evt).toBeNull();
done();
});
this.start();
});
it("ShutdownClient", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
clt.Shutdown();
this.waitForEvent(clt, finished);
});
this.thenAsync((finished) => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.Disconnected);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
this.waitForEvent(srv, finished, 100);
});
this.then(() => {
evt = srv.Dequeue();
expect(evt).toBeNull();
done();
});
this.start();
});
it("DisconnectInvalid", (done) => {
var evt: NetworkEvent;
var clt = this._CreateNetwork();
clt.Disconnect(ConnectionId.INVALID);
clt.Disconnect(new ConnectionId(1234));
this.thenAsync((finished) => {
this.waitForEvent(clt, finished, 100);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).toBeNull();
});
this.then(() => {
done();
});
this.start();
});
it("SendDataTolerateInvalidDestination", (done) => {
var evt: NetworkEvent;
var clt = this._CreateNetwork();
var testData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]);
this.thenAsync((finished) => {
clt.SendData(ConnectionId.INVALID, testData, true);
this.waitForEvent(clt, finished, 100);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).toBeNull();
});
this.thenAsync((finished) => {
clt.SendData(ConnectionId.INVALID, testData, false);
this.waitForEvent(clt, finished, 100);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).toBeNull();
});
this.then(() => {
done();
});
this.start();
});
it("SendDataReliable", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
var testMessage: string = "SendDataReliable_testmessage1234";
var testMessageBytes = Encoding.UTF16.GetBytes(testMessage);
var receivedTestMessage: string;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
clt.SendData(cltToSrvId, testMessageBytes, true);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ReliableMessageReceived);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
receivedTestMessage = Encoding.UTF16.GetString(evt.MessageData);
expect(receivedTestMessage).toBe(testMessage);
srv.SendData(srvToCltId, testMessageBytes, true);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ReliableMessageReceived);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
receivedTestMessage = Encoding.UTF16.GetString(evt.MessageData);
expect(receivedTestMessage).toBe(testMessage);
done();
});
this.start();
});
it("SendDataUnreliable", (done) => {
var srv: IBasicNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IBasicNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
var testMessage: string = "SendDataUnreliable_testmessage1234";
var testMessageBytes = Encoding.UTF16.GetBytes(testMessage);
var receivedTestMessage: string;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.thenAsync((finished) => {
clt.SendData(cltToSrvId, testMessageBytes, false);
this.waitForEvent(srv, finished);
});
this.thenAsync((finished) => {
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.UnreliableMessageReceived);
expect(srvToCltId.id).toBe(evt.ConnectionId.id);
receivedTestMessage = Encoding.UTF16.GetString(evt.MessageData);
expect(receivedTestMessage).toBe(testMessage);
srv.SendData(srvToCltId, testMessageBytes, false);
this.waitForEvent(clt, finished);
});
this.then(() => {
evt = clt.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.UnreliableMessageReceived);
expect(cltToSrvId.id).toBe(evt.ConnectionId.id);
receivedTestMessage = Encoding.UTF16.GetString(evt.MessageData);
expect(receivedTestMessage).toBe(testMessage);
done();
});
this.start();
});
}
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export * from "./LocalNetworkTest"
export * from "./WebRtcNetworkTest"
export * from "./WebsocketNetworkTest"
export * from "./CallTest"
export * from "./MediaNetworkTest"
export * from "./BrowserApiTest"
export * from "./DeviceApiTest"
{
"extends": "../awrtc/tsconfig_base",
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"module": "es2015",
"declaration": false,
"outDir": "../build/test",
"baseUrl": "."
},
"files": [
"./helper/BasicNetworkTestBase.ts",
"./helper/IBasicNetworkTest.ts",
"WebsocketNetworkTest.ts",
"WebRtcNetworkTest.ts",
"CallTest.ts",
"LocalNetworkTest.ts",
"MediaNetworkTest.ts"
]
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
//AWRTC bundle for unity / direct java script usage
function build_awrtc_config()
{
return {
entry: './src/awrtc/index.ts',
output: {
filename: 'awrtc.js',
path: path.resolve(__dirname, 'build/bundle'),
library: "awrtc",
libraryTarget: 'umd'
},
mode:"development",
devtool: "eval-source-map", //unity can't handle separate source maps
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
plugins: [new TsconfigPathsPlugin({ configFile: "./src/awrtc/tsconfig.json" })]
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader"
},
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
}
]
},
}
}
configAwrtcDebug = build_awrtc_config();
configAwrtcRelease = build_awrtc_config();
configAwrtcRelease.mode = "production";
configAwrtcRelease.output.filename = "awrtc.min.js";
configAwrtcRelease.devtool = false;
//jasmine unit test bundle
configTest =
{
entry: './src/test/test_entry.ts',
output: {
filename: 'test.js',
path: path.resolve(__dirname, 'build/bundle')
},
mode:"development",
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
plugins: [new TsconfigPathsPlugin({ configFile: "./src/test/tsconfig.json" })]
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader"
},
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
};
//bundle of awrtc + several example + test apps
function default_examples_config()
{
return {
entry: './src/apps/entry.ts',
output: {
filename: 'apps.js',
path: path.resolve(__dirname, 'build/bundle')
},
mode:"development",
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
plugins: [new TsconfigPathsPlugin({ configFile: "./src/apps/tsconfig.json" })]
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader"
},
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
}
]
},
}
}
examplesConfigDebug = default_examples_config();
examplesConfigRelease = default_examples_config();
examplesConfigRelease.mode = "production";
examplesConfigRelease.output.filename = "apps.min.js";
examplesConfigRelease.devtool = false;
module.exports =
[
configAwrtcDebug,
configAwrtcRelease,
configTest,
examplesConfigDebug,
examplesConfigRelease
];
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment