feature: Phase 18.3 — Opus delivery transport (?format= stream + seek sidecar endpoint)
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftModels.Enums;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NetBlocks.Models;
|
||||
|
||||
@@ -45,23 +46,37 @@ public class TrackMediaClient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the WAV stream for a track via an HTTP Range request starting at a
|
||||
/// Fetches the audio stream for a track via an HTTP Range request starting at a
|
||||
/// file-absolute byte offset. <paramref name="byteOffset"/> is the position from
|
||||
/// the start of the file on disk (including the WAV header) — callers seeking into
|
||||
/// audio data must add the header size themselves. The cancellation token aborts
|
||||
/// the in-flight server connection rather than leaving the server draining bytes
|
||||
/// into a dead socket.
|
||||
/// the start of the file on disk (including any container/header bytes) — callers
|
||||
/// seeking into audio data must add the header size themselves. The cancellation
|
||||
/// token aborts the in-flight server connection rather than leaving the server
|
||||
/// draining bytes into a dead socket.
|
||||
/// <para>
|
||||
/// <paramref name="format"/> selects the delivery rendering (Phase 18): the default
|
||||
/// <see cref="AudioFormat.Lossless"/> sends no <c>format</c> query param, so existing
|
||||
/// callers hit the byte-identical pre-Phase-18 endpoint; <see cref="AudioFormat.Opus"/>
|
||||
/// requests the low-data Ogg Opus artifact, which the server resolves and falls back to
|
||||
/// lossless when absent (C2). The response <see cref="TrackMediaResponse.ContentType"/>
|
||||
/// reports the format actually served, so the JS decoder dispatches on the real bytes.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public async Task<ApiResult<TrackMediaResponse>> GetTrackMedia(
|
||||
string trackId,
|
||||
long byteOffset = 0,
|
||||
AudioFormat format = AudioFormat.Lossless,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Same URL for every seek — only the Range header differs. byteOffset 0 is
|
||||
// not special-cased: "bytes=0-" requests the whole file from the start.
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/track/{trackId}");
|
||||
// Lossless omits the format param entirely so the request is byte-identical to
|
||||
// the pre-Phase-18 endpoint; only Opus appends ?format=opus.
|
||||
var uri = format == AudioFormat.Lossless
|
||||
? $"api/track/{trackId}"
|
||||
: $"api/track/{trackId}?format={format.ToString().ToLowerInvariant()}";
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
request.Headers.Range = new RangeHeaderValue(byteOffset, null);
|
||||
|
||||
// Use HttpCompletionOption.ResponseHeadersRead to get stream immediately
|
||||
@@ -115,4 +130,33 @@ public class TrackMediaClient
|
||||
return ApiResult<WaveformProfileDto>.CreateFailResult(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a track's Opus seek/setup sidecar — the combined OpusHead/OpusTags setup header plus the
|
||||
/// granule→byte seek index (Phase 18). The caller (18.5 player wiring) fetches this once on track load
|
||||
/// and parses it into the JS-side OpusSeekData before issuing any Opus seek. A 404 means no Opus
|
||||
/// artifact / sidecar exists for the track (legacy row, not backfilled, or transcode failed); callers
|
||||
/// treat that as "this track has no Opus seek data — stay on lossless" rather than an error, so it
|
||||
/// surfaces as a fail result with a stable message rather than throwing (mirrors GetWaveformProfileAsync).
|
||||
/// </summary>
|
||||
public async Task<ApiResult<byte[]>> GetOpusSidecarAsync(string trackId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"api/track/{trackId}/opus/seekdata", cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return ApiResult<byte[]>.CreateFailResult("No Opus sidecar available");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
return ApiResult<byte[]>.CreatePassResult(bytes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ApiResult<byte[]>.CreateFailResult(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user