Snippet

Ingest live audio over WebSocket

A complete ASP.NET Core endpoint that authenticates a native helper WebSocket before accepting binary PCM frames.

csharpOct 6, 2026MIT

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 ImplicitUsings enabled (the dotnet new default)
  • 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:

Program.cs (excerpt)
app.UseWebSockets();
app.MapGet("/ws/meetings/{meetingSessionId:guid}/live-audio", LiveAudioWebSocketEndpoint.HandleAsync);
LiveAudioWebSocketEndpoint.cs
// 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.