feature: Phase 18.3 — Opus delivery transport (?format= stream + seek sidecar endpoint)

This commit is contained in:
daniel-c-harvey
2026-06-23 08:34:37 -04:00
parent e807ddb91b
commit 740d01a67f
4 changed files with 462 additions and 15 deletions
+75 -2
View File
@@ -5,6 +5,7 @@ using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Services;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.Processors;
using DeepDrftContent.Processors.Opus;
using DeepDrftData;
using DeepDrftModels.DTOs;
using DeepDrftModels.Enums;
@@ -20,6 +21,7 @@ public class TrackController : ControllerBase
private readonly UnifiedTrackService _unifiedService;
private readonly ITrackService _sqlTrackService;
private readonly WaveformProfileService _waveformProfileService;
private readonly TrackFormatResolver _formatResolver;
private readonly UploadStagingDirectory _stagingDirectory;
private readonly ILogger<TrackController> _logger;
@@ -35,6 +37,7 @@ public class TrackController : ControllerBase
UnifiedTrackService unifiedService,
ITrackService sqlTrackService,
WaveformProfileService waveformProfileService,
TrackFormatResolver formatResolver,
UploadStagingDirectory stagingDirectory,
ILogger<TrackController> logger)
{
@@ -43,6 +46,7 @@ public class TrackController : ControllerBase
_unifiedService = unifiedService;
_sqlTrackService = sqlTrackService;
_waveformProfileService = waveformProfileService;
_formatResolver = formatResolver;
_stagingDirectory = stagingDirectory;
_logger = logger;
}
@@ -642,10 +646,27 @@ public class TrackController : ControllerBase
// --- Parameterized routes ---
// GET api/track/{trackId}?format=opus|lossless (unauthenticated)
// Streams the track's audio bytes with HTTP Range support. The optional `format` selector (Phase 18.3)
// picks the delivery rendering: absent or unrecognized ⇒ Lossless (byte-identical to pre-Phase-18 —
// the existing zero-copy disk-stream path, untouched); `opus` ⇒ the derived Ogg Opus 320 artifact
// when present, falling back to lossless when it is not (C2 — never 404/silence). The Opus path serves
// the resolved in-memory bytes via File(..., enableRangeProcessing: true) so Range: bytes=X- still
// yields 206 (load-bearing for streaming + seek), matching the lossless disk-stream's range behavior.
[HttpGet("{trackId}")]
public async Task<ActionResult> GetTrack(string trackId)
public async Task<ActionResult> GetTrack(string trackId, [FromQuery] string? format = null)
{
_logger.LogInformation("GetTrack called with trackId: {TrackId}", trackId);
_logger.LogInformation("GetTrack called with trackId: {TrackId}, format: {Format}", trackId, format);
// Only `opus` diverges from today's behavior; everything else (null, "lossless", garbage) takes the
// unchanged lossless disk-stream path below, preserving the large-file zero-copy streaming. Routing
// lossless through the resolver would force the whole source (up to ~1 GB) into memory per request —
// a regression the resolver's in-memory byte[] result is fine for Opus (small) but not for lossless.
if (Enum.TryParse<AudioFormat>(format, ignoreCase: true, out var requestedFormat)
&& requestedFormat == AudioFormat.Opus)
{
return await GetTrackOpusAsync(trackId);
}
try
{
@@ -700,6 +721,58 @@ public class TrackController : ControllerBase
}
}
// The ?format=opus arm of GetTrack. Resolves the Opus artifact (or the lossless fallback when none
// exists, C2) via TrackFormatResolver and serves the resolved bytes with explicit range processing.
// enableRangeProcessing:true is the load-bearing detail the 18.2 reviewer flagged: File(byte[], ...)
// does NOT get ASP.NET's automatic range handling unless asked, so without this flag a Range: bytes=X-
// would silently return the whole body as 200 instead of a 206 slice — breaking seek for the Opus path
// (and Phase 21 windowing). The resolver reports the *actually-served* format via ResolvedAudio, so the
// content-type matches the bytes (audio/ogg on a hit, the source MIME on a fallback) and the eventual
// client decoder dispatches correctly.
private async Task<ActionResult> GetTrackOpusAsync(string trackId)
{
try
{
var resolved = await _formatResolver.ResolveAsync(trackId, AudioFormat.Opus);
if (resolved is null)
{
_logger.LogWarning("Track not found for Opus request: {TrackId}", trackId);
return NotFound();
}
_logger.LogInformation(
"Streaming track {TrackId} as {Format} ({Size} bytes, {ContentType})",
trackId, resolved.ResolvedFormat, resolved.Audio.Buffer.Length, resolved.ContentType);
return File(resolved.Audio.Buffer, resolved.ContentType, enableRangeProcessing: true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving track as Opus: {TrackId}", trackId);
return StatusCode(500, "Internal server error");
}
}
// GET api/track/{trackId}/opus/seekdata (unauthenticated)
// Returns the Opus setup-header + granule→byte seek-index sidecar bytes (Phase 18.3). The client
// fetches this once on track load and parses it into OpusSeekData (18.4) before issuing any Opus seek.
// Raw octet-stream — the bytes are the OpusSidecar blob exactly as 18.1 stored them. 404 when no sidecar
// is stored (no Opus artifact yet, or an older derive predating the sidecar); the client then degrades
// to lossless, mirroring the C2 posture of the audio path. Same public auth posture as the audio stream.
// The "opus/seekdata" literal suffix keeps this distinct from the audio and waveform routes.
[HttpGet("{trackId}/opus/seekdata")]
public async Task<ActionResult> GetOpusSeekData(string trackId)
{
var sidecar = await _formatResolver.GetOpusSidecarAsync(trackId);
if (sidecar is null)
{
_logger.LogInformation("No Opus sidecar for track: {TrackId}", trackId);
return NotFound();
}
return File(sidecar, "application/octet-stream");
}
// GET api/track/{trackId}/waveform (unauthenticated)
// Returns the stored waveform loudness profile for a track, base64-encoded. Public listener
// data, same auth posture as GET api/track/{trackId} streaming. 404 when no profile is stored