// What a player went through, for the client log (E22 S5) — one definition for every player, so a // stall in the Plex player and one in the library player read the same in a diagnosis. // // A media ERROR is a warning at every level: it is the thing a user reports. The rest — play, pause, // buffering, seeking, the end, metadata, rate — is Részletes (DEBUG) and costs nothing below it. // Positions are whole seconds; nothing here names what is playing (the request log already has the // item's route template, and a title is the user's content). import { useCallback, useRef } from "react"; import { enabled, log } from "./log"; const MEDIA_EVENTS = [ "loadedmetadata", "play", "pause", "waiting", "stalled", "seeking", "seeked", "ratechange", "ended", "emptied", "error", ] as const; /** Log `media`'s playback events as `player`. Returns the detach. */ export function logMediaElement(media: HTMLMediaElement, player: string): () => void { const onEvent = (e: Event) => { const at = Math.round(media.currentTime || 0); if (e.type === "error") { const err = media.error; log.warn("player.media_error", `${player} media error ${err?.code ?? "?"}`, { player, code: err?.code ?? null, message: err?.message || null, at, }); return; } if (!enabled("DEBUG")) return; const attrs: Record = { player, event: e.type, at }; if (e.type === "ratechange") attrs.rate = media.playbackRate; if (e.type === "loadedmetadata" && Number.isFinite(media.duration)) { attrs.duration = Math.round(media.duration); } log.debug("player.media", `${player} ${e.type}`, attrs); }; for (const type of MEDIA_EVENTS) media.addEventListener(type, onEvent); return () => { for (const type of MEDIA_EVENTS) media.removeEventListener(type, onEvent); }; } /** A ref callback that logs the element it is given (and stops when React hands it null). */ export function useMediaLogRef(player: string): (media: HTMLMediaElement | null) => void { const detach = useRef<(() => void) | null>(null); return useCallback( (media: HTMLMediaElement | null) => { detach.current?.(); detach.current = media ? logMediaElement(media, player) : null; }, [player], ); } // The IFrame API's numeric player states, named (https://developers.google.com/youtube/iframe_api_reference). const YT_STATES: Record = { [-1]: "unstarted", 0: "ended", 1: "playing", 2: "paused", 3: "buffering", 5: "cued", }; /** The YouTube player changed state (DEBUG). */ export function logYouTubeState(state: number, at: number): void { if (!enabled("DEBUG")) return; const name = YT_STATES[state] ?? String(state); log.debug("player.media", `youtube ${name}`, { player: "youtube", event: name, at: Math.round(at), }); } /** The YouTube player refused the video — 2 bad id, 5 HTML5 error, 100 gone, 101/150 embed denied. */ export function logYouTubeError(code: number): void { log.warn("player.media_error", `youtube error ${code}`, { player: "youtube", code }); }