feat(player): the OS media controls, the basic keys and resume for the library and shared-link players
Plex, library and /watch tell the operating system what plays (title, line, picture) and take its
play / pause / skip / seek through one hook (lib/mediaSession); the Plex player drives it with its own
seek and clock, so the lock screen shows the film's position, not the stream's. The YouTube player is
left alone: its iframe already publishes all of it, and the OS routes to the frame that plays.
The library and /watch players get the basic keys (lib/mediaKeys, the keymap's mediaPlayer rows) and
open where the viewer stopped (lib/resume): the library keeps the point on the server, per user and
library item (library_states, POST /api/downloads/{id}/progress, listed back on both listings), the
shared link in this browser. A started library row continues, its menu can start it over, and its
picture carries the cards' progress bar. The resume rule (too early / near the end = from the start)
is one function shared with the YouTube route.
The library dialog opens focused on its panel (Modal initialFocus "player"): on the ✕, Space closed
the film. While the media element itself has the focus, Space and the arrows stay its own controls'
— the browser acts on them whatever a listener does, and one Space played and paused.
This commit is contained in:
@@ -286,3 +286,41 @@ describe("WatchPage honest failures", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// A shared link opens where THIS browser stopped in it (the viewer has no account), and keeps the
|
||||
// point as it plays — per link, so another link starts at its own place.
|
||||
describe("WatchPage resume", () => {
|
||||
function loaded(video: HTMLVideoElement, duration = 600) {
|
||||
let t = 0;
|
||||
Object.defineProperties(video, {
|
||||
currentTime: { get: () => t, set: (v: number) => void (t = v), configurable: true },
|
||||
duration: { get: () => duration, configurable: true },
|
||||
readyState: { get: () => 1, configurable: true },
|
||||
});
|
||||
video.dispatchEvent(new Event("loadedmetadata"));
|
||||
return video;
|
||||
}
|
||||
|
||||
it("keeps where it stopped, and opens there on the next visit", async () => {
|
||||
window.history.replaceState({}, "", "/watch/tok-resume");
|
||||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||||
const first = render(<WatchPage />);
|
||||
await waitFor(() => expect(first.container.querySelector("video")).not.toBeNull());
|
||||
const video = loaded(first.container.querySelector("video")!);
|
||||
expect(video.currentTime).toBe(0);
|
||||
video.dispatchEvent(new Event("playing"));
|
||||
video.currentTime = 250;
|
||||
video.dispatchEvent(new Event("pause"));
|
||||
cleanup();
|
||||
|
||||
const second = render(<WatchPage />);
|
||||
await waitFor(() => expect(second.container.querySelector("video")).not.toBeNull());
|
||||
expect(loaded(second.container.querySelector("video")!).currentTime).toBe(250);
|
||||
cleanup();
|
||||
|
||||
window.history.replaceState({}, "", "/watch/tok-other");
|
||||
const other = render(<WatchPage />);
|
||||
await waitFor(() => expect(other.container.querySelector("video")).not.toBeNull());
|
||||
expect(loaded(other.container.querySelector("video")!).currentTime).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user