Wire Opus end-to-end playback + Backfill-Opus action (Phase 18.5)

Player picks Opus when the browser can decode it and a sidecar exists (else lossless), injecting the sidecar before stream init; seek reuses the same format. Adds the Backfill-Opus bulk API endpoint + CMS action.
This commit is contained in:
daniel-c-harvey
2026-06-23 12:39:13 -04:00
parent dce5530890
commit 2bde4908d7
12 changed files with 961 additions and 1 deletions
@@ -70,6 +70,36 @@ public class AudioInteropService : IAsyncDisposable
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.initializeStreaming", playerId, totalStreamLength, contentType);
}
/// <summary>
/// Probes whether this browser can decode Ogg Opus via <c>decodeAudioData</c> (Safari &lt; 18.4 cannot).
/// Phase 18 capability gate (OQ2): the player only requests Opus when this returns true, otherwise it
/// stays on the universal lossless path (AC7 — no listener ever gets silence over a codec gap). Probe
/// failures degrade to <c>false</c> (assume incapable) so an interop error can never silence playback.
/// </summary>
public async Task<bool> CanDecodeOggOpus(string playerId)
{
try
{
return await _jsRuntime.InvokeAsync<bool>("DeepDrftAudio.canDecodeOggOpus");
}
catch
{
return false;
}
}
/// <summary>
/// Hands the raw Opus seek/setup sidecar bytes (setup header + granule→byte seek index) to the JS player
/// so the next Opus stream's decoder has them BEFORE init (the 18.4 set-before-init contract). The player
/// parses and stashes them; <see cref="InitializeStreaming"/> applies them when it builds the Opus decoder.
/// Must be called before <see cref="InitializeStreaming"/> on an Opus stream. Returns the parse result —
/// a failure means the bytes were not a valid sidecar, and the caller falls back to lossless.
/// </summary>
public async Task<AudioOperationResult> SetOpusSidecar(string playerId, byte[] sidecarBytes)
{
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.setOpusSidecar", playerId, sidecarBytes);
}
public async Task<StreamingResult> ProcessStreamingChunk(string playerId, byte[] audioChunk)
{
return await InvokeJsAsync<StreamingResult>("DeepDrftAudio.processStreamingChunk", playerId, audioChunk);