Snippet
Ingest live audio over WebSocket
A complete ASP.NET Core endpoint that authenticates a native helper WebSocket before accepting binary PCM frames.
AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy
What it does#
Authenticates a helper’s live-audio WebSocket request before accepting the upgrade. Once connected, it accepts binary frames only, reassembles fragmented messages, and forwards each PCM frame to an active transcription session.
You need#
- .NET SDK 10
- A project with
ImplicitUsingsenabled (thedotnet newdefault) - ASP.NET Core shared framework from
Microsoft.NET.Sdk.Web - A short-lived session-token store
- A native helper that sends
Sec-WebSocket-Protocol: thinkcubator.v1, thinkcubator.bearer.<token> - A live transcription service that accepts PCM frames
Where it goes#
Save it in the API project as LiveAudioWebSocketEndpoint.cs, register your services, and map it with:
app.UseWebSockets();
app.MapGet("/ws/meetings/{meetingSessionId:guid}/live-audio", LiveAudioWebSocketEndpoint.HandleAsync);
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
using System.Collections.Concurrent;
using System.Net;
using System.Net.WebSockets;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
public static class LiveAudioWebSocketEndpoint
{
private const int ReceiveBufferBytes = 8 * 1024;
private const long MaxMessageBytes = 1024 * 1024;
private const string AcceptedProtocol = "thinkcubator.v1";
private const string BearerProtocolPrefix = "thinkcubator.bearer.";
public static async Task HandleAsync(HttpContext context, Guid meetingSessionId)
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
var sessions = context.RequestServices.GetRequiredService<IHelperSessionStore>();
if (!TryAuthenticate(context, meetingSessionId, sessions))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
if (!IsOriginAllowed(context))
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
var gates = context.RequestServices.GetRequiredService<LiveAudioConnectionGates>();
if (!gates.TryBegin(meetingSessionId))
{
context.Response.StatusCode = (int)HttpStatusCode.Conflict;
return;
}
try
{
var transcription = context.RequestServices.GetRequiredService<ILiveAudioSink>();
using var socket = await context.WebSockets.AcceptWebSocketAsync(AcceptedProtocol);
await PumpAsync(socket, meetingSessionId, transcription, context.RequestAborted);
}
finally
{
gates.End(meetingSessionId);
}
}
private static async Task PumpAsync(
WebSocket socket,
Guid meetingSessionId,
ILiveAudioSink transcription,
CancellationToken cancellationToken)
{
var buffer = new byte[ReceiveBufferBytes];
while (socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
WebSocketReceiveResult result;
try
{
result = await socket.ReceiveAsync(buffer, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (WebSocketException)
{
break;
}
if (result.MessageType == WebSocketMessageType.Close)
{
await TryCloseAsync(socket);
break;
}
if (result.MessageType != WebSocketMessageType.Binary)
{
continue;
}
using var frame = new MemoryStream();
frame.Write(buffer, 0, result.Count);
if (frame.Length > MaxMessageBytes)
{
await TryCloseAsync(socket, WebSocketCloseStatus.MessageTooBig, "audio message too large");
break;
}
while (!result.EndOfMessage)
{
result = await socket.ReceiveAsync(buffer, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
{
await TryCloseAsync(socket, WebSocketCloseStatus.NormalClosure, "ingest closed");
return;
}
frame.Write(buffer, 0, result.Count);
if (frame.Length > MaxMessageBytes)
{
await TryCloseAsync(socket, WebSocketCloseStatus.MessageTooBig, "audio message too large");
return;
}
}
if (frame.Length > 0)
{
await transcription.WriteAudioAsync(meetingSessionId, frame.ToArray(), cancellationToken);
}
}
}
private static Task TryCloseAsync(WebSocket socket) =>
TryCloseAsync(socket, WebSocketCloseStatus.NormalClosure, "ingest closed");
private static async Task TryCloseAsync(WebSocket socket, WebSocketCloseStatus status, string description)
{
if (socket.State is not (WebSocketState.Open or WebSocketState.CloseReceived))
{
return;
}
try
{
await socket.CloseAsync(status, description, CancellationToken.None);
}
catch (WebSocketException)
{
}
catch (OperationCanceledException)
{
}
}
private static bool TryAuthenticate(HttpContext context, Guid meetingSessionId, IHelperSessionStore sessions)
{
var token = ExtractToken(context);
if (string.IsNullOrWhiteSpace(token))
{
return false;
}
if (!sessions.TryGet(token, out var session))
{
return false;
}
return session.MeetingSessionId == meetingSessionId
&& !session.Revoked
&& session.ExpiresAtUtc >= DateTime.UtcNow;
}
private static string? ExtractToken(HttpContext context)
{
if (!context.WebSockets.WebSocketRequestedProtocols.Contains(AcceptedProtocol))
{
return null;
}
return context.WebSockets.WebSocketRequestedProtocols
.FirstOrDefault(protocol => protocol.StartsWith(BearerProtocolPrefix, StringComparison.Ordinal))
?[BearerProtocolPrefix.Length..];
}
private static bool IsOriginAllowed(HttpContext context)
{
var origin = context.Request.Headers.Origin.ToString();
return string.IsNullOrEmpty(origin)
|| origin is "http://localhost:5173" or "http://127.0.0.1:5173";
}
}
public interface ILiveAudioSink
{
Task WriteAudioAsync(Guid meetingSessionId, byte[] pcmFrame, CancellationToken cancellationToken);
}
public interface IHelperSessionStore
{
bool TryGet(string token, out HelperSession session);
}
public sealed record HelperSession(Guid MeetingSessionId, DateTime ExpiresAtUtc, bool Revoked);
public sealed class LiveAudioConnectionGates
{
private readonly ConcurrentDictionary<Guid, byte> _active = new();
public bool TryBegin(Guid meetingSessionId) => _active.TryAdd(meetingSessionId, 0);
public void End(Guid meetingSessionId) => _active.TryRemove(meetingSessionId, out _);
}
Check that it works#
Call the endpoint without a WebSocket upgrade. It returns HTTP 400. Then connect with
Sec-WebSocket-Protocol: thinkcubator.v1, thinkcubator.bearer.<token> and send a binary
PCM frame; your ILiveAudioSink receives the frame. The server echoes only
thinkcubator.v1.