From 92dc6d8203d02e96401b471dc6dc9f6e7b1fb18f Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 17 Jan 2026 10:46:11 +0100 Subject: [PATCH] fix: change in server side API for live stream --- .../Controllers/StreamProxyController.cs | 7 +++++-- .../Interfaces/IStreamProxyService.cs | 3 ++- .../Services/StreamProxyService.cs | 20 ++++++++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs b/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs index 03e1aea..d4b7df4 100644 --- a/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs +++ b/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs @@ -183,7 +183,8 @@ public class StreamProxyController : ControllerBase try { // Fetch the variant manifest as a segment - var manifestData = await _proxyService.GetSegmentAsync(actualItemId, fullPath, cancellationToken).ConfigureAwait(false); + var queryString = Request.QueryString.HasValue ? Request.QueryString.Value : null; + var manifestData = await _proxyService.GetSegmentAsync(actualItemId, fullPath, queryString, cancellationToken).ConfigureAwait(false); if (manifestData == null) { @@ -234,7 +235,9 @@ public class StreamProxyController : ControllerBase try { - var segmentData = await _proxyService.GetSegmentAsync(actualItemId, segmentPath, cancellationToken).ConfigureAwait(false); + // Pass the original query string to preserve segment-specific parameters (e.g., ?m=timestamp) + var queryString = Request.QueryString.HasValue ? Request.QueryString.Value : null; + var segmentData = await _proxyService.GetSegmentAsync(actualItemId, segmentPath, queryString, cancellationToken).ConfigureAwait(false); if (segmentData == null) { diff --git a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IStreamProxyService.cs b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IStreamProxyService.cs index b3e7346..9ad0702 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IStreamProxyService.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IStreamProxyService.cs @@ -56,9 +56,10 @@ public interface IStreamProxyService /// /// The item ID. /// The segment path. + /// The original query string from the request (preserves segment-specific parameters like timestamps). /// Cancellation token. /// The segment content as bytes. - Task GetSegmentAsync(string itemId, string segmentPath, CancellationToken cancellationToken = default); + Task GetSegmentAsync(string itemId, string segmentPath, string? queryString = null, CancellationToken cancellationToken = default); /// /// Cleans up old and expired stream mappings. diff --git a/Jellyfin.Plugin.SRFPlay/Services/StreamProxyService.cs b/Jellyfin.Plugin.SRFPlay/Services/StreamProxyService.cs index 1f3e2ce..1204a11 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/StreamProxyService.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/StreamProxyService.cs @@ -701,11 +701,13 @@ public class StreamProxyService : IStreamProxyService /// /// The item ID. /// The segment path. + /// The original query string from the request (preserves segment-specific parameters like timestamps). /// Cancellation token. /// The segment content as bytes. public async Task GetSegmentAsync( string itemId, string segmentPath, + string? queryString = null, CancellationToken cancellationToken = default) { var authenticatedUrl = await GetAuthenticatedUrlAsync(itemId, cancellationToken).ConfigureAwait(false); @@ -720,17 +722,29 @@ public class StreamProxyService : IStreamProxyService var baseUri = new Uri(authenticatedUrl); var baseUrl = $"{baseUri.Scheme}://{baseUri.Host}{string.Join('/', baseUri.AbsolutePath.Split('/')[..^1])}"; - // Extract query parameters (auth tokens) from authenticated URL - var queryParams = baseUri.Query; + // Use the original query string from the request (preserves segment-specific params like ?m=timestamp) + // If no query string is provided, check if we need to add auth params from the master manifest + var queryParams = string.Empty; + if (!string.IsNullOrEmpty(queryString)) + { + // Use the original query string from the segment request + queryParams = queryString.StartsWith('?') ? queryString : $"?{queryString}"; + } + else if (!segmentPath.Contains("hdntl=", StringComparison.OrdinalIgnoreCase)) + { + // Only append master manifest query params if segment doesn't have path-based auth + queryParams = baseUri.Query; + } // Build full segment URL var segmentUrl = $"{baseUrl}/{segmentPath}{queryParams}"; _logger.LogDebug( - "Fetching segment - BaseUri: {BaseUri}, BaseUrl: {BaseUrl}, SegmentPath: {SegmentPath}, FullUrl: {FullUrl}", + "Fetching segment - BaseUri: {BaseUri}, BaseUrl: {BaseUrl}, SegmentPath: {SegmentPath}, QueryString: {QueryString}, FullUrl: {FullUrl}", authenticatedUrl, baseUrl, segmentPath, + queryString ?? "(none)", segmentUrl); using var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); var segmentData = await httpClient.GetByteArrayAsync(segmentUrl, cancellationToken).ConfigureAwait(false);