fix(api): stream audio store path to eliminate whole-file buffering (OOM)
Processors now emit a ProcessedAudio plan with a streamed writer instead of a whole-file AudioBinary; vault writes stream via RegisterResourceStreamingAsync. Header parsing is bounded. Wave 2 (waveform/Opus) still re-reads the full file by design.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
namespace DeepDrftContent.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// The product of processing an uploaded audio file on the store path: the metadata SQL and the
|
||||
/// vault index need, plus a streamed writer that emits the canonical vault bytes to a destination
|
||||
/// stream without ever materializing the whole file in a managed <c>byte[]</c>.
|
||||
///
|
||||
/// This replaces the former whole-file <c>AudioBinary</c> as the processor output for upload /
|
||||
/// replace-audio (Wave 1 OOM fix): passthrough formats (standard-PCM WAV, MP3, FLAC) stream the
|
||||
/// source file straight to the destination, and EXTENSIBLE WAVs stream their normalization to
|
||||
/// standard PCM. The vault <em>load</em> path still uses <c>AudioBinary</c> (a full buffer) — that
|
||||
/// is the Wave 2 read path and is out of scope here.
|
||||
///
|
||||
/// <see cref="WriteToAsync"/> is invoked exactly once by the streaming vault register, against the
|
||||
/// freshly opened backing <see cref="System.IO.FileStream"/>. The writer re-opens the source file
|
||||
/// itself, so the source (a staging file) must still exist when the register runs — it does, because
|
||||
/// processing and registration are sequential within the store call, before the staging-file
|
||||
/// <c>finally</c> cleanup.
|
||||
/// </summary>
|
||||
public sealed class ProcessedAudio
|
||||
{
|
||||
/// <summary>The stored file extension (e.g. <c>.wav</c>, <c>.mp3</c>, <c>.flac</c>).</summary>
|
||||
public string Extension { get; }
|
||||
|
||||
/// <summary>Audio duration in seconds, extracted from the header.</summary>
|
||||
public double Duration { get; }
|
||||
|
||||
/// <summary>Audio bitrate in kbps, extracted from (or estimated for) the header.</summary>
|
||||
public int Bitrate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The canonical stored byte count — computed from the header and file length, never by
|
||||
/// buffering the body. Used only for diagnostics (confirming the streamed path was taken).
|
||||
/// </summary>
|
||||
public long Size { get; }
|
||||
|
||||
private readonly Func<Stream, CancellationToken, Task> _writeTo;
|
||||
|
||||
public ProcessedAudio(
|
||||
string extension,
|
||||
double duration,
|
||||
int bitrate,
|
||||
long size,
|
||||
Func<Stream, CancellationToken, Task> writeTo)
|
||||
{
|
||||
Extension = extension;
|
||||
Duration = duration;
|
||||
Bitrate = bitrate;
|
||||
Size = size;
|
||||
_writeTo = writeTo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams the canonical vault bytes to <paramref name="destination"/>. Bounded-buffer — peak
|
||||
/// managed memory is O(buffer), not O(filesize).
|
||||
/// </summary>
|
||||
public Task WriteToAsync(Stream destination, CancellationToken cancellationToken = default)
|
||||
=> _writeTo(destination, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a passthrough plan: the stored bytes are byte-identical to the source file (standard
|
||||
/// PCM WAV, MP3, FLAC — no transcoding). The writer is a bounded disk-to-disk copy.
|
||||
/// </summary>
|
||||
public static ProcessedAudio Passthrough(
|
||||
string sourcePath, string extension, double duration, int bitrate, long sourceLength)
|
||||
=> new(extension, duration, bitrate, sourceLength,
|
||||
(destination, ct) => AudioStoreStream.CopyFileAsync(sourcePath, destination, ct));
|
||||
}
|
||||
Reference in New Issue
Block a user