Stream Opus/derived read path: serve from seekable disk FileStream, never a whole-file byte[]; HasOpusAsync is index-only

This commit is contained in:
daniel-c-harvey
2026-06-26 14:58:11 -04:00
parent 1e17ffc380
commit d72263aea1
5 changed files with 241 additions and 61 deletions
+32 -12
View File
@@ -705,9 +705,10 @@ public class TrackController : ControllerBase
// 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.
// when present, falling back to lossless when it is not (C2 — never 404/silence). The Opus path streams
// the resolved artifact from a seekable disk FileStream via File(..., enableRangeProcessing: true)
// no whole-file byte[] — 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, [FromQuery] string? format = null)
{
@@ -777,13 +778,16 @@ 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.
// exists, C2) via TrackFormatResolver and streams the resolved bytes from a seekable disk FileStream —
// never a whole-file byte[] (a ~220 MB Opus / ~970 MB lossless managed allocation per request was the
// read-path OOM defect this closes). enableRangeProcessing:true is load-bearing: the seekable FileStream
// lets ASP.NET honour Range: bytes=X- with a 206 slice (seek + 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.
//
// Disposal mirrors the lossless GetTrack path exactly: File() takes ownership of the stream on success
// and disposes it after the response; the inner try disposes ResolvedAudio (and its FileStream) on any
// pre-handoff throw so a handle never leaks.
private async Task<ActionResult> GetTrackOpusAsync(string trackId)
{
try
@@ -795,11 +799,27 @@ public class TrackController : ControllerBase
return NotFound();
}
string contentType;
long streamLength;
Stream innerStream;
try
{
contentType = resolved.ContentType;
// Length from the seekable FileStream — a metadata read, not a body load.
streamLength = resolved.Stream.Length;
innerStream = resolved.Stream;
}
catch
{
await resolved.DisposeAsync();
throw;
}
_logger.LogInformation(
"Streaming track {TrackId} as {Format} ({Size} bytes, {ContentType})",
trackId, resolved.ResolvedFormat, resolved.Audio.Buffer.Length, resolved.ContentType);
trackId, resolved.ResolvedFormat, streamLength, contentType);
return File(resolved.Audio.Buffer, resolved.ContentType, enableRangeProcessing: true);
return File(innerStream, contentType, enableRangeProcessing: true);
}
catch (Exception ex)
{