using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; namespace DeepDrftContent.Processors.Opus; /// /// Derives and persists a track's low-data Ogg Opus artifacts (Phase 18.1). Mirrors /// 's derived-artifact lifecycle: compute from the stored source, /// store in a dedicated vault keyed by EntryKey, regenerable, failure-tolerant. For one track it /// produces two entries in the vault โ€” the Opus audio bytes and a /// combined setup-header + seek-index sidecar (ยง3.4a). Strictly additive: the source tracks vault /// is never touched, and a failure here leaves the track lossless-only and eligible for backfill (C2/C6). /// public sealed class OpusTranscodeService { private readonly FileDb _fileDatabase; private readonly FfmpegOpusEncoder _encoder; private readonly OpusTranscodeOptions _options; private readonly ILogger _logger; public OpusTranscodeService( FileDb fileDatabase, FfmpegOpusEncoder encoder, IOptions options, ILogger logger) { _fileDatabase = fileDatabase; _encoder = encoder; _options = options.Value; _logger = logger; } /// /// Reads the source audio for from the tracks vault, transcodes it /// to Ogg Opus 320, walks the encoded stream to build the seek index + capture the setup header, and /// stores the Opus bytes and the sidecar in the vault under the /// same key. Re-runnable โ€” a second call overwrites the prior artifacts (backfill / replace-audio). /// Returns false (logged) on any failure; never throws for expected failure modes (C6). The only /// propagated exception is on genuine shutdown. /// public async Task TranscodeAndStoreAsync(string entryKey, CancellationToken ct) { var source = await _fileDatabase.LoadResourceAsync(VaultConstants.Tracks, entryKey); if (source is null) { _logger.LogWarning("Opus transcode: no source audio in vault for {EntryKey}; skipping.", entryKey); return false; } Directory.CreateDirectory(_options.StagingPath); var sourcePath = Path.Combine(_options.StagingPath, $"opus-src-{Guid.NewGuid():N}{source.Extension}"); var opusPath = Path.Combine(_options.StagingPath, $"opus-out-{Guid.NewGuid():N}{OggOpusConstants.OpusExtension}"); try { await File.WriteAllBytesAsync(sourcePath, source.Buffer, ct); if (!await _encoder.EncodeAsync(sourcePath, opusPath, ct)) return false; // encoder already logged the cause var opusBytes = await File.ReadAllBytesAsync(opusPath, ct); var walk = OggOpusParser.Walk(opusBytes); if (walk is null) { _logger.LogError( "Opus transcode: ffmpeg produced output but the Ogg stream could not be walked for {EntryKey}; " + "no artifacts stored.", entryKey); return false; } await EnsureVaultAsync(); var opusBitrate = source.Duration > 0 ? (int)(opusBytes.Length * 8 / source.Duration / 1000) : _options.BitrateKbps; var audioBinary = new AudioBinary(new AudioBinaryParams( opusBytes, opusBytes.Length, OggOpusConstants.OpusExtension, source.Duration, opusBitrate)); var sidecar = new OpusSidecar(walk.SetupHeaderBytes, walk.SeekIndex).ToBytes(); var sidecarBinary = new MediaBinary(new MediaBinaryParams( sidecar, sidecar.Length, OggOpusConstants.SidecarExtension)); // Store the audio first, then the sidecar. If the sidecar write fails the Opus bytes are // present but unseekable โ€” treat that as a failed derive (return false) so a backfill re-runs // it; do not leave a half-derived track that the delivery layer would treat as complete. var audioStored = await _fileDatabase.RegisterResourceAsync( VaultConstants.TrackOpus, OpusAudioKey(entryKey), audioBinary); if (!audioStored) { _logger.LogError("Opus transcode: vault write of Opus audio failed for {EntryKey}.", entryKey); return false; } var sidecarStored = await _fileDatabase.RegisterResourceAsync( VaultConstants.TrackOpus, OpusSidecarKey(entryKey), sidecarBinary); if (!sidecarStored) { _logger.LogError("Opus transcode: vault write of sidecar failed for {EntryKey}.", entryKey); return false; } _logger.LogInformation( "Opus transcode complete for {EntryKey}: {OpusBytes} bytes, {Points} seek points, {Duration:F1}s.", entryKey, opusBytes.Length, walk.SeekIndex.Points.Count, walk.SeekIndex.TotalDurationSeconds); return true; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) { _logger.LogError(ex, "Opus transcode failed for {EntryKey}; track stays lossless-only.", entryKey); return false; } finally { TryDelete(sourcePath); TryDelete(opusPath); } } /// The vault entry key under which a track's Opus audio bytes are stored. public static string OpusAudioKey(string entryKey) => entryKey; /// The vault entry key under which a track's setup-header + seek-index sidecar is stored. public static string OpusSidecarKey(string entryKey) => $"{entryKey}-sidecar"; private async Task EnsureVaultAsync() { if (!_fileDatabase.HasVault(VaultConstants.TrackOpus)) await _fileDatabase.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); } private void TryDelete(string path) { try { if (File.Exists(path)) File.Delete(path); } catch (Exception ex) { _logger.LogWarning(ex, "Opus transcode: failed to delete staging file {Path}.", path); } } }