@zezosoft/react-player 0.0.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { memo, useCallback, useEffect, useRef, useState, useMemo } from 'react';
3
3
  import { useShallow } from 'zustand/react/shallow';
4
+ import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
4
5
  import { create } from 'zustand';
5
6
  import { IoVolumeMuteOutline, IoVolumeHighOutline } from 'react-icons/io5';
6
7
  import { IoMdClose } from 'react-icons/io';
7
- import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
8
8
  import screenfull from 'screenfull';
9
9
  import Hls from 'hls.js';
10
10
  import * as dashjs from 'dashjs';
@@ -36,7 +36,7 @@ function styleInject(css, ref) {
36
36
  }
37
37
  }
38
38
 
39
- var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-yellow-500: oklch(79.5% 0.184 86.047);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wide: 0.025em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .block {\n display: block;\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-24 {\n height: calc(var(--spacing) * 24);\n }\n .h-full {\n height: 100%;\n }\n .max-h-80 {\n max-height: calc(var(--spacing) * 80);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-24 {\n width: calc(var(--spacing) * 24);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[2px\\] {\n width: 2px;\n }\n .w-\\[10vw\\] {\n width: 10vw;\n }\n .w-\\[15vw\\] {\n width: 15vw;\n }\n .w-\\[720px\\] {\n width: 720px;\n }\n .w-fit {\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .rotate-45 {\n rotate: 45deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-not-allowed {\n cursor: not-allowed;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-3 {\n gap: calc(var(--spacing) * 3);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-\\[5px\\] {\n border-radius: 5px;\n }\n .rounded-\\[7px\\] {\n border-radius: 7px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-gray-600 {\n border-color: var(--color-gray-600);\n }\n .border-gray-700\\/60 {\n border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-gray-700) 60%, transparent);\n }\n }\n .border-white\\/10 {\n border-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .border-white\\/30 {\n border-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .border-white\\/40 {\n border-color: color-mix(in srgb, #fff 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 40%, transparent);\n }\n }\n .bg-\\[\\#2D2F31\\] {\n background-color: #2D2F31;\n }\n .bg-\\[\\#3a4049\\] {\n background-color: #3a4049;\n }\n .bg-\\[\\#454545\\] {\n background-color: #454545;\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/60 {\n background-color: color-mix(in srgb, #000 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 60%, transparent);\n }\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-gray-500 {\n background-color: var(--color-gray-500);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-white\\/5 {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n .bg-white\\/10 {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .bg-white\\/20 {\n background-color: color-mix(in srgb, #fff 20%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 20%, transparent);\n }\n }\n .bg-white\\/80 {\n background-color: color-mix(in srgb, #fff 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 80%, transparent);\n }\n }\n .bg-linear-to-b {\n --tw-gradient-position: to bottom;\n @supports (background-image: linear-gradient(in lab, red, red)) {\n --tw-gradient-position: to bottom in oklab;\n }\n background-image: linear-gradient(var(--tw-gradient-stops));\n }\n .from-black {\n --tw-gradient-from: var(--color-black);\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .from-black\\/80 {\n --tw-gradient-from: color-mix(in srgb, #000 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-from: color-mix(in oklab, var(--color-black) 80%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .via-transparent {\n --tw-gradient-via: transparent;\n --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-via-stops);\n }\n .to-black\\/90 {\n --tw-gradient-to: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-to: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\n }\n .object-contain {\n object-fit: contain;\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-10 {\n padding: calc(var(--spacing) * 10);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-5 {\n padding-inline: calc(var(--spacing) * 5);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .px-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .px-20 {\n padding-inline: calc(var(--spacing) * 20);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-6 {\n padding-top: calc(var(--spacing) * 6);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pb-4 {\n padding-bottom: calc(var(--spacing) * 4);\n }\n .pb-6 {\n padding-bottom: calc(var(--spacing) * 6);\n }\n .pb-10 {\n padding-bottom: calc(var(--spacing) * 10);\n }\n .pb-16 {\n padding-bottom: calc(var(--spacing) * 16);\n }\n .text-left {\n text-align: left;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wide {\n --tw-tracking: var(--tracking-wide);\n letter-spacing: var(--tracking-wide);\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-gray-200 {\n color: var(--color-gray-200);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-900 {\n color: var(--color-gray-900);\n }\n .text-sky-300 {\n color: var(--color-sky-300);\n }\n .text-white {\n color: var(--color-white);\n }\n .text-yellow-500 {\n color: var(--color-yellow-500);\n }\n .uppercase {\n text-transform: uppercase;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .backdrop-blur-md {\n --tw-backdrop-blur: blur(var(--blur-md));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .select-none {\n -webkit-user-select: none;\n user-select: none;\n }\n .hover\\:scale-105 {\n &:hover {\n @media (hover: hover) {\n --tw-scale-x: 105%;\n --tw-scale-y: 105%;\n --tw-scale-z: 105%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n }\n .hover\\:border-white\\/50 {\n &:hover {\n @media (hover: hover) {\n border-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n }\n .hover\\:bg-gray-300 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-gray-300);\n }\n }\n }\n .hover\\:bg-white\\/5 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/10 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/30 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 90%, transparent);\n }\n }\n }\n }\n .hover\\:text-gray-200 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n .hover\\:shadow-lg {\n &:hover {\n @media (hover: hover) {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-gray-400 {\n &:focus {\n --tw-ring-color: var(--color-gray-400);\n }\n }\n .focus\\:ring-offset-1 {\n &:focus {\n --tw-ring-offset-width: 1px;\n --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n .active\\:scale-95 {\n &:active {\n --tw-scale-x: 95%;\n --tw-scale-y: 95%;\n --tw-scale-z: 95%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:bg-white\\/50 {\n &:disabled {\n background-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .lg\\:h-32 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 32);\n }\n }\n .lg\\:w-32 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 32);\n }\n }\n .lg\\:pb-12 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 12);\n }\n }\n .lg\\:text-2xl {\n @media (width >= 64rem) {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n }\n .lg\\:text-3xl {\n @media (width >= 64rem) {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n }\n .lg\\:text-base {\n @media (width >= 64rem) {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\n}\n.icon-class {\n height: calc(var(--spacing) * 14);\n width: calc(var(--spacing) * 14);\n cursor: pointer;\n color: var(--color-gray-400);\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-gradient-position {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-via {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-to {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-via-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 0%;\n}\n@property --tw-gradient-via-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 50%;\n}\n@property --tw-gradient-to-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-backdrop-blur {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-scale-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-gradient-position: initial;\n --tw-gradient-from: #0000;\n --tw-gradient-via: #0000;\n --tw-gradient-to: #0000;\n --tw-gradient-stops: initial;\n --tw-gradient-via-stops: initial;\n --tw-gradient-from-position: 0%;\n --tw-gradient-via-position: 50%;\n --tw-gradient-to-position: 100%;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n }\n }\n}\n";
39
+ var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-red-400: oklch(70.4% 0.191 22.216);\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-red-600: oklch(57.7% 0.245 27.325);\n --color-red-700: oklch(50.5% 0.213 27.518);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --container-md: 28rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wider: 0.05em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .container {\n width: 100%;\n @media (width >= 40rem) {\n max-width: 40rem;\n }\n @media (width >= 48rem) {\n max-width: 48rem;\n }\n @media (width >= 64rem) {\n max-width: 64rem;\n }\n @media (width >= 80rem) {\n max-width: 80rem;\n }\n @media (width >= 96rem) {\n max-width: 96rem;\n }\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mt-4 {\n margin-top: calc(var(--spacing) * 4);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-1 {\n margin-left: calc(var(--spacing) * 1);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-12 {\n height: calc(var(--spacing) * 12);\n }\n .h-14 {\n height: calc(var(--spacing) * 14);\n }\n .h-full {\n height: 100%;\n }\n .max-h-80 {\n max-height: calc(var(--spacing) * 80);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-12 {\n width: calc(var(--spacing) * 12);\n }\n .w-14 {\n width: calc(var(--spacing) * 14);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[2px\\] {\n width: 2px;\n }\n .w-\\[10vw\\] {\n width: 10vw;\n }\n .w-\\[15vw\\] {\n width: 15vw;\n }\n .w-fit {\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .max-w-md {\n max-width: var(--container-md);\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .rotate-45 {\n rotate: 45deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-default {\n cursor: default;\n }\n .cursor-not-allowed {\n cursor: not-allowed;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .gap-1 {\n gap: calc(var(--spacing) * 1);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-3 {\n gap: calc(var(--spacing) * 3);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-\\[5px\\] {\n border-radius: 5px;\n }\n .rounded-\\[7px\\] {\n border-radius: 7px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-gray-600 {\n border-color: var(--color-gray-600);\n }\n .border-gray-700\\/60 {\n border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-gray-700) 60%, transparent);\n }\n }\n .border-white\\/10 {\n border-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .border-white\\/30 {\n border-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .border-white\\/40 {\n border-color: color-mix(in srgb, #fff 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 40%, transparent);\n }\n }\n .bg-\\[\\#3a4049\\] {\n background-color: #3a4049;\n }\n .bg-\\[\\#454545\\] {\n background-color: #454545;\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/60 {\n background-color: color-mix(in srgb, #000 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 60%, transparent);\n }\n }\n .bg-black\\/90 {\n background-color: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-gray-500 {\n background-color: var(--color-gray-500);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-red-600 {\n background-color: var(--color-red-600);\n }\n .bg-transparent {\n background-color: transparent;\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-white\\/10 {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .bg-white\\/20 {\n background-color: color-mix(in srgb, #fff 20%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 20%, transparent);\n }\n }\n .bg-white\\/80 {\n background-color: color-mix(in srgb, #fff 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 80%, transparent);\n }\n }\n .bg-linear-to-b {\n --tw-gradient-position: to bottom;\n @supports (background-image: linear-gradient(in lab, red, red)) {\n --tw-gradient-position: to bottom in oklab;\n }\n background-image: linear-gradient(var(--tw-gradient-stops));\n }\n .from-black {\n --tw-gradient-from: var(--color-black);\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .from-black\\/80 {\n --tw-gradient-from: color-mix(in srgb, #000 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-from: color-mix(in oklab, var(--color-black) 80%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .via-transparent {\n --tw-gradient-via: transparent;\n --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-via-stops);\n }\n .to-black\\/90 {\n --tw-gradient-to: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-to: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\n }\n .object-contain {\n object-fit: contain;\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-6 {\n padding: calc(var(--spacing) * 6);\n }\n .p-10 {\n padding: calc(var(--spacing) * 10);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-5 {\n padding-inline: calc(var(--spacing) * 5);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .px-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .px-20 {\n padding-inline: calc(var(--spacing) * 20);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-6 {\n padding-top: calc(var(--spacing) * 6);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pb-4 {\n padding-bottom: calc(var(--spacing) * 4);\n }\n .pb-6 {\n padding-bottom: calc(var(--spacing) * 6);\n }\n .pb-10 {\n padding-bottom: calc(var(--spacing) * 10);\n }\n .pb-16 {\n padding-bottom: calc(var(--spacing) * 16);\n }\n .text-center {\n text-align: center;\n }\n .text-left {\n text-align: left;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wider {\n --tw-tracking: var(--tracking-wider);\n letter-spacing: var(--tracking-wider);\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-gray-200 {\n color: var(--color-gray-200);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-900 {\n color: var(--color-gray-900);\n }\n .text-red-400 {\n color: var(--color-red-400);\n }\n .text-red-500 {\n color: var(--color-red-500);\n }\n .text-red-600 {\n color: var(--color-red-600);\n }\n .text-sky-300 {\n color: var(--color-sky-300);\n }\n .text-white {\n color: var(--color-white);\n }\n .normal-case {\n text-transform: none;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .backdrop-blur-md {\n --tw-backdrop-blur: blur(var(--blur-md));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .select-none {\n -webkit-user-select: none;\n user-select: none;\n }\n .hover\\:scale-105 {\n &:hover {\n @media (hover: hover) {\n --tw-scale-x: 105%;\n --tw-scale-y: 105%;\n --tw-scale-z: 105%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n }\n .hover\\:border-white\\/50 {\n &:hover {\n @media (hover: hover) {\n border-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n }\n .hover\\:bg-gray-300 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-gray-300);\n }\n }\n }\n .hover\\:bg-red-700 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-red-700);\n }\n }\n }\n .hover\\:bg-white\\/5 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/10 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/30 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 90%, transparent);\n }\n }\n }\n }\n .hover\\:text-gray-200 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n .hover\\:shadow-lg {\n &:hover {\n @media (hover: hover) {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-gray-400 {\n &:focus {\n --tw-ring-color: var(--color-gray-400);\n }\n }\n .focus\\:ring-offset-1 {\n &:focus {\n --tw-ring-offset-width: 1px;\n --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n .active\\:scale-95 {\n &:active {\n --tw-scale-x: 95%;\n --tw-scale-y: 95%;\n --tw-scale-z: 95%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:bg-white\\/50 {\n &:disabled {\n background-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .lg\\:h-18 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n }\n .lg\\:w-18 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n }\n .lg\\:pb-12 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 12);\n }\n }\n .lg\\:text-2xl {\n @media (width >= 64rem) {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n }\n .lg\\:text-3xl {\n @media (width >= 64rem) {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n }\n .lg\\:text-base {\n @media (width >= 64rem) {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\n}\n.icon-class {\n height: calc(var(--spacing) * 14);\n width: calc(var(--spacing) * 14);\n cursor: pointer;\n color: var(--color-gray-400);\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-gradient-position {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-via {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-to {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-via-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 0%;\n}\n@property --tw-gradient-via-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 50%;\n}\n@property --tw-gradient-to-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-backdrop-blur {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-scale-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-gradient-position: initial;\n --tw-gradient-from: #0000;\n --tw-gradient-via: #0000;\n --tw-gradient-to: #0000;\n --tw-gradient-stops: initial;\n --tw-gradient-via-stops: initial;\n --tw-gradient-from-position: 0%;\n --tw-gradient-via-position: 50%;\n --tw-gradient-to-position: 100%;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n }\n }\n}\n";
40
40
  styleInject(css_248z$4,{"insertAt":"top"});
41
41
 
42
42
  const createVideoRefsSlice = (set) => ({
@@ -138,6 +138,12 @@ const createAdsSlice = (set, get) => ({
138
138
  setAdVideoRef: (adVideoRef) => set({ adVideoRef }),
139
139
  });
140
140
 
141
+ const createErrorSlice = (set) => ({
142
+ error: null,
143
+ setError: (error) => set({ error }),
144
+ clearError: () => set({ error: null }),
145
+ });
146
+
141
147
  const createResetSlice = (set, get) => ({
142
148
  resetStore: () => {
143
149
  const safeStopMediaElement = (media) => {
@@ -211,6 +217,7 @@ const useVideoStore = create()((set, get, store) => ({
211
217
  ...createEpisodesSlice(set),
212
218
  ...createIntroSlice(set),
213
219
  ...createAdsSlice(set),
220
+ ...createErrorSlice(set),
214
221
  ...createResetSlice(set, get),
215
222
  }));
216
223
 
@@ -635,7 +642,22 @@ const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThum
635
642
  React__default.createElement(Thumb, { max: max, currentTime: currentTime, isThumbActive: isThumbActive, trackColor: trackColor })));
636
643
  };
637
644
 
638
- const BottomControls = ({ config }) => {
645
+ const formatTimeMemo = (() => {
646
+ const cache = new Map();
647
+ return (seconds) => {
648
+ if (cache.has(seconds))
649
+ return cache.get(seconds);
650
+ const formatted = timeFormat(seconds);
651
+ cache.set(seconds, formatted);
652
+ if (cache.size > 100) {
653
+ const firstKey = cache.keys().next().value;
654
+ if (firstKey !== undefined)
655
+ cache.delete(firstKey);
656
+ }
657
+ return formatted;
658
+ };
659
+ })();
660
+ const BottomControls = memo(({ config }) => {
639
661
  const { videoRef, currentTime, isFullscreen, bufferedProgress, isAdPlaying } = useVideoStore(useShallow((state) => ({
640
662
  videoRef: state.videoRef,
641
663
  currentTime: state.currentTime,
@@ -647,31 +669,33 @@ const BottomControls = ({ config }) => {
647
669
  const currentTimeValue = currentTime || 0;
648
670
  const bufferedValue = bufferedProgress || 0;
649
671
  const handleSeek = useCallback((currentTimeInMs) => {
650
- if (!videoRef) {
672
+ if (!videoRef)
651
673
  return;
652
- }
653
674
  videoRef.currentTime = currentTimeInMs / 1000;
654
675
  }, [videoRef]);
655
676
  const bufferTime = useMemo(() => {
656
- if (!duration) {
677
+ if (!duration)
657
678
  return 0;
658
- }
659
679
  return secondsToMilliseconds(duration * (bufferedValue / 100));
660
680
  }, [bufferedValue, duration]);
661
- const durationFormatted = useMemo(() => timeFormat(duration), [duration]);
662
- const currentTimeFormatted = useMemo(() => timeFormat(currentTimeValue), [currentTimeValue]);
663
- if (isAdPlaying) {
681
+ const roundedCurrentTime = useMemo(() => Math.floor(currentTimeValue), [currentTimeValue]);
682
+ const roundedDuration = useMemo(() => Math.floor(duration), [duration]);
683
+ const durationFormatted = useMemo(() => formatTimeMemo(roundedDuration), [roundedDuration]);
684
+ const currentTimeFormatted = useMemo(() => formatTimeMemo(roundedCurrentTime), [roundedCurrentTime]);
685
+ const seekSliderMax = useMemo(() => secondsToMilliseconds(duration), [duration]);
686
+ const seekSliderCurrentTime = useMemo(() => secondsToMilliseconds(currentTimeValue), [currentTimeValue]);
687
+ if (isAdPlaying)
664
688
  return null;
665
- }
666
689
  return (React__default.createElement("div", { className: "px-10" },
667
- React__default.createElement(VideoSeekSlider, { max: secondsToMilliseconds(duration), currentTime: secondsToMilliseconds(currentTimeValue), bufferTime: bufferTime, onChange: handleSeek, secondsPrefix: "00:00:", minutesPrefix: "00:", getPreviewScreenUrl: config?.seekBarConfig?.getPreviewScreenUrl, timeCodes: config?.seekBarConfig?.timeCodes, trackColor: config?.seekBarConfig?.trackColor }),
690
+ React__default.createElement(VideoSeekSlider, { max: seekSliderMax, currentTime: seekSliderCurrentTime, bufferTime: bufferTime, onChange: handleSeek, secondsPrefix: "00:00:", minutesPrefix: "00:", getPreviewScreenUrl: config?.seekBarConfig?.getPreviewScreenUrl, timeCodes: config?.seekBarConfig?.timeCodes, trackColor: config?.seekBarConfig?.trackColor }),
668
691
  React__default.createElement("div", { className: `pt-6 ${isFullscreen ? "pb-10" : "pb-16"} lg:pb-12 flex items-center gap-4 text-white` },
669
692
  React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-white cursor-pointer hover:text-gray-200 transition-colors duration-200" }, currentTimeFormatted),
670
693
  React__default.createElement("span", { className: "text-lg lg:text-3xl font-semibold text-gray-500 cursor-pointer hover:text-gray-200 transition-colors duration-200" }, "/"),
671
694
  React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-gray-400 cursor-pointer hover:text-gray-200 transition-colors duration-200" }, durationFormatted))));
672
- };
695
+ });
696
+ BottomControls.displayName = "BottomControls";
673
697
 
674
- var css_248z$2 = ".icon-button {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.icon-button:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .icon-button {\n width: 32px;\n height: 32px;\n }\n}\n\n.fullscreen-icon {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.fullscreen-icon:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .fullscreen-icon {\n width: 32px;\n height: 32px;\n }\n}\n\n.pip-toggle {\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.pip-toggle:hover {\n color: #e5e7eb;\n}\n\n.pip-icon {\n width: 15px;\n height: 15px;\n}\n\n@media (min-width: 1024px) {\n .pip-icon {\n width: 28px;\n height: 28px;\n }\n}\n";
698
+ var css_248z$2 = ".icon-button {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.icon-button:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .icon-button {\n width: 32px;\n height: 32px;\n }\n}\n\n.fullscreen-icon {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.fullscreen-icon:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .fullscreen-icon {\n width: 32px;\n height: 32px;\n }\n}\n\n.pip-toggle {\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.pip-toggle:hover {\n color: #e5e7eb;\n}\n\n.pip-icon {\n width: 15px;\n height: 15px;\n}\n\n@media (min-width: 1024px) {\n .pip-icon {\n width: 28px;\n height: 28px;\n }\n}\n\n/* Ad Badge Header - Dark/Black Background */\n.ad-badge-header {\n background: rgba(45, 47, 49, 0.95);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: \n 0 4px 16px rgba(0, 0, 0, 0.4),\n 0 0 0 1px rgba(255, 255, 255, 0.1);\n border: 1px solid rgba(255, 255, 255, 0.1);\n position: relative;\n}\n\n/* Time display in badge - lighter gray */\n.ad-badge-header > span:last-child {\n color: rgba(209, 213, 219, 0.9);\n font-weight: 500;\n letter-spacing: 0.025em;\n}\n\n/* Pulse animation for the dot */\n@keyframes pulse-dot {\n 0%, 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.7;\n transform: scale(0.9);\n }\n}\n\n.ad-badge-header .animate-pulse {\n animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n/* Responsive adjustments */\n@media (max-width: 768px) {\n .ad-badge-header {\n padding: 0.375rem 0.75rem;\n font-size: 0.625rem;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ad-badge-header .animate-pulse {\n animation: none;\n }\n}\n\n@media (prefers-contrast: high) {\n .ad-badge-header {\n background: #2d2f31;\n border: 2px solid #fff;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n }\n}";
675
699
  styleInject(css_248z$2,{"insertAt":"top"});
676
700
 
677
701
  const FullScreenToggle = ({ isFullScreen, onClick, className = "fullscreen-icon", }) => {
@@ -691,7 +715,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
691
715
  const [isOpen, setIsOpen] = useState(false);
692
716
  const popoverRef = useRef(null);
693
717
  const buttonRef = useRef(null);
694
- // Close on outside click or Escape key
695
718
  useEffect(() => {
696
719
  const handleClickOutside = (event) => {
697
720
  if (popoverRef.current &&
@@ -716,7 +739,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
716
739
  const togglePopover = () => {
717
740
  setIsOpen((prev) => (closeOnButtonClick ? !prev : true));
718
741
  };
719
- // Get alignment classes
720
742
  const getAlignmentClasses = () => {
721
743
  switch (align) {
722
744
  case "center":
@@ -728,7 +750,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
728
750
  return "left-0";
729
751
  }
730
752
  };
731
- // Arrow is always centered regardless of popover alignment
732
753
  const getArrowPositionClasses = () => {
733
754
  return "left-1/2 -translate-x-1/2";
734
755
  };
@@ -752,15 +773,39 @@ const Tooltip = ({ children, title, position = "top", className, }) => {
752
773
  visible && (React__default.createElement("div", { className: `absolute z-50 px-3 py-1 text-sm text-white bg-gray-900 rounded-md shadow-md transition-opacity duration-200 ease-in-out whitespace-nowrap ${positionStyles[position]}` }, title))));
753
774
  };
754
775
 
755
- const Settings = ({ iconClassName }) => {
776
+ const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
777
+ const Settings = ({ iconClassName, qualityConfig, }) => {
778
+ const showQualityInSettings = qualityConfig?.showInSettings !== false;
756
779
  const { qualityLevels, activeQuality, currentQuality, subtitles, activeSubtitle, setActiveSubtitle, videoRef, streamType, } = useVideoStore();
757
- const [speed, setSpeed] = React.useState(1);
780
+ const getStoredPlaybackSpeed = () => {
781
+ try {
782
+ const stored = localStorage.getItem("react-player-playback-speed");
783
+ if (stored) {
784
+ const speed = parseFloat(stored);
785
+ if (speedOptions.includes(speed))
786
+ return speed;
787
+ }
788
+ }
789
+ catch { }
790
+ return 1;
791
+ };
792
+ const [speed, setSpeed] = React.useState(getStoredPlaybackSpeed());
758
793
  const [activeMenu, setActiveMenu] = React.useState("main");
794
+ React.useEffect(() => {
795
+ if (videoRef) {
796
+ const storedSpeed = getStoredPlaybackSpeed();
797
+ videoRef.playbackRate = storedSpeed;
798
+ setSpeed(storedSpeed);
799
+ }
800
+ }, [videoRef]);
759
801
  const handleSpeedChange = (newSpeed) => {
760
802
  setSpeed(newSpeed);
761
- if (videoRef) {
803
+ if (videoRef)
762
804
  videoRef.playbackRate = newSpeed;
805
+ try {
806
+ localStorage.setItem("react-player-playback-speed", newSpeed.toString());
763
807
  }
808
+ catch { }
764
809
  };
765
810
  const isAdaptiveStream = streamType === "hls" || streamType === "dash";
766
811
  const qualityOptions = React.useMemo(() => {
@@ -785,27 +830,23 @@ const Settings = ({ iconClassName }) => {
785
830
  return b.originalIndex - a.originalIndex;
786
831
  });
787
832
  }, [qualityLevels, isAdaptiveStream, streamType]);
788
- const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
789
833
  const handleBack = () => setActiveMenu("main");
790
834
  const formatBitrate = (bitrate) => {
791
835
  if (!bitrate || bitrate <= 0)
792
836
  return "";
793
- if (bitrate >= 1000000) {
837
+ if (bitrate >= 1000000)
794
838
  return `${(bitrate / 1000000).toFixed(1)} Mbps`;
795
- }
796
839
  return `${Math.round(bitrate / 1000)} Kbps`;
797
840
  };
798
- // Get quality label: show explicit resolution to avoid duplicates
799
841
  const getQualityName = (height, bitrate) => {
800
842
  if (height && height > 0)
801
843
  return `${height}p`;
802
844
  const bitrateLabel = formatBitrate(bitrate);
803
845
  return bitrateLabel || "Quality";
804
846
  };
805
- // Get quality label for display
806
847
  const getQualityLabel = () => {
807
- if (!isAdaptiveStream)
808
- return "Auto";
848
+ if (!isAdaptiveStream || qualityOptions.length === 0)
849
+ return "Off";
809
850
  if (currentQuality === "auto")
810
851
  return "Auto";
811
852
  const option = qualityOptions.find((q) => q.value === currentQuality);
@@ -814,15 +855,12 @@ const Settings = ({ iconClassName }) => {
814
855
  const label = getQualityName(option.height, option.bitrate);
815
856
  return label === "Quality" ? "Custom" : label;
816
857
  };
817
- // Get estimated data usage using bitrate when available
858
+ const hasQualityOptions = isAdaptiveStream && qualityOptions.length > 0;
818
859
  const getDataUsage = (height, bitrate) => {
819
- // bitrate in bits/sec -> GB/hour
820
860
  if (bitrate && bitrate > 0) {
821
861
  const gbPerHour = (bitrate * 3600) / 8 / 1e9;
822
- const rounded = gbPerHour.toFixed(2);
823
- return `Uses about ${rounded} GB per hour`;
862
+ return `Uses about ${gbPerHour.toFixed(2)} GB per hour`;
824
863
  }
825
- // Fallback by resolution when bitrate missing
826
864
  if (height >= 2160)
827
865
  return "Uses about 7.00 GB per hour";
828
866
  if (height >= 1440)
@@ -844,7 +882,7 @@ const Settings = ({ iconClassName }) => {
844
882
  React.createElement("h3", { className: "text-white font-bold text-xl mb-4" }, "Settings"),
845
883
  React.createElement("p", { className: "text-gray-300 text-sm mb-4" }, "Customize playback"),
846
884
  React.createElement("div", { className: "space-y-0 border-t border-gray-600" },
847
- React.createElement("button", { onClick: () => setActiveMenu("quality"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
885
+ showQualityInSettings && (React.createElement("button", { onClick: () => setActiveMenu("quality"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
848
886
  React.createElement("div", { className: "flex items-center gap-3" },
849
887
  React.createElement("div", { className: "p-2 bg-blue-500 rounded-md" },
850
888
  React.createElement("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
@@ -852,7 +890,7 @@ const Settings = ({ iconClassName }) => {
852
890
  React.createElement("div", { className: "text-left" },
853
891
  React.createElement("div", { className: "text-white font-semibold" }, "Quality"),
854
892
  React.createElement("div", { className: "text-gray-400 text-sm" }, getQualityLabel()))),
855
- React.createElement(ChevronRight, { className: "w-5 h-5 text-gray-400" })),
893
+ React.createElement(ChevronRight, { className: "w-5 h-5 text-gray-400" }))),
856
894
  React.createElement("button", { onClick: () => setActiveMenu("subtitles"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
857
895
  React.createElement("div", { className: "flex items-center gap-3" },
858
896
  React.createElement("div", { className: "p-2 bg-purple-500 rounded-md" },
@@ -878,30 +916,26 @@ const Settings = ({ iconClassName }) => {
878
916
  React.createElement("button", { onClick: handleBack, className: "p-1 rounded-md transition-colors" },
879
917
  React.createElement(ChevronRight, { className: "w-6 h-6 text-white rotate-180" })),
880
918
  React.createElement("h3", { className: "text-white font-bold text-xl" }, "Video Quality")),
881
- React.createElement("div", { className: "space-y-3" },
882
- React.createElement("button", { onClick: () => {
883
- if (isAdaptiveStream) {
884
- QualityManager.setQuality(streamType, "auto");
885
- }
886
- }, disabled: !isAdaptiveStream, className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
919
+ React.createElement("div", { className: "space-y-3" }, hasQualityOptions ? (React.createElement(React.Fragment, null,
920
+ React.createElement("button", { onClick: () => QualityManager.setQuality(streamType, "auto"), className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
887
921
  ? "bg-white/10"
888
- : isAdaptiveStream
889
- ? "hover:bg-white/5"
890
- : "opacity-50 cursor-not-allowed"}` },
922
+ : "hover:bg-white/5"}` },
891
923
  React.createElement("div", { className: "flex items-start justify-between" },
892
924
  React.createElement("div", null,
893
925
  React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, "Auto"),
894
926
  React.createElement("div", { className: "text-gray-400 text-sm" }, "Adjust to your connection")),
895
927
  activeQuality === "auto" && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" })))),
896
- isAdaptiveStream && qualityOptions.length > 0 ? (qualityOptions.map((level) => (React.createElement("button", { key: level.value, onClick: () => QualityManager.setQuality(streamType, level.value), className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === level.value
928
+ qualityOptions.map((level) => (React.createElement("button", { key: level.value, onClick: () => QualityManager.setQuality(streamType, level.value), className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === level.value
897
929
  ? "bg-white/10"
898
930
  : "hover:bg-white/5"}` },
899
931
  React.createElement("div", { className: "flex items-start justify-between" },
900
932
  React.createElement("div", null,
901
933
  React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, getQualityName(level.height, level.bitrate)),
902
934
  React.createElement("div", { className: "text-gray-400 text-sm" }, getDataUsage(level.height, level.bitrate))),
903
- (activeQuality === level.value ||
904
- currentQuality === level.value) && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" }))))))) : (React.createElement("div", { className: "px-4 py-3 text-gray-400 text-sm bg-white/5 rounded-md" }, "Quality selection is unavailable for this stream."))))),
935
+ activeQuality === level.value && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" })))))))) : (React.createElement("button", { className: "w-full text-left px-4 py-3 rounded-md bg-white/10 cursor-default" },
936
+ React.createElement("div", { className: "flex items-start justify-between" },
937
+ React.createElement("span", { className: "text-white font-semibold text-lg" }, "Off"),
938
+ React.createElement(Check, { className: "w-6 h-6 text-white mt-1" }))))))),
905
939
  activeMenu === "subtitles" && (React.createElement("div", { className: "p-4" },
906
940
  React.createElement("div", { className: "flex items-center gap-3 mb-4" },
907
941
  React.createElement("button", { onClick: handleBack, className: "p-1 hover:bg-white/10 rounded-md transition-colors" },
@@ -910,7 +944,7 @@ const Settings = ({ iconClassName }) => {
910
944
  React.createElement("div", { className: "space-y-3" },
911
945
  React.createElement("button", { onClick: () => setActiveSubtitle(null), className: `w-full text-left px-4 py-3 rounded-[5px] transition-all flex items-center justify-between ${!activeSubtitle ? "bg-[#454545]" : ""}` },
912
946
  React.createElement("span", { className: "text-white font-semibold text-lg" }, "Off"),
913
- !activeSubtitle && React.createElement(Check, { className: "w-6 h-6 text-white" })),
947
+ !activeSubtitle && (React.createElement(Check, { className: "w-6 h-6 text-white" }))),
914
948
  subtitles?.map((subtitle, index) => (React.createElement("button", { key: index, onClick: () => setActiveSubtitle(subtitle), className: `w-full text-left px-4 py-3 rounded-md transition-all flex items-center justify-between ${activeSubtitle?.label === subtitle.label
915
949
  ? "bg-white/10"
916
950
  : "hover:bg-white/5"}` },
@@ -923,12 +957,12 @@ const Settings = ({ iconClassName }) => {
923
957
  React.createElement("h3", { className: "text-white font-bold text-xl" }, "Playback Speed")),
924
958
  React.createElement("div", { className: "space-y-3 max-h-80 overflow-y-auto" }, speedOptions.map((s) => (React.createElement("button", { key: s, onClick: () => handleSpeedChange(s), className: `w-full text-left px-4 py-3 rounded-[5px] transition-all flex items-center justify-between ${speed === s ? "bg-[#454545]" : ""}` },
925
959
  React.createElement("span", { className: "text-white font-semibold text-lg" }, s === 1 ? "Normal" : `${s}x`),
926
- speed === s && React.createElement(Check, { className: "w-6 h-6 text-white" })))))))))));
960
+ speed === s && (React.createElement(Check, { className: "w-6 h-6 text-white" }))))))))))));
927
961
  };
928
962
 
929
963
  const ControlsHeader = ({ config }) => {
930
964
  const iconClassName = "icon-button";
931
- const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, } = useVideoStore(useShallow((state) => ({
965
+ const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, adCurrentTime, } = useVideoStore(useShallow((state) => ({
932
966
  videoWrapperRef: state.videoWrapperRef,
933
967
  videoRef: state.videoRef,
934
968
  adVideoRef: state.adVideoRef,
@@ -938,7 +972,40 @@ const ControlsHeader = ({ config }) => {
938
972
  isAdPlaying: state.isAdPlaying,
939
973
  muted: state.muted,
940
974
  setMuted: state.setMuted,
975
+ adCurrentTime: state.adCurrentTime,
941
976
  })));
977
+ const [adDuration, setAdDuration] = React.useState(0);
978
+ React.useEffect(() => {
979
+ if (!adVideoRef || !isAdPlaying) {
980
+ setAdDuration(0);
981
+ return;
982
+ }
983
+ const updateDuration = () => {
984
+ if (adVideoRef.duration && Number.isFinite(adVideoRef.duration)) {
985
+ setAdDuration(adVideoRef.duration);
986
+ }
987
+ };
988
+ updateDuration();
989
+ adVideoRef.addEventListener("loadedmetadata", updateDuration);
990
+ adVideoRef.addEventListener("durationchange", updateDuration);
991
+ return () => {
992
+ adVideoRef.removeEventListener("loadedmetadata", updateDuration);
993
+ adVideoRef.removeEventListener("durationchange", updateDuration);
994
+ };
995
+ }, [adVideoRef, isAdPlaying]);
996
+ const formatTime = React.useCallback((seconds) => {
997
+ if (isNaN(seconds) || seconds < 0)
998
+ return "0:00";
999
+ const mins = Math.floor(seconds / 60);
1000
+ const secs = Math.floor(seconds % 60);
1001
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1002
+ }, []);
1003
+ const adTimeRemaining = React.useMemo(() => {
1004
+ if (adDuration <= 0 || adCurrentTime <= 0)
1005
+ return null;
1006
+ const remaining = Math.max(0, adDuration - adCurrentTime);
1007
+ return formatTime(remaining);
1008
+ }, [adDuration, adCurrentTime, formatTime]);
942
1009
  const [isPipActive, setIsPipActive] = React.useState(false);
943
1010
  const [isFullscreen, setIsFullscreen] = React.useState(false);
944
1011
  const handleFullscreen = () => {
@@ -986,7 +1053,7 @@ const ControlsHeader = ({ config }) => {
986
1053
  setIsPipActive(false);
987
1054
  }
988
1055
  }
989
- catch (_error) { }
1056
+ catch { }
990
1057
  };
991
1058
  React.useEffect(() => {
992
1059
  const handlePipChange = () => setIsPipActive(!!document.pictureInPictureElement);
@@ -999,12 +1066,12 @@ const ControlsHeader = ({ config }) => {
999
1066
  }, []);
1000
1067
  const handleClose = () => {
1001
1068
  resetStore();
1002
- if (config?.onClose) {
1003
- config.onClose();
1004
- }
1069
+ config?.onClose?.();
1005
1070
  };
1006
1071
  const renderAdHeader = () => (React.createElement("div", { className: "flex items-center gap-4" },
1007
- React.createElement("span", { className: "inline-flex items-center rounded bg-[#2D2F31] px-3 py-1 text-xs font-semibold uppercase tracking-wide text-yellow-500" }, "Ad")));
1072
+ React.createElement("span", { className: "inline-flex items-center gap-1 rounded-full px-4 py-2 font-medium text-xs tracking-wider text-red-600 border border-gray-700/60" },
1073
+ React.createElement("span", null, "Ad"),
1074
+ adTimeRemaining && (React.createElement("span", { className: "text-gray-300 font-normal normal-case ml-1 text-xs" }, adTimeRemaining)))));
1008
1075
  const renderVideoHeader = () => (React.createElement("div", { className: "flex" },
1009
1076
  React.createElement("div", null,
1010
1077
  React.createElement("h1", { className: "text-gray-200 text-lg lg:text-2xl font-semibold" }, episodeList.length > 0
@@ -1014,7 +1081,7 @@ const ControlsHeader = ({ config }) => {
1014
1081
  return (React.createElement("div", { className: "flex items-center justify-between p-10 bg-linear-to-b from-black" },
1015
1082
  isAdPlaying ? renderAdHeader() : renderVideoHeader(),
1016
1083
  React.createElement("div", { className: "flex items-center gap-7 text-white" },
1017
- !isAdPlaying && React.createElement(Settings, { iconClassName: iconClassName }),
1084
+ !isAdPlaying && (React.createElement(Settings, { iconClassName: iconClassName, qualityConfig: config?.qualityConfig })),
1018
1085
  React.createElement("div", { onClick: handleMute }, muted ? (React.createElement(Tooltip, { title: "Unmute" },
1019
1086
  React.createElement(IoVolumeMuteOutline, { className: iconClassName }))) : (React.createElement(Tooltip, { title: "Mute" },
1020
1087
  React.createElement(IoVolumeHighOutline, { className: iconClassName })))),
@@ -1062,7 +1129,10 @@ const MiddleControls = () => {
1062
1129
  setIsPlaying: state.setIsPlaying,
1063
1130
  isAdPlaying: state.isAdPlaying,
1064
1131
  })));
1065
- const [isBuffering, setIsBuffering] = useState(false);
1132
+ const { setIsBuffering } = useVideoStore(useShallow((state) => ({
1133
+ setIsBuffering: state.setIsBuffering,
1134
+ })));
1135
+ const [isBuffering, setIsBufferingLocal] = useState(false);
1066
1136
  const videoElement = isAdPlaying ? adVideoRef : videoRef;
1067
1137
  const resetControlsVisibility = useCallback(() => {
1068
1138
  if (typeof window === "undefined") {
@@ -1103,15 +1173,33 @@ const MiddleControls = () => {
1103
1173
  useEffect(() => {
1104
1174
  if (!videoElement)
1105
1175
  return;
1106
- const handleWaiting = () => setIsBuffering(true);
1107
- const handlePlaying = () => setIsBuffering(false);
1176
+ const handleWaiting = () => {
1177
+ setIsBufferingLocal(true);
1178
+ setIsBuffering(true);
1179
+ };
1180
+ const handlePlaying = () => {
1181
+ setIsBufferingLocal(false);
1182
+ setIsBuffering(false);
1183
+ };
1184
+ const handleCanPlay = () => {
1185
+ setIsBufferingLocal(false);
1186
+ setIsBuffering(false);
1187
+ };
1188
+ const handleStalled = () => {
1189
+ setIsBufferingLocal(true);
1190
+ setIsBuffering(true);
1191
+ };
1108
1192
  videoElement.addEventListener("waiting", handleWaiting);
1109
1193
  videoElement.addEventListener("playing", handlePlaying);
1194
+ videoElement.addEventListener("canplay", handleCanPlay);
1195
+ videoElement.addEventListener("stalled", handleStalled);
1110
1196
  return () => {
1111
1197
  videoElement.removeEventListener("waiting", handleWaiting);
1112
1198
  videoElement.removeEventListener("playing", handlePlaying);
1199
+ videoElement.removeEventListener("canplay", handleCanPlay);
1200
+ videoElement.removeEventListener("stalled", handleStalled);
1113
1201
  };
1114
- }, [videoElement, isAdPlaying]);
1202
+ }, [videoElement, isAdPlaying, setIsBuffering]);
1115
1203
  useEffect(() => {
1116
1204
  const handleKeyDown = (e) => {
1117
1205
  if (!videoElement || isAdPlaying)
@@ -1143,11 +1231,12 @@ const MiddleControls = () => {
1143
1231
  ]);
1144
1232
  if (isAdPlaying) {
1145
1233
  return (React__default.createElement("div", { className: "flex justify-center items-center" },
1146
- React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(Loader, { className: "w-24 h-24 lg:w-32 lg:h-32 animate-spin text-white" })) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) })));
1234
+ React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement("div", { className: "relative" },
1235
+ React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" }))) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) })));
1147
1236
  }
1148
1237
  return (React__default.createElement("div", { className: "flex justify-center items-center" },
1149
1238
  React__default.createElement(ControlButton, { onClick: handleBackward, className: "w-[15vw]", icon: React__default.createElement(BackwardIcon, null) }),
1150
- React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(Loader, { className: "w-24 h-24 lg:w-32 lg:h-32 animate-spin text-white" })) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) }),
1239
+ React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" })) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) }),
1151
1240
  React__default.createElement(ControlButton, { onClick: handleForward, className: "w-[15vw]", icon: React__default.createElement(ForwardIcon, null) })));
1152
1241
  };
1153
1242
 
@@ -1159,8 +1248,7 @@ const VideoPlayerControls = ({ config }) => {
1159
1248
  React.createElement(BottomControls, { config: config?.bottomConfig?.config }))));
1160
1249
  };
1161
1250
 
1162
- const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "left", }) => {
1163
- // Increase icon size and apply consistent color to icon
1251
+ const VideoActionButton = React__default.memo(({ text, onClick, icon, disabled = false, position = "left", }) => {
1164
1252
  const renderedIcon = icon
1165
1253
  ? React__default.cloneElement(icon, {
1166
1254
  className: "h-5 w-5 text-gray-900",
@@ -1170,9 +1258,10 @@ const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "
1170
1258
  React__default.createElement("button", { onClick: onClick, disabled: disabled, className: "\n bg-white/80 text-gray-900 font-semibold px-6 py-2 \n rounded-md text-sm flex items-center\n backdrop-blur-sm shadow-md\n hover:bg-white/90\n transition\n focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-gray-400\n disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-white/50\n " },
1171
1259
  renderedIcon && React__default.createElement("span", { className: "inline mr-2" }, renderedIcon),
1172
1260
  text)));
1173
- };
1261
+ });
1262
+ VideoActionButton.displayName = "VideoActionButton";
1174
1263
 
1175
- const Overlay = ({ config }) => {
1264
+ const Overlay = React__default.memo(({ config }) => {
1176
1265
  const controlsTimerRef = useRef(null);
1177
1266
  const containerRef = useRef(null);
1178
1267
  const { setControls, controls, showCountdown, countdownTime, setShowCountdown, setAutoPlayNext, setCurrentEpisodeIndex, episodeList, setCountdownTime, videoRef, currentEpisodeIndex, isAdPlaying, } = useVideoStore(useShallow((state) => ({
@@ -1219,7 +1308,9 @@ const Overlay = ({ config }) => {
1219
1308
  let timer;
1220
1309
  if (showCountdown && countdownTime > 0 && episodeList.length > 0) {
1221
1310
  timer = setInterval(() => {
1222
- setCountdownTime(countdownTime - 1);
1311
+ const currentTime = useVideoStore.getState().countdownTime;
1312
+ const next = currentTime - 1;
1313
+ setCountdownTime(next > 0 ? next : 0);
1223
1314
  }, 1000);
1224
1315
  }
1225
1316
  return () => {
@@ -1239,7 +1330,7 @@ const Overlay = ({ config }) => {
1239
1330
  window.removeEventListener(CONTROL_INTERACTION_EVENT, handleExternalInteraction);
1240
1331
  };
1241
1332
  }, [handleControlsInteraction]);
1242
- const handleNextEpisodeManually = () => {
1333
+ const handleNextEpisodeManually = useCallback(() => {
1243
1334
  const nextIndex = currentEpisodeIndex + 1;
1244
1335
  if (nextIndex < episodeList.length && videoRef && episodeList[nextIndex]) {
1245
1336
  setCurrentEpisodeIndex(nextIndex);
@@ -1253,63 +1344,168 @@ const Overlay = ({ config }) => {
1253
1344
  else if (onClose) {
1254
1345
  onClose();
1255
1346
  }
1256
- };
1347
+ }, [
1348
+ currentEpisodeIndex,
1349
+ episodeList,
1350
+ videoRef,
1351
+ setCurrentEpisodeIndex,
1352
+ setAutoPlayNext,
1353
+ setShowCountdown,
1354
+ setCountdownTime,
1355
+ handleControlsInteraction,
1356
+ onClose,
1357
+ ]);
1358
+ // Memoize countdown display to prevent unnecessary re-renders
1359
+ const shouldShowCountdown = useMemo(() => showCountdown &&
1360
+ episodeList.length > 0 &&
1361
+ currentEpisodeIndex + 1 < episodeList.length, [showCountdown, episodeList.length, currentEpisodeIndex]);
1257
1362
  return (React__default.createElement("div", { id: "videoPlayerControls", ref: containerRef, className: "absolute inset-0", onMouseMove: handleMouseEnter },
1258
1363
  controls && !isAdPlaying && React__default.createElement(VideoPlayerControls, { config: config }),
1259
- showCountdown &&
1260
- episodeList.length > 0 &&
1261
- currentEpisodeIndex + 1 < episodeList.length && (React__default.createElement(VideoActionButton, { text: "Next Episode", onClick: handleNextEpisodeManually, icon: React__default.createElement(ArrowRight, { className: "h-5 w-5 text-gray-900" }), disabled: currentEpisodeIndex + 1 >= episodeList.length, position: "right" }))));
1364
+ shouldShowCountdown && (React__default.createElement(VideoActionButton, { text: "Next Episode", onClick: handleNextEpisodeManually, icon: React__default.createElement(ArrowRight, { className: "h-5 w-5 text-gray-900" }), disabled: currentEpisodeIndex + 1 >= episodeList.length, position: "right" }))));
1365
+ });
1366
+ Overlay.displayName = "Overlay";
1367
+
1368
+ /** Returns true if value is a Tailwind background class (e.g. bg-red, bg-black, bg-transparent) ya CSS "transparent" */
1369
+ const isTailwindBackground = (value) => typeof value === "string" &&
1370
+ (value.trim().startsWith("bg-") || value.trim().toLowerCase() === "transparent");
1371
+ const useSubtitleStyling = (config) => {
1372
+ const { videoRef } = useVideoStore();
1373
+ useEffect(() => {
1374
+ if (!videoRef)
1375
+ return;
1376
+ const applySubtitleStyles = () => {
1377
+ const style = document.createElement("style");
1378
+ style.id = "custom-subtitle-styles";
1379
+ const existingStyle = document.getElementById("custom-subtitle-styles");
1380
+ if (existingStyle) {
1381
+ existingStyle.remove();
1382
+ }
1383
+ const bgRaw = config?.backgroundColor;
1384
+ const useTailwindBg = isTailwindBackground(bgRaw);
1385
+ const cueBackground = useTailwindBg
1386
+ ? "transparent"
1387
+ : bgRaw || "linear-gradient(135deg, #fbbf24, #f59e0b)";
1388
+ const styles = `
1389
+ .video-player video::cue {
1390
+ font-size: ${config?.fontSize || "1.75rem"} !important;
1391
+ background: ${cueBackground} !important;
1392
+ color: ${config?.textColor || "#000000"} !important;
1393
+ border-radius: ${config?.borderRadius || "12px"} !important;
1394
+ padding: ${config?.padding || "12px 20px"} !important;
1395
+ max-width: ${config?.maxWidth || "80%"} !important;
1396
+ ${config?.position === "top"
1397
+ ? "top: 10% !important; bottom: auto !important;"
1398
+ : ""}
1399
+ ${config?.position === "center"
1400
+ ? "top: 50% !important; bottom: auto !important; transform: translateX(-50%) translateY(-50%) !important;"
1401
+ : ""}
1402
+ ${config?.position === "bottom" || !config?.position
1403
+ ? "bottom: 15% !important; top: auto !important;"
1404
+ : ""}
1405
+ }
1406
+ `;
1407
+ style.textContent = styles;
1408
+ document.head.appendChild(style);
1409
+ };
1410
+ applySubtitleStyles();
1411
+ return () => {
1412
+ const existingStyle = document.getElementById("custom-subtitle-styles");
1413
+ if (existingStyle) {
1414
+ existingStyle.remove();
1415
+ }
1416
+ };
1417
+ }, [videoRef, config]);
1262
1418
  };
1263
1419
 
1420
+ const getPositionStyles = (position) => {
1421
+ const pos = position || "bottom";
1422
+ switch (pos) {
1423
+ case "top":
1424
+ return {
1425
+ top: "10%",
1426
+ left: "50%",
1427
+ transform: "translateX(-50%)",
1428
+ };
1429
+ case "center":
1430
+ return {
1431
+ top: "50%",
1432
+ left: "50%",
1433
+ transform: "translate(-50%, -50%)",
1434
+ };
1435
+ case "bottom":
1436
+ default:
1437
+ return {
1438
+ bottom: "15%",
1439
+ left: "50%",
1440
+ transform: "translateX(-50%)",
1441
+ };
1442
+ }
1443
+ };
1264
1444
  const SubtitleOverlay = ({ styleConfig }) => {
1265
- const { videoRef, activeSubtitle } = useVideoStore();
1445
+ const { videoRef, activeSubtitle } = useVideoStore(useShallow((state) => ({
1446
+ videoRef: state.videoRef,
1447
+ activeSubtitle: state.activeSubtitle,
1448
+ })));
1266
1449
  const [currentSubtitle, setCurrentSubtitle] = useState("");
1267
1450
  const [isVisible, setIsVisible] = useState(false);
1451
+ const rafRef = useRef(null);
1268
1452
  useEffect(() => {
1269
1453
  if (!videoRef)
1270
1454
  return;
1271
1455
  const handleTimeUpdate = () => {
1272
- if (!activeSubtitle) {
1273
- setCurrentSubtitle("");
1274
- setIsVisible(false);
1456
+ if (rafRef.current !== null)
1275
1457
  return;
1276
- }
1277
- const currentTime = videoRef.currentTime;
1278
- const textTracks = Array.from(videoRef.textTracks);
1279
- const activeTrack = textTracks.find((track) => track.mode === "showing" && track.label === activeSubtitle.label);
1280
- if (activeTrack && activeTrack.cues) {
1281
- const activeCues = Array.from(activeTrack.cues).filter((cue) => currentTime >= cue.startTime && currentTime <= cue.endTime);
1282
- if (activeCues.length > 0) {
1283
- const cue = activeCues[0];
1284
- let cueText = "";
1285
- try {
1286
- if ("text" in cue) {
1287
- cueText = cue.text;
1288
- }
1289
- else if (typeof cue.getCueAsHTML === "function") {
1290
- const htmlElement = cue.getCueAsHTML();
1291
- cueText =
1292
- htmlElement?.textContent || htmlElement?.innerText || "";
1458
+ rafRef.current = requestAnimationFrame(() => {
1459
+ rafRef.current = null;
1460
+ if (!activeSubtitle) {
1461
+ setCurrentSubtitle("");
1462
+ setIsVisible(false);
1463
+ return;
1464
+ }
1465
+ const currentTime = videoRef.currentTime;
1466
+ const textTracks = Array.from(videoRef.textTracks);
1467
+ const activeTrack = textTracks.find((track) => track.mode === "showing" && track.label === activeSubtitle.label);
1468
+ if (activeTrack?.cues) {
1469
+ const activeCues = Array.from(activeTrack.cues).filter((cue) => currentTime >= cue.startTime && currentTime <= cue.endTime);
1470
+ if (activeCues.length > 0) {
1471
+ const cue = activeCues[0];
1472
+ let cueText = "";
1473
+ try {
1474
+ if ("text" in cue) {
1475
+ cueText = cue.text;
1476
+ }
1477
+ else if (typeof cue.getCueAsHTML === "function") {
1478
+ const fragment = cue.getCueAsHTML();
1479
+ cueText = fragment?.textContent ?? "";
1480
+ }
1481
+ else {
1482
+ cueText = cue.toString() ?? "";
1483
+ }
1293
1484
  }
1294
- else {
1295
- cueText = cue.toString() || "";
1485
+ catch {
1486
+ cueText = "";
1296
1487
  }
1488
+ setCurrentSubtitle(cueText);
1489
+ setIsVisible(!!cueText);
1297
1490
  }
1298
- catch (_error) {
1299
- cueText = "";
1491
+ else {
1492
+ setCurrentSubtitle("");
1493
+ setIsVisible(false);
1300
1494
  }
1301
- setCurrentSubtitle(cueText);
1302
- setIsVisible(!!cueText);
1303
1495
  }
1304
1496
  else {
1305
1497
  setCurrentSubtitle("");
1306
1498
  setIsVisible(false);
1307
1499
  }
1308
- }
1500
+ });
1309
1501
  };
1310
1502
  videoRef.addEventListener("timeupdate", handleTimeUpdate);
1311
1503
  return () => {
1312
1504
  videoRef.removeEventListener("timeupdate", handleTimeUpdate);
1505
+ if (rafRef.current !== null) {
1506
+ cancelAnimationFrame(rafRef.current);
1507
+ rafRef.current = null;
1508
+ }
1313
1509
  };
1314
1510
  }, [videoRef, activeSubtitle]);
1315
1511
  useEffect(() => {
@@ -1318,55 +1514,43 @@ const SubtitleOverlay = ({ styleConfig }) => {
1318
1514
  setIsVisible(false);
1319
1515
  }
1320
1516
  }, [activeSubtitle]);
1517
+ const bgValue = styleConfig?.backgroundColor ?? "rgba(0, 0, 0, 0.4)";
1518
+ const isTailwindBg = isTailwindBackground(bgValue);
1519
+ const bgClassName = isTailwindBg
1520
+ ? (bgValue.trim().toLowerCase() === "transparent" ? "bg-transparent" : bgValue.trim())
1521
+ : undefined;
1522
+ const isTransparentBg = bgValue.trim().toLowerCase() === "transparent" ||
1523
+ bgClassName === "bg-transparent";
1524
+ const subtitleStyle = useMemo(() => {
1525
+ const base = {
1526
+ position: "absolute",
1527
+ ...getPositionStyles(styleConfig?.position),
1528
+ fontSize: styleConfig?.fontSize ?? "1.2rem",
1529
+ fontWeight: "500",
1530
+ lineHeight: "1.2",
1531
+ textAlign: "center",
1532
+ color: styleConfig?.textColor ?? "#fff",
1533
+ padding: styleConfig?.padding ?? "8px 16px",
1534
+ borderRadius: styleConfig?.borderRadius ?? "5px",
1535
+ maxWidth: styleConfig?.maxWidth ?? "60%",
1536
+ minWidth: "fit-content",
1537
+ boxShadow: isTransparentBg ? "none" : "0 6px 20px rgba(0, 0, 0, 0.4)",
1538
+ backdropFilter: isTransparentBg ? "none" : "blur(6px)",
1539
+ border: isTransparentBg ? "none" : "1px solid rgba(255, 255, 255, 0.2)",
1540
+ transition: "all 0.2s ease-in-out",
1541
+ opacity: isVisible ? 1 : 0,
1542
+ zIndex: 10,
1543
+ pointerEvents: "none",
1544
+ };
1545
+ if (!isTailwindBg)
1546
+ base.background = bgValue;
1547
+ return base;
1548
+ }, [styleConfig, isVisible, bgValue, isTailwindBg, isTransparentBg]);
1321
1549
  if (!isVisible || !currentSubtitle)
1322
1550
  return null;
1323
- const getPositionStyles = () => {
1324
- const position = styleConfig?.position || "bottom";
1325
- switch (position) {
1326
- case "top":
1327
- return {
1328
- top: "10%",
1329
- left: "50%",
1330
- transform: "translateX(-50%)",
1331
- };
1332
- case "center":
1333
- return {
1334
- top: "50%",
1335
- left: "50%",
1336
- transform: "translate(-50%, -50%)",
1337
- };
1338
- case "bottom":
1339
- default:
1340
- return {
1341
- bottom: "15%",
1342
- left: "50%",
1343
- transform: "translateX(-50%)",
1344
- };
1345
- }
1346
- };
1347
- const subtitleStyle = {
1348
- position: "absolute",
1349
- ...getPositionStyles(),
1350
- fontSize: styleConfig?.fontSize || "1.2rem",
1351
- fontWeight: "500",
1352
- lineHeight: "1.2",
1353
- textAlign: "center",
1354
- background: styleConfig?.backgroundColor || "rgba(0, 0, 0, 0.4)",
1355
- color: styleConfig?.textColor || "#fff",
1356
- padding: styleConfig?.padding || "8px 16px",
1357
- borderRadius: styleConfig?.borderRadius || "5px",
1358
- maxWidth: styleConfig?.maxWidth || "60%",
1359
- minWidth: "fit-content",
1360
- boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
1361
- backdropFilter: "blur(6px)",
1362
- border: "1px solid rgba(255, 255, 255, 0.2)",
1363
- transition: "all 0.2s ease-in-out",
1364
- opacity: isVisible ? 1 : 0,
1365
- zIndex: 10,
1366
- pointerEvents: "none",
1367
- };
1368
- return React__default.createElement("div", { style: subtitleStyle }, currentSubtitle);
1551
+ return (React__default.createElement("div", { className: bgClassName, style: subtitleStyle }, currentSubtitle));
1369
1552
  };
1553
+ SubtitleOverlay.displayName = "SubtitleOverlay";
1370
1554
 
1371
1555
  const HLS_CONFIG = {
1372
1556
  enableWorker: true,
@@ -1821,50 +2005,62 @@ const useVideoSource = (trackSrc, type) => {
1821
2005
 
1822
2006
  const useSubtitles = (subtitles) => {
1823
2007
  const { videoRef, activeSubtitle, setSubtitles } = useVideoStore();
2008
+ const timeoutIdsRef = useRef([]);
1824
2009
  useEffect(() => {
1825
- if (videoRef) {
1826
- const tracks = videoRef.getElementsByTagName("track");
1827
- while (tracks.length > 0) {
1828
- videoRef.removeChild(tracks[0]);
1829
- }
1830
- Array.from(videoRef.textTracks).forEach((track) => {
1831
- track.mode = "disabled";
1832
- });
1833
- if (activeSubtitle && subtitles) {
1834
- const index = subtitles.findIndex((s) => s.label === activeSubtitle.label);
1835
- if (index !== -1) {
1836
- const trackElement = document.createElement("track");
1837
- trackElement.kind = "subtitles";
1838
- trackElement.label = activeSubtitle.label;
1839
- trackElement.srclang = activeSubtitle.lang;
1840
- trackElement.src = activeSubtitle.url;
1841
- trackElement.default = false;
1842
- videoRef.appendChild(trackElement);
1843
- Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1844
- const handleTrackLoad = () => {
2010
+ // Clear any pending timeouts from previous effect runs
2011
+ timeoutIdsRef.current.forEach((id) => clearTimeout(id));
2012
+ timeoutIdsRef.current = [];
2013
+ if (!videoRef)
2014
+ return;
2015
+ const tracks = videoRef.getElementsByTagName("track");
2016
+ while (tracks.length > 0) {
2017
+ videoRef.removeChild(tracks[0]);
2018
+ }
2019
+ Array.from(videoRef.textTracks).forEach((track) => {
2020
+ track.mode = "disabled";
2021
+ });
2022
+ let trackElement = null;
2023
+ let handleTrackLoad = null;
2024
+ if (activeSubtitle && subtitles) {
2025
+ const index = subtitles.findIndex((s) => s.label === activeSubtitle.label);
2026
+ if (index !== -1) {
2027
+ trackElement = document.createElement("track");
2028
+ trackElement.kind = "subtitles";
2029
+ trackElement.label = activeSubtitle.label;
2030
+ trackElement.srclang = activeSubtitle.lang;
2031
+ trackElement.src = activeSubtitle.url;
2032
+ trackElement.default = false;
2033
+ videoRef.appendChild(trackElement);
2034
+ handleTrackLoad = () => {
2035
+ const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
2036
+ if (textTrack) {
2037
+ textTrack.mode = "showing";
2038
+ }
2039
+ };
2040
+ trackElement.addEventListener("load", handleTrackLoad);
2041
+ // Fallback attempts with proper cleanup tracking
2042
+ const attempts = [100, 500, 1000];
2043
+ attempts.forEach((delay) => {
2044
+ const timeoutId = setTimeout(() => {
1845
2045
  const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1846
- if (textTrack) {
2046
+ if (textTrack && textTrack.mode !== "showing") {
1847
2047
  textTrack.mode = "showing";
1848
2048
  }
1849
- };
1850
- trackElement.addEventListener("load", handleTrackLoad);
1851
- const attempts = [100, 500, 1000];
1852
- attempts.forEach((delay) => {
1853
- setTimeout(() => {
1854
- const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1855
- if (textTrack && textTrack.mode !== "showing") {
1856
- textTrack.mode = "showing";
1857
- }
1858
- }, delay);
1859
- });
1860
- }
1861
- }
1862
- else {
1863
- Array.from(videoRef.textTracks).forEach((track) => {
1864
- track.mode = "disabled";
2049
+ }, delay);
2050
+ timeoutIdsRef.current.push(timeoutId);
1865
2051
  });
1866
2052
  }
1867
2053
  }
2054
+ // Cleanup function
2055
+ return () => {
2056
+ // Clear all pending timeouts
2057
+ timeoutIdsRef.current.forEach((id) => clearTimeout(id));
2058
+ timeoutIdsRef.current = [];
2059
+ // Remove event listener if it was added
2060
+ if (trackElement && handleTrackLoad) {
2061
+ trackElement.removeEventListener("load", handleTrackLoad);
2062
+ }
2063
+ };
1868
2064
  }, [videoRef, activeSubtitle, subtitles]);
1869
2065
  useEffect(() => {
1870
2066
  if (subtitles) {
@@ -1873,75 +2069,32 @@ const useSubtitles = (subtitles) => {
1873
2069
  }, [subtitles, setSubtitles]);
1874
2070
  };
1875
2071
 
1876
- const useSubtitleStyling = (config) => {
1877
- const { videoRef } = useVideoStore();
2072
+ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
2073
+ const { videoRef, setShowCountdown } = useVideoStore();
2074
+ const isViewCounted = useRef(false);
2075
+ const lastVideoSrcRef = useRef(null);
2076
+ // Reset view count when video source changes
1878
2077
  useEffect(() => {
1879
2078
  if (!videoRef)
1880
2079
  return;
1881
- const applySubtitleStyles = () => {
1882
- const style = document.createElement("style");
1883
- style.id = "custom-subtitle-styles";
1884
- const existingStyle = document.getElementById("custom-subtitle-styles");
1885
- if (existingStyle) {
1886
- existingStyle.remove();
1887
- }
1888
- const styles = `
1889
- .video-player video::cue {
1890
- font-size: ${config?.fontSize || "1.75rem"} !important;
1891
- background: ${config?.backgroundColor ||
1892
- "linear-gradient(135deg, #fbbf24, #f59e0b)"} !important;
1893
- color: ${config?.textColor || "#000000"} !important;
1894
- border-radius: ${config?.borderRadius || "12px"} !important;
1895
- padding: ${config?.padding || "12px 20px"} !important;
1896
- max-width: ${config?.maxWidth || "80%"} !important;
1897
- ${config?.position === "top"
1898
- ? "top: 10% !important; bottom: auto !important;"
1899
- : ""}
1900
- ${config?.position === "center"
1901
- ? "top: 50% !important; bottom: auto !important; transform: translateX(-50%) translateY(-50%) !important;"
1902
- : ""}
1903
- ${config?.position === "bottom" || !config?.position
1904
- ? "bottom: 15% !important; top: auto !important;"
1905
- : ""}
2080
+ const currentSrc = videoRef.src || videoRef.currentSrc;
2081
+ // If video source changed, reset the view count
2082
+ if (lastVideoSrcRef.current !== currentSrc) {
2083
+ isViewCounted.current = false;
2084
+ lastVideoSrcRef.current = currentSrc;
1906
2085
  }
1907
- `;
1908
- style.textContent = styles;
1909
- document.head.appendChild(style);
1910
- };
1911
- applySubtitleStyles();
1912
- return () => {
1913
- const existingStyle = document.getElementById("custom-subtitle-styles");
1914
- if (existingStyle) {
1915
- existingStyle.remove();
1916
- }
1917
- };
1918
- }, [videoRef, config]);
1919
- };
1920
-
1921
- const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
1922
- const { videoRef, setIsPlaying, setShowCountdown } = useVideoStore();
1923
- const startTime = useRef(null);
1924
- const isViewCounted = useRef(false);
2086
+ }, [videoRef?.src, videoRef?.currentSrc, videoRef]);
1925
2087
  useEffect(() => {
1926
2088
  if (!videoRef)
1927
2089
  return;
2090
+ // Only handle view tracking on play - setIsPlaying is handled by useVideoEvents
1928
2091
  const onPlay = () => {
1929
2092
  if (!isViewCounted.current) {
1930
2093
  isViewCounted.current = true;
1931
2094
  tracking?.onViewed?.();
1932
2095
  }
1933
- startTime.current = Date.now();
1934
- setIsPlaying(true);
1935
- };
1936
- const onPause = () => {
1937
- if (startTime.current) {
1938
- const elapsedTime = (Date.now() - startTime.current) / 1000;
1939
- const getCurrentTime = localStorage.getItem("current_time");
1940
- localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
1941
- startTime.current = null;
1942
- }
1943
- setIsPlaying(false);
1944
2096
  };
2097
+ // Handle episode end logic - playback state is handled by useVideoEvents
1945
2098
  const onEnded = () => {
1946
2099
  if (episodeList &&
1947
2100
  episodeList.length > 0 &&
@@ -1959,11 +2112,9 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
1959
2112
  }
1960
2113
  };
1961
2114
  videoRef.addEventListener("play", onPlay);
1962
- videoRef.addEventListener("pause", onPause);
1963
2115
  videoRef.addEventListener("ended", onEnded);
1964
2116
  return () => {
1965
2117
  videoRef.removeEventListener("play", onPlay);
1966
- videoRef.removeEventListener("pause", onPause);
1967
2118
  videoRef.removeEventListener("ended", onEnded);
1968
2119
  };
1969
2120
  }, [
@@ -1972,29 +2123,8 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
1972
2123
  currentEpisodeIndex,
1973
2124
  onClose,
1974
2125
  tracking,
1975
- setIsPlaying,
1976
2126
  setShowCountdown,
1977
2127
  ]);
1978
- useEffect(() => {
1979
- const handleUnload = () => {
1980
- if (startTime.current) {
1981
- const elapsedTime = (Date.now() - startTime.current) / 1000;
1982
- const getCurrentTime = localStorage.getItem("current_time");
1983
- localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
1984
- }
1985
- const totalTimeWatched = Number(localStorage.getItem("current_time") || 0);
1986
- if (totalTimeWatched >= 30) {
1987
- tracking?.onWatchTimeUpdated?.({
1988
- watchTime: totalTimeWatched,
1989
- });
1990
- }
1991
- localStorage.setItem("current_time", "0");
1992
- };
1993
- window.addEventListener("unload", handleUnload);
1994
- return () => {
1995
- window.removeEventListener("unload", handleUnload);
1996
- };
1997
- }, [tracking]);
1998
2128
  };
1999
2129
 
2000
2130
  const useIntroSkip = (intro) => {
@@ -2159,7 +2289,6 @@ const useVideoEvents = () => {
2159
2289
  const onLoadedMetadata = (e) => {
2160
2290
  const duration = e?.currentTarget?.duration;
2161
2291
  if (typeof duration === "number" && !Number.isNaN(duration)) {
2162
- localStorage.setItem("current_time", "0");
2163
2292
  setDuration(duration);
2164
2293
  }
2165
2294
  };
@@ -2267,8 +2396,13 @@ const useAdManager = (adConfig) => {
2267
2396
  })));
2268
2397
  const preRollPlayedRef = useRef(false);
2269
2398
  const postRollPlayedRef = useRef(false);
2270
- const midRollCheckIntervalRef = useRef(null);
2271
2399
  const resumeAfterAdRef = useRef(false);
2400
+ // Track maximum time reached to prevent ad replay when seeking backward
2401
+ const maxTimeReachedRef = useRef(0);
2402
+ // Throttle ad checking to prevent performance issues
2403
+ const adCheckThrottleRef = useRef(null);
2404
+ // Track if we're currently processing an ad to prevent race conditions
2405
+ const isProcessingAdRef = useRef(false);
2272
2406
  const stopMediaElement = useCallback((media) => {
2273
2407
  if (!media)
2274
2408
  return;
@@ -2288,36 +2422,29 @@ const useAdManager = (adConfig) => {
2288
2422
  setMidRollQueue([]);
2289
2423
  return;
2290
2424
  }
2291
- let sortedMidRolls = [...adConfig.midRoll].sort((a, b) => a.time - b.time);
2292
- if (adConfig.smartPlacement?.enabled) {
2293
- sortedMidRolls = applySmartPlacement(sortedMidRolls, duration, adConfig.smartPlacement);
2294
- }
2295
- setMidRollQueue(sortedMidRolls);
2296
- }, [adConfig?.midRoll, adConfig?.smartPlacement, duration, setMidRollQueue]);
2297
- const applySmartPlacement = (ads, videoDuration, options) => {
2298
- const minVideoDuration = options.minVideoDuration || 60;
2299
- const minGap = options.minGapBetweenAds || 30;
2300
- const avoidNearEnd = options.avoidNearEnd || 10;
2301
- if (videoDuration < minVideoDuration) {
2302
- return [];
2303
- }
2304
- const validAds = [];
2305
- let lastAdTime = 0;
2306
- for (const ad of ads) {
2307
- if (ad.time < minGap)
2308
- continue;
2309
- if (ad.time > videoDuration - avoidNearEnd)
2310
- continue;
2311
- if (ad.time - lastAdTime < minGap)
2312
- continue;
2313
- validAds.push(ad);
2314
- const adLength = Number.isFinite(ad.duration)
2315
- ? ad.duration
2316
- : 0;
2317
- lastAdTime = ad.time + adLength;
2425
+ // Filter out invalid ads and ensure all required fields are present
2426
+ const validAds = adConfig.midRoll.filter((ad) => ad &&
2427
+ typeof ad.time === "number" &&
2428
+ ad.time >= 0 &&
2429
+ ad.time < Number.MAX_SAFE_INTEGER &&
2430
+ typeof ad.id === "string" &&
2431
+ ad.id.trim() !== "" &&
2432
+ typeof ad.adUrl === "string" &&
2433
+ ad.adUrl.trim() !== "" &&
2434
+ typeof ad.type === "string" &&
2435
+ ad.type === "mid-roll");
2436
+ if (validAds.length === 0) {
2437
+ setMidRollQueue([]);
2438
+ return;
2318
2439
  }
2319
- return validAds;
2320
- };
2440
+ // Sort ads by time to ensure they play in order
2441
+ const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
2442
+ // Remove duplicate IDs (keep first occurrence)
2443
+ const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
2444
+ setMidRollQueue(uniqueAds);
2445
+ }, [adConfig?.midRoll, setMidRollQueue]);
2446
+ // Removed smartPlacement - users should configure exact ad times
2447
+ // This ensures ads appear exactly when specified
2321
2448
  const playPreRollAd = async () => {
2322
2449
  if (!adConfig?.preRoll || preRollPlayedRef.current || !videoRef)
2323
2450
  return;
@@ -2333,6 +2460,46 @@ const useAdManager = (adConfig) => {
2333
2460
  setAdType("pre-roll");
2334
2461
  adConfig.onAdStart?.(adBreak);
2335
2462
  };
2463
+ const playMidRollAd = useCallback(async (adBreak) => {
2464
+ if (!videoRef || isAdPlaying || isProcessingAdRef.current)
2465
+ return;
2466
+ // Prevent duplicate ad playback
2467
+ if (playedAdBreaks.includes(adBreak.id))
2468
+ return;
2469
+ // Mark as processing to prevent race conditions
2470
+ isProcessingAdRef.current = true;
2471
+ const updatedQueue = midRollQueue.filter((ad) => ad.id !== adBreak.id);
2472
+ setMidRollQueue(updatedQueue);
2473
+ addPlayedAdBreak(adBreak.id);
2474
+ const wasPlaying = !videoRef.paused;
2475
+ videoRef.pause();
2476
+ setPlaying(false);
2477
+ setIsPlaying(false);
2478
+ resumeAfterAdRef.current = wasPlaying;
2479
+ stopMediaElement(useVideoStore.getState().adVideoRef);
2480
+ setIsAdPlaying(true);
2481
+ setCurrentAd(adBreak);
2482
+ setAdType("mid-roll");
2483
+ adConfig?.onAdStart?.(adBreak);
2484
+ // Reset processing flag after a short delay
2485
+ setTimeout(() => {
2486
+ isProcessingAdRef.current = false;
2487
+ }, 100);
2488
+ }, [
2489
+ videoRef,
2490
+ isAdPlaying,
2491
+ playedAdBreaks,
2492
+ midRollQueue,
2493
+ setMidRollQueue,
2494
+ addPlayedAdBreak,
2495
+ setPlaying,
2496
+ setIsPlaying,
2497
+ setCurrentAd,
2498
+ setAdType,
2499
+ setIsAdPlaying,
2500
+ adConfig,
2501
+ stopMediaElement,
2502
+ ]);
2336
2503
  const playPostRollAd = async () => {
2337
2504
  if (!adConfig?.postRoll || postRollPlayedRef.current || !videoRef)
2338
2505
  return;
@@ -2350,23 +2517,40 @@ const useAdManager = (adConfig) => {
2350
2517
  const adTypeState = useVideoStore.getState().adType;
2351
2518
  const videoRefState = useVideoStore.getState().videoRef;
2352
2519
  const adVideoRefState = useVideoStore.getState().adVideoRef;
2353
- if (!videoRefState || !currentAdState)
2520
+ if (!currentAdState)
2354
2521
  return;
2522
+ // Reset processing flag
2523
+ isProcessingAdRef.current = false;
2524
+ // Clean up ad video element
2355
2525
  if (adVideoRefState) {
2356
2526
  stopMediaElement(adVideoRefState);
2357
2527
  setAdVideoRef(null);
2358
2528
  }
2529
+ // Reset ad state
2359
2530
  setIsAdPlaying(false);
2360
2531
  setCurrentAd(null);
2361
2532
  setAdType(null);
2362
2533
  setAdCurrentTime(0);
2363
2534
  setCanSkipAd(false);
2364
2535
  setSkipCountdown(0);
2536
+ // Call end callback
2365
2537
  adConfig?.onAdEnd?.(currentAdState);
2366
- if (resumeAfterAdRef.current && adTypeState !== "post-roll") {
2367
- videoRefState.play().catch(() => undefined);
2368
- setPlaying(true);
2369
- setIsPlaying(true);
2538
+ // Resume main video if needed
2539
+ if (resumeAfterAdRef.current &&
2540
+ adTypeState !== "post-roll" &&
2541
+ videoRefState) {
2542
+ // Small delay to ensure ad cleanup is complete
2543
+ setTimeout(() => {
2544
+ if (videoRefState && !videoRefState.paused)
2545
+ return;
2546
+ videoRefState.play().catch(() => {
2547
+ // If autoplay fails, user will need to click play
2548
+ setPlaying(false);
2549
+ setIsPlaying(false);
2550
+ });
2551
+ setPlaying(true);
2552
+ setIsPlaying(true);
2553
+ }, 100);
2370
2554
  }
2371
2555
  resumeAfterAdRef.current = false;
2372
2556
  }, [
@@ -2443,46 +2627,87 @@ const useAdManager = (adConfig) => {
2443
2627
  videoRef.removeEventListener("canplay", handleCanPlay);
2444
2628
  };
2445
2629
  }, [videoRef, adConfig?.preRoll]);
2630
+ // Precise mid-roll ad checking with accurate timing
2446
2631
  useEffect(() => {
2447
2632
  if (!videoRef || !adConfig?.midRoll || isAdPlaying) {
2448
- if (midRollCheckIntervalRef.current) {
2449
- clearInterval(midRollCheckIntervalRef.current);
2450
- midRollCheckIntervalRef.current = null;
2633
+ // Clear any pending throttle
2634
+ if (adCheckThrottleRef.current !== null) {
2635
+ cancelAnimationFrame(adCheckThrottleRef.current);
2636
+ adCheckThrottleRef.current = null;
2451
2637
  }
2452
2638
  return;
2453
2639
  }
2454
- midRollCheckIntervalRef.current = setInterval(() => {
2640
+ // Precise ad check function
2641
+ const checkMidRollAds = () => {
2642
+ // Clear throttle ref
2643
+ adCheckThrottleRef.current = null;
2455
2644
  const state = useVideoStore.getState();
2456
- if (state.isAdPlaying ||
2457
- !state.midRollQueue ||
2458
- state.midRollQueue.length === 0) {
2645
+ // Skip if ad is already playing or being processed
2646
+ if (state.isAdPlaying || isProcessingAdRef.current) {
2647
+ return;
2648
+ }
2649
+ // Check if we have mid-roll ads in queue
2650
+ if (!state.midRollQueue || state.midRollQueue.length === 0) {
2459
2651
  return;
2460
2652
  }
2461
- const nextAd = state.midRollQueue[0];
2462
- if (nextAd &&
2463
- state.currentTime >= nextAd.time &&
2464
- !state.playedAdBreaks.includes(nextAd.id)) {
2465
- const updatedQueue = state.midRollQueue.filter((ad) => ad.id !== nextAd.id);
2466
- state.setMidRollQueue(updatedQueue);
2467
- state.addPlayedAdBreak(nextAd.id);
2468
- const wasPlaying = !state.videoRef?.paused;
2469
- state.videoRef?.pause();
2470
- state.setPlaying(false);
2471
- state.setIsPlaying(false);
2472
- resumeAfterAdRef.current = wasPlaying;
2473
- state.setIsAdPlaying(true);
2474
- state.setCurrentAd(nextAd);
2475
- state.setAdType("mid-roll");
2476
- adConfig?.onAdStart?.(nextAd);
2477
- }
2478
- }, 1000);
2653
+ // Get current time directly from video element for maximum accuracy
2654
+ const currentVideoTime = videoRef.currentTime || 0;
2655
+ // Update max time reached (only forward, not backward)
2656
+ if (currentVideoTime > maxTimeReachedRef.current) {
2657
+ maxTimeReachedRef.current = currentVideoTime;
2658
+ }
2659
+ // Find the next ad in queue that should play
2660
+ // Check ads in order, but skip already-played ones
2661
+ for (const ad of state.midRollQueue) {
2662
+ // Skip if already played
2663
+ if (state.playedAdBreaks.includes(ad.id)) {
2664
+ continue;
2665
+ }
2666
+ // Precise timing check: ad should trigger when we reach or pass its time
2667
+ // Use 1 second tolerance to catch ads even if timeupdate fires slightly late
2668
+ const timeDifference = currentVideoTime - ad.time;
2669
+ const shouldTrigger = timeDifference >= 0 && timeDifference <= 1.0;
2670
+ // Also check if we've reached the max time (prevents replay on backward seek)
2671
+ // This ensures ads only play if we've actually watched past them
2672
+ const hasReachedMaxTime = maxTimeReachedRef.current >= ad.time;
2673
+ if (shouldTrigger && hasReachedMaxTime) {
2674
+ // Play the ad and break (only one ad at a time)
2675
+ playMidRollAd(ad);
2676
+ break;
2677
+ }
2678
+ }
2679
+ };
2680
+ // Throttle function using requestAnimationFrame for smooth performance
2681
+ const throttledCheck = () => {
2682
+ if (adCheckThrottleRef.current === null) {
2683
+ adCheckThrottleRef.current = requestAnimationFrame(checkMidRollAds);
2684
+ }
2685
+ };
2686
+ // Listen to timeupdate event (throttled for performance)
2687
+ videoRef.addEventListener("timeupdate", throttledCheck);
2688
+ // Also check immediately on seek to catch rapid seeks past ad times
2689
+ const handleSeeking = () => {
2690
+ // Force immediate check when seeking
2691
+ if (adCheckThrottleRef.current !== null) {
2692
+ cancelAnimationFrame(adCheckThrottleRef.current);
2693
+ adCheckThrottleRef.current = null;
2694
+ }
2695
+ // Check immediately
2696
+ checkMidRollAds();
2697
+ };
2698
+ videoRef.addEventListener("seeking", handleSeeking);
2699
+ videoRef.addEventListener("seeked", handleSeeking);
2479
2700
  return () => {
2480
- if (midRollCheckIntervalRef.current) {
2481
- clearInterval(midRollCheckIntervalRef.current);
2482
- midRollCheckIntervalRef.current = null;
2701
+ videoRef.removeEventListener("timeupdate", throttledCheck);
2702
+ videoRef.removeEventListener("seeking", handleSeeking);
2703
+ videoRef.removeEventListener("seeked", handleSeeking);
2704
+ // Clear any pending animation frame
2705
+ if (adCheckThrottleRef.current !== null) {
2706
+ cancelAnimationFrame(adCheckThrottleRef.current);
2707
+ adCheckThrottleRef.current = null;
2483
2708
  }
2484
2709
  };
2485
- }, [videoRef, isAdPlaying, adConfig]);
2710
+ }, [videoRef, isAdPlaying, adConfig, playMidRollAd]);
2486
2711
  useEffect(() => {
2487
2712
  if (!videoRef || !adConfig?.postRoll || postRollPlayedRef.current)
2488
2713
  return;
@@ -2499,19 +2724,46 @@ const useAdManager = (adConfig) => {
2499
2724
  useEffect(() => {
2500
2725
  if (!videoRef?.src)
2501
2726
  return;
2727
+ // Reset ad state when video source changes
2502
2728
  preRollPlayedRef.current = false;
2503
2729
  postRollPlayedRef.current = false;
2504
2730
  resumeAfterAdRef.current = false;
2731
+ maxTimeReachedRef.current = 0;
2732
+ isProcessingAdRef.current = false;
2733
+ // Clear any pending throttle
2734
+ if (adCheckThrottleRef.current !== null) {
2735
+ cancelAnimationFrame(adCheckThrottleRef.current);
2736
+ adCheckThrottleRef.current = null;
2737
+ }
2505
2738
  setIsAdPlaying(false);
2506
2739
  setCurrentAd(null);
2507
2740
  setAdType(null);
2741
+ // Re-initialize mid-roll queue with strict validation
2508
2742
  if (adConfig?.midRoll && adConfig.midRoll.length > 0) {
2509
- const sortedMidRolls = [...adConfig.midRoll].sort((a, b) => a.time - b.time);
2510
- setMidRollQueue(sortedMidRolls);
2743
+ // Filter and validate ads
2744
+ const validAds = adConfig.midRoll.filter((ad) => ad &&
2745
+ typeof ad.time === "number" &&
2746
+ ad.time >= 0 &&
2747
+ typeof ad.id === "string" &&
2748
+ ad.id.trim() !== "" &&
2749
+ typeof ad.adUrl === "string" &&
2750
+ ad.adUrl.trim() !== "" &&
2751
+ typeof ad.type === "string" &&
2752
+ ad.type === "mid-roll");
2753
+ if (validAds.length > 0) {
2754
+ // Sort by time and remove duplicates
2755
+ const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
2756
+ const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
2757
+ setMidRollQueue(uniqueAds);
2758
+ }
2759
+ else {
2760
+ setMidRollQueue([]);
2761
+ }
2511
2762
  }
2512
2763
  else {
2513
2764
  setMidRollQueue([]);
2514
2765
  }
2766
+ // Clean up any lingering ad video
2515
2767
  const lingeringAdRef = useVideoStore.getState().adVideoRef;
2516
2768
  if (lingeringAdRef) {
2517
2769
  stopMediaElement(lingeringAdRef);
@@ -2721,13 +2973,93 @@ const usePrimaryVideoLifecycle = ({ hasPreRoll, trackSrc, }) => {
2721
2973
  };
2722
2974
  };
2723
2975
 
2724
- const AdOverlay = ({ adBreak, onSkip, config }) => {
2725
- const { adVideoRef, setAdVideoRef, adCurrentTime, setAdCurrentTime, canSkipAd, setCanSkipAd, skipCountdown, setSkipCountdown, videoRef, muted, setIsPlaying, } = useVideoStore();
2976
+ const getErrorType = (code) => {
2977
+ // MediaError codes: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
2978
+ switch (code) {
2979
+ case 1: // MEDIA_ERR_ABORTED
2980
+ return "unknown";
2981
+ case 2: // MEDIA_ERR_NETWORK
2982
+ return "network";
2983
+ case 3: // MEDIA_ERR_DECODE
2984
+ return "decode";
2985
+ case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
2986
+ return "src";
2987
+ default:
2988
+ return "unknown";
2989
+ }
2990
+ };
2991
+ const getErrorMessage = (code) => {
2992
+ switch (code) {
2993
+ case 1:
2994
+ return "Video playback was aborted.";
2995
+ case 2:
2996
+ return "A network error occurred while loading the video.";
2997
+ case 3:
2998
+ return "An error occurred while decoding the video.";
2999
+ case 4:
3000
+ return "The video format is not supported.";
3001
+ default:
3002
+ return "An unknown error occurred.";
3003
+ }
3004
+ };
3005
+ const useVideoError = () => {
3006
+ const { setError, clearError, error } = useVideoStore();
3007
+ const handleVideoError = useCallback((e) => {
3008
+ const video = e.currentTarget;
3009
+ const mediaError = video.error;
3010
+ if (mediaError) {
3011
+ const errorData = {
3012
+ code: mediaError.code,
3013
+ message: mediaError.message || getErrorMessage(mediaError.code),
3014
+ type: getErrorType(mediaError.code),
3015
+ };
3016
+ setError(errorData);
3017
+ }
3018
+ else {
3019
+ setError({
3020
+ code: 0,
3021
+ message: "An unknown error occurred.",
3022
+ type: "unknown",
3023
+ });
3024
+ }
3025
+ }, [setError]);
3026
+ const retry = useCallback(() => {
3027
+ clearError();
3028
+ const { videoRef } = useVideoStore.getState();
3029
+ if (videoRef) {
3030
+ videoRef.load();
3031
+ videoRef.play().catch(() => undefined);
3032
+ }
3033
+ }, [clearError]);
3034
+ return {
3035
+ error,
3036
+ handleVideoError,
3037
+ clearError,
3038
+ retry,
3039
+ };
3040
+ };
3041
+
3042
+ const AdOverlay = React__default.memo(({ adBreak, onSkip, config }) => {
3043
+ const { adVideoRef, setAdVideoRef, adCurrentTime, setAdCurrentTime, canSkipAd, setCanSkipAd, skipCountdown, setSkipCountdown, videoRef, muted, setIsPlaying, } = useVideoStore(useShallow((state) => ({
3044
+ adVideoRef: state.adVideoRef,
3045
+ setAdVideoRef: state.setAdVideoRef,
3046
+ adCurrentTime: state.adCurrentTime,
3047
+ setAdCurrentTime: state.setAdCurrentTime,
3048
+ canSkipAd: state.canSkipAd,
3049
+ setCanSkipAd: state.setCanSkipAd,
3050
+ skipCountdown: state.skipCountdown,
3051
+ setSkipCountdown: state.setSkipCountdown,
3052
+ videoRef: state.videoRef,
3053
+ muted: state.muted,
3054
+ setIsPlaying: state.setIsPlaying,
3055
+ })));
2726
3056
  const [showControls, setShowControls] = useState(true);
2727
3057
  const [isHovered, setIsHovered] = useState(false);
2728
3058
  const [adDuration, setAdDuration] = useState(0);
2729
3059
  const [requiresInteraction, setRequiresInteraction] = useState(false);
3060
+ const [adLoadError, setAdLoadError] = useState(false);
2730
3061
  const controlsTimeoutRef = useRef(null);
3062
+ const loadTimeoutRef = useRef(null);
2731
3063
  const safelySetCanSkipAd = useCallback((value) => {
2732
3064
  if (useVideoStore.getState().canSkipAd !== value) {
2733
3065
  setCanSkipAd(value);
@@ -2768,7 +3100,23 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2768
3100
  useEffect(() => {
2769
3101
  setAdDuration(0);
2770
3102
  setRequiresInteraction(false);
2771
- }, [adBreak.id]);
3103
+ setAdLoadError(false);
3104
+ setAdCurrentTime(0);
3105
+ if (loadTimeoutRef.current) {
3106
+ clearTimeout(loadTimeoutRef.current);
3107
+ loadTimeoutRef.current = null;
3108
+ }
3109
+ if (adBreak.skipable !== undefined) {
3110
+ setCanSkipAd(false);
3111
+ setSkipCountdown(0);
3112
+ }
3113
+ }, [
3114
+ adBreak.id,
3115
+ adBreak.skipable,
3116
+ setAdCurrentTime,
3117
+ setCanSkipAd,
3118
+ setSkipCountdown,
3119
+ ]);
2772
3120
  useEffect(() => {
2773
3121
  if (!adBreak.skipable) {
2774
3122
  safelySetCanSkipAd(false);
@@ -2792,43 +3140,76 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2792
3140
  if (!adVideoRef)
2793
3141
  return;
2794
3142
  setRequiresInteraction(false);
3143
+ setAdLoadError(false);
3144
+ if (!adVideoRef.src && adBreak.adUrl) {
3145
+ adVideoRef.src = adBreak.adUrl;
3146
+ adVideoRef.load();
3147
+ return;
3148
+ }
2795
3149
  const playPromise = adVideoRef.play();
2796
3150
  if (playPromise && "catch" in playPromise) {
2797
- playPromise.catch(() => {
3151
+ playPromise.catch((error) => {
3152
+ console.warn("Ad play failed:", error);
2798
3153
  setRequiresInteraction(true);
2799
3154
  setIsPlaying(false);
2800
3155
  });
2801
3156
  }
2802
- }, [adVideoRef, setIsPlaying]);
3157
+ }, [adVideoRef, adBreak.adUrl, setIsPlaying]);
3158
+ const timeUpdateRafRef = useRef(null);
3159
+ const lastUpdateTimeRef = useRef(0);
2803
3160
  useEffect(() => {
2804
3161
  if (!adVideoRef)
2805
3162
  return;
2806
3163
  const handleTimeUpdate = () => {
2807
- const currentTime = adVideoRef.currentTime;
2808
- setAdCurrentTime(currentTime);
2809
- if (adBreak.skipable) {
2810
- const remaining = skipAfter - currentTime;
2811
- if (remaining <= 0) {
2812
- safelySetCanSkipAd(true);
2813
- safelySetSkipCountdown(0);
3164
+ if (timeUpdateRafRef.current !== null)
3165
+ return;
3166
+ timeUpdateRafRef.current = requestAnimationFrame(() => {
3167
+ timeUpdateRafRef.current = null;
3168
+ const currentTime = adVideoRef.currentTime;
3169
+ if (Math.abs(currentTime - lastUpdateTimeRef.current) < 0.1) {
3170
+ return;
2814
3171
  }
2815
- else {
2816
- const remainingForDisplay = Math.max(Math.ceil(remaining), 0);
2817
- safelySetSkipCountdown(remainingForDisplay);
2818
- if (canSkipAd) {
2819
- safelySetCanSkipAd(false);
3172
+ lastUpdateTimeRef.current = currentTime;
3173
+ setAdCurrentTime(currentTime);
3174
+ if (adBreak.skipable) {
3175
+ const remaining = skipAfter - currentTime;
3176
+ if (remaining <= 0) {
3177
+ safelySetCanSkipAd(true);
3178
+ safelySetSkipCountdown(0);
3179
+ }
3180
+ else {
3181
+ const remainingForDisplay = Math.max(Math.ceil(remaining), 0);
3182
+ safelySetSkipCountdown(remainingForDisplay);
3183
+ if (canSkipAd) {
3184
+ safelySetCanSkipAd(false);
3185
+ }
2820
3186
  }
2821
3187
  }
2822
- }
3188
+ });
2823
3189
  };
2824
3190
  const handleLoadedMetadata = () => {
3191
+ if (loadTimeoutRef.current) {
3192
+ clearTimeout(loadTimeoutRef.current);
3193
+ loadTimeoutRef.current = null;
3194
+ }
2825
3195
  const duration = Number.isFinite(adVideoRef.duration)
2826
3196
  ? adVideoRef.duration
2827
3197
  : 0;
2828
3198
  setAdDuration(duration);
3199
+ setAdLoadError(false);
2829
3200
  setIsPlaying(!adVideoRef.paused);
2830
3201
  attemptAdPlayback();
2831
3202
  };
3203
+ if (loadTimeoutRef.current) {
3204
+ clearTimeout(loadTimeoutRef.current);
3205
+ }
3206
+ loadTimeoutRef.current = setTimeout(() => {
3207
+ if (adVideoRef && adVideoRef.readyState < 2) {
3208
+ console.warn("Ad load timeout:", adBreak.id);
3209
+ setAdLoadError(true);
3210
+ setRequiresInteraction(true);
3211
+ }
3212
+ }, 30000);
2832
3213
  const handlePlay = () => {
2833
3214
  setIsPlaying(true);
2834
3215
  setRequiresInteraction(false);
@@ -2843,7 +3224,21 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2843
3224
  setIsPlaying(true);
2844
3225
  setRequiresInteraction(false);
2845
3226
  };
2846
- const handleError = () => {
3227
+ const handleError = (e) => {
3228
+ if (loadTimeoutRef.current) {
3229
+ clearTimeout(loadTimeoutRef.current);
3230
+ loadTimeoutRef.current = null;
3231
+ }
3232
+ const error = e.target;
3233
+ const errorCode = error.error?.code;
3234
+ const errorMessage = error.error?.message || "Unknown ad error";
3235
+ console.error("Ad playback error:", {
3236
+ adId: adBreak.id,
3237
+ errorCode,
3238
+ errorMessage,
3239
+ src: adVideoRef.src,
3240
+ });
3241
+ setAdLoadError(true);
2847
3242
  setRequiresInteraction(true);
2848
3243
  setIsPlaying(false);
2849
3244
  };
@@ -2862,10 +3257,20 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2862
3257
  adVideoRef.removeEventListener("waiting", handleWaiting);
2863
3258
  adVideoRef.removeEventListener("playing", handlePlaying);
2864
3259
  adVideoRef.removeEventListener("error", handleError);
3260
+ if (loadTimeoutRef.current) {
3261
+ clearTimeout(loadTimeoutRef.current);
3262
+ loadTimeoutRef.current = null;
3263
+ }
3264
+ if (timeUpdateRafRef.current !== null) {
3265
+ cancelAnimationFrame(timeUpdateRafRef.current);
3266
+ timeUpdateRafRef.current = null;
3267
+ }
3268
+ lastUpdateTimeRef.current = 0;
2865
3269
  };
2866
3270
  }, [
2867
3271
  adVideoRef,
2868
3272
  adBreak.skipable,
3273
+ adBreak.id,
2869
3274
  skipAfter,
2870
3275
  canSkipAd,
2871
3276
  setAdCurrentTime,
@@ -2877,41 +3282,78 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2877
3282
  useEffect(() => {
2878
3283
  if (!adVideoRef || !videoRef)
2879
3284
  return;
2880
- if (adVideoRef.readyState === 0) {
2881
- adVideoRef.volume = videoRef.volume;
2882
- adVideoRef.muted = muted;
2883
- adVideoRef.load();
3285
+ // Sync volume and muted state
3286
+ adVideoRef.volume = videoRef.volume;
3287
+ adVideoRef.muted = muted;
3288
+ // Check if src needs to be updated
3289
+ const currentSrc = adVideoRef.src || adVideoRef.currentSrc || "";
3290
+ const needsReload = !currentSrc || currentSrc !== adBreak.adUrl;
3291
+ // Load ad if needed
3292
+ if (needsReload && adBreak.adUrl) {
3293
+ // Clear previous src
3294
+ try {
3295
+ adVideoRef.pause();
3296
+ adVideoRef.removeAttribute("src");
3297
+ adVideoRef.src = "";
3298
+ // Set new src
3299
+ adVideoRef.src = adBreak.adUrl;
3300
+ adVideoRef.load();
3301
+ }
3302
+ catch (error) {
3303
+ console.warn("Error loading ad:", error);
3304
+ setAdLoadError(true);
3305
+ }
2884
3306
  }
2885
3307
  const handleCanPlay = () => {
2886
- if (adVideoRef && !adVideoRef.paused)
3308
+ if (!adVideoRef || adVideoRef.paused === false)
2887
3309
  return;
2888
3310
  attemptAdPlayback();
2889
3311
  };
3312
+ const handleLoadedData = () => {
3313
+ // Ensure volume is synced after load
3314
+ if (videoRef && adVideoRef) {
3315
+ try {
3316
+ adVideoRef.volume = videoRef.volume;
3317
+ adVideoRef.muted = muted;
3318
+ }
3319
+ catch (error) {
3320
+ // Ignore errors during cleanup
3321
+ }
3322
+ }
3323
+ };
2890
3324
  adVideoRef.addEventListener("canplay", handleCanPlay);
2891
- if (adVideoRef.readyState >= 3) {
3325
+ adVideoRef.addEventListener("loadeddata", handleLoadedData);
3326
+ // Try to play if already ready and src matches
3327
+ if (adVideoRef.readyState >= 3 && !needsReload) {
2892
3328
  attemptAdPlayback();
2893
3329
  }
2894
3330
  return () => {
2895
- adVideoRef.removeEventListener("canplay", handleCanPlay);
3331
+ if (adVideoRef) {
3332
+ adVideoRef.removeEventListener("canplay", handleCanPlay);
3333
+ adVideoRef.removeEventListener("loadeddata", handleLoadedData);
3334
+ }
2896
3335
  };
2897
- }, [adVideoRef, videoRef, attemptAdPlayback]);
3336
+ }, [adVideoRef, videoRef, muted, adBreak.adUrl, attemptAdPlayback]);
2898
3337
  useEffect(() => {
2899
- if (adVideoRef) {
3338
+ if (!adVideoRef)
3339
+ return;
3340
+ try {
3341
+ // Sync muted state
2900
3342
  adVideoRef.muted = muted;
3343
+ // Sync volume with main video
3344
+ if (videoRef) {
3345
+ adVideoRef.volume = videoRef.volume;
3346
+ }
3347
+ }
3348
+ catch (error) {
3349
+ // Ignore errors during state sync
2901
3350
  }
2902
- }, [adVideoRef, muted]);
3351
+ }, [adVideoRef, muted, videoRef]);
2903
3352
  const handleSkip = () => {
2904
3353
  if (canSkipAd && onSkip) {
2905
3354
  onSkip();
2906
3355
  }
2907
3356
  };
2908
- const formatTime = (seconds) => {
2909
- if (isNaN(seconds) || seconds < 0)
2910
- return "0:00";
2911
- const mins = Math.floor(seconds / 60);
2912
- const secs = Math.floor(seconds % 60);
2913
- return `${mins}:${secs.toString().padStart(2, "0")}`;
2914
- };
2915
3357
  const progressPercent = adDuration > 0 ? (adCurrentTime / adDuration) * 100 : 0;
2916
3358
  return (React__default.createElement("div", { className: "absolute inset-0 bg-black z-50 flex flex-col overflow-hidden transition-opacity duration-300", onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), onMouseMove: () => {
2917
3359
  setIsHovered(true);
@@ -2919,23 +3361,31 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2919
3361
  } },
2920
3362
  React__default.createElement("div", { className: "relative flex-1 w-full flex items-center justify-center" },
2921
3363
  React__default.createElement("video", { ref: (ref) => {
2922
- if (ref && ref !== adVideoRef) {
2923
- ref.muted = muted;
3364
+ if (!ref)
3365
+ return;
3366
+ if (ref !== adVideoRef) {
2924
3367
  setAdVideoRef(ref);
2925
- if (ref.src !== adBreak.adUrl) {
3368
+ }
3369
+ ref.muted = muted;
3370
+ if (videoRef) {
3371
+ ref.volume = videoRef.volume;
3372
+ }
3373
+ if (adBreak.adUrl) {
3374
+ const currentSrc = ref.src || ref.currentSrc || "";
3375
+ if (currentSrc !== adBreak.adUrl) {
2926
3376
  ref.src = adBreak.adUrl;
2927
- ref.load();
2928
3377
  }
2929
3378
  }
2930
3379
  }, className: "w-full h-full object-contain", autoPlay: true, playsInline: true, muted: muted, preload: "auto", key: adBreak.id }),
2931
- requiresInteraction && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
2932
- React__default.createElement("button", { onClick: attemptAdPlayback, className: "px-5 py-3 rounded bg-white/20 text-white font-semibold border border-white/40 hover:bg-white/30 transition" }, "Tap to Play Ad")))),
3380
+ (requiresInteraction || adLoadError) && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
3381
+ React__default.createElement("div", { className: "flex flex-col items-center gap-4" },
3382
+ adLoadError && (React__default.createElement("p", { className: "text-red-400 text-sm" }, "Ad failed to load")),
3383
+ React__default.createElement("button", { onClick: attemptAdPlayback, className: "px-5 py-3 rounded bg-white/20 text-white font-semibold border border-white/40 hover:bg-white/30 transition" }, adLoadError ? "Retry Ad" : "Tap to Play Ad"))))),
2933
3384
  React__default.createElement("div", { className: `absolute inset-0 transition-all duration-300 ${showControls ? "opacity-100" : "opacity-0 pointer-events-none"}` },
2934
3385
  React__default.createElement("div", { className: "absolute inset-0 bg-linear-to-b from-black/80 via-transparent to-black/90 flex flex-col justify-between" },
2935
3386
  React__default.createElement("div", { className: "shrink-0 relative" },
2936
3387
  React__default.createElement(ControlsHeader, { config: {
2937
- title: adBreak.title ||
2938
- config?.config?.headerConfig?.config?.title ||
3388
+ title: config?.config?.headerConfig?.config?.title ||
2939
3389
  "Advertisement",
2940
3390
  isTrailer: config?.config?.headerConfig?.config?.isTrailer,
2941
3391
  onClose: config?.config?.headerConfig?.config?.onClose,
@@ -2955,89 +3405,190 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
2955
3405
  React__default.createElement("div", { className: "relative h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none select-none" },
2956
3406
  React__default.createElement("div", { className: "absolute left-0 top-0 h-full bg-white rounded-full transition-all duration-300 ease-out", style: { width: `${progressPercent}%` } }),
2957
3407
  React__default.createElement("div", { className: "absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full shadow-lg transition-all duration-300 ease-out pointer-events-none", style: { left: `calc(${progressPercent}% - 6px)` } }))),
2958
- React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-between" },
2959
- React__default.createElement("div", { className: "flex items-center gap-4 text-white" },
2960
- React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold" }, formatTime(adCurrentTime)),
2961
- React__default.createElement("span", { className: "text-lg lg:text-3xl font-semibold text-gray-500" }, "/"),
2962
- React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-gray-400" }, formatTime(adDuration))),
2963
- sponsoredUrl && (React__default.createElement("a", { href: sponsoredUrl, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-semibold text-sky-300 hover:text-white transition-colors" }, "Learn More"))))))));
2964
- };
3408
+ sponsoredUrl && (React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-end" },
3409
+ React__default.createElement("a", { href: sponsoredUrl, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-semibold text-sky-300 hover:text-white transition-colors" }, "Learn More"))))))));
3410
+ });
3411
+ AdOverlay.displayName = "AdOverlay";
3412
+
3413
+ const ErrorOverlay = React__default.memo(({ error, onRetry }) => {
3414
+ const getIcon = () => {
3415
+ switch (error.type) {
3416
+ case "network":
3417
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3418
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" })));
3419
+ case "src":
3420
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3421
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" }),
3422
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 3l18 18" })));
3423
+ default:
3424
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3425
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" })));
3426
+ }
3427
+ };
3428
+ return (React__default.createElement("div", { className: "absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90" },
3429
+ React__default.createElement("div", { className: "flex flex-col items-center gap-4 p-6 text-center" },
3430
+ getIcon(),
3431
+ React__default.createElement("h3", { className: "text-xl font-semibold text-white" }, error.type === "network"
3432
+ ? "Network Error"
3433
+ : error.type === "src"
3434
+ ? "Video Unavailable"
3435
+ : "Playback Error"),
3436
+ React__default.createElement("p", { className: "text-sm text-gray-400 max-w-md" }, error.message),
3437
+ React__default.createElement("button", { onClick: onRetry, className: "mt-4 px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center gap-2" },
3438
+ React__default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3439
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" })),
3440
+ "Retry"))));
3441
+ });
3442
+ ErrorOverlay.displayName = "ErrorOverlay";
2965
3443
 
2966
3444
  var css_248z$1 = ".video-player video::cue {\n display: none !important;\n opacity: 0 !important;\n visibility: hidden !important;\n}\n\n.custom-subtitle-overlay {\n position: absolute;\n bottom: 10%;\n left: 50%;\n transform: translateX(-50%);\n\n font-size: 1.5rem;\n font-weight: 600;\n line-height: 1.4;\n text-align: center;\n\n color: #000;\n background: rgba(255, 255, 255, 0.6);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n\n padding: 10px 16px;\n border-radius: 10px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n\n max-width: 70%;\n min-width: fit-content;\n\n transition: all 0.2s ease-in-out;\n}\n\n.custom-subtitle-overlay:hover {\n transform: translateX(-50%) scale(1.02);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);\n}\n\n@media (max-width: 768px) {\n .custom-subtitle-overlay {\n font-size: 1.25rem;\n padding: 8px 14px;\n bottom: 8%;\n max-width: 85%;\n }\n}\n\n@media (max-width: 480px) {\n .custom-subtitle-overlay {\n font-size: 1rem;\n padding: 6px 10px;\n bottom: 6%;\n max-width: 90%;\n }\n}\n\n@media (prefers-contrast: high) {\n .custom-subtitle-overlay {\n background: #ffff00;\n color: #000;\n border: 3px solid #000;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-subtitle-overlay {\n transition: none;\n }\n\n .custom-subtitle-overlay:hover {\n transform: translateX(-50%);\n }\n}\n";
2967
3445
  styleInject(css_248z$1,{"insertAt":"top"});
2968
3446
 
2969
- var css_248z = "@keyframes fade-in {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes slide-up {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes pulse-glow {\n 0%,\n 100% {\n box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);\n }\n 50% {\n box-shadow: 0 0 20px rgba(239, 68, 68, 0.8);\n }\n}\n\n.animate-fade-in {\n animation: fade-in 0.3s ease-out;\n}\n\n.animate-slide-up {\n animation: slide-up 0.3s ease-out;\n}\n\n.ad-progress-bar {\n position: relative;\n overflow: hidden;\n}\n\n.ad-progress-bar::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: linear-gradient(\n 90deg,\n transparent,\n rgba(255, 255, 255, 0.3),\n transparent\n );\n animation: shimmer 2s infinite;\n}\n\n@keyframes shimmer {\n 0% {\n transform: translateX(-100%);\n }\n 100% {\n transform: translateX(100%);\n }\n}\n\n.ad-badge {\n background: linear-gradient(\n 135deg,\n rgba(239, 68, 68, 0.9),\n rgba(249, 115, 22, 0.9)\n );\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n}\n\n.ad-button {\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.ad-button:hover {\n transform: translateY(-2px);\n box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);\n}\n\n.ad-button:active {\n transform: translateY(0);\n}\n\n.skip-button-enabled {\n background: linear-gradient(\n 135deg,\n rgba(255, 255, 255, 0.2),\n rgba(255, 255, 255, 0.1)\n );\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n.skip-button-enabled:hover {\n background: linear-gradient(\n 135deg,\n rgba(255, 255, 255, 0.3),\n rgba(255, 255, 255, 0.2)\n );\n box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);\n}\n\n.skip-button-disabled {\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n}\n\n.loader {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n display: inline-block;\n border-top: 3px solid #fff;\n border-right: 3px solid transparent;\n box-sizing: border-box;\n animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@media (max-width: 768px) {\n .ad-controls {\n padding: 1rem;\n gap: 0.75rem;\n }\n\n .ad-badge {\n font-size: 0.625rem;\n padding: 0.5rem 0.75rem;\n }\n\n .skip-button {\n padding: 0.5rem 1rem;\n font-size: 0.75rem;\n }\n}\n\n@media (max-width: 480px) {\n .ad-controls {\n padding: 0.75rem;\n gap: 0.5rem;\n }\n\n .ad-badge {\n font-size: 0.5rem;\n padding: 0.375rem 0.625rem;\n }\n\n .skip-button {\n padding: 0.375rem 0.75rem;\n font-size: 0.625rem;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .animate-fade-in,\n .animate-slide-up,\n .ad-progress-bar::after {\n animation: none;\n }\n\n .ad-button {\n transition: none;\n }\n}\n\n@media (prefers-contrast: high) {\n .ad-badge {\n background: #000;\n border: 2px solid #fff;\n }\n\n .skip-button-enabled {\n background: #fff;\n color: #000;\n border: 2px solid #000;\n }\n}\n";
3447
+ var css_248z = "\n.loader {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n display: inline-block;\n border-top: 3px solid #fff;\n border-right: 3px solid transparent;\n box-sizing: border-box;\n animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .loader {\n animation: none;\n }\n}\n";
2970
3448
  styleInject(css_248z,{"insertAt":"top"});
2971
3449
 
2972
- const VideoPlayer = ({ trackSrc, trackTitle, intro, onClose, onError, trackPoster, isTrailer, className, type, height, width, timeCodes, getPreviewScreenUrl, tracking, subtitles, episodeList, currentEpisodeIndex = 0, onEnded, nextEpisodeConfig, subtitleStyle, showControls = true, isMute = false, ads, }) => {
2973
- const { setVideoWrapperRef } = useVideoStore(useShallow((state) => ({
3450
+ const VideoPlayer = React__default.memo(({ video, style, events, features }) => {
3451
+ const { src: trackSrc, title: trackTitle, poster: trackPoster, type, isTrailer, showControls = true, isMute = false, startFrom, } = video;
3452
+ const { className, width, height, subtitleStyle, qualityConfig } = style || {};
3453
+ const { onEnded, onError, onClose, onWatchHistoryUpdate } = events || {};
3454
+ const { timeCodes, getPreviewScreenUrl, tracking, subtitles, episodeList, currentEpisodeIndex = 0, intro, nextEpisodeConfig, ads, } = features || {};
3455
+ const { setVideoWrapperRef, setActiveQuality } = useVideoStore(useShallow((state) => ({
2974
3456
  setVideoWrapperRef: state.setVideoWrapperRef,
3457
+ setActiveQuality: state.setActiveQuality,
2975
3458
  })));
3459
+ React__default.useEffect(() => {
3460
+ if (qualityConfig?.defaultQuality) {
3461
+ setActiveQuality(qualityConfig.defaultQuality);
3462
+ }
3463
+ }, [qualityConfig?.defaultQuality, setActiveQuality]);
2976
3464
  const effectiveAds = React__default.useMemo(() => (isTrailer ? undefined : ads), [ads, isTrailer]);
2977
3465
  const hasPreRoll = React__default.useMemo(() => Boolean(effectiveAds?.preRoll), [effectiveAds?.preRoll]);
2978
3466
  const { registerVideoRef, videoRef, isAdPlaying, currentAd, initialAdFinished, shouldCoverMainVideo, shouldShowPlaceholder, } = usePrimaryVideoLifecycle({
2979
3467
  hasPreRoll,
2980
3468
  trackSrc,
2981
3469
  });
3470
+ const onWatchHistoryUpdateRef = React__default.useRef(onWatchHistoryUpdate);
3471
+ React__default.useEffect(() => {
3472
+ onWatchHistoryUpdateRef.current = onWatchHistoryUpdate;
3473
+ }, [onWatchHistoryUpdate]);
3474
+ const getWatchHistoryData = React__default.useCallback(() => {
3475
+ const video = useVideoStore.getState().videoRef;
3476
+ if (!video || !video.duration || isNaN(video.duration))
3477
+ return null;
3478
+ const currentTime = video.currentTime || 0;
3479
+ const duration = video.duration;
3480
+ const progress = Math.round((currentTime / duration) * 100);
3481
+ const isCompleted = progress >= 90;
3482
+ return {
3483
+ currentTime,
3484
+ duration,
3485
+ progress,
3486
+ isCompleted,
3487
+ watchedAt: Date.now(),
3488
+ };
3489
+ }, []);
3490
+ const handleClose = React__default.useCallback(() => {
3491
+ const historyData = getWatchHistoryData();
3492
+ if (historyData && onWatchHistoryUpdate) {
3493
+ onWatchHistoryUpdate(historyData);
3494
+ }
3495
+ onClose?.();
3496
+ }, [getWatchHistoryData, onWatchHistoryUpdate, onClose]);
3497
+ const overlayConfig = React__default.useMemo(() => ({
3498
+ headerConfig: {
3499
+ config: {
3500
+ isTrailer: isTrailer,
3501
+ title: trackTitle,
3502
+ onClose: handleClose,
3503
+ videoRef: videoRef,
3504
+ qualityConfig,
3505
+ },
3506
+ },
3507
+ bottomConfig: {
3508
+ config: {
3509
+ seekBarConfig: {
3510
+ timeCodes: timeCodes,
3511
+ trackColor: "red",
3512
+ getPreviewScreenUrl,
3513
+ },
3514
+ },
3515
+ },
3516
+ }), [
3517
+ isTrailer,
3518
+ trackTitle,
3519
+ handleClose,
3520
+ videoRef,
3521
+ timeCodes,
3522
+ getPreviewScreenUrl,
3523
+ qualityConfig,
3524
+ ]);
3525
+ const adOverlayConfig = React__default.useMemo(() => ({
3526
+ config: {
3527
+ headerConfig: {
3528
+ config: {
3529
+ isTrailer: isTrailer,
3530
+ title: trackTitle,
3531
+ onClose: handleClose,
3532
+ },
3533
+ },
3534
+ bottomConfig: {
3535
+ config: {
3536
+ seekBarConfig: {
3537
+ timeCodes: timeCodes,
3538
+ trackColor: "red",
3539
+ getPreviewScreenUrl,
3540
+ },
3541
+ },
3542
+ },
3543
+ },
3544
+ }), [isTrailer, trackTitle, handleClose, timeCodes, getPreviewScreenUrl]);
2982
3545
  useVideoSource(trackSrc, type);
2983
3546
  useSubtitles(subtitles);
2984
3547
  useSubtitleStyling(subtitleStyle);
2985
- useVideoTracking(tracking, episodeList, currentEpisodeIndex, onClose);
3548
+ useVideoTracking(tracking, episodeList, currentEpisodeIndex, handleClose);
2986
3549
  const { showSkipIntro, handleSkipIntro } = useIntroSkip(intro);
2987
3550
  useEpisodes(episodeList, currentEpisodeIndex, nextEpisodeConfig);
2988
3551
  const { onSeeked, onTimeUpdate, onLoadedMetadata, onProgress, onPlay, onPause, onEnded: onEndedHook, } = useVideoEvents();
2989
3552
  const { skipAd } = useAdManager(effectiveAds);
3553
+ const { error, handleVideoError, retry } = useVideoError();
3554
+ const hasResumedRef = React__default.useRef(false);
3555
+ React__default.useEffect(() => {
3556
+ return () => {
3557
+ const historyData = getWatchHistoryData();
3558
+ if (historyData && onWatchHistoryUpdateRef.current) {
3559
+ onWatchHistoryUpdateRef.current(historyData);
3560
+ }
3561
+ };
3562
+ }, [getWatchHistoryData]);
3563
+ React__default.useEffect(() => {
3564
+ if (!videoRef || !startFrom || hasResumedRef.current)
3565
+ return;
3566
+ const handleCanPlay = () => {
3567
+ if (!hasResumedRef.current && startFrom > 0) {
3568
+ videoRef.currentTime = startFrom;
3569
+ hasResumedRef.current = true;
3570
+ }
3571
+ };
3572
+ videoRef.addEventListener("canplay", handleCanPlay);
3573
+ return () => videoRef.removeEventListener("canplay", handleCanPlay);
3574
+ }, [videoRef, startFrom]);
2990
3575
  return (React__default.createElement("div", { ref: setVideoWrapperRef, className: `video-player ${height || "h-full"} ${width || "w-full"} mx-auto absolute` },
2991
3576
  trackPoster && (React__default.createElement("div", { className: "pip-poster absolute inset-0 bg-center bg-cover hidden", style: { backgroundImage: `url(${trackPoster})` } })),
2992
3577
  React__default.createElement("video", { playsInline: true, preload: hasPreRoll ? "metadata" : "auto", ref: registerVideoRef, onSeeked: onSeeked, poster: trackPoster, crossOrigin: "anonymous", controls: false, disableRemotePlayback: true, controlsList: "nodownload", onContextMenu: (e) => e.preventDefault(), onTimeUpdate: onTimeUpdate, onLoadedMetadata: onLoadedMetadata, onProgress: onProgress, onPlay: onPlay, onPause: onPause, onEnded: (e) => {
2993
3578
  onEndedHook(e);
2994
3579
  onEnded?.(e);
2995
3580
  }, onError: (e) => {
3581
+ handleVideoError(e);
2996
3582
  onError?.(e);
2997
3583
  }, autoPlay: !hasPreRoll, muted: isMute, className: `w-full h-full relative ${className || ""} ${shouldCoverMainVideo ? "opacity-0" : "opacity-100"} transition-opacity duration-200 ease-out` }),
2998
- shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex items-center justify-center bg-black" },
2999
- React__default.createElement("span", { className: "loader" }))),
3000
- showControls && initialAdFinished && (React__default.createElement(Overlay, { config: {
3001
- headerConfig: {
3002
- config: {
3003
- isTrailer: isTrailer,
3004
- title: trackTitle,
3005
- onClose: onClose,
3006
- videoRef: videoRef,
3007
- },
3008
- },
3009
- bottomConfig: {
3010
- config: {
3011
- seekBarConfig: {
3012
- timeCodes: timeCodes,
3013
- trackColor: "red",
3014
- getPreviewScreenUrl,
3015
- },
3016
- },
3017
- },
3018
- } })),
3584
+ shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm" },
3585
+ React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" }))),
3586
+ showControls && initialAdFinished && (React__default.createElement(Overlay, { config: overlayConfig })),
3019
3587
  React__default.createElement(SubtitleOverlay, { styleConfig: subtitleStyle }),
3020
3588
  showSkipIntro && !isAdPlaying && initialAdFinished && (React__default.createElement(VideoActionButton, { text: "Skip Intro", onClick: handleSkipIntro, position: "left" })),
3021
- isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config: {
3022
- config: {
3023
- headerConfig: {
3024
- config: {
3025
- isTrailer: isTrailer,
3026
- title: trackTitle,
3027
- onClose: onClose,
3028
- },
3029
- },
3030
- bottomConfig: {
3031
- config: {
3032
- seekBarConfig: {
3033
- timeCodes: timeCodes,
3034
- trackColor: "red",
3035
- getPreviewScreenUrl,
3036
- },
3037
- },
3038
- },
3039
- },
3040
- } }))));
3041
- };
3589
+ isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config: adOverlayConfig })),
3590
+ error && onError && React__default.createElement(ErrorOverlay, { error: error, onRetry: retry })));
3591
+ });
3592
+ VideoPlayer.displayName = "VideoPlayer";
3042
3593
 
3043
3594
  export { VideoPlayer, useVideoStore };