├── .gitattributes ├── .github └── screenshot.jpg ├── .gitignore ├── Confer.sln ├── Confer ├── .config │ └── dotnet-tools.json ├── .gitignore ├── Api │ └── SessionsController.cs ├── ClientApp │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── assets │ │ │ ├── video-call.png │ │ │ └── webcam.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ └── qr │ │ │ ├── LICENSE │ │ │ ├── qrcode.js │ │ │ └── qrcode.min.js │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AuthAwareContainer.tsx │ │ │ ├── Home.tsx │ │ │ ├── If.tsx │ │ │ ├── Layout.css │ │ │ ├── Layout.tsx │ │ │ ├── LoadingAnimation.css │ │ │ ├── LoadingAnimation.tsx │ │ │ ├── NavMenu.css │ │ │ ├── Session.css │ │ │ ├── Session.tsx │ │ │ ├── Settings.tsx │ │ │ ├── Sidebar.css │ │ │ ├── Sidebar.tsx │ │ │ ├── ViewComponent.tsx │ │ │ └── api-authorization │ │ │ │ ├── ApiAuthorizationConstants.js │ │ │ │ ├── ApiAuthorizationRoutes.js │ │ │ │ ├── AuthorizeRoute.js │ │ │ │ ├── AuthorizeService.js │ │ │ │ ├── Login.js │ │ │ │ ├── LoginMenu.js │ │ │ │ └── Logout.js │ │ ├── custom.css │ │ ├── index.tsx │ │ ├── interfaces │ │ │ ├── ChatMessage.ts │ │ │ ├── IceCandidateMessage.ts │ │ │ ├── Peer.ts │ │ │ ├── SdpMessage.ts │ │ │ ├── SessionDto.ts │ │ │ └── Settings.ts │ │ ├── react-app-env.d.ts │ │ ├── registerServiceWorker.js │ │ ├── services │ │ │ ├── SessionInfoContext.ts │ │ │ ├── SettingsService.ts │ │ │ └── SignalingService.ts │ │ ├── setupTests.js │ │ └── utils │ │ │ ├── EventEmitterEx.ts │ │ │ ├── MediaHelper.ts │ │ │ └── UI.ts │ └── tsconfig.json ├── Confer.csproj ├── Models │ ├── ActiveSession.cs │ ├── ApplicationUser.cs │ ├── IceServer.cs │ ├── RTCSessionDescriptionInit.cs │ ├── SessionDto.cs │ └── SessionDto.cs.d.ts ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Shared │ │ └── _LoginPartial.cshtml │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── Services │ ├── AppSettings.cs │ ├── SessionManager.cs │ └── SignalingHub.cs ├── appsettings.Development.json └── appsettings.json ├── 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 | -------------------------------------------------------------------------------- /.github/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbound/Confer/7c22fad0a7ad72c488daf75b030a0cec9722dce9/.github/screenshot.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /Confer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30523.141 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Confer", "Confer\Confer.csproj", "{25DD95BA-9BCE-49D5-B0C9-360A26730D78}" 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 | {25DD95BA-9BCE-49D5-B0C9-360A26730D78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {25DD95BA-9BCE-49D5-B0C9-360A26730D78}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {25DD95BA-9BCE-49D5-B0C9-360A26730D78}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {25DD95BA-9BCE-49D5-B0C9-360A26730D78}.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 = {9F529FA5-E748-4C73-A29A-3D3CBD71CD22} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Confer/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "5.0.8", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Confer/.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 | build/ 21 | bld/ 22 | bin/ 23 | Bin/ 24 | obj/ 25 | Obj/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | /wwwroot/dist/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | *_i.c 45 | *_p.c 46 | *_i.h 47 | *.ilk 48 | *.meta 49 | *.obj 50 | *.pch 51 | *.pdb 52 | *.pgc 53 | *.pgd 54 | *.rsp 55 | *.sbr 56 | *.tlb 57 | *.tli 58 | *.tlh 59 | *.tmp 60 | *.tmp_proj 61 | *.log 62 | *.vspscc 63 | *.vssscc 64 | .builds 65 | *.pidb 66 | *.svclog 67 | *.scc 68 | 69 | # Chutzpah Test files 70 | _Chutzpah* 71 | 72 | # Visual C++ cache files 73 | ipch/ 74 | *.aps 75 | *.ncb 76 | *.opendb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | *.sap 86 | 87 | # TFS 2012 Local Workspace 88 | $tf/ 89 | 90 | # Guidance Automation Toolkit 91 | *.gpState 92 | 93 | # ReSharper is a .NET coding add-in 94 | _ReSharper*/ 95 | *.[Rr]e[Ss]harper 96 | *.DotSettings.user 97 | 98 | # JustCode is a .NET coding add-in 99 | .JustCode 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | _NCrunch_* 109 | .*crunch*.local.xml 110 | nCrunchTemp_* 111 | 112 | # MightyMoose 113 | *.mm.* 114 | AutoTest.Net/ 115 | 116 | # Web workbench (sass) 117 | .sass-cache/ 118 | 119 | # Installshield output folder 120 | [Ee]xpress/ 121 | 122 | # DocProject is a documentation generator add-in 123 | DocProject/buildhelp/ 124 | DocProject/Help/*.HxT 125 | DocProject/Help/*.HxC 126 | DocProject/Help/*.hhc 127 | DocProject/Help/*.hhk 128 | DocProject/Help/*.hhp 129 | DocProject/Help/Html2 130 | DocProject/Help/html 131 | 132 | # Click-Once directory 133 | publish/ 134 | 135 | # Publish Web Output 136 | *.[Pp]ublish.xml 137 | *.azurePubxml 138 | # TODO: Comment the next line if you want to checkin your web deploy settings 139 | # but database connection strings (with potential passwords) will be unencrypted 140 | *.pubxml 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Microsoft Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Microsoft Azure Emulator 157 | ecf/ 158 | rcf/ 159 | 160 | # Microsoft Azure ApplicationInsights config file 161 | ApplicationInsights.config 162 | 163 | # Windows Store app package directory 164 | AppPackages/ 165 | BundleArtifacts/ 166 | 167 | # Visual Studio cache files 168 | # files ending in .cache can be ignored 169 | *.[Cc]ache 170 | # but keep track of directories ending in .cache 171 | !*.[Cc]ache/ 172 | 173 | # Others 174 | ClientBin/ 175 | ~$* 176 | *~ 177 | *.dbmdl 178 | *.dbproj.schemaview 179 | *.pfx 180 | *.publishsettings 181 | orleans.codegen.cs 182 | 183 | /node_modules 184 | 185 | # RIA/Silverlight projects 186 | Generated_Code/ 187 | 188 | # Backup & report files from converting an old project file 189 | # to a newer Visual Studio version. Backup files are not needed, 190 | # because we have git ;-) 191 | _UpgradeReport_Files/ 192 | Backup*/ 193 | UpgradeLog*.XML 194 | UpgradeLog*.htm 195 | 196 | # SQL Server files 197 | *.mdf 198 | *.ldf 199 | 200 | # Business Intelligence projects 201 | *.rdl.data 202 | *.bim.layout 203 | *.bim_*.settings 204 | 205 | # Microsoft Fakes 206 | FakesAssemblies/ 207 | 208 | # GhostDoc plugin setting file 209 | *.GhostDoc.xml 210 | 211 | # Node.js Tools for Visual Studio 212 | .ntvs_analysis.dat 213 | 214 | # Visual Studio 6 build log 215 | *.plg 216 | 217 | # Visual Studio 6 workspace options file 218 | *.opt 219 | 220 | # Visual Studio LightSwitch build output 221 | **/*.HTMLClient/GeneratedArtifacts 222 | **/*.DesktopClient/GeneratedArtifacts 223 | **/*.DesktopClient/ModelManifest.xml 224 | **/*.Server/GeneratedArtifacts 225 | **/*.Server/ModelManifest.xml 226 | _Pvt_Extensions 227 | 228 | # Paket dependency manager 229 | .paket/paket.exe 230 | 231 | # FAKE - F# Make 232 | .fake/ 233 | -------------------------------------------------------------------------------- /Confer/Api/SessionsController.cs: -------------------------------------------------------------------------------- 1 | using Confer.Models; 2 | using Confer.Services; 3 | using Microsoft.AspNetCore.Cors; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | 7 | // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 8 | 9 | namespace Confer.Api 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | [EnableCors("OpenPolicy")] 14 | public class SessionsController : ControllerBase 15 | { 16 | private readonly ISessionManager _sessionManager; 17 | private readonly ILogger _logger; 18 | 19 | public SessionsController(ISessionManager sessionManager, ILogger logger) 20 | { 21 | _sessionManager = sessionManager; 22 | _logger = logger; 23 | } 24 | 25 | [HttpPost] 26 | public ActionResult Post([FromBody]SessionDto session) 27 | { 28 | session.Id = _sessionManager.CreateNewSession(session); 29 | 30 | return Ok(session); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Confer/ClientApp/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /Confer/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /Confer/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "confer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@microsoft/signalr": "^5.0.17", 7 | "bootstrap": "^5.2.1", 8 | "jquery": "^3.6.1", 9 | "merge": "^2.1.1", 10 | "oidc-client": "^1.11.5", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-icons": "^4.4.0", 14 | "react-router-bootstrap": "^0.25.0", 15 | "react-router-dom": "^5.3.3", 16 | "react-scripts": "5.0.1", 17 | "reactstrap": "^8.10.1", 18 | "rimraf": "^3.0.2" 19 | }, 20 | "devDependencies": { 21 | "@types/jest": "^26.0.24", 22 | "@types/node": "^16.11.62", 23 | "@types/react": "^17.0.50", 24 | "@types/react-dom": "^17.0.17", 25 | "@types/react-router-dom": "^5.3.3", 26 | "@types/reactstrap": "^8.7.2", 27 | "ajv": "^8.11.0", 28 | "cross-env": "^7.0.3", 29 | "eslint": "^7.32.0", 30 | "eslint-config-react-app": "^6.0.0", 31 | "eslint-plugin-flowtype": "^5.10.0", 32 | "eslint-plugin-import": "^2.26.0", 33 | "eslint-plugin-jsx-a11y": "^6.6.1", 34 | "eslint-plugin-react": "^7.31.8", 35 | "nan": "^2.16.0", 36 | "typescript": "^4.8.3" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app", 40 | "rules": { 41 | "eqeqeq": "off" 42 | } 43 | }, 44 | "scripts": { 45 | "start": "rimraf ./build && react-scripts start", 46 | "build": "react-scripts build", 47 | "test": "cross-env CI=true react-scripts test --env=jsdom", 48 | "eject": "react-scripts eject", 49 | "lint": "eslint ./src/" 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Confer/ClientApp/public/assets/video-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbound/Confer/7c22fad0a7ad72c488daf75b030a0cec9722dce9/Confer/ClientApp/public/assets/video-call.png -------------------------------------------------------------------------------- /Confer/ClientApp/public/assets/webcam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbound/Confer/7c22fad0a7ad72c488daf75b030a0cec9722dce9/Confer/ClientApp/public/assets/webcam.png -------------------------------------------------------------------------------- /Confer/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbound/Confer/7c22fad0a7ad72c488daf75b030a0cec9722dce9/Confer/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /Confer/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Confer 24 | 25 | 26 | 29 |
30 |
31 | 62 |
63 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /Confer/ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Confer", 3 | "name": "Confer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /Confer/ClientApp/public/qr/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Confer/ClientApp/public/qr/qrcode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * - Using the 'QRCode for Javascript library' 4 | * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. 5 | * - this library has no dependencies. 6 | * 7 | * @author davidshimjs 8 | * @see http://www.d-project.com/ 9 | * @see http://jeromeetienne.github.com/jquery-qrcode/ 10 | */ 11 | var QRCode; 12 | 13 | (function () { 14 | //--------------------------------------------------------------------- 15 | // QRCode for JavaScript 16 | // 17 | // Copyright (c) 2009 Kazuhiko Arase 18 | // 19 | // URL: http://www.d-project.com/ 20 | // 21 | // Licensed under the MIT license: 22 | // http://www.opensource.org/licenses/mit-license.php 23 | // 24 | // The word "QR Code" is registered trademark of 25 | // DENSO WAVE INCORPORATED 26 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 27 | // 28 | //--------------------------------------------------------------------- 29 | function QR8bitByte(data) { 30 | this.mode = QRMode.MODE_8BIT_BYTE; 31 | this.data = data; 32 | this.parsedData = []; 33 | 34 | // Added to support UTF-8 Characters 35 | for (var i = 0, l = this.data.length; i < l; i++) { 36 | var byteArray = []; 37 | var code = this.data.charCodeAt(i); 38 | 39 | if (code > 0x10000) { 40 | byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); 41 | byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); 42 | byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); 43 | byteArray[3] = 0x80 | (code & 0x3F); 44 | } else if (code > 0x800) { 45 | byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); 46 | byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); 47 | byteArray[2] = 0x80 | (code & 0x3F); 48 | } else if (code > 0x80) { 49 | byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); 50 | byteArray[1] = 0x80 | (code & 0x3F); 51 | } else { 52 | byteArray[0] = code; 53 | } 54 | 55 | this.parsedData.push(byteArray); 56 | } 57 | 58 | this.parsedData = Array.prototype.concat.apply([], this.parsedData); 59 | 60 | if (this.parsedData.length != this.data.length) { 61 | this.parsedData.unshift(191); 62 | this.parsedData.unshift(187); 63 | this.parsedData.unshift(239); 64 | } 65 | } 66 | 67 | QR8bitByte.prototype = { 68 | getLength: function (buffer) { 69 | return this.parsedData.length; 70 | }, 71 | write: function (buffer) { 72 | for (var i = 0, l = this.parsedData.length; i < l; i++) { 73 | buffer.put(this.parsedData[i], 8); 74 | } 75 | } 76 | }; 77 | 78 | function QRCodeModel(typeNumber, errorCorrectLevel) { 79 | this.typeNumber = typeNumber; 80 | this.errorCorrectLevel = errorCorrectLevel; 81 | this.modules = null; 82 | this.moduleCount = 0; 83 | this.dataCache = null; 84 | this.dataList = []; 85 | } 86 | 87 | QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} 88 | return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} 90 | if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} 91 | this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} 92 | return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} 98 | for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} 99 | for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} 100 | this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} 101 | var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} 102 | this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} 103 | row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" 106 | +buffer.getLengthInBits() 107 | +">" 108 | +totalDataCount*8 109 | +")");} 110 | if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} 111 | while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} 112 | while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} 113 | buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} 114 | buffer.put(QRCodeModel.PAD1,8);} 115 | return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} 117 | var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} 121 | return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} 122 | return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} 123 | return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} 129 | for(var row=0;row=256){n-=255;} 136 | return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} 151 | if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} 152 | this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; 153 | 154 | function _isSupportCanvas() { 155 | return typeof CanvasRenderingContext2D != "undefined"; 156 | } 157 | 158 | // android 2.x doesn't support Data-URI spec 159 | function _getAndroid() { 160 | var android = false; 161 | var sAgent = navigator.userAgent; 162 | 163 | if (/android/i.test(sAgent)) { // android 164 | android = true; 165 | var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); 166 | 167 | if (aMat && aMat[1]) { 168 | android = parseFloat(aMat[1]); 169 | } 170 | } 171 | 172 | return android; 173 | } 174 | 175 | var svgDrawer = (function() { 176 | 177 | var Drawing = function (el, htOption) { 178 | this._el = el; 179 | this._htOption = htOption; 180 | }; 181 | 182 | Drawing.prototype.draw = function (oQRCode) { 183 | var _htOption = this._htOption; 184 | var _el = this._el; 185 | var nCount = oQRCode.getModuleCount(); 186 | var nWidth = Math.floor(_htOption.width / nCount); 187 | var nHeight = Math.floor(_htOption.height / nCount); 188 | 189 | this.clear(); 190 | 191 | function makeSVG(tag, attrs) { 192 | var el = document.createElementNS('http://www.w3.org/2000/svg', tag); 193 | for (var k in attrs) 194 | if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); 195 | return el; 196 | } 197 | 198 | var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); 199 | svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 200 | _el.appendChild(svg); 201 | 202 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); 203 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); 204 | 205 | for (var row = 0; row < nCount; row++) { 206 | for (var col = 0; col < nCount; col++) { 207 | if (oQRCode.isDark(row, col)) { 208 | var child = makeSVG("use", {"x": String(col), "y": String(row)}); 209 | child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") 210 | svg.appendChild(child); 211 | } 212 | } 213 | } 214 | }; 215 | Drawing.prototype.clear = function () { 216 | while (this._el.hasChildNodes()) 217 | this._el.removeChild(this._el.lastChild); 218 | }; 219 | return Drawing; 220 | })(); 221 | 222 | var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; 223 | 224 | // Drawing in DOM by using Table tag 225 | var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { 226 | var Drawing = function (el, htOption) { 227 | this._el = el; 228 | this._htOption = htOption; 229 | }; 230 | 231 | /** 232 | * Draw the QRCode 233 | * 234 | * @param {QRCode} oQRCode 235 | */ 236 | Drawing.prototype.draw = function (oQRCode) { 237 | var _htOption = this._htOption; 238 | var _el = this._el; 239 | var nCount = oQRCode.getModuleCount(); 240 | var nWidth = Math.floor(_htOption.width / nCount); 241 | var nHeight = Math.floor(_htOption.height / nCount); 242 | var aHTML = ['']; 243 | 244 | for (var row = 0; row < nCount; row++) { 245 | aHTML.push(''); 246 | 247 | for (var col = 0; col < nCount; col++) { 248 | aHTML.push(''); 249 | } 250 | 251 | aHTML.push(''); 252 | } 253 | 254 | aHTML.push('
'); 255 | _el.innerHTML = aHTML.join(''); 256 | 257 | // Fix the margin values as real size. 258 | var elTable = _el.childNodes[0]; 259 | var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; 260 | var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; 261 | 262 | if (nLeftMarginTable > 0 && nTopMarginTable > 0) { 263 | elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; 264 | } 265 | }; 266 | 267 | /** 268 | * Clear the QRCode 269 | */ 270 | Drawing.prototype.clear = function () { 271 | this._el.innerHTML = ''; 272 | }; 273 | 274 | return Drawing; 275 | })() : (function () { // Drawing in Canvas 276 | function _onMakeImage() { 277 | this._elImage.src = this._elCanvas.toDataURL("image/png"); 278 | this._elImage.style.display = "block"; 279 | this._elCanvas.style.display = "none"; 280 | } 281 | 282 | // Android 2.1 bug workaround 283 | // http://code.google.com/p/android/issues/detail?id=5141 284 | if (this._android && this._android <= 2.1) { 285 | var factor = 1 / window.devicePixelRatio; 286 | var drawImage = CanvasRenderingContext2D.prototype.drawImage; 287 | CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { 288 | if (("nodeName" in image) && /img/i.test(image.nodeName)) { 289 | for (var i = arguments.length - 1; i >= 1; i--) { 290 | arguments[i] = arguments[i] * factor; 291 | } 292 | } else if (typeof dw == "undefined") { 293 | arguments[1] *= factor; 294 | arguments[2] *= factor; 295 | arguments[3] *= factor; 296 | arguments[4] *= factor; 297 | } 298 | 299 | drawImage.apply(this, arguments); 300 | }; 301 | } 302 | 303 | /** 304 | * Check whether the user's browser supports Data URI or not 305 | * 306 | * @private 307 | * @param {Function} fSuccess Occurs if it supports Data URI 308 | * @param {Function} fFail Occurs if it doesn't support Data URI 309 | */ 310 | function _safeSetDataURI(fSuccess, fFail) { 311 | var self = this; 312 | self._fFail = fFail; 313 | self._fSuccess = fSuccess; 314 | 315 | // Check it just once 316 | if (self._bSupportDataURI === null) { 317 | var el = document.createElement("img"); 318 | var fOnError = function() { 319 | self._bSupportDataURI = false; 320 | 321 | if (self._fFail) { 322 | self._fFail.call(self); 323 | } 324 | }; 325 | var fOnSuccess = function() { 326 | self._bSupportDataURI = true; 327 | 328 | if (self._fSuccess) { 329 | self._fSuccess.call(self); 330 | } 331 | }; 332 | 333 | el.onabort = fOnError; 334 | el.onerror = fOnError; 335 | el.onload = fOnSuccess; 336 | el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. 337 | return; 338 | } else if (self._bSupportDataURI === true && self._fSuccess) { 339 | self._fSuccess.call(self); 340 | } else if (self._bSupportDataURI === false && self._fFail) { 341 | self._fFail.call(self); 342 | } 343 | }; 344 | 345 | /** 346 | * Drawing QRCode by using canvas 347 | * 348 | * @constructor 349 | * @param {HTMLElement} el 350 | * @param {Object} htOption QRCode Options 351 | */ 352 | var Drawing = function (el, htOption) { 353 | this._bIsPainted = false; 354 | this._android = _getAndroid(); 355 | 356 | this._htOption = htOption; 357 | this._elCanvas = document.createElement("canvas"); 358 | this._elCanvas.width = htOption.width; 359 | this._elCanvas.height = htOption.height; 360 | el.appendChild(this._elCanvas); 361 | this._el = el; 362 | this._oContext = this._elCanvas.getContext("2d"); 363 | this._bIsPainted = false; 364 | this._elImage = document.createElement("img"); 365 | this._elImage.alt = "Scan me!"; 366 | this._elImage.style.display = "none"; 367 | this._el.appendChild(this._elImage); 368 | this._bSupportDataURI = null; 369 | }; 370 | 371 | /** 372 | * Draw the QRCode 373 | * 374 | * @param {QRCode} oQRCode 375 | */ 376 | Drawing.prototype.draw = function (oQRCode) { 377 | var _elImage = this._elImage; 378 | var _oContext = this._oContext; 379 | var _htOption = this._htOption; 380 | 381 | var nCount = oQRCode.getModuleCount(); 382 | var nWidth = _htOption.width / nCount; 383 | var nHeight = _htOption.height / nCount; 384 | var nRoundedWidth = Math.round(nWidth); 385 | var nRoundedHeight = Math.round(nHeight); 386 | 387 | _elImage.style.display = "none"; 388 | this.clear(); 389 | 390 | for (var row = 0; row < nCount; row++) { 391 | for (var col = 0; col < nCount; col++) { 392 | var bIsDark = oQRCode.isDark(row, col); 393 | var nLeft = col * nWidth; 394 | var nTop = row * nHeight; 395 | _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 396 | _oContext.lineWidth = 1; 397 | _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 398 | _oContext.fillRect(nLeft, nTop, nWidth, nHeight); 399 | 400 | // 안티 앨리어싱 방지 처리 401 | _oContext.strokeRect( 402 | Math.floor(nLeft) + 0.5, 403 | Math.floor(nTop) + 0.5, 404 | nRoundedWidth, 405 | nRoundedHeight 406 | ); 407 | 408 | _oContext.strokeRect( 409 | Math.ceil(nLeft) - 0.5, 410 | Math.ceil(nTop) - 0.5, 411 | nRoundedWidth, 412 | nRoundedHeight 413 | ); 414 | } 415 | } 416 | 417 | this._bIsPainted = true; 418 | }; 419 | 420 | /** 421 | * Make the image from Canvas if the browser supports Data URI. 422 | */ 423 | Drawing.prototype.makeImage = function () { 424 | if (this._bIsPainted) { 425 | _safeSetDataURI.call(this, _onMakeImage); 426 | } 427 | }; 428 | 429 | /** 430 | * Return whether the QRCode is painted or not 431 | * 432 | * @return {Boolean} 433 | */ 434 | Drawing.prototype.isPainted = function () { 435 | return this._bIsPainted; 436 | }; 437 | 438 | /** 439 | * Clear the QRCode 440 | */ 441 | Drawing.prototype.clear = function () { 442 | this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); 443 | this._bIsPainted = false; 444 | }; 445 | 446 | /** 447 | * @private 448 | * @param {Number} nNumber 449 | */ 450 | Drawing.prototype.round = function (nNumber) { 451 | if (!nNumber) { 452 | return nNumber; 453 | } 454 | 455 | return Math.floor(nNumber * 1000) / 1000; 456 | }; 457 | 458 | return Drawing; 459 | })(); 460 | 461 | /** 462 | * Get the type by string length 463 | * 464 | * @private 465 | * @param {String} sText 466 | * @param {Number} nCorrectLevel 467 | * @return {Number} type 468 | */ 469 | function _getTypeNumber(sText, nCorrectLevel) { 470 | var nType = 1; 471 | var length = _getUTF8Length(sText); 472 | 473 | for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { 474 | var nLimit = 0; 475 | 476 | switch (nCorrectLevel) { 477 | case QRErrorCorrectLevel.L : 478 | nLimit = QRCodeLimitLength[i][0]; 479 | break; 480 | case QRErrorCorrectLevel.M : 481 | nLimit = QRCodeLimitLength[i][1]; 482 | break; 483 | case QRErrorCorrectLevel.Q : 484 | nLimit = QRCodeLimitLength[i][2]; 485 | break; 486 | case QRErrorCorrectLevel.H : 487 | nLimit = QRCodeLimitLength[i][3]; 488 | break; 489 | } 490 | 491 | if (length <= nLimit) { 492 | break; 493 | } else { 494 | nType++; 495 | } 496 | } 497 | 498 | if (nType > QRCodeLimitLength.length) { 499 | throw new Error("Too long data"); 500 | } 501 | 502 | return nType; 503 | } 504 | 505 | function _getUTF8Length(sText) { 506 | var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); 507 | return replacedText.length + (replacedText.length != sText ? 3 : 0); 508 | } 509 | 510 | /** 511 | * @class QRCode 512 | * @constructor 513 | * @example 514 | * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); 515 | * 516 | * @example 517 | * var oQRCode = new QRCode("test", { 518 | * text : "http://naver.com", 519 | * width : 128, 520 | * height : 128 521 | * }); 522 | * 523 | * oQRCode.clear(); // Clear the QRCode. 524 | * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. 525 | * 526 | * @param {HTMLElement|String} el target element or 'id' attribute of element. 527 | * @param {Object|String} vOption 528 | * @param {String} vOption.text QRCode link data 529 | * @param {Number} [vOption.width=256] 530 | * @param {Number} [vOption.height=256] 531 | * @param {String} [vOption.colorDark="#000000"] 532 | * @param {String} [vOption.colorLight="#ffffff"] 533 | * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] 534 | */ 535 | QRCode = function (el, vOption) { 536 | this._htOption = { 537 | width : 256, 538 | height : 256, 539 | typeNumber : 4, 540 | colorDark : "#000000", 541 | colorLight : "#ffffff", 542 | correctLevel : QRErrorCorrectLevel.H 543 | }; 544 | 545 | if (typeof vOption === 'string') { 546 | vOption = { 547 | text : vOption 548 | }; 549 | } 550 | 551 | // Overwrites options 552 | if (vOption) { 553 | for (var i in vOption) { 554 | this._htOption[i] = vOption[i]; 555 | } 556 | } 557 | 558 | if (typeof el == "string") { 559 | el = document.getElementById(el); 560 | } 561 | 562 | if (this._htOption.useSVG) { 563 | Drawing = svgDrawer; 564 | } 565 | 566 | this._android = _getAndroid(); 567 | this._el = el; 568 | this._oQRCode = null; 569 | this._oDrawing = new Drawing(this._el, this._htOption); 570 | 571 | if (this._htOption.text) { 572 | this.makeCode(this._htOption.text); 573 | } 574 | }; 575 | 576 | /** 577 | * Make the QRCode 578 | * 579 | * @param {String} sText link data 580 | */ 581 | QRCode.prototype.makeCode = function (sText) { 582 | this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); 583 | this._oQRCode.addData(sText); 584 | this._oQRCode.make(); 585 | this._el.title = sText; 586 | this._oDrawing.draw(this._oQRCode); 587 | this.makeImage(); 588 | }; 589 | 590 | /** 591 | * Make the Image from Canvas element 592 | * - It occurs automatically 593 | * - Android below 3 doesn't support Data-URI spec. 594 | * 595 | * @private 596 | */ 597 | QRCode.prototype.makeImage = function () { 598 | if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { 599 | this._oDrawing.makeImage(); 600 | } 601 | }; 602 | 603 | /** 604 | * Clear the QRCode 605 | */ 606 | QRCode.prototype.clear = function () { 607 | this._oDrawing.clear(); 608 | }; 609 | 610 | /** 611 | * @name QRCode.CorrectLevel 612 | */ 613 | QRCode.CorrectLevel = QRErrorCorrectLevel; 614 | })(); 615 | -------------------------------------------------------------------------------- /Confer/ClientApp/public/qr/qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); -------------------------------------------------------------------------------- /Confer/ClientApp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | it('renders without crashing', async () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | 10 | 11 | , div); 12 | await new Promise(resolve => setTimeout(resolve, 1000)); 13 | }); 14 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router'; 3 | import { Layout } from './components/Layout'; 4 | import { Home } from './components/Home'; 5 | import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizationRoutes'; 6 | import { ApplicationPaths } from './components/api-authorization/ApiAuthorizationConstants'; 7 | import './custom.css' 8 | import { SettingsComp } from './components/Settings'; 9 | import { SessionInfoContextData, SessionInfoContext } from './services/SessionInfoContext'; 10 | import { ViewComponent } from './components/ViewComponent'; 11 | 12 | 13 | export default class App extends Component { 14 | static displayName = App.name; 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/AuthAwareContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import authService from "./api-authorization/AuthorizeService"; 3 | 4 | interface AuthAwareContainerProps { 5 | loggedInRender: React.ReactNode; 6 | loggedOutRender: React.ReactNode; 7 | } 8 | interface AuthAwareContainerState { 9 | authCheckCompleted: boolean; 10 | isAuthenticated: boolean; 11 | userName?: string; 12 | }; 13 | 14 | export class AuthAwareContainer extends Component { 15 | constructor(props: AuthAwareContainerProps) { 16 | super(props); 17 | 18 | this.state = { 19 | authCheckCompleted: false, 20 | isAuthenticated: false 21 | }; 22 | } 23 | 24 | _subscription: number = -1; 25 | 26 | componentDidMount() { 27 | this._subscription = authService.subscribe(() => this.populateState()); 28 | this.populateState(); 29 | } 30 | 31 | componentWillUnmount() { 32 | authService.unsubscribe(this._subscription); 33 | } 34 | 35 | async populateState() { 36 | const [isAuthenticated, user] = await Promise.all([authService.isAuthenticated(), authService.getUser()]) 37 | this.setState({ 38 | authCheckCompleted: true, 39 | isAuthenticated, 40 | userName: user && user.name 41 | }); 42 | } 43 | 44 | render() { 45 | if (!this.state.authCheckCompleted){ 46 | return null; 47 | } 48 | 49 | if (this.state.isAuthenticated){ 50 | return this.props.loggedInRender; 51 | } 52 | 53 | return this.props.loggedOutRender; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Col, FormGroup, Input, Label, Row } from 'reactstrap'; 3 | import { SessionDto } from '../interfaces/SessionDto'; 4 | 5 | interface HomeProps {} 6 | 7 | interface HomeState { 8 | sessionInfo: SessionDto 9 | } 10 | 11 | export class Home extends Component { 12 | static displayName = Home.name; 13 | 14 | state: HomeState = { 15 | sessionInfo: { 16 | id: "", 17 | titleBackgroundColor: "darkslategray", 18 | titleTextColor: "white", 19 | titleText: "Confer Chat", 20 | logoUrl: "/assets/webcam.png", 21 | pageBackgroundColor: "lightgray", 22 | pageTextColor: "black" 23 | } 24 | } 25 | 26 | render() { 27 | const { 28 | logoUrl, 29 | pageBackgroundColor, 30 | pageTextColor, 31 | titleBackgroundColor, 32 | titleText, 33 | titleTextColor 34 | } = this.state.sessionInfo; 35 | 36 | return ( 37 |
38 |

Welcome to Confer!

39 |
40 | Fill out this form to start a branded video chat session. 41 |
42 | 43 | 44 | 45 |

Session Options

46 |
47 | 48 | { 52 | this.setState({ 53 | sessionInfo: { 54 | ...this.state.sessionInfo, 55 | titleText: ev.currentTarget.value 56 | } 57 | }) 58 | }} 59 | /> 60 |
61 |
62 | 63 | { 67 | this.setState({ 68 | sessionInfo: { 69 | ...this.state.sessionInfo, 70 | titleTextColor: ev.currentTarget.value 71 | } 72 | }) 73 | }} 74 | /> 75 |
76 |
77 | 78 | { 82 | this.setState({ 83 | sessionInfo: { 84 | ...this.state.sessionInfo, 85 | titleBackgroundColor: ev.currentTarget.value 86 | } 87 | }) 88 | }} 89 | /> 90 |
91 |
92 | 93 | { 97 | this.setState({ 98 | sessionInfo: { 99 | ...this.state.sessionInfo, 100 | logoUrl: ev.currentTarget.value 101 | } 102 | }) 103 | }} 104 | /> 105 |
106 |
107 | 108 | { 112 | this.setState({ 113 | sessionInfo: { 114 | ...this.state.sessionInfo, 115 | pageBackgroundColor: ev.currentTarget.value 116 | } 117 | }) 118 | }} 119 | /> 120 |
121 |
122 | 123 | { 127 | this.setState({ 128 | sessionInfo: { 129 | ...this.state.sessionInfo, 130 | pageTextColor: ev.currentTarget.value 131 | } 132 | }) 133 | }} 134 | /> 135 |
136 |
137 | 164 |
165 | 166 |
167 | 168 |
169 | Or send a POST to /api/sessions with the following structure: 170 |
171 |
172 |
173 | 174 | { 175 | `{\n "titleBackgroundColor": "rgb(50,50,50)",\n "titleTextColor": "white",\n "titleText": "Awesome Chats",\n "logoUrl": "https://mywebsite.com/media/my_logo.png",\n "pageBackgroundColor": "darkgray",\n "pageTextColor": "black"\n}` 176 | } 177 | 178 |
179 |
180 |
181 | The response object will contain an id property. Go to {`/session/{id}`} in your browser to start. 182 |
183 |
184 | ) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/If.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | 3 | interface IfProps { 4 | condition: boolean; 5 | } 6 | 7 | export class If extends Component { 8 | render() { 9 | if (this.props.condition) { 10 | return this.props.children; 11 | } 12 | return ''; 13 | } 14 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Layout.css: -------------------------------------------------------------------------------- 1 | .menu-button { 2 | margin-right: 5px; 3 | } 4 | 5 | .menu-button.hidden { 6 | opacity: 0; 7 | width: 0; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Sidebar } from './Sidebar'; 3 | import { getWindowSize, WindowSize } from '../utils/UI'; 4 | import { FaBars } from 'react-icons/fa'; 5 | import './Layout.css'; 6 | import { Navbar, NavbarBrand } from 'reactstrap'; 7 | import authService from './api-authorization/AuthorizeService'; 8 | import { If } from './If'; 9 | import { Session } from './Session'; 10 | import { Route } from 'react-router-dom'; 11 | import { SessionInfoContext } from '../services/SessionInfoContext'; 12 | 13 | interface LayoutProps { } 14 | interface LayoutState { 15 | isSidebarOpen: boolean; 16 | isSidebarFixed: boolean; 17 | username?: string; 18 | } 19 | 20 | export class Layout extends Component { 21 | static displayName = Layout.name; 22 | static contextType = SessionInfoContext; 23 | context!: React.ContextType; 24 | 25 | subscription: number = 1; 26 | 27 | constructor(props: LayoutProps) { 28 | super(props); 29 | 30 | this.state = { 31 | isSidebarOpen: this.shouldSidebarBeFixed(), 32 | isSidebarFixed: this.shouldSidebarBeFixed() 33 | } 34 | } 35 | 36 | componentDidMount() { 37 | //this.subscription = authService.subscribe(() => this.setUser()); 38 | //this.setUser(); 39 | 40 | window.addEventListener("resize", this.onWindowResized); 41 | } 42 | 43 | componentWillUnmount() { 44 | window.removeEventListener("resize", this.onWindowResized); 45 | //authService.unsubscribe(); 46 | } 47 | 48 | shouldSidebarBeFixed() { 49 | return getWindowSize() !== WindowSize.Small; 50 | } 51 | 52 | onWindowResized = () => { 53 | if (!this.context.isSession) { 54 | this.setState({ 55 | isSidebarOpen: this.shouldSidebarBeFixed(), 56 | isSidebarFixed: this.shouldSidebarBeFixed() 57 | }); 58 | } 59 | } 60 | 61 | 62 | render() { 63 | if (this.context?.isSession) { 64 | return this.renderMainContent(false); 65 | } 66 | 67 | var gridTemplate = this.state.isSidebarFixed ? 68 | "auto 1fr" : 69 | "1fr"; 70 | 71 | return ( 72 |
73 | this.setState({ isSidebarOpen: false })} /> 77 | 78 | {this.renderMainContent(true)} 79 |
80 | ); 81 | } 82 | 83 | private renderMainContent(menuVisible: boolean) { 84 | const { 85 | isSession 86 | } = this.context; 87 | 88 | const { 89 | logoUrl, 90 | pageBackgroundColor, 91 | titleBackgroundColor, 92 | titleText, 93 | titleTextColor 94 | } = this.context.sessionInfo || {}; 95 | 96 | var menuButtonClass = this.state.isSidebarOpen ? 97 | "navbar-toggler menu-button hidden" : 98 | "navbar-toggler menu-button"; 99 | 100 | return ( 101 |
102 |
103 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | Branding Logo 118 | 119 | 120 | 125 | {titleText} 126 | 127 | 128 | 129 | 130 | 131 |
132 | 133 |
134 | 135 | {this.props.children} 136 | 137 | 138 | 139 | 140 | 141 |
142 |
143 | ) 144 | } 145 | 146 | 147 | private async setUser() { 148 | var user = await authService.getUser(); 149 | this.setState({ 150 | username: user && user.name 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/LoadingAnimation.css: -------------------------------------------------------------------------------- 1 | .signal-container { 2 | position: absolute; 3 | text-align: center; 4 | left: 50%; 5 | top: 45vh; 6 | transform: translate(-50%, -50%) 7 | } 8 | .signal { 9 | display: inline-block; 10 | border: 5px solid #333; 11 | border-radius: 30px; 12 | height: 30px; 13 | opacity: 0; 14 | width: 30px; 15 | animation: pulsate 1s ease-out; 16 | animation-iteration-count: infinite; 17 | } 18 | 19 | @keyframes pulsate { 20 | 0% { 21 | transform: scale(.1); 22 | opacity: 0.0; 23 | } 24 | 25 | 50% { 26 | opacity: 1; 27 | } 28 | 29 | 100% { 30 | transform: scale(1.2); 31 | opacity: 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/LoadingAnimation.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import "./LoadingAnimation.css" 3 | 4 | interface LoadingAnimationProps { 5 | message: string; 6 | } 7 | 8 | export class LoadingAnimation extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 |

14 | {this.props.message} 15 |

16 |
17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/NavMenu.css: -------------------------------------------------------------------------------- 1 | a.navbar-brand { 2 | white-space: normal; 3 | text-align: center; 4 | word-break: break-all; 5 | } 6 | 7 | html { 8 | font-size: 14px; 9 | } 10 | @media (min-width: 768px) { 11 | html { 12 | font-size: 16px; 13 | } 14 | } 15 | 16 | .box-shadow { 17 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 18 | } 19 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Session.css: -------------------------------------------------------------------------------- 1 | .session-grid { 2 | display: grid; 3 | grid-template-rows: auto 1fr; 4 | grid-template-columns: 3fr 2fr; 5 | height: 100%; 6 | padding-bottom: 30px; 7 | grid-row-gap: 10px; 8 | grid-column-gap: 10px; 9 | } 10 | 11 | .thumbnail-banner { 12 | display: flex; 13 | flex-direction: row; 14 | overflow-x: auto; 15 | grid-column: 1 / span 2; 16 | } 17 | 18 | .thumbnail-video-wrapper { 19 | position: relative; 20 | height: 100px; 21 | width: 130px; 22 | margin-right: 10px; 23 | overflow: hidden; 24 | } 25 | 26 | .thumbnail-video { 27 | position: absolute; 28 | height: 100px; 29 | width: 130px; 30 | object-fit: cover; 31 | cursor: pointer; 32 | background-color: black; 33 | } 34 | 35 | .chat-messages-wrapper { 36 | position: relative; 37 | display: grid; 38 | grid-template-rows: 1fr auto; 39 | grid-row-gap: 5px; 40 | color: black; 41 | } 42 | 43 | .chat-messages-window { 44 | height: 100%; 45 | width: 100%; 46 | position: absolute; 47 | padding: 5px; 48 | overflow-y: auto; 49 | overflow-x: hidden; 50 | background-color: whitesmoke; 51 | border-radius: 5px; 52 | word-break: break-word; 53 | } 54 | 55 | .nameplate { 56 | position: absolute; 57 | bottom: 2px; 58 | right: 2px; 59 | text-align: center; 60 | color: white; 61 | background-color: rgba(0, 0, 0, 0.7); 62 | padding: 1px 4px; 63 | user-select: none; 64 | pointer-events: none; 65 | border-radius: 3px; 66 | white-space: nowrap; 67 | } 68 | 69 | .share-screen-button { 70 | position: absolute; 71 | bottom: 2px; 72 | left: 2px; 73 | } 74 | 75 | .main-viewing-video { 76 | position: absolute; 77 | width: 100%; 78 | height: 100%; 79 | object-fit: cover; 80 | background-color: black; 81 | } 82 | 83 | @media (max-width : 500px) { 84 | .session-grid { 85 | grid-template-rows: auto 300px 300px; 86 | grid-template-columns: 1fr; 87 | height: unset; 88 | } 89 | 90 | .thumbnail-banner { 91 | grid-column: unset; 92 | } 93 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Session.tsx: -------------------------------------------------------------------------------- 1 | import { HubConnectionState } from "@microsoft/signalr"; 2 | import React, { Component } from "react"; 3 | import { LoadingAnimation } from "./LoadingAnimation"; 4 | import { SessionInfoContext } from "../services/SessionInfoContext"; 5 | import { If } from "./If"; 6 | import { getSettings } from "../services/SettingsService"; 7 | import { Signaler } from "../services/SignalingService"; 8 | import { ChatMessage } from "../interfaces/ChatMessage"; 9 | import "./Session.css"; 10 | import { SettingsComp } from "./Settings"; 11 | import { Col, Row } from "reactstrap"; 12 | import { Peer } from "../interfaces/Peer"; 13 | import { IceCandidateMessage } from "../interfaces/IceCandidateMessage"; 14 | import { SdpMessage } from "../interfaces/SdpMessage"; 15 | 16 | interface SessionProps { 17 | 18 | } 19 | 20 | interface SessionState { 21 | mainViewingStream?: MediaStream; 22 | selectedName?: string; 23 | connectionState: HubConnectionState; 24 | localMediaStream: MediaStream; 25 | peers: Peer[]; 26 | isScreenSharing: boolean; 27 | chatMessages: ChatMessage[]; 28 | } 29 | 30 | export class Session extends Component { 31 | static contextType = SessionInfoContext; 32 | context!: React.ContextType; 33 | 34 | constructor(props: SessionProps) { 35 | super(props); 36 | this.state = { 37 | selectedName: "", 38 | chatMessages: [], 39 | connectionState: Signaler.connectionState, 40 | isScreenSharing: false, 41 | peers: [], 42 | localMediaStream: new MediaStream() 43 | } 44 | } 45 | 46 | async componentDidMount() { 47 | Signaler.onConnectionStateChanged.subscribe(this.handleConnectionStateChanged); 48 | Signaler.onSdpReceived.subscribe(this.handleSdpReceived); 49 | Signaler.onIceCandidateReceived.subscribe(this.handleIceCandidateReceived); 50 | Signaler.onChatMessageReceived.subscribe(this.handleChatMessageReceived); 51 | Signaler.onPeerLeft.subscribe(this.handlePeerLeft); 52 | 53 | if (Signaler.connectionState == HubConnectionState.Connecting) { 54 | await Signaler.connect(); 55 | } 56 | else if (this.context.sessionInfo && this.context.sessionJoined) { 57 | await this.initLocalMedia(); 58 | await this.updatePeers(); 59 | await this.sendOffers(); 60 | } 61 | } 62 | 63 | componentWillUnmount() { 64 | Signaler.onConnectionStateChanged.unsubscribe(this.handleConnectionStateChanged); 65 | Signaler.onSdpReceived.unsubscribe(this.handleSdpReceived); 66 | Signaler.onIceCandidateReceived.unsubscribe(this.handleIceCandidateReceived); 67 | Signaler.onChatMessageReceived.unsubscribe(this.handleChatMessageReceived); 68 | Signaler.onPeerLeft.unsubscribe(this.handlePeerLeft); 69 | } 70 | 71 | public toggleShareScreen = async () => { 72 | const { 73 | localMediaStream, 74 | isScreenSharing, 75 | peers 76 | } = this.state; 77 | 78 | try { 79 | if (!(navigator.mediaDevices as any).getDisplayMedia) { 80 | alert("Screen sharing is not supported on this browser/device."); 81 | return; 82 | } 83 | 84 | localMediaStream.getVideoTracks().forEach(x => { 85 | localMediaStream.removeTrack(x); 86 | x.stop(); 87 | }); 88 | 89 | if (!isScreenSharing) { 90 | var displayMedia = await (navigator.mediaDevices as any).getDisplayMedia({ 91 | video:true 92 | }); 93 | displayMedia.getVideoTracks().forEach((x:any) => { 94 | localMediaStream.addTrack(x); 95 | }) 96 | } 97 | else { 98 | await this.loadVideoStream(); 99 | } 100 | 101 | var newVideoTrack = localMediaStream.getVideoTracks()[0]; 102 | 103 | peers.forEach(peer => { 104 | peer.peerConnection?.getSenders().forEach(sender =>{ 105 | if (sender.track?.kind == "video") { 106 | sender.replaceTrack(newVideoTrack); 107 | } 108 | }) 109 | }) 110 | } 111 | catch (ex) { 112 | console.error(ex); 113 | } 114 | finally { 115 | this.setState({ 116 | isScreenSharing: !isScreenSharing 117 | }); 118 | } 119 | } 120 | 121 | 122 | private addLocalMediaTracks = (pc: RTCPeerConnection) => { 123 | console.log("Adding tracks"); 124 | this.state.localMediaStream.getTracks().forEach(track => { 125 | pc?.addTrack(track, this.state.localMediaStream); 126 | }); 127 | } 128 | 129 | private configurePeerConnection = (pc: RTCPeerConnection, peerId: string) => { 130 | const { 131 | peers 132 | } = this.state; 133 | 134 | console.log("Configure peer connection for ID: ", peerId); 135 | pc.addEventListener("icecandidate", ev => { 136 | if (ev.candidate) { 137 | Signaler.sendIceCandidate(peerId, ev.candidate); 138 | } 139 | }); 140 | pc.addEventListener("connectionstatechange", ev => { 141 | console.log(`PeerConnection state changed to ${pc.connectionState} for peer ${peerId}.`); 142 | if (pc.connectionState == "closed" || 143 | pc.connectionState == "failed") { 144 | // TODO: Check with signaling server if peer is still there. 145 | } 146 | }); 147 | pc.addEventListener("iceconnectionstatechange", ev => { 148 | console.log(`ICE connection state changed to ${pc.iceConnectionState} for peer ${peerId}.`); 149 | if (pc.iceConnectionState == "failed" || 150 | pc.iceConnectionState == "closed") { 151 | // TOD: Check with signaling server if peer is still there. 152 | } 153 | }) 154 | pc.addEventListener("negotiationneeded", async (ev) => { 155 | // TODO: Handle offer collisions politely. 156 | console.log("Negotation needed."); 157 | var offer = await pc.createOffer(); 158 | await pc.setLocalDescription(offer); 159 | console.log("Sending renegotiation offer: ", pc.localDescription); 160 | await Signaler.sendSdp(peerId, getSettings().displayName, pc.localDescription); 161 | }); 162 | pc.addEventListener("track", ev => { 163 | console.log("Track received: ", ev.track); 164 | let peer = peers.find(x => x.signalingId == peerId); 165 | if (!peer){ 166 | console.error("Peer not found for ID ", peerId); 167 | return; 168 | } 169 | if (peer.remoteMediaStream) { 170 | peer.remoteMediaStream.addTrack(ev.track); 171 | } 172 | else { 173 | peer.remoteMediaStream = new MediaStream([ev.track]); 174 | } 175 | this.forceUpdate(); 176 | }); 177 | } 178 | 179 | private handleChatMessageReceived = async (message: ChatMessage) => { 180 | console.log("Chat message received."); 181 | this.setState({ 182 | chatMessages: [...this.state.chatMessages, message] 183 | }) 184 | } 185 | 186 | private handleConnectionStateChanged = async (connectionState: HubConnectionState) => { 187 | console.log("WebSocket connection state changed: ", connectionState); 188 | this.setState({ 189 | connectionState: connectionState 190 | }); 191 | 192 | if (connectionState == HubConnectionState.Connected) { 193 | this.setState({ 194 | peers: [] 195 | }) 196 | 197 | if (!this.state.localMediaStream?.getTracks()) { 198 | return; 199 | } 200 | 201 | this.context.getSessionInfo(); 202 | } 203 | } 204 | 205 | private handleIceCandidateReceived = (iceMessage: IceCandidateMessage) => { 206 | console.log("ICE candidate received: ", iceMessage); 207 | var peer = this.state.peers.find(x => x.signalingId == iceMessage.peerId); 208 | if (peer && peer.peerConnection) { 209 | try { 210 | peer.peerConnection.addIceCandidate(iceMessage.iceCandidate); 211 | } 212 | catch (ex) { 213 | console.warn("Failed to set ICE candidate. ", ex); 214 | } 215 | } 216 | else { 217 | console.log(`Peer ID ${iceMessage.peerId} not found in `, this.state.peers); 218 | } 219 | } 220 | 221 | private handlePeerLeft = (peerId: string) => { 222 | console.log("Peer left: ", peerId); 223 | this.setState({ 224 | peers: this.state.peers.filter(x => x.signalingId != peerId) 225 | }); 226 | } 227 | 228 | private handleSdpReceived = async (sdpMessage: SdpMessage) => { 229 | console.log("Received SDP: ", sdpMessage); 230 | var peer = this.state.peers.find(x=>x.signalingId == sdpMessage.signalingId); 231 | 232 | if (sdpMessage.description.type == "offer") { 233 | var iceServers = await Signaler.getIceServers(); 234 | 235 | var pc = peer?.peerConnection || new RTCPeerConnection({ 236 | iceServers: iceServers 237 | }); 238 | 239 | if (!peer || !peer.peerConnection) { 240 | this.addLocalMediaTracks(pc); 241 | peer = { 242 | signalingId: sdpMessage.signalingId, 243 | displayName: sdpMessage.displayName, 244 | peerConnection: pc 245 | }; 246 | 247 | this.setState({ 248 | peers: [...this.state.peers, peer] 249 | }) 250 | 251 | this.configurePeerConnection(pc, sdpMessage.signalingId); 252 | } 253 | 254 | await pc.setRemoteDescription(sdpMessage.description); 255 | var answer = await pc.createAnswer(); 256 | await pc.setLocalDescription(answer); 257 | 258 | console.log("Sending SDP answer to: ", sdpMessage.signalingId); 259 | await Signaler.sendSdp(sdpMessage.signalingId, getSettings().displayName, pc.localDescription); 260 | } 261 | else if (sdpMessage.description.type == "answer") { 262 | if (!peer) { 263 | console.error(`Unable to find peer with ID ${sdpMessage.signalingId}.`); 264 | return; 265 | } 266 | peer.displayName = sdpMessage.displayName; 267 | peer.signalingId = sdpMessage.signalingId; 268 | await peer.peerConnection?.setRemoteDescription(sdpMessage.description); 269 | } 270 | else { 271 | console.error("Unhandled SDP type.", sdpMessage); 272 | } 273 | } 274 | 275 | private initLocalMedia = async () => { 276 | await this.loadAudioStream(); 277 | await this.loadVideoStream(); 278 | } 279 | 280 | private loadAudioStream = async () => { 281 | try { 282 | let settings = getSettings(); 283 | let audioStream = await navigator.mediaDevices.getUserMedia({ 284 | audio: { 285 | deviceId: { 286 | ideal: settings.defaultAudioInput 287 | } 288 | } 289 | }); 290 | audioStream.getTracks().forEach(x => { 291 | this.state.localMediaStream.addTrack(x); 292 | }) 293 | } 294 | catch { 295 | console.warn("Failed to get audio device."); 296 | } 297 | } 298 | 299 | private loadVideoStream = async () => { 300 | try { 301 | let settings = getSettings(); 302 | let videoStream = await navigator.mediaDevices.getUserMedia({ 303 | video: { 304 | deviceId: { 305 | ideal: settings.defaultVideoInput 306 | } 307 | } 308 | }); 309 | videoStream.getTracks().forEach(x => { 310 | this.state.localMediaStream.addTrack(x); 311 | }) 312 | } 313 | catch { 314 | console.warn("Failed to get video device."); 315 | } 316 | } 317 | 318 | 319 | private sendOffers = async () => { 320 | const iceServers = await Signaler.getIceServers(); 321 | this.state.peers.forEach(x => { 322 | x.peerConnection = new RTCPeerConnection({ 323 | iceServers: iceServers 324 | }); 325 | 326 | this.configurePeerConnection(x.peerConnection, x.signalingId); 327 | 328 | this.addLocalMediaTracks(x.peerConnection); 329 | }) 330 | } 331 | 332 | private updatePeers = async () => { 333 | var peerIds = await Signaler.getPeers(); 334 | 335 | peerIds.forEach(x => { 336 | if (!this.state.peers.some(y => y.signalingId == x)) { 337 | this.setState({ 338 | peers: [...this.state.peers, {signalingId: x}] 339 | }) 340 | } 341 | }) 342 | } 343 | 344 | render() { 345 | const { 346 | sessionChecked, 347 | sessionInfo, 348 | sessionJoined, 349 | } = this.context; 350 | 351 | const { 352 | chatMessages, 353 | connectionState, 354 | localMediaStream, 355 | peers, 356 | isScreenSharing 357 | } = this.state; 358 | 359 | switch (connectionState) { 360 | 361 | case HubConnectionState.Connecting: 362 | return ( 363 | 364 | ) 365 | case HubConnectionState.Reconnecting: 366 | return ( 367 | 368 | ) 369 | case HubConnectionState.Disconnected: 370 | return ( 371 |
372 |

Disconnected.

373 |
Please refresh the browser to try again.
374 |
375 | ) 376 | default: 377 | break; 378 | } 379 | 380 | if (!sessionChecked) { 381 | return ( 382 |

Finding session...

383 | ) 384 | } 385 | 386 | if (!sessionInfo) { 387 | return ( 388 |

Session ID not found.

389 | ) 390 | } 391 | 392 | if (!sessionJoined) { 393 | return ( 394 |
395 | 396 | 397 |
398 | Adjust your settings below, then join! 399 |
400 |
401 | 407 |
408 | 409 |
410 | 411 | 412 |
413 | ) 414 | } 415 | 416 | let { 417 | mainViewingStream, 418 | selectedName 419 | } = this.state; 420 | 421 | if (!mainViewingStream && peers?.length > 0) { 422 | mainViewingStream = peers[0].remoteMediaStream 423 | selectedName = peers[0].displayName; 424 | } 425 | 426 | let settings = getSettings(); 427 | 428 | return ( 429 |
430 |
431 |
432 |
459 | 460 | 461 | {peers.map((x, index) => ( 462 |
465 |
495 | ))} 496 |
497 | 498 |
499 | 500 | 522 | 523 | 530 | 531 | 532 |

533 | Select a video feed. 534 |

535 |
536 |
537 | 538 |
539 |
540 |
{ 542 | if (ref) { 543 | ref.scrollTo(0, ref.scrollHeight); 544 | } 545 | }} 546 | className="chat-messages-window"> 547 | {chatMessages.map((x, index) => ( 548 |
549 | 550 | {x.senderDisplayName} 551 | 552 | 553 | {` (${x.timestamp}): `} 554 | 555 | {x.message} 556 |
557 | ))} 558 |
559 | 560 |
561 | 562 | { 566 | if (ev.key.toLowerCase() == "enter") { 567 | var messageText = ev.currentTarget.value; 568 | Signaler.sendChatMessage(messageText, settings.displayName); 569 | ev.currentTarget.value = ""; 570 | } 571 | }} /> 572 |
573 |
574 | ) 575 | } 576 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Form, 5 | FormGroup, 6 | Input, 7 | Label, 8 | Progress, 9 | Row 10 | } from 'reactstrap'; 11 | import { getSettings, saveSettings } from '../services/SettingsService'; 12 | import { 13 | enumerateAudioInputs, 14 | enumerateVideoInputs, 15 | loadAudioDevice, 16 | loadVideoDevice, 17 | enumerateAudioOuputs 18 | } from '../utils/MediaHelper'; 19 | import { If } from './If'; 20 | 21 | interface SettingsProps { 22 | 23 | } 24 | 25 | interface SettingsState { 26 | videoInputs: MediaDeviceInfo[]; 27 | audioInputs: MediaDeviceInfo[]; 28 | audioOutputs: MediaDeviceInfo[]; 29 | selectedVideoInput?: string; 30 | selectedAudioInput?: string; 31 | selectedAudioOutput?: string; 32 | videoStream?: MediaStream; 33 | audioStream?: MediaStream; 34 | displayName?: string; 35 | audioContext?: AudioContext; 36 | audioProcessor?: ScriptProcessorNode; 37 | audioStreamSource?: MediaStreamAudioSourceNode; 38 | } 39 | 40 | export class SettingsComp extends Component { 41 | static displayName = SettingsComp.name; 42 | 43 | private audioLevelProgress: React.RefObject; 44 | 45 | constructor(props: SettingsProps) { 46 | super(props); 47 | const settings = getSettings(); 48 | this.state = { 49 | audioInputs: [], 50 | audioOutputs: [], 51 | videoInputs: [], 52 | displayName: settings.displayName 53 | } 54 | this.audioLevelProgress = React.createRef(); 55 | } 56 | 57 | async componentDidMount() { 58 | await this.initVideoInputs(); 59 | await this.initAudioInputs(); 60 | await this.initAudioOutputs(); 61 | } 62 | 63 | initAudioInputs = async () => { 64 | try { 65 | const settings = getSettings(); 66 | 67 | let mics = await enumerateAudioInputs(); 68 | this.setState({ 69 | audioInputs: mics 70 | }); 71 | 72 | let selectedAudio = mics.find(x => 73 | x.deviceId == settings.defaultAudioInput && 74 | mics.some(mic => mic.deviceId == x.deviceId)) || mics[0]; 75 | 76 | if (selectedAudio) { 77 | this.loadSelectedAudioDevice(selectedAudio.deviceId); 78 | } 79 | } 80 | catch (ex) { 81 | console.error(ex); 82 | alert("Failed to initialize audio devices."); 83 | } 84 | } 85 | 86 | initAudioOutputs = async () => { 87 | try { 88 | const settings = getSettings(); 89 | 90 | let speakers = await enumerateAudioOuputs(); 91 | this.setState({ 92 | audioOutputs: speakers 93 | }); 94 | 95 | let selectedOutput = speakers.find(x => 96 | x.deviceId == settings.defaultAudioOutput && 97 | speakers.some(speaker => speaker.deviceId == x.deviceId)) || speakers[0]; 98 | 99 | if (selectedOutput) { 100 | this.setState({ 101 | selectedAudioOutput: selectedOutput.deviceId 102 | }) 103 | } 104 | } 105 | catch (ex) { 106 | console.error(ex); 107 | alert("Failed to initialize audio devices."); 108 | } 109 | } 110 | 111 | initVideoInputs = async () => { 112 | try { 113 | const settings = getSettings(); 114 | 115 | let cameras = await enumerateVideoInputs(); 116 | this.setState({ 117 | videoInputs: cameras 118 | }); 119 | 120 | let selectedVideo = cameras.find(x => 121 | x.deviceId == settings.defaultVideoInput && 122 | cameras.some(cam => cam.deviceId == x.deviceId)) || cameras[0]; 123 | 124 | if (selectedVideo) { 125 | this.loadSelectedVideoDevice(selectedVideo.deviceId); 126 | } 127 | } 128 | catch (ex) { 129 | console.error(ex); 130 | alert("Failed to initialize video devices."); 131 | } 132 | } 133 | 134 | loadSelectedAudioDevice = (deviceId: string) => { 135 | try { 136 | this.setState({ 137 | selectedAudioInput: deviceId 138 | }); 139 | 140 | this.state.audioStream?.getAudioTracks().forEach(x => { 141 | x.stop(); 142 | }); 143 | 144 | loadAudioDevice(deviceId).then(audioStream => { 145 | 146 | if (this.state.audioProcessor) { 147 | this.state.audioProcessor.removeEventListener("audioprocess", this.processAudio); 148 | } 149 | 150 | let audioContext = new AudioContext(); 151 | let scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1); 152 | scriptProcessor.addEventListener("audioprocess", this.processAudio); 153 | 154 | let streamSource = audioContext.createMediaStreamSource(audioStream); 155 | streamSource.connect(scriptProcessor); 156 | scriptProcessor.connect(audioContext.destination); 157 | 158 | this.setState({ 159 | audioStream: audioStream, 160 | audioContext: audioContext, 161 | audioProcessor: scriptProcessor, 162 | audioStreamSource: streamSource 163 | }); 164 | 165 | }).catch(error => { 166 | console.error(error); 167 | alert("Failed to get audio stream."); 168 | }) 169 | } 170 | catch { 171 | alert("Failed to load audio device."); 172 | } 173 | 174 | } 175 | 176 | loadSelectedVideoDevice = (deviceId: string) => { 177 | try { 178 | this.setState({ 179 | selectedVideoInput: deviceId 180 | }); 181 | this.state.videoStream?.getVideoTracks().forEach(x => { 182 | x.stop(); 183 | }); 184 | loadVideoDevice(deviceId).then(videoStream => { 185 | this.setState({ 186 | videoStream: videoStream 187 | }) 188 | }).catch(reason => { 189 | console.error(reason); 190 | alert("Failed to get video stream."); 191 | }) 192 | } 193 | catch { 194 | alert("Failed to load video device."); 195 | } 196 | } 197 | 198 | processAudio = (ev: AudioProcessingEvent) => { 199 | if (this.audioLevelProgress?.current) { 200 | const input = ev.inputBuffer.getChannelData(0); 201 | let sum = 0.0; 202 | for (var i = 0; i < input.length; ++i) { 203 | sum += input[i] * input[i]; 204 | } 205 | let audioLevel = Math.sqrt(sum / input.length); 206 | this.audioLevelProgress.current.value = audioLevel * 2; 207 | } 208 | } 209 | 210 | 211 | render() { 212 | return ( 213 | 214 | 215 |

General

216 |
217 | 218 | { 222 | const settings = getSettings(); 223 | saveSettings({ 224 | defaultAudioInput: settings.defaultAudioInput, 225 | defaultVideoInput: settings.defaultVideoInput, 226 | defaultAudioOutput: settings.defaultAudioOutput, 227 | displayName: ev.target.value 228 | }) 229 | }} /> 230 |
231 |

Default Devices

232 |
233 | 234 | 257 |
258 | 259 | 260 |
261 |
274 |
275 | 276 |
277 | 278 | 301 |
302 |
303 | 311 |
312 | 313 |
314 | 315 | 340 |
341 | 342 |
343 | ); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar-frame { 2 | position: absolute; 3 | display: flex; 4 | flex-direction: column; 5 | left: 0; 6 | top: 0; 7 | height: 100%; 8 | width: 0; 9 | overflow-x: hidden; 10 | overflow-y: auto; 11 | text-align: center; 12 | background-color: rgb(50,50,50); 13 | opacity: 0; 14 | z-index: 3; 15 | transition: .25s ease all; 16 | } 17 | 18 | .sidebar-frame.open { 19 | width: 200px; 20 | opacity: 1; 21 | transition: .25s ease all; 22 | } 23 | 24 | .sidebar-frame.fixed { 25 | position: unset; 26 | } 27 | 28 | .sidebar-frame button { 29 | white-space: nowrap; 30 | } 31 | 32 | a.navbar-brand { 33 | white-space: normal; 34 | text-align: center; 35 | word-break: break-all; 36 | } 37 | 38 | html { 39 | font-size: 14px; 40 | } 41 | @media (min-width: 768px) { 42 | html { 43 | font-size: 16px; 44 | } 45 | } 46 | 47 | .box-shadow { 48 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 49 | } 50 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink } from 'reactstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import "./Sidebar.css" 5 | import { FaHome, FaWrench } from 'react-icons/fa'; 6 | 7 | interface SidebarProps { 8 | isOpen: boolean; 9 | isFixed: boolean; 10 | onSidebarClosed: () => void; 11 | } 12 | 13 | interface SidebarState { } 14 | 15 | export class Sidebar extends Component { 16 | render() { 17 | 18 | var sidebarClass = "sidebar-frame"; 19 | if (this.props.isOpen) { 20 | sidebarClass += " open"; 21 | } 22 | 23 | if (this.props.isFixed) { 24 | sidebarClass += " fixed"; 25 | } 26 | 27 | return ( 28 |
29 |
30 | 31 |

32 | Confer 33 |

34 | 35 |
36 | 39 |
40 | 41 |
42 | 43 | 49 | 55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/ViewComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EventEmitterEx } from '../utils/EventEmitterEx'; 3 | 4 | interface ViewModel { 5 | stateUpdated: EventEmitterEx; 6 | } 7 | 8 | interface ViewComponentProps> { 9 | viewModel: T; 10 | viewContext: React.Context; 11 | } 12 | 13 | 14 | export class ViewComponent> extends React.Component, T> { 15 | constructor(props: ViewComponentProps) { 16 | super(props); 17 | this.state = props.viewModel; 18 | this.state.stateUpdated.subscribe((updatedViewModel) => { 19 | this.setState({ 20 | ...updatedViewModel 21 | }) 22 | }); 23 | } 24 | render() { 25 | const ViewContext = React.createContext(this.state); 26 | return ( 27 | 28 | {this.props.children} 29 | 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/ApiAuthorizationConstants.js: -------------------------------------------------------------------------------- 1 | export const ApplicationName = 'Confer'; 2 | 3 | export const QueryParameterNames = { 4 | ReturnUrl: 'returnUrl', 5 | Message: 'message' 6 | }; 7 | 8 | export const LogoutActions = { 9 | LogoutCallback: 'logout-callback', 10 | Logout: 'logout', 11 | LoggedOut: 'logged-out' 12 | }; 13 | 14 | export const LoginActions = { 15 | Login: 'login', 16 | LoginCallback: 'login-callback', 17 | LoginFailed: 'login-failed', 18 | Profile: 'profile', 19 | Register: 'register' 20 | }; 21 | 22 | const prefix = '/authentication'; 23 | 24 | export const ApplicationPaths = { 25 | DefaultLoginRedirectPath: '/', 26 | ApiAuthorizationClientConfigurationUrl: `_configuration/${ApplicationName}`, 27 | ApiAuthorizationPrefix: prefix, 28 | Login: `${prefix}/${LoginActions.Login}`, 29 | LoginFailed: `${prefix}/${LoginActions.LoginFailed}`, 30 | LoginCallback: `${prefix}/${LoginActions.LoginCallback}`, 31 | Register: `${prefix}/${LoginActions.Register}`, 32 | Profile: `${prefix}/${LoginActions.Profile}`, 33 | LogOut: `${prefix}/${LogoutActions.Logout}`, 34 | LoggedOut: `${prefix}/${LogoutActions.LoggedOut}`, 35 | LogOutCallback: `${prefix}/${LogoutActions.LogoutCallback}`, 36 | IdentityRegisterPath: 'Identity/Account/Register', 37 | IdentityManagePath: 'Identity/Account/Manage' 38 | }; 39 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/ApiAuthorizationRoutes.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Route } from 'react-router'; 3 | import { Login } from './Login' 4 | import { Logout } from './Logout' 5 | import { ApplicationPaths, LoginActions, LogoutActions } from './ApiAuthorizationConstants'; 6 | 7 | export default class ApiAuthorizationRoutes extends Component { 8 | 9 | render () { 10 | return( 11 | 12 | loginAction(LoginActions.Login)} /> 13 | loginAction(LoginActions.LoginFailed)} /> 14 | loginAction(LoginActions.LoginCallback)} /> 15 | loginAction(LoginActions.Profile)} /> 16 | loginAction(LoginActions.Register)} /> 17 | logoutAction(LogoutActions.Logout)} /> 18 | logoutAction(LogoutActions.LogoutCallback)} /> 19 | logoutAction(LogoutActions.LoggedOut)} /> 20 | ); 21 | } 22 | } 23 | 24 | function loginAction(name){ 25 | return (); 26 | } 27 | 28 | function logoutAction(name) { 29 | return (); 30 | } 31 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/AuthorizeRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react' 3 | import { Route, Redirect } from 'react-router-dom' 4 | import { ApplicationPaths, QueryParameterNames } from './ApiAuthorizationConstants' 5 | import authService from './AuthorizeService' 6 | 7 | export default class AuthorizeRoute extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | ready: false, 13 | authenticated: false 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this._subscription = authService.subscribe(() => this.authenticationChanged()); 19 | this.populateAuthenticationState(); 20 | } 21 | 22 | componentWillUnmount() { 23 | authService.unsubscribe(this._subscription); 24 | } 25 | 26 | render() { 27 | const { ready, authenticated } = this.state; 28 | var link = document.createElement("a"); 29 | link.href = this.props.path; 30 | const returnUrl = `${link.protocol}//${link.host}${link.pathname}${link.search}${link.hash}`; 31 | const redirectUrl = `${ApplicationPaths.Login}?${QueryParameterNames.ReturnUrl}=${encodeURI(returnUrl)}` 32 | if (!ready) { 33 | return
; 34 | } else { 35 | const { component: Component, ...rest } = this.props; 36 | return { 38 | if (authenticated) { 39 | return 40 | } else { 41 | return 42 | } 43 | }} /> 44 | } 45 | } 46 | 47 | async populateAuthenticationState() { 48 | const authenticated = await authService.isAuthenticated(); 49 | this.setState({ ready: true, authenticated }); 50 | } 51 | 52 | async authenticationChanged() { 53 | this.setState({ ready: false, authenticated: false }); 54 | await this.populateAuthenticationState(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/AuthorizeService.js: -------------------------------------------------------------------------------- 1 | import { UserManager, WebStorageStateStore } from 'oidc-client'; 2 | import { ApplicationPaths, ApplicationName } from './ApiAuthorizationConstants'; 3 | 4 | export class AuthorizeService { 5 | _callbacks = []; 6 | _nextSubscriptionId = 0; 7 | _user = null; 8 | _isAuthenticated = false; 9 | 10 | // By default pop ups are disabled because they don't work properly on Edge. 11 | // If you want to enable pop up authentication simply set this flag to false. 12 | _popUpDisabled = true; 13 | 14 | async isAuthenticated() { 15 | const user = await this.getUser(); 16 | return !!user; 17 | } 18 | 19 | async getUser() { 20 | if (this._user && this._user.profile) { 21 | return this._user.profile; 22 | } 23 | 24 | await this.ensureUserManagerInitialized(); 25 | const user = await this.userManager.getUser(); 26 | return user && user.profile; 27 | } 28 | 29 | async getAccessToken() { 30 | await this.ensureUserManagerInitialized(); 31 | const user = await this.userManager.getUser(); 32 | return user && user.access_token; 33 | } 34 | 35 | // We try to authenticate the user in three different ways: 36 | // 1) We try to see if we can authenticate the user silently. This happens 37 | // when the user is already logged in on the IdP and is done using a hidden iframe 38 | // on the client. 39 | // 2) We try to authenticate the user using a PopUp Window. This might fail if there is a 40 | // Pop-Up blocker or the user has disabled PopUps. 41 | // 3) If the two methods above fail, we redirect the browser to the IdP to perform a traditional 42 | // redirect flow. 43 | async signIn(state) { 44 | await this.ensureUserManagerInitialized(); 45 | try { 46 | const silentUser = await this.userManager.signinSilent(this.createArguments()); 47 | this.updateState(silentUser); 48 | return this.success(state); 49 | } catch (silentError) { 50 | // User might not be authenticated, fallback to popup authentication 51 | console.log("Silent authentication error: ", silentError); 52 | 53 | try { 54 | if (this._popUpDisabled) { 55 | throw new Error('Popup disabled. Change \'AuthorizeService.js:AuthorizeService._popupDisabled\' to false to enable it.') 56 | } 57 | 58 | const popUpUser = await this.userManager.signinPopup(this.createArguments()); 59 | this.updateState(popUpUser); 60 | return this.success(state); 61 | } catch (popUpError) { 62 | if (popUpError.message === "Popup window closed") { 63 | // The user explicitly cancelled the login action by closing an opened popup. 64 | return this.error("The user closed the window."); 65 | } else if (!this._popUpDisabled) { 66 | console.log("Popup authentication error: ", popUpError); 67 | } 68 | 69 | // PopUps might be blocked by the user, fallback to redirect 70 | try { 71 | await this.userManager.signinRedirect(this.createArguments(state)); 72 | return this.redirect(); 73 | } catch (redirectError) { 74 | console.log("Redirect authentication error: ", redirectError); 75 | return this.error(redirectError); 76 | } 77 | } 78 | } 79 | } 80 | 81 | async completeSignIn(url) { 82 | try { 83 | await this.ensureUserManagerInitialized(); 84 | const user = await this.userManager.signinCallback(url); 85 | this.updateState(user); 86 | return this.success(user && user.state); 87 | } catch (error) { 88 | console.log('There was an error signing in: ', error); 89 | return this.error('There was an error signing in.'); 90 | } 91 | } 92 | 93 | // We try to sign out the user in two different ways: 94 | // 1) We try to do a sign-out using a PopUp Window. This might fail if there is a 95 | // Pop-Up blocker or the user has disabled PopUps. 96 | // 2) If the method above fails, we redirect the browser to the IdP to perform a traditional 97 | // post logout redirect flow. 98 | async signOut(state) { 99 | await this.ensureUserManagerInitialized(); 100 | try { 101 | if (this._popUpDisabled) { 102 | throw new Error('Popup disabled. Change \'AuthorizeService.js:AuthorizeService._popupDisabled\' to false to enable it.') 103 | } 104 | 105 | await this.userManager.signoutPopup(this.createArguments()); 106 | this.updateState(undefined); 107 | return this.success(state); 108 | } catch (popupSignOutError) { 109 | console.log("Popup signout error: ", popupSignOutError); 110 | try { 111 | await this.userManager.signoutRedirect(this.createArguments(state)); 112 | return this.redirect(); 113 | } catch (redirectSignOutError) { 114 | console.log("Redirect signout error: ", redirectSignOutError); 115 | return this.error(redirectSignOutError); 116 | } 117 | } 118 | } 119 | 120 | async completeSignOut(url) { 121 | await this.ensureUserManagerInitialized(); 122 | try { 123 | const response = await this.userManager.signoutCallback(url); 124 | this.updateState(null); 125 | return this.success(response && response.data); 126 | } catch (error) { 127 | console.log(`There was an error trying to log out '${error}'.`); 128 | return this.error(error); 129 | } 130 | } 131 | 132 | updateState(user) { 133 | this._user = user; 134 | this._isAuthenticated = !!this._user; 135 | this.notifySubscribers(); 136 | } 137 | 138 | subscribe(callback) { 139 | this._callbacks.push({ callback, subscription: this._nextSubscriptionId++ }); 140 | return this._nextSubscriptionId - 1; 141 | } 142 | 143 | unsubscribe(subscriptionId) { 144 | const subscriptionIndex = this._callbacks 145 | .map((element, index) => element.subscription === subscriptionId ? { found: true, index } : { found: false }) 146 | .filter(element => element.found === true); 147 | if (subscriptionIndex.length !== 1) { 148 | throw new Error(`Found an invalid number of subscriptions ${subscriptionIndex.length}`); 149 | } 150 | 151 | this._callbacks.splice(subscriptionIndex[0].index, 1); 152 | } 153 | 154 | notifySubscribers() { 155 | for (let i = 0; i < this._callbacks.length; i++) { 156 | const callback = this._callbacks[i].callback; 157 | callback(); 158 | } 159 | } 160 | 161 | createArguments(state) { 162 | return { useReplaceToNavigate: true, data: state }; 163 | } 164 | 165 | error(message) { 166 | return { status: AuthenticationResultStatus.Fail, message }; 167 | } 168 | 169 | success(state) { 170 | return { status: AuthenticationResultStatus.Success, state }; 171 | } 172 | 173 | redirect() { 174 | return { status: AuthenticationResultStatus.Redirect }; 175 | } 176 | 177 | async ensureUserManagerInitialized() { 178 | if (this.userManager !== undefined) { 179 | return; 180 | } 181 | 182 | let response = await fetch(ApplicationPaths.ApiAuthorizationClientConfigurationUrl); 183 | if (!response.ok) { 184 | throw new Error(`Could not load settings for '${ApplicationName}'`); 185 | } 186 | 187 | let settings = await response.json(); 188 | settings.automaticSilentRenew = true; 189 | settings.includeIdTokenInSilentRenew = true; 190 | settings.userStore = new WebStorageStateStore({ 191 | prefix: ApplicationName 192 | }); 193 | 194 | this.userManager = new UserManager(settings); 195 | 196 | this.userManager.events.addUserSignedOut(async () => { 197 | await this.userManager.removeUser(); 198 | this.updateState(undefined); 199 | }); 200 | } 201 | 202 | static get instance() { return authService } 203 | } 204 | 205 | const authService = new AuthorizeService(); 206 | 207 | export default authService; 208 | 209 | export const AuthenticationResultStatus = { 210 | Redirect: 'redirect', 211 | Success: 'success', 212 | Fail: 'fail' 213 | }; 214 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react'; 3 | import authService from './AuthorizeService'; 4 | import { AuthenticationResultStatus } from './AuthorizeService'; 5 | import { LoginActions, QueryParameterNames, ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | // The main responsibility of this component is to handle the user's login process. 8 | // This is the starting point for the login process. Any component that needs to authenticate 9 | // a user can simply perform a redirect to this component with a returnUrl query parameter and 10 | // let the component perform the login and return back to the return url. 11 | export class Login extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | message: undefined 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | const action = this.props.action; 22 | switch (action) { 23 | case LoginActions.Login: 24 | this.login(this.getReturnUrl()); 25 | break; 26 | case LoginActions.LoginCallback: 27 | this.processLoginCallback(); 28 | break; 29 | case LoginActions.LoginFailed: 30 | const params = new URLSearchParams(window.location.search); 31 | const error = params.get(QueryParameterNames.Message); 32 | this.setState({ message: error }); 33 | break; 34 | case LoginActions.Profile: 35 | this.redirectToProfile(); 36 | break; 37 | case LoginActions.Register: 38 | this.redirectToRegister(); 39 | break; 40 | default: 41 | throw new Error(`Invalid action '${action}'`); 42 | } 43 | } 44 | 45 | render() { 46 | const action = this.props.action; 47 | const { message } = this.state; 48 | 49 | if (!!message) { 50 | return
{message}
51 | } else { 52 | switch (action) { 53 | case LoginActions.Login: 54 | return (
Processing login
); 55 | case LoginActions.LoginCallback: 56 | return (
Processing login callback
); 57 | case LoginActions.Profile: 58 | case LoginActions.Register: 59 | return (
); 60 | default: 61 | throw new Error(`Invalid action '${action}'`); 62 | } 63 | } 64 | } 65 | 66 | async login(returnUrl) { 67 | const state = { returnUrl }; 68 | const result = await authService.signIn(state); 69 | switch (result.status) { 70 | case AuthenticationResultStatus.Redirect: 71 | break; 72 | case AuthenticationResultStatus.Success: 73 | await this.navigateToReturnUrl(returnUrl); 74 | break; 75 | case AuthenticationResultStatus.Fail: 76 | this.setState({ message: result.message }); 77 | break; 78 | default: 79 | throw new Error(`Invalid status result ${result.status}.`); 80 | } 81 | } 82 | 83 | async processLoginCallback() { 84 | const url = window.location.href; 85 | const result = await authService.completeSignIn(url); 86 | switch (result.status) { 87 | case AuthenticationResultStatus.Redirect: 88 | // There should not be any redirects as the only time completeSignIn finishes 89 | // is when we are doing a redirect sign in flow. 90 | throw new Error('Should not redirect.'); 91 | case AuthenticationResultStatus.Success: 92 | await this.navigateToReturnUrl(this.getReturnUrl(result.state)); 93 | break; 94 | case AuthenticationResultStatus.Fail: 95 | this.setState({ message: result.message }); 96 | break; 97 | default: 98 | throw new Error(`Invalid authentication result status '${result.status}'.`); 99 | } 100 | } 101 | 102 | getReturnUrl(state) { 103 | const params = new URLSearchParams(window.location.search); 104 | const fromQuery = params.get(QueryParameterNames.ReturnUrl); 105 | if (fromQuery && !fromQuery.startsWith(`${window.location.origin}/`)) { 106 | // This is an extra check to prevent open redirects. 107 | throw new Error("Invalid return url. The return url needs to have the same origin as the current page.") 108 | } 109 | return (state && state.returnUrl) || fromQuery || `${window.location.origin}/`; 110 | } 111 | 112 | redirectToRegister() { 113 | this.redirectToApiAuthorizationPath(`${ApplicationPaths.IdentityRegisterPath}?${QueryParameterNames.ReturnUrl}=${encodeURI(ApplicationPaths.Login)}`); 114 | } 115 | 116 | redirectToProfile() { 117 | this.redirectToApiAuthorizationPath(ApplicationPaths.IdentityManagePath); 118 | } 119 | 120 | redirectToApiAuthorizationPath(apiAuthorizationPath) { 121 | const redirectUrl = `${window.location.origin}/${apiAuthorizationPath}`; 122 | // It's important that we do a replace here so that when the user hits the back arrow on the 123 | // browser they get sent back to where it was on the app instead of to an endpoint on this 124 | // component. 125 | window.location.replace(redirectUrl); 126 | } 127 | 128 | navigateToReturnUrl(returnUrl) { 129 | // It's important that we do a replace here so that we remove the callback uri with the 130 | // fragment containing the tokens from the browser history. 131 | window.location.replace(returnUrl); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/LoginMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { NavLink } from 'reactstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import authService from './AuthorizeService'; 5 | import { ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | export class LoginMenu extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | isAuthenticated: false, 13 | userName: null 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this._subscription = authService.subscribe(() => this.populateState()); 19 | this.populateState(); 20 | } 21 | 22 | componentWillUnmount() { 23 | authService.unsubscribe(this._subscription); 24 | } 25 | 26 | async populateState() { 27 | const [isAuthenticated, user] = await Promise.all([authService.isAuthenticated(), authService.getUser()]) 28 | this.setState({ 29 | isAuthenticated, 30 | userName: user && user.name 31 | }); 32 | } 33 | 34 | render() { 35 | const { isAuthenticated, userName } = this.state; 36 | if (!isAuthenticated) { 37 | const registerPath = `${ApplicationPaths.Register}`; 38 | const loginPath = `${ApplicationPaths.Login}`; 39 | return this.anonymousView(registerPath, loginPath); 40 | } else { 41 | const profilePath = `${ApplicationPaths.Profile}`; 42 | const logoutPath = { pathname: `${ApplicationPaths.LogOut}`, state: { local: true } }; 43 | return this.authenticatedView(userName, profilePath, logoutPath); 44 | } 45 | } 46 | 47 | authenticatedView(userName, profilePath, logoutPath) { 48 | return ( 49 | 52 | 55 | ); 56 | 57 | } 58 | 59 | anonymousView(registerPath, loginPath) { 60 | return ( 61 | 64 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/components/api-authorization/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react'; 3 | import authService from './AuthorizeService'; 4 | import { AuthenticationResultStatus } from './AuthorizeService'; 5 | import { QueryParameterNames, LogoutActions, ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | // The main responsibility of this component is to handle the user's logout process. 8 | // This is the starting point for the logout process, which is usually initiated when a 9 | // user clicks on the logout button on the LoginMenu component. 10 | export class Logout extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | message: undefined, 16 | isReady: false, 17 | authenticated: false 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | const action = this.props.action; 23 | switch (action) { 24 | case LogoutActions.Logout: 25 | if (!!window.history.state.state.local) { 26 | this.logout(this.getReturnUrl()); 27 | } else { 28 | // This prevents regular links to /authentication/logout from triggering a logout 29 | this.setState({ isReady: true, message: "The logout was not initiated from within the page." }); 30 | } 31 | break; 32 | case LogoutActions.LogoutCallback: 33 | this.processLogoutCallback(); 34 | break; 35 | case LogoutActions.LoggedOut: 36 | this.setState({ isReady: true, message: "You successfully logged out!" }); 37 | break; 38 | default: 39 | throw new Error(`Invalid action '${action}'`); 40 | } 41 | 42 | this.populateAuthenticationState(); 43 | } 44 | 45 | render() { 46 | const { isReady, message } = this.state; 47 | if (!isReady) { 48 | return
49 | } 50 | if (!!message) { 51 | return (
{message}
); 52 | } else { 53 | const action = this.props.action; 54 | switch (action) { 55 | case LogoutActions.Logout: 56 | return (
Processing logout
); 57 | case LogoutActions.LogoutCallback: 58 | return (
Processing logout callback
); 59 | case LogoutActions.LoggedOut: 60 | return (
{message}
); 61 | default: 62 | throw new Error(`Invalid action '${action}'`); 63 | } 64 | } 65 | } 66 | 67 | async logout(returnUrl) { 68 | const state = { returnUrl }; 69 | const isauthenticated = await authService.isAuthenticated(); 70 | if (isauthenticated) { 71 | const result = await authService.signOut(state); 72 | switch (result.status) { 73 | case AuthenticationResultStatus.Redirect: 74 | break; 75 | case AuthenticationResultStatus.Success: 76 | await this.navigateToReturnUrl(returnUrl); 77 | break; 78 | case AuthenticationResultStatus.Fail: 79 | this.setState({ message: result.message }); 80 | break; 81 | default: 82 | throw new Error("Invalid authentication result status."); 83 | } 84 | } else { 85 | this.setState({ message: "You successfully logged out!" }); 86 | } 87 | } 88 | 89 | async processLogoutCallback() { 90 | const url = window.location.href; 91 | const result = await authService.completeSignOut(url); 92 | switch (result.status) { 93 | case AuthenticationResultStatus.Redirect: 94 | // There should not be any redirects as the only time completeAuthentication finishes 95 | // is when we are doing a redirect sign in flow. 96 | throw new Error('Should not redirect.'); 97 | case AuthenticationResultStatus.Success: 98 | await this.navigateToReturnUrl(this.getReturnUrl(result.state)); 99 | break; 100 | case AuthenticationResultStatus.Fail: 101 | this.setState({ message: result.message }); 102 | break; 103 | default: 104 | throw new Error("Invalid authentication result status."); 105 | } 106 | } 107 | 108 | async populateAuthenticationState() { 109 | const authenticated = await authService.isAuthenticated(); 110 | this.setState({ isReady: true, authenticated }); 111 | } 112 | 113 | getReturnUrl(state) { 114 | const params = new URLSearchParams(window.location.search); 115 | const fromQuery = params.get(QueryParameterNames.ReturnUrl); 116 | if (fromQuery && !fromQuery.startsWith(`${window.location.origin}/`)) { 117 | // This is an extra check to prevent open redirects. 118 | throw new Error("Invalid return url. The return url needs to have the same origin as the current page.") 119 | } 120 | return (state && state.returnUrl) || 121 | fromQuery || 122 | `${window.location.origin}${ApplicationPaths.LoggedOut}`; 123 | } 124 | 125 | navigateToReturnUrl(returnUrl) { 126 | return window.location.replace(returnUrl); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/custom.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | } 8 | 9 | #root { 10 | height: 100%; 11 | } 12 | 13 | /* Provide sufficient contrast against white background */ 14 | a { 15 | color: #0366d6; 16 | } 17 | 18 | code { 19 | color: #E01A76; 20 | } 21 | 22 | .btn-primary { 23 | color: #fff; 24 | background-color: #1b6ec2; 25 | border-color: #1861ac; 26 | } 27 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App'; 6 | //import registerServiceWorker from './registerServiceWorker'; 7 | 8 | const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href') || ""; 9 | const rootElement = document.getElementById('root'); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | rootElement); 16 | 17 | // Uncomment the line above that imports the registerServiceWorker function 18 | // and the line below to register the generated service worker. 19 | // By default create-react-app includes a service worker to improve the 20 | // performance of the application by caching static assets. This service 21 | // worker can interfere with the Identity UI, so it is 22 | // disabled by default when Identity is being used. 23 | // 24 | //registerServiceWorker(); 25 | 26 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/ChatMessage.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | message: string; 3 | senderDisplayName: string; 4 | senderSignalingId: string; 5 | timestamp: string; 6 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/IceCandidateMessage.ts: -------------------------------------------------------------------------------- 1 | export interface IceCandidateMessage { 2 | peerId: string; 3 | iceCandidate: RTCIceCandidate; 4 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/Peer.ts: -------------------------------------------------------------------------------- 1 | export interface Peer { 2 | signalingId: string; 3 | displayName?: string; 4 | remoteMediaStream?: MediaStream; 5 | peerConnection?: RTCPeerConnection; 6 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/SdpMessage.ts: -------------------------------------------------------------------------------- 1 | export interface SdpMessage { 2 | signalingId: string; 3 | description: RTCSessionDescriptionInit; 4 | displayName: string; 5 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/SessionDto.ts: -------------------------------------------------------------------------------- 1 | export interface SessionDto { 2 | id: string; 3 | titleBackgroundColor: string; 4 | titleTextColor: string; 5 | titleText: string; 6 | logoUrl: string; 7 | pageBackgroundColor: string; 8 | pageTextColor: string; 9 | } 10 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/interfaces/Settings.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | defaultVideoInput: string; 3 | defaultAudioInput: string; 4 | defaultAudioOutput: string; 5 | displayName: string; 6 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register () { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW (swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker (swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister () { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/services/SessionInfoContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SessionDto } from '../interfaces/SessionDto'; 3 | import { Signaler } from './SignalingService'; 4 | import { EventEmitterEx } from '../utils/EventEmitterEx'; 5 | 6 | export class SessionInfoContextState { 7 | public isSession: boolean = false; 8 | public sessionChecked: boolean = false; 9 | public sessionId?: string; 10 | public sessionInfo?: SessionDto; 11 | public sessionJoined: boolean = false; 12 | public readonly stateUpdated: EventEmitterEx = new EventEmitterEx(); 13 | 14 | constructor() { 15 | this.isSession = window.location.pathname 16 | .toLowerCase() 17 | .includes("/session/"); 18 | 19 | if (this.isSession) { 20 | this.sessionId = window.location.pathname 21 | .toLowerCase() 22 | .replace("/session/", "") 23 | .split("/") 24 | .join(""); 25 | } 26 | } 27 | 28 | public getSessionInfo = async () => { 29 | var sessionInfo = await Signaler.getSessionInfo(String(this.sessionId)); 30 | this.setSessionInfo(false, sessionInfo); 31 | } 32 | 33 | public joinSession = async () => { 34 | var sessionInfo = await Signaler.joinSession(String(this.sessionId)); 35 | this.setSessionInfo(true, sessionInfo); 36 | } 37 | 38 | private setSessionInfo = (joined: boolean, sessionInfo?: SessionDto) => { 39 | if (this.isSession && sessionInfo) { 40 | document.body.style.backgroundColor = sessionInfo.pageBackgroundColor; 41 | document.body.style.color = sessionInfo.pageTextColor; 42 | } 43 | this.sessionInfo = sessionInfo; 44 | this.sessionChecked = true; 45 | this.sessionJoined = joined; 46 | this.update(); 47 | } 48 | 49 | public update = () => { 50 | this.stateUpdated.publish(this); 51 | } 52 | } 53 | 54 | export const SessionInfoContextData = new SessionInfoContextState(); 55 | export const SessionInfoContext = React.createContext(SessionInfoContextData); -------------------------------------------------------------------------------- /Confer/ClientApp/src/services/SettingsService.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from "../interfaces/Settings"; 2 | 3 | const settingsKey = "Confer_Settings"; 4 | 5 | const defaultSettings = () => { 6 | var defaults = { 7 | defaultAudioInput: "", 8 | defaultVideoInput: "", 9 | defaultAudioOutput: "", 10 | displayName: getRandomUser() 11 | }; 12 | localStorage.setItem(settingsKey, JSON.stringify(defaults)); 13 | return defaults; 14 | } 15 | 16 | const getRandomUser = () => { 17 | var user = "User-"; 18 | for (var i = 0; i < 10; i++) { 19 | user += String(Math.floor(Math.random() * 9)); 20 | } 21 | return user; 22 | } 23 | 24 | export function getSettings(): Settings { 25 | try { 26 | var settingsJson = localStorage.getItem(settingsKey); 27 | return JSON.parse(String(settingsJson)) || defaultSettings(); 28 | } 29 | catch { 30 | return defaultSettings(); 31 | } 32 | } 33 | 34 | export function saveSettings(settings: Settings) { 35 | localStorage.setItem(settingsKey, JSON.stringify(settings)); 36 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/services/SignalingService.ts: -------------------------------------------------------------------------------- 1 | import { HubConnection, HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr"; 2 | import { ChatMessage } from "../interfaces/ChatMessage"; 3 | import { IceCandidateMessage } from "../interfaces/IceCandidateMessage"; 4 | import { SdpMessage } from "../interfaces/SdpMessage"; 5 | import { SessionDto } from "../interfaces/SessionDto"; 6 | import { EventEmitterEx } from "../utils/EventEmitterEx"; 7 | 8 | class SignalingService { 9 | private connection?: HubConnection; 10 | private initialized: boolean = false; 11 | 12 | public get connectionState(): HubConnectionState { 13 | return this.connection?.state || HubConnectionState.Connecting; 14 | } 15 | 16 | public readonly onChatMessageReceived = new EventEmitterEx(); 17 | public readonly onConnectionStateChanged = new EventEmitterEx(); 18 | public readonly onSdpReceived = new EventEmitterEx(); 19 | public readonly onIceCandidateReceived = new EventEmitterEx(); 20 | public readonly onPeerLeft = new EventEmitterEx(); 21 | 22 | public connect(): Promise { 23 | if (this.initialized) { 24 | return Promise.resolve(true); 25 | } 26 | 27 | this.initialized = true; 28 | return new Promise(resolve => { 29 | 30 | if (this.connection) { 31 | this.connection.stop(); 32 | } 33 | 34 | this.connection = new HubConnectionBuilder() 35 | .withUrl("/signaling") 36 | .withAutomaticReconnect() 37 | .build(); 38 | 39 | this.connection.on("Sdp", (peerId: string, displayName: string, description: RTCSessionDescriptionInit) => { 40 | this.onSdpReceived.publish({ 41 | description: description, 42 | displayName: displayName, 43 | signalingId: peerId 44 | }) 45 | }); 46 | this.connection.on("IceCandidate", (peerId: string, jsonCandidate: string) => { 47 | if (!jsonCandidate) { 48 | return; 49 | } 50 | this.onIceCandidateReceived.publish({ 51 | iceCandidate: JSON.parse(jsonCandidate), 52 | peerId: peerId 53 | }) 54 | }); 55 | this.connection.on("ChatMessage", (message: string, displayName: string, peerId: string) => { 56 | this.onChatMessageReceived.publish({ 57 | message: message, 58 | senderDisplayName: displayName, 59 | senderSignalingId: peerId, 60 | timestamp: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}` 61 | }) 62 | }); 63 | this.connection.on("PeerLeft", (peerId:string) => { 64 | this.onPeerLeft.publish(peerId); 65 | }) 66 | 67 | this.connection.onclose(() => this.onConnectionStateChanged.publish(HubConnectionState.Disconnected)); 68 | this.connection.onreconnecting(() => this.onConnectionStateChanged.publish(HubConnectionState.Reconnecting)); 69 | this.connection.onreconnected(() => this.onConnectionStateChanged.publish(HubConnectionState.Connected)); 70 | 71 | this.connection.start() 72 | .then(() => { 73 | this.onConnectionStateChanged.publish(HubConnectionState.Connected); 74 | resolve(true); 75 | }) 76 | .catch(reason => { 77 | console.error(reason); 78 | this.onConnectionStateChanged.publish(HubConnectionState.Disconnected); 79 | resolve(false); 80 | }); 81 | }) 82 | } 83 | 84 | public getIceServers() : Promise { 85 | return this.connection?.invoke("GetIceServers") || Promise.resolve([]); 86 | } 87 | 88 | public getPeers() : Promise { 89 | return this.connection?.invoke("GetPeers") || Promise.resolve([]); 90 | } 91 | 92 | public getSessionInfo(sessionId: string) : Promise { 93 | return this.connection?.invoke("GetSessionInfo", sessionId) || Promise.resolve(undefined); 94 | } 95 | 96 | public joinSession(sessionId: string) : Promise { 97 | return this.connection?.invoke("JoinSession", sessionId) || Promise.resolve(undefined); 98 | } 99 | 100 | public sendIceCandidate(peerId: string, candidate: RTCIceCandidate | null) { 101 | var jsonCandidate = candidate ? JSON.stringify(candidate) : candidate; 102 | return this.connection?.invoke("SendIceCandidate", peerId, jsonCandidate); 103 | } 104 | 105 | public sendChatMessage(message: string, displayName: string) { 106 | this.connection?.invoke("SendChatMessage", message, displayName); 107 | } 108 | 109 | public sendSdp(signalingId: string, displayName: string, localDescription: RTCSessionDescription | null) { 110 | if (localDescription == null){ 111 | console.error("Session description is null."); 112 | return; 113 | } 114 | 115 | return this.connection?.invoke("SendSdp", signalingId, displayName, localDescription); 116 | } 117 | } 118 | 119 | 120 | export const Signaler = new SignalingService(); -------------------------------------------------------------------------------- /Confer/ClientApp/src/setupTests.js: -------------------------------------------------------------------------------- 1 | const localStorageMock = { 2 | getItem: jest.fn(), 3 | setItem: jest.fn(), 4 | removeItem: jest.fn(), 5 | clear: jest.fn(), 6 | }; 7 | global.localStorage = localStorageMock; 8 | 9 | // Mock the request issued by the react app to get the client configuration parameters. 10 | window.fetch = () => { 11 | return Promise.resolve( 12 | { 13 | ok: true, 14 | json: () => Promise.resolve({ 15 | "authority": "https://localhost:5001", 16 | "client_id": "Confer", 17 | "redirect_uri": "https://localhost:5001/authentication/login-callback", 18 | "post_logout_redirect_uri": "https://localhost:5001/authentication/logout-callback", 19 | "response_type": "id_token token", 20 | "scope": "ConferAPI openid profile" 21 | }) 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/utils/EventEmitterEx.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | export class EventEmitterEx extends EventEmitter { 4 | subscribe(listener: (args: T) => void): EventEmitter { 5 | this.on("", listener); 6 | return this; 7 | } 8 | publish(value: T) { 9 | this.emit("", value); 10 | } 11 | unsubscribe(listener: (args: T) => void) { 12 | this.off("", listener); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Confer/ClientApp/src/utils/MediaHelper.ts: -------------------------------------------------------------------------------- 1 | export async function enumerateAudioInputs(): Promise { 2 | try { 3 | var tempStraem = await navigator.mediaDevices.getUserMedia({ 4 | audio: true, 5 | video: false 6 | }); 7 | var devices = await navigator.mediaDevices.enumerateDevices(); 8 | 9 | // Tracks must be stopped to avoid errors when 10 | // attempting to get the media device again. 11 | tempStraem.getTracks().forEach(x => { 12 | x.stop(); 13 | }); 14 | 15 | return devices.filter(x => x.kind == "audioinput"); 16 | } 17 | catch { 18 | console.warn("Failed to get user audio while enumerating."); 19 | return []; 20 | } 21 | } 22 | 23 | export async function enumerateAudioOuputs(): Promise { 24 | var tempStream; 25 | try { 26 | tempStream = await navigator.mediaDevices.getUserMedia({ 27 | audio: true, 28 | video: false 29 | }); 30 | } 31 | catch { 32 | console.warn("Failed to get user audio while enumerating."); 33 | } 34 | 35 | try { 36 | var devices = await navigator.mediaDevices.enumerateDevices(); 37 | tempStream?.getTracks().forEach(x => { 38 | x.stop(); 39 | }); 40 | return devices.filter(x => x.kind == "audiooutput"); 41 | } 42 | catch { 43 | console.warn("Failed to get user audio while enumerating."); 44 | return []; 45 | } 46 | } 47 | 48 | export async function enumerateVideoInputs(): Promise { 49 | var tempStream; 50 | try { 51 | tempStream = await navigator.mediaDevices.getUserMedia({ 52 | video: true, 53 | audio: false 54 | }); 55 | } 56 | catch { 57 | console.warn("Failed to get user video while enumerating."); 58 | } 59 | 60 | try { 61 | var devices = await navigator.mediaDevices.enumerateDevices(); 62 | tempStream?.getTracks().forEach(x => { 63 | x.stop(); 64 | }) 65 | return devices.filter(x => x.kind == "videoinput");; 66 | } 67 | catch { 68 | console.warn("Failed to get user video while enumerating."); 69 | return []; 70 | } 71 | 72 | } 73 | 74 | export function loadAudioDevice(deviceId: string): Promise { 75 | return navigator.mediaDevices.getUserMedia({ 76 | audio: { 77 | deviceId: { 78 | exact: deviceId 79 | } 80 | }, 81 | video: false 82 | }); 83 | } 84 | 85 | export function loadVideoDevice(deviceId: string): Promise { 86 | return navigator.mediaDevices.getUserMedia({ 87 | video: { 88 | deviceId: { 89 | exact: deviceId 90 | } 91 | }, 92 | audio: false 93 | }); 94 | } -------------------------------------------------------------------------------- /Confer/ClientApp/src/utils/UI.ts: -------------------------------------------------------------------------------- 1 | export enum WindowSize { 2 | Small, 3 | Medium, 4 | Large 5 | } 6 | 7 | export function getWindowSize() : WindowSize { 8 | if (!window.matchMedia) { 9 | return WindowSize.Medium; 10 | } 11 | 12 | if (matchMedia("(max-width: 576px)").matches){ 13 | return WindowSize.Small; 14 | } 15 | if (matchMedia("(max-width: 768px)").matches) { 16 | return WindowSize.Medium; 17 | } 18 | if (matchMedia("(max-width: 992px").matches) { 19 | 20 | } 21 | return WindowSize.Large; 22 | } -------------------------------------------------------------------------------- /Confer/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Confer/Confer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 0f15e56d-8c7d-4e71-9c08-d6764c2370eb 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | DtsGenerator 27 | SessionDto.cs.d.ts 28 | 29 | 30 | 31 | 32 | 33 | True 34 | True 35 | SessionDto.cs 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | %(DistFiles.Identity) 59 | PreserveNewest 60 | true 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Confer/Models/ActiveSession.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Confer.Models 4 | { 5 | public class ActiveSession 6 | { 7 | public ActiveSession(SessionDto dto) 8 | { 9 | Id = dto.Id; 10 | LogoUrl = dto.LogoUrl; 11 | PageBackgroundColor = dto.PageBackgroundColor; 12 | TitleBackgroundColor = dto.TitleBackgroundColor; 13 | TitleText = dto.TitleText; 14 | TitleTextColor = dto.TitleTextColor; 15 | PageTextColor = dto.PageTextColor; 16 | } 17 | 18 | public string Id { get; } 19 | public string LogoUrl { get; } 20 | public string PageBackgroundColor { get; } 21 | public string PageTextColor { get; } 22 | public string TitleBackgroundColor { get; } 23 | public ConcurrentDictionary Participants { get; } = new ConcurrentDictionary(); 24 | public string TitleText { get; } 25 | public string TitleTextColor { get; } 26 | 27 | public SessionDto ToDto() 28 | { 29 | return new SessionDto() 30 | { 31 | Id = Id, 32 | LogoUrl = LogoUrl, 33 | PageBackgroundColor = PageBackgroundColor, 34 | PageTextColor = PageTextColor, 35 | TitleBackgroundColor = TitleBackgroundColor, 36 | TitleText = TitleText, 37 | TitleTextColor = TitleTextColor 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Confer/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace Confer.Models 4 | { 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Confer/Models/IceServer.cs: -------------------------------------------------------------------------------- 1 | namespace Confer.Models 2 | { 3 | public class IceServer 4 | { 5 | public string Credential { get; set; } 6 | public string CredentialType { get; set; } 7 | public string Urls { get; set; } 8 | public string Username { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Confer/Models/RTCSessionDescriptionInit.cs: -------------------------------------------------------------------------------- 1 | namespace Confer.Models 2 | { 3 | public class RTCSessionDescriptionInit 4 | { 5 | public string Sdp { get; set; } 6 | public string Type { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Confer/Models/SessionDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Confer.Models 4 | { 5 | public class SessionDto 6 | { 7 | public string Id { get; set; } 8 | 9 | [StringLength(30)] 10 | public string TitleBackgroundColor { get; set; } 11 | 12 | [StringLength(30)] 13 | public string TitleTextColor { get; set; } 14 | 15 | [StringLength(50)] 16 | public string TitleText { get; set; } 17 | 18 | [StringLength(500)] 19 | public string LogoUrl { get; set; } 20 | 21 | [StringLength(30)] 22 | public string PageBackgroundColor { get; set; } 23 | 24 | [StringLength(30)] 25 | public string PageTextColor { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Confer/Models/SessionDto.cs.d.ts: -------------------------------------------------------------------------------- 1 | interface sessionDto { 2 | id: string; 3 | titleBackgroundColor: string; 4 | titleTextColor: string; 5 | titleText: string; 6 | logoUrl: string; 7 | pageBackgroundColor: string; 8 | pageTextColor: string; 9 | } 10 | -------------------------------------------------------------------------------- /Confer/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 the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /Confer/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Confer.Pages 7 | { 8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 9 | public class ErrorModel : PageModel 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public ErrorModel(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | public string RequestId { get; set; } 19 | 20 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Confer/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using Confer.Models; 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | 6 | @{ 7 | string returnUrl = null; 8 | var query = ViewContext.HttpContext.Request.Query; 9 | if (query.ContainsKey("returnUrl")) 10 | { 11 | returnUrl = query["returnUrl"]; 12 | } 13 | } 14 | 15 | 37 | -------------------------------------------------------------------------------- /Confer/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Confer 2 | @namespace Confer.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Confer/Program.cs: -------------------------------------------------------------------------------- 1 | using Confer.Services; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | var configuration = builder.Configuration; 10 | var services = builder.Services; 11 | 12 | services.AddCors(options => 13 | { 14 | options.AddPolicy("OpenPolicy", builder => builder 15 | .AllowAnyOrigin() 16 | .AllowAnyHeader() 17 | .AllowAnyMethod() 18 | ); 19 | }); 20 | 21 | services.AddControllersWithViews(); 22 | services.AddRazorPages(); 23 | 24 | services.AddSignalR(options => 25 | { 26 | options.EnableDetailedErrors = builder.Environment.IsDevelopment(); 27 | }); 28 | 29 | // In production, the React files will be served from this directory 30 | services.AddSpaStaticFiles(configuration => 31 | { 32 | configuration.RootPath = "ClientApp/build"; 33 | }); 34 | 35 | services.AddSingleton(); 36 | services.AddScoped(); 37 | 38 | 39 | var app = builder.Build(); 40 | 41 | 42 | if (app.Environment.IsDevelopment()) 43 | { 44 | app.UseDeveloperExceptionPage(); 45 | } 46 | else 47 | { 48 | app.UseExceptionHandler("/Error"); 49 | app.UseHsts(); 50 | } 51 | 52 | app.UseHttpsRedirection(); 53 | app.UseStaticFiles(); 54 | app.UseSpaStaticFiles(); 55 | 56 | app.UseRouting(); 57 | 58 | app.UseCors("OpenPolicy"); 59 | app.UseEndpoints(endpoints => 60 | { 61 | endpoints.MapHub("/signaling"); 62 | endpoints.MapControllers(); 63 | endpoints.MapRazorPages(); 64 | }); 65 | 66 | app.UseSpa(spa => 67 | { 68 | spa.Options.SourcePath = "ClientApp"; 69 | 70 | if (app.Environment.IsDevelopment()) 71 | { 72 | spa.UseReactDevelopmentServer(npmScript: "start"); 73 | } 74 | }); 75 | 76 | await app.RunAsync(); -------------------------------------------------------------------------------- /Confer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52901", 7 | "sslPort": 44380 8 | } 9 | }, 10 | "profiles": { 11 | "Confer": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "IIS Express": { 20 | "commandName": "IISExpress", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Confer/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql", 5 | "connectionId": "DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Confer/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql.local", 5 | "connectionId": "DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Confer/Services/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using Confer.Models; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace Confer.Services 5 | { 6 | public interface IAppSettings 7 | { 8 | IceServer[] IceServers { get; } 9 | } 10 | 11 | public class AppSettings : IAppSettings 12 | { 13 | private readonly IConfiguration _config; 14 | 15 | private readonly IceServer[] fallbackIceServers = new IceServer[] 16 | { 17 | new IceServer() { Urls = "stun:stun.l.google.com:19302"}, 18 | new IceServer() { Urls = "stun:stun4.l.google.com:19302"} 19 | }; 20 | 21 | public AppSettings(IConfiguration config) 22 | { 23 | _config = config; 24 | } 25 | 26 | public IceServer[] IceServers => _config.GetSection("IceServers").Get() ?? fallbackIceServers; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Confer/Services/SessionManager.cs: -------------------------------------------------------------------------------- 1 | using Confer.Models; 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace Confer.Services 8 | { 9 | public interface ISessionManager 10 | { 11 | string CreateNewSession(SessionDto session); 12 | bool TryGetSession(string sessionId, out ActiveSession session); 13 | } 14 | public class SessionManager : ISessionManager 15 | { 16 | private readonly ILogger _logger; 17 | private readonly MemoryCache _sessions = new MemoryCache(new MemoryCacheOptions()); 18 | public SessionManager(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | public string CreateNewSession(SessionDto session) 24 | { 25 | var guid = Guid.NewGuid().ToString(); 26 | session.Id = guid; 27 | var activeSession = new ActiveSession(session); 28 | _sessions.Set(guid, activeSession, CreateEntryOptions()); 29 | return guid; 30 | } 31 | 32 | public bool TryGetSession(string sessionId, out ActiveSession session) 33 | { 34 | if (string.IsNullOrWhiteSpace(sessionId)) 35 | { 36 | session = null; 37 | return false; 38 | } 39 | return _sessions.TryGetValue(sessionId, out session); 40 | } 41 | 42 | private MemoryCacheEntryOptions CreateEntryOptions() 43 | { 44 | var entryOptions = new MemoryCacheEntryOptions() 45 | { 46 | SlidingExpiration = TimeSpan.FromMinutes(10) 47 | }; 48 | entryOptions.RegisterPostEvictionCallback(OnEntryEvicted); 49 | return entryOptions; 50 | } 51 | 52 | private void OnEntryEvicted(object key, object value, EvictionReason reason, object state) 53 | { 54 | _logger.LogInformation("Evicting entry {key}.", key); 55 | 56 | if ((value as ActiveSession)?.Participants?.Any() == true) 57 | { 58 | _logger.LogInformation("Session still has participants. Adding back to cache."); 59 | _sessions.Set(key, value, CreateEntryOptions()); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Confer/Services/SignalingHub.cs: -------------------------------------------------------------------------------- 1 | using Confer.Models; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Confer.Services 11 | { 12 | public class SignalingHub : Hub 13 | { 14 | private readonly ISessionManager _sessionManager; 15 | private readonly ILogger _logger; 16 | private readonly IAppSettings _appSettings; 17 | 18 | public static ConcurrentDictionary Connections { get; } = 19 | new ConcurrentDictionary(); 20 | 21 | public SignalingHub(IAppSettings appSettings, 22 | ISessionManager sessionManager, 23 | ILogger logger) 24 | { 25 | _appSettings = appSettings; 26 | _sessionManager = sessionManager; 27 | _logger = logger; 28 | } 29 | 30 | private string SessionId 31 | { 32 | get 33 | { 34 | return Context.Items["SessionId"] as string; 35 | } 36 | set 37 | { 38 | Context.Items["SessionId"] = value; 39 | } 40 | } 41 | 42 | 43 | public override Task OnConnectedAsync() 44 | { 45 | _logger.LogDebug("New connection. Count: {count}", Connections.Count); 46 | Connections.AddOrUpdate(Context.ConnectionId, Clients.Caller, (k, v) => Clients.Caller); 47 | return base.OnConnectedAsync(); 48 | } 49 | 50 | public override async Task OnDisconnectedAsync(Exception exception) 51 | { 52 | _logger.LogDebug("Connection lost. Count: {count}", Connections.Count); 53 | Connections.TryRemove(Context.ConnectionId, out _); 54 | 55 | if (!string.IsNullOrWhiteSpace(SessionId)) 56 | { 57 | if (_sessionManager.TryGetSession(SessionId, out var session)) 58 | { 59 | session.Participants.Remove(Context.ConnectionId, out _); 60 | } 61 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, SessionId); 62 | await Clients.OthersInGroup(SessionId).SendAsync("PeerLeft", Context.ConnectionId); 63 | } 64 | await base.OnDisconnectedAsync(exception); 65 | } 66 | 67 | public IceServer[] GetIceServers() 68 | { 69 | return _appSettings.IceServers; 70 | } 71 | 72 | public string[] GetPeers() 73 | { 74 | if (_sessionManager.TryGetSession(SessionId, out var session)) 75 | { 76 | return session.Participants.Keys 77 | .Where(x => x != Context.ConnectionId) 78 | .ToArray(); 79 | } 80 | else 81 | { 82 | return Array.Empty(); 83 | } 84 | } 85 | 86 | public SessionDto GetSessionInfo(string sessionId) 87 | { 88 | if (!_sessionManager.TryGetSession(sessionId, out var session)) 89 | { 90 | return null; 91 | } 92 | 93 | return session.ToDto(); 94 | } 95 | 96 | public async Task JoinSession(string sessionId) 97 | { 98 | if (!_sessionManager.TryGetSession(sessionId, out var session)) 99 | { 100 | return null; 101 | } 102 | 103 | SessionId = sessionId; 104 | session.Participants.AddOrUpdate(Context.ConnectionId, string.Empty, (k,v) => string.Empty); 105 | await Groups.AddToGroupAsync(Context.ConnectionId, SessionId); 106 | return session.ToDto(); 107 | } 108 | 109 | public Task SendIceCandidate(string signalingId, string jsonCandidate) 110 | { 111 | return Clients.Client(signalingId).SendAsync("IceCandidate", Context.ConnectionId, jsonCandidate); 112 | } 113 | 114 | public Task SendChatMessage(string message, string displayName) 115 | { 116 | return Clients.Group(SessionId).SendAsync("ChatMessage", message, displayName, Context.ConnectionId); 117 | } 118 | 119 | public Task SendSdp(string signalingId, string displayName, RTCSessionDescriptionInit sessionDescription) 120 | { 121 | return Clients.Client(signalingId).SendAsync("Sdp", Context.ConnectionId, displayName, sessionDescription); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Confer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "IdentityServer": { 10 | "Key": { 11 | "Type": "Development" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Confer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Confer-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "IdentityServer": { 13 | "Clients": { 14 | "Confer": { 15 | "Profile": "IdentityServerSPA" 16 | } 17 | } 18 | }, 19 | "AllowedHosts": "*", 20 | "IceServers": [ 21 | { 22 | "Urls": "stun:stun.l.google.com:19302", 23 | "Credential": "", 24 | "CredentialType": "password", 25 | "Username": "" 26 | }, 27 | { 28 | "Urls": "stun:stun.l.google.com:19302", 29 | "Credential": "", 30 | "CredentialType": "password", 31 | "Username": "" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jared Goodwin 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 | # Confer 2 | Create branded, on-demand video chat sessions in your browser. 3 | 4 | ## Project Links 5 | Hosted App: https://confer.jaredg.dev 6 | Sponsor: https://github.com/sponsors/lucent-sea 7 | Donate: https://www.paypal.com/donate?hosted_button_id=L8ZB3L9NCBNWW 8 | 9 | ## Build Requirements 10 | * .NET 6 SDK 11 | * Node.js (latest) 12 | 13 | ## Building 14 | 15 | ``` 16 | git clone https://github.com/lucent-sea/confer 17 | cd ./confer/confer 18 | dotnet build 19 | dotnet run 20 | ``` 21 | 22 | ## Screenshot 23 | !["Confer Screennshot](.github/screenshot.jpg) 24 | --------------------------------------------------------------------------------