WIP: First implementation of audio recording

This commit is contained in:
Thorsten Sommer 2026-01-01 17:55:05 +01:00
parent 47975870b4
commit b1336abbfd
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 119 additions and 2 deletions

View File

@ -0,0 +1,33 @@
using AIStudio.Tools.Services;
namespace AIStudio;
public static class AudioRecorderHandler
{
public static void AddAudioRecorderHandlers(this IEndpointRouteBuilder app)
{
var router = app.MapGroup("/audio");
router.MapPost("/upload", UploadAudio)
.DisableAntiforgery();
}
private static async Task<IResult> UploadAudio(IFormFile audio, RustService rustService)
{
if (audio.Length == 0)
return Results.BadRequest();
var dataDirectory = await rustService.GetDataDirectory();
var recordingDirectory = Path.Combine(dataDirectory, "audioRecordings");
if(!Path.Exists(recordingDirectory))
Directory.CreateDirectory(recordingDirectory);
var fileName = $"recording_{DateTime.UtcNow:yyyyMMdd_HHmmss}.webm";
var filePath = Path.Combine(recordingDirectory, fileName);
await using var stream = File.Create(filePath);
await audio.CopyToAsync(stream);
return Results.Ok(new { FileName = fileName });
}
}

View File

@ -20,6 +20,13 @@
</MudNavLink>
}
</MudNavMenu>
<MudToggleIconButton Toggled="@this.isRecording"
ToggledChanged="@this.OnRecordingToggled"
Icon="@Icons.Material.Filled.Mic"
ToggledIcon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
ToggledColor="Color.Error" />
</MudDrawer>
</MudDrawerContainer>
}
@ -41,6 +48,13 @@
}
}
</MudNavMenu>
<MudToggleIconButton Toggled="@this.isRecording"
ToggledChanged="@this.OnRecordingToggled"
Icon="@Icons.Material.Filled.Mic"
ToggledIcon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
ToggledColor="Color.Error" />
</MudPaper>
}
}

View File

@ -39,6 +39,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
[Inject]
private MudTheme ColorTheme { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private HttpClient HttpClient { get; init; } = null!;
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
@ -53,6 +59,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private UpdateResponse? currentUpdateResponse;
private MudThemeProvider themeProvider = null!;
private bool useDarkMode;
private bool isRecording;
private IReadOnlyCollection<NavBarItem> navItems = [];
@ -341,6 +348,33 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
await this.MessageBus.SendMessage<bool>(this, Event.COLOR_THEME_CHANGED);
this.StateHasChanged();
}
private async Task OnRecordingToggled(bool toggled)
{
if (toggled)
{
await this.JsRuntime.InvokeVoidAsync("audioRecorder.start");
this.isRecording = true;
}
else
{
var base64Audio = await this.JsRuntime.InvokeAsync<string>("audioRecorder.stop");
this.isRecording = false;
this.StateHasChanged();
await this.SendAudioToBackend(base64Audio);
}
}
private async Task SendAudioToBackend(string base64Audio)
{
var audioBytes = Convert.FromBase64String(base64Audio);
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(audioBytes), "audio", "recording.webm");
await this.HttpClient.PostAsync("/audio/upload", content);
}
#region Implementation of IDisposable

View File

@ -83,7 +83,6 @@ internal sealed class Program
}
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
{
kestrelServerOptions.ConfigureEndpointDefaults(listenOptions =>
@ -193,6 +192,7 @@ internal sealed class Program
programLogger.LogInformation("Initialize internal file system.");
app.Use(Redirect.HandlerContentAsync);
app.Use(FileHandler.HandlerAsync);
app.AddAudioRecorderHandlers();
#if DEBUG
app.UseStaticFiles();

View File

@ -25,4 +25,40 @@ window.clearDiv = function (divName) {
window.scrollToBottom = function(element) {
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
}
}
let mediaRecorder;
let audioChunks = [];
window.audioRecorder = {
start: async function () {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.start();
},
stop: async function () {
return new Promise((resolve) => {
mediaRecorder.onstop = async () => {
const blob = new Blob(audioChunks, { type: 'audio/webm' });
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
);
// Tracks stoppen, damit das Mic-Icon verschwindet
mediaRecorder.stream.getTracks().forEach(track => track.stop());
resolve(base64);
};
mediaRecorder.stop();
});
}
};