Player: now-playing title with cover, start banner, one status pill; media art for everyone
The swapper sends the media's poster and banner once (`art`, Jellyfin images or AniList by parsed name); state carries their hashes and every member fetches them from /i/ like avatars. state.media.swapBy names who put the media up. uosc and the standard bar show cover, title and episode; a banner card shows before playback starts. The top-left health pill and the waiting pill become one frosted status pill in plain words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d092ea371a
commit
b534c8c7ea
29 files changed
+1892
-236
No files matched your search
+26
-5
@@ -604,9 +604,22 @@ sideways until an end, then the page).
|
||||
The player keys (Settings → Keyboard shortcuts) work while the player is
|
||||
full and nothing is in front of it. Something coming up while the player
|
||||
is closed opens it: full right after joining or after putting it up
|
||||
here, else mini. While held, a pill under the picture names who
|
||||
everyone waits for, or says "Waiting for you · press Space" (a click
|
||||
marks you ready); the mini shows it in its hover bar. ⚙ in both
|
||||
here, else mini. Both control designs show `NowPlayingTitle` top left
|
||||
(`now_playing_title.dart`: 2:3 cover, title, episode line from
|
||||
`coordinator.nowPlaying`, art through `artBytes`; it fades through when
|
||||
the media changes); the mini shows title and episode without the cover.
|
||||
Until playback first starts on a media (server not playing, position
|
||||
under 1 s) `StartBanner` (`start_banner.dart`) lies under the controls:
|
||||
the banner (else a blur of the cover, else dark), darkened, with cover,
|
||||
big title and episode; it fades out on play and stays gone for that
|
||||
media. One status pill (`player_status.dart`) sits bottom centre above
|
||||
the controls: `playerStatusFor` (pure) picks the most pressing of
|
||||
removed, reconnecting (Cancel), a warn/error health message (Retry),
|
||||
connecting (only after 2 s), finished, someone loading (their avatar
|
||||
and buffer progress), you not ready (a click marks you ready) and who
|
||||
everyone waits for (avatars, "2/3 ready"); nothing otherwise, info
|
||||
never. Frosted glass, colour only for problems; it fades through
|
||||
between kinds. The mini shows its short line in its hover bar. ⚙ in both
|
||||
control designs opens the player settings card (`controls/
|
||||
player_settings.dart`, `showPopover`): player design, progress line
|
||||
when hidden, upscaling shader, hardware decoding (set live on mpv's
|
||||
@@ -686,7 +699,7 @@ Session controls (the Standard player design): media_kit's
|
||||
`screens/session/player_controls.dart`); its own double-click and keys are
|
||||
off (its double-click pushes a fullscreen route with a second `Video`), the
|
||||
picture's double-click is a raw pointer observer (`DoubleClickListener`).
|
||||
The top bar holds the title, Change video, chat and back-to-library
|
||||
The top bar holds the now-playing title, Change video, chat and back-to-library
|
||||
(fullscreen: exit, then the window buttons); the bottom bar ends with the
|
||||
shader button, ⚙ and fullscreen. Its seek bar is off; ours
|
||||
(`ChapterSeekBar`: chapter diamonds, hover time with the chapter title)
|
||||
@@ -714,7 +727,8 @@ change video, chat & activity, reload, mark everyone ready, fullscreen,
|
||||
leave),
|
||||
subtitles and audio with track counts, a shader badge ("A HQ", opening the
|
||||
shared `showShaderMenu`), ready, mark everyone ready, reload; 1:30,
|
||||
chapters, previous / next episode, change video, volume, ⚙, fullscreen; chat
|
||||
chapters, previous / next episode, change video, volume, ⚙, fullscreen; the
|
||||
now-playing title top left (`UoscControls.topLeft`, with a top gradient), chat
|
||||
and back-to-library (fullscreen: exit) top right. Menus (`showUoscMenu`: a popup route above the button, the
|
||||
current row highlighted) list tracks and chapters with start times. All
|
||||
actions are the coordinator's (seeks through `player.seek`, the user path;
|
||||
@@ -791,3 +805,10 @@ this repo, written by `tool/release.sh`.
|
||||
the bytes of current members in memory and serves them at `/a/...`, which
|
||||
clients fetch once per hash (`AvatarCache`). Uplink per broadcast stays
|
||||
small whatever the picture size (cap 256 KiB base64 per avatar).
|
||||
- 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.
|
||||
`coordinator.nowPlaying` derives title, episode line and art hashes from
|
||||
`state.media` (`now_playing_info.dart`).
|
||||
+28
-3
@@ -24,7 +24,8 @@ with different `proto` refuse each other at `hello` (`proto mismatch`).
|
||||
`GET /j/<sid>/<token>/<itemId>` relays the current Jellyfin media (see
|
||||
"Jellyfin media" below). `/r/<sid>/<token>/...` is the relay of a
|
||||
member's media (see "Relay" below). `GET /a/<sid>/<token>/<hash>` serves
|
||||
a member's picture (see "Avatars" below).
|
||||
a member's picture (see "Avatars" below), `GET /i/<sid>/<token>/<hash>`
|
||||
the current media's art (see "Media art" below).
|
||||
|
||||
## Client to server
|
||||
|
||||
@@ -44,6 +45,7 @@ with different `proto` refuse each other at `hello` (`proto mismatch`).
|
||||
{"t":"chat","text":"..."}
|
||||
{"t":"kick","target":"<memberId>","reason":""}
|
||||
{"t":"profile","name":"...","avatar":"<base64>"}
|
||||
{"t":"art","mediaId":2,"poster":"<base64>","banner":"<base64>"}
|
||||
```
|
||||
|
||||
Field notes:
|
||||
@@ -77,6 +79,7 @@ Field notes:
|
||||
that is absent (or null) stays as it is; `avatar: ""` removes the picture.
|
||||
Same rules as `hello.user`; a too large avatar gets `err`
|
||||
`rejected: avatar too large` (the socket stays open, nothing changes).
|
||||
- `art`: pictures of the current media (see "Media art").
|
||||
- `chat.text` is cut to 500 characters.
|
||||
- `swap.media.url` is the link; `name`, `size`, `duration` are informational.
|
||||
`swap.media.jellyfin` makes it Jellyfin media (see below); then `url` is
|
||||
@@ -91,7 +94,7 @@ Field notes:
|
||||
"state":{<state>}}
|
||||
{"t":"state","eventId":7,"transport":"playing"|"held"|"finished","position":12.3,
|
||||
"serverTime":"<ISO-8601 UTC>","mediaId":1,
|
||||
"media":{name,size,duration,url?,jellyfin?}|absent,
|
||||
"media":{name,size,duration,url?,jellyfin?,relay?,swapBy?,art?}|absent,
|
||||
"members":[{"id","name","avatarHash"?,"ready","inSet","buffered","stale","jf"?,
|
||||
"host"?}],
|
||||
"seekBy":"m1","seekSeq":3,"relay":true}
|
||||
@@ -116,7 +119,9 @@ Field notes:
|
||||
`eventId` increases on seek, swap and transport changes.
|
||||
`mediaId` increases on swap. `media.relay` is present for relayed media
|
||||
(see "Relay"). `media.duration` is 0 until a client reports
|
||||
it. `seekBy` (member id) and `seekSeq` are present only while the current
|
||||
it. `media.swapBy`: member id of who swapped it in (omitted once that
|
||||
member left, and for the media the server started with). `media.art`:
|
||||
see "Media art". `seekBy` (member id) and `seekSeq` are present only while the current
|
||||
`eventId` came from a seek. `relay` (omitted when false): the server can
|
||||
relay the current Jellyfin media. `members[].jf` is the member's last
|
||||
`report.jf`, omitted when it never sent one. `members[].avatarHash`
|
||||
@@ -150,6 +155,26 @@ Field notes:
|
||||
picture, 410 unknown session. Pictures go with their member (leave,
|
||||
change, removal).
|
||||
|
||||
## Media art
|
||||
|
||||
```
|
||||
client {"t":"art","mediaId":2,"poster":"<base64>","banner":"<base64>"}
|
||||
state media.art = {"poster":"<hash>","banner":"<hash>"}
|
||||
```
|
||||
|
||||
- A portrait poster and a wide banner for the current media, so every
|
||||
member (guests without Jellyfin included) can show them. Sent by the
|
||||
member who made the current swap (`media.swapBy`) or the host, for the
|
||||
current `mediaId`; from anyone else, or for another `mediaId`, it is
|
||||
ignored. Each field is optional; one that is not a picture (same check
|
||||
as avatars) or larger than its cap (poster 256 KiB, banner 768 KiB of
|
||||
decoded bytes) counts as none. A later `art` replaces the earlier one
|
||||
whole; any swap clears it.
|
||||
- `media.art` carries hashes like `avatarHash`; a key is omitted when that
|
||||
picture is absent, `art` when both are.
|
||||
- `GET /i/<sid>/<token>/<hash>`: as `/a/` (see "Avatars"); 404 when the
|
||||
current media has no such art.
|
||||
|
||||
## Jellyfin media
|
||||
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user