├── .gitattributes ├── .gitignore ├── AngularAppSol.sln ├── AngularAppWeb ├── .gitignore ├── AngularAppWeb.csproj ├── ClientApp │ ├── .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.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── app-intro.component.html │ │ │ │ ├── app-intro.component.ts │ │ │ │ ├── app-member.component.html │ │ │ │ ├── app-member.component.scss │ │ │ │ ├── app-member.component.ts │ │ │ │ ├── app-rtc.component.html │ │ │ │ ├── app-rtc.component.scss │ │ │ │ └── app-rtc.component.ts │ │ │ ├── services │ │ │ │ ├── chat-signalr.service.ts │ │ │ │ └── rtc-signalr.service.ts │ │ │ └── user.model.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 ├── Hubs │ ├── ChatHub.cs │ └── WebRtcHub.cs ├── Models │ ├── Room.cs │ ├── RtcIceServer.cs │ └── User.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ └── _ViewImports.cshtml ├── Program.cs ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json ├── web.config └── wwwroot │ └── favicon.ico ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /FsApi/Scripts/angular/fs-app.js 263 | -------------------------------------------------------------------------------- /AngularAppSol.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30204.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AngularAppWeb", "AngularAppWeb\AngularAppWeb.csproj", "{4EC6072F-2D74-4E5A-842D-1E5C331D2B00}" 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 | {4EC6072F-2D74-4E5A-842D-1E5C331D2B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4EC6072F-2D74-4E5A-842D-1E5C331D2B00}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4EC6072F-2D74-4E5A-842D-1E5C331D2B00}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4EC6072F-2D74-4E5A-842D-1E5C331D2B00}.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 = {234F85BA-BAAB-45F2-957F-E4748D43DC07} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /AngularAppWeb/.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | bld/ 24 | bin/ 25 | Bin/ 26 | obj/ 27 | Obj/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opendb 78 | *.opensdf 79 | *.sdf 80 | *.cachefile 81 | 82 | # Visual Studio profiler 83 | *.psess 84 | *.vsp 85 | *.vspx 86 | *.sap 87 | 88 | # TFS 2012 Local Workspace 89 | $tf/ 90 | 91 | # Guidance Automation Toolkit 92 | *.gpState 93 | 94 | # ReSharper is a .NET coding add-in 95 | _ReSharper*/ 96 | *.[Rr]e[Ss]harper 97 | *.DotSettings.user 98 | 99 | # JustCode is a .NET coding add-in 100 | .JustCode 101 | 102 | # TeamCity is a build add-in 103 | _TeamCity* 104 | 105 | # DotCover is a Code Coverage Tool 106 | *.dotCover 107 | 108 | # NCrunch 109 | _NCrunch_* 110 | .*crunch*.local.xml 111 | nCrunchTemp_* 112 | 113 | # MightyMoose 114 | *.mm.* 115 | AutoTest.Net/ 116 | 117 | # Web workbench (sass) 118 | .sass-cache/ 119 | 120 | # Installshield output folder 121 | [Ee]xpress/ 122 | 123 | # DocProject is a documentation generator add-in 124 | DocProject/buildhelp/ 125 | DocProject/Help/*.HxT 126 | DocProject/Help/*.HxC 127 | DocProject/Help/*.hhc 128 | DocProject/Help/*.hhk 129 | DocProject/Help/*.hhp 130 | DocProject/Help/Html2 131 | DocProject/Help/html 132 | 133 | # Click-Once directory 134 | publish/ 135 | 136 | # Publish Web Output 137 | *.[Pp]ublish.xml 138 | *.azurePubxml 139 | # TODO: Comment the next line if you want to checkin your web deploy settings 140 | # but database connection strings (with potential passwords) will be unencrypted 141 | *.pubxml 142 | *.publishproj 143 | 144 | # NuGet Packages 145 | *.nupkg 146 | # The packages folder can be ignored because of Package Restore 147 | **/packages/* 148 | # except build/, which is used as an MSBuild target. 149 | !**/packages/build/ 150 | # Uncomment if necessary however generally it will be regenerated when needed 151 | #!**/packages/repositories.config 152 | 153 | # Microsoft Azure Build Output 154 | csx/ 155 | *.build.csdef 156 | 157 | # Microsoft Azure Emulator 158 | ecf/ 159 | rcf/ 160 | 161 | # Microsoft Azure ApplicationInsights config file 162 | ApplicationInsights.config 163 | 164 | # Windows Store app package directory 165 | AppPackages/ 166 | BundleArtifacts/ 167 | 168 | # Visual Studio cache files 169 | # files ending in .cache can be ignored 170 | *.[Cc]ache 171 | # but keep track of directories ending in .cache 172 | !*.[Cc]ache/ 173 | 174 | # Others 175 | ClientBin/ 176 | ~$* 177 | *~ 178 | *.dbmdl 179 | *.dbproj.schemaview 180 | *.pfx 181 | *.publishsettings 182 | orleans.codegen.cs 183 | 184 | /node_modules 185 | 186 | # RIA/Silverlight projects 187 | Generated_Code/ 188 | 189 | # Backup & report files from converting an old project file 190 | # to a newer Visual Studio version. Backup files are not needed, 191 | # because we have git ;-) 192 | _UpgradeReport_Files/ 193 | Backup*/ 194 | UpgradeLog*.XML 195 | UpgradeLog*.htm 196 | 197 | # SQL Server files 198 | *.mdf 199 | *.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | 209 | # GhostDoc plugin setting file 210 | *.GhostDoc.xml 211 | 212 | # Node.js Tools for Visual Studio 213 | .ntvs_analysis.dat 214 | 215 | # Visual Studio 6 build log 216 | *.plg 217 | 218 | # Visual Studio 6 workspace options file 219 | *.opt 220 | 221 | # Visual Studio LightSwitch build output 222 | **/*.HTMLClient/GeneratedArtifacts 223 | **/*.DesktopClient/GeneratedArtifacts 224 | **/*.DesktopClient/ModelManifest.xml 225 | **/*.Server/GeneratedArtifacts 226 | **/*.Server/ModelManifest.xml 227 | _Pvt_Extensions 228 | 229 | # Paket dependency manager 230 | .paket/paket.exe 231 | 232 | # FAKE - F# Make 233 | .fake/ 234 | -------------------------------------------------------------------------------- /AngularAppWeb/AngularAppWeb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 11 | 12 | false 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | %(DistFiles.Identity) 47 | PreserveNewest 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # TestApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "test-app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/test-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | } 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "browserTarget": "test-app:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "test-app:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "test-app:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "main": "src/test.ts", 87 | "polyfills": "src/polyfills.ts", 88 | "tsConfig": "tsconfig.spec.json", 89 | "karmaConfig": "karma.conf.js", 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ], 94 | "styles": [ 95 | "src/styles.scss" 96 | ], 97 | "scripts": [] 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-devkit/build-angular:tslint", 102 | "options": { 103 | "tsConfig": [ 104 | "tsconfig.app.json", 105 | "tsconfig.spec.json", 106 | "e2e/tsconfig.json" 107 | ], 108 | "exclude": [ 109 | "**/node_modules/**" 110 | ] 111 | } 112 | }, 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "test-app:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "test-app:serve:production" 122 | } 123 | } 124 | } 125 | } 126 | }}, 127 | "defaultProject": "test-app" 128 | } 129 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('test-app app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/e2e/tsconfig.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/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/test-app'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~10.2.0", 15 | "@angular/cdk": "^10.2.6", 16 | "@angular/common": "~10.2.0", 17 | "@angular/compiler": "~10.2.0", 18 | "@angular/core": "~10.2.0", 19 | "@angular/forms": "~10.2.0", 20 | "@angular/http": "^7.2.16", 21 | "@angular/material": "^10.2.6", 22 | "@angular/platform-browser": "~10.2.0", 23 | "@angular/platform-browser-dynamic": "~10.2.0", 24 | "@angular/router": "^10.2.1", 25 | "@microsoft/signalr": "^3.1.9", 26 | "rxjs": "~6.6.0", 27 | "tslib": "^2.0.0", 28 | "webrtc-adapter": "^7.7.0", 29 | "zone.js": "~0.10.2" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.1002.0", 33 | "@angular/cli": "~10.2.0", 34 | "@angular/compiler-cli": "~10.2.0", 35 | "@types/node": "^12.11.1", 36 | "@types/jasmine": "~3.5.0", 37 | "@types/jasminewd2": "~2.0.3", 38 | "codelyzer": "^6.0.0", 39 | "jasmine-core": "~3.6.0", 40 | "jasmine-spec-reporter": "~5.0.0", 41 | "karma": "~5.0.0", 42 | "karma-chrome-launcher": "~3.1.0", 43 | "karma-coverage-istanbul-reporter": "~3.0.2", 44 | "karma-jasmine": "~4.0.0", 45 | "karma-jasmine-html-reporter": "^1.5.0", 46 | "protractor": "~7.0.0", 47 | "ts-node": "~8.3.0", 48 | "tslint": "~6.1.0", 49 | "typescript": "~4.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Menu 6 | 7 | 8 | Link 1 9 | Link 2 10 | Link 3 11 | 12 | 13 | 14 | 15 | 18 | Conference 19 | 20 |
21 | 22 |
23 |
24 |
-------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav { 6 | width: 200px; 7 | } 8 | 9 | .mat-toolbar.mat-primary { 10 | position: sticky; 11 | top: 0; 12 | z-index: 1; 13 | } 14 | 15 | .content-container { 16 | padding: 25px; 17 | height: 100%; 18 | overflow: hidden; 19 | } 20 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 2 | import { Component } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent { 12 | isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset) 13 | .pipe( 14 | map(result => result.matches) 15 | ); 16 | 17 | constructor(private breakpointObserver: BreakpointObserver) {} 18 | } 19 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | import { NgModule } from '@angular/core'; 7 | import { RouterModule, Routes } from '@angular/router'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatCheckboxModule } from '@angular/material/checkbox'; 11 | import { MatDialogModule } from '@angular/material/dialog'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { MatListModule } from '@angular/material/list'; 15 | import { MatMenuModule } from '@angular/material/menu'; 16 | import { MatSelectModule } from '@angular/material/select'; 17 | import { MatSidenavModule } from '@angular/material/sidenav'; 18 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 19 | import { MatTabsModule } from '@angular/material/tabs'; 20 | import { MatGridListModule } from '@angular/material/grid-list'; 21 | import { MatToolbarModule } from '@angular/material/toolbar'; 22 | 23 | import { AppComponent } from './app.component'; 24 | import { AppIntroComponent } from './components/app-intro.component'; 25 | import { AppRtcComponent } from './components/app-rtc.component'; 26 | import { ChatSignalRService } from './services/chat-signalr.service'; 27 | import { RtcSignalRService } from './services/rtc-signalr.service'; 28 | import { AppMemberComponent } from './components/app-member.component'; 29 | import { LayoutModule } from '@angular/cdk/layout'; 30 | 31 | const appRoutes: Routes = [ 32 | { 33 | path: 'intro', 34 | component: AppIntroComponent, 35 | }, 36 | { 37 | path: 'rtc', 38 | component: AppRtcComponent, 39 | }, 40 | { 41 | path: '', 42 | redirectTo: '/rtc', 43 | pathMatch: 'full' 44 | }, 45 | { 46 | path: '**', 47 | redirectTo: '/rtc' } 48 | ]; 49 | 50 | 51 | @NgModule({ 52 | declarations: [ 53 | AppComponent, 54 | AppIntroComponent, 55 | AppRtcComponent, 56 | AppMemberComponent 57 | ], 58 | imports: [ 59 | BrowserModule, 60 | BrowserAnimationsModule, 61 | RouterModule.forRoot( 62 | appRoutes, 63 | { enableTracing: false } // <-- debugging purposes only 64 | ), 65 | FormsModule, 66 | HttpClientModule, 67 | MatButtonModule, 68 | MatCardModule, 69 | MatCheckboxModule, 70 | MatDialogModule, 71 | MatIconModule, 72 | MatInputModule, 73 | MatListModule, 74 | MatMenuModule, 75 | MatSelectModule, 76 | MatSidenavModule, 77 | MatSlideToggleModule, 78 | MatTabsModule, 79 | MatToolbarModule, 80 | MatGridListModule, 81 | LayoutModule 82 | ], 83 | providers: [ 84 | ChatSignalRService, 85 | RtcSignalRService 86 | ], 87 | bootstrap: [AppComponent] 88 | }) 89 | export class AppModule { } 90 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-intro.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |

Send some basic messages

6 | 7 |
8 |
9 |
10 | 11 | 13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
#Messages
{{i + 1}}{{message}}
33 |
34 |
35 |
36 | No messages 37 |
38 |
39 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-intro.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ChatSignalRService } from '../services/chat-signalr.service'; 3 | 4 | @Component({ 5 | selector: 'app-intro', 6 | templateUrl: './app-intro.component.html' 7 | }) 8 | export class AppIntroComponent implements OnInit { 9 | message = ''; 10 | messages: string[] = []; 11 | 12 | constructor(private signalrService: ChatSignalRService) { 13 | } 14 | 15 | ngOnInit() { 16 | this.signalrService.getObservable().subscribe(messs => { 17 | this.messages = messs; 18 | }); 19 | } 20 | 21 | public sendMessage(): void { 22 | this.signalrService.sendMessage(this.message); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-member.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{user.user.userName}} 4 |
5 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-member.component.scss: -------------------------------------------------------------------------------- 1 | .videoWrap { 2 | padding: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-member.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef, Input } from '@angular/core'; 2 | import { UserConnection } from '../services/rtc-signalr.service'; 3 | 4 | @Component({ 5 | selector: 'app-member', 6 | templateUrl: './app-member.component.html', 7 | styleUrls: ['./app-rtc.component.scss'] 8 | }) 9 | export class AppMemberComponent implements OnInit { 10 | @Input() 11 | user: UserConnection; 12 | 13 | theVideo: HTMLVideoElement; 14 | @ViewChild('theVideo') 15 | set mainLocalVideo(el: ElementRef) { 16 | this.theVideo = el.nativeElement; 17 | } 18 | 19 | constructor() { 20 | } 21 | 22 | ngOnInit() { 23 | this.user.streamObservable.subscribe(stream => { 24 | if (stream) { 25 | if (this.user.isCurrentUser) { 26 | this.theVideo.srcObject = stream; 27 | this.theVideo.defaultMuted = true; 28 | this.theVideo.volume = 0; 29 | this.theVideo.muted = true; 30 | } else { 31 | this.theVideo.srcObject = stream; 32 | } 33 | } 34 | else { 35 | console.log('No stream'); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-rtc.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-rtc.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacoferre/netcore-signalr-angular-webrtc/9225e01262cca947e6dc1d3257ebb80d9417b589/AngularAppWeb/ClientApp/src/app/components/app-rtc.component.scss -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/components/app-rtc.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { RtcSignalRService, IUser, UserConnection } from '../services/rtc-signalr.service'; 3 | 4 | @Component({ 5 | selector: 'app-rtc', 6 | templateUrl: './app-rtc.component.html', 7 | styleUrls: ['./app-rtc.component.scss'] 8 | }) 9 | export class AppRtcComponent implements OnInit { 10 | userName = ''; 11 | users: UserConnection[]; 12 | 13 | joined = false; 14 | 15 | roomName = 'Test1'; 16 | 17 | constructor(public rtcService: RtcSignalRService) { 18 | rtcService.usersObservable 19 | .subscribe(users => { 20 | this.users = users; 21 | }); 22 | } 23 | 24 | ngOnInit() { 25 | } 26 | 27 | connect() { 28 | this.rtcService.join(this.userName, this.roomName); 29 | this.joined = true; 30 | } 31 | 32 | trackByFn(user: IUser) { 33 | return user.connectionId; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/services/chat-signalr.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HubConnection } from '@microsoft/signalr'; 3 | import * as signalR from '@microsoft/signalr'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class ChatSignalRService { 8 | private messSub = new BehaviorSubject([]); 9 | private messObservable: Observable; 10 | private _hubConnection: HubConnection | undefined; 11 | private messages: string[] = []; 12 | 13 | constructor() { 14 | this._hubConnection = new signalR.HubConnectionBuilder() 15 | .withUrl('./sgr/chat') 16 | .configureLogging(signalR.LogLevel.Information) 17 | .build(); 18 | 19 | this._hubConnection 20 | .start() 21 | .catch(err => console.error(err.toString())); 22 | 23 | this._hubConnection 24 | .on('Send', (data: any) => { 25 | const received = `Received: ${data}`; 26 | this.messages.push(received); 27 | 28 | this.messSub.next(this.messages); 29 | }); 30 | } 31 | 32 | public getObservable() { 33 | if (!this.messObservable) { 34 | this.messObservable = this.messSub.asObservable(); 35 | } 36 | 37 | return this.messObservable; 38 | } 39 | 40 | public sendMessage(message: string): void { 41 | if (this._hubConnection) { 42 | this._hubConnection.invoke('Send', message); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/services/rtc-signalr.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as signalR from '@microsoft/signalr'; 3 | import 'webrtc-adapter'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | 6 | export interface IUser { 7 | userName: string; 8 | connectionId: string; 9 | } 10 | 11 | export class UserConnection { 12 | user: IUser; 13 | isCurrentUser: boolean; 14 | rtcConnection: RTCPeerConnection; 15 | streamSub: BehaviorSubject; 16 | streamObservable: Observable; 17 | creatingOffer = false; 18 | creatingAnswer = false; 19 | 20 | constructor(user: IUser, isCurrentUser: boolean, rtcConnection: RTCPeerConnection) { 21 | this.user = user; 22 | this.isCurrentUser = isCurrentUser; 23 | this.rtcConnection = rtcConnection; 24 | this.streamSub = new BehaviorSubject(undefined); 25 | this.streamObservable = this.streamSub.asObservable(); 26 | } 27 | 28 | setStream(stream: MediaStream) { 29 | this.streamSub.next(stream); 30 | } 31 | 32 | end() { 33 | if (this.rtcConnection) { 34 | this.rtcConnection.close(); 35 | } 36 | if (this.streamSub.getValue()) { 37 | this.setStream(undefined); 38 | } 39 | } 40 | } 41 | 42 | export interface IOtherUserMedia { 43 | otherUserConnectionId: string; 44 | track: RTCTrackEvent; 45 | } 46 | 47 | enum SignalType { 48 | newIceCandidate, 49 | videoOffer, 50 | videoAnswer 51 | } 52 | 53 | interface ISignal { 54 | type: SignalType; 55 | sdp?: RTCSessionDescription; 56 | candidate?: RTCIceCandidate; 57 | } 58 | 59 | @Injectable() 60 | export class RtcSignalRService { 61 | private _hubConnection: signalR.HubConnection; 62 | private _connections: { [index: string]: UserConnection } = {}; 63 | 64 | private connSub = new BehaviorSubject(false); 65 | public connObservable = this.connSub.asObservable(); 66 | private usersSub = new BehaviorSubject(undefined); 67 | public usersObservable = this.usersSub.asObservable(); 68 | 69 | public currentConnectionId: string; 70 | public currentRoomName: string; 71 | public currentMediaStream: MediaStream; 72 | public currentIceServers: RTCIceServer[]; 73 | public connected = false; 74 | 75 | private reset() { 76 | this.connected = false; 77 | this.connSub.next(false); 78 | this.usersSub.next(undefined); 79 | } 80 | 81 | constructor() { 82 | this._hubConnection = new signalR.HubConnectionBuilder() 83 | .withUrl('./sgr/rtc') 84 | .configureLogging(signalR.LogLevel.Debug) 85 | .build(); 86 | 87 | (async () => { 88 | try { 89 | await this._hubConnection.start(); 90 | const connectionId = await this._hubConnection.invoke('GetConnectionId'); 91 | this.currentConnectionId = connectionId; 92 | this.connected = true; 93 | this.closeAllVideoCalls(); 94 | this.connSub.next(true); 95 | } catch (error) { 96 | console.error(error); 97 | } 98 | })(); 99 | 100 | this._hubConnection 101 | .onclose((err) => { 102 | console.error(err); 103 | this.connected = false; 104 | this.reset(); 105 | }); 106 | 107 | this._hubConnection 108 | .on('callToUserList', async (roomName: string, users: IUser[]) => { 109 | if (this.currentRoomName === roomName) { 110 | users.forEach(user => { 111 | if (this._connections[user.connectionId] === undefined 112 | && user.connectionId !== this.currentConnectionId) { 113 | this.initiateOffer(user); 114 | } 115 | }); 116 | 117 | await this.updateUserList(users); 118 | } 119 | }); 120 | 121 | this._hubConnection 122 | .on('updateUserList', async (roomName: string, users: IUser[]) => { 123 | if (this.currentRoomName === roomName) { 124 | Object.keys(this._connections) 125 | .forEach(key => { 126 | if (!users.find(user => user.connectionId === key)) { 127 | this.closeVideoCall(key); 128 | } 129 | }); 130 | await this.updateUserList(users); 131 | } 132 | }); 133 | 134 | this._hubConnection 135 | .on('receiveSignal', async (user: IUser, signal: string) => { 136 | await this.newSignal(user, signal); 137 | }); 138 | } 139 | 140 | private async updateUserList(users: IUser[]): Promise { 141 | const iceServers = await this.getIceServers(); 142 | 143 | users.forEach(async user => { 144 | const connection = await this.getConnection(user.connectionId, iceServers, false); 145 | if (connection.user.userName !== user.userName) { 146 | connection.user.userName = user.userName; 147 | } 148 | if (connection.isCurrentUser && connection.streamSub.getValue() === undefined) { 149 | const stream = await this.getUserMediaInternal(); 150 | 151 | if (connection.streamSub.getValue() === undefined) { 152 | connection.streamSub.next(stream); 153 | } 154 | } 155 | }); 156 | this.usersSub.next(Object.values(this._connections)); 157 | } 158 | 159 | public join(userName: string, room: string) { 160 | if (!this.connected) { 161 | this.reset(); 162 | 163 | return; 164 | } 165 | 166 | this.closeAllVideoCalls(); 167 | 168 | this._connections[this.currentConnectionId] = 169 | new UserConnection({ userName: userName, connectionId: this.currentConnectionId }, true, undefined); 170 | this.currentRoomName = room; 171 | this._hubConnection 172 | .invoke('Join', userName, room); 173 | } 174 | 175 | public hangUp() { 176 | this._hubConnection.invoke('hangUp'); 177 | this.closeVideoCall(this.currentConnectionId); 178 | } 179 | 180 | private async getUserMediaInternal(): Promise { 181 | if (this.currentMediaStream) { 182 | return this.currentMediaStream; 183 | } 184 | 185 | try { 186 | return await navigator.mediaDevices.getUserMedia({ 187 | video: true, 188 | audio: true 189 | }); 190 | } catch (error) { 191 | console.error('Failed to get hardware access', error); 192 | } 193 | } 194 | 195 | private async getIceServers(): Promise { 196 | if (this.currentIceServers) { 197 | return this.currentIceServers; 198 | } 199 | 200 | try { 201 | return await this._hubConnection 202 | .invoke('GetIceServers'); 203 | } catch (error) { 204 | console.error('GetIceServers error: ', error); 205 | } 206 | } 207 | 208 | private async initiateOffer(acceptingUser: IUser) { 209 | const partnerClientId = acceptingUser.connectionId; 210 | 211 | console.log('Initiate offer to ' + acceptingUser.userName); 212 | 213 | if (this._connections[partnerClientId]) { 214 | console.log('Cannot initiate an offer with existing partner.'); 215 | return; 216 | } 217 | 218 | const iceServers = await this.getIceServers(); 219 | 220 | await this.getConnection(partnerClientId, iceServers, true); 221 | } 222 | 223 | private async sendSignal(message: ISignal, partnerClientId: string) { 224 | await this._hubConnection.invoke('SendSignal', JSON.stringify(message), partnerClientId); 225 | } 226 | 227 | private async newSignal(user: IUser, data: string) { 228 | const partnerClientId = user.connectionId; 229 | const signal: ISignal = JSON.parse(data); 230 | 231 | console.log('WebRTC: received signal'); 232 | 233 | if (signal.type === SignalType.newIceCandidate) { 234 | await this.receivedNewIceCandidate(partnerClientId, signal.candidate); 235 | } else if (signal.type === SignalType.videoOffer) { 236 | await this.receivedVideoOffer(partnerClientId, signal.sdp); 237 | } else if (signal.type === SignalType.videoAnswer) { 238 | await this.receivedVideoAnswer(partnerClientId, signal.sdp); 239 | } 240 | } 241 | 242 | private async receivedNewIceCandidate(partnerClientId: string, candidate: RTCIceCandidate) { 243 | console.log('Adding received ICE candidate: ' + JSON.stringify(candidate)); 244 | 245 | try { 246 | const iceServers = await this.getIceServers(); 247 | const connection = await this.getConnection(partnerClientId, iceServers, false); 248 | await connection.rtcConnection.addIceCandidate(candidate); 249 | } catch (error) { 250 | console.error('Error adding ICE candidate:', error); 251 | } 252 | } 253 | 254 | private async receivedVideoOffer(partnerClientId: string, sdp: RTCSessionDescription) { 255 | 256 | console.log('Starting to accept invitation from ' + partnerClientId); 257 | 258 | const desc = new RTCSessionDescription(sdp); 259 | const iceServers = await this.getIceServers(); 260 | const connection = await this.getConnection(partnerClientId, iceServers, false); 261 | 262 | if (connection.creatingAnswer) { 263 | console.warn('Second answer not created.'); 264 | 265 | return; 266 | } 267 | connection.creatingAnswer = true; 268 | 269 | try { 270 | console.log('setRemoteDescription'); 271 | await connection.rtcConnection.setRemoteDescription(desc); 272 | console.log('createAnswer'); 273 | const senders = connection.rtcConnection.getSenders(); 274 | if (!senders || senders.length === 0) { 275 | console.log('AddSenders needed'); 276 | const localStream = await this.getUserMediaInternal(); 277 | localStream.getTracks().forEach(track => connection.rtcConnection.addTrack(track, localStream)); 278 | } 279 | const answer = await connection.rtcConnection.createAnswer(); 280 | console.log('setLocalDescription', answer); 281 | await connection.rtcConnection.setLocalDescription(answer); 282 | await this.sendSignal({ 283 | type: SignalType.videoAnswer, 284 | sdp: connection.rtcConnection.localDescription 285 | }, partnerClientId); 286 | } catch (error) { 287 | console.error('Error in receivedVideoOffer:', error); 288 | } 289 | 290 | connection.creatingAnswer = false; 291 | } 292 | 293 | private async receivedVideoAnswer(partnerClientId: string, sdp: RTCSessionDescription) { 294 | console.log('Call recipient has accepted our call'); 295 | 296 | try { 297 | const iceServers = await this.getIceServers(); 298 | const connection = await this.getConnection(partnerClientId, iceServers, false); 299 | 300 | await connection.rtcConnection.setRemoteDescription(sdp); 301 | } catch (error) { 302 | console.error('Error in receivedVideoAnswer:', error); 303 | } 304 | } 305 | 306 | private async getConnection(partnerClientId: string, iceServers: RTCIceServer[], createOffer: boolean): Promise { 307 | const connection = this._connections[partnerClientId] 308 | || (await this.createConnection(partnerClientId, iceServers, createOffer)); 309 | return connection; 310 | } 311 | 312 | private async createConnection(partnerClientId: string, iceServers: RTCIceServer[], createOffer: boolean): Promise { 313 | console.log('WebRTC: creating connection...'); 314 | 315 | if (this._connections[partnerClientId]) { 316 | this.closeVideoCall(partnerClientId); 317 | } 318 | 319 | const connection = new RTCPeerConnection({ iceServers: iceServers }); 320 | const userConnection = new UserConnection({ userName: '', connectionId: partnerClientId }, 321 | false, connection); 322 | 323 | this._connections[partnerClientId] = userConnection; 324 | 325 | const localStream = await this.getUserMediaInternal(); 326 | localStream.getTracks().forEach(track => connection.addTrack(track, localStream)); 327 | 328 | connection.oniceconnectionstatechange = () => { 329 | switch (connection.iceConnectionState) { 330 | case 'closed': 331 | case 'failed': 332 | case 'disconnected': 333 | this.closeAllVideoCalls(); 334 | break; 335 | } 336 | }; 337 | connection.onicegatheringstatechange = () => { 338 | console.log('*** ICE gathering state changed to: ' + connection.iceGatheringState); 339 | }; 340 | connection.onsignalingstatechange = (event) => { 341 | console.log('*** WebRTC signaling state changed to: ' + connection.signalingState); 342 | switch (connection.signalingState) { 343 | case 'closed': 344 | this.closeAllVideoCalls(); 345 | break; 346 | } 347 | }; 348 | connection.onicecandidate = async (event) => { 349 | if (event.candidate) { 350 | console.log('WebRTC: new ICE candidate'); 351 | await this.sendSignal({ 352 | type: SignalType.newIceCandidate, 353 | candidate: event.candidate 354 | }, partnerClientId); 355 | } else { 356 | console.log('WebRTC: ICE candidate gathering complete'); 357 | } 358 | }; 359 | connection.onconnectionstatechange = (state) => { 360 | const states = { 361 | 'iceConnectionState': connection.iceConnectionState, 362 | 'iceGatheringState': connection.iceGatheringState, 363 | 'connectionState': connection.connectionState, 364 | 'signalingState': connection.signalingState 365 | }; 366 | 367 | console.log(JSON.stringify(states), state); 368 | }; 369 | 370 | connection.ontrack = (event) => { 371 | console.log('Track received from ' + partnerClientId); 372 | userConnection.setStream(event.streams[0]); 373 | }; 374 | 375 | if (createOffer) { 376 | try { 377 | const desc = await connection.createOffer(); 378 | 379 | await connection.setLocalDescription(desc); 380 | await this.sendSignal({ 381 | type: SignalType.videoOffer, 382 | sdp: connection.localDescription 383 | }, partnerClientId); 384 | } catch (error) { 385 | console.error('Error in onnegotiationneeded:', error); 386 | } 387 | } 388 | 389 | return userConnection; 390 | } 391 | 392 | private closeAllVideoCalls() { 393 | Object.keys(this._connections) 394 | .forEach(key => { 395 | this.closeVideoCall(key); 396 | }); 397 | this._connections = {}; 398 | } 399 | 400 | private closeVideoCall(partnerClientId: string) { 401 | const connection = this._connections[partnerClientId]; 402 | if (connection) { 403 | connection.end(); 404 | this._connections[partnerClientId] = undefined; 405 | 406 | delete this._connections[partnerClientId]; 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/app/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | name: string; 3 | details: string; 4 | isAdmin: boolean; 5 | isCool: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacoferre/netcore-signalr-angular-webrtc/9225e01262cca947e6dc1d3257ebb80d9417b589/AngularAppWeb/ClientApp/src/assets/.gitkeep -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacoferre/netcore-signalr-angular-webrtc/9225e01262cca947e6dc1d3257ebb80d9417b589/AngularAppWeb/ClientApp/src/favicon.ico -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TestApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | /** IE10 and 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/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom Theming for Angular Material 3 | // For more information: https://material.angular.io/guide/theming 4 | @import '~@angular/material/theming'; 5 | // Plus imports for other components in your app. 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat-core(); 11 | 12 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 13 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 14 | // hue. Available color palettes: https://material.io/design/color/ 15 | $angular-app-primary: mat-palette($mat-indigo); 16 | $angular-app-accent: mat-palette($mat-pink, A200, A100, A400); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $angular-app-warn: mat-palette($mat-red); 20 | 21 | // Create the theme object (a Sass map containing all of the palettes). 22 | $angular-app-theme: mat-light-theme($angular-app-primary, $angular-app-accent, $angular-app-warn); 23 | 24 | // Include theme styles for core and each component used in your app. 25 | // Alternatively, you can import and @include the theme mixins for each component 26 | // that you are using. 27 | @include angular-material-theme($angular-app-theme); 28 | 29 | html, body { 30 | display: flex; 31 | flex-direction: column; 32 | 33 | font-family: Roboto, Arial, sans-serif; 34 | margin: 0; 35 | height: 100%; 36 | } 37 | 38 | html, body { height: 100%; } 39 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 40 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | -------------------------------------------------------------------------------- /AngularAppWeb/ClientApp/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 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /AngularAppWeb/Hubs/ChatHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AngularAppWeb.Hubs 8 | { 9 | public class ChatHub : Hub 10 | { 11 | public Task Send(string message) 12 | { 13 | return Clients.All.SendAsync("Send", message); 14 | } 15 | 16 | public override Task OnConnectedAsync() 17 | { 18 | return base.OnConnectedAsync(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AngularAppWeb/Hubs/WebRtcHub.cs: -------------------------------------------------------------------------------- 1 | using AngularAppWeb.Models; 2 | using Microsoft.AspNetCore.SignalR; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace AngularAppWeb.Hubs 7 | { 8 | public class WebRtcHub : Hub 9 | { 10 | public string GetConnectionId() 11 | { 12 | return Context.ConnectionId; 13 | } 14 | 15 | public RtcIceServer[] GetIceServers() 16 | { 17 | // Perhaps Ice server management. 18 | 19 | return new RtcIceServer[] { new RtcIceServer() { Username = "", Credential = "" } }; 20 | } 21 | 22 | public async Task Join(string userName, string roomName) 23 | { 24 | var user = User.Get(userName, Context.ConnectionId); 25 | var room = Room.Get(roomName); 26 | 27 | if (user.CurrentRoom != null) 28 | { 29 | room.Users.Remove(user); 30 | await SendUserListUpdate(Clients.Others, room, false); 31 | } 32 | 33 | user.CurrentRoom = room; 34 | room.Users.Add(user); 35 | 36 | await SendUserListUpdate(Clients.Caller, room, true); 37 | await SendUserListUpdate(Clients.Others, room, false); 38 | } 39 | 40 | public override async Task OnDisconnectedAsync(Exception exception) 41 | { 42 | await HangUp(); 43 | 44 | await base.OnDisconnectedAsync(exception); 45 | } 46 | 47 | public async Task HangUp() 48 | { 49 | var callingUser = User.Get(Context.ConnectionId); 50 | 51 | if (callingUser == null) 52 | { 53 | return; 54 | } 55 | 56 | if (callingUser.CurrentRoom != null) 57 | { 58 | callingUser.CurrentRoom.Users.Remove(callingUser); 59 | await SendUserListUpdate(Clients.Others, callingUser.CurrentRoom, false); 60 | } 61 | 62 | User.Remove(callingUser); 63 | } 64 | 65 | // WebRTC Signal Handler 66 | public async Task SendSignal(string signal, string targetConnectionId) 67 | { 68 | var callingUser = User.Get(Context.ConnectionId); 69 | var targetUser = User.Get(targetConnectionId); 70 | 71 | // Make sure both users are valid 72 | if (callingUser == null || targetUser == null) 73 | { 74 | return; 75 | } 76 | 77 | // These folks are in a call together, let's let em talk WebRTC 78 | await Clients.Client(targetConnectionId).SendAsync("receiveSignal", callingUser, signal); 79 | } 80 | 81 | private async Task SendUserListUpdate(IClientProxy to, Room room, bool callTo) 82 | { 83 | await to.SendAsync(callTo ? "callToUserList" : "updateUserList" , room.Name, room.Users); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /AngularAppWeb/Models/Room.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AngularAppWeb.Models 7 | { 8 | public class Room 9 | { 10 | private static readonly List Rooms = new List(); 11 | 12 | public string Name { get; set; } 13 | public List Users { get; set; } = new List(); 14 | 15 | public static int TotalUsers => Rooms.Sum(room => room.Users.Count); 16 | 17 | public static Room Get(string name) 18 | { 19 | lock (Rooms) 20 | { 21 | var current = Rooms.SingleOrDefault(r => r.Name == name); 22 | 23 | if (current == default(Room)) 24 | { 25 | current = new Room 26 | { 27 | Name = name 28 | }; 29 | Rooms.Add(current); 30 | } 31 | 32 | return current; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /AngularAppWeb/Models/RtcIceServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AngularAppWeb.Models 7 | { 8 | public class RtcIceServer 9 | { 10 | public string Urls { get; set; } = "stun:stun1.l.google.com:19302"; 11 | public string Username { get; set; } 12 | public string Credential { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AngularAppWeb/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace AngularAppWeb.Models 8 | { 9 | public class User 10 | { 11 | private static readonly List Users = new List(); 12 | 13 | public string UserName { get; set; } 14 | public string ConnectionId { get; set; } 15 | [JsonIgnore] 16 | public Room CurrentRoom { get; set; } 17 | 18 | public static void Remove(User user) 19 | { 20 | Users.Remove(user); 21 | } 22 | 23 | public static User Get(string connectionId) 24 | { 25 | return Users.SingleOrDefault(u => u.ConnectionId == connectionId); 26 | } 27 | 28 | public static User Get(string userName, string connectionId) 29 | { 30 | lock (Users) 31 | { 32 | var current = Users.SingleOrDefault(u => u.ConnectionId == connectionId); 33 | 34 | if (current == default(User)) 35 | { 36 | current = new User 37 | { 38 | UserName = userName, 39 | ConnectionId = connectionId 40 | }; 41 | Users.Add(current); 42 | } 43 | else 44 | { 45 | current.UserName = userName; 46 | } 47 | 48 | return current; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /AngularAppWeb/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to Development environment will display more detailed information about the error that occurred. 20 |

21 |

22 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 23 |

24 | -------------------------------------------------------------------------------- /AngularAppWeb/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace AngularAppWeb.Pages 10 | { 11 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 12 | public class ErrorModel : PageModel 13 | { 14 | public string RequestId { get; set; } 15 | 16 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 17 | 18 | public void OnGet() 19 | { 20 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AngularAppWeb/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using AngularAppWeb 2 | @namespace AngularAppWeb.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /AngularAppWeb/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace AngularAppWeb 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AngularAppWeb/Startup.cs: -------------------------------------------------------------------------------- 1 | using AngularAppWeb.Hubs; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.AspNetCore.SpaServices.AngularCli; 13 | 14 | namespace AngularAppWeb 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddControllers(); 29 | 30 | // In production, the Angular files will be served from this directory 31 | services.AddSpaStaticFiles(configuration => 32 | { 33 | configuration.RootPath = "ClientApp/dist/angular-app"; 34 | }); 35 | 36 | services.AddSignalR(conf => 37 | { 38 | conf.StreamBufferCapacity = 100000; 39 | }); 40 | } 41 | 42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | if (env.IsDevelopment()) 46 | { 47 | app.UseDeveloperExceptionPage(); 48 | } 49 | else 50 | { 51 | app.UseExceptionHandler("/Error"); 52 | app.UseHttpsRedirection(); 53 | } 54 | 55 | app.UseRouting(); 56 | 57 | app.UseEndpoints(endpoints => 58 | { 59 | endpoints.MapControllers(); 60 | endpoints.MapHub("/sgr/chat"); 61 | endpoints.MapHub("/sgr/rtc"); 62 | }); 63 | 64 | app.UseStaticFiles(); 65 | app.UseSpaStaticFiles(); 66 | 67 | app.UseSpa(spa => 68 | { 69 | // To learn more about options for serving an Angular SPA from ASP.NET Core, 70 | // see https://go.microsoft.com/fwlink/?linkid=864501 71 | 72 | spa.Options.SourcePath = "ClientApp"; 73 | 74 | if (env.IsDevelopment()) 75 | { 76 | //spa.UseAngularCliServer("start"); 77 | spa.UseProxyToSpaDevelopmentServer(baseUri: "http://localhost:4200"); 78 | } 79 | }); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /AngularAppWeb/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AngularAppWeb/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /AngularAppWeb/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AngularAppWeb/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacoferre/netcore-signalr-angular-webrtc/9225e01262cca947e6dc1d3257ebb80d9417b589/AngularAppWeb/wwwroot/favicon.ico -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paco Ferre 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netcore-signalr-angular-webrtc 2 | A simple many to many conference sample using .NET Core + SignarR Core + Angular 7 + WebRTC 3 | --------------------------------------------------------------------------------