Commit ba3e05cf by Иван Кубота

merge

parents c472dbe0 4d232112
* text=auto eol=lf
build/apps
build/awrtc
build/bundle
node_modules
.idea
yarn.lock
\ No newline at end of file
BSD 3-Clause License
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.
# MR 2021 Amazon Web # MR 2021 Amazon Web
# based on awrtc_browser repo
Run the following commands in the project root directory:
* npm install
* npm run build
Now you should have the final build in the ./build/bundle directory. You can test it using the test applications:
* ./build/callapp.html for the typescrypt example using ./build/bundle/apps.js (contains the merged source code from ./src/apps and ./src/awrtc )
* ./build/callapp_js.html for javascript example that runs the same app but as javascript within the html file so you can easily change the code and experiment. It is using the library only bundle from ./build/bundle/awrtc.js (source code at ./src/awrtc)
<!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>
<svg width="88" height="32" viewBox="0 0 88 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.63192 4H10.1722C11.1963 4.05596 12.2439 4.27109 13.12 4.82819C13.6976 5.19813 14.1739 5.74093 14.4158 6.38694C14.6912 7.1113 14.7154 7.89782 14.7167 8.66321C14.7142 10.1965 14.7154 11.7304 14.7148 13.2636C14.7117 13.7188 14.7832 14.1813 14.984 14.593C15.1619 14.9766 15.4435 15.2955 15.6754 15.645C15.8023 15.8253 15.8222 16.1057 15.6307 16.2499C15.0599 16.7368 14.4904 17.2255 13.9308 17.7254C13.7778 17.8578 13.6068 18.0139 13.3892 17.9965C13.1436 17.9909 12.9869 17.7739 12.8135 17.6303C12.3813 17.2504 12.0655 16.766 11.7465 16.2922C11.2031 16.8381 10.6162 17.366 9.90674 17.6856C9.2657 17.9797 8.55316 18.0717 7.85368 18.0817C6.83461 18.1196 5.75834 17.8249 5.02653 17.0837C4.33637 16.4029 4.04041 15.4205 4 14.4717V14.0458C4.03606 13.0578 4.37741 12.0661 5.04953 11.3293C5.74342 10.5447 6.7314 10.0877 7.73617 9.85078C8.8914 9.57285 10.0802 9.49202 11.2572 9.35834C11.2361 8.65326 11.3617 7.86984 10.9525 7.2456C10.6068 6.74197 9.94466 6.57036 9.36269 6.62259C8.76767 6.66425 8.16456 6.94839 7.84435 7.46819C7.69326 7.68891 7.63855 7.95378 7.55959 8.20497C7.50114 8.39275 7.32207 8.56933 7.11192 8.53637C6.39503 8.46611 5.68 8.37907 4.96311 8.30881C4.79088 8.29264 4.6 8.26155 4.4887 8.1142C4.33326 7.92394 4.4371 7.66964 4.48187 7.45824C4.71378 6.51254 5.2715 5.64394 6.06798 5.07627C7.09513 4.32953 8.38342 4.05098 9.63192 4ZM9.09907 11.6396C8.63212 11.8093 8.1913 12.1009 7.92643 12.5299C7.62114 13.018 7.55461 13.6199 7.6199 14.182C7.67834 14.6589 7.90218 15.1482 8.32808 15.4012C8.87586 15.7258 9.60145 15.6369 10.1057 15.2682C10.6852 14.8504 11.0259 14.1708 11.1652 13.4831C11.297 12.7749 11.2473 12.0512 11.2578 11.3349C10.5304 11.3405 9.78798 11.3884 9.09907 11.6396Z" fill="white"/>
<path d="M41.9044 4H42.4466C43.3674 4.04601 44.2995 4.2257 45.1214 4.65845C45.7942 5.01161 46.3699 5.57679 46.6653 6.28435C46.9712 7.01306 47.0048 7.81762 47.0072 8.59793C47.0054 10.1536 47.006 11.7092 47.0054 13.2655C47.0048 13.6665 47.0557 14.0738 47.2106 14.4468C47.3728 14.8566 47.6526 15.2023 47.9082 15.5561C48.0251 15.7146 48.1177 15.9329 48.0232 16.1237C47.9436 16.2475 47.8199 16.3333 47.7111 16.429C47.1913 16.8642 46.6839 17.3132 46.1759 17.7621C46.0454 17.8727 45.9005 17.9983 45.7183 17.9971C45.451 18.0126 45.2794 17.777 45.0953 17.6222C44.6669 17.2448 44.3529 16.7641 44.0377 16.2929C43.2978 17.0508 42.4242 17.7552 41.3573 17.9585C40.2195 18.1668 38.9598 18.1743 37.9364 17.5656C37.097 17.0812 36.5529 16.1878 36.3838 15.2458C36.1631 14.0645 36.3173 12.7699 37.0031 11.7602C37.5658 10.9134 38.4605 10.3333 39.418 10.0249C40.7523 9.59337 42.1637 9.51938 43.5478 9.35834C43.5304 8.7142 43.628 8.02839 43.3451 7.42591C43.1386 6.97326 42.6636 6.70466 42.1855 6.63813C41.5457 6.55047 40.84 6.70715 40.3712 7.17223C40.11 7.42218 39.9621 7.76166 39.8806 8.1086C39.8358 8.33927 39.6406 8.58301 39.3807 8.53513C38.6309 8.45617 37.881 8.37596 37.1318 8.29264C36.9073 8.27959 36.6729 8.09679 36.6972 7.8543C36.8533 6.74757 37.4638 5.70301 38.3822 5.05762C39.4012 4.32829 40.6709 4.05409 41.9044 4ZM41.2348 11.6999C40.7368 11.9057 40.2922 12.28 40.0796 12.7836C39.8676 13.2848 39.8352 13.8543 39.944 14.3847C40.0342 14.8155 40.2848 15.234 40.6858 15.4392C41.2236 15.7159 41.9032 15.6232 42.3869 15.2738C42.954 14.8715 43.2947 14.2149 43.4427 13.5478C43.5931 12.8203 43.5366 12.0723 43.5484 11.3343C42.7668 11.3418 41.9635 11.3946 41.2348 11.6999Z" fill="white"/>
<path d="M21.8278 4.91769C22.6404 4.16723 23.8385 3.99127 24.8931 4.20391C25.6143 4.34629 26.2504 4.79707 26.6707 5.39396C26.9132 5.7235 27.1028 6.08847 27.2688 6.46153C27.5679 5.65448 28.1405 4.93759 28.9115 4.54028C29.9225 4.01303 31.1753 3.98443 32.2143 4.44951C32.9486 4.77842 33.5778 5.38028 33.8651 6.14008C34.102 6.76432 34.1474 7.4408 34.1554 8.10298C34.1306 11.1073 34.1449 14.1123 34.1405 17.1173C34.1299 17.315 34.1797 17.5388 34.0553 17.7111C33.9546 17.8597 33.7687 17.9206 33.5952 17.9113C32.8068 17.9088 32.0184 17.9144 31.2301 17.9088C30.977 17.9131 30.7476 17.6874 30.7625 17.4313C30.7606 15.1917 30.7612 12.9515 30.7625 10.7113C30.7612 9.98443 30.8023 9.25386 30.719 8.52951C30.663 8.09241 30.4528 7.63044 30.0313 7.43894C29.5264 7.21945 28.904 7.22878 28.4352 7.5322C28.0814 7.7523 27.8184 8.10919 27.7096 8.51085C27.5629 9.04432 27.5536 9.60391 27.5442 10.1535C27.5424 12.5809 27.5455 15.0083 27.5424 17.435C27.5561 17.6912 27.3254 17.9156 27.0717 17.9088C26.2802 17.9144 25.4887 17.9088 24.6972 17.9113C24.4765 17.9274 24.2427 17.7975 24.1842 17.5755C24.1532 17.4257 24.1675 17.2715 24.165 17.1198C24.1662 14.9641 24.1612 12.8078 24.1637 10.6522C24.1631 9.88432 24.2234 9.10028 24.0269 8.34919C23.9163 7.92267 23.6458 7.5036 23.2124 7.35438C22.8058 7.21759 22.3432 7.22878 21.9478 7.39852C21.5523 7.57075 21.2931 7.95313 21.1637 8.3523C20.9728 8.93365 20.9548 9.5523 20.9449 10.1585C20.9424 12.584 20.9461 15.0089 20.943 17.4338C20.9567 17.6856 20.7328 17.9113 20.4823 17.9082C19.6709 17.9144 18.8595 17.9106 18.0481 17.9094C17.7932 17.9206 17.5488 17.6999 17.5668 17.4387C17.5656 13.2474 17.5656 9.05614 17.5668 4.86546C17.5469 4.6037 17.7888 4.37925 18.045 4.38982C18.7948 4.38484 19.5447 4.38733 20.2945 4.38857C20.5295 4.37427 20.7658 4.55831 20.7795 4.79894C20.7963 5.36785 20.7733 5.938 20.7907 6.50754C21.0512 5.9293 21.3409 5.34236 21.8278 4.91769Z" fill="white"/>
<path d="M65.2099 4.17417C66.2955 4.03303 67.4358 4.17169 68.4164 4.67531C69.2141 5.08008 69.8819 5.71614 70.3693 6.46226C70.9824 7.39925 71.3349 8.48671 71.5028 9.58848C71.7745 11.4301 71.6234 13.3843 70.7871 15.0699C70.3824 15.8826 69.8104 16.6206 69.081 17.1659C67.2829 18.535 64.544 18.5188 62.7907 17.0795C62.0215 16.4546 61.4601 15.6034 61.092 14.69C60.6176 13.5118 60.4373 12.231 60.4528 10.9663C60.469 9.74205 60.6767 8.50661 61.1704 7.38122C61.559 6.49645 62.1422 5.68319 62.92 5.10122C63.5853 4.59822 64.3849 4.28423 65.2099 4.17417ZM65.5158 6.77625C64.9357 6.94972 64.5751 7.50122 64.3973 8.04962C64.0727 9.04754 64.0845 10.1126 64.0864 11.1503C64.0945 12.0867 64.1274 13.0355 64.3712 13.9452C64.5086 14.4332 64.7156 14.9331 65.1161 15.2639C65.4493 15.5462 65.915 15.6183 66.3347 15.535C66.8228 15.4517 67.2226 15.0967 67.4483 14.6676C67.8555 13.896 67.932 13.0044 67.9768 12.1483C68.0079 10.9955 68.0079 9.83345 67.812 8.69376C67.7044 8.16464 67.5913 7.59822 67.2238 7.18039C66.8072 6.7091 66.0972 6.61521 65.5158 6.77625Z" fill="white"/>
<path d="M77.8951 4.8431C78.5088 4.29533 79.3575 4.09699 80.1633 4.1231C80.9685 4.1343 81.7899 4.39481 82.3874 4.9488C82.7772 5.3057 83.0806 5.75958 83.2448 6.26321C83.5196 7.08642 83.5569 7.9631 83.5855 8.82362V17.5513C83.5177 17.7652 83.3163 17.9206 83.09 17.9094C82.2823 17.9119 81.474 17.9138 80.6663 17.9082C80.4151 17.9119 80.182 17.6912 80.205 17.4344C80.2056 14.9455 80.2006 12.456 80.2075 9.96704C80.2037 9.38756 80.1957 8.79502 80.0178 8.23792C79.9009 7.8686 79.6634 7.51419 79.2978 7.35751C78.8129 7.14673 78.2147 7.20518 77.7963 7.53533C77.2846 7.91709 77.0769 8.56062 76.9557 9.16124C76.7865 10.1057 76.8668 11.07 76.8475 12.0238C76.8462 13.8263 76.85 15.6294 76.8462 17.4319C76.8587 17.6887 76.6274 17.9119 76.3743 17.9076C75.5853 17.9163 74.7963 17.9082 74.0079 17.9113C73.7766 17.9293 73.5254 17.7869 73.4837 17.5476C73.4539 17.3231 73.4726 17.0968 73.4694 16.8711C73.4688 12.8694 73.4657 8.86715 73.467 4.86487C73.4464 4.6031 73.6858 4.37989 73.9426 4.39046C74.6918 4.38424 75.4423 4.38611 76.1921 4.38984C76.4234 4.38051 76.6665 4.56331 76.6622 4.80704C76.6734 5.44186 76.6491 6.07792 76.6746 6.71274C76.9892 6.03813 77.3144 5.33492 77.8951 4.8431Z" fill="white"/>
<path d="M50.0041 4.80904C49.9892 4.54044 50.2479 4.32345 50.5096 4.35453C53.1826 4.35516 55.8568 4.35329 58.5297 4.35516C58.7815 4.33277 59.0309 4.54168 59.0215 4.80096C59.029 5.2592 59.0191 5.71868 59.0253 6.17754C59.0669 6.69422 58.6783 7.09091 58.4172 7.49319C57.0182 9.49215 55.618 11.4905 54.2203 13.4901C55.6448 13.4584 57.1034 13.6461 58.4029 14.2592C58.6062 14.3612 58.8201 14.4519 58.9979 14.5974C59.2012 14.7734 59.2752 15.0532 59.2671 15.3137C59.2628 15.9168 59.2709 16.5199 59.2634 17.123C59.2603 17.3661 59.0315 17.6117 58.7778 17.5663C58.6099 17.5135 58.4601 17.4177 58.3003 17.3469C55.8624 16.2177 52.9258 16.2078 50.4928 17.3556C50.3318 17.4283 50.1807 17.5241 50.0128 17.5794C49.7604 17.613 49.5428 17.3643 49.5434 17.1237C49.5372 16.6449 49.544 16.1661 49.5415 15.6874C49.544 15.2584 49.5154 14.8144 49.6646 14.4041C49.7498 14.1442 49.9252 13.9297 50.0775 13.7077C51.6182 11.4961 53.1602 9.28573 54.6997 7.07412C53.2827 7.07101 51.8651 7.07847 50.4481 7.07039C50.2056 7.07661 49.9886 6.85899 50.0041 6.61526C50.001 6.01339 50.0004 5.41091 50.0041 4.80904Z" fill="white"/>
<path d="M53.3399 18.5392C54.239 18.4453 55.1517 18.4547 56.044 18.6039C56.4164 18.6754 56.8 18.7519 57.1314 18.9459C57.4063 19.1019 57.4062 19.4594 57.428 19.7355C57.4436 20.7073 57.2197 21.6679 56.9132 22.585C56.4568 23.9001 55.7536 25.1697 54.6873 26.0868C54.5405 26.2136 54.3242 26.3473 54.1321 26.2404C53.9928 26.1409 54.0338 25.9488 54.0904 25.8145C54.5039 24.7749 54.9136 23.7297 55.2071 22.6485C55.3377 22.143 55.4626 21.6263 55.4489 21.1009C55.4402 20.8528 55.3762 20.5699 55.148 20.4331C54.7464 20.1944 54.2632 20.167 53.8087 20.1347C52.4956 20.0775 51.1861 20.2398 49.8836 20.3852C49.7604 20.3977 49.6075 20.3828 49.5403 20.264C49.4688 20.1266 49.5559 19.9705 49.6541 19.8723C49.8058 19.728 49.9911 19.6255 50.1683 19.516C51.1327 18.9508 52.2357 18.6605 53.3399 18.5392Z" fill="#FF9900"/>
<path d="M15.3745 19.355C15.6717 19.2138 15.9472 19.4656 16.1977 19.5937C22.0013 22.8803 28.6187 24.6741 35.2759 24.9191C41.2491 25.1342 47.2572 23.984 52.7604 21.6636C53.1372 21.4975 53.6806 21.7096 53.713 22.1547C53.7378 22.5359 53.3872 22.7659 53.1124 22.955C51.6624 23.9865 50.0844 24.8289 48.4473 25.521C46.0641 26.5245 43.5534 27.2159 40.9998 27.6182C39.8502 27.8109 38.6862 27.8855 37.5273 27.9999H34.9121C34.1213 27.9091 33.3254 27.8781 32.5351 27.7792C29.6191 27.4379 26.7497 26.6886 24.045 25.5433C20.8653 24.2003 17.9132 22.317 15.3602 19.9941C15.1644 19.8368 15.1264 19.4799 15.3745 19.355Z" fill="#FF9900"/>
</svg>
let content, remoteVideoEl;
const store = window.store = new Store({
conference_id: 'LUCID-4',
connection: false,
webcamtypes: [
{text: 'Center', value: 'Center'},
{text: 'Right', value: 'Right'},
{text: 'Left', value: 'Left'},
],
userRoles: [
{text: 'Remote User', value: 'RemoteUser'},
{text: 'Office User Right', value: 'OfficeUserRight'},
{text: 'Office User Left', value: 'OfficeUserLeft'},
],
});
const isConnected = new Store.Value.Boolean(false);
store.sub('connection', function(val) {
isConnected.set(val)
});
Screen.show('Main');
/*
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
canvasCtx.drawImage(results.segmentationMask, 0, 0,
canvasElement.width, canvasElement.height);
// Only overwrite existing pixels.
canvasCtx.globalCompositeOperation = 'source-in';
canvasCtx.fillStyle = '#ffffff';
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
// Only overwrite missing pixels.
canvasCtx.globalCompositeOperation = 'destination-atop';
canvasCtx.drawImage(
results.image, 0, 0, canvasElement.width, canvasElement.height);
canvasCtx.restore();
}
*/
<!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">
<div class="resolution">
width: <input type="text" class="callapp_width" size="4" value="1280"> </input>
height:<input type="text" class="callapp_height" size="4" value="720"> </input>
</div>
<button class="callapp_button"> Join </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"> Join </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 via "npm run tsc -- -p src/apps"
//but 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.Ui_OnStartStopButtonClicked = () => {
if (this.mIsRunning) {
this.Stop();
}
else {
this.Start(this.mAddress, this.mAudio, this.mVideo);
}
};
this.Ui_OnUpdate = () => {
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 = true;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/conferenceapp";
}
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;
}
Start(address, audio, video) {
if (this.mCall != null)
this.Stop();
this.mIsRunning = true;
this.Ui_OnStart();
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;
//For usage in HTML set FrameUpdates to false and wait for MediaUpdate to
//get the VideoElement. By default awrtc would deliver frames individually
//for use in Unity WebGL
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 lambda 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() {
this.Cleanup();
}
Cleanup() {
if (this.mCall != null) {
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
this.mIsRunning = false;
this.mLocalVideo = null;
this.mRemoteVideo = {};
}
this.Ui_OnCleanup();
}
Update() {
if (this.mCall != null)
this.mCall.Update();
}
OnNetworkEvent(sender, args) {
//User gave access to requested camera/ microphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete) {
console.log("configuration complete");
}
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.Ui_OnLocalVideo(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.Ui_OnRemoteVideo(videoElement, margs.ConnectionId);
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 conference 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 {
let errorMsg = "Listening failed. Offline? Server dead?";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
}
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, ...
let errorMsg = "Connection failed. Offline? Server dead? ";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call ended or was disconnected
var callEndedEvent = args;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
delete this.mRemoteVideo[callEndedEvent.ConnectionId.id];
this.Ui_OnLog("Disconnected from user with id " + callEndedEvent.ConnectionId.id);
//check if this was the last user
if (this.mNetConfig.IsConference == false && Object.keys(this.mRemoteVideo).length == 0) {
//1 to 1 call and only user left -> quit
this.Cleanup();
return;
}
}
else if (args.Type == awrtc.CallEventType.Message) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args;
this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else {
console.log("Unhandled event: " + args.Type);
}
}
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.Ui_OnUpdate;
this.mUiVideo.onclick = this.Ui_OnUpdate;
this.mUiAddress.onkeyup = this.Ui_OnUpdate;
this.mUiButton.onclick = this.Ui_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.Ui_Update();
}
//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);
}
Ui_OnStart() {
this.mUiButton.textContent = "Stop";
}
Ui_OnCleanup() {
this.mUiButton.textContent = "Join";
while (this.mUiLocalVideoParent.hasChildNodes()) {
this.mUiLocalVideoParent.removeChild(this.mUiLocalVideoParent.firstChild);
}
while (this.mUiRemoteVideoParent.hasChildNodes()) {
this.mUiRemoteVideoParent.removeChild(this.mUiRemoteVideoParent.firstChild);
}
}
Ui_OnLog(msg) {
}
Ui_OnError(msg) {
}
Ui_OnLocalVideo(video) {
this.mUiLocalVideoParent.appendChild(document.createElement("br"));
this.mUiLocalVideoParent.appendChild(video);
}
Ui_OnRemoteVideo(video, id) {
this.mUiRemoteVideoParent.appendChild(document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(new Text("connection " + id.id));
this.mUiRemoteVideoParent.appendChild(document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(video);
}
Ui_Update() {
console.log("UpdateUi");
this.mUiAddress.value = this.mAddress;
this.mUiAudio.checked = this.mAudio;
this.mUiVideo.checked = this.mVideo;
this.mUiUrl.innerHTML = this.GetUrl();
}
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();
}
}
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"));
</script>
</body>
</html>
\ No newline at end of file
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUXh+iPwPcKyq6o4sOPt9sc1t11rgwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA5MTUwMTQzNTNaFw0zMTA5
MTMwMTQzNTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCmaz2F06WDP8tEQ/fQH3nrjqmVTfsyBx2ngkR6s2p+
CVEWQLjcEP7B7q9mI83nTwCWHdOBa4Xs1e5BivUSOniGJcPKL6X3M9c9sKVO6V9x
7yMBSc1NpBbOvT46znDQXV6uGjwFOxeVijX1RiS3QQH5XAiZA47TScyWXDIO4fZ/
ZvQp4LLdGeWmnJwTp2Mn9qxni7fVKjjfjAwP+ba1t8QckQicOsfv+THrvvHNrImy
h14Bp7l7A8pYZnXUXggSDONStZlKroAMJ1vu4CejmiQqt4vWNQFZ/hknJ8GTp+40
2RBoflYEnGwJLNpU6rIwDF+W7Nnv+5o6ZWHIN/EHmcPbAgMBAAGjUzBRMB0GA1Ud
DgQWBBS4FuRfsr87fWho9wUnpOrv/CwsiDAfBgNVHSMEGDAWgBS4FuRfsr87fWho
9wUnpOrv/CwsiDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBA
/dHAbjLewomJeDUkJyAiVzD6qmmzulEtEnV61HXNI3n2YMmQCtYQYJha7qqmkW8y
ivqXkc/Gz3I3xxsWulZ4TUTFWPSit7Fs/EHdNl77/4CwUaMEowZx5J9GfRuXoima
cLNkluTbzE7VUwKTsd20/gWcma7Z/I5FhtwFE4/UfYcgXZLxADhKb/1O05JYLQXF
aECyteOKtcxw0KOPMzBBsqJKomOhONtvPe5BscUMyJd4IMGengN/F296g0bl+5PH
0xx+zKc5mANUM2PuZTjmTXVXzxQZixNV5iThVRp6rO95sr0k3orG5vzA1dejGZ9O
iGBcXTbdRuE54TY542NX
-----END CERTIFICATE-----
<!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
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmaz2F06WDP8tE
Q/fQH3nrjqmVTfsyBx2ngkR6s2p+CVEWQLjcEP7B7q9mI83nTwCWHdOBa4Xs1e5B
ivUSOniGJcPKL6X3M9c9sKVO6V9x7yMBSc1NpBbOvT46znDQXV6uGjwFOxeVijX1
RiS3QQH5XAiZA47TScyWXDIO4fZ/ZvQp4LLdGeWmnJwTp2Mn9qxni7fVKjjfjAwP
+ba1t8QckQicOsfv+THrvvHNrImyh14Bp7l7A8pYZnXUXggSDONStZlKroAMJ1vu
4CejmiQqt4vWNQFZ/hknJ8GTp+402RBoflYEnGwJLNpU6rIwDF+W7Nnv+5o6ZWHI
N/EHmcPbAgMBAAECggEAa+0EezPlFRdcNat2nUysfu2IYUGvdKRUwPukeYa9u3tD
UREsUdvbu92VdsIlmKsNkE1Li5U1x4B+AZeik+3DmAwJy6fXFFhUcLeMnX6P3Y3e
v8kjEgUmnXDxWKXlV9wVuQdegW3vP5XgpG6XBRlttE/Ra5p5F+G2gpTMCgbVG42X
ch24ksvJB6dqduf5DEnG03lCEPFkjxMboq7+NABxTM+49I+tMVdSwlBmkgUlWsCj
GsTFRLgd7VRqtLyiHgiyTtgvRfToEWuO3Pn89ZJc0Dzz+wC85CW/jqJm3F1pQrUm
QbXcm79Aa9xqv8GGahwxkGYFZBS/ZCyyMxfevIKR8QKBgQDQ4OOGfRHv7QvbvaZi
PXrbj8UaoCZrrii9T8UqltR/BCR3Dd78Pm9TG0ExRgriowGZq+GvW4Z1zwo1hiqJ
mXV5jpP6uizzlGXMpA7hAbT4qZlPyGA1FBqmz3pMYhVKQ7J6ZadNzDEpoTEM3kRm
5SIRtIYXaJYBDyEso2q7R0Td9wKBgQDL9jwNR67XSWjv24AhZ11bBUl3QmnvSBwP
dbS6jcvCZT0I1rfR/+8bxZgd082aGWaUVVc2KwWSeI2tf/5HjyM5ATQ+GGbE6mq3
AZ4s0cWkVs/uWrtR+9W3JlXb1lGp89Wp9HSrq2dRtehFhIyxvPrVgA5smcRVyZS2
SI4fnp4gPQKBgAbaOdyhocMDPc5ZrGmwpqUpVEgJVPlXX1LGmStKg3IfSqr1M4mG
6tQJItxg9d51honqD59XG1QmFKmo2yBkfmwcd1JUZUEgby6Fe096ZNt7hOIPKsjS
/gTW7aYv2Y70JZjKWPC+cCZzeU54xDmz7qgyCewerD0gp+/09H1sWyAFAoGAHATQ
jRZ621JXWxPxTyVu+rEIPZzfBqMGT81grXwLMMJNlyhAClY5V7xWQEq3ZOL2Z4wt
teBFHLD47Yu2t8ffE9apgZpCi+yUCl3rh8atevz+BYVrVEDfRBC9HL0dbNZ1VKqj
WG3sfJobtWkXFteaMbgswxiAkzRk/IGNHKTinhUCgYEAuaSMFyqDuFQWeG1qkl/q
ViQjWnvvpJijz/CO3afu/YT3ZvTteDR/GGx8i7DZevSOfRRSuYFxsKa8iMZ05UeL
jEFs+EC5Xlft6qK+J/0tu7DGUzq2RFHxq9X+qkiynRAGaHQv2a+JI0TlePmJa1Ax
f44L21DaDQqn0R8PFLhGZvc=
-----END PRIVATE KEY-----
<!DOCTYPE html>
<html>
<head>
<link rel="icon"
type="image/png"
href="favicon.png">
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/selfie_segmentation.js" crossorigin="anonymous"></script>
<script src="https://form.dev/vanilla/build/rDOM_latest.js"></script>
<style>
@font-face {
font-family: "Amazon Ember";
src: url("font/AmazonEmber_W_Bd.woff2"), url("font/AmazonEmber_W_Bd.woff");
font-weight: bold;
}
@font-face {
font-family: "Amazon Ember";
src: url("font/AmazonEmber_W_Rg.woff2"), url("font/AmazonEmber_W_Rg.woff");
font-weight: normal;
}
* {
font-family: "Amazon Ember", Verdana;
}
img.logo {
position: absolute;
top: 40px;
left: 40px;
}
html {
height: 100%;
}
.main-content {
margin: 16px 0;
}
#content {
height: 100%;
position: relative;
}
.main-background {
height: 100%;
background: url(/img/back.jpg);
background-size: cover;
background-position-x: center;
}
.main-background:before {
content: '';
position: absolute;
left: 0;
top: 0;
background: rgba(0,0,0,0.5);
right: 0;
bottom: 0;
}
.main-background .p, .main-background .h1 {
color: #fff;
}
.main-background .p {
margin-top: 24px;
}
.top-bar {
height: 64px;
background: #1E143A;
padding: 21px 44px;
box-sizing: border-box;
}
body {
margin: 0;
height: 100%;
}
.button {
box-sizing: border-box;
border-radius: 20px;
padding: 4px 20px;
margin-left: 16px;
cursor: pointer;
}
.button.primary {
background: #584DDA;
color: #fff;
border: 2px solid #584DDA;
}
.button.secondary {
background: transparent;
color: #fff;
border: 2px solid;
}
.buttons {
float: right;
display: flex;
}
.main-buttons {
display: flex;
margin-top: 40px;
}
.sub-content {
position: absolute;
left: 12.4%;
top: 50%;
transform: translateY(-50%);
}
.main-background .button {
margin-right: 16px;
margin-left: 0;
padding: 8px 32px;
font-size: 16px;
}
button.dropdown-field__toggler:after {
content: '▼';
}
button.dropdown-field__toggler {
background: transparent !important;
border: 0;
border-bottom: 2px solid #594CD7;
width: 366px;
position: relative;
text-align: left;
padding: 4px;
font-size: 14px;
line-height: 24px;
cursor: pointer;
}
button.dropdown-field__toggler:after {
content: '▼';
position: absolute;
right: 10px;
font-family: arial;
font-size: 10px;
transform: scaleY(0.7) translateY(4px);
}
.dropdown-field--opened button.dropdown-field__toggler:after {
content: '▲';
}
input[type="text"] {
background: transparent !important;
border: 0;
line-height: 36px;
border-bottom: 2px solid #fff;
font-size: 16px;
color: #fff;
outline: none;
}
input[type="text"]:focus {
border-bottom: 2px solid #594CD7;
}
.content {
margin: 112px 178px;
}
.block {
margin-bottom: 40px;
}
.header {
font-size: 32px;
margin-bottom: 8px;
}
.h1 {
font-size: 40px;
line-height: 64px;
font-style: normal;
font-weight: bold;
}
.p {
font-size: 16px;
line-height: 32px;
font-style: normal;
font-weight: normal;
}
.input {
border: none;
padding: 4px 8px;
border-bottom: 2px solid #00000066;
width: 366px;
}
.input:focus-visible {
outline: none;
}
.input:focus {
border-bottom: 2px solid #594CD7;
}
.label_label {
display: inline-block;
width: 108px;
font-size: 16px;
padding: 4px 0 0 2px;
}
.callapp_local_video {
display: none;
}
.hidden {
display: none;
}
.income-video {
display: inline-block;
margin: 20px 8px 0 0;
}
.income-video-id {
position: absolute;
z-index: 1;
}
.dropdown-field__tooltip {
display: none;
}
.dropdown-field--opened .dropdown-field__tooltip {
display: block;
position: absolute;
background: #fff;
border: 2px solid #594CD7;
z-index: 1;
padding: 8px 0;
margin-top: -2px;
min-width: 362px;
}
.dropdown-item {
padding: 8px 20px;
font-size: 14px;
line-height: 20px;
/* margin-bottom: 8px; */
}
.dropdown-item:hover {
background: #594CD7;
color: #fff;
cursor: pointer;
}
.form-field__label-text {
font-size: 24px;
font-weight: bold;
margin: 40px 0 4px;
display: block;
line-height: 32px;
}
.form-field {
margin: 2px 0 12px;
}
.canvas-resizer canvas {
display: block;
}
.canvas-resizer {
float: right;
position: relative;
top: 66px;
}
.description.gray {
color: #5C5C5C;
}
.description {
font-size: 14px;
line-height: 24px;
}
</style>
</head>
<body>
<div id="videoinputapp1">
<!-- URL:
<span class="callapp_url">
</span>-->
<div class="main-content hidden">
<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
<div class="callapp_local_video">My camera feed</div>
<div class="callapp_remote_video">Remote video</div>
</div>
</div>
<!--<canvas id="canvas1"> </canvas>-->
<div class="container">
<video class="input_video" style="display: none"></video>
</div>
<div id="content"></div>
<script src="view/Select.js"></script>
<script src="view/Input.js"></script>
<script src="view/CanvasResizer.js"></script>
<script src="view/Screen.js"></script>
<script src="view/MainScreen.js"></script>
<script src="view/CallScreen.js"></script>
<script src="app.js"></script>
<script src="./bundle/apps.js"></script>
<script>
store.set('appLoaded', true);
D.ext(D('.callapp_button')[0], {onclick: function() {
}});
</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.CAPI_WebRtcNetwork_testapp()">CAPI_WebRtcNetwork_testapp</button><br>
<button onclick="apps.CAPI_MediaNetwork_testapp()">CAPI_MediaNetwork_testapp</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>
</head>
<body>
<div id="videoinputapp1">
<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"> Join </button>
<div class="callapp_local_video">local video</div>
<div class="callapp_remote_video">remote video</div>
</div>
<canvas id="canvas1"> </canvas>
<script>
var rgbToHex = function (rgb) {
var hex = Number(rgb).toString(16);
if (hex.length < 2) {
hex = "0" + hex;
}
return hex;
};
const canvas = document.querySelector("#canvas1");
const ctx = canvas.getContext("2d");
let counter = 0;
setInterval(()=>{
const color = "#FFFF" + rgbToHex(counter%255);
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
counter++;
}, 50);
apps.videoinputapp(document.querySelector("#videoinputapp1"), canvas);
//apps.callapp(document.querySelector("#callapp2"));
</script>
</body>
</html>
\ No newline at end of file
Screen.Conference = function() {
var RTC = function(parent, canvas, devname, id, remoteParent) {
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 apps.VideoInputApp();
callApp.mUiRemoteVideoParent = remoteParent;
const media = new awrtc.Media();
awrtc.Media.SharedInstance.VideoInput.AddCanvasDevice(canvas, devname, canvas.width / 2, canvas.height / 2, 30);
setInterval(() => {
awrtc.Media.SharedInstance.VideoInput.UpdateFrame(devname);
}, 50);
//apps.VideoInputApp.sVideoDevice = devname;
callApp.sVideoDevice = devname;
callApp.setupUi(parent);
this.callApp = callApp;
callApp.___id = id;
this.id = id;
}
var resultProcessor = function(canvasElement) {
window.canvasElement = canvasElement;
const segmentations = [false,false,false,false];
let sPointer = 0;
var w = 720, h = 405;
var tmpCanvas = D.h('canvas', {width: w, height: h}),
tmpCtx = tmpCanvas.getContext('2d'),
composedMaskCanvas = D.h('canvas', {width: w, height: h}),
composedMaskCtx = composedMaskCanvas.getContext('2d'),
composedData = composedMaskCtx.getImageData(0,0,w,h),
composedDataData = composedData.data;
const canvasCtx = window.canvasCtx = canvasElement.getContext('2d');
return function(results){
var max = Math.max;
var image = results.segmentationMask;
tmpCtx.clearRect(0,0,w,h);
tmpCtx.drawImage(image,0,0,image.width,image.height,0,0,w,h);
segmentations[(sPointer++)%4] = tmpCtx.getImageData(0,0,w,h).data;
var pos = canvasElement.position;
var from = w*pos[0]/100,
to = w*pos[1]/100;
if(sPointer>3){
var pointer;
for(var j = 0; j < h; j++){
pointer = j*w*4+3;
for( var i = 0; i < w; i++ ){
//if( ){
composedDataData[ pointer ] =
composedDataData[ pointer - 2 ] =
i<from || i > to ||
segmentations[ 3 ][ pointer ] + segmentations[ 2 ][ pointer ] + segmentations[ 1 ][ pointer ] + segmentations[ 0 ][ pointer ] < 120 ? 255 : 0;
pointer += 4;
//}
}
}
composedMaskCtx.putImageData(composedData,0,0);
}
canvasCtx.drawImage(
results.image, 0, 0, canvasElement.width, canvasElement.height );
canvasCtx.drawImage( composedMaskCanvas/*results.segmentationMask*/, 0, 0,
canvasElement.width, canvasElement.height );
};
}
var camera1canvas, camera2canvas, single = new Store.Value.Boolean(true);
var linkHook = _=>
store.sub('conference_id', function(val) {
_(document.location.origin+document.location.pathname+'#/'+val)
}),
link = D.h('a', { href: linkHook }, linkHook );
var streams = {};
var userID = 0;
var remoteVideoEl = D.div({cls: 'remote-video'});
var generateUserUI = function() {
var currentUserID = userID;
var camTypes = store.get('webcamtypes');
store.set('webcam'+userID, camTypes[userID % camTypes.length].value);
var userRoles = store.get('userRoles');
store.set('userRole'+userID, userRoles[userID % userRoles.length].value);
var cameraCanvas, canvasResizerEl,
dom = D.div({cls: 'block', style: {marginBottom: '80px'}},
D.div({cls: 'header'}, 'User '+(currentUserID+1)),
canvasResizerEl = CanvasResizer(
cameraCanvas = D.h('canvas', {
cls: "output_canvas",
width: "640px",
height: "480px",
style: {
/*display: 'none',*/
width: '320px'
}
})
),
Select({values: 'userRoles', bind: store.bind('userRole'+userID), label: 'Your role'}),
Select({values: 'videoinput', bind: store.bind('videoinput'+userID), label: 'Camera device'}),
Select({values: 'webcamtypes', bind: store.bind('webcam'+userID), label: 'Camera position'})
);
var video1 = D.h('video');
var cameraCanvasCtx = cameraCanvas.getContext('2d');
var updateVideos = function() {
cameraCanvasCtx.drawImage(video1,0,0);
requestAnimationFrame(updateVideos);
};
requestAnimationFrame(updateVideos);
store.sub(['inputsSetted'], (is) => {
/*store.set('videoinput1', devices.videoinput[devices.videoinput.length - 1].value);
store.set('videoinput2', devices.videoinput[Math.max(0,devices.videoinput.length - 2)].value);
store.set('videoinput3', devices.videoinput[Math.max(0,devices.videoinput.length - 3)].value);
*/
if(!is)
return;
var count = devices.videoinput.length;
store.set('videoinput'+currentUserID, devices.videoinput[
(count-1-currentUserID +count*currentUserID)%count
].value);
});
store.sub(['inputsSetted', 'videoinput'+userID], async (is, input) => {
if(!is)
return;
if(input){
try{
if(input in streams){
stream = streams[input];
}else{
var stream = await navigator.mediaDevices.getUserMedia( {
video: {
deviceId: { exact: input }
}
} );
}
streams[input] = stream;
var video = video1;
video.srcObject = stream;
video.play();
canvasResizerEl.style.display = 'block';
}catch(e){
canvasResizerEl.style.display = 'none';
console.error(e)
}
}
for(var streamID in streams){
var inUse = false;
for(var j = 0; j <= userID; j++){
if(store.get('videoinput'+j) === streamID){
inUse = true;
break;
}
}
if(!inUse){
var stream = streams[streamID];
stream.getTracks().forEach(function(track) {
track.stop();
});
}
}
});
var RTCinstance;
store.sub('appLoaded', function(loaded) {
if(!loaded)
return;
RTCinstance = new RTC(
currentUserID ? null : document.querySelector("#videoinputapp1"),
cameraCanvas, 'canvas'+currentUserID,
currentUserID,
currentUserID ? null : remoteVideoEl
);
RTCinstance.mUiRemoteVideoParent = currentUserID ? null : remoteVideoEl;
var selfieSegmentation = new SelfieSegmentation( {
locateFile: ( file ) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`;
}
} );
selfieSegmentation.setOptions( {
modelSelection: 1,
} );
var resultProcessorFn = resultProcessor(cameraCanvas)
selfieSegmentation.onResults( function(){
resultProcessorFn.apply( this, arguments )
} );
var segmentation = async function() {
if(video1.videoWidth>10 && store.get('videoinput'+currentUserID)){
await selfieSegmentation.send( { image: video1 } );
}
requestAnimationFrame(segmentation);
};
requestAnimationFrame(segmentation);
});
userID++;
return dom;
}
var usersBlock;
var dom = D.div({cls: 'top-bar'},
D.h('img', {src: 'amazon-logo.svg'}),
D.div({cls: 'buttons'},
D.h('button', {cls: ['button secondary', {hidden: Store.NOT(isConnected)}], onclick: ()=>store.set('connection', false)}, 'End session'),
D.h('button', {cls: ['button primary', {hidden: isConnected}], onclick: ()=>store.set('connection', true)}, 'Ready to connect')
),
content = D.div({cls: 'content'},
D.div({cls: 'block'},
D.div({cls: 'description gray'}, 'You are the host of this session'),
D.div({cls: 'description'}, 'Session ID: ', store.val('conference_id')),
D.div({cls: 'description'}, 'Link to session: ', link),
),
usersBlock = D.div({cls: 'users-block'},
generateUserUI()
),
D.h('button', {cls: ['button primary', {hidden: isConnected}],
onclick: ()=> usersBlock.appendChild(generateUserUI())
}, 'Add user'),
D.div({cls: 'block hidden'},
D.div({cls: 'header'}, 'Conference info'),
LabeledInput({label: 'Session ID', cls: 'callapp_address input', autocomplete: 'off', value: 'LUCID-4'}, 'conference_id'),
),
D.div({cls: ['block', {hidden: true}]}, //Store.NOT(isConnected)
D.div({cls: ['header']}, 'Remote video'),
remoteVideoEl
)
)
);
var devices = {audioinput: [], audiooutput: [], videoinput: []};
function gotDevices(deviceInfos) {
// Handles being called several times to update labels. Preserve values.
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = {value: deviceInfo.deviceId};
if(!(deviceInfo.kind in devices)){
devices[ deviceInfo.kind ] = [];
}
devices[deviceInfo.kind].push(option);
var count = devices[deviceInfo.kind].length;
if (deviceInfo.kind === 'audioinput') {
option.text = deviceInfo.label || `microphone ${count}`;
} else if (deviceInfo.kind === 'audiooutput') {
option.text = deviceInfo.label || `speaker ${count}`;
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `camera ${count}`;
} else {
console.log('Some other kind of source/device: ', deviceInfo);
}
}
store.set('videoinput', devices.videoinput);
store.set('audioinput', devices.audioinput);
store.set('inputsSetted', true);
console.log(devices);
/*const values = selectors.map(select => select.value);
selectors.forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audioinput') {
option.text = deviceInfo.label || `microphone ${audioInputSelect.length + 1}`;
audioInputSelect.appendChild(option);
} else if (deviceInfo.kind === 'audiooutput') {
option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else {
console.log('Some other kind of source/device: ', deviceInfo);
continue;
}
}
selectors.forEach((select, selectorIndex) => {
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
select.value = values[selectorIndex];
}
});*/
}
var handleError = function(e) {
console.log('Error', e)
}
//gotDevices([{"deviceId":"default","kind":"audioinput","label":"Default - Microphone (2- Razer Kiyo) (1532:0e03)","groupId":"c9b2965558c8600ceaa11c06017152dc99f8721fef0585ad9432838b46f8f9f7"},{"deviceId":"communications","kind":"audioinput","label":"Communications - Microphone (2- Razer Kiyo) (1532:0e03)","groupId":"c9b2965558c8600ceaa11c06017152dc99f8721fef0585ad9432838b46f8f9f7"},{"deviceId":"1517d03bc4bc8e08292b02b454b79e3a93f39fa14d7c589c47c80d093093ed1e","kind":"audioinput","label":"Microphone (Steam Streaming Microphone)","groupId":"20a28b389c696bd36556474101f44876ee56f686b3717913354b85fa804c8c7a"},{"deviceId":"b963d76466dcc0490a58d9624e987a3e69d689c2529adf66f39efae8fccd031c","kind":"audioinput","label":"Microphone (Razer Kiyo) (1532:0e03)","groupId":"ea58a01565e810e30b4d7873050388edb9dc43a11007253ecc9dabd4201230fd"},{"deviceId":"3a664d8417e7000b78f8619667da3e38280269edb222a74d1375786e9c8e70d3","kind":"audioinput","label":"Mic in at rear panel (Pink) (Realtek(R) Audio)","groupId":"8e1aab63dd80f414dce561f4e368bb484c065834f3941db045ca722f61d254a4"},{"deviceId":"46ac5b427bdd38a095664fe7e817a5e7fb7cd2a7077558e72112ade79f27f3a5","kind":"audioinput","label":"Microphone (2- Razer Kiyo) (1532:0e03)","groupId":"c9b2965558c8600ceaa11c06017152dc99f8721fef0585ad9432838b46f8f9f7"},{"deviceId":"78f07683774a206866fdb098d8cac2cb5afc980ba3e670f85393eb16d5e914b9","kind":"audioinput","label":"Headset Microphone (Oculus Virtual Audio Device)","groupId":"05a90cf47adb203e6eb67353f82529c636dc92833ed13f011042a32a370c44c7"},{"deviceId":"68cb94c20ca0b8f17bbaad703c76cbce402793b5e18ddc89e68f38865ed82478","kind":"videoinput","label":"Intel(R) RealSense(TM) 435 with RGB Module Depth (8086:0b07)","groupId":"eb53f0590ee544f7848664d5e2ac6c8430659c4a34e26edf401b57fd9a8ce83c"},{"deviceId":"943c211869cd4e58039d142e22b98ca0cc53dc4d7f60723c832cf73b52b49d6f","kind":"videoinput","label":"Intel(R) RealSense(TM) 435 with RGB Module RGB (8086:0b07)","groupId":"16dd5ec91babf1e3d461bb5afe04bd4713b0c95d3c538ea6f3515dc5c3aacb9a"},{"deviceId":"0329d90dd74edc8657af0bb0d32652989fb1c162d0c294794cbc7fd76e4cf9a9","kind":"videoinput","label":"USB Video Device (1532:0e03)","groupId":"a5fe4973e22ce5b529b8cccd14ce14ed356709d356c94228cb5c2e77d804f297"},{"deviceId":"426bd00749eecdb8ce1da28ecae7d99e78310cc57aedbdf947e9288f4660ad12","kind":"videoinput","label":"USB Video Device (1532:0e03)","groupId":"1b4fc078561da2a5b19c21531d27485fa6f19368d8440d2740d94809c3b5da84"},{"deviceId":"default","kind":"audiooutput","label":"Default - Speakers (Realtek(R) Audio)","groupId":"8e1aab63dd80f414dce561f4e368bb484c065834f3941db045ca722f61d254a4"},{"deviceId":"communications","kind":"audiooutput","label":"Communications - Headphones (Oculus Virtual Audio Device)","groupId":"05a90cf47adb203e6eb67353f82529c636dc92833ed13f011042a32a370c44c7"},{"deviceId":"7c5103d23dc4e21fb0c436dbf039909bc2c70611a92587da582edd74bbee8460","kind":"audiooutput","label":"PHL BDM3490 (NVIDIA High Definition Audio)","groupId":"731a927b663a4a209a3359d16c1c1f214ded54c07f084f659ff4809e6cf588d5"},{"deviceId":"218fa70f234dbf497b6044345c531d15120f56a49e872e3062a0d9da11f8d158","kind":"audiooutput","label":"Speakers (Steam Streaming Microphone)","groupId":"20a28b389c696bd36556474101f44876ee56f686b3717913354b85fa804c8c7a"},{"deviceId":"11c4193f457b6e9cfa44f88fa6742d8a151ce7dfd8bd71b8fd97d0f5b664a6ec","kind":"audiooutput","label":"DELL U2715H (NVIDIA High Definition Audio)","groupId":"4cd1fd9ed5fe4bb80fe7984a3c60e472548f37e157239e4d901ffe8394af5205"},{"deviceId":"f9893c7a1ce8fd1f229a37d8963f9db9504c03cff47e173bc4f1ab4ee20696db","kind":"audiooutput","label":"Headphones (Oculus Virtual Audio Device)","groupId":"05a90cf47adb203e6eb67353f82529c636dc92833ed13f011042a32a370c44c7"},{"deviceId":"c611d06286b441cb1e3440e9bf09f534d98b4ed2e41528eb9c54cb7e41779c63","kind":"audiooutput","label":"Speakers (Steam Streaming Speakers)","groupId":"4d0e12aa90d61a5867f01beea961e77cb03796fd77a2c8cde7f25ec82b718ee9"},{"deviceId":"e00a5fe0a89b8c5bd2858060265e9b0937249601678974e5f3180f797e99ef9e","kind":"audiooutput","label":"Speakers (Realtek(R) Audio)","groupId":"8e1aab63dd80f414dce561f4e368bb484c065834f3941db045ca722f61d254a4"},{"deviceId":"e29c7603a6e3b56aaf3ffc5535f563dd2bce09ce208722837aecee06eaf358d9","kind":"audiooutput","label":"Realtek Digital Output (Realtek(R) Audio)","groupId":"7ae806bf56274db2fab2c6c03a14eed45022de19176702d33821bb7c8e436e8d"}])
;(async function() {
try{
var permissionObj = await navigator.permissions.query( { name: 'microphone' } )
console.log(permissionObj.state);
}catch(error){
console.log('Got error :', error);
}
try{
var permissionObj = await navigator.permissions.query( { name: 'camera' } )
console.log(permissionObj.state);
}catch(error){
console.log('Got error :', error);
}
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError);
setInterval(function() {
D('video').map(a=>a.muted = true)
}, 2000);
})();
return dom;
};
\ No newline at end of file
AddCss('/view/canvasResizer.css');
const resizeMinGap = 20; //%
var CanvasResizer = function(canvas) {
var leftMover = D.div({cls: 'canvas-resizer--mover canvas-resizer--mover-left'});
var rightMover = D.div({cls: 'canvas-resizer--mover canvas-resizer--mover-right'});
var els = [leftMover, rightMover];
var position = [0, 100];
canvas.addEventListener('mousedown', function(e) {
var percent = e.offsetX/e.currentTarget.clientWidth*100,
moving;
if(percent<position[0]){
moving = 0;
}else if(percent>position[1]){
moving = 1;
}else{
moving = Math.abs(position[0] - percent) < Math.abs(position[1] - percent) ? 0 : 1;
}
var rect = D.getRect(canvas);
var move = function(e) {
var percent = ((e.clientX-rect.left)/(rect.width)*100);
percent = Math.min(percent, 100);
percent = Math.max(percent, 0);
if(moving === 0){
percent = Math.min(position[1]-resizeMinGap, percent);
}
if(moving === 1){
percent = Math.max(position[0]+resizeMinGap, percent);
}
position[moving] = ((percent*10)|0)/10;
els[0].style.width = position[0]+'%';
els[1].style.width = (100-position[1])+'%';
e.preventDefault();
e.stopPropagation();
};
var up = function(e) {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
e.preventDefault();
e.stopPropagation();
}
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
});
var dom = D.div({cls: 'canvas-resizer'}, canvas, leftMover, rightMover);
canvas.position = position;
return dom;
}
\ No newline at end of file
const AddCss = function(href) {
D.h('link', {
href,
rel: 'stylesheet',
renderTo: document.head
})
};
const Input = function(cfg, bind) {
cfg.type = 'text';
cfg.onkeyup = cfg.oninput = cfg.onchange = function() {
store.set(bind, input.value);
};
const input = D.h('input', cfg);
store.sub(bind, function(val) {
input.value = val;
});
return input;
};
const LabeledInput = function(cfg, bind) {
return D.h('label', {},
D.div.apply(D, [{cls: 'label_label'}].concat(cfg.label)),
Input(cfg, bind)
)
}
\ No newline at end of file
Screen.Main = function() {
const showJoinForm = new Store.Value.Boolean(false);
const dom = D.div({cls: 'main-background'},
D.h('img', {src: 'amazon-logo.svg', cls: 'logo'}),
D.div({cls: 'sub-content'},
D.div({cls: ['main-connection-type-select', {hidden: showJoinForm}]},
D.div({cls: 'h1'}, 'Conferences made easy'),
D.div({cls: 'p'},
'Use VR calls with remote guests to immerse in conversation and',
D.h('br'),
'feel more involved'
),
D.div({cls: 'main-buttons'},
D.h('button', {cls: 'button primary', onclick: ()=>Screen.show('Conference')}, 'Create New Session'),
D.h('button', {cls: 'button primary', onclick: ()=> showJoinForm.set(true)}, 'Connect to Session'),
)
),
D.div({cls: ['join-screen', {hidden: Store.NOT(showJoinForm)}]},
D.div({cls: 'h1'}, 'Enter Session ID'),
D.div({cls: 'p'},
Input({placeholder: 'Session ID'}, 'conference_id')
),
D.div({cls: 'main-buttons'},
D.h('button', {cls: 'button primary', onclick: ()=>Screen.show('Conference')}, 'Connect to Session'),
D.h('button', {cls: 'button secondary', onclick: ()=> showJoinForm.set(false)}, 'Go Back'),
)
)
)
);
return dom;
};
\ No newline at end of file
const Screen = {
cache: {},
show: function(name) {
const renderEl = document.getElementById('content');
D.removeChildren(renderEl);
if(!(name in Screen.cache)){
Screen.cache[ name ] = Screen[ name ]();
}
renderEl.appendChild(Screen.cache[name]);
}
};
\ No newline at end of file
var Select = function(cfg) {
let isOpen = this.isOpen = new Store.Value.Boolean(false);
var table = D.div({cls: 'dropdown-list'}, _=>{
store.sub(cfg.values, function(val) {
if(val){
_(val.map(function(v) {
return D.div({cls: 'dropdown-item', 'data-id': v.value, onclick: function() {
cfg.bind.set(v.value);
isOpen.set(false);
}}, v.text)
}));
}
});
});
var dom = D.div({cls: 'form-field'},
D.div({cls: 'form-field__label'},
D.span({cls: 'form-field__label-text'}, cfg.label),
D.div({cls: ['dropdown-field', {
'dropdown-field--opened': isOpen
}]},
D.h('button', {
cls: 'dropdown-field__toggler',
onclick: () => isOpen.toggle()
},
D.span({cls: D.cls(
"dropdown-field__placeholder", {
"dropdown-field__placeholder--filled": _=>cfg.bind.sub(val=>_(!!(cfg.multivalue ? val && val.length : val && val.name)))
} )},
_ => {
cfg.bind.sub( val => {
if( val ){
var vals = store.get(cfg.values) || [],
theVal = vals.filter(v=>v.value === val)[0];
_(theVal?theVal.text: 'Select')
}else{
_( cfg.placeholder );
}
} );
}
)
),
D.div({cls: 'dropdown-field__tooltip'},
table
)
)
)
);
return dom;
}
\ No newline at end of file
.canvas-resizer--mover {
position: absolute;
top: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
cursor: ew-resize;
pointer-events: none;
}
.canvas-resizer {
display: none;
}
.canvas-resizer canvas {
cursor: ew-resize;
}
.canvas-resizer--mover-left {
left: 0;
width: 0px;
border-right: 2px solid #594CD7;
}
.canvas-resizer--mover-right {
right: 0;
width: 0px;
border-left: 2px solid #594CD7;
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebGL Demo</title>
<link rel="stylesheet" href="../webgl.css" type="text/css">
</head>
<body>
<canvas id="glcanvas" width="640" height="480"></canvas>
</body>
<script src="../bundle/awrtc.js"></script>
<script src="gl-matrix.js"></script>
<script src="webgl-demo_changed.js"></script>
<script>
let canvas = document.querySelector("#glcanvas");
let nconfig = new awrtc.NetworkConfig();
let call = new awrtc.BrowserWebRtcCall(nconfig);
call.addEventListener((sender, args) => {
if(args.Type === awrtc.CallEventType.FrameUpdate)
{
let gl = canvas.getContext("webgl");
if(args.Frame.Width != globalTextureWidth || args.Frame.Height != globalTextureHeight)
{
const pixel = new Uint8Array(args.Frame.Width * args.Frame.Height * 3 );
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, args.Frame.Width, args.Frame.Height, 0, gl.RGB, gl.UNSIGNED_BYTE, pixel);
globalTextureWidth = args.Frame.Width ;
globalTextureHeight = args.Frame.Height ;
}
args.Frame.ToTexture(gl, globalTextureId);
}
});
//As the system is designed for realtime graphics we have to call the Update method. Events are only
//triggered during this Update call!
let intervalId = setInterval(() => {
call.Update();
}, 50);
let config = new awrtc.MediaConfig();
config.Audio = false;
config.Video = true;
config.FrameUpdates = true;
config.IdealWidth = 640;
config.IdealHeight = 480;
config.IdealFps = 30;
console.log("requested config:" + JSON.stringify(config));
call.Configure(config);
</script>
</html>
\ No newline at end of file
Slightly changed WebGL example from mozzila to test the frame copy from WebRTC -> VideoElement -> WebGL Texture
Source:
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL
https://github.com/mdn/webgl-examples/tree/gh-pages/tutorial/sample6
\ No newline at end of file
var globalTextureId = -1;
var globalTextureWidth = -1;
var globalTextureHeight = -1;
var cubeRotation = 0.0;
main();
//
// Start here
//
function main() {
const canvas = document.querySelector('#glcanvas');
const gl = canvas.getContext('webgl');
// If we don't have a GL context, give up now
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
return;
}
// Vertex shader program
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoord = aTextureCoord;
}
`;
// Fragment shader program
const fsSource = `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void) {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
`;
// Initialize a shader program; this is where all the lighting
// for the vertices and so forth is established.
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
// Collect all the info needed to use the shader program.
// Look up which attributes our shader program is using
// for aVertexPosition, aTextureCoord and also
// look up uniform locations.
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
},
};
// Here's where we call the routine that builds all the
// objects we'll be drawing.
const buffers = initBuffers(gl);
loadTexture(gl, 'cubetexture.png');
var then = 0;
// Draw the scene repeatedly
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
drawScene(gl, programInfo, buffers, globalTextureId, deltaTime);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
//
// initBuffers
//
// Initialize the buffers we'll need. For this demo, we just
// have one object -- a simple three-dimensional cube.
//
function initBuffers(gl) {
// Create a buffer for the cube's vertex positions.
const positionBuffer = gl.createBuffer();
// Select the positionBuffer as the one to apply buffer
// operations to from here out.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Now create an array of positions for the cube.
const positions = [
// Front face
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
];
// Now pass the list of positions into WebGL to build the
// shape. We do this by creating a Float32Array from the
// JavaScript array, then use it to fill the current buffer.
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Now set up the texture coordinates for the faces.
const textureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
const textureCoordinates = [
// Front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Back
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Top
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Bottom
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Right
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Left
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
gl.STATIC_DRAW);
// Build the element array buffer; this specifies the indices
// into the vertex arrays for each face's vertices.
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// This array defines each face as two triangles, using the
// indices into the vertex array to specify each triangle's
// position.
const indices = [
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // back
8, 9, 10, 8, 10, 11, // top
12, 13, 14, 12, 14, 15, // bottom
16, 17, 18, 16, 18, 19, // right
20, 21, 22, 20, 22, 23, // left
];
// Now send the element array to GL
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices), gl.STATIC_DRAW);
return {
position: positionBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
}
//
// Initialize a texture and load an image.
// When the image finished loading copy it into the texture.
//
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Because images have to be download over the internet
// they might take a moment until they are ready.
// Until then put a single pixel in the texture so we can
// use it immediately. When the image has finished downloading
// we'll update the texture with the contents of the image.
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
width, height, border, srcFormat, srcType,
pixel);
const image = new Image();
image.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
srcFormat, srcType, image);
// WebGL1 has different requirements for power of 2 images
// vs non power of 2 images so check if the image is a
// power of 2 in both dimensions.
//if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
// // Yes, it's a power of 2. Generate mips.
// gl.generateMipmap(gl.TEXTURE_2D);
//} else {
// No, it's not a power of 2. Turn of mips and set
// wrapping to clamp to edge
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
//}
globalTextureWidth = image.width;
globalTextureHeight = image.height;
};
image.src = url;
globalTextureId = texture;
return texture;
}
function isPowerOf2(value) {
return (value & (value - 1)) == 0;
}
//
// Draw the scene.
//
function drawScene(gl, programInfo, buffers, texture, deltaTime) {
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Create a perspective matrix, a special matrix that is
// used to simulate the distortion of perspective in a camera.
// Our field of view is 45 degrees, with a width/height
// ratio that matches the display size of the canvas
// and we only want to see objects between 0.1 units
// and 100 units away from the camera.
const fieldOfView = 45 * Math.PI / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
// note: glmatrix.js always has the first argument
// as the destination to receive the result.
mat4.perspective(projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar);
// Set the drawing position to the "identity" point, which is
// the center of the scene.
const modelViewMatrix = mat4.create();
// Now move the drawing position a bit to where we want to
// start drawing the square.
mat4.translate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[-0.0, 0.0, -6.0]); // amount to translate
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
cubeRotation, // amount to rotate in radians
[0, 0, 1]); // axis to rotate around (Z)
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
cubeRotation * .7,// amount to rotate in radians
[0, 1, 0]); // axis to rotate around (X)
// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute
{
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(
programInfo.attribLocations.vertexPosition);
}
// Tell WebGL how to pull out the texture coordinates from
// the texture coordinate buffer into the textureCoord attribute.
{
const numComponents = 2;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
gl.vertexAttribPointer(
programInfo.attribLocations.textureCoord,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(
programInfo.attribLocations.textureCoord);
}
// Tell WebGL which indices to use to index the vertices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
// Tell WebGL to use our program when drawing
gl.useProgram(programInfo.program);
// Set the shader uniforms
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix);
// Specify the texture to map onto the faces.
// Tell WebGL we want to affect texture unit 0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture to texture unit 0
gl.bindTexture(gl.TEXTURE_2D, texture);
// Tell the shader we bound the texture to texture unit 0
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
{
const vertexCount = 36;
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
// Update the rotation for the next draw
cubeRotation += deltaTime / 4;
}
//
// Initialize a shader program, so WebGL knows how to draw our data
//
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
// Create the shader program
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// If creating the shader program failed, alert
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
//
// creates a shader of the given type, uploads the source and
// compiles it.
//
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
// Send the source to the shader object
gl.shaderSource(shader, source);
// Compile the shader program
gl.compileShader(shader);
// See if it compiled successfully
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Karma configuration
// Generated on Mon Jun 24 2019 19:59:32 GMT+1200 (New Zealand Standard Time)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'build/bundle/*.js'
],
// list of files / patterns to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['FirefoxCustom'],
customLaunchers: {
FirefoxCustom: {
base: 'Firefox',
prefs: {
'media.navigator.permission.disabled': true,
'media.navigator.streams.fake' : true
}
}
},
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "awrtc_browser",
"version": "1.985.2",
"description": "Compatible browser implementation to the Unity asset WebRTC Video Chat. Try examples in build folder",
"author": "because-why-not.com Limited",
"license": "BSD-3-Clause",
"main": "build/bundle/awrtc.js",
"types": "build/awrtc/index.d.ts",
"scripts": {
"tsc": "tsc",
"webpack": "webpack",
"build": "webpack && tsc -p ./src/awrtc",
"watch": "webpack --watch",
"clean": "shx rm -rf ./build/awrtc ./build/bundle"
},
"files": [
"build",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/because-why-not/awrtc_browser"
},
"devDependencies": {
"@types/jasmine": "^2.8.17",
"jasmine": "^2.99.0",
"jasmine-core": "^3.6.0",
"karma": "^6.3.3",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.3.0",
"karma-jasmine": "^2.0.1",
"shx": "^0.3.2",
"source-map-loader": "^0.2.4",
"ts-loader": "^5.4.5",
"tsconfig-paths-webpack-plugin": "^3.3.0",
"typescript": "^3.9.7",
"uglify-js": "^2.8.29",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webrtc-adapter": "^7.x"
}
}
/*
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"
import { MediaConfig } from "../awrtc/index";
/**
* Main (and most complicated) example for using BrowserWebRtcCall.
* Have a look at examples.html for easier scenarios.
*
*
*
* Features:
* - Build a "Join" system on top of the regular Listen / Call model to make it easier to use.
* - basic user interface (This is for easy testing not for use as a final application!!! Write your own using the API)
* - setup to be compatible with the Unity Asset's CallApp (but without TURN server!)
* - Get parameters from the address line to configure the call
* - autostart the call (this might not work in all browsers. Mostly used for testing)
* Todo:
* - text message system (so far it sends back the same message)
* - conference call support
*
*
*/
export class CallApp
{
private mAddress;
private mNetConfig = new awrtc.NetworkConfig();
private mCall : awrtc.BrowserWebRtcCall = null;
//update loop
private mIntervalId:any = -1;
private mLocalVideo: HTMLVideoElement = null;
private mRemoteVideo = {};
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 = true;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/conferenceapp";
}
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;
}
public Start(address: string) : void
{
if(this.mCall != null)
this.Stop();
this.mIsRunning = true;
this.Ui_OnStart()
console.log("start");
console.log("Using signaling server url: " + this.mNetConfig.SignalingUrl);
//create media configuration
var config = this.mMediaConfig;
config.IdealFps = 30;
//For usage in HTML set FrameUpdates to false and wait for MediaUpdate to
//get the VideoElement. By default awrtc would deliver frames individually
//for use in Unity WebGL
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 lambda 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
{
this.Cleanup();
}
private Cleanup():void{
if(this.mCall != null)
{
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
this.mIsRunning = false;
this.mLocalVideo = null;
this.mRemoteVideo = {};
}
this.Ui_OnCleanup();
}
private Update():void
{
if(this.mCall != null)
this.mCall.Update();
}
private OnNetworkEvent(sender: any, args: awrtc.CallEventArgs):void{
//User gave access to requested camera/ microphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete){
console.log("configuration complete");
}
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.Ui_OnLocalVideo(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.Ui_OnRemoteVideo(videoElement, margs.ConnectionId);
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 conference 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 {
let errorMsg = "Listening failed. Offline? Server dead?";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
}
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, ...
let errorMsg = "Connection failed. Offline? Server dead? ";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call ended or was disconnected
var callEndedEvent = args as awrtc.CallEndedEventArgs;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
delete this.mRemoteVideo[callEndedEvent.ConnectionId.id];
this.Ui_OnLog("Disconnected from user with id " + callEndedEvent.ConnectionId.id);
//check if this was the last user
if(this.mNetConfig.IsConference == false && Object.keys(this.mRemoteVideo).length == 0)
{
//1 to 1 call and only user left -> quit
this.Cleanup();
return;
}
}
else if (args.Type == awrtc.CallEventType.Message) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args as awrtc.MessageEventArgs;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args as awrtc.DataMessageEventArgs;
this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.CallAccepted) {
let arg = args as awrtc.CallAcceptedEventArgs;
console.log("New call accepted id: " + arg.ConnectionId.id);
}
else if (args.Type == awrtc.CallEventType.WaitForIncomingCall) {
console.log("Waiting for incoming call ...");
}
else {
console.log("Unhandled event: " + args.Type);
}
}
//UI calls. should be moved out into its own class later
private mMediaConfig : MediaConfig;
private mAutostart;
private mUiAddress: HTMLInputElement;
private mUiAudio: HTMLInputElement;
private mUiVideo: HTMLInputElement;
private mUiWidth: HTMLInputElement;
private mUiHeight: HTMLInputElement;
private mUiButton: HTMLButtonElement;
private mUiUrl: HTMLElement;
private mUiLocalVideoParent: HTMLElement;
private mUiRemoteVideoParent: HTMLElement;
public setupUi(parent : HTMLElement)
{
this.mMediaConfig = new MediaConfig();
this.mUiAddress = parent.querySelector<HTMLInputElement>(".callapp_address");
this.mUiAudio = parent.querySelector<HTMLInputElement>(".callapp_send_audio");
this.mUiVideo = parent.querySelector<HTMLInputElement>(".callapp_send_video");
this.mUiWidth = parent.querySelector<HTMLInputElement>(".callapp_width");
this.mUiHeight = parent.querySelector<HTMLInputElement>(".callapp_height");
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.Ui_OnUpdate;
this.mUiVideo.onclick = this.Ui_OnUpdate;
this.mUiAddress.onkeyup = this.Ui_OnUpdate;
this.mUiButton.onclick = this.Ui_OnStartStopButtonClicked;
this.UI_ParameterToUi();
this.UI_UiToValues();
//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.Ui_ValuesToUi();
}
//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);
}
console.log("address: " + this.mAddress + " audio: " + this.mMediaConfig.Audio + " video: " + this.mMediaConfig.Video + " autostart: " + this.mAutostart);
}
private Ui_OnStart(){
this.mUiButton.textContent = "Stop";
}
private Ui_OnCleanup()
{
this.mUiButton.textContent = "Join";
while (this.mUiLocalVideoParent.hasChildNodes()) {
this.mUiLocalVideoParent.removeChild(this.mUiLocalVideoParent.firstChild);
}
while (this.mUiRemoteVideoParent.hasChildNodes()) {
this.mUiRemoteVideoParent.removeChild(this.mUiRemoteVideoParent.firstChild);
}
}
private Ui_OnLog(msg:string){
}
private Ui_OnError(msg:string){
}
private Ui_OnLocalVideo(video : HTMLVideoElement){
this.mUiLocalVideoParent.appendChild( document.createElement("br"));
this.mUiLocalVideoParent.appendChild(video);
}
private Ui_OnRemoteVideo(video : HTMLVideoElement, id: awrtc.ConnectionId){
this.mUiRemoteVideoParent.appendChild( document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(new Text("connection " + id.id));
this.mUiRemoteVideoParent.appendChild( document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(video);
}
public Ui_OnStartStopButtonClicked = ()=>{
this.UI_UiToValues();
if(this.mIsRunning) {
this.Stop();
}else{
this.Start(this.mAddress);
}
}
private UI_ParameterToUi() {
this.mUiAudio.checked = this.tobool(this.GetParameterByName("audio") , true)
this.mUiVideo.checked = this.tobool(this.GetParameterByName("video") , true);
let width = this.GetParameterByName("width");
if(width)
this.mUiWidth.value = width;
let height = this.GetParameterByName("height");
if(height)
this.mUiHeight.value = height;
this.mUiAddress.value = this.GetParameterByName("a");
this.mAutostart = this.GetParameterByName("autostart");
this.mAutostart = this.tobool(this.mAutostart, false);
}
//UI to values
public Ui_OnUpdate = ()=>
{
console.debug("OnUiUpdate");
this.UI_UiToValues();
}
private UI_ParseRes(element: HTMLInputElement){
if(element)
{
let val = Math.floor(element.value as any);
if(val > 0)
return val;
}
return -1;
}
private UI_UiToValues(){
this.mAddress = this.mUiAddress.value;
this.mMediaConfig.Audio = this.mUiAudio.checked;
this.mMediaConfig.Video = this.mUiVideo.checked;
this.mMediaConfig.IdealWidth = this.UI_ParseRes(this.mUiWidth);
this.mMediaConfig.IdealHeight = this.UI_ParseRes(this.mUiHeight);
this.mUiUrl.innerHTML = `<a href="${this.ValuesToParameter()}">${this.ValuesToParameter()}</a>`;
}
//Values to UI
public Ui_ValuesToUi() : void
{
console.log("UpdateUi");
this.mUiAddress.value = this.mAddress;
this.mUiAudio.checked = this.mMediaConfig.Audio;
this.mUiVideo.checked = this.mMediaConfig.Video;
this.mUiWidth.value = "";
if(this.mMediaConfig.IdealWidth > 0)
this.mUiWidth.value = ""+this.mMediaConfig.IdealWidth;
this.mUiHeight.value = "";
if(this.mMediaConfig.IdealHeight > 0)
this.mUiHeight.value = ""+this.mMediaConfig.IdealHeight;
this.mUiUrl.innerHTML = this.ValuesToParameter();
}
private GenerateRandomKey() {
var result = "LUCID-"+(Math.random()*10|0);
/*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.mMediaConfig.Audio + "&video=" + this.mMediaConfig.Video + "&" + "autostart=" + false;
}
private ValuesToParameter() {
return location.protocol + '//' + location.host + location.pathname + this.GetUrlParams();
}
}
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 individual 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);
}/* Old system. not used anymore
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.MediaUpdate) {
let margs = args as awrtc.MediaUpdatedEventArgs;
if (this.mLocalVideo == null && margs.ConnectionId == awrtc.ConnectionId.INVALID) {
var videoElement = margs.VideoElement;
this.mLocalVideo = videoElement;
this.mDiv.innerHTML += "local video: " + "<br>";
this.mDiv.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.mDiv.innerHTML += "remote " + this.mId + "<br>";
this.mDiv.appendChild(videoElement);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}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() {
let netConfig = new awrtc.NetworkConfig();
netConfig.IsConference = false;
netConfig.SignalingUrl = DefaultValues.Signaling;
let mediaConfigSender = new awrtc.MediaConfig();
mediaConfigSender.Video = true;
mediaConfigSender.Audio = true;
mediaConfigSender.FrameUpdates = false;
let mediaConfigReceiver = new awrtc.MediaConfig();
mediaConfigReceiver.Video = false;
mediaConfigReceiver.Audio = false;
mediaConfigReceiver.FrameUpdates = 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"
export * from "./videoinputapp"
\ 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, BrowserMediaStream } 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 CAPI_WebRtcNetwork_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.CAPI_WebRtcNetwork_Create(configuration);
awrtc.CAPI_WebRtcNetwork_StartServer(srv, "Room1");
var clt = awrtc.CAPI_WebRtcNetwork_Create(configuration);
setInterval(() => {
awrtc.CAPI_WebRtcNetwork_Update(srv);
var evt = null;
while (evt = awrtc.CAPI_WebRtcNetwork_Dequeue(srv)) {
console.log("server inc: " + evt.toString());
if (evt.Type == awrtc.NetEventType.ServerInitialized) {
console.log("server started. Address " + evt.Info);
awrtc.CAPI_WebRtcNetwork_Connect(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.CAPI_WebRtcNetwork_Shutdown(srv);
} else if (evt.Type == awrtc.NetEventType.ReliableMessageReceived) {
//srv.SendData(evt.ConnectionId, evt.MessageData, true);
awrtc.CAPI_WebRtcNetwork_SendData(srv, evt.ConnectionId.id, evt.MessageData, true);
} else if (evt.Type == awrtc.NetEventType.UnreliableMessageReceived) {
//srv.SendData(evt.ConnectionId, evt.MessageData, false);
awrtc.CAPI_WebRtcNetwork_SendData(srv, evt.ConnectionId.id, evt.MessageData, false);
}
}
//srv.Flush();
awrtc.CAPI_WebRtcNetwork_Flush(srv);
//clt.Update();
awrtc.CAPI_WebRtcNetwork_Update(clt);
while (evt = awrtc.CAPI_WebRtcNetwork_Dequeue(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.CAPI_WebRtcNetwork_SendData(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.CAPI_WebRtcNetwork_SendData(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.CAPI_WebRtcNetwork_Disconnect(clt, evt.ConnectionId.id);
console.log("client shutting down");
//clt.Shutdown();
awrtc.CAPI_WebRtcNetwork_Shutdown(clt);
}
}
//clt.Flush();
awrtc.CAPI_WebRtcNetwork_Flush(clt);
}, 100);
}
//for testing the media API used by the unity plugin
export function CAPI_MediaNetwork_testapp()
{
awrtc.BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
var signalingUrl : string = DefaultValues.Signaling;
let lIndex = awrtc.CAPI_MediaNetwork_Create("{\"IceUrls\":[\"stun:stun.l.google.com:19302\"], \"SignalingUrl\":\"ws://because-why-not.com:12776\"}");
let configDone = false;
awrtc.CAPI_MediaNetwork_Configure(lIndex, true, true, 160, 120, 640, 480, 640, 480, -1, -1, -1);
console.log(awrtc.CAPI_MediaNetwork_GetConfigurationState(lIndex));
let startTime = new Date().getTime();
let mainLoop = function () {
awrtc.CAPI_WebRtcNetwork_Update(lIndex);
if (awrtc.CAPI_MediaNetwork_GetConfigurationState(lIndex) == (awrtc.MediaConfigurationState.Successful as number) && configDone == false) {
configDone = true;
console.log("configuration done");
}
if (awrtc.CAPI_MediaNetwork_GetConfigurationState(lIndex) == (awrtc.MediaConfigurationState.Failed as number)) {
alert("configuration failed");
}
if (configDone == false)
console.log(awrtc.CAPI_MediaNetwork_GetConfigurationState(lIndex));
if ((new Date().getTime() - startTime) < 15000) {
window.requestAnimationFrame(mainLoop);
} else {
console.log("shutting down");
awrtc.CAPI_WebRtcNetwork_Release(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;
isNew = false;
public get Fps()
{
return Math.round(this.fps);
}
public get IsNew() : boolean
{
if(this.isNew){
this.isNew = false;
return true;
}
return false;
}
Update():void
{
this.counter++;
let diff = new Date().getTime() - this.lastRefresh;
let refresh_time = 2000;
if(diff > refresh_time)
{
this.fps = this.counter / (diff / 1000);
this.counter = 0;
this.lastRefresh = new Date().getTime();
this.isNew = true;
}
}
}
//Sends video data between two peers within the same browser window
//and accesses the resulting frame data directly
export function BrowserMediaNetwork_frameaccess() {
//BrowserMediaStream.DEFAULT_FRAMERATE = 60;
//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 = false;
mediaConfig1.Video = true;
/*
mediaConfig1.IdealWidth = 320;
mediaConfig1.IdealHeight = 240;
//fps seems to be ignored by browsers even if
//the camera specifically supports that setting
mediaConfig1.IdealFps = 15;
*/
let mediaConfig2 = new awrtc.MediaConfig();
mediaConfig2.Audio = false;
mediaConfig2.Video = false;
let localFps = new FpsCounter();
let remoteFps = new FpsCounter();
let loopRate = 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();
loopRate.Update();
if(loopRate.IsNew)
console.log("Loop rate: " + loopRate.Fps);
let frame1: awrtc.IFrameData = null;
let frame2: awrtc.IFrameData = null;
frame1 = network1.TryGetFrame(awrtc.ConnectionId.INVALID);
if (frame1 != null)
{
localFps.Update();
if(localFps.IsNew)
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.IsNew)
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"
]
}
import * as awrtc from "../awrtc/index"
/**
* Copy of the CallApp to test custom video input
*/
const store = window['store'];
export class VideoInputApp {
public sVideoDevice = null;
private mAddress;
private mNetConfig = new awrtc.NetworkConfig();
public mCall: awrtc.BrowserWebRtcCall = null;
//update loop
private mIntervalId: any = -1;
private mLocalVideo: HTMLVideoElement = null;
private mRemoteVideo = {};
private mIsRunning = false;
public ___id: string;
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 = true;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/conferenceapp";
}
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;
}
public Start(address, audio, video): void {
if (this.mCall != null)
this.Stop();
this.mIsRunning = true;
this.Ui_OnStart()
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;
if (this.sVideoDevice !== null) {
config.VideoDeviceName = this.sVideoDevice;
}
//For usage in HTML set FrameUpdates to false and wait for MediaUpdate to
//get the VideoElement. By default awrtc would deliver frames individually
//for use in Unity WebGL
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 lambda 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 {
this.Cleanup();
}
private Cleanup(): void {
if (this.mCall != null) {
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
this.mIsRunning = false;
this.mLocalVideo = null;
this.mRemoteVideo = {};
}
this.Ui_OnCleanup();
}
private Update(): void {
if (this.mCall != null)
this.mCall.Update();
}
private OnNetworkEvent(sender: any, args: awrtc.CallEventArgs): void {
//User gave access to requested camera/ microphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete) {
console.log("configuration complete");
}
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.Ui_OnLocalVideo(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.Ui_OnRemoteVideo(videoElement, margs.ConnectionId);
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 conference 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 {
let errorMsg = "Listening failed. Offline? Server dead?";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
}
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, ...
let errorMsg = "Connection failed. Offline? Server dead? ";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call ended or was disconnected
var callEndedEvent = args as awrtc.CallEndedEventArgs;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
delete this.mRemoteVideo[callEndedEvent.ConnectionId.id];
this.Ui_OnLog("Disconnected from user with id " + callEndedEvent.ConnectionId.id);
//check if this was the last user
if (this.mNetConfig.IsConference == false && Object.keys(this.mRemoteVideo).length == 0) {
//1 to 1 call and only user left -> quit
this.Cleanup();
return;
}
}
else if (args.Type == awrtc.CallEventType.Message) {
//no ui for this yet. simply echo messages for testing
/*let messageArgs = args as awrtc.MessageEventArgs;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);*/
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
//no ui for this yet. simply echo messages for testing
//let messageArgs = args as awrtc.DataMessageEventArgs;
//this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.CallAccepted) {
let arg = args as awrtc.CallAcceptedEventArgs;
console.log("New call accepted id: " + arg.ConnectionId.id);
let messageArgs = args as awrtc.MessageEventArgs;
this.mCall.Send(JSON.stringify({
userId: store.get('userRole'+this.___id),
webCam: store.get('webcam'+this.___id)
}), messageArgs.Reliable, messageArgs.ConnectionId);
/* this.mCall.SendData(JSON.stringify({
userId: store.get('userRole'+this.___id),
webCam: store.get('webcam'+this.___id)
}), messageArgs.Reliable, messageArgs.ConnectionId);*/
}
else if (args.Type == awrtc.CallEventType.WaitForIncomingCall) {
console.log("Waiting for incoming call ...");
}
else {
console.log("Unhandled event: " + args.Type);
}
}
//UI calls. should be moved out into its own class later
private mAudio;
private mVideo;
private mAutostart;
private mUiAudio: HTMLInputElement;
private mUiVideo: HTMLInputElement;
private mUiUrl: HTMLElement;
private mUiLocalVideoParent: HTMLElement;
private mUiRemoteVideoParent: HTMLElement;
public setupUi(parent: HTMLElement) {
this.mUiAudio = parent.querySelector<HTMLInputElement>(".callapp_send_audio");
this.mUiVideo = parent.querySelector<HTMLInputElement>(".callapp_send_video");
this.mUiUrl = parent.querySelector<HTMLParagraphElement>(".callapp_url");
this.mUiLocalVideoParent = parent.querySelector<HTMLParagraphElement>(".callapp_local_video");
this.mUiAudio.onclick = this.Ui_OnUpdate;
this.mUiVideo.onclick = this.Ui_OnUpdate;
store.sub('conference_id', this.Ui_OnUpdate);
store.sub('connection', this.Ui_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.Ui_Update();
}
//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 Ui_OnStart() {
store.set('connection', true)
}
private Ui_OnCleanup() {
store.set('connection', false);
while (this.mUiLocalVideoParent.hasChildNodes()) {
this.mUiLocalVideoParent.removeChild(this.mUiLocalVideoParent.firstChild);
}
if(this.mUiRemoteVideoParent) {
while (this.mUiRemoteVideoParent.hasChildNodes()) {
this.mUiRemoteVideoParent.removeChild(this.mUiRemoteVideoParent.firstChild);
}
}
}
private Ui_OnLog(msg: string) {
}
private Ui_OnError(msg: string) {
}
private Ui_OnLocalVideo(video: HTMLVideoElement) {
this.mUiLocalVideoParent.appendChild(document.createElement("br"));
this.mUiLocalVideoParent.appendChild(video);
}
private Ui_OnRemoteVideo(video: HTMLVideoElement, id: awrtc.ConnectionId) {
const D = window['D'];
video.muted = true;
if(this.mUiRemoteVideoParent){
this.mUiRemoteVideoParent.appendChild(
D.div({cls: 'income-video'},
D.div({cls: 'income-video-id'}, 'stream '+id.id),
video
)
);
}
}
public Ui_OnStartStopButtonClicked = () => {
if(store.get('connection') === this.mIsRunning)
return;
if (this.mIsRunning) {
this.Stop();
} else {
this.Start(this.mAddress, this.mAudio, this.mVideo);
}
}
public Ui_OnUpdate = () => {
console.debug("OnUiUpdate");
this.mAddress = store.get('conference_id');
this.mAudio = this.mUiAudio.checked;
this.mVideo = this.mUiVideo.checked;
store.set('url', this.GetUrl())
}
public Ui_Update(): void {
console.log("UpdateUi");
store.set('conference_id', this.mAddress);
this.mUiAudio.checked = this.mAudio;
this.mUiVideo.checked = this.mVideo;
store.set('url', this.GetUrl())
}
private GenerateRandomKey() {
var result = "LUCID-"+(Math.random()*10|0);
result = store.get('conference_id');
/*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();
}
}
export default VideoInputApp;
/*function videoinputapp(parent: HTMLElement, canvas: HTMLCanvasElement) {
let callApp: VideoInputApp;
console.log("init callapp");
if (parent == null) {
console.log("parent was null");
parent = document.body;
}
awrtc.SLog.SetLogLevel(awrtc.SLogLevel.Info);
callApp = new VideoInputApp();
const media = new awrtc.Media();
const devname = "canvas";
awrtc.Media.SharedInstance.VideoInput.AddCanvasDevice(canvas, devname, canvas.width / 2, canvas.height / 2, 30);
setInterval(() => {
awrtc.Media.SharedInstance.VideoInput.UpdateFrame(devname);
}, 50);
VideoInputApp.sVideoDevice = devname;
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.
*/
//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;
/*
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!");
/*
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() { }
public ToTexture(gl: WebGL2RenderingContext, texture: WebGLTexture) : boolean{
return false;
}
/*
public ToTexture2(gl: WebGL2RenderingContext) : WebGLTexture{
return null;
}
*/
}
//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.
*
* This comes with the downside of risking a change in Width / Height at the moment. In theory the video could
* change the resolution causing the values of Width / Height to change over time before Buffer is accessed to create
* a copy that will be save to use. This should be ok as long as the frame is used at the time it is received.
*/
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;
}
/**Returns the expected width of the frame.
* Watch out this might change inbetween frames!
*
*/
public get Width(): number {
if (this.mRawFrame == null)
{
return this.mFrameGenerator.VideoElement.videoWidth;
}else{
return this.mRawFrame.Width;
}
/*
this.GenerateFrame();
if (this.mRawFrame == null)
return -1;
return this.mRawFrame.Width;
*/
}
/**Returns the expected height of the frame.
* Watch out this might change inbetween frames!
*
*/
public get Height(): number {
if (this.mRawFrame == null)
{
return this.mFrameGenerator.VideoElement.videoHeight;
}else{
return this.mRawFrame.Height;
}
/*
this.GenerateFrame();
if (this.mRawFrame == null)
return -1;
return this.mRawFrame.Height;
*/
}
constructor(frameGenerator: BrowserMediaStream) {
super();
this.mFrameGenerator = frameGenerator;
}
/**Intendet for use via the Unity plugin.
* Will copy the image directly into a texture to avoid overhead of a CPU side copy.
*
* The given texture should have the correct size before calling this method.
*
* @param gl
* @param texture
*/
public ToTexture(gl: WebGL2RenderingContext, texture: WebGLTexture) : boolean{
gl.bindTexture(gl.TEXTURE_2D, texture);
/*
const level = 0;
const internalFormat = gl.RGBA;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, this.mFrameGenerator.VideoElement);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
*/
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGB, gl.UNSIGNED_BYTE, this.mFrameGenerator.VideoElement);
return true;
}
/*
public ToTexture2(gl: WebGL2RenderingContext) : WebGLTexture{
let tex = gl.createTexture()
this.ToTexture(gl, tex)
return;
}
*/
//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";
import { Media } from "./Media";
/**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) {
SLog.L("calling GetUserMedia. Media config: " + JSON.stringify(config));
if(DeviceApi.IsUserMediaAvailable())
{
let promise : Promise<MediaStream> = null;
promise = Media.SharedInstance.getUserMedia(config);
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);
//console.debug("Local tracks: ", stream.getTracks());
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{
//no access to media device -> fail
let error = "Configuration failed. navigator.mediaDevices is unedfined. The browser might not allow media access." +
"Is the page loaded via http or file URL? Some browsers only support https!";
SLog.LE(error);
this.OnConfigurationFailed(error);
}
} 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";
/**
* Mostly used for debugging at the moment. Browser API doesn't seem to have a standard way to
* determine if a frame was updated. This class currently uses several different methods based
* on availability
*
*/
enum FrameEventMethod{
/**We use a set default framerate. FPS is unknown and we can't recognize if a frame was updated.
* Used for remote video tracks on firefox as the "framerate" property will not be set.
*/
DEFAULT_FALLBACK = "DEFAULT_FALLBACK",
/**
* Using the tracks meta data to decide the framerate. We might drop frames or deliver them twice
* because we can't tell when exactly they are updated.
* Some video devices also claim 30 FPS but generate less causing us to waste performance copying the same image
* multipel times
*
* This system works with local video in firefox
*/
TRACK = "TRACK",
/**
* uses frame numbers returned by the browser. This works for webkit based browsers only so far.
* Firefox is either missing the needed properties or they return always 0
*/
EXACT = "EXACT"
}
/**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;
public static MUTE_IF_AUTOPLAT_BLOCKED = false;
//Gives each FrameBuffer and its HTMLVideoElement a fixed id for debugging purposes.
public static sNextInstanceId = 1;
public static VERBOSE = false;
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 = 30;
private mMsPerFrame = 1.0 / BrowserMediaStream.DEFAULT_FRAMERATE * 1000;
private mFrameEventMethod = FrameEventMethod.DEFAULT_FALLBACK;
//used to buffer last volume level as part of the
//autoplat workaround that will mute the audio until it gets the ok from the user
private mDefaultVolume = 0.5;
//Time the last frame was generated
private mLastFrameTime = 0;
private mNextFrameTime = 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;
private static sBlockedStreams : Set<BrowserMediaStream> = new Set();
public static onautoplayblocked: () => void = null;
//must be called from onclick, touchstart, ... event handlers
public static ResolveAutoplay() : void{
SLog.L("ResolveAutoplay. Trying to restart video / turn on audio after user interaction " );
let streams = BrowserMediaStream.sBlockedStreams;
BrowserMediaStream.sBlockedStreams = new Set();
for(let v of Array.from(streams)){
v.ResolveAutoplay();
}
}
public ResolveAutoplay():void{
if(BrowserMediaStream.MUTE_IF_AUTOPLAT_BLOCKED)
{
SLog.L("Try to replay video with audio. " );
//if muted due to autoplay -> unmute
this.SetVolume(this.mDefaultVolume);
if(this.mVideoElement.muted){
this.mVideoElement.muted = false;
}
}
//call play again if needed
if(this.mVideoElement.paused)
this.mVideoElement.play();
}
constructor(stream: MediaStream) {
this.mStream = stream;
this.mInstanceId = BrowserMediaStream.sNextInstanceId;
BrowserMediaStream.sNextInstanceId++;
this.mMsPerFrame = 1.0 / BrowserMediaStream.DEFAULT_FRAMERATE * 1000;
this.mFrameEventMethod = FrameEventMethod.DEFAULT_FALLBACK;
this.SetupElements();
}
private CheckFrameRate():void
{
if(this.mVideoElement)
{
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)
{
if(BrowserMediaStream.VERBOSE)
{
console.log("Track FPS: " + fps);
}
this.mMsPerFrame = 1.0 / fps * 1000;
this.mFrameEventMethod = FrameEventMethod.TRACK;
}
}
//try to get the video fps via the track
//fails on firefox if the track comes from a remote source
if(this.GetFrameNumber() != -1)
{
if(BrowserMediaStream.VERBOSE)
{
console.log("Get frame available.");
}
//browser returns exact frame information
this.mFrameEventMethod = FrameEventMethod.EXACT;
}
//failed to determine any frame rate. This happens on firefox with
//remote tracks
if(this.mFrameEventMethod === FrameEventMethod.DEFAULT_FALLBACK)
{
//firefox and co won't tell us the FPS for remote stream
SLog.LW("Framerate unknown for stream " + this.mInstanceId + ". Using default framerate of " + BrowserMediaStream.DEFAULT_FRAMERATE);
}
}
}
private TriggerAutoplayBlockled(){
BrowserMediaStream.sBlockedStreams.add(this);
if(BrowserMediaStream.onautoplayblocked !== null){
BrowserMediaStream.onautoplayblocked();
}
}
private TryPlay(){
let playPromise = this.mVideoElement.play();
this.mDefaultVolume = this.mVideoElement.volume;
if (typeof playPromise !== "undefined")
{
playPromise.then(function() {
//all good
}).catch((error) => {
if(BrowserMediaStream.MUTE_IF_AUTOPLAT_BLOCKED === false){
//browser blocked replay. print error & setup auto play workaround
console.error(error);
this.TriggerAutoplayBlockled();
}else{
//Below: Safari on Mac is able to just deactivate audio and show the video
//once user interacts with the content audio will be activated again via SetVolue
//WARNING: This fails on iOS! SetVolume fails and audio won't ever come back
//keep MUTE_IF_AUTOPLAT_BLOCKED === false for iOS support
console.warn(error);
SLog.LW("Replay of video failed. The browser might have blocked the video due to autoplay restrictions. Retrying without audio ...");
//try to play without audio enabled
this.SetVolume(0);
let promise2 = this.mVideoElement.play();
if(typeof promise2 !== "undefined"){
promise2.then(()=>{
SLog.L("Playing video successful but muted.");
//still trigger for unmute on next click
this.TriggerAutoplayBlockled();
}).catch((error)=>{
SLog.LE("Replay of video failed. This error is likely caused due to autoplay restrictions of the browser. Try allowing autoplay.");
console.error(error);
this.TriggerAutoplayBlockled();
});
}
}
});
}
}
private 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) => {
//console.log("onloadedmetadata");
//we might have shutdown everything by now already
if(this.mVideoElement == null)
return;
this.TryPlay();
if(this.InternalStreamAdded != null)
this.InternalStreamAdded(this);
this.CheckFrameRate();
let video_log = "Resolution: " + this.mVideoElement.videoWidth + "x" + this.mVideoElement.videoHeight
+ " fps method: " + this.mFrameEventMethod + " " + Math.round(1000/(this.mMsPerFrame));
SLog.L(video_log);
if(BrowserMediaStream.VERBOSE){
console.log(video_log)
}
//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)
{
if((this.mVideoElement as any).webkitDecodedFrameCount)
{
frameNumber = (this.mVideoElement as any).webkitDecodedFrameCount;
}
/*
None of these work and future versions might return numbers that are only
updated once a second or so. For now it is best to ignore these.
TODO: Check if any of these will work in the future. this.mVideoElement.getVideoPlaybackQuality().totalVideoFrames;
might also help in the future (so far always 0)
this.mVideoElement.currentTime also won't work because this is updated faster than the framerate (would result in >100+ framerate)
else if((this.mVideoElement as any).mozParsedFrames)
{
frameNumber = (this.mVideoElement as any).mozParsedFrames;
}else if((this.mVideoElement as any).mozDecodedFrames)
{
frameNumber = (this.mVideoElement as any).mozDecodedFrames;
}else if((this.mVideoElement as any).decodedFrameCount)
{
frameNumber = (this.mVideoElement as any).decodedFrameCount;
}
*/
else
{
frameNumber = -1;
}
}else{
frameNumber = -1;
}
return frameNumber;
}
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.GenerateFrame();
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)
{
this.mFrameEventMethod = FrameEventMethod.EXACT;
//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
let now = new Date().getTime();
if (this.mNextFrameTime <= now) {
{
return true;
}
}
}
}
return false;
}
public Update(): void {
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;
BrowserMediaStream.sBlockedStreams.delete(this);
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;
}
}
//Old buffed frame was replaced with a wrapepr that avoids buffering internally
//Only point of generate frame is now to ensure a consistent framerate
private GenerateFrame(): void
{
this.mLastFrameNumber = this.GetFrameNumber();
let now = new Date().getTime();
//js timing is very inaccurate. reduce time until next frame if we are
//late with this one.
let diff = now - this.mNextFrameTime;
let delta = (this.mMsPerFrame - diff);
delta = Math.min(this.mMsPerFrame, Math.max(1, delta))
this.mLastFrameTime = now;
this.mNextFrameTime = now + delta;
//console.log("last frame , new frame", this.mLastFrameTime, this.mNextFrameTime, delta);
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.setAttribute("playsinline", "");
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";
import { MediaConfig } from "media/MediaConfig";
import { VideoInput } from "./VideoInput";
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 sIsPending = false;
public static get IsPending(){
return DeviceApi.sIsPending;
}
private static sLastError:string = null;
private static get LastError()
{
return this.sLastError;
}
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);
}else{
SLog.LW("Tried to remove an unknown event handler in DeviceApi.RemOnChangedHandler");
}
}
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.sIsPending = false;
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)
{
var tracks = DeviceApi.sAccessStream.getTracks();
for (var i = 0; i < tracks.length; i++) {
tracks[i].stop();
}
DeviceApi.sAccessStream = null;
}
DeviceApi.TriggerChangedEvent();
}
public static get Devices()
{
return DeviceApi.sDeviceInfo;
}
public static GetVideoDevices(): string[]{
const devices = DeviceApi.Devices;
const keys = Object.keys(devices);
const labels = keys.map((x)=>{return devices[x].label});
return labels;
}
public static Reset()
{
DeviceApi.sUpdateEvents = [];
DeviceApi.sLastUpdate = 0;
DeviceApi.sDeviceInfo = {};
DeviceApi.sVideoDeviceCounter = 1;
DeviceApi.sAccessStream = null;
DeviceApi.sLastError = null;
DeviceApi.sIsPending = false;
}
private static InternalOnErrorCatch = (err:DOMError)=>
{
let txt :string = err.toString();
DeviceApi.InternalOnErrorString(txt);
}
private static InternalOnErrorString = (err:string)=>
{
DeviceApi.sIsPending = false;
DeviceApi.sLastError = err;
SLog.LE(err);
DeviceApi.TriggerChangedEvent();
}
private static InternalOnStream = (stream:MediaStream)=>
{
DeviceApi.sAccessStream = stream;
DeviceApi.Update();
}
static ENUM_FAILED = "Can't access mediaDevices or enumerateDevices";
/**Updates the device list based on the current
* access. Gives the devices numbers if the name isn't known.
*/
public static Update():void
{
DeviceApi.sLastError = null;
if(DeviceApi.IsApiAvailable())
{
DeviceApi.sIsPending = true;
navigator.mediaDevices.enumerateDevices()
.then(DeviceApi.InternalOnEnum)
.catch(DeviceApi.InternalOnErrorCatch);
}else{
DeviceApi.InternalOnErrorString(DeviceApi.ENUM_FAILED);
}
}
public static async UpdateAsync():Promise<void>
{
return new Promise((resolve, fail)=>{
DeviceApi.sLastError = null;
if(DeviceApi.IsApiAvailable() == false)
{
DeviceApi.InternalOnErrorString(DeviceApi.ENUM_FAILED);
fail(DeviceApi.ENUM_FAILED);
}
resolve();
}).then(()=>{
DeviceApi.sIsPending = true;
return navigator.mediaDevices.enumerateDevices()
.then(DeviceApi.InternalOnEnum)
.catch(DeviceApi.InternalOnErrorCatch);
});
}
/**Checks if the API is available in the browser.
* false - browser doesn't support this API
* true - browser supports the API (might still refuse to give
* us access later on)
*/
public static IsApiAvailable():boolean
{
if(navigator && navigator.mediaDevices && navigator.mediaDevices.enumerateDevices)
return true;
return false;
}
/**Asks the user for access first to get the full
* device names.
*/
public static RequestUpdate():void
{
DeviceApi.sLastError = null;
if(DeviceApi.IsApiAvailable())
{
DeviceApi.sIsPending = true;
let constraints = {video:true};
navigator.mediaDevices.getUserMedia(constraints)
.then(DeviceApi.InternalOnStream)
.catch(DeviceApi.InternalOnErrorCatch);
}else{
DeviceApi.InternalOnErrorString("Can't access mediaDevices or enumerateDevices");
}
}
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;
}
public static IsUserMediaAvailable()
{
if(navigator && navigator.mediaDevices)
return true;
return false;
}
public static ToConstraints(config: MediaConfig): MediaStreamConstraints
{
//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 later
let deviceId:string = null;
if(config.Video && config.VideoDeviceName && config.VideoDeviceName !== "")
{
deviceId = DeviceApi.GetDeviceId(config.VideoDeviceName);
SLog.L("using device " + config.VideoDeviceName);
if(deviceId === "")
{
//Workaround for Chrome 81: If no camera access is allowed chrome returns the deviceId ""
//thus we can only request any video device. We can't select a specific one
deviceId = null;
}else if(deviceId !== null)
{
//all good
}
else{
SLog.LE("Failed to find deviceId for label " + config.VideoDeviceName);
throw new Error("Unknown device " + 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;
return constraints;
}
public static getBrowserUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>{
return navigator.mediaDevices.getUserMedia(constraints);
}
public static getAssetUserMedia(config: MediaConfig): Promise<MediaStream>{
return new Promise((resolve)=>{
const res = DeviceApi.ToConstraints(config);
resolve(res);
}).then((constraints)=>{
return DeviceApi.getBrowserUserMedia(constraints as MediaStreamConstraints);
});
}
}
\ No newline at end of file
import { DeviceApi } from "./DeviceApi";
import { VideoInput } from "./VideoInput";
import { MediaConfig } from "media/MediaConfig";
export class Media{
//experimental. Will be used instead of the device api to create streams
private static sSharedInstance :Media = new Media();
/**
* Singleton used for now as the browser version is missing a proper factory yet.
* Might be removed later.
*/
public static get SharedInstance(){
return this.sSharedInstance;
}
public static ResetSharedInstance(){
this.sSharedInstance = new Media();
}
private videoInput: VideoInput = null;
public get VideoInput() : VideoInput{
if(this.videoInput === null)
this.videoInput = new VideoInput();
return this.videoInput;
}
public constructor(){
}
public GetVideoDevices(): string[] {
const real_devices = DeviceApi.GetVideoDevices();
const virtual_devices : string[] = this.VideoInput.GetDeviceNames();
return real_devices.concat(virtual_devices);
}
public static IsNameSet(videoDeviceName: string) : boolean{
if(videoDeviceName !== null && videoDeviceName !== "" )
{
return true;
}
return false;
}
public getUserMedia(config: MediaConfig): Promise<MediaStream>{
if(config.Video && Media.IsNameSet(config.VideoDeviceName)
&& this.videoInput != null
&& this.videoInput.HasDevice(config.VideoDeviceName))
{
let res = Promise.resolve().then(async ()=>{
let stream = this.videoInput.GetStream(config.VideoDeviceName);
if(config.Audio)
{
let constraints = {} as MediaStreamConstraints
constraints.audio = true;
let audio_stream = await DeviceApi.getBrowserUserMedia(constraints);
stream.addTrack(audio_stream.getTracks()[0])
}
return stream;
})
return res;
}
return DeviceApi.getAssetUserMedia(config);
}
}
\ 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
/**TS version of the C++ / C# side Native VideoInput API
*
*
* In addition it also supports adding a HTMLCanvasElement as a video device. This can be
* a lot faster in the browser than the C / C++ style UpdateFrame methods that use raw byte arrays
* or pointers to deliver an image.
*
* Note there are currently three distinct ways how this is used:
* 1. Using AddCanvasDevice without scaling (wdith = 0, height =0 or the same as the canvas)
* In this mode the MediaStream will be returned from the canvas. Drawing calls from the canvas
* turn into video frames of the video without any manual UpdateFrame calls
*
* 2. Using AddCanvasDevice with scaling by setting a width / height different from the canvas.
* In this mode the user draws to the canvas and every time UpdateFrame is called a scaled frame
* is created that will turn into video frames. Lower UpdateFrame calls will reduce the framerate
* even if the original canvas us used a higher framerate.
* This mode should result in lower data usage.
*
* 3. Using AddDevice and UpdateFrame to deliver raw byte array frames. This is a compatibility mode
* that works similar to the C / C++ and C# API. An internal canvas is created and updated based on
* the data the user delivers. This mode makes sense if you generate custom data that doesn't have
* a canvas as its source.
* This mode can be quite slow and inefficient.
*
* TODO:
* - Using AddDevice with one resolution & UpdateFrame with another might not support scaling yet but
* activating the 2nd canvas for scaling might
* reduce the performance even more. Check if there is a better solution and if scaling is even needed.
* It could easily be added by calling initScaling but it must be known if scaling is required before
* the device is selected by the user. Given that scaling can reduce the performance doing so by default
* might cause problems for some users.
*
* - UpdateFrame rotation and firstRowIsBottom aren't supported yet. Looks like they aren't needed for
* WebGL anyway. Looks like frames here always start with the top line and rotation is automatically
* handled by the browser.
*
*/
export class VideoInput {
private canvasDevices: CanvasMap = {};
constructor() {
}
/**Adds a canvas to use as video source for streaming.
*
* Make sure canvas.getContext is at least called once before calling this method.
*
* @param canvas
* @param deviceName
* @param width
* @param height
* @param fps
*/
public AddCanvasDevice(canvas: HTMLCanvasElement, deviceName: string, width: number, height: number, fps: number) {
let cdev = CanvasDevice.CreateExternal(canvas, fps);
if (width != canvas.width || height != canvas.height) {
//console.warn("testing scaling");
cdev.initScaling(width, height);
}
this.canvasDevices[deviceName] = cdev;
}
/**For internal use.
* Allows to check if the device already exists.
*
* @param dev
*/
public HasDevice(dev: string): boolean {
return dev in this.canvasDevices;
}
/**For internal use.
* Lists all registered devices.
*
*/
public GetDeviceNames(): Array<string> {
return Object.keys(this.canvasDevices);
}
/**For internal use.
* Returns a MediaStream for the given device.
*
* @param dev
*/
public GetStream(dev: string): MediaStream | null {
if (this.HasDevice(dev)) {
let device = this.canvasDevices[dev];
//watch out: This can trigger an exception if getContext has never been called before.
//There doesn't seem to way to detect this beforehand though
let stream = device.captureStream();
return stream;
}
return null;
}
/**C# API: public void AddDevice(string name, int width, int height, int fps);
*
* Adds a device that will be accessible via the given name. Width / Height determines
* the size of the canvas that is used to stream the video.
*
*
* @param name unique name for the canvas
* @param width width of the canvase used for the stream
* @param height height of the canvase used for the stream
* @param fps Expected FPS used by the stream. 0 or undefined to let the browser decide (likely based on actual draw calls)
*/
public AddDevice(name: string, width: number, height: number, fps?: number): void {
let cdev = CanvasDevice.CreateInternal(width, height, fps);
this.canvasDevices[name] = cdev;
}
private RemCanvasDevice(deviceName: string) {
let cdev = this.canvasDevices[deviceName];
if (cdev) {
delete this.canvasDevices[deviceName];
}
}
//C# API: public void RemoveDevice(string name);
public RemoveDevice(name: string): void {
this.RemCanvasDevice(name);
}
public UpdateFrame(name: string): boolean;
public UpdateFrame(name: string, dataPtr: Uint8ClampedArray, width: number, height: number, type: VideoInputType): boolean
public UpdateFrame(name: string, dataPtr: Uint8ClampedArray, width: number, height: number, type: VideoInputType, rotation: number, firstRowIsBottom: boolean): boolean
/**
* Use UpdateFrame with name only to trigger a new frame without changing the content (e.g. if AddCanvasDevice was used to add the device and it needs scaling)
* Use UpdateFrame with image data if you added the device via AddDevice and want to updat its content
*
*
*
* @param name name of the device
* @param dataPtr array to the image data
* @param width must be the exact width of the image in dataPtr
* @param height must be the exact height of the image in dataPtr
* @param type must be ARGB at the moment
* @param rotation not yet supported
* @param firstRowIsBottom not yet supported
*/
public UpdateFrame(name: string, dataPtr?: Uint8ClampedArray, width?: number, height?: number, type: VideoInputType = VideoInputType.ARGB, rotation: number = 0, firstRowIsBottom: boolean = true): boolean {
if (this.HasDevice(name)) {
let device = this.canvasDevices[name];
if (device.IsExternal() || dataPtr == null) {
//can't change external images / no data available. just generate a new frame without new data
device.UpdateFrame();
} else {
var data = new ImageData(dataPtr, width, height);
device.UpdateFrame(data);
}
return true;
}
return false;
}
}
interface FancyHTMLCanvasElement extends HTMLCanvasElement {
captureStream(fps?: number): MediaStream;
}
/**Wraps around a canvas object to use as a source for MediaStream.
* It supports streaming via a second canvas that is used to scale the image
* before streaming. For scaling UpdateFrame needs to be called one a frame.
* Without scaling the browser will detect changes in the original canvas
* and automatically update the stream
*
*/
class CanvasDevice {
/**Main canvas. This is actively drawn onto by the user (external)
* or by this class.
*
*/
private canvas: FancyHTMLCanvasElement;
/**false = we own the canvas and can change its settings e.g. via VideoInput
* true = externally used canvas. Can't change width / height or any other settings
*/
private external_canvas = false;
/** FPS used for the canvas captureStream.
* 0 or undefined to let the browser handle it automatically via captureStream()
*/
private fps?: number;
/**Canvas element to handle scaling.
* Remains null if initScaling is never called and width / height is expected to
* fit the canvas.
*
*/
private scaling_canvas: FancyHTMLCanvasElement = null;
//private scaling_interval = -1;
private is_capturing = false;
public getStreamingCanvas() {
if (this.scaling_canvas == null)
return this.canvas;
return this.scaling_canvas;
}
public captureStream() {
if (this.is_capturing == false && this.scaling_canvas) {
//scaling is active.
this.startScaling();
}
this.is_capturing = true;
if (this.fps && this.fps > 0) {
return this.getStreamingCanvas().captureStream(this.fps);
}
return this.getStreamingCanvas().captureStream();
}
private constructor(c: HTMLCanvasElement, external_canvas: boolean, fps?: number) {
this.canvas = c as FancyHTMLCanvasElement;
this.external_canvas = external_canvas;
this.fps = fps;
}
public static CreateInternal(width: number, height: number, fps?: number) {
const c = CanvasDevice.MakeCanvas(width, height);
return new CanvasDevice(c, false, fps);
}
public static CreateExternal(c: HTMLCanvasElement, fps?: number) {
return new CanvasDevice(c, true, fps);
}
/**Adds scaling support to this canvas device.
*
* @param width
* @param height
*/
public initScaling(width: number, height: number) {
this.scaling_canvas = document.createElement("canvas") as FancyHTMLCanvasElement;
this.scaling_canvas.width = width;
this.scaling_canvas.height = height;
this.scaling_canvas.getContext("2d");
}
/**Used to update the frame data if the canvas is managed internally.
* Use without image data to just trigger the scaling / generation of a new frame if the canvas is drawn to externally.
*
* If the canvas is managed externally and scaling is not required this method won't do anything. A new frame is instead
* generated automatically based on the browser & canvas drawing operations.
*/
public UpdateFrame(data?: ImageData): void {
if (data) {
let ctx = this.canvas.getContext("2d");
//TODO: This doesn't seem to support scaling out of the box
//we might need to combien this with the scaling system as well
//in case users deliver different resolutions than the device is setup for
ctx.putImageData(data, 0, 0);
}
this.scaleNow();
}
/**Called the first time we need the scaled image to ensure
* the buffers are all filled.
*/
private startScaling() {
this.scaleNow();
}
private scaleNow() {
if (this.scaling_canvas != null) {
let ctx = this.scaling_canvas.getContext("2d");
//ctx.fillStyle = "#FF0000";
//ctx.fillRect(0, 0, this.scaling_canvas.width, this.scaling_canvas.height);
//ctx.clearRect(0, 0, this.scaling_canvas.width, this.scaling_canvas.height)
ctx.clearRect(0,0, this.scaling_canvas.width, this.scaling_canvas.height);
ctx.drawImage(this.canvas, 0, 0, this.scaling_canvas.width, this.scaling_canvas.height);
}
}
public IsExternal(): boolean {
return this.external_canvas;
}
private static MakeCanvas(width: number, height: number): FancyHTMLCanvasElement {
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
//make red for debugging purposes
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas as FancyHTMLCanvasElement;
}
}
interface CanvasMap {
[key: string]: CanvasDevice;
}
/** Only one format supported by browsers so far.
* Maybe more can be added in the future.
*/
export enum VideoInputType {
ARGB
}
/*
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'
export * from './VideoInput'
export * from './Media'
/*
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 RequestLogLevel(level: SLogLevel)
{
if(level > SLog.sLogLevel)
SLog.sLogLevel = level;
}
public static L(msg: any, tag?:string): void {
SLog.Log(msg, tag);
}
public static LW(msg: any, tag?:string): void {
SLog.LogWarning(msg, tag);
}
public static LE(msg: any, tag?:string): void {
SLog.LogError(msg, tag);
}
public static Log(msg: any, tag?:string): void {
if(SLog.sLogLevel >= SLogLevel.Info)
{
if(tag)
{
console.log(msg, tag);
}else{
console.log(msg);
}
}
}
public static LogWarning(msg: any, tag?:string): void {
if(!tag)
tag = "";
if(SLog.sLogLevel >= SLogLevel.Warnings)
{
if(tag)
{
console.warn(msg, tag);
}else{
console.warn(msg);
}
}
}
public static LogError(msg: any, tag?:string) {
if(SLog.sLogLevel >= SLogLevel.Errors)
{
if(tag)
{
console.error(msg, tag);
}else{
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
/// <summary>
/// This value and higher are reserved for other uses.
/// Should never get to the user and should be filtered out.
/// </summary>
ReservedStart = 200,
/// <summary>
/// Reserved.
/// Used by protocols that forward NetworkEvents
/// </summary>
MetaVersion = 201,
/// <summary>
/// Reserved.
/// Used by protocols that forward NetworkEvents.
/// </summary>
MetaHeartbeat = 202
}
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 {
GetBufferedAmount(id: ConnectionId, reliable:boolean): number;
}
//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 GetBufferedAmount(id: ConnectionId, reliable: boolean): number {
let peer = this.mIdToConnection[id.id];
if (peer) {
return peer.GetBufferedAmount(reliable);
} else {
SLog.LogWarning("unknown connection id");
return -1;
}
}
public Disconnect(id: ConnectionId): void {
let peer = this.mIdToConnection[id.id];
if (peer) {
this.HandleDisconnect(id);
}
}
public Shutdown(): void {
//bugfix. Make copy before the loop as Disconnect changes the original mConnectionIds array
let ids = this.mConnectionIds.slice();
for (var id of ids) {
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();
}
//search for the index to remove the id (user might provide a different object with the same id
//don't use indexOf!
let index = this.mConnectionIds.findIndex( e => e.id == id.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 };
private mReadyForIce = false;
private mBufferedIceCandidates : RTCIceCandidate[] = [];
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 = this.OnIceCandidate;
this.mPeer.oniceconnectionstatechange = this.OnIceConnectionStateChange;
this.mPeer.onicegatheringstatechange = this.OnIceGatheringStateChange;
this.mPeer.onnegotiationneeded = this.OnRenegotiationNeeded;
this.mPeer.onconnectionstatechange = this.OnConnectionStateChange;
this.mPeer.onsignalingstatechange = 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;
}
}
private BufferIceCandidate(ice: RTCIceCandidate){
this.mBufferedIceCandidates.push(ice);
}
/**Called after setRemoteDescription succeeded.
* After this call we accept ice candidates and add all buffered ice candidates we received
* until then.
*
* This is a workaround for problems between Safari & Firefox. Safari sometimes sends ice candidates before
* it sends an answer causing an error in firefox.
*/
private StartIce(){
Debug.Log("accepting ice candidates");
this.mReadyForIce = true;
if(this.mBufferedIceCandidates.length > 0)
{
Debug.Log("adding locally buffered ice candidates");
//signaling active. Forward ice candidates we received so far
const candidates = this.mBufferedIceCandidates;
this.mBufferedIceCandidates = [];
for (var candidate of candidates) {
this.AddIceCandidate(candidate);
}
}
}
private AddIceCandidate(ice: RTCIceCandidate){
//based on the shim internals there is a risk it triggers errors outside of the promise
try{
let promise = this.mPeer.addIceCandidate(ice);
promise.then(() => {/*success*/ });
promise.catch((error: DOMError) => { Debug.LogError(error); });
}catch(error){
Debug.LogError(error);
}
}
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) {
if(this.mReadyForIce)
{
//expected normal behaviour
this.AddIceCandidate(ice);
}else{
//Safari sometimes sends ice candidates before the answer message
//causing firefox to trigger an error
//buffer and reemit once setRemoteCandidate has been called
this.BufferIceCandidate(ice);
}
}
}
}
}
}
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_in: RTCSessionDescription) => {
let desc_out = this.ProcLocalSdp(desc_in);
let msg: string = JSON.stringify(desc_out);
let setDescPromise = this.mPeer.setLocalDescription(desc_in);
setDescPromise.then(() => {
this.RtcSetSignalingStarted();
this.EnqueueOutgoing(msg);
});
setDescPromise.catch((error: DOMError) => {
Debug.LogError(error);
Debug.LogError("Error during setLocalDescription with sdp: " + JSON.stringify(desc_in));
this.RtcSetSignalingFailed();
});
});
createOfferPromise.catch((error: DOMError) => {
Debug.LogError(error);
this.RtcSetSignalingFailed();
});
}
//Gives a specific codec priority over the others
private EditCodecs(lines: string[]){
let prefCodec = "H264";
console.warn("sdp munging: prioritizing codec " + prefCodec);
//index and list of all video codec id's
//e.g.: m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
let vcodecs_line_index;
let vcodecs_line_split: string[];
let vcodecs_list : string[];
for(let i = 0; i < lines.length; i++){
let line = lines[i];
if(line.startsWith("m=video")){
vcodecs_line_split= line.split(" ");
vcodecs_list= vcodecs_line_split.slice(3, vcodecs_line_split.length);
vcodecs_line_index = i;
//console.log(vcodecs_list);
break;
}
}
//list of video codecs positioned based on our priority list
let vcodecs_list_new : string[] = [] ;
//start below the the m=video line
for(let i = vcodecs_line_index + 1; i < lines.length; i++){
let line = lines[i];
let prefix = "a=rtpmap:";
if(line.startsWith(prefix)){
let subline = line.substr(prefix.length);
let split = subline.split(" ");
let codecId = split[0];
let codecDesc = split[1];
let codecSplit= codecDesc.split("/");
let codecName = codecSplit[0];
//sanity check. is this a video codec?
if(vcodecs_list.includes(codecId)){
if(codecName === prefCodec){
vcodecs_list_new.unshift(codecId);
}else{
vcodecs_list_new.push(codecId);
}
}
}
}
//first 3 elements remain the same
let vcodecs_line_new = vcodecs_line_split[0] + " " + vcodecs_line_split[1] + " " + vcodecs_line_split[2];
//add new codec list after it
vcodecs_list_new.forEach((x)=>{vcodecs_line_new = vcodecs_line_new + " " + x});
//replace old line
lines[vcodecs_line_index] = vcodecs_line_new;
}
//Replaces H264 profile levels
//iOS workaround. Streaming from iOS to browser currently fails without this if
//resolution is above 720p and h264 is active
private EditProfileLevel(lines: string[]){
//TODO: Make sure we only edit H264. There could be other codecs in the future
//that look identical
console.warn("sdp munging: replacing h264 profile-level with 2a");
let vcodecs_line_index;
let vcodecs_line_split: string[];
let vcodecs_list : string[];
for(let i = 0; i < lines.length; i++){
let line = lines[i];
if(line.startsWith("a=fmtp:"))
{
//looking for profile-level-id=42001f
//we replace the 1f
let searchString = "profile-level-id=";
let sublines = line.split(";");
let updateLine = false;
for(let k = 0; k < sublines.length; k++){
let subline = sublines[k];
if(subline.startsWith(searchString)){
let len = searchString.length + 4;
sublines[k] = sublines[k].substr(0, len) + "2a";
updateLine = true;
break;
}
}
if(updateLine){
lines[i] = sublines.join(";");
}
}
}
}
static MUNGE_SDP = false;
private ProcLocalSdp(desc: RTCSessionDescription) :RTCSessionDescription {
if(AWebRtcPeer.MUNGE_SDP === false)
return desc
console.warn("sdp munging active");
let sdp_in = desc.sdp;
let sdp_out = "";
let lines = sdp_in.split("\r\n");
this.EditCodecs(lines);
//this.EditProfileLevel(lines);
sdp_out = lines.join("\r\n");
let desc_out = {type: desc.type, sdp: sdp_out} as RTCSessionDescription;
return desc_out;
}
private ProcRemoteSdp(desc: RTCSessionDescription) : RTCSessionDescription{
if(AWebRtcPeer.MUNGE_SDP === false)
return desc;
//console.warn("sdp munging active");
return desc;
}
private CreateAnswer(offer: RTCSessionDescription): void {
Debug.Log("CreateAnswer");
offer = this.ProcRemoteSdp(offer);
let remoteDescPromise = this.mPeer.setRemoteDescription(offer);
remoteDescPromise.then(() => {
this.StartIce();
let createAnswerPromise = this.mPeer.createAnswer();
createAnswerPromise.then((desc_in: RTCSessionDescription) => {
let desc_out = this.ProcLocalSdp(desc_in);
let msg: string = JSON.stringify(desc_out);
let localDescPromise = this.mPeer.setLocalDescription(desc_in);
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");
answer = this.ProcRemoteSdp(answer);
let remoteDescPromise = this.mPeer.setRemoteDescription(answer);
remoteDescPromise.then(() => {
//all done
this.StartIce();
});
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)
{
Debug.Log("triggering closure");
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 OnIceConnectionStateChange = (ev: Event): void =>
{
Debug.Log("oniceconnectionstatechange: " + this.mPeer.iceConnectionState);
//Chrome stopped emitting "failed" events. We have to react to disconnected events now
if (this.mPeer.iceConnectionState == "failed" || this.mPeer.iceConnectionState == "disconnected")
{
if(this.mState == WebRtcPeerState.Signaling)
{
this.RtcSetSignalingFailed();
}else if(this.mState == WebRtcPeerState.Connected)
{
this.RtcSetClosed();
}
}
}
/*
So far useless. never triggered in firefox.
In Chrome it triggers together with the DataChannels opening which might be more useful in the future
*/
private OnConnectionStateChange = (ev:Event): void =>
{
Debug.Log("onconnectionstatechange: " + this.mPeer.iceConnectionState);
}
private OnIceGatheringStateChange = (ev:Event): void =>
{
Debug.Log("onicegatheringstatechange: " + this.mPeer.iceGatheringState);
}
private OnRenegotiationNeeded = (ev:Event): void =>
{
}
//broken in chrome. won't switch to closed anymore
private OnSignalingChange = (ev:Event): void =>
{
Debug.Log("onsignalingstatechange:" + this.mPeer.signalingState);
//obsolete
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 GetBufferedAmount(reliable: boolean): number {
let result = -1;
try {
if (reliable) {
if (this.mReliableDataChannel.readyState === "open")
{
result = this.mReliableDataChannel.bufferedAmount;
}
}
else {
if (this.mUnreliableDataChannel.readyState === "open")
{
result = this.mUnreliableDataChannel.bufferedAmount;
}
}
} catch (e) {
SLog.LogError("Exception while trying to access GetBufferedAmount: " + e);
}
return result;
}
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 {
public static readonly LOGTAG = "WebsocketNetwork";
//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);
/// <summary>
/// Version of the protocol implemented here
/// </summary>
public static readonly PROTOCOL_VERSION = 2;
/// <summary>
/// Minimal protocol version that is still supported.
/// V 1 servers won't understand heartbeat and version
/// messages but would just log an unknown message and
/// continue normally.
/// </summary>
public static readonly PROTOCOL_VERSION_MIN = 1;
/// <summary>
/// Assume 1 until message received
/// </summary>
private mRemoteProtocolVersion = 1;
private mUrl: string = null;
private mConfig: WebsocketNetwork.Configuration;
private mLastHeartbeat: number;
private mHeartbeatReceived = true;
private mIsDisposed = false;
public constructor(url: string, configuration?:WebsocketNetwork.Configuration) {
this.mUrl = url;
this.mStatus = WebsocketConnectionStatus.NotConnected;
this.mConfig = configuration;
if(!this.mConfig)
this.mConfig = new WebsocketNetwork.Configuration();
this.mConfig.Lock();
}
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); };
}
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 UpdateHeartbeat():void{
if(this.mStatus == WebsocketConnectionStatus.Connected && this.mConfig.Heartbeat > 0)
{
let diff = Date.now() - this.mLastHeartbeat;
if(diff > (this.mConfig.Heartbeat * 1000))
{
//We trigger heatbeat timeouts only for protocol V2
//protocol 1 can receive the heatbeats but
//won't send a reply
//(still helpful to trigger TCP ACK timeout)
if(this.mRemoteProtocolVersion > 1
&& this.mHeartbeatReceived == false)
{
this.TriggerHeartbeatTimeout();
return;
}
this.mLastHeartbeat = Date.now();
this.mHeartbeatReceived = false;
this.SendHeartbeat();
}
}
}
private TriggerHeartbeatTimeout(){
SLog.L("Closing due to heartbeat timeout. Server didn't respond in time.", WebsocketNetwork.LOGTAG);
this.Cleanup();
}
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', WebsocketNetwork.LOGTAG);
this.mStatus = WebsocketConnectionStatus.Connected;
this.mLastHeartbeat = Date.now();
this.SendVersion();
}
private OnWebsocketOnClose(event: CloseEvent) {
SLog.L('Closed: ' + JSON.stringify(event), WebsocketNetwork.LOGTAG);
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 msg = new Uint8Array(event.data);
this.ParseMessage(msg);
}
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 ParseMessage(msg:Uint8Array):void{
if(msg.length == 0)
{
}else if(msg[0] == NetEventType.MetaVersion)
{
if (msg.length > 1)
{
this.mRemoteProtocolVersion = msg[1];
}
else
{
SLog.LW("Received an invalid MetaVersion header without content.");
}
}else if(msg[0] == NetEventType.MetaHeartbeat)
{
this.mHeartbeatReceived = true;
}else
{
let evt = NetworkEvent.fromByteArray(msg);
this.HandleIncomingEvent(evt);
}
}
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();
this.SendNetworkEvent(evt);
}
}
private SendHeartbeat() : void
{
let msg = new Uint8Array(1);
msg[0] = NetEventType.MetaHeartbeat;
this.InternalSend(msg);
}
private SendVersion() :void
{
let msg = new Uint8Array(2);
msg[0] = NetEventType.MetaVersion;
msg[1] = WebsocketNetwork.PROTOCOL_VERSION;
this.InternalSend(msg);
}
private SendNetworkEvent(evt: NetworkEvent):void
{
var msg = NetworkEvent.toByteArray(evt);
this.InternalSend(msg);
}
private InternalSend(msg: Uint8Array): void
{
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.UpdateHeartbeat();
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 || id.id == ConnectionId.INVALID.id)
{
SLog.LW("Ignored message. Invalid connection id.");
return;
}
if (data == null || data.length == 0)
{
SLog.LW("Ignored message. Invalid data.");
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;
}
}
export namespace WebsocketNetwork{
export class Configuration{
mHeartbeat:number = 30;
get Heartbeat():number
{
return this.mHeartbeat;
}
set Heartbeat(value:number){
if(this.mLocked)
{
throw new Error("Can't change configuration once used.");
}
this.mHeartbeat = value;
}
mLocked = false;
Lock():void
{
this.mLocked = true;
}
}
}
//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;
}
\ 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 './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",
"lib" : ["ES2016", "dom"],
"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/DeviceApi.ts",
"./media_browser/MediaPeer.ts",
"./media_browser/VideoInput.ts",
"./media_browser/Media.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, BrowserWebRtcCall, Media, VideoInputType } from "../media_browser/index";
var CAPI_InitMode = {
//Original mode. Devices will be unknown after startup
Default: 0,
//Waits for the desvice info to come in
//names might be missing though (browser security thing)
WaitForDevices: 1,
//Asks the user for camera / audio access to be able to
//get accurate device information
RequestAccess: 2
};
var CAPI_InitState = {
Uninitialized: 0,
Initializing: 1,
Initialized: 2,
Failed: 3
};
var gCAPI_InitState = CAPI_InitState.Uninitialized;
var gCAPI_Canvas: HTMLCanvasElement = null;
declare var GLctx: any;
export function CAPI_InitAsync(initmode) {
console.debug("CAPI_InitAsync mode: " + initmode);
gCAPI_InitState = CAPI_InitState.Initializing;
if (GLctx && GLctx.canvas) {
gCAPI_Canvas = GLctx.canvas as HTMLCanvasElement;
}
InitAutoplayWorkaround();
let hasDevApi = DeviceApi.IsApiAvailable();
if (hasDevApi && initmode == CAPI_InitMode.WaitForDevices) {
DeviceApi.Update();
} else if (hasDevApi && initmode == CAPI_InitMode.RequestAccess) {
DeviceApi.RequestUpdate();
} else {
//either no device access available or not requested. Switch
//to init state immediately without device info
gCAPI_InitState = CAPI_InitState.Initialized;
if (hasDevApi == false) {
console.debug("Initialized without accessible DeviceAPI");
}
}
}
function InitAutoplayWorkaround(){
if(gCAPI_Canvas == null){
SLog.LW("Autoplay workaround inactive. No canvas object known to register click & touch event handlers.");
return;
}
let listener : ()=>void = null;
listener = ()=>{
//called during user input event
BrowserMediaStream.ResolveAutoplay();
gCAPI_Canvas.removeEventListener("click", listener, false);
gCAPI_Canvas.removeEventListener("touchstart", listener, false);
};
//If a stream runs into autoplay issues we add a listener for the next on click / touchstart event
//and resolve it on the next incoming event
BrowserMediaStream.onautoplayblocked = ()=>{
gCAPI_Canvas.addEventListener("click", listener, false);
gCAPI_Canvas.addEventListener("touchstart", listener, false);
};
}
export function CAPI_PollInitState() {
//keep checking if the DeviceApi left pending state
//Once completed init is finished.
//Later we might do more here
if (DeviceApi.IsPending == false && gCAPI_InitState == CAPI_InitState.Initializing) {
gCAPI_InitState = CAPI_InitState.Initialized;
console.debug("Init completed.");
}
return gCAPI_InitState;
}
/**
*
* @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);
}
var gCAPI_WebRtcNetwork_Instances: { [id: number]: WebRtcNetwork } = {};
var gCAPI_WebRtcNetwork_InstancesNextIndex = 1;
export function CAPI_WebRtcNetwork_IsAvailable() {
//used by C# component to check if this plugin is loaded.
//can only go wrong due to programming error / packaging
if (WebRtcNetwork && WebsocketNetwork)
return true;
return false;
}
export function CAPI_WebRtcNetwork_IsBrowserSupported() {
if (RTCPeerConnection && RTCDataChannel)
return true;
return false;
}
export function CAPI_WebRtcNetwork_Create(lConfiguration: string) {
var lIndex = gCAPI_WebRtcNetwork_InstancesNextIndex;
gCAPI_WebRtcNetwork_InstancesNextIndex++;
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 };
gCAPI_WebRtcNetwork_Instances[lIndex] = new WebRtcNetwork(signalingConfig, rtcConfiguration);
} else {
SLog.LogWarning("Parsing configuration failed. Configuration: " + lConfiguration);
return -1;
}
}
//gCAPI_WebRtcNetwork_Instances[lIndex].OnLog = function (lMsg) {
// console.debug(lMsg);
//};
return lIndex;
}
export function CAPI_WebRtcNetwork_Release(lIndex: number) {
if (lIndex in gCAPI_WebRtcNetwork_Instances) {
gCAPI_WebRtcNetwork_Instances[lIndex].Dispose();
delete gCAPI_WebRtcNetwork_Instances[lIndex];
}
}
export function CAPI_WebRtcNetwork_Connect(lIndex: number, lRoom: string) {
return gCAPI_WebRtcNetwork_Instances[lIndex].Connect(lRoom);
}
export function CAPI_WebRtcNetwork_StartServer(lIndex: number, lRoom: string) {
gCAPI_WebRtcNetwork_Instances[lIndex].StartServer(lRoom);
}
export function CAPI_WebRtcNetwork_StopServer(lIndex: number) {
gCAPI_WebRtcNetwork_Instances[lIndex].StopServer();
}
export function CAPI_WebRtcNetwork_Disconnect(lIndex: number, lConnectionId: number) {
gCAPI_WebRtcNetwork_Instances[lIndex].Disconnect(new ConnectionId(lConnectionId));
}
export function CAPI_WebRtcNetwork_Shutdown(lIndex: number) {
gCAPI_WebRtcNetwork_Instances[lIndex].Shutdown();
}
export function CAPI_WebRtcNetwork_Update(lIndex: number) {
gCAPI_WebRtcNetwork_Instances[lIndex].Update();
}
export function CAPI_WebRtcNetwork_Flush(lIndex: number) {
gCAPI_WebRtcNetwork_Instances[lIndex].Flush();
}
export function CAPI_WebRtcNetwork_SendData(lIndex: number, lConnectionId: number, lUint8ArrayData: Uint8Array, lReliable: boolean) {
gCAPI_WebRtcNetwork_Instances[lIndex].SendData(new ConnectionId(lConnectionId), lUint8ArrayData, lReliable);
}
//helper for emscripten
export function CAPI_WebRtcNetwork_SendDataEm(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 gCAPI_WebRtcNetwork_Instances[lIndex].SendData(new ConnectionId(lConnectionId), arrayBuffer, lReliable);
}
export function CAPI_WebRtcNetwork_GetBufferedAmount(lIndex: number, lConnectionId: number, lReliable: boolean) {
return gCAPI_WebRtcNetwork_Instances[lIndex].GetBufferedAmount(new ConnectionId(lConnectionId), lReliable);
}
export function CAPI_WebRtcNetwork_Dequeue(lIndex: number): NetworkEvent {
return gCAPI_WebRtcNetwork_Instances[lIndex].Dequeue();
}
export function CAPI_WebRtcNetwork_Peek(lIndex: number): NetworkEvent {
return gCAPI_WebRtcNetwork_Instances[lIndex].Peek();
}
/**Allows to peek into the next event to figure out its length and allocate
* the memory needed to store it before calling
* CAPI_WebRtcNetwork_DequeueEm
*
* @param {type} lIndex
* @returns {Number}
*/
export function CAPI_WebRtcNetwork_PeekEventDataLength(lIndex) {
var lNetEvent = gCAPI_WebRtcNetwork_Instances[lIndex].Peek();
return CAPI_WebRtcNetwork_CheckEventLength(lNetEvent);
}
//helper
export function CAPI_WebRtcNetwork_CheckEventLength(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 CAPI_WebRtcNetwork_EventDataToUint8Array(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 CAPI_WebRtcNetwork_DequeueEm(lIndex: number, lTypeIntArray: Int32Array, lTypeIntIndex: number, lConidIntArray: Int32Array, lConidIndex: number, lDataUint8Array: Uint8Array, lDataOffset: number, lDataLength: number, lDataLenIntArray: Int32Array, lDataLenIntIndex: number) {
var nEvt = CAPI_WebRtcNetwork_Dequeue(lIndex);
if (nEvt == null)
return false;
lTypeIntArray[lTypeIntIndex] = nEvt.Type;
lConidIntArray[lConidIndex] = nEvt.ConnectionId.id;
//console.debug("event" + nEvt.netEventType);
var length = CAPI_WebRtcNetwork_EventDataToUint8Array(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 CAPI_WebRtcNetwork_PeekEm(lIndex: number, lTypeIntArray: Int32Array, lTypeIntIndex: number, lConidIntArray: Int32Array, lConidIndex: number, lDataUint8Array: Uint8Array, lDataOffset: number, lDataLength: number, lDataLenIntArray: Int32Array, lDataLenIntIndex: number) {
var nEvt = CAPI_WebRtcNetwork_Peek(lIndex);
if (nEvt == null)
return false;
lTypeIntArray[lTypeIntIndex] = nEvt.Type;
lConidIntArray[lConidIndex] = nEvt.ConnectionId.id;
//console.debug("event" + nEvt.netEventType);
var length = CAPI_WebRtcNetwork_EventDataToUint8Array(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 CAPI_MediaNetwork_IsAvailable(): boolean {
if (BrowserMediaNetwork && BrowserWebRtcCall)
return true;
return false;
}
export function CAPI_MediaNetwork_HasUserMedia(): boolean {
if (navigator && navigator.mediaDevices)
return true;
return false;
}
export function CAPI_MediaNetwork_Create(lJsonConfiguration): number {
let config = new NetworkConfig();
config = JSON.parse(lJsonConfiguration);
let mediaNetwork = new BrowserMediaNetwork(config);
var lIndex = gCAPI_WebRtcNetwork_InstancesNextIndex;
gCAPI_WebRtcNetwork_InstancesNextIndex++;
gCAPI_WebRtcNetwork_Instances[lIndex] = mediaNetwork;
return lIndex;
}
//Configure(config: MediaConfig): void;
export function CAPI_MediaNetwork_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 = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
mediaNetwork.Configure(config);
}
//GetConfigurationState(): MediaConfigurationState;
export function CAPI_MediaNetwork_GetConfigurationState(lIndex: number): number {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.GetConfigurationState() as number;
}
//Note: not yet glued to the C# version!
//GetConfigurationError(): string;
export function CAPI_MediaNetwork_GetConfigurationError(lIndex: number): string {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.GetConfigurationError();
}
//ResetConfiguration(): void;
export function CAPI_MediaNetwork_ResetConfiguration(lIndex: number): void {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.ResetConfiguration();
}
//TryGetFrame(id: ConnectionId): RawFrame;
export function CAPI_MediaNetwork_TryGetFrame(lIndex: number, lConnectionId: number,
lWidthInt32Array: Int32Array, lWidthIntArrayIndex: number,
lHeightInt32Array: Int32Array, lHeightIntArrayIndex: number,
lBufferUint8Array: Uint8Array, lBufferUint8ArrayOffset: number, lBufferUint8ArrayLength: number): boolean {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.TryGetFrame(new ConnectionId(lConnectionId));
if (frame == null || frame.Buffer == null) {
return false;
} else {
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;
}
}
export function CAPI_MediaNetwork_TryGetFrame_ToTexture(lIndex: number, lConnectionId: number,
lWidth: number,
lHeight: number,
gl: WebGL2RenderingContext, texture: WebGLTexture): boolean {
//console.log("CAPI_MediaNetwork_TryGetFrame_ToTexture");
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.TryGetFrame(new ConnectionId(lConnectionId));
if (frame == null) {
return false;
} else if (frame.Width != lWidth || frame.Height != lHeight) {
SLog.LW("CAPI_MediaNetwork_TryGetFrame_ToTexture failed. Width height expected: " + frame.Width + "x" + frame.Height + " but received " + lWidth + "x" + lHeight);
return false;
} else {
frame.ToTexture(gl, texture);
return true;
}
}
/*
export function CAPI_MediaNetwork_TryGetFrame_ToTexture2(lIndex: number, lConnectionId: number,
lWidthInt32Array: Int32Array, lWidthIntArrayIndex: number,
lHeightInt32Array: Int32Array, lHeightIntArrayIndex: number,
gl:WebGL2RenderingContext): WebGLTexture
{
//console.log("CAPI_MediaNetwork_TryGetFrame_ToTexture");
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.TryGetFrame(new ConnectionId(lConnectionId));
if (frame == null) {
return false;
} else {
lWidthInt32Array[lWidthIntArrayIndex] = frame.Width;
lHeightInt32Array[lHeightIntArrayIndex] = frame.Height;
let texture = frame.ToTexture2(gl);
return texture;
}
}
*/
export function CAPI_MediaNetwork_TryGetFrame_Resolution(lIndex: number, lConnectionId: number,
lWidthInt32Array: Int32Array, lWidthIntArrayIndex: number,
lHeightInt32Array: Int32Array, lHeightIntArrayIndex: number): boolean {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
let frame = mediaNetwork.PeekFrame(new ConnectionId(lConnectionId));
if (frame == null) {
return false;
} else {
lWidthInt32Array[lWidthIntArrayIndex] = frame.Width;
lHeightInt32Array[lHeightIntArrayIndex] = frame.Height;
return true;
}
}
//Returns the frame buffer size or -1 if no frame is available
export function CAPI_MediaNetwork_TryGetFrameDataLength(lIndex: number, connectionId: number): number {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[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 CAPI_MediaNetwork_SetVolume(lIndex: number, volume: number, connectionId: number): void {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
mediaNetwork.SetVolume(volume, new ConnectionId(connectionId));
}
export function CAPI_MediaNetwork_HasAudioTrack(lIndex: number, connectionId: number): boolean {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.HasAudioTrack(new ConnectionId(connectionId));
}
export function CAPI_MediaNetwork_HasVideoTrack(lIndex: number, connectionId: number): boolean {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
return mediaNetwork.HasVideoTrack(new ConnectionId(connectionId));
}
export function CAPI_MediaNetwork_SetMute(lIndex: number, value: boolean) {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[lIndex] as BrowserMediaNetwork;
mediaNetwork.SetMute(value);
}
export function CAPI_MediaNetwork_IsMute(lIndex: number) {
let mediaNetwork = gCAPI_WebRtcNetwork_Instances[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_Media_GetVideoDevices_Length(): number {
return Media.SharedInstance.GetVideoDevices().length;
}
export function CAPI_Media_GetVideoDevices(index: number): string {
const devs = Media.SharedInstance.GetVideoDevices();
if (devs.length > index) {
return devs[index];
}
else {
SLog.LE("Requested device with index " + index + " does not exist.");
//it needs to be "" to behave the same to the C++ API. std::string can't be null
return "";
}
}
export function CAPI_VideoInput_AddCanvasDevice(query: string, name: string, width: number, height: number, fps: number): boolean {
let canvas = document.querySelector(query) as HTMLCanvasElement;
if (canvas) {
console.debug("CAPI_VideoInput_AddCanvasDevice", { query, name, width, height, fps });
if (width <= 0 || height <= 0) {
width = canvas.width;
height = canvas.height;
}
Media.SharedInstance.VideoInput.AddCanvasDevice(canvas as HTMLCanvasElement, name, width, height, fps);//, width, height, fps);
return true;
}
return false;
}
export function CAPI_VideoInput_AddDevice(name: string, width: number, height: number, fps: number) {
Media.SharedInstance.VideoInput.AddDevice(name, width, height, fps);
}
export function CAPI_VideoInput_RemoveDevice(name: string) {
Media.SharedInstance.VideoInput.RemoveDevice(name);
}
export function CAPI_VideoInput_UpdateFrame(name: string,
lBufferUint8Array: Uint8Array, lBufferUint8ArrayOffset: number, lBufferUint8ArrayLength: number,
width: number, height: number,
rotation: number, firstRowIsBottom: boolean): boolean {
let dataPtrClamped: Uint8ClampedArray = null;
if (lBufferUint8Array && lBufferUint8ArrayLength > 0) {
dataPtrClamped = new Uint8ClampedArray(lBufferUint8Array.buffer, lBufferUint8ArrayOffset, lBufferUint8ArrayLength);
}
return Media.SharedInstance.VideoInput.UpdateFrame(name, dataPtrClamped, width, height, VideoInputType.ARGB, rotation, firstRowIsBottom);
}
export function GetUnityCanvas(): HTMLCanvasElement {
if (gCAPI_Canvas !== null)
return gCAPI_Canvas;
SLog.LogWarning("Using GetUnityCanvas without a known cavans reference.");
return document.querySelector("canvas");
}
export function GetUnityContext(): WebGL2RenderingContext {
return GetUnityCanvas().getContext("webgl2");
}
/*
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 { Media } from "../media_browser/Media";
import { GetUnityCanvas } from "./CAPI";
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.getTracks().forEach(t => {
t.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.getTracks().forEach(t => {
t.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_Media_GetVideoDevices_Length,
CAPI_Media_GetVideoDevices,
MediaConfig,
Media} 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];
//these tests don't work anymore due to forcing permissions for devices in
//unit tests.
//In a real browser we don't have access to device names until GetUserMedia
//returned. Meaning the API will fill in the names using "videoinput 1"
//"videoinput 2" and so on.
//Now the tests force permissions = true so we already have full
//access at the start
/*
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, "Chrome fails this now. Likely due to file://. Check for better test setup");
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;
const devices_length_unitialized = CAPI_Media_GetVideoDevices_Length();
expect(devices_length_unitialized).toBe(0);
DeviceApi.AddOnChangedHandler(()=>{
let dev_length = CAPI_Media_GetVideoDevices_Length();
expect(dev_length).not.toBe(0);
expect(dev_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_Media_GetVideoDevices(counter);
expect(actual).toBe(expectedVal);
counter++;
}
done();
});
CAPI_DeviceApi_Update();
});
it("isMediaAvailable", () => {
const res = DeviceApi.IsUserMediaAvailable();
expect(res).toBe(true);
});
it("getUserMedia", async () => {
let stream = await DeviceApi.getBrowserUserMedia({audio:true});
expect(stream).not.toBeNull();
expect(stream.getVideoTracks().length).toBe(0);
expect(stream.getAudioTracks().length).toBe(1);
stream = await DeviceApi.getBrowserUserMedia({video:true});
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
});
it("getAssetMedia", async () => {
let config = new MediaConfig();
config.Audio = true;
config.Video = false;
let stream = await DeviceApi.getAssetUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getVideoTracks().length).toBe(0);
expect(stream.getAudioTracks().length).toBe(1);
config = new MediaConfig();
config.Audio = false;
config.Video = true;
stream = await DeviceApi.getAssetUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
});
it("getAssetMedia_invalid", async () => {
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let error = null;
let stream :MediaStream = null;
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try
{
stream = await DeviceApi.getAssetUserMedia(config);
}catch(err){
error = err;
}
expect(stream).toBeNull();
expect(error).toBeTruthy();
});
//check for a specific bug causing promise catch not to trigger correctly
//due to error in ToConstraints
it("getAssetMedia_invalid_promise", (done) => {
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let result: Promise<MediaStream> = null;
result = DeviceApi.getAssetUserMedia(config);
result.then(()=>{
fail("getAssetUserMedia returned but was expected to fail");
}).catch((error)=>{
expect(error).toBeTruthy();
done();
})
});
it("UpdateAsync", async (done) => {
expect(DeviceApi.GetVideoDevices().length).toBe(0);
await DeviceApi.UpdateAsync();
expect(DeviceApi.GetVideoDevices().length).toBeGreaterThan(0);
expect(DeviceApi.GetVideoDevices().length).toBe(CAPI_Media_GetVideoDevices_Length());
done();
});
/*
it("Devices", async () => {
DeviceApi.RequestUpdate
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let error = null;
let stream :MediaStream = null;
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try
{
stream = await DeviceApi.getAssetUserMedia(config);
}catch(err){
error = err;
}
expect(stream).toBeNull();
expect(error).toBeTruthy();
});
*/
});
/*
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, BrowserMediaStream } 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("MediaEventLocal", (done) => {
BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
let mediaConfig = new MediaConfig();
let network = this.createDefault();
network.Configure(mediaConfig);
setInterval(()=>{
network.Update();
let evt : MediaEvent = null;
while((evt = network.DequeueMediaEvent()) != null)
{
console.log("Stream added",evt );
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) => {
BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
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();
}, 40);
}, 15000);
}
}
describe("MediaNetworkTest", () => {
it("TestEnvironment", () => {
expect(null).toBeNull();
});
var test = new MediaNetworkTest();
test.setup();
});
import { VideoInput, Media, DeviceApi, MediaConfig, CAPI_Media_GetVideoDevices_Length, CAPI_Media_GetVideoDevices, BrowserMediaStream, WaitForIncomingCallEventArgs } from "../awrtc/index";
import { MakeTestCanvas } from "VideoInputTest";
export function MediaTest_export()
{
}
describe("MediaTest", () => {
beforeEach((done)=>{
let handler = ()=>{
DeviceApi.RemOnChangedHandler(handler);
done();
};
DeviceApi.AddOnChangedHandler(handler);
DeviceApi.Update();
Media.ResetSharedInstance();
});
it("SharedInstance", () => {
expect(Media.SharedInstance).toBeTruthy();
let instance1 = Media.SharedInstance;
Media.ResetSharedInstance();
expect(Media.SharedInstance).not.toBe(instance1);
});
it("GetVideoDevices", () => {
const media = new Media();
let devs = media.GetVideoDevices();
expect(devs).toBeTruthy();
expect(devs.length).toBeGreaterThan(0);
});
it("GetUserMedia", async () => {
const media = new Media();
let config = new MediaConfig();
config.Audio = false;
let stream = await media.getUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
stream = null;
let err = null;
config.VideoDeviceName = "invalid name"
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try{
stream = await media.getUserMedia(config);
}catch(error){
err = error;
}
expect(err).not.toBeNull();
expect(stream).toBeNull();
});
it("GetUserMedia_videoinput", async (done) => {
const name = "test_canvas";
const media = new Media();
const config = new MediaConfig();
config.Audio = false;
config.Video = true;
const canvas = MakeTestCanvas();
media.VideoInput.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
const streamCamera = await media.getUserMedia(config);
expect(streamCamera).not.toBeNull();
expect(streamCamera.getAudioTracks().length).toBe(0);
expect(streamCamera.getVideoTracks().length).toBe(1);
config.VideoDeviceName = name;
const streamCanvas = await media.getUserMedia(config);
expect(streamCanvas).not.toBeNull();
expect(streamCanvas.getAudioTracks().length).toBe(0);
expect(streamCanvas.getVideoTracks().length).toBe(1);
const streamCanvas2 = await media.getUserMedia(config);
expect(streamCanvas2).not.toBeNull();
expect(streamCanvas2.getAudioTracks().length).toBe(0);
expect(streamCanvas2.getVideoTracks().length).toBe(1);
done();
});
it("GetUserMedia_videoinput_and_audio", async () => {
const name = "test_canvas";
const media = new Media();
const config = new MediaConfig();
config.Audio = true;
config.Video = true;
const canvas = MakeTestCanvas();
media.VideoInput.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
config.VideoDeviceName = name;
let stream : MediaStream = null;
try{
stream = await media.getUserMedia(config);
}catch(err){
console.error(err);
fail(err);
}
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(1);
expect(stream.getVideoTracks().length).toBe(1);
config.VideoDeviceName = "invalid name";
stream = null;
let error_result : string = null
try{
stream = await media.getUserMedia(config);
}catch(err){
error_result = err;
}
expect(error_result).not.toBeNull();
expect(stream).toBeNull();
}, 15000);
//CAPI needs to be changed to use Media only instead the device API
it("MediaCapiVideoInput", async (done) => {
//empty normal device api
DeviceApi.Reset();
expect(CAPI_Media_GetVideoDevices_Length()).toBe(0);
const name = "test_canvas";
const canvas = MakeTestCanvas();
Media.SharedInstance.VideoInput.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
expect(CAPI_Media_GetVideoDevices_Length()).toBe(1);
expect(CAPI_Media_GetVideoDevices(0)).toBe(name);
done();
});
});
describe("MediaStreamTest", () => {
beforeEach((done)=>{
let handler = ()=>{
DeviceApi.RemOnChangedHandler(handler);
done();
};
DeviceApi.AddOnChangedHandler(handler);
DeviceApi.Update();
Media.ResetSharedInstance();
});
class TestStreamContainer
{
public canvas: HTMLCanvasElement;
public stream : MediaStream;
public constructor()
{
let canvas = document.createElement("canvas");
canvas.width = 4;
canvas.height = 4;
let ctx = canvas.getContext("2d");
//make blue for debugging purposes
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
this.canvas = canvas;
this.stream = (canvas as any).captureStream() as MediaStream;
}
public MakeFrame(color : string){
let ctx = this.canvas.getContext("2d");
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
//make blue for debugging purposes
ctx.fillStyle = color;
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function MakeTestStreamContainer()
{
return new TestStreamContainer();
}
//TODO: need proper way to wait and check with async/ await
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function WaitFor(){
}
it("buffer_and_trygetframe", async(done) => {
const testcontainer = MakeTestStreamContainer();
const stream = new BrowserMediaStream(testcontainer.stream);
//frames are not available at the start until fully loaded
let frame = stream.TryGetFrame();
expect(frame).toBeNull();
await sleep(100);
stream.Update();
//waited for the internals to get initialized. We should have a frame now
frame = stream.TryGetFrame();
expect(frame).not.toBeNull();;
//and a buffer
let buffer = frame.Buffer;
expect(buffer).not.toBeNull();;
//expected to be blue
let r = buffer[0];
let g = buffer[1];
let b = buffer[2];
let a = buffer[3];
expect(r).toBe(0);
expect(g).toBe(0);
expect(b).toBe(255);
expect(a).toBe(255);
//we removed the frame now. this should be null
frame = stream.TryGetFrame();
expect(frame).toBeNull();
//make a new frame with different color
testcontainer.MakeFrame("#FFFF00");
await sleep(100);
stream.Update();
//get new frame
frame = stream.TryGetFrame();
expect(frame).not.toBeNull();;
buffer = frame.Buffer;
expect(buffer).not.toBeNull();;
//should be different color now
r = buffer[0];
g = buffer[1];
b = buffer[2];
a = buffer[3];
expect(r).toBe(255);
expect(g).toBe(255);
expect(b).toBe(0);
expect(a).toBe(255);
//done
done();
});
function createTexture(gl: WebGL2RenderingContext) : WebGLTexture
{
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Because images have to be download over the internet
// they might take a moment until they are ready.
// Until then put a single pixel in the texture so we can
// use it immediately. When the image has finished downloading
// we'll update the texture with the contents of the image.
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
width, height, border, srcFormat, srcType,
pixel);
return texture;
}
it("texture", async(done) => {
//blue test container to stream from
const testcontainer = MakeTestStreamContainer();
const stream = new BrowserMediaStream(testcontainer.stream);
//document.body.appendChild(testcontainer.canvas);
//waited for the internals to get initialized. We should have a frame now
await sleep(100);
stream.Update();
let frame = stream.PeekFrame()
expect(frame).not.toBeNull();
//create another canvas but with WebGL context
//this is where we copy the texture to
let canvas = document.createElement("canvas");
canvas.width = testcontainer.canvas.width;
canvas.height = testcontainer.canvas.height;
//document.body.appendChild(canvas);
let gl = canvas.getContext("webgl2");
//testing only. draw this one red
gl.clearColor(1,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
//create new texture and copy the image into it
let texture = createTexture(gl);
let res = frame.ToTexture(gl, texture);
expect(res).toBe(true);
//we attach our test texture to a frame buffer, then read from it to copy the data back from the GPU
//into an array dst_buffer
const dst_buffer = new Uint8Array(testcontainer.canvas.width * testcontainer.canvas.height * 4);
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.readPixels(0, 0, testcontainer.canvas.width, testcontainer.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, dst_buffer);
//check if we have the expected blue color we use to setup the testcontainer canvas
let r = dst_buffer[0];
let g = dst_buffer[1];
let b = dst_buffer[2];
let a = dst_buffer[3];
expect(r).toBe(0);
expect(g).toBe(0);
expect(b).toBe(255);
expect(a).toBe(255);
//TODO: could compare whole src / dst buffer to check if something is cut off
//const compare_buffer = frame.Buffer;
done();
});
});
\ No newline at end of file
import { VideoInput, VideoInputType } from "../awrtc/index";
export function VideoInputTest_export() {
}
export function MakeTestCanvas(w?: number, h?: number): HTMLCanvasElement {
if (w == null)
w = 4;
if (h == null)
h = 4;
let canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
let ctx = canvas.getContext("2d");
//make blue for debugging purposes
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}
export function MakeBrokenTestCanvas(): HTMLCanvasElement {
let canvas = document.createElement("canvas");
return canvas;
}
/**Create test image with pattern
* Black White
* White Black
*
* So each corner can be tested for correct results.
*
* @param src_width
* @param src_height
*/
export function MakeTestImage(src_width: number, src_height: number): ImageData {
let src_size = src_width * src_height * 4;
let src_data = new Uint8ClampedArray(src_size);
for (let y = 0; y < src_height; y++) {
for (let x = 0; x < src_width; x++) {
let pos = y * src_width + x;
let xp = x >= src_width / 2;
let yp = y >= src_height / 2;
let val = 0;
if (xp || yp)
val = 255;
if (xp && yp)
val = 0;
src_data[pos * 4 + 0] = val;
src_data[pos * 4 + 1] = val;
src_data[pos * 4 + 2] = val;
src_data[pos * 4 + 3] = 255;
}
}
var src_img = new ImageData(src_data, src_width, src_height);
return src_img;
}
export function ExtractData(video: HTMLVideoElement): ImageData {
var canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
let dst_context = canvas.getContext('2d')
dst_context.drawImage(video, 0, 0, canvas.width, canvas.height);
let dst_img = dst_context.getImageData(0, 0, canvas.width, canvas.height);
return dst_img
}
describe("VideoInputTest", () => {
beforeEach(() => {
});
it("AddRem", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = document.createElement("canvas")
expect(vi.HasDevice(name)).toBe(false);
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
expect(vi.HasDevice(name)).toBe(true);
vi.RemoveDevice(name);
expect(vi.HasDevice(name)).toBe(false);
});
it("GetDeviceNames", () => {
let name = "test_canvas";
let name2 = "test_canvas2";
let vi = new VideoInput();
let canvas = document.createElement("canvas")
let names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(0);
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(1);
expect(names[0]).toBe(name);
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(1);
expect(names[0]).toBe(name);
vi.AddCanvasDevice(canvas, name2, canvas.width, canvas.height, 30);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(2);
expect(names.sort()).toEqual([name, name2].sort());
});
it("GetStream", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = MakeTestCanvas();
let stream = vi.GetStream(name);
expect(stream).toBeNull();
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
stream = vi.GetStream(name);
expect(stream).toBeTruthy();
});
it("AddCanvasDevice_no_scaling", (done) => {
let name = "test_canvas";
let vi = new VideoInput();
const src_width = 40;
const src_height = 30;
let canvas = MakeTestCanvas(src_width, src_height);
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
let stream = vi.GetStream(name);
expect(stream).toBeTruthy();
let videoOutput = document.createElement("video")
videoOutput.onloadedmetadata = () => {
expect(videoOutput.videoWidth).toBe(src_width)
expect(videoOutput.videoHeight).toBe(src_height)
done()
}
videoOutput.srcObject = stream;
}, 1000);
it("AddCanvasDevice_scaling", (done) => {
let debug = false;
let name = "test_canvas";
let vi = new VideoInput();
const src_width = 64;
const src_height = 64;
const dst_width = 32;
const dst_height = 32;
let canvas = MakeTestCanvas(src_width, src_height);
let srcContext = canvas.getContext("2d");
var src_img = MakeTestImage(src_width, src_height);
srcContext.putImageData(src_img, 0, 0)
if (debug)
document.body.appendChild(canvas);
vi.AddCanvasDevice(canvas, name, dst_width, dst_height, 30);
let stream = vi.GetStream(name);
expect(stream).toBeTruthy();
let videoOutput = document.createElement("video")
if (debug)
document.body.appendChild(videoOutput);
videoOutput.onloadedmetadata = () => {
expect(videoOutput.videoWidth).toBe(dst_width)
expect(videoOutput.videoHeight).toBe(dst_height)
let dst_img_data = ExtractData(videoOutput)
//upper left
expect(dst_img_data.data[0]).toBe(0);
//upper right
expect(dst_img_data.data[((dst_width - 1) * 4)]).toBe(255);
//lower left
expect(dst_img_data.data[((dst_height - 1) * dst_width) * 4]).toBe(255);
//lower right
expect(dst_img_data.data[(dst_height * dst_width - 1) * 4]).toBe(0);
vi.RemoveDevice(name);
done()
}
videoOutput.srcObject = stream;
}, 1000);
//not yet clear how this can be handled
//this test will trigger an error in firefox
xit("GetStream_no_context", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = MakeBrokenTestCanvas();
//if we try to record from a canvas before
//a context was accessed it will fail.
//uncommenting this line fixes the bug
//but this is out of our control / within user code
//let ctx = canvas.getContext("2d");
let stream = vi.GetStream(name);
expect(stream).toBeNull();
vi.AddCanvasDevice(canvas, name, canvas.width, canvas.height, 30);
stream = vi.GetStream(name);
expect(stream).toBeTruthy();
});
//not yet clear how this can be handled
//this test will trigger an error in firefox
it("AddRemDevice", () => {
let name = "test_canvas";
const w = 640;
const h = 480;
const fps = 30;
let vi = new VideoInput();
let stream = vi.GetStream(name);
expect(stream).toBeNull();
vi.AddDevice(name, w, h, fps);
let res = vi.GetDeviceNames().indexOf(name);
expect(res).toBe(0);
vi.RemoveDevice(name);
let res2 = vi.GetDeviceNames().indexOf(name);
expect(res2).toBe(-1);
});
it("Device_int_array", () => {
let name = "test_canvas";
const w = 2;
const h = 2;
const fps = 30;
let arr = new Uint8ClampedArray([
1, 2, 3, 255,
4, 5, 6, 255,
7, 8, 9, 255,
10, 11, 12, 255,
13, 14, 15, 255
]);
let vi = new VideoInput();
vi.AddDevice(name, w, h, fps);
let stream = vi.GetStream(name);
expect(stream).toBeTruthy();
const clamped = new Uint8ClampedArray(arr.buffer, 4, 4 * 4);
const res = vi.UpdateFrame(name, clamped, w, h, VideoInputType.ARGB, 0, false);
expect(res).toBe(true);
let result_canvas = (vi as any).canvasDevices[name].canvas as HTMLCanvasElement;
expect(result_canvas.width).toBe(w);
expect(result_canvas.height).toBe(h);
let result_img = result_canvas.getContext("2d").getImageData(0, 0, result_canvas.width, result_canvas.height);
const result_arr = new Uint8Array(result_img.data.buffer);
const base_arr = new Uint8Array(arr.buffer, 4, 4 * 4);
expect(base_arr).toEqual(result_arr);
});
it("Device_full", () => {
let src_canvas = MakeTestCanvas();
let src_ctx = src_canvas.getContext("2d");
src_ctx.fillStyle = "yellow";
src_ctx.fillRect(0, 0, src_canvas.width, src_canvas.height);
let name = "test_canvas";
const w = 2;
const h = 2;
const fps = 30;
src_canvas.width = w;
src_canvas.height = h;
let vi = new VideoInput();
let src_img = src_ctx.getImageData(0, 0, src_canvas.width, src_canvas.height);
vi.AddDevice(name, w, h, fps);
let stream = vi.GetStream(name);
expect(stream).toBeTruthy();
const res = vi.UpdateFrame(name, src_img.data, src_img.width, src_img.height, VideoInputType.ARGB, 0, false);
expect(res).toBe(true);
//test if the internal array was set correctly
let result_canvas = (vi as any).canvasDevices[name].canvas as HTMLCanvasElement;
expect(result_canvas.width).toBe(src_canvas.width);
expect(result_canvas.height).toBe(src_canvas.height);
let result_img = result_canvas.getContext("2d").getImageData(0, 0, result_canvas.width, result_canvas.height);
expect(result_img.width).toBe(src_img.width);
expect(result_img.height).toBe(src_img.height);
expect(result_img.data).toEqual(src_img.data);
});
});
\ 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 { WebsocketTest } from "WebsocketNetworkTest";
import { IBasicNetworkTest } from "helper/IBasicNetworkTest";
import { NetworkEvent, IBasicNetwork, NetEventType, WebsocketNetwork,
ConnectionId, SignalingConfig, LocalNetwork, WebRtcNetwork, IWebRtcNetwork }
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("GetBufferedAmount", (done) => {
var srv: IWebRtcNetwork;
var address: string;
var srvToCltId: ConnectionId;
var clt: IWebRtcNetwork;
var cltToSrvId: ConnectionId;
var evt: NetworkEvent;
this.thenAsync((finished) => {
this._CreateServerClient((rsrv, raddress, rsrvToCltId, rclt, rcltToSrvId) => {
srv = rsrv as IWebRtcNetwork;
address = raddress;
srvToCltId = rsrvToCltId;
clt = rclt as IWebRtcNetwork;
cltToSrvId = rcltToSrvId;
finished();
});
});
this.then(() => {
//TODO: more detailed testing by actually triggering the buffer to fill?
//might be tricky as this is very system dependent
let buf:number;
buf = srv.GetBufferedAmount(srvToCltId, false);
expect(buf).toBe(0);
buf = srv.GetBufferedAmount(srvToCltId, true);
expect(buf).toBe(0);
buf = clt.GetBufferedAmount(cltToSrvId, false);
expect(buf).toBe(0);
buf = clt.GetBufferedAmount(cltToSrvId, true);
expect(buf).toBe(0);
done();
});
this.start();
});
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 sUrl = 'ws://192.168.1.3:12776';
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;
});
//can only be done manually so far
xit("Timeout", (done) => {
//this needs to be a local test server
//that can be disconnected to test the timeout
this.mUrl = "ws://192.168.1.3:12776";
var evt: NetworkEvent;
var srv: WebsocketNetwork;
var address;
this.thenAsync((finished) => {
this._CreateServerNetwork((rsrv, raddress) => {
srv = rsrv;
address = raddress;
finished();
});
});
this.thenAsync((finished) => {
console.log("Server ready at " + address);
expect(srv).not.toBeNull();
expect(address).not.toBeNull();
console.debug("Waiting for timeout");
this.waitForEvent(srv, finished, 120000);
});
this.then(() => {
console.log("Timeout over");
evt = srv.Dequeue();
expect(evt).not.toBeNull();
expect(evt.Type).toBe(NetEventType.ServerClosed);
expect(srv.getStatus()).toBe(WebsocketConnectionStatus.NotConnected);
done();
});
this.start();
}, 130000);
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, SLog, SLogLevel } from "../../awrtc/network/index";
export abstract class IBasicNetworkTest extends BasicNetworkTestBase {
public setup(): void {
super.setup();
let originalTimeout = 5000;
beforeEach(() => {
SLog.RequestLogLevel(SLogLevel.Info);
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = this.mDefaultWaitTimeout + 5000;
});
afterEach(() => {
console.debug("Test shutting down ...");
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"
export * from "./VideoInputTest"
export * from "./MediaTest"
{
"extends": "../awrtc/tsconfig_base",
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"lib" : ["ES2016", "dom"],
"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",
"DeviceApiTest.ts",
"VideoInputTest.ts",
"BrowserApiTest.ts",
"MediaTest.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
This source diff could not be displayed because it is too large. You can view the blob instead.
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