Skip to content

fix: ask clients to retry when live playlist is unavailable - #2963

Open
Ministorm3 wants to merge 2 commits into
ErsatzTV:mainfrom
Ministorm3:fix/hls-playlist-retry
Open

fix: ask clients to retry when live playlist is unavailable#2963
Ministorm3 wants to merge 2 commits into
ErsatzTV:mainfrom
Ministorm3:fix/hls-playlist-retry

Conversation

@Ministorm3

@Ministorm3 Ministorm3 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Two related failure paths around the live HLS playlist mishandle errors in ways that break players:

  1. GET /iptv/session/{channelNumber}/hls.m3u8 returns 404 whenever TrimPlaylist yields None. That happens when live.m3u8 does not exist yet (ffmpeg has not written the first playlist), when the client disconnected, or when reading the playlist failed. This branch is only reachable when TryGetWorker succeeded, so the session demonstrably exists. 404 tells the client the resource is gone and carries no implication that it is temporary, so a client is behaving reasonably if it tears down a stream that was about to work.

  2. A playlist that fails to parse never even reached that branch: HlsPlaylistFilter.TrimPlaylist caught the exception and fabricated a "successful" result — an empty playlist with media sequence 0 and no segments. The session endpoint served that as a 200, resetting the media sequence out from under the player. Worse, the segmenter's TrimAndDelete wrote that empty playlist over live.m3u8 on disk, turning a transient parse error into persistent corruption.

Change

Controller — Return 503 Service Unavailable with Retry-After: 1, which is what RFC 9110 defines for a resource that is temporarily unavailable. One second is well under the 4s segment duration (OutputFormatHls.SegmentSeconds), so a retrying client picks the stream up on its next poll.

Filter / session workerIHlsPlaylistFilter.TrimPlaylist and TrimPlaylistWithDiscontinuity now return Option<TrimPlaylistResult> and yield None on failure, instead of fabricating an empty result (the bad playlist is still saved to the temp pool for diagnosis). Every failure now flows through one path: the session endpoint answers None with the 503 retry response above, and TrimAndDelete leaves the on-disk playlist and PlaylistStart untouched so the session can recover on the next cycle. This also removes an inconsistency where the filter rethrew only when saving the bad playlist also failed — a malformed playlist can no longer take down the session worker.

Cancellation now logs at debug rather than warning — the client has already disconnected, and this endpoint is polled continuously by every viewer, so it was steady warning-level noise for a non-failure.

This only affects the Legacy streaming engine; the next engine's multi-variant playlist points at live.m3u8 and does not reach this action.

Tests

  • Adds ErsatzTV.Tests/Controllers/IptvControllerTests.cs covering all three branches of GetLivePlaylist. Verified the retry test fails against the previous NotFound() and passes with the change.
  • Adds HlsPlaylistFilter_ShouldReturnNone_WhenPlaylistIsMalformed, a regression test for the fabricated-empty-result path; existing filter tests updated for the Option return type. Full ErsatzTV.Core.Tests suite passes (509).

A collaboration between @Ministorm3 and Claude Code

This branch is only reached when a session worker exists, so a failed
trim is transient: either ffmpeg has not written the first playlist yet,
or the read failed. Returning 404 told clients the stream was gone, and
many abandon it rather than polling again.

Return 503 with a Retry-After hint instead. Cancellation now logs at
debug rather than warning, since the client has already disconnected and
this endpoint is polled continuously by every viewer.

Co-Authored-By: Claude Opus 5 <[email protected]>
@Ministorm3
Ministorm3 marked this pull request as draft July 29, 2026 16:57
HlsPlaylistFilter.TrimPlaylist caught parse failures and returned an
"empty" result: no segments, media sequence 0, empty playlist text.
Callers treated that as success: the session endpoint served the empty
playlist as 200, and TrimAndDelete wrote it over live.m3u8 on disk,
turning a transient parse error into persistent corruption.

Return Option<TrimPlaylistResult> with None on failure instead. The
session endpoint now answers with the 503 retry response, and
TrimAndDelete leaves the existing playlist untouched so the session
can recover on the next cycle.

Co-Authored-By: Claude Fable 5 <[email protected]>
@Ministorm3
Ministorm3 marked this pull request as ready for review July 29, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant