Merge streaming-overhaul into dev (Opus low-data streaming, windowed streaming, HW-accel-off stabilization)
This commit is contained in:
@@ -518,7 +518,10 @@
|
||||
{
|
||||
var row = _tracks[i];
|
||||
|
||||
if (row.Status == BatchRowStatus.Done)
|
||||
// Skip rows already processed in a prior submit attempt. PostProcessing counts as processed:
|
||||
// the track persisted successfully (only its background Opus derive is still settling), so a
|
||||
// re-submit after a partial failure must NOT re-upload it and mint a duplicate.
|
||||
if (row.Status is BatchRowStatus.Done or BatchRowStatus.PostProcessing)
|
||||
{
|
||||
_processedCount++;
|
||||
continue;
|
||||
@@ -638,7 +641,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
row.Status = BatchRowStatus.Done;
|
||||
// §3.1a: a new-track upload persists the track (live + lossless) and the server
|
||||
// derives Opus in the background, so the row enters the visible Post-Processing
|
||||
// phase — same as BatchUpload. (The metadata-only update path above stays Done: it
|
||||
// changes no audio, so it triggers no transcode.) Non-blocking; the Releases view
|
||||
// polls the durable Opus status to settle it.
|
||||
row.Status = BatchRowStatus.PostProcessing;
|
||||
succeeded++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,9 @@ public class BatchRowModel
|
||||
: 0;
|
||||
}
|
||||
|
||||
public enum BatchRowStatus { Queued, Uploading, Done, Failed }
|
||||
// Done is the terminal success state (track persisted + playable losslessly). PostProcessing is the
|
||||
// visible third upload phase (§3.1a): the byte transfer and server persist are finished and the track is
|
||||
// live, but the server-side background Opus transcode is still running. It is NOT a failure and never
|
||||
// blocks completion — the form may navigate away while a row sits in PostProcessing; the Releases browse
|
||||
// view polls the durable Opus status from there.
|
||||
public enum BatchRowStatus { Queued, Uploading, PostProcessing, Done, Failed }
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
{
|
||||
BatchRowStatus.Uploading => @<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-1" />Uploading</MudChip>,
|
||||
BatchRowStatus.PostProcessing => @<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-1" />Post-Processing</MudChip>,
|
||||
BatchRowStatus.Done => @<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Text" Icon="@Icons.Material.Filled.CheckCircle">Done</MudChip>,
|
||||
BatchRowStatus.Failed => @<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Text" Icon="@Icons.Material.Filled.Error">Failed</MudChip>,
|
||||
_ => @<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Text">Queued</MudChip>
|
||||
|
||||
@@ -69,6 +69,15 @@
|
||||
Value="@_tracks[0].UploadPercent"
|
||||
aria-label="Uploading track" />
|
||||
}
|
||||
else if (_tracks[0].Status == BatchRowStatus.PostProcessing)
|
||||
{
|
||||
@* §3.1a: track is live + plays lossless; the Opus transcode runs in the background.
|
||||
Indeterminate (no client-side progress for a server-side job) and non-blocking. *@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption">Post-Processing (deriving Opus)…</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
@@ -519,7 +528,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
row.Status = BatchRowStatus.Done;
|
||||
// §3.1a: the byte transfer + server persist are done and the track is live and plays
|
||||
// losslessly — the upload is successful HERE. The server then derives Opus on a
|
||||
// background worker, so the row enters the visible Post-Processing phase rather than
|
||||
// jumping straight to Done. This never blocks: the loop continues and the form may
|
||||
// navigate away while rows sit in Post-Processing; the Releases view polls the durable
|
||||
// Opus status to settle each one.
|
||||
row.Status = BatchRowStatus.PostProcessing;
|
||||
succeeded++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@inject ISnackbar Snackbar
|
||||
@inject ILogger<Releases> Logger
|
||||
@inject NavigationManager NavigationManager
|
||||
@implements IDisposable
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>Releases — Deep DRFT Management</PageTitle>
|
||||
@@ -51,6 +52,28 @@
|
||||
<span>Backfill High-res (@MissingHighResCount)</span>
|
||||
}
|
||||
</MudButton>
|
||||
@* Backfill-Opus (Phase 18.5 + 18.6 badge). The Opus derive runs on a server-side background
|
||||
worker: pressing the button enqueues every track lacking a complete Opus artifact and reports
|
||||
the (enqueued / skipped) outcome. The "missing N" badge (18.6) reads the same opus-status map
|
||||
the page polls, giving visual parity with the two waveform backfill buttons — but unlike them,
|
||||
the count is informational, not a per-track client loop (the work is scheduled, not driven from
|
||||
here). The button stays pressable at N=0 (a no-op re-run is harmless); it only disables while a
|
||||
press is in flight or another bulk run holds the page. *@
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
||||
Disabled="@(_bulkRunning || _highResBulkRunning || _opusBackfillRunning)"
|
||||
OnClick="BackfillOpusAsync">
|
||||
@if (_opusBackfillRunning)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<span>Scheduling…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Backfill Opus (@MissingOpusCount)</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
@@ -140,6 +163,22 @@
|
||||
private int MissingProfileCount => _waveformStatus.Count(s => !s.HasProfile);
|
||||
private int MissingHighResCount => _waveformStatus.Count(s => !s.HasHighRes);
|
||||
|
||||
// EntryKey → HasOpus (a complete audio+sidecar derive). Loaded alongside the waveform status on init and
|
||||
// re-read after a Backfill-Opus run so the "missing N" badge settles. Also the source the Post-Processing
|
||||
// poll watches: a freshly uploaded track lands here with HasOpus=false and flips to true once the
|
||||
// server-side background transcode finishes — the durable surface for the upload meter's Post-Processing
|
||||
// phase after the form returns the admin to this view (§3.1a).
|
||||
private IReadOnlyList<OpusStatusDto> _opusStatus = Array.Empty<OpusStatusDto>();
|
||||
|
||||
private int MissingOpusCount => _opusStatus.Count(s => !s.HasOpus);
|
||||
|
||||
// Post-Processing poll: while any track is still missing Opus (a transcode in flight or not yet
|
||||
// backfilled), re-read the opus-status map on an interval so the "missing N" badge and any per-track
|
||||
// Post-Processing indicator settle without a manual refresh. Stops itself once nothing is missing, and is
|
||||
// torn down on dispose. Non-blocking — it never gates an upload or a button; it only refreshes a count.
|
||||
private const int OpusPollIntervalMs = 4000;
|
||||
private CancellationTokenSource? _opusPollCts;
|
||||
|
||||
// Local state for the parent-owned "Generate All Profiles" bulk run.
|
||||
private bool _bulkRunning;
|
||||
private int _bulkTotal;
|
||||
@@ -150,6 +189,11 @@
|
||||
private int _highResBulkTotal;
|
||||
private int _highResBulkDone;
|
||||
|
||||
// Local state for the "Backfill Opus" action. The Opus derive is server-side and background-queued, so
|
||||
// there is no client-side per-track loop or progress total — this flag only guards the button while the
|
||||
// single scheduling call is in flight.
|
||||
private bool _opusBackfillRunning;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Seed the active tab from ?medium= so a catalogue card deep-links straight to its medium. Panel 0
|
||||
@@ -170,9 +214,81 @@
|
||||
_waveformStatus = result.Success && result.Value is not null
|
||||
? result.Value
|
||||
: Array.Empty<WaveformStatusDto>();
|
||||
|
||||
await RefreshOpusStatusAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-read the per-track Opus derive status and (re)arm the Post-Processing poll when work is still
|
||||
/// pending. Called on init, after a Backfill-Opus run, and from the poll itself. Best-effort: a failed
|
||||
/// fetch leaves the previous map in place rather than zeroing the badge on a transient API blip. Does not
|
||||
/// call StateHasChanged itself — callers batch it with their own render (the poll path renders explicitly).
|
||||
/// </summary>
|
||||
private async Task RefreshOpusStatusAsync()
|
||||
{
|
||||
var opusResult = await CmsTrackService.GetOpusStatusAsync();
|
||||
if (opusResult.Success && opusResult.Value is not null)
|
||||
{
|
||||
_opusStatus = opusResult.Value;
|
||||
}
|
||||
|
||||
if (MissingOpusCount > 0)
|
||||
{
|
||||
EnsureOpusPollRunning();
|
||||
}
|
||||
}
|
||||
|
||||
// Start the Post-Processing poll if it is not already running. The loop re-reads opus-status every
|
||||
// OpusPollIntervalMs and renders; it exits as soon as nothing is missing (or on cancel/dispose). Guarded
|
||||
// by a non-null CTS so overlapping callers (init + backfill) cannot start two loops.
|
||||
private void EnsureOpusPollRunning()
|
||||
{
|
||||
if (_opusPollCts is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_opusPollCts = new CancellationTokenSource();
|
||||
_ = PollOpusStatusAsync(_opusPollCts.Token);
|
||||
}
|
||||
|
||||
private async Task PollOpusStatusAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested && MissingOpusCount > 0)
|
||||
{
|
||||
await Task.Delay(OpusPollIntervalMs, ct);
|
||||
|
||||
var result = await CmsTrackService.GetOpusStatusAsync();
|
||||
if (result.Success && result.Value is not null)
|
||||
{
|
||||
_opusStatus = result.Value;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected on dispose / navigation away — nothing to do.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Opus Post-Processing poll stopped on an unexpected error.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_opusPollCts?.Dispose();
|
||||
_opusPollCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_opusPollCts?.Cancel();
|
||||
}
|
||||
|
||||
// Invalidates the cached per-track waveform status on all embedded grids so the next row expand
|
||||
// re-fetches fresh data. Called after each catalogue-wide bulk run so already-expanded rows
|
||||
// reflect the new waveform state on the next expand interaction.
|
||||
@@ -291,4 +407,54 @@
|
||||
Snackbar.Add($"Backfilled {succeeded} high-res datum(s); {failures} failed.", Severity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kick off the catalogue-wide Backfill-Opus pass. The API enumerates the tracks lacking a complete Opus
|
||||
/// artifact, enqueues a background derive for each, and returns the (enqueued, skipped) counts. This is a
|
||||
/// single scheduling call — the transcodes run server-side afterward — so there is no per-track progress
|
||||
/// to render here, just a busy flag and a result snackbar. Re-runnable: a second press only schedules
|
||||
/// tracks still missing Opus.
|
||||
/// </summary>
|
||||
private async Task BackfillOpusAsync()
|
||||
{
|
||||
_opusBackfillRunning = true;
|
||||
StateHasChanged();
|
||||
try
|
||||
{
|
||||
var result = await CmsTrackService.BackfillOpusAsync();
|
||||
if (!result.Success)
|
||||
{
|
||||
var error = result.Messages.FirstOrDefault()?.Message ?? "Failed to start the Opus backfill.";
|
||||
Snackbar.Add(error, Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-read the status map so the "missing N" badge reflects the just-enqueued work and the
|
||||
// Post-Processing poll arms to watch the transcodes settle from N→0 as each finishes.
|
||||
await RefreshOpusStatusAsync();
|
||||
|
||||
var (enqueued, skipped) = (result.Value.Enqueued, result.Value.Skipped);
|
||||
if (enqueued == 0)
|
||||
{
|
||||
Snackbar.Add($"All {skipped} track(s) already have Opus — nothing to backfill.", Severity.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(
|
||||
$"Scheduled {enqueued} Opus transcode(s) in the background ({skipped} already had Opus). " +
|
||||
"They will appear as each finishes.",
|
||||
Severity.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Opus backfill failed to start");
|
||||
Snackbar.Add("Failed to start the Opus backfill.", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_opusBackfillRunning = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user