Browse Source

POC: PC Chrome Audio Record

HOME 1 year ago
parent
commit
5a06d60c9a

+ 104 - 0
SimpleVoiceChat.BlazorWasm/App.razor

@@ -0,0 +1,104 @@
+@code
+{
+    private bool _isInit = false;
+    private List<byte[]> _chunks = new();
+
+    private List<string> _logs = new();
+
+    private string _playUrl;
+
+}
+
+@if (_isInit == false)
+{
+    <button @onclick="Init">Init</button>
+}
+else
+{
+
+    <button @onclick="Start">Start</button>
+
+    <button @onclick="Stop">Stop</button>
+    <button @onclick="()=>_chunks.Clear()">Clear</button>
+
+    for (var index = 0; index < _chunks.Count; index++)
+    {
+        var chunk = _chunks[index];
+        <button @onclick="() => Play(chunk)"> CHUNK-@index-@(chunk.Length)</button>
+    }
+
+    <audio src="@_playUrl" controls autoplay>
+        <source src="@_playUrl" type="audio/webm;codecs=OPUS" />
+    </audio>
+}
+
+<hr />
+@foreach (var s in _logs)
+{
+    <div>
+        <pre>@s</pre>
+    </div>
+}
+
+@code
+{
+
+    protected override async Task OnAfterRenderAsync(bool firstRender)
+    {
+        await base.OnAfterRenderAsync(firstRender);
+        if (firstRender)
+        {
+            SvcModule.OnInit += VoiceInit;
+            SvcModule.ChunkArrive += ChunkArrive;
+        }
+    }
+
+    async Task Init()
+    {
+        try
+        {
+            _isInit = await SvcModule.Init();
+        }
+        catch (Exception e)
+        {
+            _logs.Add(e.ToString());
+        }
+        StateHasChanged();
+    }
+
+    private void VoiceInit()
+    {
+
+    }
+
+    async Task Start()
+    {
+        try
+        {
+            await SvcModule.Start();
+        }
+        catch (Exception e)
+        {
+            _logs.Add(e.ToString());
+        }
+        StateHasChanged();
+    }
+
+    private void ChunkArrive(byte[] obj)
+    {
+        _chunks.Add(obj);
+        StateHasChanged();
+    }
+
+    async Task Stop()
+    {
+        await SvcModule.Stop();
+    }
+
+    private async Task Play(byte[] chunk)
+    {
+        _playUrl = await SvcModule.CreateBlob(chunk);
+        StateHasChanged();
+    }
+
+}

+ 11 - 0
SimpleVoiceChat.BlazorWasm/Program.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using SimpleVoiceChat.BlazorWasm;
+
+var builder = WebAssemblyHostBuilder.CreateDefault(args);
+builder.RootComponents.Add<App>("#app");
+builder.RootComponents.Add<HeadOutlet>("head::after");
+
+builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+
+await builder.Build().RunAsync();

+ 49 - 0
SimpleVoiceChat.BlazorWasm/Properties/launchSettings.json

@@ -0,0 +1,49 @@
+{
+  "profiles": {
+    "http": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "dotnetRunMessages": true,
+      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+      "applicationUrl": "http://0.0.0.0:5201"
+    },
+    "HotLoad": {
+      "commandName": "Executable",
+      "executablePath": "dotnet",
+      "commandLineArgs": "watch",
+      "workingDirectory": ".",
+      "environmentVariables": {
+        "DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER": "1"
+      }
+    },
+    "WSL": {
+      "commandName": "WSL2",
+      "launchBrowser": true,
+      "launchUrl": "http://localhost:5201",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development",
+        "ASPNETCORE_URLS": "http://localhost:5201"
+      },
+      "distributionName": ""
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
+    }
+  },
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:63687",
+      "sslPort": 0
+    }
+  }
+}

+ 9 - 0
SimpleVoiceChat.BlazorWasm/README.md

