fix Start stream 3 seconds behind edge
🏗️ Build Plugin / build (push) Successful in 33s
Nightly Build / nightly-build (push) Successful in 44s
🧪 Test Plugin / test (push) Successful in 29s

This commit is contained in:
2026-06-27 15:44:56 +02:00
parent f715130302
commit 7c81ef94ca
5 changed files with 105 additions and 3 deletions
@@ -71,8 +71,9 @@ public interface IStreamProxyService
/// <param name="manifestContent">The variant manifest content.</param>
/// <param name="baseProxyUrl">The base proxy URL (without query params).</param>
/// <param name="queryParams">Query parameters to append to rewritten URLs (e.g., "?token=abc").</param>
/// <param name="isLiveStream">Whether this is a livestream; if so an EXT-X-START offset is injected.</param>
/// <returns>The rewritten manifest content.</returns>
string RewriteVariantManifestUrls(string manifestContent, string baseProxyUrl, string queryParams);
string RewriteVariantManifestUrls(string manifestContent, string baseProxyUrl, string queryParams, bool isLiveStream = false);
/// <summary>
/// Cleans up old and expired stream mappings.
@@ -900,9 +900,15 @@ public class StreamProxyService : IStreamProxyService
/// <param name="manifestContent">The variant manifest content.</param>
/// <param name="baseProxyUrl">The base proxy URL (without query params).</param>
/// <param name="queryParams">Query parameters to append to rewritten URLs (e.g., "?token=abc").</param>
/// <param name="isLiveStream">Whether this is a livestream; if so an EXT-X-START offset is injected.</param>
/// <returns>The rewritten manifest content.</returns>
public string RewriteVariantManifestUrls(string manifestContent, string baseProxyUrl, string queryParams)
public string RewriteVariantManifestUrls(string manifestContent, string baseProxyUrl, string queryParams, bool isLiveStream = false)
{
if (isLiveStream)
{
manifestContent = InjectLiveStartOffset(manifestContent);
}
string RewriteUrl(string url)
{
if (url.Contains("://", StringComparison.Ordinal))
@@ -953,6 +959,82 @@ public class StreamProxyService : IStreamProxyService
return result.ToString();
}
/// <summary>
/// Injects an <c>#EXT-X-START:TIME-OFFSET</c> tag into a live media playlist so that players
/// (notably ExoPlayer on Android TV) begin a few segments back from the volatile live edge
/// instead of at the edge itself, which otherwise causes stalling/jumping until a manual skip.
/// The offset is sized relative to the playlist's own <c>EXT-X-TARGETDURATION</c> to stay
/// RFC 8216 compliant (≥ 3 target durations from the edge for live playlists).
/// </summary>
/// <param name="manifestContent">The media playlist content.</param>
/// <returns>The playlist with the tag injected, or unchanged if it is not applicable.</returns>
private string InjectLiveStartOffset(string manifestContent)
{
var segmentsBack = Plugin.Instance?.Configuration?.LiveStartSegmentsBack ?? 3;
if (segmentsBack <= 0)
{
return manifestContent; // Disabled by config.
}
// Only a media playlist (has segments, no variant list) should carry EXT-X-START.
// A master/multivariant playlist or a VOD playlist (#EXT-X-ENDLIST) is left untouched.
if (!manifestContent.Contains("#EXTINF", StringComparison.Ordinal)
|| manifestContent.Contains("#EXT-X-STREAM-INF", StringComparison.Ordinal)
|| manifestContent.Contains("#EXT-X-ENDLIST", StringComparison.Ordinal))
{
return manifestContent;
}
// Don't add a second tag if the source already specified a start point.
if (manifestContent.Contains("#EXT-X-START", StringComparison.Ordinal))
{
_logger.LogDebug("Live playlist already contains #EXT-X-START; leaving as-is");
return manifestContent;
}
var targetDurationMatch = Regex.Match(manifestContent, @"#EXT-X-TARGETDURATION:(\d+(?:\.\d+)?)");
if (!targetDurationMatch.Success
|| !double.TryParse(
targetDurationMatch.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var targetDuration)
|| targetDuration <= 0)
{
_logger.LogDebug("Could not read EXT-X-TARGETDURATION; skipping EXT-X-START injection");
return manifestContent;
}
// RFC 8216: TIME-OFFSET SHOULD NOT be within 3 target durations of the live edge.
var clampedSegments = Math.Max(3, segmentsBack);
var offsetSeconds = clampedSegments * targetDuration;
var startTag = string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"#EXT-X-START:TIME-OFFSET=-{0:0.###},PRECISE=YES",
offsetSeconds);
_logger.LogInformation(
"Injecting {StartTag} into live playlist (targetDuration={TargetDuration}s, segmentsBack={SegmentsBack})",
startTag,
targetDuration,
clampedSegments);
// Insert after the #EXTM3U header so it sits with the other top-level tags.
var headerIndex = manifestContent.IndexOf("#EXTM3U", StringComparison.Ordinal);
if (headerIndex < 0)
{
return startTag + "\n" + manifestContent;
}
var lineEnd = manifestContent.IndexOf('\n', headerIndex);
if (lineEnd < 0)
{
return manifestContent + "\n" + startTag;
}
return manifestContent[..(lineEnd + 1)] + startTag + "\n" + manifestContent[(lineEnd + 1)..];
}
/// <summary>
/// Cleans up old and expired stream mappings.
/// </summary>