Live-stream starts 6 seconds from edge not at oldest segment.
🧪 Test Plugin / test (push) Failing after 5s
🚀 Release Plugin / build-and-release (push) Failing after 4s
🏗️ Build Plugin / build (push) Successful in 2m55s

This commit is contained in:
2026-02-28 11:55:05 +01:00
parent 7c76402a4a
commit d75e69249b
3 changed files with 123 additions and 9 deletions
@@ -647,6 +647,21 @@ public class StreamProxyService : IStreamProxyService
// Rewrite the manifest to replace Akamai URLs with proxy URLs
var rewrittenContent = RewriteManifestUrls(manifestContent, authenticatedUrl, baseProxyUrl);
// For live streams, inject #EXT-X-START to tell the player to start near the live edge
// Without this, players may start at the beginning of the sliding window and stutter
// as old segments get rotated out by the CDN
if (_streamMappings.TryGetValue(itemId, out var streamInfoForManifest) && streamInfoForManifest.IsLiveStream)
{
if (!rewrittenContent.Contains("#EXT-X-START", StringComparison.Ordinal))
{
rewrittenContent = rewrittenContent.Replace(
"#EXTM3U",
"#EXTM3U\n#EXT-X-START:TIME-OFFSET=-6,PRECISE=NO",
StringComparison.Ordinal);
_logger.LogDebug("Injected #EXT-X-START tag for live stream {ItemId}", itemId);
}
}
_logger.LogDebug("Rewritten manifest for item {ItemId} ({Length} bytes):\n{Content}", itemId, rewrittenContent.Length, rewrittenContent);
return rewrittenContent;
}
@@ -720,6 +735,67 @@ public class StreamProxyService : IStreamProxyService
}
}
/// <summary>
/// Fetches a segment as a streaming response for direct forwarding to the client.
/// Uses HttpCompletionOption.ResponseHeadersRead to start streaming before the full
/// segment is downloaded, reducing time-to-first-byte for live streams.
/// </summary>
/// <param name="itemId">The item ID.</param>
/// <param name="segmentPath">The segment path.</param>
/// <param name="queryString">The original query string from the request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The HTTP response for streaming, or null if not found.</returns>
public async Task<HttpResponseMessage?> GetSegmentStreamAsync(
string itemId,
string segmentPath,
string? queryString = null,
CancellationToken cancellationToken = default)
{
var authenticatedUrl = await GetAuthenticatedUrlAsync(itemId, cancellationToken).ConfigureAwait(false);
if (authenticatedUrl == null)
{
return null;
}
try
{
var baseUri = new Uri(authenticatedUrl);
var baseUrl = $"{baseUri.Scheme}://{baseUri.Host}{string.Join('/', baseUri.AbsolutePath.Split('/')[..^1])}";
var queryParams = string.Empty;
if (!string.IsNullOrEmpty(queryString))
{
queryParams = queryString.StartsWith('?') ? queryString : $"?{queryString}";
}
else if (!segmentPath.Contains("hdntl=", StringComparison.OrdinalIgnoreCase))
{
queryParams = baseUri.Query;
}
var segmentUrl = $"{baseUrl}/{segmentPath}{queryParams}";
_logger.LogDebug("Streaming segment - SegmentPath: {SegmentPath}, FullUrl: {FullUrl}", segmentPath, segmentUrl);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
var request = new HttpRequestMessage(HttpMethod.Get, segmentUrl);
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Segment stream request failed with {StatusCode} for {SegmentPath}", response.StatusCode, segmentPath);
response.Dispose();
return null;
}
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stream segment {SegmentPath} for item {ItemId}", segmentPath, itemId);
return null;
}
}
/// <summary>
/// Rewrites URLs in HLS manifest to point to proxy.
/// </summary>