@@ -0,0 +1,9 @@
+# SimpleVoiceChat.BlazorWasm 
+press <kbd>Ctrl</kbd>+<kbd>`~</kbd> in vs2022
+
+```powershell
+cd SimpleVoiceChat.BlazorWasm
+$env:DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER=1
+dotnet watch
+
+```

+ 21 - 0
SimpleVoiceChat.BlazorWasm/SimpleVoiceChat.BlazorWasm.csproj

@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <allowUnsafeBlocks>true</allowUnsafeBlocks>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0" PrivateAssets="all" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Update="wwwroot\index.html">
+      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+    </Content>
+  </ItemGroup>
+
+</Project>

+ 57 - 0
SimpleVoiceChat.BlazorWasm/SvcModule.cs

@@ -0,0 +1,57 @@
+using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.Versioning;
+
+namespace SimpleVoiceChat.BlazorWasm
+{
+    [SupportedOSPlatform("browser")]
+    public static partial class SvcModule
+    {
+        public static Action OnInit;
+        public static Action<byte[]> ChunkArrive;
+
+        private static JSObject _jsModule;
+
+
+        public static async Task<bool> Init()
+        {
+            _jsModule ??= await JSHost.ImportAsync("SvcModule", "/svc-module.js");
+            return await JsInit();
+        }
+
+        public static async Task Start()
+        {
+            await JsStart();
+        }
+
+        public static async Task Stop()
+        {
+            JsStop();
+        }
+
+        [JSImport("init", "SvcModule")]
+        private static partial Task<bool> JsInit();
+
+        [JSImport("start", "SvcModule")]
+        private static partial Task JsStart();
+
+        [JSImport("stop", "SvcModule")]
+        private static partial void JsStop();
+
+        [JSImport("createBlobUrl", "SvcModule")]
+        public static partial Task<string> CreateBlob(byte[] data);
+
+        [JSExport]
+        private static void JsFeedChunk(byte[] chunk)
+        {
+            Console.WriteLine("chunk");
+            if (ChunkArrive != null) ChunkArrive(chunk);
+
+        }
+
+        [JSExport]
+        private static void JsInitCallBack(bool value)
+        {
+            if (OnInit != null) OnInit();
+        }
+    }
+}

+ 9 - 0
SimpleVoiceChat.BlazorWasm/_Imports.razor

@@ -0,0 +1,9 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.AspNetCore.Components.WebAssembly.Http
+@using Microsoft.JSInterop
+@using SimpleVoiceChat.BlazorWasm

BIN
SimpleVoiceChat.BlazorWasm/wwwroot/favicon.png


SimpleWebChat.BlazorWasm/wwwroot/blazor-192.png → SimpleVoiceChat.BlazorWasm/wwwroot/icon-192.png


+ 56 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/index.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <link rel="icon" type="image/x-icon" href="favicon.ico">
+    <title>Simple Web Chat</title>
+
+    <base href="/" />
+
+    <link href="lib/bootstrap/default/bootstrap.min.css" rel="stylesheet" />
+
+    <link href="svc.css" rel="stylesheet" />
+</head>
+
+<body>
+    <div id="app">
+        <div class="loader text-center" id="loading">
+            <link href="loading.css" rel="stylesheet" />
+            <div class="logo">
+                <div class="one common"></div>
+                <div class="two common"></div>
+                <div class="three common"></div>
+                <div class="four common"></div>
+                <div class="five common"></div>
+                <div class="six common"></div>
+                <div class="seven common"></div>
+                <div class="eight common"></div>
+            </div>
+            <div class="intro">
+                <img src="icon-192.png" />
+                <span>精彩即将呈现</span>
+            </div>
+            <div class="bar">
+                <div class="progress"></div>
+            </div>
+        </div>
+    </div>
+
+    <div id="blazor-error-ui">
+        <environment include="Staging,Production">
+            An error has occurred. This application may no longer respond until reloaded.
+        </environment>
+        <environment include="Development">
+            An unhandled exception has occurred. See browser dev tools for details.
+        </environment>
+        <a href="" class="reload">Reload</a>
+        <a class="dismiss"><i class="fa fa-times"></i></a>
+    </div>
+
+    <script src="_framework/blazor.webassembly.js"></script>
+    <script src="lib/bootstrap/bootstrap.bundle.min.js"></script>
+
+</body>
+</html>

File diff suppressed because it is too large
+ 7 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/lib/bootstrap/bootstrap.bundle.min.js


File diff suppressed because it is too large
+ 7 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/lib/bootstrap/default/bootstrap.min.css


+ 397 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/loading.css

@@ -0,0 +1,397 @@
+.loader {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #161B29;
+    transition: opacity .5s linear;
+    z-index: 2050;
+    display: flex;
+    flex-flow: column;
+}
+
+    .loader.is-done {
+        opacity: 0;
+    }
+
+    .loader .logo {
+        width: 200px;
+        height: 200px;
+        position: relative;
+        margin: 0 auto;
+    }
+
+@keyframes logo {
+    0%, 100% {
+        box-shadow: 1px 1px 25px 10px rgba(146, 148, 248, 0.4);
+    }
+
+    50% {
+        box-shadow: none;
+    }
+}
+
+.loader .intro {
+    width: 250px;
+    color: #fff;
+    font-size: 1.5rem;
+    text-align: center;
+    margin: 3rem auto;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    padding: 0.5rem 1rem;
+    position: relative;
+    overflow: hidden;
+    border: 1px solid rgb(146, 148, 248);
+    animation: intro 3s linear infinite
+}
+
+@keyframes intro {
+    0%, 100% {
+        box-shadow: 0px 0px 25px 10px rgba(146, 148, 248, 0.4);
+    }
+
+    40%, 60% {
+        box-shadow: 0px 0px 25px 0px rgba(146, 148, 248, 0.4);
+    }
+}
+
+.loader .intro:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient( 120deg, transparent, rgba(146, 148, 248, 0.4), transparent );
+    animation: flash 2.5s linear infinite;
+}
+
+@keyframes flash {
+    0%, 100% {
+    }
+
+    10%, 90% {
+        left: 100%;
+    }
+}
+
+.loader .intro img {
+    border-radius: 3px;
+    width: 40px;
+    margin-right: 1rem;
+}
+
+.loader .intro span {
+    color: #fff;
+    animation: title 3s linear infinite;
+}
+
+@keyframes title {
+    0%, 100% {
+        color: #fff;
+    }
+
+    60% {
+        color: #666;
+    }
+}
+
+.loader .bar {
+    width: 50%;
+    height: 4px;
+    border-radius: 2px;
+    margin: auto;
+    background: #E645D0;
+}
+
+    .loader .bar .progress {
+        width: 0%;
+        height: 4px;
+        margin: auto;
+        background: #17E1E6;
+    }
+
+.loader .logo {
+    animation: logo 5s linear infinite;
+    -moz-animation: logo 5s linear infinite;
+    /* Firefox */
+    -webkit-animation: logo 5s linear infinite;
+    /* Safari and Chrome */
+    -o-animation: logo 5s linear infinite;
+    /* Opera */
+}
+
+@keyframes logo {
+    from {
+        transform: rotate(0deg);
+    }
+
+    to {
+        transform: rotate(-360deg);
+    }
+}
+
+.loader .progress {
+    animation: progress 12s linear;
+    -moz-animation: progress 12s linear;
+    /* Firefox */
+    -webkit-animation: progress 12s linear;
+    /* Safari and Chrome */
+    -o-animation: progress 12s linear;
+    /* Opera */
+    animation: progress 12s linear infinite;
+}
+
+@keyframes progress {
+    0%, 100% {
+        width: 0%;
+        background-color: #17e1e6;
+    }
+
+    50% {
+        width: 100%;
+        background-color: #28a745;
+    }
+}
+
+.loader .common {
+    height: 5vw;
+    max-height: 100%;
+    overflow: auto;
+    width: 2vw;
+    margin: auto;
+    max-width: 100%;
+    position: absolute;
+    border-radius: 0vw 10vw 0vw 10vw;
+    box-shadow: inset 0vw 0vw 0vw .1vw #E645D0, 0vw 0vw 1.5vw 0vw #E645D0;
+}
+
+.loader .one {
+    transform: rotate(45deg);
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 7.5vw;
+}
+
+.loader .two {
+    transform: rotate(90deg);
+    left: 5.5vw;
+    right: 0;
+    top: 0;
+    bottom: 5.5vw;
+}
+
+.loader .three {
+    transform: rotate(135deg);
+    left: 7.5vw;
+    right: 0;
+    top: 0;
+    bottom: 0;
+}
+
+.loader .four {
+    transform: rotate(180deg);
+    left: 5.5vw;
+    right: 0;
+    top: 5.5vw;
+    bottom: 0;
+}
+
+.loader .five {
+    transform: rotate(225deg);
+    left: 0;
+    right: 0;
+    top: 7.5vw;
+    bottom: 0;
+}
+
+.loader .six {
+    transform: rotate(270deg);
+    left: 0;
+    right: 5.5vw;
+    top: 5.5vw;
+    bottom: 0;
+}
+
+.loader .seven {
+    transform: rotate(315deg);
+    left: 0;
+    right: 7.5vw;
+    top: 0;
+    bottom: 0;
+}
+
+.loader .eight {
+    transform: rotate(360deg);
+    left: 0;
+    right: 5.5vw;
+    top: 0;
+    bottom: 5.5vw;
+}
+
+.loader .one {
+    animation: one 1s ease infinite;
+    -moz-animation: one 1s ease infinite;
+    /* Firefox */
+    -webkit-animation: one 1s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: one 1s ease infinite;
+    /* Opera */
+}
+
+@keyframes one {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .two {
+    animation: two 1s .125s ease infinite;
+    -moz-animation: two 1s .125s ease infinite;
+    /* Firefox */
+    -webkit-animation: two 1s .125s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: two 1s .125s ease infinite;
+    /* Opera */
+}
+
+@keyframes two {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .three {
+    animation: three 1s .25s ease infinite;
+    -moz-animation: three 1s .25s ease infinite;
+    /* Firefox */
+    -webkit-animation: three 1s .25s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: three 1s .25s ease infinite;
+    /* Opera */
+}
+
+@keyframes three {
+    0%, 100% {
+    }
+
+    50% {
+        background:;
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .four {
+    animation: four 1s .375s ease infinite;
+    -moz-animation: four 1s .375s ease infinite;
+    /* Firefox */
+    -webkit-animation: four 1s .375s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: four 1s .375s ease infinite;
+    /* Opera */
+}
+
+@keyframes four {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .five {
+    animation: five 1s .5s ease infinite;
+    -moz-animation: five 1s .5s ease infinite;
+    /* Firefox */
+    -webkit-animation: five 1s .5s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: five 1s .5s ease infinite;
+    /* Opera */
+}
+
+@keyframes five {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .six {
+    animation: six 1s .625s ease infinite;
+    -moz-animation: six 1s .625s ease infinite;
+    /* Firefox */
+    -webkit-animation: six 1s .625s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: six 1s .625s ease infinite;
+    /* Opera */
+}
+
+@keyframes six {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .seven {
+    animation: seven 1s .750s ease infinite;
+    -moz-animation: seven 1s .750s ease infinite;
+    /* Firefox */
+    -webkit-animation: seven 1s .750s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: seven 1s .750s ease infinite;
+    /* Opera */
+}
+
+@keyframes seven {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+.loader .eight {
+    animation: eight 1s .875s ease infinite;
+    -moz-animation: eight 1s .875s ease infinite;
+    /* Firefox */
+    -webkit-animation: eight 1s .875s ease infinite;
+    /* Safari and Chrome */
+    -o-animation: eight 1s .875s ease infinite;
+    /* Opera */
+}
+
+@keyframes eight {
+    0%, 100% {
+    }
+
+    50% {
+        box-shadow: inset 0vw 0vw 0vw .1vw #17E1E6, 0vw 0vw 1.5vw 0vw #17E1E6;
+    }
+}
+
+@media (min-width: 768px) {
+    .loader {
+        padding-top: 8rem;
+    }
+
+        .loader .intro {
+            margin-top: 6rem;
+        }
+}

+ 53 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/svc-module.js

@@ -0,0 +1,53 @@
+const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
+var nameSpace = (await getAssemblyExports("SimpleVoiceChat.BlazorWasm.dll")).SimpleVoiceChat.BlazorWasm;
+var DotNetInterop = nameSpace.SvcModule;
+
+var isRunning = false;
+var stream;
+var recorder;
+
+export async function init() {
+    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
+        stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+    else if (navigator.webkitGetUserMedia)
+        stream = await navigator.webkitGetUserMedia({ audio: true });
+    else throw "failure to getUserMedia";
+
+    if (stream) {
+        recorder = new MediaRecorder(stream, {
+            audioBitsPerSecond: 64000,
+            mimeType: 'audio/webm;codecs=OPUS'
+        });
+
+        recorder.ondataavailable = async (r) => {
+            DotNetInterop.JsFeedChunk(new Uint8Array(await r.data.arrayBuffer()));
+        };
+
+        recorder.onstop = () => {
+            if (isRunning) {
+                recorder.start();
+                setTimeout(() => {
+                    if (isRunning) recorder.stop();
+                }, 1000);
+            }
+        };
+    }
+    return !!stream;
+}
+
+export async function start() {
+    isRunning = true;
+    recorder.start();
+    setTimeout(() => {
+        if (isRunning) recorder.stop();
+    }, 1000);
+}
+
+export async function stop() {
+    isRunning = false;
+    recorder.stop();
+}
+
+export async function createBlobUrl(bytes) {
+    return URL.createObjectURL(new Blob([bytes]));
+}

+ 18 - 0
SimpleVoiceChat.BlazorWasm/wwwroot/svc.css

@@ -0,0 +1,18 @@
+#blazor-error-ui {
+    background: lightyellow;
+    bottom: 0;
+    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+    display: none;
+    left: 0;
+    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+    position: fixed;
+    width: 100%;
+    z-index: 1080;
+}
+
+    #blazor-error-ui .dismiss {
+        cursor: pointer;
+        position: absolute;
+        right: 0.75rem;
+        top: 0.5rem;
+    }

BIN
SimpleWebChat.BlazorWasm/wwwroot/icon-192.png


+ 1 - 2
SimpleWebChat.BlazorWasm/wwwroot/index.html

@@ -10,7 +10,6 @@
     <base href="/" />
 
     <link href="lib/bootstrap/default/bootstrap.min.css" rel="stylesheet" />
-    <link href="lib/bootstrap-icons-1.9.1/bootstrap-icons.css" rel="stylesheet" />
 
     <link href="swc.css" rel="stylesheet" />
 </head>
@@ -30,7 +29,7 @@
                 <div class="eight common"></div>
             </div>
             <div class="intro">
-                <img src="blazor-192.png" />
+                <img src="icon-192.png" />
                 <span>精彩即将呈现</span>
             </div>
             <div class="bar">

+ 8 - 2
SimpleWebChat.sln

@@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 VisualStudioVersion = 17.4.32804.182
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleWebChat.ConHost", "SimpleWebChat.ConHost\SimpleWebChat.ConHost.csproj", "{4AE488F7-E3FD-4339-9C31-2BCC5AF83EDD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebChat.ConHost", "SimpleWebChat.ConHost\SimpleWebChat.ConHost.csproj", "{4AE488F7-E3FD-4339-9C31-2BCC5AF83EDD}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleWebChat.BlazorWasm", "SimpleWebChat.BlazorWasm\SimpleWebChat.BlazorWasm.csproj", "{EEBDF24C-F814-44B2-A20C-812737AA313B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebChat.BlazorWasm", "SimpleWebChat.BlazorWasm\SimpleWebChat.BlazorWasm.csproj", "{EEBDF24C-F814-44B2-A20C-812737AA313B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleVoiceChat.BlazorWasm", "SimpleVoiceChat.BlazorWasm\SimpleVoiceChat.BlazorWasm.csproj", "{012A093B-33A9-405E-979E-321C0E83336D}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,6 +23,10 @@ Global
 		{EEBDF24C-F814-44B2-A20C-812737AA313B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{EEBDF24C-F814-44B2-A20C-812737AA313B}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{EEBDF24C-F814-44B2-A20C-812737AA313B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{012A093B-33A9-405E-979E-321C0E83336D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{012A093B-33A9-405E-979E-321C0E83336D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{012A093B-33A9-405E-979E-321C0E83336D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{012A093B-33A9-405E-979E-321C0E83336D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 0
SimpleWebChat.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Wasm/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>