├── .gitattributes ├── .gitignore ├── LICENSE ├── MediaServer ├── .eslintrc.json ├── dist │ ├── app │ │ ├── app.js │ │ ├── app.js.map │ │ ├── models │ │ │ ├── ice-candidate-message.js │ │ │ ├── ice-candidate-message.js.map │ │ │ ├── media-stream-mixer.js │ │ │ ├── media-stream-mixer.js.map │ │ │ ├── webrtc-client.js │ │ │ ├── webrtc-client.js.map │ │ │ ├── webrtc-session.js │ │ │ └── webrtc-session.js.map │ │ └── services │ │ │ ├── signalr.service.js │ │ │ └── signalr.service.js.map │ └── environments │ │ ├── environment.js │ │ ├── environment.js.map │ │ ├── environment.prod.js │ │ └── environment.prod.js.map ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.ts │ │ ├── models │ │ │ ├── ice-candidate-message.ts │ │ │ ├── media-stream-mixer.ts │ │ │ ├── webrtc-client.ts │ │ │ └── webrtc-session.ts │ │ └── services │ │ │ └── signalr.service.ts │ └── environments │ │ ├── environment.prod.ts │ │ └── environment.ts ├── tsconfig.json └── web.config ├── README.md ├── SignalingServer ├── SignalingServer.sln └── SignalingServer │ ├── HubClient.cs │ ├── Hubs │ ├── AuthHub.cs │ ├── MCUSignalingHub.cs │ ├── MeshSignalingHub.cs │ ├── SFUSignalingHub.cs │ ├── SignalingHub.cs │ └── StarSignalingHub.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── SignalingServer.csproj │ ├── Startup.cs │ ├── TokenHelper.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ └── index.html ├── WebRTCAndroidApp ├── .gitignore ├── .idea │ ├── checkstyle-idea.xml │ ├── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── compiler.xml │ ├── deploymentTargetSelector.xml │ ├── dictionaries │ │ └── Dalibor.xml │ ├── gradle.xml │ ├── jarRepositories.xml │ ├── migrations.xml │ ├── misc.xml │ ├── other.xml │ └── vcs.xml ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── webrtcandroidapp │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── webrtcandroidapp │ │ │ │ ├── MainActivity.java │ │ │ │ ├── SessionCallActivity.java │ │ │ │ ├── WebRTCAndroidApp.java │ │ │ │ ├── ai │ │ │ │ └── FaceAnonymizer.java │ │ │ │ ├── capturers │ │ │ │ └── AndroidCameraCapturer.java │ │ │ │ ├── observers │ │ │ │ ├── CustomPeerConnectionObserver.java │ │ │ │ └── CustomSdpObserver.java │ │ │ │ └── services │ │ │ │ ├── IceServerService.java │ │ │ │ └── SignalrService.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_main_smartglass.xml │ │ │ ├── activity_session_call.xml │ │ │ └── activity_session_call_smartglass.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── webrtcandroidapp │ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local-sdk │ ├── build.gradle │ ├── build │ │ ├── .transforms │ │ │ └── 6030f42f041e570f73e6a8a4f8622bb1 │ │ │ │ ├── results.bin │ │ │ │ └── transformed │ │ │ │ └── empty │ │ │ │ └── desugar_graph.bin │ │ ├── libs │ │ │ └── local-sdk.jar │ │ └── tmp │ │ │ └── jar │ │ │ └── MANIFEST.MF │ └── libs │ │ └── google-webrtc-1.0.30039.aar └── settings.gradle └── WebRTCWebApp ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ └── home.component.ts │ │ ├── session-call-mcu │ │ │ ├── session-call-mcu.component.html │ │ │ ├── session-call-mcu.component.scss │ │ │ ├── session-call-mcu.component.spec.ts │ │ │ └── session-call-mcu.component.ts │ │ ├── session-call-mesh │ │ │ ├── session-call-mesh.component.html │ │ │ ├── session-call-mesh.component.scss │ │ │ ├── session-call-mesh.component.spec.ts │ │ │ └── session-call-mesh.component.ts │ │ ├── session-call-sfu │ │ │ ├── session-call-sfu.component.html │ │ │ ├── session-call-sfu.component.scss │ │ │ ├── session-call-sfu.component.spec.ts │ │ │ └── session-call-sfu.component.ts │ │ ├── session-call-star │ │ │ ├── session-call-star.component.html │ │ │ ├── session-call-star.component.scss │ │ │ ├── session-call-star.component.spec.ts │ │ │ └── session-call-star.component.ts │ │ └── session-call │ │ │ ├── session-call.component.html │ │ │ ├── session-call.component.scss │ │ │ ├── session-call.component.spec.ts │ │ │ └── session-call.component.ts │ ├── models │ │ ├── message-sender.ts │ │ ├── webrtc-client-type.ts │ │ └── webrtc-client.ts │ └── services │ │ ├── signalr.service.spec.ts │ │ ├── signalr.service.ts │ │ ├── webrtc-utils.service.spec.ts │ │ └── webrtc-utils.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dalibor Kofjač 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MediaServer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MediaServer/dist/app/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const express_1 = __importDefault(require("express")); 16 | const webrtc_session_1 = require("./models/webrtc-session"); 17 | const signalr_service_1 = require("./services/signalr.service"); 18 | const app = (0, express_1.default)(); 19 | const port = process.env.PORT || 3000; 20 | const sessions = []; 21 | const authSignaling = new signalr_service_1.SignalrService(); 22 | const sfuSignaling = new signalr_service_1.SignalrService(); 23 | const mcuSignaling = new signalr_service_1.SignalrService(); 24 | app.get('/', (req, res) => { 25 | res.send('The WebRTC media server is running.'); 26 | }); 27 | app.listen(port, () => { 28 | return console.log(`Express is listening at http://localhost:${port}`); 29 | }); 30 | authSignaling.connect('/auth').then(() => { 31 | if (authSignaling.isConnected()) { 32 | authSignaling.invoke('Authorize').then((token) => { 33 | if (token) { 34 | sfuSignaling.connect('/sfu-signaling', token).then(() => __awaiter(void 0, void 0, void 0, function* () { 35 | if (sfuSignaling.isConnected()) { 36 | sfuSignaling.define('room created', (room) => { 37 | if (!sessions.find(s => s.getRoom() === room)) { 38 | const newSession = new webrtc_session_1.WebRTCSession(room, sfuSignaling); 39 | sessions.push(newSession); 40 | } 41 | }); 42 | } 43 | })); 44 | mcuSignaling.connect('/mcu-signaling', token).then(() => __awaiter(void 0, void 0, void 0, function* () { 45 | if (mcuSignaling.isConnected()) { 46 | mcuSignaling.define('room created', (room) => { 47 | if (!sessions.find(s => s.getRoom() === room)) { 48 | const newSession = new webrtc_session_1.WebRTCSession(room, mcuSignaling, true); 49 | sessions.push(newSession); 50 | } 51 | }); 52 | } 53 | })); 54 | } 55 | }); 56 | } 57 | }); 58 | //# sourceMappingURL=app.js.map -------------------------------------------------------------------------------- /MediaServer/dist/app/app.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/app/app.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,sDAA8B;AAC9B,4DAAwD;AACxD,gEAA4D;AAE5D,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AACtC,MAAM,QAAQ,GAAoB,EAAE,CAAC;AACrC,MAAM,aAAa,GAAG,IAAI,gCAAc,EAAE,CAAC;AAC3C,MAAM,YAAY,GAAG,IAAI,gCAAc,EAAE,CAAC;AAC1C,MAAM,YAAY,GAAG,IAAI,gCAAc,EAAE,CAAC;AAE1C,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACxB,GAAG,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,4CAA4C,IAAI,EAAE,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AAEH,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvC,IAAI,aAAa,CAAC,WAAW,EAAE,EAAE;QAC/B,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,KAAa,EAAE,EAAE;YACvD,IAAI,KAAK,EAAE;gBACT,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,GAAS,EAAE;oBAC5D,IAAI,YAAY,CAAC,WAAW,EAAE,EAAE;wBAC9B,YAAY,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,IAAY,EAAE,EAAE;4BACnD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,CAAC,EAAE;gCAC7C,MAAM,UAAU,GAAG,IAAI,8BAAa,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;gCACzD,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;6BAC3B;wBACH,CAAC,CAAC,CAAC;qBACJ;gBACH,CAAC,CAAA,CAAC,CAAC;gBACH,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,GAAS,EAAE;oBAC5D,IAAI,YAAY,CAAC,WAAW,EAAE,EAAE;wBAC9B,YAAY,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,IAAY,EAAE,EAAE;4BACnD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,CAAC,EAAE;gCAC7C,MAAM,UAAU,GAAG,IAAI,8BAAa,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;gCAC/D,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;6BAC3B;wBACH,CAAC,CAAC,CAAC;qBACJ;gBACH,CAAC,CAAA,CAAC,CAAC;aACJ;QACH,CAAC,CAAC,CAAC;KACJ;AACH,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /MediaServer/dist/app/models/ice-candidate-message.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.IceCandidateMessage = void 0; 4 | class IceCandidateMessage { 5 | } 6 | exports.IceCandidateMessage = IceCandidateMessage; 7 | //# sourceMappingURL=ice-candidate-message.js.map -------------------------------------------------------------------------------- /MediaServer/dist/app/models/ice-candidate-message.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ice-candidate-message.js","sourceRoot":"","sources":["../../../src/app/models/ice-candidate-message.ts"],"names":[],"mappings":";;;AAAA,MAAa,mBAAmB;CAI/B;AAJD,kDAIC"} -------------------------------------------------------------------------------- /MediaServer/dist/app/models/webrtc-client.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"webrtc-client.js","sourceRoot":"","sources":["../../../src/app/models/webrtc-client.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /MediaServer/dist/app/models/webrtc-session.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"webrtc-session.js","sourceRoot":"","sources":["../../../src/app/models/webrtc-session.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /MediaServer/dist/app/services/signalr.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.SignalrService = void 0; 13 | const signalr_1 = require("@microsoft/signalr"); 14 | const environment_1 = require("../../environments/environment"); 15 | class SignalrService { 16 | constructor() { 17 | this.baseUrl = environment_1.environment.signalingServerUrl; 18 | } 19 | getConnectionId() { 20 | return this.hubConnection.connectionId; 21 | } 22 | connect(path, token = null) { 23 | return __awaiter(this, void 0, void 0, function* () { 24 | const url = this.baseUrl + path; 25 | const builder = new signalr_1.HubConnectionBuilder(); 26 | if (!token) { 27 | builder.withUrl(url); 28 | } 29 | else { 30 | builder.withUrl(url, { 31 | accessTokenFactory: () => { 32 | return token; 33 | } 34 | }); 35 | } 36 | this.hubConnection = builder.withAutomaticReconnect().build(); 37 | return this.hubConnection.start() 38 | .then(() => { 39 | if (this.isConnected()) { 40 | console.log('SignalR: Connected to the server: ' + url); 41 | } 42 | }) 43 | .catch(err => { 44 | console.error('SignalR: Failed to start with error: ' + err.toString()); 45 | }); 46 | }); 47 | } 48 | define(methodName, newMethod) { 49 | return __awaiter(this, void 0, void 0, function* () { 50 | if (this.hubConnection) { 51 | this.hubConnection.on(methodName, newMethod); 52 | } 53 | }); 54 | } 55 | invoke(methodName, ...args) { 56 | return __awaiter(this, void 0, void 0, function* () { 57 | if (this.isConnected()) { 58 | return this.hubConnection.invoke(methodName, ...args); 59 | } 60 | }); 61 | } 62 | disconnect() { 63 | if (this.isConnected()) { 64 | this.hubConnection.stop(); 65 | } 66 | } 67 | isConnected() { 68 | return this.hubConnection && this.hubConnection.state === signalr_1.HubConnectionState.Connected; 69 | } 70 | } 71 | exports.SignalrService = SignalrService; 72 | //# sourceMappingURL=signalr.service.js.map -------------------------------------------------------------------------------- /MediaServer/dist/app/services/signalr.service.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"signalr.service.js","sourceRoot":"","sources":["../../../src/app/services/signalr.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,gDAAqH;AACrH,gEAA6D;AAE7D,MAAa,cAAc;IAA3B;QAEU,YAAO,GAAW,yBAAW,CAAC,kBAAkB,CAAC;IAuD3D,CAAC;IAnDC,eAAe;QACb,OAAO,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC;IACzC,CAAC;IAEK,OAAO,CAAC,IAAY,EAAE,QAAgB,IAAI;;YAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YAEhC,MAAM,OAAO,GAAG,IAAI,8BAAoB,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,EAAE;gBACV,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;aACtB;iBAAM;gBACL,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;oBACnB,kBAAkB,EAAE,GAAG,EAAE;wBACvB,OAAO,KAAK,CAAC;oBACf,CAAC;iBACwB,CAAC,CAAC;aAC9B;YACD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,KAAK,EAAE,CAAC;YAE9D,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE;iBAC9B,IAAI,CAAC,GAAG,EAAE;gBACT,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE;oBACtB,OAAO,CAAC,GAAG,CAAC,oCAAoC,GAAG,GAAG,CAAC,CAAC;iBACzD;YACH,CAAC,CAAC;iBACD,KAAK,CAAC,GAAG,CAAC,EAAE;gBACX,OAAO,CAAC,KAAK,CAAC,uCAAuC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC1E,CAAC,CAAC,CAAC;QACP,CAAC;KAAA;IAEK,MAAM,CAAC,UAAkB,EAAE,SAAuC;;YACtE,IAAI,IAAI,CAAC,aAAa,EAAE;gBACtB,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;aAC9C;QACH,CAAC;KAAA;IAEK,MAAM,CAAC,UAAkB,EAAE,GAAG,IAAe;;YACjD,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE;gBACtB,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC;aACvD;QACH,CAAC;KAAA;IAED,UAAU;QACR,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE;YACtB,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;SAC3B;IACH,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,aAAa,CAAC,KAAK,KAAK,4BAAkB,CAAC,SAAS,CAAC;IACzF,CAAC;CACF;AAzDD,wCAyDC"} -------------------------------------------------------------------------------- /MediaServer/dist/environments/environment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // This file can be replaced during build by using the `fileReplacements` array. 3 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 4 | // The list of file replacements can be found in `angular.json`. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.environment = void 0; 7 | exports.environment = { 8 | production: false, 9 | signalingServerUrl: 'http://localhost:5000/hubs', 10 | iceServers: [ 11 | { urls: 'stun:stun.1.google.com:19302' }, 12 | { urls: 'stun:stun1.l.google.com:19302' } 13 | ] 14 | }; 15 | /* 16 | * For easier debugging in development mode, you can import the following file 17 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 18 | * 19 | * This import should be commented out in production mode because it will have a negative impact 20 | * on performance if an error is thrown. 21 | */ 22 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 23 | //# sourceMappingURL=environment.js.map -------------------------------------------------------------------------------- /MediaServer/dist/environments/environment.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/environments/environment.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,0EAA0E;AAC1E,gEAAgE;;;AAEnD,QAAA,WAAW,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,kBAAkB,EAAE,4BAA4B;IAChD,UAAU,EAAE;QACV,EAAC,IAAI,EAAE,8BAA8B,EAAC;QACtC,EAAC,IAAI,EAAE,+BAA+B,EAAC;KACxC;CACF,CAAC;AAEF;;;;;;GAMG;AACH,mEAAmE"} -------------------------------------------------------------------------------- /MediaServer/dist/environments/environment.prod.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.environment = void 0; 4 | exports.environment = { 5 | production: true, 6 | signalingServerUrl: 'http://localhost:5000/hubs', 7 | iceServers: [ 8 | { urls: 'stun:stun.1.google.com:19302' }, 9 | { urls: 'stun:stun1.l.google.com:19302' } 10 | ] 11 | }; 12 | //# sourceMappingURL=environment.prod.js.map -------------------------------------------------------------------------------- /MediaServer/dist/environments/environment.prod.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"environment.prod.js","sourceRoot":"","sources":["../../src/environments/environment.prod.ts"],"names":[],"mappings":";;;AAAa,QAAA,WAAW,GAAG;IACzB,UAAU,EAAE,IAAI;IAChB,kBAAkB,EAAE,4BAA4B;IAChD,UAAU,EAAE;QACV,EAAC,IAAI,EAAE,8BAA8B,EAAC;QACtC,EAAC,IAAI,EAAE,+BAA+B,EAAC;KACxC;CACF,CAAC"} -------------------------------------------------------------------------------- /MediaServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediaserver", 3 | "version": "1.0.0", 4 | "description": "The WebRTC media server.", 5 | "main": "dist/app/app.js", 6 | "scripts": { 7 | "start": "tsc && node dist/app/app.js", 8 | "lint": "eslint . --ext .ts", 9 | "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"", 10 | "build": "tsc" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/express": "^4.17.1", 16 | "@typescript-eslint/eslint-plugin": "^5.51.0", 17 | "@typescript-eslint/parser": "^5.51.0", 18 | "concurrently": "^7.6.0", 19 | "eslint": "^8.34.0", 20 | "nodemon": "^2.0.20", 21 | "typescript": "^4.9.5" 22 | }, 23 | "dependencies": { 24 | "@mapbox/node-pre-gyp": "^1.0.10", 25 | "@microsoft/signalr": "^8.0.7", 26 | "canvas": "^2.11.2", 27 | "express": "^4.17.1", 28 | "wrtc": "0.4.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MediaServer/src/app/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { WebRTCSession } from './models/webrtc-session'; 3 | import { SignalrService } from './services/signalr.service'; 4 | 5 | const app = express(); 6 | const port = process.env.PORT || 3000; 7 | const sessions: WebRTCSession[] = []; 8 | const authSignaling = new SignalrService(); 9 | const sfuSignaling = new SignalrService(); 10 | const mcuSignaling = new SignalrService(); 11 | 12 | app.get('/', (req, res) => { 13 | res.send('The WebRTC media server is running.'); 14 | }); 15 | 16 | app.listen(port, () => { 17 | return console.log(`Express is listening at http://localhost:${port}`); 18 | }); 19 | 20 | authSignaling.connect('/auth').then(() => { 21 | if (authSignaling.isConnected()) { 22 | authSignaling.invoke('Authorize').then((token: string) => { 23 | if (token) { 24 | sfuSignaling.connect('/sfu-signaling', token).then(async () => { 25 | if (sfuSignaling.isConnected()) { 26 | sfuSignaling.define('room created', (room: string) => { 27 | if (!sessions.find(s => s.getRoom() === room)) { 28 | const newSession = new WebRTCSession(room, sfuSignaling); 29 | sessions.push(newSession); 30 | } 31 | }); 32 | } 33 | }); 34 | mcuSignaling.connect('/mcu-signaling', token).then(async () => { 35 | if (mcuSignaling.isConnected()) { 36 | mcuSignaling.define('room created', (room: string) => { 37 | if (!sessions.find(s => s.getRoom() === room)) { 38 | const newSession = new WebRTCSession(room, mcuSignaling, true); 39 | sessions.push(newSession); 40 | } 41 | }); 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | }); 48 | 49 | -------------------------------------------------------------------------------- /MediaServer/src/app/models/ice-candidate-message.ts: -------------------------------------------------------------------------------- 1 | export class IceCandidateMessage { 2 | label: number; 3 | candidate: string; 4 | id: string; 5 | } -------------------------------------------------------------------------------- /MediaServer/src/app/models/webrtc-client.ts: -------------------------------------------------------------------------------- 1 | import { environment } from "../../environments/environment"; 2 | import { IceCandidateMessage } from "./ice-candidate-message"; 3 | import { RTCPeerConnection, MediaStream, RTCIceCandidate, 4 | RTCPeerConnectionIceEvent, RTCRtpTransceiver, RTCSessionDescription } from 'wrtc'; 5 | 6 | export class WebRTCClient { 7 | 8 | private streams: MediaStream[] = []; 9 | private peerConnection: RTCPeerConnection; 10 | private isStarted = false; 11 | 12 | constructor( 13 | private clientId: string, 14 | private isInitiator: boolean, 15 | private sendMessageCallback: (message: unknown) => void, 16 | private onStreamCallback: (stream: MediaStream) => void, 17 | private onHangupCallback: () => void, 18 | private remoteStreamsCallback: () => MediaStream[] = () => { return null; }) { 19 | } 20 | 21 | getClientId(): string { 22 | return this.clientId; 23 | } 24 | 25 | getIsInitiator(): boolean { 26 | return this.isInitiator; 27 | } 28 | 29 | getIsStarted(): boolean { 30 | return this.isStarted; 31 | } 32 | 33 | getStreams(): MediaStream[] { 34 | return this.streams; 35 | } 36 | 37 | initiateCall(): void { 38 | console.log('Initiating a call.', this.clientId); 39 | if (!this.isStarted) { 40 | this.createPeerConnection(); 41 | 42 | this.remoteStreamsCallback()?.forEach(stream => { 43 | this.peerConnection.addTrack(stream.getVideoTracks()[0], stream); 44 | this.peerConnection.addTrack(stream.getAudioTracks()[0], stream); 45 | }); 46 | 47 | this.isStarted = true; 48 | if (this.isInitiator) { 49 | this.sendOffer(); 50 | } 51 | } 52 | } 53 | 54 | createPeerConnection(): void { 55 | console.log('Creating peer connection.', this.clientId); 56 | try { 57 | this.peerConnection = new RTCPeerConnection({ 58 | iceServers: environment.iceServers, 59 | sdpSemantics: 'unified-plan' 60 | } as RTCConfiguration); 61 | 62 | this.peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => { 63 | if (event.candidate) { 64 | this.sendIceCandidate(event); 65 | } else { 66 | console.log('End of candidates.', this.clientId); 67 | } 68 | }; 69 | 70 | this.peerConnection.ontrack = (event: RTCTrackEvent) => { 71 | if (event.streams[0]) { 72 | this.addRemoteStream(event.streams[0]); 73 | } 74 | }; 75 | } catch (e) { 76 | console.log('Failed to create PeerConnection.', this.clientId, e.message); 77 | return; 78 | } 79 | } 80 | 81 | sendOffer(): void { 82 | console.log('Sending offer to peer.', this.clientId); 83 | this.addTransceivers(); 84 | this.peerConnection.createOffer() 85 | .then((sdp: RTCSessionDescription) => { 86 | this.peerConnection.setLocalDescription(sdp); 87 | this.sendMessageCallback(sdp); 88 | }); 89 | } 90 | 91 | sendAnswer(): void { 92 | console.log('Sending answer to peer.', this.clientId); 93 | this.addTransceivers(); 94 | this.peerConnection.createAnswer() 95 | .then((sdp: RTCSessionDescription) => { 96 | this.peerConnection.setLocalDescription(sdp); 97 | this.sendMessageCallback(sdp); 98 | }); 99 | } 100 | 101 | addIceCandidate(message: IceCandidateMessage): void { 102 | console.log('Adding ice candidate.', this.clientId); 103 | const candidate = new RTCIceCandidate({ 104 | sdpMLineIndex: message.label, 105 | candidate: message.candidate, 106 | sdpMid: message.id 107 | }); 108 | this.peerConnection.addIceCandidate(candidate); 109 | } 110 | 111 | sendIceCandidate(event: RTCPeerConnectionIceEvent): void { 112 | console.log('Sending ice candidate to remote peer.', this.clientId); 113 | this.sendMessageCallback({ 114 | type: 'candidate', 115 | label: event.candidate.sdpMLineIndex, 116 | id: event.candidate.sdpMid, 117 | candidate: event.candidate.candidate 118 | }); 119 | } 120 | 121 | setRemoteDescription(message: RTCSessionDescription) { 122 | console.log('Setting remote description.', this.clientId); 123 | this.peerConnection.setRemoteDescription(new RTCSessionDescription(message)); 124 | } 125 | 126 | addTransceivers(): void { 127 | console.log('Adding transceivers.', this.clientId); 128 | const init = { direction: 'recvonly', streams: [], sendEncodings: [] } as RTCRtpTransceiver; 129 | this.peerConnection.addTransceiver('audio', init); 130 | this.peerConnection.addTransceiver('video', init); 131 | } 132 | 133 | addRemoteStream(stream: MediaStream): void { 134 | console.log('Adding remote stream.', this.clientId); 135 | if (!this.streams.find(s => s.id === stream.id)) { 136 | this.streams.push(stream); 137 | this.onStreamCallback(stream); 138 | } 139 | } 140 | 141 | addRemoteTracks(stream: MediaStream): void { 142 | console.log('Adding remote tracks.', this.clientId); 143 | this.peerConnection.addTrack(stream.getVideoTracks()[0], stream); 144 | this.peerConnection.addTrack(stream.getAudioTracks()[0], stream); 145 | this.sendOffer(); 146 | } 147 | 148 | removeRemoteStream(streamId: string): void { 149 | console.log('Removing remote stream.', this.clientId); 150 | const streamToRemove = this.streams.find(s => s.id === streamId); 151 | this.streams = this.streams.filter(s => s !== streamToRemove); 152 | streamToRemove.getTracks().forEach((track) => { track.stop(); }); 153 | } 154 | 155 | handleRemoteHangup(): void { 156 | console.log('Session terminated by remote peer.', this.clientId); 157 | this.stopPeerConnection(); 158 | this.onHangupCallback(); 159 | } 160 | 161 | stopPeerConnection(): void { 162 | console.log('Stopping peer connection.', this.clientId); 163 | this.isStarted = false; 164 | if (this.peerConnection) { 165 | this.peerConnection.close(); 166 | this.peerConnection = null; 167 | } 168 | this.streams.forEach(stream => { 169 | if (stream && stream.active) { 170 | stream.getTracks().forEach((track) => { track.stop(); }); 171 | } 172 | }); 173 | } 174 | } -------------------------------------------------------------------------------- /MediaServer/src/app/models/webrtc-session.ts: -------------------------------------------------------------------------------- 1 | import { SignalrService } from "../services/signalr.service"; 2 | import { WebRTCClient } from "./webrtc-client"; 3 | import { MediaStream } from 'wrtc'; 4 | import { MediaStreamMixer } from "./media-stream-mixer"; 5 | 6 | export enum WebRTCClientType { 7 | CentralUnit = 'central_unit', 8 | SideUnit = 'side_unit' 9 | } 10 | 11 | export class WebRTCSession { 12 | 13 | remoteStreams: MediaStream[] = []; 14 | clients: WebRTCClient[] = []; 15 | mixer: MediaStreamMixer; 16 | 17 | constructor( 18 | private room: string, 19 | private signaling: SignalrService, 20 | private mixStreams = false) { 21 | if (this.mixStreams) { 22 | this.mixer = new MediaStreamMixer(); 23 | } 24 | this.start(); 25 | } 26 | 27 | getRoom(): string { 28 | return this.room; 29 | } 30 | 31 | async start(): Promise { 32 | if (this.signaling.isConnected()) { 33 | const otherClientsInRoom = (await this.signaling.invoke('CreateOrJoinRoom', this.room, WebRTCClientType.CentralUnit)) as string[]; 34 | otherClientsInRoom.forEach(client => { 35 | this.tryCreateClient(client, false); 36 | }); 37 | } 38 | 39 | this.defineSignaling(); 40 | } 41 | 42 | defineSignaling(): void { 43 | this.signaling.define('log', (message: unknown) => { 44 | console.log(message); 45 | }); 46 | 47 | this.signaling.define('joined', (clientId: string) => { 48 | this.tryCreateClient(clientId, true); 49 | }); 50 | 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | this.signaling.define('message', (message: any, clientId: string) => { 53 | const client = this.findClient(clientId); 54 | if (client) { 55 | if (message === 'got user media') { 56 | client.initiateCall(); 57 | 58 | } else if (message.type === 'offer') { 59 | if (!client.getIsStarted()) { 60 | client.initiateCall(); 61 | } 62 | client.setRemoteDescription(message); 63 | client.sendAnswer(); 64 | 65 | } else if (message.type === 'answer' && client.getIsStarted()) { 66 | client.setRemoteDescription(message); 67 | 68 | } else if (message.type === 'candidate' && client.getIsStarted()) { 69 | client.addIceCandidate(message); 70 | 71 | } else if (message.type === 'streams removed' && client.getIsStarted()) { 72 | const streamIds = message.streams; 73 | streamIds.forEach((streamId: string) => { 74 | client.removeRemoteStream(streamId); 75 | }); 76 | this.removeRemoteStreams(client.getClientId(), streamIds); 77 | 78 | } else if (message === 'bye' && client.getIsStarted()) { 79 | client.handleRemoteHangup(); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | findClient(clientId: string): WebRTCClient { 86 | return this.clients?.filter(c => c.getClientId() === clientId)[0]; 87 | } 88 | 89 | tryCreateClient(clientId: string, initiator: boolean): void { 90 | if (!this.findClient(clientId)) { 91 | const client = new WebRTCClient(clientId, initiator, 92 | (message: unknown) => this.sendMessage(message, clientId), 93 | (remoteStream: MediaStream) => this.addRemoteStream(clientId, remoteStream), 94 | () => this.removeClient(clientId), 95 | () => this.remoteStreams); 96 | 97 | this.clients.push(client); 98 | 99 | setTimeout(() => { 100 | if (!client.getIsStarted()) { 101 | client.initiateCall(); 102 | } 103 | }, 3000); 104 | } 105 | } 106 | 107 | addRemoteStream(clientId: string, stream: MediaStream): void { 108 | if (!this.remoteStreams.find(s => s.id === stream.id)) { 109 | this.remoteStreams.push(stream); 110 | if (this.mixStreams) { 111 | this.mixer.appendStreams(stream); 112 | if (this.remoteStreams.length === 1) { 113 | return; 114 | } else if (this.remoteStreams.length === 2) { 115 | this.clients.forEach(client => { 116 | const mixedStream = this.mixer.getMixedStream(); 117 | if (mixedStream) { 118 | client.addRemoteTracks(mixedStream); 119 | } 120 | }); 121 | } else { 122 | const client = this.findClient(clientId); 123 | const mixedStream = this.mixer.getMixedStream(); 124 | if (mixedStream) { 125 | client.addRemoteTracks(mixedStream); 126 | } 127 | } 128 | } else { 129 | this.clients.forEach(client => { 130 | if (clientId !== client.getClientId()) { 131 | client.addRemoteTracks(stream); 132 | } 133 | }); 134 | } 135 | } else { 136 | this.clients.forEach(client => { 137 | if (clientId !== client.getClientId()) { 138 | client.addRemoteTracks(stream); 139 | } 140 | }); 141 | } 142 | } 143 | 144 | removeRemoteStreams(clientId: string, streamIds: string[]): void { 145 | this.remoteStreams = this.remoteStreams.filter(s => !streamIds.find(i => i === s.id)); 146 | if (this.mixStreams) { 147 | this.mixer.removeStreams(streamIds); 148 | } else { 149 | this.clients.forEach(client => { 150 | if (client.getClientId() !== clientId) { 151 | const message = { 152 | type: 'streams removed', 153 | streams: streamIds 154 | }; 155 | this.sendMessage(message, client.getClientId()); 156 | } 157 | }); 158 | } 159 | 160 | } 161 | 162 | removeClient(clientId: string): void { 163 | const clientToRemove = this.clients.find(c => c.getClientId() === clientId); 164 | this.clients = this.clients.filter(c => c !== clientToRemove); 165 | this.removeRemoteStreams(clientId, clientToRemove.getStreams().map(s => s.id)); 166 | } 167 | 168 | sendMessage(message: unknown, client: string = null): void { 169 | this.signaling.invoke('SendMessage', message, this.room, client); 170 | } 171 | } -------------------------------------------------------------------------------- /MediaServer/src/app/services/signalr.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HubConnection, HubConnectionBuilder, HubConnectionState, IHttpConnectionOptions } from '@microsoft/signalr'; 3 | import { environment } from '../../environments/environment'; 4 | 5 | export class SignalrService { 6 | 7 | private baseUrl: string = environment.signalingServerUrl; 8 | 9 | private hubConnection: HubConnection | undefined; 10 | 11 | getConnectionId(): string { 12 | return this.hubConnection.connectionId; 13 | } 14 | 15 | async connect(path: string, token: string = null): Promise { 16 | const url = this.baseUrl + path; 17 | 18 | const builder = new HubConnectionBuilder(); 19 | if (!token) { 20 | builder.withUrl(url); 21 | } else { 22 | builder.withUrl(url, { 23 | accessTokenFactory: () => { 24 | return token; 25 | } 26 | } as IHttpConnectionOptions); 27 | } 28 | this.hubConnection = builder.withAutomaticReconnect().build(); 29 | 30 | return this.hubConnection.start() 31 | .then(() => { 32 | if (this.isConnected()) { 33 | console.log('SignalR: Connected to the server: ' + url); 34 | } 35 | }) 36 | .catch(err => { 37 | console.error('SignalR: Failed to start with error: ' + err.toString()); 38 | }); 39 | } 40 | 41 | async define(methodName: string, newMethod: (...args: unknown[]) => void): Promise { 42 | if (this.hubConnection) { 43 | this.hubConnection.on(methodName, newMethod); 44 | } 45 | } 46 | 47 | async invoke(methodName: string, ...args: unknown[]): Promise { 48 | if (this.isConnected()) { 49 | return this.hubConnection.invoke(methodName, ...args); 50 | } 51 | } 52 | 53 | disconnect(): void { 54 | if (this.isConnected()) { 55 | this.hubConnection.stop(); 56 | } 57 | } 58 | 59 | isConnected(): boolean { 60 | return this.hubConnection && this.hubConnection.state === HubConnectionState.Connected; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MediaServer/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | signalingServerUrl: 'http://localhost:5000/hubs', 4 | iceServers: [ 5 | {urls: 'stun:stun.1.google.com:19302'}, 6 | {urls: 'stun:stun1.l.google.com:19302'} 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /MediaServer/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | signalingServerUrl: 'http://localhost:5000/hubs', 8 | iceServers: [ 9 | {urls: 'stun:stun.1.google.com:19302'}, 10 | {urls: 'stun:stun1.l.google.com:19302'} 11 | ] 12 | }; 13 | 14 | /* 15 | * For easier debugging in development mode, you can import the following file 16 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 17 | * 18 | * This import should be commented out in production mode because it will have a negative impact 19 | * on performance if an error is thrown. 20 | */ 21 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 22 | -------------------------------------------------------------------------------- /MediaServer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": ["es2015"] 11 | } -------------------------------------------------------------------------------- /MediaServer/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC tutorials 2 | The series of tutorial apps for WebRTC using Angular (Typescript), Android (Java), SignalR (C#) and NodeJS (Typescript). 3 | 4 | The tutorial apps include: 5 | - basic peer-to-peer implementation (for web and mobile apps), 6 | - multi-peer implementation (for web apps) via all four of the most known WebRTC topologies - MESH, STAR, SFU (Selective Forwarding Unit) and MCU (Multipoint Control Unit). 7 | 8 | The signaling server is implemented using Microsoft SignalR. The media (streaming) server is implemented using NodeJS. 9 | 10 | # The MCU audio/video track(s) mixing 11 | The audio/video track(s) mixing for MCU (Multipoint Control Unit) topology is implemented fully custom in NodeJS, using [node-webrtc](https://github.com/node-webrtc/node-webrtc) and its Nonstandard APIs. The implementation can be seen in the [MediaStreamMixer class](https://github.com/dalkofjac/webrtc-tutorials/blob/master/MediaServer/src/app/models/media-stream-mixer.ts). 12 | 13 | # The face recognition (with MediaPipe) 14 | The face recognition (anonymization) in Android app is implemented using [Google's MediaPipe](https://github.com/google-ai-edge/mediapipe) algorithms. The implementation can be seen in the [FaceAnonymizer class](https://github.com/dalkofjac/webrtc-tutorials/blob/master/WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/ai/FaceAnonymizer.java). To test it out, just set the "USE_FACE_ANONYMIZATION" flag to "true" in [AndroidCameraCapturer class](https://github.com/dalkofjac/webrtc-tutorials/blob/master/WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/capturers/AndroidCameraCapturer.java). 15 | 16 | # Android-based AR smartglass support 17 | The WebRTC Android app now has full support for Android-based Augmented Reality smartglass devices (such as Vuzix M400, Google Glass EE2, RealWear Navigator 500, Almer Arc2 and similar). The implementation can be seen in [WebRTCAndroidApp](https://github.com/dalkofjac/webrtc-tutorials/tree/master/WebRTCAndroidApp). To test it out, just set the "USE_SMARTGLASS_OPTIMIZATION" flag to "true" in [WebRTCAndroidApp class](https://github.com/dalkofjac/webrtc-tutorials/blob/master/WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/WebRTCAndroidApp.java). 18 | 19 | # Blog 20 | Find more details on the implementation itself on the [blog site](https://ekobit.com/author/dkofjacekobit-hr/) and [my LinkedIn](https://www.linkedin.com/in/dalibor-kofjac/). 21 | 22 | # License 23 | This repository uses [MIT license](https://github.com/dalkofjac/webrtc-tutorials/blob/master/LICENSE). Copyright (c) Dalibor Kofjač. 24 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalingServer", "SignalingServer\SignalingServer.csproj", "{3B43B9EF-34B6-4665-A60E-493CB851FFD1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3B43B9EF-34B6-4665-A60E-493CB851FFD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3B43B9EF-34B6-4665-A60E-493CB851FFD1}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3B43B9EF-34B6-4665-A60E-493CB851FFD1}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3B43B9EF-34B6-4665-A60E-493CB851FFD1}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {DBEF8DC5-EEC8-470A-9756-9642243932E1} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/HubClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SignalingServer 4 | { 5 | public class HubClient 6 | { 7 | public string Id { get; set; } 8 | 9 | public string Type { get; set; } 10 | 11 | public HubClient(string id, string type) 12 | { 13 | Id = id; 14 | Type = type; 15 | } 16 | } 17 | 18 | public static class HubClientType 19 | { 20 | public static readonly string CentralUnit = "central_unit"; 21 | 22 | public static readonly string SideUnit = "side_unit"; 23 | 24 | public static readonly List AllTypes = new() { CentralUnit, SideUnit }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/AuthHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Threading.Tasks; 4 | 5 | namespace SignalingServer.Hubs 6 | { 7 | [AllowAnonymous] 8 | public class AuthHub : Hub 9 | { 10 | public async Task Authorize() 11 | { 12 | return await Task.Run(() => { return TokenHelper.GenerateToken(); }); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/MCUSignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SignalingServer.Hubs 8 | { 9 | [Authorize] 10 | public class MCUSignalingHub : Hub 11 | { 12 | private static readonly Dictionary> ConnectedClients = new(); 13 | 14 | public async Task SendMessage(object message, string roomName, string receiver = null) 15 | { 16 | if (IsClientInRoom(roomName)) 17 | { 18 | if (receiver == null) 19 | { 20 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the whole room: " + message, roomName); 21 | await Clients.OthersInGroup(roomName).SendAsync("message", message, Context.ConnectionId); 22 | } 23 | else 24 | { 25 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the client " + receiver + ": " + message, roomName); 26 | await Clients.Client(receiver).SendAsync("message", message, Context.ConnectionId); 27 | } 28 | } 29 | } 30 | 31 | public async Task> CreateOrJoinRoom(string roomName, string clientType) 32 | { 33 | await EmitLog("Received request to create or join room " + roomName + " from a client " + Context.ConnectionId + " of type " + clientType, roomName); 34 | 35 | if (clientType == HubClientType.CentralUnit && ConnectedClients.ContainsKey(roomName) 36 | && ConnectedClients[roomName].Where(c => c.Type == HubClientType.CentralUnit).FirstOrDefault() != null) 37 | { 38 | await EmitFull(); 39 | return null; 40 | } 41 | 42 | if (!ConnectedClients.ContainsKey(roomName)) 43 | { 44 | ConnectedClients.Add(roomName, new List()); 45 | } 46 | 47 | if (!IsClientInRoom(roomName)) 48 | { 49 | ConnectedClients[roomName].Add(new HubClient(Context.ConnectionId, clientType)); 50 | } 51 | 52 | await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 53 | 54 | await EmitJoined(roomName, clientType); 55 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientType + " joined the room " + roomName, roomName); 56 | 57 | var numberOfClients = ConnectedClients[roomName].Count; 58 | await EmitLog("Room " + roomName + " now has " + numberOfClients + " client(s)", roomName); 59 | if (numberOfClients == 1) 60 | { 61 | await EmitRoomCreated(roomName); 62 | } 63 | 64 | return GetOppositeTypeHubClients(roomName, clientType); 65 | } 66 | 67 | public async Task LeaveRoom(string roomName) 68 | { 69 | await EmitLog("Received request to leave the room " + roomName + " from a client " + Context.ConnectionId, roomName); 70 | 71 | if (IsClientInRoom(roomName)) 72 | { 73 | var clientToRemove = ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault(); 74 | ConnectedClients[roomName].Remove(clientToRemove); 75 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientToRemove.Type + " left the room " + roomName, roomName); 76 | 77 | if (ConnectedClients[roomName].Count == 0) 78 | { 79 | ConnectedClients.Remove(roomName); 80 | await EmitLog("Room " + roomName + " is now empty - resetting its state", roomName); 81 | } 82 | } 83 | 84 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); 85 | } 86 | 87 | private async Task EmitRoomCreated(string roomName) 88 | { 89 | await Clients.Others.SendAsync("room created", roomName); 90 | } 91 | 92 | private async Task EmitJoined(string roomName, string clientType) 93 | { 94 | await Clients.Clients(GetOppositeTypeHubClients(roomName, clientType)).SendAsync("joined", Context.ConnectionId); 95 | } 96 | 97 | private async Task EmitFull() 98 | { 99 | await Clients.Caller.SendAsync("full"); 100 | } 101 | 102 | private async Task EmitLog(string message, string roomName) 103 | { 104 | await Clients.Group(roomName).SendAsync("log", "[Server]: " + message); 105 | } 106 | 107 | private bool IsClientInRoom(string roomName) 108 | { 109 | return ConnectedClients.ContainsKey(roomName) && ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault() != null; 110 | } 111 | 112 | private List GetOppositeTypeHubClients(string roomName, string clientType) 113 | { 114 | return ConnectedClients[roomName]?.Where(c => c.Type == HubClientType.AllTypes.FirstOrDefault(t => t != clientType))?.Select(c => c.Id).ToList(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/MeshSignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SignalingServer.Hubs 8 | { 9 | [Authorize] 10 | public class MeshSignalingHub : Hub 11 | { 12 | private static readonly Dictionary> ConnectedClients = new(); 13 | 14 | public async Task SendMessage(object message, string roomName, string receiver = null) 15 | { 16 | if (IsClientInRoom(roomName)) 17 | { 18 | if (receiver == null) 19 | { 20 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the whole room: " + message, roomName); 21 | await Clients.OthersInGroup(roomName).SendAsync("message", message, Context.ConnectionId); 22 | } 23 | else 24 | { 25 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the client " + receiver + ": " + message, roomName); 26 | await Clients.Client(receiver).SendAsync("message", message, Context.ConnectionId); 27 | } 28 | } 29 | } 30 | 31 | public async Task> CreateOrJoinRoom(string roomName) 32 | { 33 | await EmitLog("Received request to create or join room " + roomName + " from a client " + Context.ConnectionId, roomName); 34 | 35 | if (!ConnectedClients.ContainsKey(roomName)) 36 | { 37 | ConnectedClients.Add(roomName, new List()); 38 | } 39 | 40 | if (!IsClientInRoom(roomName)) 41 | { 42 | ConnectedClients[roomName].Add(Context.ConnectionId); 43 | } 44 | 45 | await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 46 | 47 | await EmitJoined(roomName); 48 | await EmitLog("Client " + Context.ConnectionId + " joined the room " + roomName, roomName); 49 | 50 | var numberOfClients = ConnectedClients[roomName].Count; 51 | await EmitLog("Room " + roomName + " now has " + numberOfClients + " client(s)", roomName); 52 | 53 | var othersInRoom = ConnectedClients[roomName].Where(c => !c.Equals(Context.ConnectionId)).ToList(); 54 | return othersInRoom; 55 | } 56 | 57 | public async Task LeaveRoom(string roomName) 58 | { 59 | await EmitLog("Received request to leave the room " + roomName + " from a client " + Context.ConnectionId, roomName); 60 | 61 | if (IsClientInRoom(roomName)) 62 | { 63 | ConnectedClients[roomName].Remove(Context.ConnectionId); 64 | await EmitLog("Client " + Context.ConnectionId + " left the room " + roomName, roomName); 65 | 66 | if (ConnectedClients[roomName].Count == 0) 67 | { 68 | ConnectedClients.Remove(roomName); 69 | await EmitLog("Room " + roomName + " is now empty - resetting its state", roomName); 70 | } 71 | } 72 | 73 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); 74 | } 75 | 76 | private async Task EmitJoined(string roomName) 77 | { 78 | await Clients.OthersInGroup(roomName).SendAsync("joined", Context.ConnectionId); 79 | } 80 | 81 | private async Task EmitLog(string message, string roomName) 82 | { 83 | await Clients.Group(roomName).SendAsync("log", "[Server]: " + message); 84 | } 85 | 86 | private bool IsClientInRoom(string roomName) 87 | { 88 | return ConnectedClients.ContainsKey(roomName) && ConnectedClients[roomName].Contains(Context.ConnectionId); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/SFUSignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SignalingServer.Hubs 8 | { 9 | [Authorize] 10 | public class SFUSignalingHub : Hub 11 | { 12 | private static readonly Dictionary> ConnectedClients = new(); 13 | 14 | public async Task SendMessage(object message, string roomName, string receiver = null) 15 | { 16 | if (IsClientInRoom(roomName)) 17 | { 18 | if (receiver == null) 19 | { 20 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the whole room: " + message, roomName); 21 | await Clients.OthersInGroup(roomName).SendAsync("message", message, Context.ConnectionId); 22 | } 23 | else 24 | { 25 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the client " + receiver + ": " + message, roomName); 26 | await Clients.Client(receiver).SendAsync("message", message, Context.ConnectionId); 27 | } 28 | } 29 | } 30 | 31 | public async Task> CreateOrJoinRoom(string roomName, string clientType) 32 | { 33 | await EmitLog("Received request to create or join room " + roomName + " from a client " + Context.ConnectionId + " of type " + clientType, roomName); 34 | 35 | if (clientType == HubClientType.CentralUnit && ConnectedClients.ContainsKey(roomName) 36 | && ConnectedClients[roomName].Where(c => c.Type == HubClientType.CentralUnit).FirstOrDefault() != null) 37 | { 38 | await EmitFull(); 39 | return null; 40 | } 41 | 42 | if (!ConnectedClients.ContainsKey(roomName)) 43 | { 44 | ConnectedClients.Add(roomName, new List()); 45 | } 46 | 47 | if (!IsClientInRoom(roomName)) 48 | { 49 | ConnectedClients[roomName].Add(new HubClient(Context.ConnectionId, clientType)); 50 | } 51 | 52 | await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 53 | 54 | await EmitJoined(roomName, clientType); 55 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientType + " joined the room " + roomName, roomName); 56 | 57 | var numberOfClients = ConnectedClients[roomName].Count; 58 | await EmitLog("Room " + roomName + " now has " + numberOfClients + " client(s)", roomName); 59 | if (numberOfClients == 1) 60 | { 61 | await EmitRoomCreated(roomName); 62 | } 63 | 64 | return GetOppositeTypeHubClients(roomName, clientType); 65 | } 66 | 67 | public async Task LeaveRoom(string roomName) 68 | { 69 | await EmitLog("Received request to leave the room " + roomName + " from a client " + Context.ConnectionId, roomName); 70 | 71 | if (IsClientInRoom(roomName)) 72 | { 73 | var clientToRemove = ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault(); 74 | ConnectedClients[roomName].Remove(clientToRemove); 75 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientToRemove.Type + " left the room " + roomName, roomName); 76 | 77 | if (ConnectedClients[roomName].Count == 0) 78 | { 79 | ConnectedClients.Remove(roomName); 80 | await EmitLog("Room " + roomName + " is now empty - resetting its state", roomName); 81 | } 82 | } 83 | 84 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); 85 | } 86 | 87 | private async Task EmitRoomCreated(string roomName) 88 | { 89 | await Clients.Others.SendAsync("room created", roomName); 90 | } 91 | 92 | private async Task EmitJoined(string roomName, string clientType) 93 | { 94 | await Clients.Clients(GetOppositeTypeHubClients(roomName, clientType)).SendAsync("joined", Context.ConnectionId); 95 | } 96 | 97 | private async Task EmitFull() 98 | { 99 | await Clients.Caller.SendAsync("full"); 100 | } 101 | 102 | private async Task EmitLog(string message, string roomName) 103 | { 104 | await Clients.Group(roomName).SendAsync("log", "[Server]: " + message); 105 | } 106 | 107 | private bool IsClientInRoom(string roomName) 108 | { 109 | return ConnectedClients.ContainsKey(roomName) && ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault() != null; 110 | } 111 | 112 | private List GetOppositeTypeHubClients(string roomName, string clientType) 113 | { 114 | return ConnectedClients[roomName]?.Where(c => c.Type == HubClientType.AllTypes.FirstOrDefault(t => t != clientType))?.Select(c => c.Id).ToList(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/SignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace SignalingServer.Hubs 7 | { 8 | [Authorize] 9 | public class SignalingHub : Hub 10 | { 11 | public static Dictionary> ConnectedClients = new Dictionary>(); 12 | 13 | public async Task SendMessage(object message, string roomName) 14 | { 15 | await EmitLog("Client " + Context.ConnectionId + " said: " + message, roomName); 16 | 17 | await Clients.OthersInGroup(roomName).SendAsync("message", message); 18 | } 19 | 20 | public async Task CreateOrJoinRoom(string roomName) 21 | { 22 | await EmitLog("Received request to create or join room " + roomName + " from a client " + Context.ConnectionId, roomName); 23 | 24 | if (!ConnectedClients.ContainsKey(roomName)) 25 | { 26 | ConnectedClients.Add(roomName, new List()); 27 | } 28 | 29 | if (!ConnectedClients[roomName].Contains(Context.ConnectionId)) 30 | { 31 | ConnectedClients[roomName].Add(Context.ConnectionId); 32 | } 33 | 34 | await EmitJoinRoom(roomName); 35 | 36 | var numberOfClients = ConnectedClients[roomName].Count; 37 | 38 | if (numberOfClients == 1) 39 | { 40 | await EmitCreated(); 41 | await EmitLog("Client "+ Context.ConnectionId + " created the room " + roomName, roomName); 42 | } 43 | else 44 | { 45 | await EmitJoined(roomName); 46 | await EmitLog("Client " + Context.ConnectionId + " joined the room " + roomName, roomName); 47 | } 48 | 49 | await EmitLog("Room " + roomName + " now has " + numberOfClients + " client(s)", roomName); 50 | } 51 | 52 | public async Task LeaveRoom(string roomName) 53 | { 54 | await EmitLog("Received request to leave the room " + roomName + " from a client " + Context.ConnectionId, roomName); 55 | 56 | if (ConnectedClients.ContainsKey(roomName) && ConnectedClients[roomName].Contains(Context.ConnectionId)) 57 | { 58 | ConnectedClients[roomName].Remove(Context.ConnectionId); 59 | await EmitLog("Client " + Context.ConnectionId + " left the room " + roomName, roomName); 60 | 61 | if (ConnectedClients[roomName].Count == 0) 62 | { 63 | ConnectedClients.Remove(roomName); 64 | await EmitLog("Room " + roomName + " is now empty - resetting its state", roomName); 65 | } 66 | } 67 | 68 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); 69 | } 70 | 71 | private async Task EmitJoinRoom(string roomName) 72 | { 73 | await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 74 | } 75 | 76 | private async Task EmitCreated() 77 | { 78 | await Clients.Caller.SendAsync("created"); 79 | } 80 | 81 | private async Task EmitJoined(string roomName) 82 | { 83 | await Clients.Group(roomName).SendAsync("joined"); 84 | } 85 | 86 | private async Task EmitLog(string message, string roomName) 87 | { 88 | await Clients.Group(roomName).SendAsync("log", "[Server]: " + message); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Hubs/StarSignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SignalingServer.Hubs 8 | { 9 | public class StarSignalingHub : Hub 10 | { 11 | private static readonly Dictionary> ConnectedClients = new(); 12 | 13 | public async Task SendMessage(object message, string roomName, string receiver = null) 14 | { 15 | if (IsClientInRoom(roomName)) 16 | { 17 | if (receiver == null) 18 | { 19 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the whole room: " + message, roomName); 20 | await Clients.OthersInGroup(roomName).SendAsync("message", message, Context.ConnectionId); 21 | } 22 | else 23 | { 24 | await EmitLog("Client " + Context.ConnectionId + " sent a message to the client " + receiver + ": " + message, roomName); 25 | await Clients.Client(receiver).SendAsync("message", message, Context.ConnectionId); 26 | } 27 | } 28 | } 29 | 30 | public async Task> CreateOrJoinRoom(string roomName, string clientType) 31 | { 32 | await EmitLog("Received request to create or join room " + roomName + " from a client " + Context.ConnectionId + " of type " + clientType, roomName); 33 | 34 | if (clientType == HubClientType.CentralUnit && ConnectedClients.ContainsKey(roomName) 35 | && ConnectedClients[roomName].Where(c => c.Type == HubClientType.CentralUnit).FirstOrDefault() != null) 36 | { 37 | await EmitFull(); 38 | return null; 39 | } 40 | 41 | if (!ConnectedClients.ContainsKey(roomName)) 42 | { 43 | ConnectedClients.Add(roomName, new List()); 44 | } 45 | 46 | if (!IsClientInRoom(roomName)) 47 | { 48 | ConnectedClients[roomName].Add(new HubClient(Context.ConnectionId, clientType)); 49 | } 50 | 51 | await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 52 | 53 | await EmitJoined(roomName, clientType); 54 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientType + " joined the room " + roomName, roomName); 55 | 56 | var numberOfClients = ConnectedClients[roomName].Count; 57 | await EmitLog("Room " + roomName + " now has " + numberOfClients + " client(s)", roomName); 58 | 59 | return GetOppositeTypeHubClients(roomName, clientType); 60 | } 61 | 62 | public async Task LeaveRoom(string roomName) 63 | { 64 | await EmitLog("Received request to leave the room " + roomName + " from a client " + Context.ConnectionId, roomName); 65 | 66 | if (IsClientInRoom(roomName)) 67 | { 68 | var clientToRemove = ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault(); 69 | ConnectedClients[roomName].Remove(clientToRemove); 70 | await EmitLog("Client " + Context.ConnectionId + " of type " + clientToRemove.Type + " left the room " + roomName, roomName); 71 | 72 | if (ConnectedClients[roomName].Count == 0) 73 | { 74 | ConnectedClients.Remove(roomName); 75 | await EmitLog("Room " + roomName + " is now empty - resetting its state", roomName); 76 | } 77 | } 78 | 79 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); 80 | } 81 | 82 | private async Task EmitJoined(string roomName, string clientType) 83 | { 84 | await Clients.Clients(GetOppositeTypeHubClients(roomName, clientType)).SendAsync("joined", Context.ConnectionId); 85 | } 86 | 87 | private async Task EmitFull() 88 | { 89 | await Clients.Caller.SendAsync("full"); 90 | } 91 | 92 | private async Task EmitLog(string message, string roomName) 93 | { 94 | await Clients.Group(roomName).SendAsync("log", "[Server]: " + message); 95 | } 96 | 97 | private bool IsClientInRoom(string roomName) 98 | { 99 | return ConnectedClients.ContainsKey(roomName) && ConnectedClients[roomName].Where(c => c.Id == Context.ConnectionId).FirstOrDefault() != null; 100 | } 101 | 102 | private List GetOppositeTypeHubClients(string roomName, string clientType) 103 | { 104 | return ConnectedClients[roomName]?.Where(c => c.Type == HubClientType.AllTypes.FirstOrDefault(t => t != clientType))?.Select(c => c.Id).ToList(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace SignalingServer 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => 17 | { 18 | webBuilder.UseStartup(); 19 | }) 20 | .ConfigureLogging(logging => 21 | { 22 | logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug); 23 | logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "IIS Express": { 4 | "commandName": "IISExpress", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | }, 10 | "SignalingServer": { 11 | "commandName": "Project", 12 | "launchBrowser": true, 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | }, 16 | "applicationUrl": "http://localhost:5000" 17 | } 18 | }, 19 | "$schema": "http://json.schemastore.org/launchsettings.json", 20 | "iisSettings": { 21 | "windowsAuthentication": false, 22 | "anonymousAuthentication": true, 23 | "iisExpress": { 24 | "applicationUrl": "http://localhost:5000", 25 | "sslPort": 0 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/SignalingServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.IdentityModel.Tokens; 8 | using SignalingServer.Hubs; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace SignalingServer 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | services.AddCors(o => o.AddPolicy("default", builder => 27 | { 28 | builder.WithOrigins("http://localhost:4200") 29 | .SetIsOriginAllowedToAllowWildcardSubdomains() 30 | .AllowAnyMethod() 31 | .AllowAnyHeader() 32 | .AllowCredentials(); 33 | })); 34 | 35 | services.AddAuthentication(options => 36 | { 37 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 38 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 39 | }) 40 | .AddJwtBearer(options => 41 | { 42 | options.TokenValidationParameters = new TokenValidationParameters() 43 | { 44 | ValidateIssuer = false, 45 | ValidateAudience = false, 46 | ValidateIssuerSigningKey = true, 47 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenHelper.SECRET)) 48 | }; 49 | options.Events = new JwtBearerEvents 50 | { 51 | OnMessageReceived = context => 52 | { 53 | if ((context.Request.Path.Value.StartsWith("/hubs/signaling") 54 | || context.Request.Path.Value.StartsWith("/hubs/mesh-signaling") 55 | || context.Request.Path.Value.StartsWith("/hubs/star-signaling") 56 | || context.Request.Path.Value.StartsWith("/hubs/sfu-signaling") 57 | || context.Request.Path.Value.StartsWith("/hubs/mcu-signaling")) 58 | && (context.Request.Query.ContainsKey("access_token"))) 59 | { 60 | context.Token = context.Request.Query["access_token"]; 61 | } 62 | 63 | return Task.CompletedTask; 64 | }, 65 | }; 66 | }); 67 | 68 | services.AddSignalR(o => 69 | { 70 | o.EnableDetailedErrors = true; 71 | o.MaximumReceiveMessageSize = 1000000; 72 | }); 73 | } 74 | 75 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 76 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 77 | { 78 | if (env.IsDevelopment()) 79 | { 80 | app.UseDeveloperExceptionPage(); 81 | } 82 | 83 | app.UseCors("default"); 84 | 85 | app.UseDefaultFiles(); 86 | 87 | app.UseStaticFiles(); 88 | 89 | app.UseRouting(); 90 | 91 | app.UseAuthentication(); 92 | 93 | app.UseAuthorization(); 94 | 95 | app.UseEndpoints(endpoints => 96 | { 97 | endpoints.MapHub("/hubs/auth"); 98 | endpoints.MapHub("/hubs/signaling"); 99 | endpoints.MapHub("/hubs/mesh-signaling"); 100 | endpoints.MapHub("/hubs/star-signaling"); 101 | endpoints.MapHub("/hubs/sfu-signaling"); 102 | endpoints.MapHub("/hubs/mcu-signaling"); 103 | }); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/TokenHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Text; 5 | 6 | namespace SignalingServer 7 | { 8 | public static class TokenHelper 9 | { 10 | public static string SECRET = "secret_signing_key_with_the_key_size_of_at_least_256_bits"; 11 | 12 | public static string GenerateToken() 13 | { 14 | var tokenHandler = new JwtSecurityTokenHandler(); 15 | var key = Encoding.ASCII.GetBytes(SECRET); 16 | var tokenDescriptor = new SecurityTokenDescriptor 17 | { 18 | Expires = DateTime.UtcNow.AddDays(7), 19 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) 20 | }; 21 | var token = tokenHandler.CreateToken(tokenDescriptor); 22 | return tokenHandler.WriteToken(token); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /SignalingServer/SignalingServer/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Signaling Server 6 | 7 | 8 | The Signaling Server is running. 9 | 10 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.17.0 5 | JavaOnly 6 | true 7 | 15 | 16 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 116 | 117 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/dictionaries/Dalibor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | anonymization 5 | anonymizer 6 | capturer 7 | gson 8 | pixelation 9 | signalr 10 | smartglass 11 | 12 | 13 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.example.webrtcandroidapp" 10 | minSdkVersion 24 11 | targetSdkVersion 33 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | vectorDrawables.useSupportLibrary = true 17 | multiDexEnabled true 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | buildConfigField("String", "SIGNALING_SERVER_URL", '"http://localhost:5000/hubs"') 24 | buildConfigField("String", "STUN_SERVER_1", '"stun:stun.1.google.com:19302"') 25 | buildConfigField("String", "STUN_SERVER_2", '"stun:stun1.l.google.com:19302"') 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | manifestPlaceholders.networkSecurityConfig = '@xml/network_security_config' 28 | } 29 | debug { 30 | minifyEnabled false 31 | buildConfigField("String", "SIGNALING_SERVER_URL", '"http://localhost:5000/hubs"') 32 | buildConfigField("String", "STUN_SERVER_1", '"stun:stun.1.google.com:19302"') 33 | buildConfigField("String", "STUN_SERVER_2", '"stun:stun1.l.google.com:19302"') 34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 35 | manifestPlaceholders.networkSecurityConfig = '@xml/network_security_config' 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_17 40 | targetCompatibility JavaVersion.VERSION_17 41 | } 42 | namespace 'com.example.webrtcandroidapp' 43 | } 44 | 45 | dependencies { 46 | implementation fileTree(dir: 'libs', include: ['*.jar']) 47 | api project(':local-sdk') 48 | implementation 'androidx.appcompat:appcompat:1.5.1' 49 | implementation 'com.android.support:multidex:1.0.3' 50 | implementation 'com.google.android.material:material:1.6.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 55 | // Gson 56 | implementation 'com.google.code.gson:gson:2.8.6' 57 | // SignalR 58 | implementation 'com.microsoft.signalr:signalr:3.1.22' 59 | // MediaPipe 60 | def mediaPipeSdkVersion = '0.10.15' 61 | implementation "com.google.mediapipe:solution-core:${mediaPipeSdkVersion}" 62 | implementation "com.google.mediapipe:facedetection:${mediaPipeSdkVersion}" 63 | } -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/androidTest/java/com/example/webrtcandroidapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.example.webrtcandroidapp", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.core.app.ActivityCompat; 5 | 6 | import android.Manifest; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.SharedPreferences; 10 | import android.content.pm.PackageManager; 11 | import android.os.Bundle; 12 | import android.view.View; 13 | import android.widget.EditText; 14 | import android.widget.Toast; 15 | 16 | import com.example.webrtcandroidapp.services.SignalrService; 17 | 18 | import io.reactivex.disposables.Disposable; 19 | 20 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 21 | 22 | private final String[] mPermissions = { 23 | Manifest.permission.CAMERA, 24 | Manifest.permission.RECORD_AUDIO 25 | }; 26 | 27 | private SignalrService mSignalrService; 28 | private Disposable mTokenDisposable; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(WebRTCAndroidApp.USE_SMARTGLASS_OPTIMIZATION 34 | ? R.layout.activity_main_smartglass : R.layout.activity_main); 35 | findViewById(R.id.btn_start).setOnClickListener(this); 36 | mSignalrService = new SignalrService( "/auth", false); 37 | 38 | if (!hasPermissions(this, mPermissions)) { 39 | requestPermissions(); 40 | } 41 | } 42 | 43 | @Override 44 | protected void onStart() { 45 | super.onStart(); 46 | getAccessToken(); 47 | } 48 | 49 | @Override 50 | protected void onDestroy() { 51 | mSignalrService.dispose(); 52 | mTokenDisposable.dispose(); 53 | super.onDestroy(); 54 | } 55 | 56 | @Override 57 | public void onClick(View v) { 58 | if (v.getId() == R.id.btn_start) { 59 | String roomName = ((EditText) findViewById(R.id.et_room)).getText().toString(); 60 | 61 | if (roomName.length() < 2) { 62 | Toast.makeText(MainActivity.this, getString(R.string.error_room_name_length), Toast.LENGTH_SHORT).show(); 63 | return; 64 | } 65 | 66 | Intent intent = new Intent(this, SessionCallActivity.class); 67 | Bundle dataBundle = new Bundle(); 68 | dataBundle.putString("ROOM_NAME", roomName); 69 | intent.putExtras(dataBundle); 70 | startActivity(intent); 71 | } 72 | } 73 | 74 | @Override 75 | public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { 76 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 77 | 78 | if (!hasPermissions(this, mPermissions)) { 79 | this.finish(); 80 | } 81 | } 82 | 83 | private void requestPermissions(){ 84 | int PERMISSION_ALL = 1; 85 | 86 | if (!hasPermissions(this, mPermissions)) { 87 | ActivityCompat.requestPermissions(this, mPermissions, PERMISSION_ALL); 88 | } 89 | } 90 | 91 | private static boolean hasPermissions(Context context, String... permissions) { 92 | if (context != null && permissions != null) { 93 | for (String permission : permissions) { 94 | if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { 95 | return false; 96 | } 97 | } 98 | } 99 | return true; 100 | } 101 | 102 | private void getAccessToken() { 103 | mSignalrService.connect(() -> { 104 | if (mSignalrService.isConnected()) { 105 | mTokenDisposable = mSignalrService.invoke(String.class, "Authorize").subscribe(token -> { 106 | if (token != null) { 107 | SharedPreferences.Editor editor = getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE).edit(); 108 | editor.putString("TOKEN", token); 109 | editor.apply(); 110 | } 111 | }); 112 | } 113 | }); 114 | } 115 | } -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/WebRTCAndroidApp.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp; 2 | 3 | import android.app.Application; 4 | 5 | public class WebRTCAndroidApp extends Application { 6 | 7 | // Set this variable to "true" in case smart glasses' display optimization is needed 8 | public static final boolean USE_SMARTGLASS_OPTIMIZATION = false; 9 | 10 | public static WebRTCAndroidApp mInstance; 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | mInstance = this; 16 | } 17 | 18 | public static WebRTCAndroidApp get() { return mInstance; } 19 | } 20 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/observers/CustomPeerConnectionObserver.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp.observers; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.DataChannel; 6 | import org.webrtc.IceCandidate; 7 | import org.webrtc.MediaStream; 8 | import org.webrtc.PeerConnection; 9 | import org.webrtc.RtpReceiver; 10 | import org.webrtc.RtpTransceiver; 11 | 12 | public class CustomPeerConnectionObserver implements PeerConnection.Observer { 13 | 14 | private static final String TAG = "PeerConnectionObserver"; 15 | 16 | @Override 17 | public void onSignalingChange(PeerConnection.SignalingState signalingState) { 18 | Log.d(TAG, "onSignalingChange: " + signalingState); 19 | } 20 | 21 | @Override 22 | public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { 23 | Log.d(TAG, "onIceConnectionChange: " + iceConnectionState); 24 | } 25 | 26 | @Override 27 | public void onIceConnectionReceivingChange(boolean b) { 28 | Log.d(TAG, "onIceConnectionReceivingChange: " + b); 29 | } 30 | 31 | @Override 32 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 33 | Log.d(TAG, "onIceGatheringChange: " + iceGatheringState); 34 | } 35 | 36 | @Override 37 | public void onIceCandidate(IceCandidate iceCandidate) { 38 | Log.d(TAG, "onIceCandidate: " + iceCandidate); 39 | } 40 | 41 | @Override 42 | public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { 43 | Log.d(TAG, "onIceCandidatesRemoved: " + iceCandidates); 44 | } 45 | 46 | @Override 47 | public void onAddStream(MediaStream mediaStream) { 48 | Log.d(TAG, "onAddStream: " + mediaStream); 49 | } 50 | 51 | @Override 52 | public void onRemoveStream(MediaStream mediaStream) { 53 | Log.d(TAG, "onRemoveStream: " + mediaStream); 54 | } 55 | 56 | @Override 57 | public void onDataChannel(DataChannel dataChannel) { 58 | Log.d(TAG, "onDataChannel: " + dataChannel); 59 | } 60 | 61 | @Override 62 | public void onRenegotiationNeeded() { 63 | Log.d(TAG, "onRenegotiationNeeded"); 64 | } 65 | 66 | @Override 67 | public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { 68 | Log.d(TAG, "onAddTrack: " + mediaStreams); 69 | } 70 | 71 | @Override 72 | public void onConnectionChange(PeerConnection.PeerConnectionState newState) { 73 | Log.d(TAG, "onConnectionChange: " + newState); 74 | } 75 | 76 | @Override 77 | public void onTrack(RtpTransceiver transceiver) { 78 | Log.d(TAG, "onTrack: " + transceiver); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/observers/CustomSdpObserver.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp.observers; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.SdpObserver; 6 | import org.webrtc.SessionDescription; 7 | 8 | public class CustomSdpObserver implements SdpObserver { 9 | 10 | private static final String TAG = "SdpObserver"; 11 | 12 | @Override 13 | public void onCreateSuccess(SessionDescription sessionDescription) { 14 | Log.d(TAG, "onCreateSuccess"); 15 | } 16 | 17 | @Override 18 | public void onSetSuccess() { 19 | Log.d(TAG, "onSetSuccess"); 20 | } 21 | 22 | @Override 23 | public void onCreateFailure(String s) { 24 | Log.d(TAG, "onCreateFailure"); 25 | } 26 | 27 | @Override 28 | public void onSetFailure(String s) { 29 | Log.d(TAG, "onSetFailure"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/services/IceServerService.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp.services; 2 | 3 | import com.example.webrtcandroidapp.BuildConfig; 4 | 5 | import org.webrtc.PeerConnection; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class IceServerService { 11 | 12 | private static final String[] ICE_SERVERS = new String[] { BuildConfig.STUN_SERVER_1, BuildConfig.STUN_SERVER_2 }; 13 | 14 | public static List getIceServers() { 15 | List iceServers = new ArrayList<>(); 16 | 17 | for (String iceServer : ICE_SERVERS) { 18 | PeerConnection.IceServer peerIceServer = PeerConnection.IceServer.builder(iceServer).createIceServer(); 19 | iceServers.add(peerIceServer); 20 | } 21 | 22 | return iceServers; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/java/com/example/webrtcandroidapp/services/SignalrService.java: -------------------------------------------------------------------------------- 1 | package com.example.webrtcandroidapp.services; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.example.webrtcandroidapp.BuildConfig; 7 | import com.example.webrtcandroidapp.WebRTCAndroidApp; 8 | import com.microsoft.signalr.Action; 9 | import com.microsoft.signalr.Action1; 10 | import com.microsoft.signalr.HubConnection; 11 | import com.microsoft.signalr.HubConnectionBuilder; 12 | import com.microsoft.signalr.HubConnectionState; 13 | 14 | import io.reactivex.Single; 15 | 16 | import static io.reactivex.internal.functions.Functions.emptyConsumer; 17 | 18 | public class SignalrService { 19 | 20 | public interface SignalrConnectionListener { 21 | void onConnected(); 22 | } 23 | 24 | private static final String TAG = "SignalrService"; 25 | 26 | private HubConnection mHubConnection; 27 | 28 | public SignalrService(String path, boolean withToken) { 29 | Log.d(TAG, "SignalrService; path: " + path); 30 | 31 | try { 32 | String hubUrl = BuildConfig.SIGNALING_SERVER_URL + path; 33 | if(withToken) { 34 | mHubConnection = HubConnectionBuilder.create(hubUrl) 35 | .withAccessTokenProvider(Single.defer(() -> { 36 | String token = WebRTCAndroidApp.get() 37 | .getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE) 38 | .getString("TOKEN", null); 39 | return (token != null) ? Single.just(token) : Single.just(""); 40 | })).build(); 41 | } else { 42 | mHubConnection = HubConnectionBuilder.create(hubUrl).build(); 43 | } 44 | } catch (Exception e) { 45 | Log.e(TAG, "SignalrService: Failed. [Error]: ", e); 46 | } 47 | } 48 | 49 | public void connect(SignalrConnectionListener listener) { 50 | Log.d(TAG, "connect"); 51 | 52 | try { 53 | mHubConnection.start().doOnComplete(() -> { 54 | if(listener != null) { 55 | listener.onConnected(); 56 | } 57 | }).doOnError(emptyConsumer()).blockingAwait(); 58 | } catch (Exception e) { 59 | Log.e(TAG, "connect: Failed. [Error]: ", e); 60 | } 61 | } 62 | 63 | public void define(String methodName, Action callback) { 64 | Log.d(TAG, "define; methodName: " + methodName); 65 | 66 | if (mHubConnection != null) { 67 | try { 68 | mHubConnection.on(methodName, callback); 69 | } catch (Exception e) { 70 | Log.e(TAG, "define: Failed. [Connection]: " + (mHubConnection != null ? mHubConnection.getConnectionState() : null)); 71 | } 72 | } else { 73 | Log.e(TAG, "define: Failed. - mHubConnection is null."); 74 | } 75 | } 76 | 77 | public void define(String methodName, Action1 callback, Class param) { 78 | Log.d(TAG, "define; methodName: " + methodName); 79 | 80 | if (mHubConnection != null) { 81 | try { 82 | mHubConnection.on(methodName, callback, param); 83 | } catch (Exception e) { 84 | Log.e(TAG, "define: Failed. [Connection]: " + (mHubConnection != null ? mHubConnection.getConnectionState() : null)); 85 | } 86 | } else { 87 | Log.e(TAG, "define: Failed. - mHubConnection is null."); 88 | } 89 | } 90 | 91 | public void invoke(String methodName, Object... args) { 92 | Log.d(TAG, "invoke; methodName: " + methodName); 93 | 94 | if (isConnected()) { 95 | try { 96 | mHubConnection.invoke(methodName, args); 97 | } catch (Exception e) { 98 | Log.e(TAG, "invoke: Failed. [Error]: ", e); 99 | } 100 | } else { 101 | Log.e(TAG, "invoke: Failed. [Connection]: " + (mHubConnection != null ? mHubConnection.getConnectionState() : null)); 102 | } 103 | } 104 | 105 | public Single invoke(Class returnType, String methodName, Object... args) { 106 | Log.d(TAG, "invoke; methodName: " + methodName); 107 | 108 | if (isConnected()) { 109 | try { 110 | return mHubConnection.invoke(returnType, methodName, args); 111 | } catch (Exception e) { 112 | Log.e(TAG, "invoke: Failed. [Error]: ", e); 113 | } 114 | } else { 115 | Log.e(TAG, "invoke: Failed. [Connection]: " + (mHubConnection != null ? mHubConnection.getConnectionState() : null)); 116 | } 117 | 118 | return Single.never(); 119 | } 120 | 121 | public void disconnect() { 122 | Log.d(TAG, "disconnect"); 123 | 124 | try { 125 | mHubConnection.stop(); 126 | } catch (Exception e) { 127 | Log.e(TAG, "disconnect: Failed. [Connection]: " + (mHubConnection != null ? mHubConnection.getConnectionState() : null)); 128 | } 129 | } 130 | 131 | public void dispose() { 132 | Log.d(TAG, "dispose"); 133 | 134 | disconnect(); 135 | mHubConnection = null; 136 | } 137 | 138 | public boolean isConnected() { 139 | return mHubConnection != null && mHubConnection.getConnectionState() == HubConnectionState.CONNECTED; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /WebRTCAndroidApp/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 26 | 27 | 30 | 31 | 40 | 41 | 42 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: grid; 3 | text-align: center; 4 | align-content: center; 5 | margin: 10px; 6 | } 7 | 8 | .send-button { 9 | width: 280px; 10 | height: 50px; 11 | text-transform: uppercase; 12 | font-weight: bold; 13 | margin-top: 15px; 14 | } 15 | 16 | .input-field { 17 | width: 350px; 18 | text-align: center; 19 | } 20 | 21 | .main-text { 22 | font-size: 20px; 23 | } 24 | 25 | .form-field-select { 26 | width: 350px; 27 | text-align: center; 28 | } 29 | 30 | .form-field-div { 31 | margin: 5px; 32 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { WebRTCClientType } from 'src/app/models/webrtc-client-type'; 4 | import { SignalrService } from 'src/app/services/signalr.service'; 5 | 6 | @Component({ 7 | selector: 'app-home', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.scss'] 10 | }) 11 | export class HomeComponent implements OnInit { 12 | 13 | room: string; 14 | mode = 'Peer-to-Peer'; 15 | clientType: WebRTCClientType = WebRTCClientType.CentralUnit; 16 | 17 | modes: string[] = ['Peer-to-Peer', 'Mesh Conference Call', 'Star Conference Call', 'SFU Conference Call', 'MCU Conference Call']; 18 | clientTypes: WebRTCClientType[] = [WebRTCClientType.CentralUnit, WebRTCClientType.SideUnit]; 19 | 20 | constructor( 21 | private router: Router, 22 | private signaling: SignalrService 23 | ) { } 24 | 25 | ngOnInit(): void { 26 | this.signaling.connect('/auth', false).then(() => { 27 | if (this.signaling.isConnected()) { 28 | this.signaling.invoke('Authorize').then((token: string) => { 29 | if (token) { 30 | sessionStorage.setItem('token', token); 31 | } 32 | }); 33 | } 34 | }); 35 | } 36 | 37 | startSessionCall(): void { 38 | switch (this.mode) { 39 | case 'Peer-to-Peer': 40 | this.router.navigate(['session-call/' + this.room]); 41 | break; 42 | case 'Mesh Conference Call': 43 | this.router.navigate(['session-call/mesh/' + this.room]); 44 | break; 45 | case 'Star Conference Call': 46 | this.router.navigate(['session-call/star/' + this.room + '/' + this.clientType]); 47 | break; 48 | case 'SFU Conference Call': 49 | this.router.navigate(['session-call/sfu/' + this.room]); 50 | break; 51 | case 'MCU Conference Call': 52 | this.router.navigate(['session-call/mcu/' + this.room]); 53 | break; 54 | default: 55 | break; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mcu/session-call-mcu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC Webapp 4 | 5 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mcu/session-call-mcu.component.scss: -------------------------------------------------------------------------------- 1 | .main-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | .videos { 10 | margin-top: 5px; 11 | text-align: center; 12 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mcu/session-call-mcu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionCallMCUComponent } from './session-call-mcu.component'; 4 | 5 | describe('SessionCallMcuComponent', () => { 6 | let component: SessionCallMCUComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionCallMCUComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionCallMCUComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mcu/session-call-mcu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { WebRTCClient } from 'src/app/models/webrtc-client'; 5 | import { WebRTCClientType } from 'src/app/models/webrtc-client-type'; 6 | import { SignalrService } from 'src/app/services/signalr.service'; 7 | 8 | @Component({ 9 | selector: 'app-session-call-mcu', 10 | templateUrl: './session-call-mcu.component.html', 11 | styleUrls: ['./session-call-mcu.component.scss'] 12 | }) 13 | export class SessionCallMCUComponent implements OnInit, OnDestroy { 14 | 15 | @ViewChild('video') video: ElementRef; 16 | 17 | stream: MediaStream; 18 | room: string; 19 | clients: WebRTCClient[] = []; 20 | clientType = WebRTCClientType.SideUnit; 21 | 22 | constructor( 23 | private snack: MatSnackBar, 24 | private route: ActivatedRoute, 25 | private signaling: SignalrService 26 | ) { 27 | this.route.paramMap.subscribe(async param => { 28 | this.room = param['params']['room']; 29 | }); 30 | } 31 | 32 | ngOnInit(): void { 33 | this.start(); 34 | } 35 | 36 | async start(): Promise { 37 | // #1 connect to signaling server 38 | this.signaling.connect('/mcu-signaling', true).then(async () => { 39 | if (this.signaling.isConnected()) { 40 | const otherClientsInRoom = (await this.signaling.invoke('CreateOrJoinRoom', this.room, this.clientType)) as string[]; 41 | otherClientsInRoom.forEach(client => { 42 | this.tryCreateClient(client, false); 43 | }); 44 | } 45 | }); 46 | 47 | // #2 define signaling communication 48 | this.defineSignaling(); 49 | 50 | // #3 get media from current client 51 | this.getUserMedia(); 52 | } 53 | 54 | defineSignaling(): void { 55 | this.signaling.define('log', (message: any) => { 56 | console.log(message); 57 | }); 58 | 59 | this.signaling.define('joined', (clientId: string) => { 60 | this.tryCreateClient(clientId, true); 61 | }); 62 | 63 | this.signaling.define('message', (message: any, clientId: string) => { 64 | const client = this.findClient(clientId); 65 | if (client) { 66 | if (message === 'got user media') { 67 | client.initiateCall(); 68 | 69 | } else if (message.type === 'offer') { 70 | if (!client.getIsStarted()) { 71 | client.initiateCall(); 72 | } 73 | client.setRemoteDescription(message); 74 | client.sendAnswer(); 75 | 76 | } else if (message.type === 'answer' && client.getIsStarted()) { 77 | client.setRemoteDescription(message); 78 | 79 | } else if (message.type === 'candidate' && client.getIsStarted()) { 80 | client.addIceCandidate(message); 81 | 82 | } else if (message === 'bye' && client.getIsStarted()) { 83 | client.handleRemoteHangup(); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | getUserMedia(): void { 90 | navigator.mediaDevices.getUserMedia({ 91 | audio: true, 92 | video: true 93 | }) 94 | .then((stream: MediaStream) => { 95 | this.addLocalStream(stream); 96 | this.sendMessage('got user media'); 97 | this.clients.forEach(client => { 98 | if (client.getIsInitiator()) { 99 | client.initiateCall(); 100 | } 101 | }); 102 | }) 103 | .catch((e) => { 104 | alert('getUserMedia() error: ' + e.name + ': ' + e.message); 105 | }); 106 | } 107 | 108 | findClient(clientId: string): WebRTCClient { 109 | return this.clients?.filter(c => c.getClientId() === clientId)[0]; 110 | } 111 | 112 | tryCreateClient(clientId: string, initiator: boolean): void { 113 | if (!this.findClient(clientId)) { 114 | const client = new WebRTCClient(clientId, initiator, 115 | (message: any) => this.sendMessage(message, clientId), 116 | (message: string) => this.snack.open(message, 'Dismiss', { duration: 5000 }), 117 | (remoteStream: MediaStream) => this.addRemoteStream(remoteStream), 118 | () => this.removeClient(clientId), 119 | () => this.stream); 120 | 121 | this.clients.push(client); 122 | } 123 | } 124 | 125 | removeClient(clientId: string): void { 126 | const clientToRemove = this.clients.find(c => c.getClientId() === clientId); 127 | this.clients = this.clients.filter(c => c !== clientToRemove); 128 | } 129 | 130 | sendMessage(message: any, client: string = null): void { 131 | this.signaling.invoke('SendMessage', message, this.room, client); 132 | } 133 | 134 | addLocalStream(stream: MediaStream): void { 135 | console.log('Local stream added.'); 136 | this.stream = stream; 137 | this.video.nativeElement.srcObject = this.stream; 138 | } 139 | 140 | addRemoteStream(stream: MediaStream): void { 141 | console.log('Remote stream added.'); 142 | this.stream = stream; 143 | this.video.nativeElement.srcObject = this.stream; 144 | } 145 | 146 | hangup(): void { 147 | console.log('Hanging up.'); 148 | this.clients.forEach(client => { 149 | client.stopPeerConnection(); 150 | }); 151 | this.sendMessage('bye'); 152 | this.signaling.invoke('LeaveRoom', this.room); 153 | setTimeout(() => { 154 | this.signaling.disconnect(); 155 | }, 1000); 156 | } 157 | 158 | ngOnDestroy(): void { 159 | this.hangup(); 160 | if (this.stream && this.stream.active) { 161 | this.stream.getTracks().forEach((track) => { track.stop(); }); 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mesh/session-call-mesh.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC Webapp 4 | 5 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mesh/session-call-mesh.component.scss: -------------------------------------------------------------------------------- 1 | .main-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | .videos { 10 | margin-top: 5px; 11 | text-align: center; 12 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mesh/session-call-mesh.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionCallMeshComponent } from './session-call-mesh.component'; 4 | 5 | describe('SessionCallMeshComponent', () => { 6 | let component: SessionCallMeshComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionCallMeshComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionCallMeshComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-mesh/session-call-mesh.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { WebRTCClient } from 'src/app/models/webrtc-client'; 5 | import { SignalrService } from 'src/app/services/signalr.service'; 6 | 7 | @Component({ 8 | selector: 'app-session-call-mesh', 9 | templateUrl: './session-call-mesh.component.html', 10 | styleUrls: ['./session-call-mesh.component.scss'] 11 | }) 12 | export class SessionCallMeshComponent implements OnInit, OnDestroy { 13 | 14 | @ViewChild('localVideo') localVideo: ElementRef; 15 | 16 | localStream: MediaStream; 17 | room: string; 18 | clients: WebRTCClient[] = []; 19 | 20 | constructor( 21 | private snack: MatSnackBar, 22 | private route: ActivatedRoute, 23 | private signaling: SignalrService 24 | ) { 25 | this.route.paramMap.subscribe(async param => { 26 | this.room = param['params']['room']; 27 | }); 28 | } 29 | 30 | ngOnInit(): void { 31 | this.start(); 32 | } 33 | 34 | async start(): Promise { 35 | // #1 connect to signaling server 36 | this.signaling.connect('/mesh-signaling', true).then(async () => { 37 | if (this.signaling.isConnected()) { 38 | const otherClientsInRoom = (await this.signaling.invoke('CreateOrJoinRoom', this.room)) as string[]; 39 | otherClientsInRoom.forEach(client => { 40 | this.tryCreateClient(client, false); 41 | }); 42 | } 43 | }); 44 | 45 | // #2 define signaling communication 46 | this.defineSignaling(); 47 | 48 | // #3 get media from current client 49 | this.getUserMedia(); 50 | } 51 | 52 | defineSignaling(): void { 53 | this.signaling.define('log', (message: any) => { 54 | console.log(message); 55 | }); 56 | 57 | this.signaling.define('joined', (clientId: string) => { 58 | this.tryCreateClient(clientId, true); 59 | }); 60 | 61 | this.signaling.define('message', (message: any, clientId: string) => { 62 | const client = this.findClient(clientId); 63 | if (client) { 64 | if (message === 'got user media') { 65 | client.initiateCall(); 66 | 67 | } else if (message.type === 'offer') { 68 | if (!client.getIsStarted()) { 69 | client.initiateCall(); 70 | } 71 | client.setRemoteDescription(message); 72 | client.sendAnswer(); 73 | 74 | } else if (message.type === 'answer' && client.getIsStarted()) { 75 | client.setRemoteDescription(message); 76 | 77 | } else if (message.type === 'candidate' && client.getIsStarted()) { 78 | client.addIceCandidate(message); 79 | 80 | } else if (message === 'bye' && client.getIsStarted()) { 81 | client.handleRemoteHangup(); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | getUserMedia(): void { 88 | navigator.mediaDevices.getUserMedia({ 89 | audio: true, 90 | video: true 91 | }) 92 | .then((stream: MediaStream) => { 93 | this.addLocalStream(stream); 94 | this.sendMessage('got user media'); 95 | this.clients.forEach(client => { 96 | if (client.getIsInitiator()) { 97 | client.initiateCall(); 98 | } 99 | }); 100 | }) 101 | .catch((e) => { 102 | alert('getUserMedia() error: ' + e.name + ': ' + e.message); 103 | }); 104 | } 105 | 106 | findClient(clientId: string): WebRTCClient { 107 | return this.clients?.filter(c => c.getClientId() === clientId)[0]; 108 | } 109 | 110 | tryCreateClient(clientId: string, initiator: boolean): void { 111 | if (!this.findClient(clientId)) { 112 | const client = new WebRTCClient(clientId, initiator, 113 | (message: any) => this.sendMessage(message, clientId), 114 | (message: string) => this.snack.open(message, 'Dismiss', { duration: 5000 }), 115 | (remoteStream: MediaStream) => this.addRemoteStream(clientId, remoteStream), 116 | () => this.removeClient(clientId), 117 | () => this.localStream); 118 | 119 | this.clients.push(client); 120 | } 121 | } 122 | 123 | removeClient(clientId: string): void { 124 | this.clients = this.clients.filter(c => c.getClientId() !== clientId); 125 | this.removeRemoteStream(clientId); 126 | } 127 | 128 | sendMessage(message: any, client: string = null): void { 129 | this.signaling.invoke('SendMessage', message, this.room, client); 130 | } 131 | 132 | addLocalStream(stream: MediaStream): void { 133 | console.log('Local stream added.'); 134 | this.localStream = stream; 135 | this.localVideo.nativeElement.srcObject = this.localStream; 136 | this.localVideo.nativeElement.muted = 'muted'; 137 | } 138 | 139 | addRemoteStream(clientId: string, stream: MediaStream): void { 140 | const videoElementId = 'remoteVideo-' + clientId; 141 | this.createVideoElement(videoElementId, stream); 142 | } 143 | 144 | removeRemoteStream(clientId: string): void { 145 | const videoElement = document.getElementById('remoteVideo-' + clientId); 146 | if (videoElement) { 147 | videoElement.parentNode.removeChild(videoElement); 148 | } 149 | } 150 | 151 | createVideoElement(id: string, stream: MediaStream): void { 152 | if (!document.getElementById(id)) { 153 | const videosDiv = document.getElementById('all-videos'); 154 | const remoteVideo = document.createElement('video'); 155 | remoteVideo.id = id; 156 | remoteVideo.srcObject = stream; 157 | remoteVideo.autoplay = true; 158 | remoteVideo.controls = true; 159 | remoteVideo.muted = true; 160 | videosDiv.appendChild(remoteVideo); 161 | } 162 | } 163 | 164 | hangup(): void { 165 | console.log('Hanging up.'); 166 | this.clients.forEach(client => { 167 | client.stopPeerConnection(); 168 | }); 169 | this.sendMessage('bye'); 170 | this.signaling.invoke('LeaveRoom', this.room); 171 | setTimeout(() => { 172 | this.signaling.disconnect(); 173 | }, 1000); 174 | } 175 | 176 | ngOnDestroy(): void { 177 | this.hangup(); 178 | if (this.localStream && this.localStream.active) { 179 | this.localStream.getTracks().forEach((track) => { track.stop(); }); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-sfu/session-call-sfu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC Webapp 4 | 5 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-sfu/session-call-sfu.component.scss: -------------------------------------------------------------------------------- 1 | .main-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | .videos { 10 | margin-top: 5px; 11 | text-align: center; 12 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-sfu/session-call-sfu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionCallSFUComponent } from './session-call-sfu.component'; 4 | 5 | describe('SessionCallSfuComponent', () => { 6 | let component: SessionCallSFUComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionCallSFUComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SessionCallSFUComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-star/session-call-star.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC Webapp 4 | 5 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-star/session-call-star.component.scss: -------------------------------------------------------------------------------- 1 | .main-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | .videos { 10 | margin-top: 5px; 11 | text-align: center; 12 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call-star/session-call-star.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionCallStarComponent } from './session-call-star.component'; 4 | 5 | describe('SessionCallStarComponent', () => { 6 | let component: SessionCallStarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionCallStarComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionCallStarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call/session-call.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC Webapp 4 | 5 | 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
-------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call/session-call.component.scss: -------------------------------------------------------------------------------- 1 | .main-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | .videos { 10 | margin-top: 5px; 11 | text-align: center; 12 | height: 50%; 13 | 14 | video { 15 | width: 49%; 16 | height: 50vh; 17 | margin: 3px; 18 | } 19 | } -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/components/session-call/session-call.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionCallComponent } from './session-call.component'; 4 | 5 | describe('SessionCallComponent', () => { 6 | let component: SessionCallComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionCallComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionCallComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/models/message-sender.ts: -------------------------------------------------------------------------------- 1 | export interface MessageSender { 2 | sendMessage(message: any): void; 3 | } 4 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/models/webrtc-client-type.ts: -------------------------------------------------------------------------------- 1 | export enum WebRTCClientType { 2 | CentralUnit = 'central_unit', 3 | SideUnit = 'side_unit' 4 | } 5 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/models/webrtc-client.ts: -------------------------------------------------------------------------------- 1 | import { environment } from 'src/environments/environment'; 2 | 3 | export class WebRTCClient { 4 | 5 | private streams: MediaStream[] = []; 6 | private peerConnection: RTCPeerConnection; 7 | private isStarted = false; 8 | 9 | constructor( 10 | private clientId: string, 11 | private isInitiator: boolean, 12 | private sendMessageCallback: (message: any) => void, 13 | private notifyUserCallback: (message: string) => void, 14 | private onStreamCallback: (stream: MediaStream) => void, 15 | private onHangupCallback: () => void, 16 | private localStreamCallback: () => MediaStream, 17 | private remoteStreamsCallback: () => MediaStream[] = () => null) { 18 | } 19 | 20 | getClientId(): string { 21 | return this.clientId; 22 | } 23 | 24 | getIsInitiator(): boolean { 25 | return this.isInitiator; 26 | } 27 | 28 | getIsStarted(): boolean { 29 | return this.isStarted; 30 | } 31 | 32 | getStreams(): MediaStream[] { 33 | return this.streams; 34 | } 35 | 36 | initiateCall(): void { 37 | console.log('Initiating a call.', this.clientId); 38 | if (!this.isStarted && this.localStreamCallback()) { 39 | this.createPeerConnection(); 40 | 41 | const localStream = this.localStreamCallback(); 42 | this.peerConnection.addTrack(localStream.getVideoTracks()[0], localStream); 43 | this.peerConnection.addTrack(localStream.getAudioTracks()[0], localStream); 44 | 45 | this.remoteStreamsCallback()?.forEach(stream => { 46 | this.peerConnection.addTrack(stream.getVideoTracks()[0], stream); 47 | this.peerConnection.addTrack(stream.getAudioTracks()[0], stream); 48 | }); 49 | 50 | this.isStarted = true; 51 | if (this.isInitiator) { 52 | this.sendOffer(); 53 | } 54 | } 55 | } 56 | 57 | createPeerConnection(): void { 58 | console.log('Creating peer connection.', this.clientId); 59 | try { 60 | this.peerConnection = new RTCPeerConnection({ 61 | iceServers: environment.iceServers, 62 | sdpSemantics: 'unified-plan' 63 | } as RTCConfiguration); 64 | 65 | this.peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => { 66 | if (event.candidate) { 67 | this.sendIceCandidate(event); 68 | } else { 69 | console.log('End of candidates.', this.clientId); 70 | } 71 | }; 72 | 73 | this.peerConnection.ontrack = (event: RTCTrackEvent) => { 74 | if (event.streams[0]) { 75 | this.addRemoteStream(event.streams[0]); 76 | } 77 | }; 78 | } catch (e) { 79 | console.log('Failed to create PeerConnection.', this.clientId, e.message); 80 | return; 81 | } 82 | } 83 | 84 | sendOffer(): void { 85 | console.log('Sending offer to peer.', this.clientId); 86 | this.addTransceivers(); 87 | this.peerConnection.createOffer() 88 | .then((sdp: RTCSessionDescriptionInit) => { 89 | this.peerConnection.setLocalDescription(sdp); 90 | this.sendMessageCallback(sdp); 91 | }); 92 | } 93 | 94 | sendAnswer(): void { 95 | console.log('Sending answer to peer.', this.clientId); 96 | this.addTransceivers(); 97 | this.peerConnection.createAnswer() 98 | .then((sdp: RTCSessionDescription) => { 99 | this.peerConnection.setLocalDescription(sdp); 100 | this.sendMessageCallback(sdp); 101 | }); 102 | } 103 | 104 | addIceCandidate(message: any): void { 105 | console.log('Adding ice candidate.', this.clientId); 106 | const candidate = new RTCIceCandidate({ 107 | sdpMLineIndex: message.label, 108 | candidate: message.candidate 109 | }); 110 | this.peerConnection.addIceCandidate(candidate); 111 | } 112 | 113 | sendIceCandidate(event: RTCPeerConnectionIceEvent): void { 114 | console.log('Sending ice candidate to remote peer.', this.clientId); 115 | this.sendMessageCallback({ 116 | type: 'candidate', 117 | label: event.candidate.sdpMLineIndex, 118 | id: event.candidate.sdpMid, 119 | candidate: event.candidate.candidate 120 | }); 121 | } 122 | 123 | setRemoteDescription(message: any): void { 124 | console.log('Setting remote description.', this.clientId); 125 | this.peerConnection.setRemoteDescription(new RTCSessionDescription(message)); 126 | } 127 | 128 | addTransceivers(): void { 129 | console.log('Adding transceivers.', this.clientId); 130 | const init = { direction: 'recvonly', streams: [], sendEncodings: [] } as RTCRtpTransceiverInit; 131 | this.peerConnection.addTransceiver('audio', init); 132 | this.peerConnection.addTransceiver('video', init); 133 | } 134 | 135 | addRemoteStream(stream: MediaStream): void { 136 | console.log('Adding remote stream.', this.clientId); 137 | if (!this.streams.find(s => s.id === stream.id)) { 138 | this.streams.push(stream); 139 | this.onStreamCallback(stream); 140 | } 141 | } 142 | 143 | addRemoteTracks(stream: MediaStream): void { 144 | console.log('Adding remote tracks.', this.clientId); 145 | this.peerConnection.addTrack(stream.getVideoTracks()[0], stream); 146 | this.peerConnection.addTrack(stream.getAudioTracks()[0], stream); 147 | this.sendOffer(); 148 | } 149 | 150 | removeRemoteStream(streamId: string): void { 151 | console.log('Removing remote stream.', this.clientId); 152 | const streamToRemove = this.streams.find(s => s.id === streamId); 153 | this.streams = this.streams.filter(s => s !== streamToRemove); 154 | streamToRemove.getTracks().forEach((track) => { track.stop(); }); 155 | } 156 | 157 | handleRemoteHangup(): void { 158 | console.log('Session terminated by remote peer.', this.clientId); 159 | this.stopPeerConnection(); 160 | this.onHangupCallback(); 161 | this.notifyUserCallback('Remote client' + this.clientId + ' has left the call.'); 162 | } 163 | 164 | stopPeerConnection(): void { 165 | console.log('Stopping peer connection.', this.clientId); 166 | this.isStarted = false; 167 | if (this.peerConnection) { 168 | this.peerConnection.close(); 169 | this.peerConnection = null; 170 | } 171 | this.streams.forEach(stream => { 172 | if (stream && stream.active) { 173 | stream.getTracks().forEach((track) => { track.stop(); }); 174 | } 175 | }); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/services/signalr.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SignalrService } from './signalr.service'; 4 | 5 | describe('SignalrService', () => { 6 | let service: SignalrService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(SignalrService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/services/signalr.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HubConnection, HubConnectionBuilder, HubConnectionState, IHttpConnectionOptions } from '@microsoft/signalr'; 3 | import { environment } from 'src/environments/environment'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SignalrService { 9 | 10 | private baseUrl: string = environment.signalingServerUrl; 11 | 12 | private hubConnection: HubConnection | undefined; 13 | 14 | constructor() { } 15 | 16 | getConnectionId(): string { 17 | return this.hubConnection.connectionId; 18 | } 19 | 20 | async connect(path: string, withToken: boolean): Promise { 21 | const url = this.baseUrl + path; 22 | 23 | const builder = new HubConnectionBuilder(); 24 | if (!withToken) { 25 | builder.withUrl(url); 26 | } else { 27 | builder.withUrl(url, { 28 | accessTokenFactory: () => { 29 | return sessionStorage.getItem('token'); 30 | } 31 | } as IHttpConnectionOptions); 32 | } 33 | this.hubConnection = builder.withAutomaticReconnect().build(); 34 | 35 | return this.hubConnection.start() 36 | .then(() => { 37 | if (this.isConnected()) { 38 | console.log('SignalR: Connected to the server: ' + url); 39 | } 40 | }) 41 | .catch(err => { 42 | console.error('SignalR: Failed to start with error: ' + err.toString()); 43 | }); 44 | } 45 | 46 | async define(methodName: string, newMethod: (...args: any[]) => void): Promise { 47 | if (this.hubConnection) { 48 | this.hubConnection.on(methodName, newMethod); 49 | } 50 | } 51 | 52 | async invoke(methodName: string, ...args: any[]): Promise { 53 | if (this.isConnected()) { 54 | return this.hubConnection.invoke(methodName, ...args); 55 | } 56 | } 57 | 58 | disconnect(): void { 59 | if (this.isConnected()) { 60 | this.hubConnection.stop(); 61 | } 62 | } 63 | 64 | isConnected(): boolean { 65 | return this.hubConnection && this.hubConnection.state === HubConnectionState.Connected; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/services/webrtc-utils.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WebrtcUtils } from './webrtc-utils.service'; 4 | 5 | describe('WebrtcUtils', () => { 6 | let service: WebrtcUtils; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(WebrtcUtils); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/app/services/webrtc-utils.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MessageSender } from '../models/message-sender'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class WebrtcUtils { 8 | 9 | public static readonly OPUS = 'opus'; 10 | public static readonly H264 = 'H264'; 11 | 12 | constructor() { } 13 | 14 | // WebRTC peer connection utils 15 | 16 | public static createPeerConnection( 17 | iceServers: RTCIceServer[], 18 | sdpSemantics: 'unified-plan' | 'plan-b', 19 | bundlePolicy: RTCBundlePolicy, 20 | iceTransportPolicy: RTCIceTransportPolicy, 21 | rtcpMuxPolicy: RTCRtcpMuxPolicy, 22 | peerIdentity: string, 23 | certificates: RTCCertificate[], 24 | iceCandidatePoolSize: number): RTCPeerConnection { 25 | return new RTCPeerConnection({ 26 | iceServers, sdpSemantics, bundlePolicy, iceTransportPolicy, rtcpMuxPolicy, peerIdentity, certificates, iceCandidatePoolSize 27 | } as RTCConfiguration); 28 | } 29 | 30 | public static doIceRestart(peerConnection: RTCPeerConnection | any, messageSender: MessageSender): void { 31 | console.log('doIceRestart'); 32 | try { 33 | // try using new restartIce method 34 | peerConnection.restartIce(); 35 | } catch (error) { 36 | // if it is not supported, use the old implementation 37 | peerConnection.createOffer({ 38 | iceRestart: true 39 | }) 40 | .then((sdp: RTCSessionDescriptionInit) => { 41 | peerConnection.setLocalDescription(sdp); 42 | messageSender.sendMessage(sdp); 43 | }); 44 | } 45 | } 46 | 47 | // WebRTC stats reports 48 | 49 | public static logStats(peerConnection: RTCPeerConnection, type: 'inbound' | 'outbound' | 'all'): void { 50 | peerConnection.getStats().then(stat => { 51 | stat.forEach(report => { 52 | switch (type) { 53 | case 'inbound': 54 | if (report.type === 'inbound-rtp') { 55 | console.log(report); 56 | } 57 | break; 58 | case 'outbound': 59 | if (report.type === 'outbound-rtp') { 60 | console.log(report); 61 | } 62 | break; 63 | default: 64 | console.log(report); 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | // WebRTC bitrate manipulation 71 | 72 | public static changeBitrate(sdp: RTCSessionDescriptionInit, start: string, min: string, max: string): any | RTCSessionDescriptionInit { 73 | const sdpLines = sdp.sdp.split('\r\n'); 74 | sdpLines.forEach((str, i) => { 75 | // use only relevant lines 76 | if (str.indexOf('a=fmtp') !== -1) { 77 | // if bitrates are not yet set, create required lines and set them, otherwise change them to new values 78 | if (str.indexOf('x-google-') === -1) { 79 | sdpLines[i] = str + `;x-google-max-bitrate=${max};x-google-min-bitrate=${min};x-google-start-bitrate=${start}`; 80 | } else { 81 | sdpLines[i] = str.split(';x-google-')[0] + `;x-google-max-bitrate=${max};x-google-min-bitrate=${min};x-google-start-bitrate=${start}`; 82 | } 83 | } 84 | }); 85 | sdp = new RTCSessionDescription({ 86 | type: sdp.type, 87 | sdp: sdpLines.join('\r\n'), 88 | }); 89 | return sdp; 90 | } 91 | 92 | // WebRTC codecs manipulation 93 | 94 | public static getCodecs(type: 'audio' | 'video'): string[] { 95 | return RTCRtpSender.getCapabilities(type).codecs.map(c => c.mimeType).filter((value, index, self) => self.indexOf(value) === index); 96 | } 97 | 98 | public static setCodecs(sdp: RTCSessionDescriptionInit, type: 'audio' | 'video', codecMimeType: string): RTCSessionDescriptionInit { 99 | const sdpLines = sdp.sdp.split('\r\n'); 100 | sdpLines.forEach((str, i) => { 101 | // use only relevant type SDP lines 102 | if (str.startsWith('m=' + type)) { 103 | const lineWords = str.split(' '); 104 | // get all lines (payloads) related to given codec 105 | const payloads = this.getPayloads(sdp.sdp, codecMimeType); 106 | // proceed only with relavant payloads for this specific sdp line 107 | const relevantPayloads = payloads.filter(p => lineWords.indexOf(p) !== -1); 108 | if (relevantPayloads.length > 0) { 109 | // remove the codecs from current positions in the line 110 | relevantPayloads.forEach(codec => { 111 | const index = lineWords.indexOf(codec, 2); 112 | lineWords.splice(index, 1); 113 | }); 114 | // add first three default values (M=, #, protocols) 115 | str = lineWords[0] + ' ' + lineWords[1] + ' ' + lineWords[2]; 116 | // add chosen codecs on the beginning 117 | relevantPayloads.forEach(codec => { 118 | str = str + ' ' + codec; 119 | }); 120 | // add the rest of codecs on the end 121 | for (let k = 3; k < lineWords.length; k++) { 122 | str = str + ' ' + lineWords[k]; 123 | } 124 | } 125 | sdpLines[i] = str; 126 | } 127 | }); 128 | // create new SDP with changed codecs 129 | sdp = new RTCSessionDescription({ 130 | type: sdp.type, 131 | sdp: sdpLines.join('\r\n'), 132 | }); 133 | return sdp; 134 | } 135 | 136 | private static getPayloads(sdp: string, codec: string): string[] { 137 | const payloads = []; 138 | const sdpLines = sdp.split('\r\n'); 139 | sdpLines.forEach((str, i) => { 140 | if (str.indexOf('a=rtpmap:') !== -1 && str.indexOf(codec) !== -1) { 141 | payloads.push(str.split('a=rtpmap:').pop().split(' ')[0]); 142 | } 143 | }); 144 | return payloads.filter((v, i) => payloads.indexOf(v) === i); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkofjac/webrtc-tutorials/f8366bdba59dc38868f2fce32c7fce16c0cd3238/WebRTCWebApp/src/assets/.gitkeep -------------------------------------------------------------------------------- /WebRTCWebApp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | signalingServerUrl: 'http://localhost:5000/hubs', 4 | iceServers: [ 5 | {urls: 'stun:stun.1.google.com:19302'}, 6 | {urls: 'stun:stun1.l.google.com:19302'} 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | signalingServerUrl: 'http://localhost:5000/hubs', 8 | iceServers: [ 9 | {urls: 'stun:stun.1.google.com:19302'}, 10 | {urls: 'stun:stun1.l.google.com:19302'} 11 | ] 12 | }; 13 | 14 | /* 15 | * For easier debugging in development mode, you can import the following file 16 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 17 | * 18 | * This import should be commented out in production mode because it will have a negative impact 19 | * on performance if an error is thrown. 20 | */ 21 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 22 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkofjac/webrtc-tutorials/f8366bdba59dc38868f2fce32c7fce16c0cd3238/WebRTCWebApp/src/favicon.ico -------------------------------------------------------------------------------- /WebRTCWebApp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebRTC Webapp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js'; 59 | import 'zone.js/testing'; 60 | 61 | 62 | /*************************************************************************************************** 63 | * APPLICATION IMPORTS 64 | */ 65 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /WebRTCWebApp/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /WebRTCWebApp/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /WebRTCWebApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2020", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebRTCWebApp/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /WebRTCWebApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "no-string-literal": false, 140 | "directive-selector": [ 141 | true, 142 | "attribute", 143 | "app", 144 | "camelCase" 145 | ], 146 | "component-selector": [ 147 | true, 148 | "element", 149 | "app", 150 | "kebab-case" 151 | ] 152 | } 153 | } 154 | --------------------------------------------------------------------------------