Swap shows only the new media, no guessed endings, quieter screen
- A swap stops the previous media at once; buffer counts only for the media the player holds (client) and the current mediaId (server), so the old video can no longer start the new one. - The automatic frozen-picture reload is gone (false alarms on still scenes); Reload video stays. - The "Next episode" pill shows only from a known ending. - A web link's thumbnail is its cover, under the video's own name. - While playing, the unread chip stays off the picture; chat pop-ups can be All / Only when paused / Off. - Server fetch failures say when the server has no cookies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ab133dd82f
commit
27b4ec2c96
25 files changed
+274
-485
No files matched your search
+26
-21
@@ -98,7 +98,11 @@ Loosely modeled on Syncplay, with a different readiness design.
|
||||
the media `duration` and its `mediaId` once the player knows it. The
|
||||
server stores `buffered` and liveness, and takes the first reported
|
||||
duration for the current media (swap resets it); a report with `eventId`
|
||||
older than current has its position ignored.
|
||||
older than current has its position ignored. `buffered` is the current
|
||||
media's only: the client sends 0 until the player holds it (a swap's
|
||||
media still resolving, being fetched by the server or opening), and the
|
||||
server counts a report naming an earlier `mediaId` as 0, so the old
|
||||
media's buffer never passes the gate for the new one.
|
||||
- **Ready gate** (HELD to PLAYING): the in-set is non-empty and every
|
||||
in-set member is ready, fresh and has `buffered >= need`. `need` is 4 s
|
||||
(`minBufferSecs`), or near the end what is left minus 2 s
|
||||
@@ -336,17 +340,10 @@ session: created on join, disposed on leave, replaced by `recreate()` for
|
||||
`StateDiff` actions, the play gate (`localPlayAllowed`: self in-set and
|
||||
ready) or the user's own seeks (see Sync model). A server seek within 0.5 s of the current position is skipped. `leave()`
|
||||
stops the player (otherwise audio keeps playing). On media change
|
||||
(`mediaId` bump) the coordinator opens the new URL and `StateDiff` seeks to 0,
|
||||
holds and resets rate.
|
||||
|
||||
Frozen picture: `FrameWatchdog` samples about once a second. It hashes a
|
||||
small capture of the compositor's centre (`ShownFrameProbe`) and suspects a
|
||||
freeze when position advances while that hash stays constant for 3 s. Two
|
||||
differing mpv `screenshot-raw` frames show the decoder is still producing
|
||||
pictures, which confirms the freeze (a still scene is dismissed). A confirmed
|
||||
freeze reloads the video with a toast. It fires once until the picture moves
|
||||
again for 10 s, so it cannot loop. Seeks, pause, buffering and a hidden window
|
||||
disarm it.
|
||||
(`mediaId` bump) the coordinator stops the previous media at once (no old
|
||||
picture or sound while the new one resolves, is fetched or opens; a
|
||||
reconnect to the media the player holds keeps it), then opens the new URL
|
||||
and `StateDiff` seeks to 0, holds and resets rate.
|
||||
|
||||
Chapters, segments, tracks (`PlaybackController` signals, values from
|
||||
`playback/media_marks.dart`, no widgets; any controls UI reads them):
|
||||
@@ -673,7 +670,7 @@ all three link modes. Nothing is bundled or installed by hand.
|
||||
(`AppYtDlp.probe`, remembered 10 minutes; media file names too, not a
|
||||
Jellyfin item link) for the
|
||||
Link preview and the swap: a page is named by its title, never its URL
|
||||
tail, and its thumbnail goes up as the banner (no poster). A link with a
|
||||
tail, and its thumbnail goes up as its art (see "Media art"). A link with a
|
||||
problem, or one the probe cannot reach ("Couldn't reach the site"), is
|
||||
not put up in any mode: the Link page shows the reason (preview and
|
||||
under the button) and offers "Put it up anyway" (`LinkMedia.anyway`),
|
||||
@@ -747,9 +744,9 @@ Client side (`lib/src/jellyfin/session_media.dart`, `progress.dart`,
|
||||
`/Shows/{id}/Episodes` call; `episodeBefore` / `episodeAfter`) gives the
|
||||
previous and next episode. The controls show previous / next buttons
|
||||
(also `<` `>`, Shift+P / Shift+N); from `nextPromptStart` (the ending's
|
||||
start when one is known, see "Skip rules", else the last 2 min of the
|
||||
runtime) a floating "Next episode" pill shows outside the auto-hiding
|
||||
controls. Auto-advance still waits for `finished`. Any
|
||||
start, see "Skip rules") a floating "Next episode" pill shows outside
|
||||
the auto-hiding controls. Without a known ending there is no pill (never
|
||||
a guessed time); the end card at `finished` is the only prompt. Auto-advance still waits for `finished`. Any
|
||||
of them swaps to that episode's ref with the clicker's account. Guests
|
||||
(relay, blocked) see none of it.
|
||||
- **Auto-advance** (Settings, on by default): at `finished` with a known
|
||||
@@ -978,12 +975,16 @@ always scrolls the page). Space never presses a focused button
|
||||
feed lines between them. Opening focuses the field; Tab or Esc in it
|
||||
close the panel and focus goes back where it was. Unread chat badges
|
||||
the strip's and both player designs' chat buttons; in fullscreen with
|
||||
the controls down a glass chip (`PlayerGlass`) top right shows it.
|
||||
the controls down and the video not playing a glass chip
|
||||
(`PlayerGlass`) top right shows it (while it plays nothing sits on the
|
||||
picture).
|
||||
**Toasts** (`toasts.dart`): the coordinator's toasts bottom left, with
|
||||
the avatar of their `ToastItem.actor` (feed and chat toasts; the app's
|
||||
own have none); each
|
||||
slides in, stays 5 s, then slides out and collapses its gap. A click on
|
||||
a chat toast opens the chat; the others let clicks through.
|
||||
a chat toast opens the chat; the others let clicks through. Chat toasts
|
||||
follow `AppSettings.chatPopups` (Settings → Notifications: All, Only
|
||||
when paused, Off); the message is in the chat either way.
|
||||
|
||||
Settings persist in two Hive boxes: `connect_form` (last form, recent
|
||||
servers and links, the profile picture and colour; saved even when a join
|
||||
@@ -1187,7 +1188,11 @@ testers land back on stable. `0.0.0-dev` (a dev run) checks nothing.
|
||||
- Media art the same way (`/i/...`, `ArtCache` as `coordinator.art`): the
|
||||
member who swapped the media in fetches poster and banner once the state
|
||||
shows its swap landed (`media_art.dart`: Jellyfin series/item images with
|
||||
its own account, else an AniList title search on the parsed release
|
||||
name) and sends `art`, so guests without Jellyfin get covers too.
|
||||
its own account; a web link's yt-dlp thumbnail, the one the Library's
|
||||
Recent grid shows, as poster and banner both, shrunk to a JPEG under the
|
||||
poster cap when larger; else an AniList title search on the parsed
|
||||
release name) and sends `art`, so guests without Jellyfin get covers too.
|
||||
`coordinator.nowPlaying` derives title, episode line and art hashes from
|
||||
`state.media` (`now_playing_info.dart`).
|
||||
`state.media` (`now_playing_info.dart`); poster and banner of one hash
|
||||
mark a thumbnail (`NowPlaying.thumbnail`): the media name as is, no
|
||||
episode line, a 16:9 cover in the controls and none on the start card.
|
||||
Reference in new issue
Block a user