Stream Opus/derived read path: serve from seekable disk FileStream, never a whole-file byte[]; HasOpusAsync is index-only
This commit is contained in:
@@ -1,23 +1,36 @@
|
||||
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.
|
||||
/// (Phase 18.2; read-path streaming). Carries an <em>open, seekable, disk-backed</em> <see cref="Stream"/>
|
||||
/// over the artifact's bytes — never a buffered <c>byte[]</c>, so a ~220 MB Opus file or ~970 MB lossless
|
||||
/// source is never materialized in a managed array per request. Also carries 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.
|
||||
/// <para>
|
||||
/// Ownership: the resolver opens the stream; the caller takes ownership. On the success path the caller hands
|
||||
/// <see cref="Stream"/> to <c>File(..., enableRangeProcessing: true)</c>, which disposes it after the
|
||||
/// response. On any pre-handoff throw the caller disposes this instance (which disposes the stream) so the
|
||||
/// underlying <see cref="FileStream"/> never leaks — mirroring the lossless disk-stream path's catch-path
|
||||
/// disposal.
|
||||
/// </para>
|
||||
/// </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="Stream">An open, seekable, disk-backed stream over the resolved artifact. The caller owns it.</param>
|
||||
/// <param name="ContentType">The MIME type of the bytes in <paramref name="Stream"/> (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)
|
||||
public sealed record ResolvedAudio(Stream Stream, string ContentType, AudioFormat ResolvedFormat)
|
||||
: IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>True when an Opus request was served the lossless artifact because no Opus existed (C2).</summary>
|
||||
public bool DidFallBack(AudioFormat requested) => requested != ResolvedFormat;
|
||||
|
||||
public void Dispose() => Stream.Dispose();
|
||||
|
||||
public ValueTask DisposeAsync() => Stream.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -13,11 +13,17 @@ namespace DeepDrftContent.Processors.Opus;
|
||||
/// 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.
|
||||
/// Additive and non-breaking (C2): the lossless branch streams the source exactly as the existing read
|
||||
/// path does (via <see cref="TrackContentService.OpenAudioMediaStreamAsync"/>, a non-buffering disk
|
||||
/// stream), 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>
|
||||
/// <para>
|
||||
/// Read-path streaming: artifacts are resolved as open, seekable, disk-backed <see cref="ResolvedAudio"/>
|
||||
/// handles — never whole-file <c>byte[]</c> loads — so the delivery layer streams them straight to the
|
||||
/// response (Range/206 honoured by the seekable <c>FileStream</c>) without buffering a ~220 MB Opus file
|
||||
/// or a ~970 MB lossless source per request.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class TrackFormatResolver
|
||||
@@ -48,10 +54,16 @@ public sealed class TrackFormatResolver
|
||||
{
|
||||
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);
|
||||
var opusVault = _fileDatabase.GetVault(VaultConstants.TrackOpus);
|
||||
if (opusVault is not null)
|
||||
{
|
||||
// Disk-backed, seekable stream over the Opus artifact — no whole-file buffer. The caller
|
||||
// owns the stream (hands it to File(...) on success, disposes on a pre-handoff throw).
|
||||
var opus = await opusVault.GetEntryStreamAsync(OpusTranscodeService.OpusAudioKey(entryKey));
|
||||
if (opus is not null)
|
||||
return new ResolvedAudio(
|
||||
opus.Stream, 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".
|
||||
@@ -63,16 +75,18 @@ public sealed class TrackFormatResolver
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Resolves the lossless source artifact and its real MIME as a non-buffering disk stream — the existing
|
||||
/// read path. Shared by the explicit-lossless branch and the Opus fallback so both produce identical
|
||||
/// bytes + content-type. The returned stream is seekable, so the delivery layer's Range→206 still works.
|
||||
/// </summary>
|
||||
private async Task<ResolvedAudio?> ResolveLosslessAsync(string entryKey)
|
||||
{
|
||||
var source = await _trackContentService.GetAudioBinaryAsync(entryKey);
|
||||
var source = await _trackContentService.OpenAudioMediaStreamAsync(entryKey);
|
||||
if (source is null)
|
||||
return null;
|
||||
|
||||
return new ResolvedAudio(source, MimeTypeExtensions.GetMimeType(source.Extension), AudioFormat.Lossless);
|
||||
return new ResolvedAudio(
|
||||
source.Stream, MimeTypeExtensions.GetMimeType(source.Extension), AudioFormat.Lossless);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,13 +112,18 @@ public sealed class TrackFormatResolver
|
||||
/// </summary>
|
||||
public async Task<bool> HasOpusAsync(string entryKey)
|
||||
{
|
||||
var audio = await _fileDatabase.LoadResourceAsync<AudioBinary>(
|
||||
VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey));
|
||||
if (audio is null)
|
||||
// Index-only existence — never read a file body. The opus-status admin endpoint calls this in a loop
|
||||
// over the entire catalogue, so a body load here would stream the whole library's audio sequentially.
|
||||
// HasIndexEntry is a pure in-memory index lookup (no disk read, no allocation per track).
|
||||
var opusVault = _fileDatabase.GetVault(VaultConstants.TrackOpus);
|
||||
if (opusVault is null)
|
||||
return false;
|
||||
|
||||
var sidecar = await _fileDatabase.LoadResourceAsync<MediaBinary>(
|
||||
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey));
|
||||
return sidecar is not null;
|
||||
if (!await opusVault.HasIndexEntry(OpusTranscodeService.OpusAudioKey(entryKey)))
|
||||
return false;
|
||||
|
||||
// Both halves required: audio without the seek/setup sidecar is unseekable, so a half-derived track
|
||||
// counts as not-having-Opus (the same completeness rule the Backfill-Opus pass enqueues against).
|
||||
return await opusVault.HasIndexEntry(OpusTranscodeService.OpusSidecarKey(entryKey));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user