import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { qk } from "../lib/queryKeys"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { plexLang } from "../i18n"; import { exposeVideoToBackgroundPlay } from "../lib/browserBackgroundPlay"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Hls from "hls.js"; import { ArrowLeft, Clock, Download, Gauge, Info, Keyboard, Languages, Loader2, Maximize, Minimize, Pause, Play, RotateCcw, Settings, SkipBack, SkipForward, Square, Volume2, VolumeX, } from "lucide-react"; import { api, HttpError } from "../lib/api"; import { notifyActionError } from "../lib/actionErrors"; import { audioOrdForLang, clamp01, clockParts, DEFAULT_PREFS, fmt, HLS_SUB_LAG, subOrdForLang, subShadowCss, type PlexPlayerPrefs, } from "../lib/plexPlayerPrefs"; import { Ctrl, PanelHead, Skip10 } from "./plexPlayerControls"; import { PlexPlayerSettings } from "./PlexPlayerSettings"; import { PlexStatsOverlay } from "./PlexStatsOverlay"; import { createFragMeter } from "../lib/plexStats"; import { enabled, log } from "../lib/log"; import { logMediaElement } from "../lib/mediaLog"; import { UpNextCard } from "./UpNextCard"; import Toaster, { useOverlayOrder } from "./ui/Toaster"; import { playhead, useAutoSkip, useControlsVisibility, useFullscreen, usePlayerVolume, usePlexProgress, usePlexTracks, useWallClock, } from "../lib/plexPlayerHooks"; import { fullscreenElement } from "../lib/fullscreen"; import { dispatch, isActivationTarget, isTypingTarget, toggleKeyboardHelp, useHelpConditions, useKeyboardHelpOpen, useKeyScope, usePaletteOpen, } from "../lib/keymap"; import { LS, useAccountPersistedObject } from "../lib/storage"; import { useDismiss } from "../lib/useDismiss"; import { useScrollFade } from "../lib/useScrollFade"; import { useBackToClose } from "../lib/history"; import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack"; // The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page. const PlexInfo = lazy(() => import("./PlexInfo")); // Reclaims of a 404'd HLS session, within `RECLAIM_WINDOW_MS`, before the player stops and says so. // Two covers the real case (the session was reaped, once) without letting a server that cannot serve // the stream keep the viewer on a spinner (see the ERROR handler). const MAX_SESSION_RECLAIMS = 2; // A ROLLING window, because "consecutive" could not bound anything: the budget used to be cleared by // the first fragment of any session, and a reclaimed session essentially always delivers one — // `start_session` does not return until its first segment exists. So both cases the ERROR handler // names restarted ffmpeg forever anyway (measured: 40 loss cycles → 41 sessions, never an error). // Two losses inside two minutes is a session that keeps being lost; one an hour apart is not. const RECLAIM_WINDOW_MS = 120_000; // The Plex module's rich, full-page player. Plays the LOCAL file: direct-playable files stream raw // (native