Skip to main content

## Module stream_end

Source
Expand description

Telling a finished stream apart from a truncated one.

TRACES: UR-040 | DR-129 | UT-117

Background audio-only playback of a video item streams a progressive mp3 transcode over plain HTTP (see `OnlineRepository::build_audio_only_stream_url_for_video`). That response has no reliable length — a live transcode is chunked — so when the connection drops mid-episode the data source simply sees end-of-input. ExoPlayer cannot distinguish that from the real end of the media and reports `Player.STATE_ENDED`, which the app then treats as “the episode finished”.

The user-visible damage is not the missed advance itself. Playback parks in ExoPlayer’s `STATE_ENDED`, and the next play intent from the lockscreen, notification or a Bluetooth reconnect goes through media3’s `Util.handlePlayButtonAction`, which seeks an ENDED player to its default position before playing — so the episode starts over from 0:00. On a flaky connection that reads as “it randomly restarts the episode”.

The player itself has no way to know; the duration does. Jellyfin gives us the item’s real runtime, so an end reported well short of it is a truncation, not a finish — and the right response is to re-open the stream where it died, which is the “buffer and resume” the user expects.

## Structs§

ObservedTime
  The last playback time actually observed while media was loaded.
ResumeTracker
  Budget for consecutive resume attempts that make no progress.

## Constants§

MAX_STALLED_RESUME_ATTEMPTS
  Consecutive resume attempts allowed at the same position before giving up.
RESUME_PROGRESS_EPSILON_SECS 🔒
  Position change that counts as “this is a different playback context” — either the resume made progress, or a different item is loaded.
TRUNCATED_STREAM_TOLERANCE_SECS
  How far short of the item’s runtime a stream may end and still count as a natural finish.

## Functions§

is_audio_only_video
  A video item played through the native audio path — i.e. the background audio-only handoff, the only place a length-less progressive transcode is used. Jellyfin’s item-type taxonomy stays in Rust (CLAUDE.md).
is_truncated_end
  Did this end-of-stream happen far enough short of the item’s runtime to be a truncation rather than a finish?
player_retry_restarts_stream
  Would the player’s own load-error retry restart this stream from its beginning? If so the retry must be switched off and recovery left to `crate::player::PlayerController::recoverable_error_resume`.
with_start_time
  Rewrite an audio-only stream URL to start at `position_seconds`.