Merge Phase 18.2 (Opus format resolution + sidecar lookup contract) into streaming-overhaul

This commit is contained in:
daniel-c-harvey
2026-06-23 07:49:28 -04:00
5 changed files with 323 additions and 0 deletions
+5
View File
@@ -77,6 +77,11 @@ namespace DeepDrftAPI
builder.Services.AddSingleton<FfmpegOpusEncoder>();
builder.Services.AddSingleton<OpusTranscodeService>();
// Opus delivery format resolution + sidecar lookup (Phase 18.2). The seam 18.3 calls behind
// the ?format= stream param and the sidecar path. Stateless over the FileDatabase + content
// service singletons; the lossless branch reuses the existing read path unchanged (C2).
builder.Services.AddSingleton<TrackFormatResolver>();
return Task.CompletedTask;
}
@@ -0,0 +1,23 @@
using DeepDrftContent.FileDatabase.Models;
using DeepDrftModels.Enums;
namespace DeepDrftContent.Processors.Opus;
/// <summary>
/// The outcome of resolving a track + requested <see cref="AudioFormat"/> to a concrete artifact
/// (Phase 18.2). Carries the bytes, the content-type that matches <em>what was actually returned</em>,
/// and the format actually served — which may differ from the requested one when the C2 fallback fires
/// (Opus requested, no Opus artifact → the lossless artifact + its content-type). The delivery layer
/// (18.3) sets the response <c>Content-Type</c> from <see cref="ContentType"/> so the eventual decoder
/// picks the right decoder for the bytes it receives, not the bytes the listener asked for.
/// </summary>
/// <param name="Audio">The resolved audio artifact (never null when a resolution succeeds).</param>
/// <param name="ContentType">The MIME type of <paramref name="Audio"/> (e.g. <c>audio/ogg</c> for Opus,
/// or the source's real MIME for lossless).</param>
/// <param name="ResolvedFormat">The format actually returned. Equal to the requested format on a direct
/// hit; <see cref="AudioFormat.Lossless"/> when an Opus request fell back.</param>
public sealed record ResolvedAudio(AudioBinary Audio, string ContentType, AudioFormat ResolvedFormat)
{
/// <summary>True when an Opus request was served the lossless artifact because no Opus existed (C2).</summary>
public bool DidFallBack(AudioFormat requested) => requested != ResolvedFormat;
}
@@ -0,0 +1,91 @@
using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftModels.Enums;
using Microsoft.Extensions.Logging;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
namespace DeepDrftContent.Processors.Opus;
/// <summary>
/// The server-side format resolution + sidecar lookup seam (Phase 18.2). Given a track's
/// <c>EntryKey</c> and a requested <see cref="AudioFormat"/>, returns the correct audio artifact and the
/// content-type that matches it; given an <c>EntryKey</c>, returns the Opus seek/setup sidecar bytes.
/// Downstream waves call this — 18.3 wires it behind the <c>?format=</c> stream param and serves the
/// sidecar over HTTP; this wave delivers only the seam, not the HTTP surface.
/// <para>
/// Additive and non-breaking (C2): the lossless branch reads the source exactly as the existing stream
/// path does (via <see cref="TrackContentService.GetAudioBinaryAsync"/>), and an Opus request for a track
/// with no Opus artifact falls back to lossless rather than failing. Mirrors the
/// <see cref="WaveformProfileService"/> derived-artifact lookup precedent: read from the dedicated vault,
/// swallow misses to null (FileDatabase convention), let the caller decide.
/// </para>
/// </summary>
public sealed class TrackFormatResolver
{
private readonly FileDb _fileDatabase;
private readonly TrackContentService _trackContentService;
private readonly ILogger<TrackFormatResolver> _logger;
public TrackFormatResolver(
FileDb fileDatabase,
TrackContentService trackContentService,
ILogger<TrackFormatResolver> logger)
{
_fileDatabase = fileDatabase;
_trackContentService = trackContentService;
_logger = logger;
}
/// <summary>
/// Resolves <paramref name="entryKey"/> + <paramref name="requestedFormat"/> to the audio artifact to
/// serve plus its content-type. <see cref="AudioFormat.Lossless"/> resolves the source artifact in the
/// <c>tracks</c> vault with its real MIME (WAV/MP3/FLAC). <see cref="AudioFormat.Opus"/> resolves the
/// derived Opus artifact (<c>audio/ogg</c>) when present, and <strong>falls back to lossless</strong>
/// when it is not (C2). Returns null only when even the lossless source is missing — i.e. the track has
/// no audio at all (an unknown key or a genuinely empty vault), the one case the caller treats as 404.
/// </summary>
public async Task<ResolvedAudio?> ResolveAsync(string entryKey, AudioFormat requestedFormat)
{
if (requestedFormat == AudioFormat.Opus)
{
var opus = await _fileDatabase.LoadResourceAsync<AudioBinary>(
VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey));
if (opus is not null)
return new ResolvedAudio(opus, MimeTypeExtensions.GetMimeType(opus.Extension), AudioFormat.Opus);
// C2 fallback: no Opus artifact yet (legacy row, not backfilled, or transcode failed). Degrade
// to lossless rather than 404 — Opus is strictly additive; its absence never means "no audio".
_logger.LogInformation(
"Opus requested for {EntryKey} but no Opus artifact exists; falling back to lossless.", entryKey);
}
return await ResolveLosslessAsync(entryKey);
}
/// <summary>
/// Resolves the lossless source artifact and its real MIME — the existing read path, unchanged. Shared
/// by the explicit-lossless branch and the Opus fallback so both produce identical bytes + content-type.
/// </summary>
private async Task<ResolvedAudio?> ResolveLosslessAsync(string entryKey)
{
var source = await _trackContentService.GetAudioBinaryAsync(entryKey);
if (source is null)
return null;
return new ResolvedAudio(source, MimeTypeExtensions.GetMimeType(source.Extension), AudioFormat.Lossless);
}
/// <summary>
/// Returns the Opus setup-header + seek-index sidecar bytes for <paramref name="entryKey"/>, or null
/// when no sidecar is stored (no Opus artifact yet, or an older derive predating the sidecar). 18.3
/// serves these on their own path; 18.4 fetches them once on track load and parses them into the
/// client's <c>OpusSeekData</c>. The bytes are the raw <see cref="OpusSidecar"/> blob
/// (<c>[uint32 setupHeaderLength][setup-header][seek-index]</c>) exactly as 18.1 stored them.
/// </summary>
public async Task<byte[]?> GetOpusSidecarAsync(string entryKey)
{
var sidecar = await _fileDatabase.LoadResourceAsync<MediaBinary>(
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey));
return sidecar?.Buffer;
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace DeepDrftModels.Enums;
/// <summary>
/// The delivery format a listener requests for a track's audio (Phase 18). One <c>TrackEntity</c> /
/// <c>EntryKey</c> addresses both renderings — "one source, multiple views" applied to delivery (C5).
/// Lives here, not in the content library, because it is a cross-boundary contract: the API stream
/// endpoint (18.3) parses it off the <c>?format=</c> query param, the WASM client (18.4 / 18.6) selects
/// it, and the content-side resolver (18.2) resolves it to bytes — all three reference one enum.
/// </summary>
public enum AudioFormat
{
/// <summary>
/// The existing source artifact in the <c>tracks</c> vault, served byte-for-byte with its real MIME
/// (WAV/MP3/FLAC — do not assume WAV). The universal, always-present rendering.
/// </summary>
Lossless,
/// <summary>
/// The derived low-data Ogg Opus 320 artifact in the <c>track-opus</c> vault (<c>audio/ogg</c>). A
/// best-effort derived artifact: when absent (not yet transcoded, or transcode failed) a request for
/// it falls back to <see cref="Lossless"/> rather than 404ing (C2).
/// </summary>
Opus
}
+180
View File
@@ -0,0 +1,180 @@
using DeepDrftContent;
using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.Processors;
using DeepDrftContent.Processors.Opus;
using DeepDrftModels.Enums;
using Microsoft.Extensions.Logging.Abstractions;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
namespace DeepDrftTests;
/// <summary>
/// Integration tests for the Phase 18.2 format resolution + sidecar lookup seam
/// (<see cref="TrackFormatResolver"/>) over a real <see cref="FileDb"/>. They exercise the four
/// resolution branches the brief specifies — lossless, Opus hit, the C2 Opus→lossless fallback, and
/// the unknown-track miss — plus sidecar hit/miss. Artifacts are seeded into the vaults exactly as
/// 18.1's <see cref="OpusTranscodeService"/> stores them (Opus audio under the bare EntryKey, sidecar
/// under the <c>-sidecar</c>-suffixed key, the source in the <c>tracks</c> vault), so the test is faithful
/// to the real storage convention rather than a stand-in.
/// </summary>
[TestFixture]
public class TrackFormatResolverTests
{
private string _testDir = string.Empty;
[SetUp]
public void SetUp()
{
_testDir = Path.Combine(Path.GetTempPath(), "TrackFormatResolverTests", Guid.NewGuid().ToString());
Directory.CreateDirectory(_testDir);
}
[TearDown]
public void TearDown()
{
try { Directory.Delete(_testDir, recursive: true); }
catch { /* Best-effort cleanup — ignore failures */ }
}
private static TrackFormatResolver CreateResolver(FileDb fileDatabase)
{
// The resolver only calls GetAudioBinaryAsync (a vault read), which never touches the router —
// but TrackContentService requires one, so supply a real router with real processors.
var router = new AudioProcessorRouter(
new AudioProcessor(), new Mp3AudioProcessor(), new FlacAudioProcessor());
var contentService = new TrackContentService(fileDatabase, router);
return new TrackFormatResolver(
fileDatabase, contentService, NullLogger<TrackFormatResolver>.Instance);
}
// Seeds a source artifact in the tracks vault with the given extension, mirroring how the upload path
// stores the original bytes (WAV/MP3/FLAC). Returns the bytes for downstream identity assertions.
private static async Task<byte[]> SeedSourceAsync(FileDb db, string entryKey, string extension)
{
await db.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
var bytes = new byte[] { 1, 2, 3, 4, 5 };
var audio = new AudioBinary(new AudioBinaryParams(bytes, bytes.Length, extension, 12.0, 1411));
var ok = await db.RegisterResourceAsync(VaultConstants.Tracks, entryKey, audio);
Assert.That(ok, Is.True, "source seed must succeed");
return bytes;
}
// Seeds the Opus audio + sidecar in the track-opus vault exactly as OpusTranscodeService does:
// audio under OpusAudioKey (the bare EntryKey) with the .opus extension, sidecar under OpusSidecarKey.
private static async Task<(byte[] opus, byte[] sidecar)> SeedOpusAsync(FileDb db, string entryKey)
{
await db.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio);
var opusBytes = new byte[] { 9, 9, 9 };
var opusAudio = new AudioBinary(new AudioBinaryParams(
opusBytes, opusBytes.Length, OggOpusConstants.OpusExtension, 12.0, 320));
var audioOk = await db.RegisterResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey), opusAudio);
Assert.That(audioOk, Is.True, "opus audio seed must succeed");
var sidecarBytes = new byte[] { 7, 7, 7, 7 };
var sidecar = new MediaBinary(new MediaBinaryParams(
sidecarBytes, sidecarBytes.Length, OggOpusConstants.SidecarExtension));
var sidecarOk = await db.RegisterResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey), sidecar);
Assert.That(sidecarOk, Is.True, "sidecar seed must succeed");
return (opusBytes, sidecarBytes);
}
[Test]
public async Task ResolveAsync_Lossless_ReturnsSourceArtifactWithItsRealMime()
{
var db = (await FileDb.FromAsync(_testDir))!;
const string entryKey = "lossless-track";
// A FLAC source — proves the lossless branch does NOT assume WAV: the content-type tracks the
// stored extension's MIME, not a hard-coded audio/wav.
var bytes = await SeedSourceAsync(db, entryKey, ".flac");
var resolver = CreateResolver(db);
var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Lossless);
Assert.That(resolved, Is.Not.Null);
Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Lossless));
Assert.That(resolved.ContentType, Is.EqualTo("audio/flac"));
Assert.That(resolved.Audio.Buffer, Is.EqualTo(bytes));
Assert.That(resolved.DidFallBack(AudioFormat.Lossless), Is.False);
}
[Test]
public async Task ResolveAsync_OpusWhenArtifactExists_ReturnsOpusWithOggContentType()
{
var db = (await FileDb.FromAsync(_testDir))!;
const string entryKey = "opus-track";
await SeedSourceAsync(db, entryKey, ".wav");
var (opusBytes, _) = await SeedOpusAsync(db, entryKey);
var resolver = CreateResolver(db);
var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Opus);
Assert.That(resolved, Is.Not.Null);
Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Opus));
Assert.That(resolved.ContentType, Is.EqualTo("audio/ogg"));
Assert.That(resolved.Audio.Buffer, Is.EqualTo(opusBytes));
Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.False);
}
[Test]
public async Task ResolveAsync_OpusWhenNoArtifact_FallsBackToLosslessNeverNull()
{
var db = (await FileDb.FromAsync(_testDir))!;
const string entryKey = "no-opus-track";
// Source exists; no Opus artifact has been derived. The C2 rule: degrade to lossless, never 404.
var bytes = await SeedSourceAsync(db, entryKey, ".wav");
var resolver = CreateResolver(db);
var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Opus);
Assert.That(resolved, Is.Not.Null, "Opus absence must degrade to lossless, not null/404");
Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Lossless),
"the resolved format must reflect what was actually returned");
Assert.That(resolved.ContentType, Is.EqualTo("audio/wav"),
"a fallback returns the lossless content-type so the decoder picks the right decoder");
Assert.That(resolved.Audio.Buffer, Is.EqualTo(bytes));
Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.True);
}
[Test]
public async Task ResolveAsync_UnknownTrack_ReturnsNull()
{
var db = (await FileDb.FromAsync(_testDir))!;
var resolver = CreateResolver(db);
// No source at all — the one case the caller maps to 404. Holds for both requested formats:
// Opus falls back to lossless, finds nothing, and returns null too.
Assert.That(await resolver.ResolveAsync("ghost", AudioFormat.Lossless), Is.Null);
Assert.That(await resolver.ResolveAsync("ghost", AudioFormat.Opus), Is.Null);
}
[Test]
public async Task GetOpusSidecarAsync_WhenPresent_ReturnsBytes()
{
var db = (await FileDb.FromAsync(_testDir))!;
const string entryKey = "sidecar-track";
var (_, sidecarBytes) = await SeedOpusAsync(db, entryKey);
var resolver = CreateResolver(db);
var bytes = await resolver.GetOpusSidecarAsync(entryKey);
Assert.That(bytes, Is.Not.Null);
Assert.That(bytes, Is.EqualTo(sidecarBytes));
}
[Test]
public async Task GetOpusSidecarAsync_WhenAbsent_ReturnsNull()
{
var db = (await FileDb.FromAsync(_testDir))!;
var resolver = CreateResolver(db);
// No Opus artifacts derived for this track — the sidecar lookup misses to null, not an exception.
var bytes = await resolver.GetOpusSidecarAsync("no-sidecar-track");
Assert.That(bytes, Is.Null);
}
}