@zezosoft/react-player 0.0.8 → 0.0.10

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.
Files changed (45) hide show
  1. package/README.md +536 -146
  2. package/dist/VideoPlayer/VideoPlayer.d.ts +1 -0
  3. package/dist/VideoPlayer/components/AdOverlay.d.ts +10 -0
  4. package/dist/VideoPlayer/components/ErrorOverlay.d.ts +8 -0
  5. package/dist/VideoPlayer/components/Overlay.d.ts +4 -0
  6. package/dist/VideoPlayer/components/SubtitleOverlay.d.ts +7 -0
  7. package/dist/VideoPlayer/components/controls/BottomControls.d.ts +5 -0
  8. package/dist/VideoPlayer/components/controls/ControlsHeader.d.ts +5 -0
  9. package/dist/VideoPlayer/components/controls/MiddleControls.d.ts +3 -0
  10. package/dist/VideoPlayer/components/controls/VideoPlayerControls.d.ts +4 -0
  11. package/dist/VideoPlayer/components/controls/index.d.ts +4 -0
  12. package/dist/VideoPlayer/components/time-line/TimeLine.d.ts +21 -0
  13. package/dist/VideoPlayer/components/time-line/components/HoverTimeWithPreview.d.ts +16 -0
  14. package/dist/VideoPlayer/components/time-line/components/Thumb.d.ts +9 -0
  15. package/dist/VideoPlayer/components/time-line/components/TimeCodeItem.d.ts +21 -0
  16. package/dist/VideoPlayer/components/time-line/components/TimeCodes.d.ts +15 -0
  17. package/dist/VideoPlayer/components/time-line/utils/getEndTimeByIndex.d.ts +2 -0
  18. package/dist/VideoPlayer/components/time-line/utils/getHoverTimePosition.d.ts +3 -0
  19. package/dist/VideoPlayer/components/time-line/utils/getPositionPercent.d.ts +1 -0
  20. package/dist/VideoPlayer/components/time-line/utils/getTimeScale.d.ts +1 -0
  21. package/dist/VideoPlayer/components/time-line/utils/isInRange.d.ts +1 -0
  22. package/dist/VideoPlayer/components/time-line/utils/positionToMs.d.ts +1 -0
  23. package/dist/VideoPlayer/components/time-line/utils/secondsToTime.d.ts +6 -0
  24. package/dist/VideoPlayer/components/time-line/utils/timeToTimeString.d.ts +1 -0
  25. package/dist/VideoPlayer/constants.d.ts +3 -0
  26. package/dist/VideoPlayer/hooks/index.d.ts +4 -0
  27. package/dist/VideoPlayer/hooks/useAdManager.d.ts +8 -0
  28. package/dist/VideoPlayer/hooks/useNetworkSpeed.d.ts +7 -0
  29. package/dist/VideoPlayer/hooks/usePrimaryVideoLifecycle.d.ts +17 -0
  30. package/dist/VideoPlayer/hooks/useVideoError.d.ts +7 -0
  31. package/dist/VideoPlayer/hooks/useVideoSource.d.ts +1 -14
  32. package/dist/VideoPlayer/hooks/useVideoTracking.d.ts +2 -2
  33. package/dist/VideoPlayer/types/AdTypes.d.ts +33 -0
  34. package/dist/VideoPlayer/types/VideoPlayerTypes.d.ts +34 -10
  35. package/dist/VideoPlayer/utils/index.d.ts +1 -1
  36. package/dist/VideoPlayer/utils/qualityManager.d.ts +6 -32
  37. package/dist/components/ui/FullScreenToggle.d.ts +1 -1
  38. package/dist/components/ui/PiPictureInPictureToggle.d.ts +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +2487 -493
  41. package/dist/store/slices/adsSlice.d.ts +24 -0
  42. package/dist/store/slices/errorSlice.d.ts +5 -0
  43. package/dist/store/slices/index.d.ts +2 -0
  44. package/dist/store/types/StoreTypes.d.ts +41 -9
  45. package/package.json +12 -11
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react';
2
- import React__default, { memo, useCallback, useEffect, useRef, useState } from 'react';
2
+ import React__default, { memo, useCallback, useEffect, useRef, useState, useMemo } from 'react';
3
+ import { useShallow } from 'zustand/react/shallow';
4
+ import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
3
5
  import { create } from 'zustand';
4
6
  import { IoVolumeMuteOutline, IoVolumeHighOutline } from 'react-icons/io5';
5
7
  import { IoMdClose } from 'react-icons/io';
6
- import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight } from 'lucide-react';
7
8
  import screenfull from 'screenfull';
8
9
  import Hls from 'hls.js';
9
10
  import * as dashjs from 'dashjs';
@@ -35,8 +36,8 @@ function styleInject(css, ref) {
35
36
  }
36
37
  }
37
38
 
38
- var css_248z$3 = "/*! tailwindcss v4.1.12 | 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-green-500: oklch(72.3% 0.219 149.579);\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-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\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-semibold: 600;\n --font-weight-bold: 700;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\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 .visible {\n visibility: visible;\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-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 .h-3 {\n height: calc(var(--spacing) * 3);\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-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 .-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 .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-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-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 .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-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\\/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\\/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-gradient-to-b {\n --tw-gradient-position: to bottom in oklab;\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 .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\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-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-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 .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\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 .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-white {\n color: var(--color-white);\n }\n .opacity-50 {\n opacity: 50%;\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-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-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, visibility, 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 .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\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\\/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 .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 .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-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@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-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 }\n }\n}\n";
39
- styleInject(css_248z$3,{"insertAt":"top"});
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-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-12 {\n width: calc(var(--spacing) * 12);\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-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-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-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-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-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";
40
+ styleInject(css_248z$4,{"insertAt":"top"});
40
41
 
41
42
  const createVideoRefsSlice = (set) => ({
42
43
  videoRef: null,
@@ -72,19 +73,19 @@ const createVideoControlsSlice = (set) => ({
72
73
  setControls: (controls) => set({ controls }),
73
74
  isFullscreen: false,
74
75
  setIsFullscreen: (isFullscreen) => set({ isFullscreen }),
75
- controlsVisible: true,
76
- setControlsVisible: (visible) => set({ controlsVisible: visible }),
77
76
  });
78
77
 
79
78
  const createVideoQualitySlice = (set) => ({
80
- hlsInstance: undefined,
79
+ hlsInstance: null,
81
80
  setHlsInstance: (hlsInstance) => set({ hlsInstance }),
82
- dashInstance: undefined,
81
+ dashInstance: null,
83
82
  setDashInstance: (dashInstance) => set({ dashInstance }),
84
- qualityLevels: undefined,
83
+ qualityLevels: [],
85
84
  setQualityLevels: (qualityLevels) => set({ qualityLevels }),
86
85
  activeQuality: "auto",
87
86
  setActiveQuality: (activeQuality) => set({ activeQuality }),
87
+ currentQuality: "auto",
88
+ setCurrentQuality: (currentQuality) => set({ currentQuality }),
88
89
  streamType: "mp4",
89
90
  setStreamType: (streamType) => set({ streamType }),
90
91
  });
@@ -114,8 +115,60 @@ const createIntroSlice = (set) => ({
114
115
  setShowIntroSkip: (show) => set({ showIntroSkip: show }),
115
116
  });
116
117
 
118
+ const createAdsSlice = (set, get) => ({
119
+ isAdPlaying: false,
120
+ setIsAdPlaying: (isAdPlaying) => set({ isAdPlaying }),
121
+ currentAd: null,
122
+ setCurrentAd: (currentAd) => set({ currentAd }),
123
+ adType: null,
124
+ setAdType: (adType) => set({ adType }),
125
+ adCurrentTime: 0,
126
+ setAdCurrentTime: (adCurrentTime) => set({ adCurrentTime }),
127
+ canSkipAd: false,
128
+ setCanSkipAd: (canSkipAd) => set({ canSkipAd }),
129
+ skipCountdown: 0,
130
+ setSkipCountdown: (skipCountdown) => set({ skipCountdown }),
131
+ playedAdBreaks: [],
132
+ addPlayedAdBreak: (id) => set((state) => ({
133
+ playedAdBreaks: [...state.playedAdBreaks, id],
134
+ })),
135
+ midRollQueue: [],
136
+ setMidRollQueue: (midRollQueue) => set({ midRollQueue }),
137
+ adVideoRef: null,
138
+ setAdVideoRef: (adVideoRef) => set({ adVideoRef }),
139
+ });
140
+
141
+ const createErrorSlice = (set) => ({
142
+ error: null,
143
+ setError: (error) => set({ error }),
144
+ clearError: () => set({ error: null }),
145
+ });
146
+
117
147
  const createResetSlice = (set, get) => ({
118
148
  resetStore: () => {
149
+ const safeStopMediaElement = (media) => {
150
+ if (!media)
151
+ return;
152
+ try {
153
+ media.pause();
154
+ }
155
+ catch (_error) { }
156
+ try {
157
+ media.currentTime = 0;
158
+ }
159
+ catch (_error) { }
160
+ media.removeAttribute("src");
161
+ media.load();
162
+ };
163
+ const { videoRef, adVideoRef, hlsInstance, dashInstance } = get();
164
+ safeStopMediaElement(videoRef);
165
+ safeStopMediaElement(adVideoRef);
166
+ if (hlsInstance && typeof hlsInstance.destroy === "function") {
167
+ hlsInstance.destroy();
168
+ }
169
+ if (dashInstance && typeof dashInstance.reset === "function") {
170
+ dashInstance.reset();
171
+ }
119
172
  set({
120
173
  videoRef: null,
121
174
  videoWrapperRef: null,
@@ -130,6 +183,7 @@ const createResetSlice = (set, get) => ({
130
183
  controls: false,
131
184
  isFullscreen: false,
132
185
  hlsInstance: undefined,
186
+ dashInstance: undefined,
133
187
  qualityLevels: undefined,
134
188
  activeQuality: "auto",
135
189
  activeSubtitle: null,
@@ -140,6 +194,15 @@ const createResetSlice = (set, get) => ({
140
194
  countdownTime: 10,
141
195
  autoPlayNext: false,
142
196
  showIntroSkip: false,
197
+ isAdPlaying: false,
198
+ currentAd: null,
199
+ adType: null,
200
+ adCurrentTime: 0,
201
+ canSkipAd: false,
202
+ skipCountdown: 0,
203
+ playedAdBreaks: [],
204
+ midRollQueue: [],
205
+ adVideoRef: null,
143
206
  });
144
207
  },
145
208
  });
@@ -153,9 +216,146 @@ const useVideoStore = create()((set, get, store) => ({
153
216
  ...createSubtitlesSlice(set),
154
217
  ...createEpisodesSlice(set),
155
218
  ...createIntroSlice(set),
156
- ...createResetSlice(set),
219
+ ...createAdsSlice(set),
220
+ ...createErrorSlice(set),
221
+ ...createResetSlice(set, get),
157
222
  }));
158
223
 
224
+ class QualityManager {
225
+ /**
226
+ * Set video quality for HLS streams with OTT-grade smoothness
227
+ *
228
+ * Best practices implemented:
229
+ * 1. Use currentLevel for immediate quality change
230
+ * 2. Use autoLevelCapping to prevent ABR from switching back
231
+ * 3. Use nextLevel to ensure next segment uses selected quality
232
+ * 4. Handle edge cases and errors gracefully
233
+ *
234
+ * @param hlsInstance HLS.js instance (null for native HLS, undefined when not available)
235
+ * @param levelIndex Quality level index (-1 for auto)
236
+ */
237
+ static setHlsQuality(hlsInstance, levelIndex) {
238
+ if (!hlsInstance)
239
+ return;
240
+ const levels = hlsInstance?.levels ?? [];
241
+ const levelExists = levelIndex >= 0 && levels[levelIndex];
242
+ if (levelIndex < -1 || (levelIndex >= 0 && !levelExists)) {
243
+ return;
244
+ }
245
+ if (levelIndex === -1) {
246
+ hlsInstance.currentLevel = -1;
247
+ hlsInstance.nextLevel = -1;
248
+ hlsInstance.loadLevel = -1;
249
+ return;
250
+ }
251
+ hlsInstance.loadLevel = levelIndex;
252
+ hlsInstance.nextLevel = levelIndex;
253
+ hlsInstance.currentLevel = levelIndex;
254
+ }
255
+ /**
256
+
257
+ * @param dashInstance DASH.js instance
258
+ * @param qualityId Quality level ID (undefined/null for auto)
259
+ */
260
+ static setDashQuality(dashInstance, qualityIndex) {
261
+ if (!dashInstance)
262
+ return;
263
+ const player = dashInstance;
264
+ if (qualityIndex === null ||
265
+ qualityIndex === undefined ||
266
+ qualityIndex < 0) {
267
+ player.setAutoSwitchQualityFor("video", true);
268
+ return;
269
+ }
270
+ player.setAutoSwitchQualityFor("video", false);
271
+ if (player.getQualityFor("video") !== qualityIndex) {
272
+ player.setQualityFor("video", qualityIndex);
273
+ }
274
+ }
275
+ /**
276
+ * @param hlsInstance HLS.js instance
277
+ * @returns Array of quality level objects
278
+ */
279
+ static getHlsQualityLevels(hlsInstance) {
280
+ if (!hlsInstance || !hlsInstance.levels) {
281
+ return [];
282
+ }
283
+ try {
284
+ return hlsInstance.levels.map((level, index) => ({
285
+ height: level.height || 0,
286
+ bitrate: level.bitrate || 0,
287
+ originalIndex: index,
288
+ }));
289
+ }
290
+ catch (_error) {
291
+ return [];
292
+ }
293
+ }
294
+ /**
295
+ * @param dashInstance DASH.js instance
296
+ * @returns Array of quality level objects
297
+ */
298
+ static getDashQualityLevels(dashInstance) {
299
+ if (!dashInstance) {
300
+ return [];
301
+ }
302
+ try {
303
+ const representations = dashInstance.getRepresentationsByType("video");
304
+ if (!representations || !representations.length) {
305
+ return [];
306
+ }
307
+ return representations.map((rep) => ({
308
+ height: rep.height || Math.round(rep.bandwidth / 1000) || 0,
309
+ bitrate: rep.bandwidth,
310
+ id: rep.id,
311
+ }));
312
+ }
313
+ catch (_error) {
314
+ return [];
315
+ }
316
+ }
317
+ /**
318
+ * @param streamType Type of stream (hls, dash, etc.)
319
+ * @param qualityIdentifier Quality level identifier (index for HLS, ID for DASH)
320
+ */
321
+ static setQuality(streamType, qualityIdentifier) {
322
+ const { hlsInstance, dashInstance, setActiveQuality, setCurrentQuality } = useVideoStore.getState();
323
+ if (qualityIdentifier === "auto") {
324
+ if (streamType === "hls")
325
+ this.setHlsQuality(hlsInstance, -1);
326
+ if (streamType === "dash")
327
+ this.setDashQuality(dashInstance ?? null, null);
328
+ setActiveQuality("auto");
329
+ setCurrentQuality("auto");
330
+ return;
331
+ }
332
+ const parseIndex = (prefix) => parseInt(qualityIdentifier.replace(prefix, ""), 10);
333
+ if (streamType === "hls" && qualityIdentifier.startsWith("hls-")) {
334
+ const levelIndex = parseIndex("hls-");
335
+ if (!Number.isNaN(levelIndex)) {
336
+ this.setHlsQuality(hlsInstance, levelIndex);
337
+ setActiveQuality(qualityIdentifier);
338
+ setCurrentQuality(qualityIdentifier);
339
+ }
340
+ return;
341
+ }
342
+ if (streamType === "dash" && qualityIdentifier.startsWith("dash-")) {
343
+ const levelIndex = parseIndex("dash-");
344
+ if (!Number.isNaN(levelIndex)) {
345
+ this.setDashQuality(dashInstance ?? null, levelIndex);
346
+ setActiveQuality(qualityIdentifier);
347
+ setCurrentQuality(qualityIdentifier);
348
+ }
349
+ }
350
+ }
351
+ /**
352
+ * @param streamType Type of stream (hls, dash, etc.)
353
+ */
354
+ static setAutoQuality(streamType) {
355
+ this.setQuality(streamType, "auto");
356
+ }
357
+ }
358
+
159
359
  /**
160
360
  * @description
161
361
  * @param seconds
@@ -188,18 +388,25 @@ const secondsToMilliseconds = (seconds) => {
188
388
  * @returns
189
389
  */
190
390
  const getExtensionFromUrl = (url) => {
191
- const extension = url?.split(".")?.pop();
391
+ if (!url) {
392
+ return undefined;
393
+ }
394
+ const sanitized = url.split("#")[0]?.split("?")[0] ?? url;
395
+ const extension = sanitized?.split(".")?.pop()?.toLowerCase();
192
396
  if (extension === "m3u8") {
193
397
  return "hls";
194
398
  }
195
399
  if (extension === "mpd") {
196
400
  return "dash";
197
401
  }
402
+ if (extension === "mp4") {
403
+ return "mp4";
404
+ }
198
405
  return extension;
199
406
  };
200
407
 
201
408
  function getPositionPercent(max, current) {
202
- const divider = max || -1; // prevent division by zero
409
+ const divider = max || -1;
203
410
  return (current * 100) / divider;
204
411
  }
205
412
 
@@ -247,7 +454,7 @@ const TimeCodes = ({ max = 1000, currentTime = 0, bufferTime = 0, seekHoverPosit
247
454
  if (label !== currentLabel) {
248
455
  setLabel(currentLabel);
249
456
  }
250
- }, [label]);
457
+ }, [label, setLabel]);
251
458
  useEffect(() => {
252
459
  if (!mobileSeeking) {
253
460
  return;
@@ -259,7 +466,7 @@ const TimeCodes = ({ max = 1000, currentTime = 0, bufferTime = 0, seekHoverPosit
259
466
  if (currentCode?.description !== label) {
260
467
  setLabel(currentCode?.description || "");
261
468
  }
262
- }, [currentTime, label, max, timeCodes]);
469
+ }, [currentTime, label, max, timeCodes, mobileSeeking, setLabel]);
263
470
  return (React__default.createElement(React__default.Fragment, null, timeCodes?.map(({ fromMs, description }, index) => {
264
471
  const endTime = getEndTimeByIndex(timeCodes, index, max);
265
472
  const isTimePassed = endTime <= currentTime;
@@ -336,12 +543,15 @@ const Thumb = ({ max, currentTime, isThumbActive, trackColor, }) => {
336
543
  left: `calc(${leftPosition}% + ${thumbConstantOffset}px)`,
337
544
  };
338
545
  };
339
- return (React__default.createElement("div", { className: isThumbActive ? "thumb active" : "thumb active", "data-testid": "testThumb", style: getThumbHandlerPosition() },
546
+ return (React__default.createElement("div", { className: "thumb active", "data-testid": "testThumb", style: getThumbHandlerPosition() },
340
547
  React__default.createElement("div", { className: "handler", style: {
341
548
  backgroundColor: trackColor || "#ff0000",
342
549
  } })));
343
550
  };
344
551
 
552
+ var css_248z$3 = ".ui-video-seek-slider {\n position: relative;\n touch-action: none;\n}\n.ui-video-seek-slider:focus {\n outline: none;\n}\n.ui-video-seek-slider .track {\n padding: 0;\n cursor: pointer;\n outline: none;\n}\n.ui-video-seek-slider .track:focus {\n border: 0;\n outline: none;\n}\n.ui-video-seek-slider .track .main {\n width: 100%;\n outline: none;\n height: 18px;\n top: 0;\n position: absolute;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n.ui-video-seek-slider .track .main:before {\n content: \"\";\n position: absolute;\n width: 100%;\n height: 3px;\n background-color: rgba(255, 255, 255, 0.2);\n overflow: hidden;\n transition: height 0.1s;\n outline: none;\n}\n.ui-video-seek-slider .track .main .inner-seek-block {\n position: absolute;\n width: 100%;\n height: 3px;\n transition: height 0.1s, opacity 0.4s, transform 0.2s ease-out;\n transform-origin: 0 0;\n}\n.ui-video-seek-slider .track .main:focus {\n border: 0;\n outline: none;\n}\n.ui-video-seek-slider .track .main .buffered {\n background-color: rgba(255, 255, 255, 0.3);\n z-index: 2;\n transition: transform 0.2s ease-out;\n}\n.ui-video-seek-slider .track .main .seek-hover {\n background-color: rgba(255, 255, 255, 0.5);\n z-index: 1;\n}\n.ui-video-seek-slider .track .main .connect {\n background-color: #ff0000;\n z-index: 3;\n transform-origin: 0 0;\n}\n.ui-video-seek-slider .track .main.with-gap .inner-seek-block,\n.ui-video-seek-slider .track .main.with-gap:before {\n width: calc(100% - 2px);\n margin: 0 auto;\n}\n@media (hover) {\n .ui-video-seek-slider .track .main:hover:before {\n height: 8px;\n }\n .ui-video-seek-slider .track .main:hover .inner-seek-block {\n height: 8px;\n }\n}\n.ui-video-seek-slider .thumb {\n pointer-events: none;\n position: absolute;\n width: 12px;\n height: 12px;\n left: -6px;\n z-index: 4;\n top: 3px;\n}\n.ui-video-seek-slider .thumb .handler {\n border-radius: 100%;\n width: 100%;\n height: 100%;\n background-color: #ff0000;\n opacity: 0;\n transform: scale(0.4);\n transition: transform 0.2s, opacity 0.2s;\n}\n.ui-video-seek-slider .thumb.active .handler {\n opacity: 1;\n transform: scale(1);\n}\n.ui-video-seek-slider .hover-time {\n text-shadow: 1px 1px 1px #000;\n position: absolute;\n line-height: 18px;\n font-size: 16px;\n color: #ddd;\n bottom: 5px;\n left: 0;\n padding: 5px 10px;\n opacity: 0;\n pointer-events: none;\n text-align: center;\n}\n.ui-video-seek-slider .hover-time.active {\n opacity: 1;\n}\n.ui-video-seek-slider .hover-time .preview-screen {\n background-repeat: no-repeat;\n background-size: cover;\n background-position: center;\n width: 200px;\n height: 110px;\n border-radius: 5px;\n background-color: #000;\n margin: 0 auto 10px;\n}\n.ui-video-seek-slider:hover .track .main .seek-hover {\n opacity: 1;\n}\n\n";
553
+ styleInject(css_248z$3,{"insertAt":"top"});
554
+
345
555
  const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThumbTooltip = false, offset = 0, secondsPrefix = "", minutesPrefix = "", limitTimeTooltipBySides = true, timeCodes, onChange = () => undefined, getPreviewScreenUrl, trackColor, }) => {
346
556
  const [seekHoverPosition, setSeekHoverPosition] = useState(0);
347
557
  const [label, setLabel] = useState("");
@@ -432,39 +642,70 @@ const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThum
432
642
  React__default.createElement(Thumb, { max: max, currentTime: currentTime, isThumbActive: isThumbActive, trackColor: trackColor })));
433
643
  };
434
644
 
435
- var css_248z$2 = ".ui-video-seek-slider {\n position: relative;\n touch-action: none;\n}\n.ui-video-seek-slider:focus {\n outline: none;\n}\n.ui-video-seek-slider .track {\n padding: 0;\n cursor: pointer;\n outline: none;\n}\n.ui-video-seek-slider .track:focus {\n border: 0;\n outline: none;\n}\n.ui-video-seek-slider .track .main {\n width: 100%;\n outline: none;\n height: 18px;\n top: 0;\n position: absolute;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n}\n.ui-video-seek-slider .track .main:before {\n content: \"\";\n position: absolute;\n width: 100%;\n height: 3px;\n background-color: rgba(255, 255, 255, 0.2);\n overflow: hidden;\n transition: height 0.1s;\n outline: none;\n}\n.ui-video-seek-slider .track .main .inner-seek-block {\n position: absolute;\n width: 100%;\n height: 3px;\n transition: height 0.1s, opacity 0.4s, transform 0.2s ease-out;\n transform-origin: 0 0;\n}\n.ui-video-seek-slider .track .main:focus {\n border: 0;\n outline: none;\n}\n.ui-video-seek-slider .track .main .buffered {\n background-color: rgba(255, 255, 255, 0.3);\n z-index: 2;\n transition: transform 0.2s ease-out;\n}\n.ui-video-seek-slider .track .main .seek-hover {\n background-color: rgba(255, 255, 255, 0.5);\n z-index: 1;\n}\n.ui-video-seek-slider .track .main .connect {\n background-color: #ff0000;\n z-index: 3;\n transform-origin: 0 0;\n}\n.ui-video-seek-slider .track .main.with-gap .inner-seek-block,\n.ui-video-seek-slider .track .main.with-gap:before {\n width: calc(100% - 2px);\n margin: 0 auto;\n}\n@media (hover) {\n .ui-video-seek-slider .track .main:hover:before {\n height: 8px;\n }\n .ui-video-seek-slider .track .main:hover .inner-seek-block {\n height: 8px;\n }\n}\n.ui-video-seek-slider .thumb {\n pointer-events: none;\n position: absolute;\n width: 12px;\n height: 12px;\n left: -6px;\n z-index: 4;\n top: 3px;\n}\n.ui-video-seek-slider .thumb .handler {\n border-radius: 100%;\n width: 100%;\n height: 100%;\n background-color: #ff0000;\n opacity: 0;\n transform: scale(0.4);\n transition: transform 0.2s, opacity 0.2s;\n}\n.ui-video-seek-slider .thumb.active .handler {\n opacity: 1;\n transform: scale(1);\n}\n.ui-video-seek-slider .hover-time {\n text-shadow: 1px 1px 1px #000;\n position: absolute;\n line-height: 18px;\n font-size: 16px;\n color: #ddd;\n bottom: 5px;\n left: 0;\n padding: 5px 10px;\n opacity: 0;\n pointer-events: none;\n text-align: center;\n}\n.ui-video-seek-slider .hover-time.active {\n opacity: 1;\n}\n.ui-video-seek-slider .hover-time .preview-screen {\n background-repeat: no-repeat;\n background-size: cover;\n background-position: center;\n width: 200px;\n height: 110px;\n border-radius: 5px;\n background-color: #000;\n margin: 0 auto 10px;\n}\n.ui-video-seek-slider:hover .track .main .seek-hover {\n opacity: 1;\n}\n";
436
- styleInject(css_248z$2,{"insertAt":"top"});
437
-
438
- const BottomControls = ({ config }) => {
439
- const { videoRef, currentTime, isFullscreen, bufferedProgress } = useVideoStore();
440
- const duration = videoRef?.duration;
645
+ // Memoized time formatter to prevent unnecessary recalculations
646
+ const formatTimeMemo = (() => {
647
+ const cache = new Map();
648
+ return (seconds) => {
649
+ if (cache.has(seconds)) {
650
+ return cache.get(seconds);
651
+ }
652
+ const formatted = timeFormat(seconds);
653
+ cache.set(seconds, formatted);
654
+ // Limit cache size to prevent memory leaks
655
+ if (cache.size > 100) {
656
+ const firstKey = cache.keys().next().value;
657
+ if (firstKey !== undefined) {
658
+ cache.delete(firstKey);
659
+ }
660
+ }
661
+ return formatted;
662
+ };
663
+ })();
664
+ const BottomControls = memo(({ config }) => {
665
+ const { videoRef, currentTime, isFullscreen, bufferedProgress, isAdPlaying } = useVideoStore(useShallow((state) => ({
666
+ videoRef: state.videoRef,
667
+ currentTime: state.currentTime,
668
+ isFullscreen: state.isFullscreen,
669
+ bufferedProgress: state.bufferedProgress,
670
+ isAdPlaying: state.isAdPlaying,
671
+ })));
672
+ const duration = videoRef?.duration ?? 0;
673
+ const currentTimeValue = currentTime || 0;
674
+ const bufferedValue = bufferedProgress || 0;
675
+ const handleSeek = useCallback((currentTimeInMs) => {
676
+ if (!videoRef) {
677
+ return;
678
+ }
679
+ videoRef.currentTime = currentTimeInMs / 1000;
680
+ }, [videoRef]);
681
+ const bufferTime = useMemo(() => {
682
+ if (!duration) {
683
+ return 0;
684
+ }
685
+ return secondsToMilliseconds(duration * (bufferedValue / 100));
686
+ }, [bufferedValue, duration]);
687
+ // Round to nearest second for time display to reduce re-renders
688
+ const roundedCurrentTime = useMemo(() => Math.floor(currentTimeValue), [currentTimeValue]);
689
+ const roundedDuration = useMemo(() => Math.floor(duration), [duration]);
690
+ const durationFormatted = useMemo(() => formatTimeMemo(roundedDuration), [roundedDuration]);
691
+ const currentTimeFormatted = useMemo(() => formatTimeMemo(roundedCurrentTime), [roundedCurrentTime]);
692
+ // Memoize seek slider props to prevent unnecessary re-renders
693
+ const seekSliderMax = useMemo(() => secondsToMilliseconds(duration), [duration]);
694
+ const seekSliderCurrentTime = useMemo(() => secondsToMilliseconds(currentTimeValue), [currentTimeValue]);
695
+ if (isAdPlaying) {
696
+ return null;
697
+ }
441
698
  return (React__default.createElement("div", { className: "px-10" },
442
- React__default.createElement(VideoSeekSlider, { max: secondsToMilliseconds(duration || 0), currentTime: secondsToMilliseconds(currentTime || 0), bufferTime: secondsToMilliseconds((duration || 0) * (bufferedProgress / 100)), onChange: (currentTime) => {
443
- if (videoRef) {
444
- videoRef.currentTime = currentTime / 1000;
445
- }
446
- }, secondsPrefix: "00:00:", minutesPrefix: "00:", getPreviewScreenUrl: config?.seekBarConfig?.getPreviewScreenUrl, timeCodes: config?.seekBarConfig?.timeCodes, trackColor: config?.seekBarConfig?.trackColor }),
699
+ 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 }),
447
700
  React__default.createElement("div", { className: `pt-6 ${isFullscreen ? "pb-10" : "pb-16"} lg:pb-12 flex items-center gap-4 text-white` },
448
- React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-white cursor-pointer hover:text-gray-200 transition-colors duration-200" }, timeFormat(currentTime || 0)),
701
+ 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),
449
702
  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" }, "/"),
450
- 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" }, timeFormat(duration || 0)))));
451
- };
452
-
453
- const Tooltip = ({ children, title, position = "top", }) => {
454
- const [visible, setVisible] = useState(false);
455
- const positionStyles = {
456
- top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
457
- bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
458
- left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
459
- right: "left-full top-1/2 transform -translate-y-1/2 ml-2",
460
- };
461
- return (React__default.createElement("div", { className: "relative inline-block cursor-pointer", onMouseEnter: () => setVisible(true), onMouseLeave: () => setVisible(false) },
462
- children,
463
- 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))));
464
- };
703
+ 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))));
704
+ });
705
+ BottomControls.displayName = "BottomControls";
465
706
 
466
- var css_248z$1 = ".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/* styles/fullscreen-toggle.css */\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/* styles/pi-picture-in-picture-toggle.css */\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";
467
- styleInject(css_248z$1,{"insertAt":"top"});
707
+ 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}";
708
+ styleInject(css_248z$2,{"insertAt":"top"});
468
709
 
469
710
  const FullScreenToggle = ({ isFullScreen, onClick, className = "fullscreen-icon", }) => {
470
711
  return (React__default.createElement("div", { onClick: onClick }, isFullScreen ? (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 25 25", className: className },
@@ -531,47 +772,111 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
531
772
  children))));
532
773
  };
533
774
 
775
+ const Tooltip = ({ children, title, position = "top", className, }) => {
776
+ const [visible, setVisible] = useState(false);
777
+ const positionStyles = {
778
+ top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
779
+ bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
780
+ left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
781
+ right: "left-full top-1/2 transform -translate-y-1/2 ml-2",
782
+ };
783
+ return (React__default.createElement("div", { className: `relative inline-block cursor-pointer ${className || ""}`, onMouseEnter: () => setVisible(true), onMouseLeave: () => setVisible(false) },
784
+ children,
785
+ 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))));
786
+ };
787
+
534
788
  const Settings = ({ iconClassName }) => {
535
- const { qualityLevels, hlsInstance, setActiveQuality, activeQuality, subtitles, activeSubtitle, setActiveSubtitle, videoRef, } = useVideoStore();
536
- const [speed, setSpeed] = React.useState(1);
789
+ const { qualityLevels, activeQuality, currentQuality, subtitles, activeSubtitle, setActiveSubtitle, videoRef, streamType, } = useVideoStore();
790
+ // Load playback speed from localStorage or default to 1
791
+ const getStoredPlaybackSpeed = () => {
792
+ try {
793
+ const stored = localStorage.getItem("react-player-playback-speed");
794
+ if (stored) {
795
+ const speed = parseFloat(stored);
796
+ if (speedOptions.includes(speed)) {
797
+ return speed;
798
+ }
799
+ }
800
+ }
801
+ catch (_error) {
802
+ // Ignore localStorage errors
803
+ }
804
+ return 1;
805
+ };
806
+ const [speed, setSpeed] = React.useState(getStoredPlaybackSpeed());
537
807
  const [activeMenu, setActiveMenu] = React.useState("main");
808
+ // Initialize playback speed from localStorage on mount
809
+ React.useEffect(() => {
810
+ if (videoRef) {
811
+ const storedSpeed = getStoredPlaybackSpeed();
812
+ videoRef.playbackRate = storedSpeed;
813
+ setSpeed(storedSpeed);
814
+ }
815
+ }, [videoRef]);
538
816
  const handleSpeedChange = (newSpeed) => {
539
817
  setSpeed(newSpeed);
540
818
  if (videoRef) {
541
819
  videoRef.playbackRate = newSpeed;
542
820
  }
821
+ // Persist to localStorage
822
+ try {
823
+ localStorage.setItem("react-player-playback-speed", newSpeed.toString());
824
+ }
825
+ catch (_error) {
826
+ // Ignore localStorage errors
827
+ }
543
828
  };
544
- const uniqueQualityLevels = React.useMemo(() => {
545
- if (!qualityLevels)
829
+ const isAdaptiveStream = streamType === "hls" || streamType === "dash";
830
+ const qualityOptions = React.useMemo(() => {
831
+ if (!qualityLevels || !isAdaptiveStream) {
546
832
  return [];
547
- const seenHeights = new Set();
548
- const unique = [];
549
- qualityLevels.forEach((level, originalIndex) => {
550
- if (seenHeights.has(level.height))
551
- return;
552
- seenHeights.add(level.height);
553
- unique.push({
554
- height: level.height,
555
- bitrate: level.bitrate,
556
- originalIndex,
557
- });
833
+ }
834
+ const prefix = streamType === "dash" ? "dash" : "hls";
835
+ return [...qualityLevels]
836
+ .map((level) => ({
837
+ value: `${prefix}-${level.originalIndex}`,
838
+ height: level.height,
839
+ bitrate: level.bitrate,
840
+ originalIndex: level.originalIndex,
841
+ }))
842
+ .sort((a, b) => {
843
+ const heightDiff = (b.height || 0) - (a.height || 0);
844
+ if (heightDiff !== 0)
845
+ return heightDiff;
846
+ const bitrateDiff = (b.bitrate || 0) - (a.bitrate || 0);
847
+ if (bitrateDiff !== 0)
848
+ return bitrateDiff;
849
+ return b.originalIndex - a.originalIndex;
558
850
  });
559
- return unique;
560
- }, [qualityLevels]);
851
+ }, [qualityLevels, isAdaptiveStream, streamType]);
561
852
  const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
562
853
  const handleBack = () => setActiveMenu("main");
854
+ const formatBitrate = (bitrate) => {
855
+ if (!bitrate || bitrate <= 0)
856
+ return "";
857
+ if (bitrate >= 1000000) {
858
+ return `${(bitrate / 1000000).toFixed(1)} Mbps`;
859
+ }
860
+ return `${Math.round(bitrate / 1000)} Kbps`;
861
+ };
862
+ // Get quality label: show explicit resolution to avoid duplicates
863
+ const getQualityName = (height, bitrate) => {
864
+ if (height && height > 0)
865
+ return `${height}p`;
866
+ const bitrateLabel = formatBitrate(bitrate);
867
+ return bitrateLabel || "Quality";
868
+ };
563
869
  // Get quality label for display
564
870
  const getQualityLabel = () => {
565
- if (activeQuality === "auto")
871
+ if (!isAdaptiveStream)
566
872
  return "Auto";
567
- const level = uniqueQualityLevels.find((l) => String(l.height) === activeQuality);
568
- return level ? `${level.height}p` : "Auto";
569
- };
570
- // Get quality label: show explicit resolution to avoid duplicates
571
- const getQualityName = (height) => {
572
- if (!height || height <= 0)
873
+ if (currentQuality === "auto")
874
+ return "Auto";
875
+ const option = qualityOptions.find((q) => q.value === currentQuality);
876
+ if (!option)
573
877
  return "Auto";
574
- return `${height}p`;
878
+ const label = getQualityName(option.height, option.bitrate);
879
+ return label === "Quality" ? "Custom" : label;
575
880
  };
576
881
  // Get estimated data usage using bitrate when available
577
882
  const getDataUsage = (height, bitrate) => {
@@ -639,33 +944,28 @@ const Settings = ({ iconClassName }) => {
639
944
  React.createElement("h3", { className: "text-white font-bold text-xl" }, "Video Quality")),
640
945
  React.createElement("div", { className: "space-y-3" },
641
946
  React.createElement("button", { onClick: () => {
642
- if (hlsInstance) {
643
- hlsInstance.currentLevel = -1;
644
- setActiveQuality("auto");
947
+ if (isAdaptiveStream) {
948
+ QualityManager.setQuality(streamType, "auto");
645
949
  }
646
- }, className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
950
+ }, disabled: !isAdaptiveStream, className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
647
951
  ? "bg-white/10"
648
- : "hover:bg-white/5"}` },
952
+ : isAdaptiveStream
953
+ ? "hover:bg-white/5"
954
+ : "opacity-50 cursor-not-allowed"}` },
649
955
  React.createElement("div", { className: "flex items-start justify-between" },
650
956
  React.createElement("div", null,
651
957
  React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, "Auto"),
652
958
  React.createElement("div", { className: "text-gray-400 text-sm" }, "Adjust to your connection")),
653
959
  activeQuality === "auto" && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" })))),
654
- uniqueQualityLevels
655
- .map((level) => (React.createElement("button", { key: level.originalIndex, onClick: () => {
656
- if (hlsInstance) {
657
- hlsInstance.currentLevel = level.originalIndex;
658
- setActiveQuality(String(level.height));
659
- }
660
- }, className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === String(level.height)
960
+ 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
661
961
  ? "bg-white/10"
662
962
  : "hover:bg-white/5"}` },
663
963
  React.createElement("div", { className: "flex items-start justify-between" },
664
964
  React.createElement("div", null,
665
- React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, getQualityName(level.height)),
965
+ React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, getQualityName(level.height, level.bitrate)),
666
966
  React.createElement("div", { className: "text-gray-400 text-sm" }, getDataUsage(level.height, level.bitrate))),
667
- activeQuality === String(level.height) && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" }))))))
668
- .reverse()))),
967
+ (activeQuality === level.value ||
968
+ 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."))))),
669
969
  activeMenu === "subtitles" && (React.createElement("div", { className: "p-4" },
670
970
  React.createElement("div", { className: "flex items-center gap-3 mb-4" },
671
971
  React.createElement("button", { onClick: handleBack, className: "p-1 hover:bg-white/10 rounded-md transition-colors" },
@@ -692,7 +992,50 @@ const Settings = ({ iconClassName }) => {
692
992
 
693
993
  const ControlsHeader = ({ config }) => {
694
994
  const iconClassName = "icon-button";
695
- const { videoWrapperRef, videoRef, episodeList, currentEpisodeIndex, resetStore, } = useVideoStore();
995
+ const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, adCurrentTime, } = useVideoStore(useShallow((state) => ({
996
+ videoWrapperRef: state.videoWrapperRef,
997
+ videoRef: state.videoRef,
998
+ adVideoRef: state.adVideoRef,
999
+ episodeList: state.episodeList,
1000
+ currentEpisodeIndex: state.currentEpisodeIndex,
1001
+ resetStore: state.resetStore,
1002
+ isAdPlaying: state.isAdPlaying,
1003
+ muted: state.muted,
1004
+ setMuted: state.setMuted,
1005
+ adCurrentTime: state.adCurrentTime,
1006
+ })));
1007
+ const [adDuration, setAdDuration] = React.useState(0);
1008
+ React.useEffect(() => {
1009
+ if (!adVideoRef || !isAdPlaying) {
1010
+ setAdDuration(0);
1011
+ return;
1012
+ }
1013
+ const updateDuration = () => {
1014
+ if (adVideoRef.duration && Number.isFinite(adVideoRef.duration)) {
1015
+ setAdDuration(adVideoRef.duration);
1016
+ }
1017
+ };
1018
+ updateDuration();
1019
+ adVideoRef.addEventListener("loadedmetadata", updateDuration);
1020
+ adVideoRef.addEventListener("durationchange", updateDuration);
1021
+ return () => {
1022
+ adVideoRef.removeEventListener("loadedmetadata", updateDuration);
1023
+ adVideoRef.removeEventListener("durationchange", updateDuration);
1024
+ };
1025
+ }, [adVideoRef, isAdPlaying]);
1026
+ const formatTime = React.useCallback((seconds) => {
1027
+ if (isNaN(seconds) || seconds < 0)
1028
+ return "0:00";
1029
+ const mins = Math.floor(seconds / 60);
1030
+ const secs = Math.floor(seconds % 60);
1031
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1032
+ }, []);
1033
+ const adTimeRemaining = React.useMemo(() => {
1034
+ if (adDuration <= 0 || adCurrentTime <= 0)
1035
+ return null;
1036
+ const remaining = Math.max(0, adDuration - adCurrentTime);
1037
+ return formatTime(remaining);
1038
+ }, [adDuration, adCurrentTime, formatTime]);
696
1039
  const [isPipActive, setIsPipActive] = React.useState(false);
697
1040
  const [isFullscreen, setIsFullscreen] = React.useState(false);
698
1041
  const handleFullscreen = () => {
@@ -715,9 +1058,17 @@ const ControlsHeader = ({ config }) => {
715
1058
  };
716
1059
  }, []);
717
1060
  const handleMute = () => {
718
- if (videoRef) {
719
- videoRef.muted = !videoRef.muted;
1061
+ const targetElement = isAdPlaying ? adVideoRef ?? videoRef : videoRef;
1062
+ if (!targetElement)
1063
+ return;
1064
+ const nextMuted = !targetElement.muted;
1065
+ if (videoRef && videoRef.muted !== nextMuted) {
1066
+ videoRef.muted = nextMuted;
1067
+ }
1068
+ if (adVideoRef && adVideoRef.muted !== nextMuted) {
1069
+ adVideoRef.muted = nextMuted;
720
1070
  }
1071
+ setMuted(nextMuted);
721
1072
  };
722
1073
  const handlePipToggle = async () => {
723
1074
  if (!videoRef)
@@ -732,9 +1083,7 @@ const ControlsHeader = ({ config }) => {
732
1083
  setIsPipActive(false);
733
1084
  }
734
1085
  }
735
- catch (error) {
736
- console.error("PiP toggle failed:", error);
737
- }
1086
+ catch (_error) { }
738
1087
  };
739
1088
  React.useEffect(() => {
740
1089
  const handlePipChange = () => setIsPipActive(!!document.pictureInPictureElement);
@@ -751,16 +1100,21 @@ const ControlsHeader = ({ config }) => {
751
1100
  config.onClose();
752
1101
  }
753
1102
  };
754
- return (React.createElement("div", { className: "flex items-center justify-between p-10 bg-gradient-to-b from-black" },
755
- React.createElement("div", { className: "flex" },
756
- React.createElement("div", null,
757
- React.createElement("h1", { className: "text-gray-200 text-lg lg:text-2xl font-semibold" }, episodeList.length > 0
758
- ? episodeList[currentEpisodeIndex]?.title
759
- : config?.title),
760
- config?.isTrailer && (React.createElement("p", { className: "text-gray-300 text-sm lg:text-base font-normal" }, "Trailer")))),
1103
+ const renderAdHeader = () => (React.createElement("div", { className: "flex items-center gap-4" },
1104
+ 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" },
1105
+ React.createElement("span", null, "Ad"),
1106
+ adTimeRemaining && (React.createElement("span", { className: "text-gray-300 font-normal normal-case ml-1 text-xs" }, adTimeRemaining)))));
1107
+ const renderVideoHeader = () => (React.createElement("div", { className: "flex" },
1108
+ React.createElement("div", null,
1109
+ React.createElement("h1", { className: "text-gray-200 text-lg lg:text-2xl font-semibold" }, episodeList.length > 0
1110
+ ? episodeList[currentEpisodeIndex]?.title
1111
+ : config?.title),
1112
+ config?.isTrailer && (React.createElement("p", { className: "text-gray-300 text-sm lg:text-base font-normal" }, "Trailer")))));
1113
+ return (React.createElement("div", { className: "flex items-center justify-between p-10 bg-linear-to-b from-black" },
1114
+ isAdPlaying ? renderAdHeader() : renderVideoHeader(),
761
1115
  React.createElement("div", { className: "flex items-center gap-7 text-white" },
762
- React.createElement(Settings, { iconClassName: iconClassName }),
763
- React.createElement("div", { onClick: handleMute }, videoRef?.muted ? (React.createElement(Tooltip, { title: "Unmute" },
1116
+ !isAdPlaying && React.createElement(Settings, { iconClassName: iconClassName }),
1117
+ React.createElement("div", { onClick: handleMute }, muted ? (React.createElement(Tooltip, { title: "Unmute" },
764
1118
  React.createElement(IoVolumeMuteOutline, { className: iconClassName }))) : (React.createElement(Tooltip, { title: "Mute" },
765
1119
  React.createElement(IoVolumeHighOutline, { className: iconClassName })))),
766
1120
  React.createElement(Tooltip, { title: isPipActive
@@ -770,9 +1124,9 @@ const ControlsHeader = ({ config }) => {
770
1124
  : "Fullscreen", className: `${iconClassName} ${isPipActive ? "opacity-50 cursor-not-allowed" : ""}` },
771
1125
  React.createElement("div", { onClick: handleFullscreen, className: isPipActive ? "pointer-events-none" : "" },
772
1126
  React.createElement(FullScreenToggle, { isFullScreen: isFullscreen, className: iconClassName }))),
773
- React.createElement(Tooltip, { className: "whitespace-nowrap", title: isPipActive ? "Exit PiP" : "Enter PiP" },
1127
+ !isAdPlaying && (React.createElement(Tooltip, { className: "whitespace-nowrap", title: isPipActive ? "Exit PiP" : "Enter PiP" },
774
1128
  React.createElement("div", { onClick: handlePipToggle },
775
- React.createElement(PiPictureInPictureToggle, { className: iconClassName }))),
1129
+ React.createElement(PiPictureInPictureToggle, { className: iconClassName })))),
776
1130
  config?.onClose && (React.createElement(React.Fragment, null,
777
1131
  React.createElement("div", { className: "w-[2px] h-10 bg-gray-500 hover:bg-gray-300 mx-2" }),
778
1132
  React.createElement("div", { onClick: handleClose },
@@ -780,55 +1134,113 @@ const ControlsHeader = ({ config }) => {
780
1134
  React.createElement(IoMdClose, { className: iconClassName }))))))));
781
1135
  };
782
1136
 
783
- const ControlButton = ({ onClick, icon, className, }) => (React__default.createElement("button", { onClick: onClick, className: `flex justify-center items-center h-full cursor-pointer ${className}` }, icon));
1137
+ const CONTROL_INTERACTION_EVENT = "video-controls:interaction";
1138
+ const CONTROLS_HIDE_DELAY_MS = 3000;
1139
+ const SKIP_INTERVAL_SECONDS = 10;
1140
+
1141
+ const BackwardIcon = memo(() => (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
1142
+ React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M33.5 0C52 0 67 15 67 33.5S52 67 33.5 67 0 52 0 33.5c.03-1.4 1.17-2.53 2.58-2.53 1.4 0 2.55 1.13 2.57 2.53 0 15.65 12.7 28.35 28.35 28.35 15.66 0 28.35-12.7 28.35-28.35 0-15.66-12.69-28.35-28.35-28.35h-.04c-7 0-13.76 2.61-18.94 7.3-.46.42-.91.85-1.34 1.29h6.58c1.42 0 2.57 1.16 2.57 2.58 0 1.42-1.15 2.58-2.57 2.58H6.01c-1.42 0-2.57-1.16-2.57-2.58V2.58C3.44 1.15 4.59 0 6.01 0c1.43 0 2.58 1.15 2.58 2.58v8.52c.78-.86 1.61-1.7 2.47-2.47A33.407 33.407 0 0 1 33.46 0h.04zM33.98 41.34c-1.6-2.21-2-5.2-2-7.85 0-2.65.4-5.63 2-7.83 1.44-1.97 3.47-2.84 5.88-2.84 2.41 0 4.42.87 5.86 2.84 1.61 2.21 2.03 5.16 2.03 7.83 0 2.66-.4 5.64-2 7.85-1.43 1.97-3.47 2.84-5.89 2.84-2.41 0-4.45-.86-5.88-2.84zm-9.73-12.77l-5 1.58v-4.21l5.87-2.65h4.28v20.47h-5.15V28.57zm17.61 9.96c.61-1.33.68-3.6.68-5.04s-.07-3.7-.68-5.02c-.4-.86-1.04-1.29-2-1.29-.95 0-1.59.42-1.99 1.29-.61 1.32-.68 3.58-.68 5.02 0 1.44.07 3.71.68 5.04.4.87 1.04 1.29 1.99 1.29.96 0 1.6-.42 2-1.29z" }))));
1143
+ BackwardIcon.displayName = "BackwardIcon";
1144
+ const ForwardIcon = memo(() => (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
1145
+ React__default.createElement("path", { fillRule: "evenodd", d: "M33.5 0C15 0 0 15 0 33.5S15 67 33.5 67 67 52 67 33.5a2.583 2.583 0 0 0-2.58-2.53c-1.4 0-2.55 1.13-2.57 2.53 0 15.66-12.69 28.35-28.35 28.35-15.65 0-28.35-12.7-28.35-28.35 0-15.66 12.7-28.35 28.35-28.35 7.3 0 13.96 2.76 18.99 7.3.46.42.9.85 1.34 1.29h-6.59a2.58 2.58 0 0 0 0 5.16h13.75c1.42 0 2.57-1.16 2.57-2.58V2.58c0-1.43-1.15-2.58-2.57-2.58-1.43 0-2.58 1.15-2.58 2.58v8.52c-.78-.87-1.61-1.7-2.47-2.48A33.446 33.446 0 0 0 33.54 0h-.04zM33.98 41.34c-1.6-2.21-2-5.2-2-7.85 0-2.65.4-5.63 2-7.83 1.44-1.97 3.47-2.84 5.88-2.84 2.41 0 4.42.87 5.86 2.84 1.61 2.21 2.03 5.16 2.03 7.83 0 2.66-.4 5.64-2 7.85-1.43 1.97-3.47 2.84-5.89 2.84-2.41 0-4.45-.87-5.88-2.84zm-9.73-12.77l-5 1.58v-4.21l5.87-2.65h4.28v20.47h-5.15V28.57zm17.61 9.96c.61-1.33.68-3.6.68-5.04s-.07-3.7-.68-5.02c-.4-.87-1.04-1.29-2-1.29-.95 0-1.59.42-1.99 1.29-.61 1.32-.68 3.58-.68 5.02 0 1.44.07 3.71.68 5.04.4.86 1.04 1.28 1.99 1.28.96 0 1.6-.42 2-1.28z" }))));
1146
+ ForwardIcon.displayName = "ForwardIcon";
1147
+ const PauseIcon = memo(() => (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
1148
+ React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M46.332 5.773a4.125 4.125 0 0 0-4.125 4.125v46.75a4.127 4.127 0 0 0 4.125 4.125 4.127 4.127 0 0 0 4.125-4.125V9.898a4.125 4.125 0 0 0-4.125-4.125zM25.707 9.898v46.75a4.125 4.125 0 1 1-8.25 0V9.898a4.123 4.123 0 0 1 4.125-4.125 4.123 4.123 0 0 1 4.125 4.125z" }))));
1149
+ PauseIcon.displayName = "PauseIcon";
1150
+ const PlayIcon = memo(() => (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
1151
+ React__default.createElement("path", { d: "M20.28 9.65c-2.205-1.268-4.026-.228-4.026 2.307v43.805c0 2.535 1.82 3.574 4.027 2.307l38.471-21.903a2.556 2.556 0 0 0 1.094-.935 2.514 2.514 0 0 0 0-2.743 2.556 2.556 0 0 0-1.093-.936L20.28 9.65z" }))));
1152
+ PlayIcon.displayName = "PlayIcon";
1153
+ const ControlButtonComponent = ({ onClick, icon, className, }) => (React__default.createElement("button", { onClick: onClick, className: `flex justify-center items-center h-full cursor-pointer ${className}` }, icon));
1154
+ const ControlButton = memo(ControlButtonComponent);
1155
+ ControlButton.displayName = "ControlButton";
784
1156
  const MiddleControls = () => {
785
- const { videoRef, isPlaying, setIsPlaying } = useVideoStore();
786
- const [isBuffering, setIsBuffering] = useState(false);
787
- const videoElement = videoRef;
1157
+ const { videoRef, adVideoRef, isPlaying, setIsPlaying, isAdPlaying } = useVideoStore(useShallow((state) => ({
1158
+ videoRef: state.videoRef,
1159
+ adVideoRef: state.adVideoRef,
1160
+ isPlaying: state.isPlaying,
1161
+ setIsPlaying: state.setIsPlaying,
1162
+ isAdPlaying: state.isAdPlaying,
1163
+ })));
1164
+ const { setIsBuffering } = useVideoStore(useShallow((state) => ({
1165
+ setIsBuffering: state.setIsBuffering,
1166
+ })));
1167
+ const [isBuffering, setIsBufferingLocal] = useState(false);
1168
+ const videoElement = isAdPlaying ? adVideoRef : videoRef;
1169
+ const resetControlsVisibility = useCallback(() => {
1170
+ if (typeof window === "undefined") {
1171
+ return;
1172
+ }
1173
+ window.dispatchEvent(new Event(CONTROL_INTERACTION_EVENT));
1174
+ }, []);
788
1175
  const handlePlayPause = useCallback(() => {
789
1176
  if (!videoElement)
790
1177
  return;
791
1178
  if (videoElement.paused) {
792
1179
  videoElement
793
1180
  .play()
794
- .then(() => setIsPlaying(true))
795
- .catch((err) => console.error("Error playing video:", err));
1181
+ .then(() => {
1182
+ setIsPlaying(true);
1183
+ resetControlsVisibility();
1184
+ })
1185
+ .catch(() => undefined);
796
1186
  }
797
1187
  else {
798
1188
  videoElement.pause();
799
1189
  setIsPlaying(false);
1190
+ resetControlsVisibility();
800
1191
  }
801
- }, [videoElement, setIsPlaying]);
1192
+ }, [videoElement, setIsPlaying, resetControlsVisibility]);
802
1193
  const handleBackward = useCallback(() => {
803
1194
  if (!videoElement)
804
1195
  return;
805
- videoElement.currentTime = Math.max(0, videoElement.currentTime - 10);
806
- }, [videoElement]);
1196
+ videoElement.currentTime = Math.max(0, videoElement.currentTime - SKIP_INTERVAL_SECONDS);
1197
+ resetControlsVisibility();
1198
+ }, [videoElement, resetControlsVisibility]);
807
1199
  const handleForward = useCallback(() => {
808
1200
  if (!videoElement)
809
1201
  return;
810
- videoElement.currentTime = Math.min(videoElement.duration, videoElement.currentTime + 10);
811
- }, [videoElement]);
1202
+ videoElement.currentTime = Math.min(videoElement.duration, videoElement.currentTime + SKIP_INTERVAL_SECONDS);
1203
+ resetControlsVisibility();
1204
+ }, [videoElement, resetControlsVisibility]);
812
1205
  useEffect(() => {
813
1206
  if (!videoElement)
814
1207
  return;
815
- const handleWaiting = () => setIsBuffering(true);
816
- const handlePlaying = () => setIsBuffering(false);
1208
+ const handleWaiting = () => {
1209
+ setIsBufferingLocal(true);
1210
+ setIsBuffering(true);
1211
+ };
1212
+ const handlePlaying = () => {
1213
+ setIsBufferingLocal(false);
1214
+ setIsBuffering(false);
1215
+ };
1216
+ const handleCanPlay = () => {
1217
+ setIsBufferingLocal(false);
1218
+ setIsBuffering(false);
1219
+ };
1220
+ const handleStalled = () => {
1221
+ setIsBufferingLocal(true);
1222
+ setIsBuffering(true);
1223
+ };
817
1224
  videoElement.addEventListener("waiting", handleWaiting);
818
1225
  videoElement.addEventListener("playing", handlePlaying);
1226
+ videoElement.addEventListener("canplay", handleCanPlay);
1227
+ videoElement.addEventListener("stalled", handleStalled);
819
1228
  return () => {
820
1229
  videoElement.removeEventListener("waiting", handleWaiting);
821
1230
  videoElement.removeEventListener("playing", handlePlaying);
1231
+ videoElement.removeEventListener("canplay", handleCanPlay);
1232
+ videoElement.removeEventListener("stalled", handleStalled);
822
1233
  };
823
- }, [videoElement]);
1234
+ }, [videoElement, isAdPlaying, setIsBuffering]);
824
1235
  useEffect(() => {
825
1236
  const handleKeyDown = (e) => {
826
- if (!videoElement)
1237
+ if (!videoElement || isAdPlaying)
827
1238
  return;
828
1239
  switch (e.code) {
829
1240
  case "Space":
830
1241
  e.preventDefault();
831
1242
  handlePlayPause();
1243
+ resetControlsVisibility();
832
1244
  break;
833
1245
  case "ArrowLeft":
834
1246
  e.preventDefault();
@@ -842,15 +1254,22 @@ const MiddleControls = () => {
842
1254
  };
843
1255
  window.addEventListener("keydown", handleKeyDown);
844
1256
  return () => window.removeEventListener("keydown", handleKeyDown);
845
- }, [videoElement, handlePlayPause, handleBackward, handleForward]);
1257
+ }, [
1258
+ videoElement,
1259
+ handlePlayPause,
1260
+ handleBackward,
1261
+ handleForward,
1262
+ isAdPlaying,
1263
+ ]);
1264
+ if (isAdPlaying) {
1265
+ return (React__default.createElement("div", { className: "flex justify-center items-center" },
1266
+ React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement("div", { className: "relative" },
1267
+ 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)) })));
1268
+ }
846
1269
  return (React__default.createElement("div", { className: "flex justify-center items-center" },
847
- React__default.createElement(ControlButton, { onClick: handleBackward, className: "w-[15vw]", icon: React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
848
- React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M33.5 0C52 0 67 15 67 33.5S52 67 33.5 67 0 52 0 33.5c.03-1.4 1.17-2.53 2.58-2.53 1.4 0 2.55 1.13 2.57 2.53 0 15.65 12.7 28.35 28.35 28.35 15.66 0 28.35-12.7 28.35-28.35 0-15.66-12.69-28.35-28.35-28.35h-.04c-7 0-13.76 2.61-18.94 7.3-.46.42-.91.85-1.34 1.29h6.58c1.42 0 2.57 1.16 2.57 2.58 0 1.42-1.15 2.58-2.57 2.58H6.01c-1.42 0-2.57-1.16-2.57-2.58V2.58C3.44 1.15 4.59 0 6.01 0c1.43 0 2.58 1.15 2.58 2.58v8.52c.78-.86 1.61-1.7 2.47-2.47A33.407 33.407 0 0 1 33.46 0h.04zM33.98 41.34c-1.6-2.21-2-5.2-2-7.85 0-2.65.4-5.63 2-7.83 1.44-1.97 3.47-2.84 5.88-2.84 2.41 0 4.42.87 5.86 2.84 1.61 2.21 2.03 5.16 2.03 7.83 0 2.66-.4 5.64-2 7.85-1.43 1.97-3.47 2.84-5.89 2.84-2.41 0-4.45-.86-5.88-2.84zm-9.73-12.77l-5 1.58v-4.21l5.87-2.65h4.28v20.47h-5.15V28.57zm17.61 9.96c.61-1.33.68-3.6.68-5.04s-.07-3.7-.68-5.02c-.4-.86-1.04-1.29-2-1.29-.95 0-1.59.42-1.99 1.29-.61 1.32-.68 3.58-.68 5.02 0 1.44.07 3.71.68 5.04.4.87 1.04 1.29 1.99 1.29.96 0 1.6-.42 2-1.29z" })) }),
849
- 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("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
850
- React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M46.332 5.773a4.125 4.125 0 0 0-4.125 4.125v46.75a4.127 4.127 0 0 0 4.125 4.125 4.127 4.127 0 0 0 4.125-4.125V9.898a4.125 4.125 0 0 0-4.125-4.125zM25.707 9.898v46.75a4.125 4.125 0 1 1-8.25 0V9.898a4.123 4.123 0 0 1 4.125-4.125 4.123 4.123 0 0 1 4.125 4.125z" }))) : (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
851
- React__default.createElement("path", { d: "M20.28 9.65c-2.205-1.268-4.026-.228-4.026 2.307v43.805c0 2.535 1.82 3.574 4.027 2.307l38.471-21.903a2.556 2.556 0 0 0 1.094-.935 2.514 2.514 0 0 0 0-2.743 2.556 2.556 0 0 0-1.093-.936L20.28 9.65z" }))) }),
852
- React__default.createElement(ControlButton, { onClick: handleForward, className: "w-[15vw]", icon: React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "icon-class", fill: "currentColor", viewBox: "0 0 67 67" },
853
- React__default.createElement("path", { fillRule: "evenodd", d: "M33.5 0C15 0 0 15 0 33.5S15 67 33.5 67 67 52 67 33.5a2.583 2.583 0 0 0-2.58-2.53c-1.4 0-2.55 1.13-2.57 2.53 0 15.66-12.69 28.35-28.35 28.35-15.65 0-28.35-12.7-28.35-28.35 0-15.66 12.7-28.35 28.35-28.35 7.3 0 13.96 2.76 18.99 7.3.46.42.9.85 1.34 1.29h-6.59a2.58 2.58 0 0 0 0 5.16h13.75c1.42 0 2.57-1.16 2.57-2.58V2.58c0-1.43-1.15-2.58-2.57-2.58-1.43 0-2.58 1.15-2.58 2.58v8.52c-.78-.87-1.61-1.7-2.47-2.48A33.446 33.446 0 0 0 33.54 0h-.04zM33.98 41.34c-1.6-2.21-2-5.2-2-7.85 0-2.65.4-5.63 2-7.83 1.44-1.97 3.47-2.84 5.88-2.84 2.41 0 4.42.87 5.86 2.84 1.61 2.21 2.03 5.16 2.03 7.83 0 2.66-.4 5.64-2 7.85-1.43 1.97-3.47 2.84-5.89 2.84-2.41 0-4.45-.87-5.88-2.84zm-9.73-12.77l-5 1.58v-4.21l5.87-2.65h4.28v20.47h-5.15V28.57zm17.61 9.96c.61-1.33.68-3.6.68-5.04s-.07-3.7-.68-5.02c-.4-.87-1.04-1.29-2-1.29-.95 0-1.59.42-1.99 1.29-.61 1.32-.68 3.58-.68 5.02 0 1.44.07 3.71.68 5.04.4.86 1.04 1.28 1.99 1.28.96 0 1.6-.42 2-1.28z" })) })));
1270
+ React__default.createElement(ControlButton, { onClick: handleBackward, className: "w-[15vw]", icon: React__default.createElement(BackwardIcon, null) }),
1271
+ 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)) }),
1272
+ React__default.createElement(ControlButton, { onClick: handleForward, className: "w-[15vw]", icon: React__default.createElement(ForwardIcon, null) })));
854
1273
  };
855
1274
 
856
1275
  const VideoPlayerControls = ({ config }) => {
@@ -861,7 +1280,7 @@ const VideoPlayerControls = ({ config }) => {
861
1280
  React.createElement(BottomControls, { config: config?.bottomConfig?.config }))));
862
1281
  };
863
1282
 
864
- const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "left", }) => {
1283
+ const VideoActionButton = React__default.memo(({ text, onClick, icon, disabled = false, position = "left", }) => {
865
1284
  // Increase icon size and apply consistent color to icon
866
1285
  const renderedIcon = icon
867
1286
  ? React__default.cloneElement(icon, {
@@ -872,33 +1291,45 @@ const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "
872
1291
  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 " },
873
1292
  renderedIcon && React__default.createElement("span", { className: "inline mr-2" }, renderedIcon),
874
1293
  text)));
875
- };
1294
+ });
1295
+ VideoActionButton.displayName = "VideoActionButton";
876
1296
 
877
- const Overlay = ({ config }) => {
1297
+ const Overlay = React__default.memo(({ config }) => {
878
1298
  const controlsTimerRef = useRef(null);
879
- const { setControls, controls, showCountdown, countdownTime, setShowCountdown, setAutoPlayNext, setCurrentEpisodeIndex, episodeList, setCountdownTime, videoRef, currentEpisodeIndex, } = useVideoStore();
1299
+ const containerRef = useRef(null);
1300
+ const { setControls, controls, showCountdown, countdownTime, setShowCountdown, setAutoPlayNext, setCurrentEpisodeIndex, episodeList, setCountdownTime, videoRef, currentEpisodeIndex, isAdPlaying, } = useVideoStore(useShallow((state) => ({
1301
+ setControls: state.setControls,
1302
+ controls: state.controls,
1303
+ showCountdown: state.showCountdown,
1304
+ countdownTime: state.countdownTime,
1305
+ setShowCountdown: state.setShowCountdown,
1306
+ setAutoPlayNext: state.setAutoPlayNext,
1307
+ setCurrentEpisodeIndex: state.setCurrentEpisodeIndex,
1308
+ episodeList: state.episodeList,
1309
+ setCountdownTime: state.setCountdownTime,
1310
+ videoRef: state.videoRef,
1311
+ currentEpisodeIndex: state.currentEpisodeIndex,
1312
+ isAdPlaying: state.isAdPlaying,
1313
+ })));
880
1314
  const { onClose } = config?.headerConfig?.config || {};
881
- const HIDE_DELAY = 2000;
1315
+ const hideControls = useCallback(() => {
1316
+ setControls(false);
1317
+ containerRef.current?.classList.add("noCursor");
1318
+ }, [setControls]);
882
1319
  const resetControlsTimer = useCallback(() => {
883
1320
  if (controlsTimerRef.current) {
884
1321
  clearTimeout(controlsTimerRef.current);
885
1322
  }
886
- controlsTimerRef.current = setTimeout(() => {
887
- setControls(false);
888
- const videoPlayerControls = document?.getElementById("videoPlayerControls");
889
- if (videoPlayerControls) {
890
- videoPlayerControls.classList.add("noCursor");
891
- }
892
- }, HIDE_DELAY);
893
- }, [setControls]);
894
- const handleMouseEnter = useCallback(() => {
895
- const videoPlayerControls = document?.getElementById("videoPlayerControls");
896
- if (videoPlayerControls) {
897
- videoPlayerControls.classList.remove("noCursor");
898
- }
1323
+ controlsTimerRef.current = setTimeout(hideControls, CONTROLS_HIDE_DELAY_MS);
1324
+ }, [hideControls]);
1325
+ const handleControlsInteraction = useCallback(() => {
1326
+ containerRef.current?.classList.remove("noCursor");
899
1327
  setControls(true);
900
1328
  resetControlsTimer();
901
- }, [setControls, resetControlsTimer]);
1329
+ }, [resetControlsTimer, setControls]);
1330
+ const handleMouseEnter = useCallback(() => {
1331
+ handleControlsInteraction();
1332
+ }, [handleControlsInteraction]);
902
1333
  useEffect(() => {
903
1334
  return () => {
904
1335
  if (controlsTimerRef.current) {
@@ -910,7 +1341,9 @@ const Overlay = ({ config }) => {
910
1341
  let timer;
911
1342
  if (showCountdown && countdownTime > 0 && episodeList.length > 0) {
912
1343
  timer = setInterval(() => {
913
- setCountdownTime(countdownTime - 1);
1344
+ const currentTime = useVideoStore.getState().countdownTime;
1345
+ const next = currentTime - 1;
1346
+ setCountdownTime(next > 0 ? next : 0);
914
1347
  }, 1000);
915
1348
  }
916
1349
  return () => {
@@ -918,87 +1351,115 @@ const Overlay = ({ config }) => {
918
1351
  clearInterval(timer);
919
1352
  };
920
1353
  }, [showCountdown, countdownTime, episodeList.length, setCountdownTime]);
921
- const handleNextEpisodeManually = () => {
1354
+ useEffect(() => {
1355
+ if (typeof window === "undefined") {
1356
+ return;
1357
+ }
1358
+ const handleExternalInteraction = () => {
1359
+ handleControlsInteraction();
1360
+ };
1361
+ window.addEventListener(CONTROL_INTERACTION_EVENT, handleExternalInteraction);
1362
+ return () => {
1363
+ window.removeEventListener(CONTROL_INTERACTION_EVENT, handleExternalInteraction);
1364
+ };
1365
+ }, [handleControlsInteraction]);
1366
+ const handleNextEpisodeManually = useCallback(() => {
922
1367
  const nextIndex = currentEpisodeIndex + 1;
923
1368
  if (nextIndex < episodeList.length && videoRef && episodeList[nextIndex]) {
924
1369
  setCurrentEpisodeIndex(nextIndex);
925
1370
  setAutoPlayNext(true);
926
1371
  videoRef.src = episodeList[nextIndex].url;
927
- videoRef
928
- .play()
929
- .catch((err) => console.error("Manual play failed:", err));
1372
+ videoRef.play().catch(() => undefined);
930
1373
  setShowCountdown(false);
931
1374
  setCountdownTime(10);
932
- setControls(true);
933
- resetControlsTimer();
1375
+ handleControlsInteraction();
934
1376
  }
935
1377
  else if (onClose) {
936
1378
  onClose();
937
1379
  }
938
- };
939
- return (React.createElement("div", { id: "videoPlayerControls", className: "absolute inset-0", onMouseMove: handleMouseEnter },
940
- controls && React.createElement(VideoPlayerControls, { config: config }),
941
- showCountdown &&
942
- episodeList.length > 0 &&
943
- currentEpisodeIndex + 1 < episodeList.length && (React.createElement(VideoActionButton, { text: "Next Episode", onClick: handleNextEpisodeManually, icon: React.createElement(ArrowRight, { className: "h-5 w-5 text-gray-900" }), disabled: currentEpisodeIndex + 1 >= episodeList.length, position: "right" }))));
944
- };
1380
+ }, [
1381
+ currentEpisodeIndex,
1382
+ episodeList,
1383
+ videoRef,
1384
+ setCurrentEpisodeIndex,
1385
+ setAutoPlayNext,
1386
+ setShowCountdown,
1387
+ setCountdownTime,
1388
+ handleControlsInteraction,
1389
+ onClose,
1390
+ ]);
1391
+ // Memoize countdown display to prevent unnecessary re-renders
1392
+ const shouldShowCountdown = useMemo(() => showCountdown &&
1393
+ episodeList.length > 0 &&
1394
+ currentEpisodeIndex + 1 < episodeList.length, [showCountdown, episodeList.length, currentEpisodeIndex]);
1395
+ return (React__default.createElement("div", { id: "videoPlayerControls", ref: containerRef, className: "absolute inset-0", onMouseMove: handleMouseEnter },
1396
+ controls && !isAdPlaying && React__default.createElement(VideoPlayerControls, { config: config }),
1397
+ 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" }))));
1398
+ });
1399
+ Overlay.displayName = "Overlay";
945
1400
 
946
- const SubtitleOverlay = ({ styleConfig }) => {
947
- const { videoRef, activeSubtitle } = useVideoStore();
1401
+ const SubtitleOverlay = React__default.memo(({ styleConfig }) => {
1402
+ const { videoRef, activeSubtitle } = useVideoStore(useShallow((state) => ({
1403
+ videoRef: state.videoRef,
1404
+ activeSubtitle: state.activeSubtitle,
1405
+ })));
948
1406
  const [currentSubtitle, setCurrentSubtitle] = useState("");
949
1407
  const [isVisible, setIsVisible] = useState(false);
1408
+ const rafRef = useRef(null);
950
1409
  useEffect(() => {
951
1410
  if (!videoRef)
952
1411
  return;
953
1412
  const handleTimeUpdate = () => {
954
- if (!activeSubtitle) {
955
- setCurrentSubtitle("");
956
- setIsVisible(false);
1413
+ if (rafRef.current !== null)
957
1414
  return;
958
- }
959
- const currentTime = videoRef.currentTime;
960
- const textTracks = Array.from(videoRef.textTracks);
961
- console.log("Available text tracks:", textTracks.map((t) => ({
962
- label: t.label,
963
- mode: t.mode,
964
- cues: t.cues?.length,
965
- })));
966
- const activeTrack = textTracks.find((track) => track.mode === "showing" && track.label === activeSubtitle.label);
967
- console.log("Active track found:", !!activeTrack, "Current time:", currentTime);
968
- if (activeTrack && activeTrack.cues) {
969
- const activeCues = Array.from(activeTrack.cues).filter((cue) => currentTime >= cue.startTime && currentTime <= cue.endTime);
970
- if (activeCues.length > 0) {
971
- const cue = activeCues[0];
972
- let cueText = "";
973
- try {
974
- if ("text" in cue) {
975
- cueText = cue.text;
976
- }
977
- else if (typeof cue.getCueAsHTML === "function") {
978
- const htmlElement = cue.getCueAsHTML();
979
- cueText =
980
- htmlElement?.textContent || htmlElement?.innerText || "";
1415
+ rafRef.current = requestAnimationFrame(() => {
1416
+ rafRef.current = null;
1417
+ if (!activeSubtitle) {
1418
+ setCurrentSubtitle("");
1419
+ setIsVisible(false);
1420
+ return;
1421
+ }
1422
+ const currentTime = videoRef.currentTime;
1423
+ const textTracks = Array.from(videoRef.textTracks);
1424
+ const activeTrack = textTracks.find((track) => track.mode === "showing" && track.label === activeSubtitle.label);
1425
+ if (activeTrack && activeTrack.cues) {
1426
+ const activeCues = Array.from(activeTrack.cues).filter((cue) => currentTime >= cue.startTime && currentTime <= cue.endTime);
1427
+ if (activeCues.length > 0) {
1428
+ const cue = activeCues[0];
1429
+ let cueText = "";
1430
+ try {
1431
+ if ("text" in cue) {
1432
+ cueText = cue.text;
1433
+ }
1434
+ else if (typeof cue.getCueAsHTML === "function") {
1435
+ const htmlElement = cue.getCueAsHTML();
1436
+ cueText =
1437
+ htmlElement?.textContent || htmlElement?.innerText || "";
1438
+ }
1439
+ else {
1440
+ cueText = cue.toString() || "";
1441
+ }
981
1442
  }
982
- else {
983
- cueText = cue.toString() || "";
1443
+ catch (_error) {
1444
+ cueText = "";
984
1445
  }
1446
+ setCurrentSubtitle(cueText);
1447
+ setIsVisible(!!cueText);
985
1448
  }
986
- catch (error) {
987
- console.warn("Error getting subtitle text:", error);
988
- cueText = "";
1449
+ else {
1450
+ setCurrentSubtitle("");
1451
+ setIsVisible(false);
989
1452
  }
990
- setCurrentSubtitle(cueText);
991
- setIsVisible(!!cueText);
992
- }
993
- else {
994
- setCurrentSubtitle("");
995
- setIsVisible(false);
996
1453
  }
997
- }
1454
+ });
998
1455
  };
999
1456
  videoRef.addEventListener("timeupdate", handleTimeUpdate);
1000
1457
  return () => {
1001
1458
  videoRef.removeEventListener("timeupdate", handleTimeUpdate);
1459
+ if (rafRef.current !== null) {
1460
+ cancelAnimationFrame(rafRef.current);
1461
+ rafRef.current = null;
1462
+ }
1002
1463
  };
1003
1464
  }, [videoRef, activeSubtitle]);
1004
1465
  useEffect(() => {
@@ -1055,246 +1516,518 @@ const SubtitleOverlay = ({ styleConfig }) => {
1055
1516
  pointerEvents: "none",
1056
1517
  };
1057
1518
  return React__default.createElement("div", { style: subtitleStyle }, currentSubtitle);
1058
- };
1519
+ });
1520
+ SubtitleOverlay.displayName = "SubtitleOverlay";
1059
1521
 
1060
- /**
1061
- * Video Source Hook
1062
- *
1063
- * Manages video source loading and streaming technology detection
1064
- * Supports HLS.js, DASH.js, and native HTML5 video
1065
- *
1066
- * Features:
1067
- * - Automatic stream type detection
1068
- * - HLS.js fallback for older browsers
1069
- * - DASH.js support with proper initialization
1070
- * - Quality level extraction for all stream types
1071
- * - Error handling and cleanup
1072
- */
1073
- const useVideoSource = (trackSrc, type) => {
1074
- const { videoRef, setQualityLevels, setHlsInstance, setDashInstance, setStreamType } = useVideoStore();
1522
+ const HLS_CONFIG = {
1523
+ enableWorker: true,
1524
+ lowLatencyMode: false,
1525
+ backBufferLength: 90,
1526
+ liveSyncDurationCount: 3,
1527
+ maxBufferSize: 80 * 1000000,
1528
+ maxBufferLength: 30,
1529
+ manifestLoadingMaxRetry: 4,
1530
+ manifestLoadingRetryDelay: 1000,
1531
+ levelLoadingMaxRetry: 4,
1532
+ levelLoadingRetryDelay: 1000,
1533
+ fragLoadingMaxRetry: 6,
1534
+ fragLoadingRetryDelay: 750,
1535
+ startLevel: -1,
1536
+ startPosition: -1,
1537
+ capLevelToPlayerSize: true,
1538
+ };
1539
+ const DASH_SETTINGS = {
1540
+ streaming: {
1541
+ abr: {
1542
+ autoSwitchBitrate: {
1543
+ video: true,
1544
+ audio: true,
1545
+ },
1546
+ limitBitrateByPortal: true,
1547
+ ABRStrategy: "abrThroughput",
1548
+ bandwidthSafetyFactor: 0.9,
1549
+ },
1550
+ buffer: {
1551
+ fastSwitchEnabled: true,
1552
+ bufferTimeAtTopQuality: 28,
1553
+ bufferTimeAtTopQualityLongForm: 55,
1554
+ },
1555
+ lowLatencyEnabled: false,
1556
+ },
1557
+ debug: {
1558
+ logLevel: dashjs.Debug.LOG_LEVEL_NONE,
1559
+ },
1560
+ };
1561
+ const MAX_HLS_NETWORK_RETRIES = 4;
1562
+ const MAX_DASH_RESTARTS = 3;
1563
+ const sanitizeUrl = (url) => {
1564
+ if (!url)
1565
+ return "";
1566
+ return url.split("#")[0]?.split("?")[0] ?? url;
1567
+ };
1568
+ const resolveStreamType = (explicitType, source) => {
1569
+ if (explicitType === "hls" ||
1570
+ explicitType === "dash" ||
1571
+ explicitType === "mp4") {
1572
+ return explicitType;
1573
+ }
1574
+ if (explicitType === "youtube" || explicitType === "other") {
1575
+ return "other";
1576
+ }
1577
+ const sanitized = sanitizeUrl(source).toLowerCase();
1578
+ const extension = getExtensionFromUrl(sanitized);
1579
+ if (extension === "hls")
1580
+ return "hls";
1581
+ if (extension === "dash")
1582
+ return "dash";
1583
+ if (extension === "mp4")
1584
+ return "mp4";
1585
+ if (sanitized.includes(".m3u8"))
1586
+ return "hls";
1587
+ if (sanitized.includes(".mpd"))
1588
+ return "dash";
1589
+ if (sanitized.includes(".mp4"))
1590
+ return "mp4";
1591
+ return "other";
1592
+ };
1593
+ const useHlsEngine = ({ enabled, source, videoElement, setHlsInstance, setQualityLevels, setCurrentQuality, }) => {
1594
+ const networkRetryRef = useRef(0);
1595
+ const retryTimerRef = useRef(undefined);
1075
1596
  useEffect(() => {
1076
- if (!videoRef)
1077
- return;
1078
- const getVideoExtension = getExtensionFromUrl(trackSrc);
1079
- const contentType = type || getVideoExtension;
1080
- // Set stream type in store for quality manager
1081
- setStreamType(contentType);
1082
- // Handle MP4 and other simple formats
1083
- if (contentType === "mp4" || contentType === "other") {
1084
- videoRef.src = trackSrc;
1085
- setQualityLevels([]);
1597
+ if (!enabled || !videoElement) {
1086
1598
  return;
1087
1599
  }
1088
- // Handle HLS streams
1089
- if (contentType === "hls") {
1090
- // Native HLS support (Safari/iOS)
1091
- if (videoRef?.canPlayType("application/vnd.apple.mpegurl")) {
1092
- console.log('📱 Using native HLS support');
1093
- videoRef.src = trackSrc;
1094
- // For native HLS, we can't control quality directly, but we can still extract info
1095
- const handleLoadedMetadata = () => {
1096
- const videoElement = videoRef;
1097
- if (videoElement.videoTracks && videoElement.videoTracks.length > 0) {
1098
- // Extract quality levels from native HLS
1099
- const tracks = Array.from(videoElement.videoTracks).map((track, index) => ({
1100
- height: track.height || 720,
1101
- bitrate: track.bandwidth || 0,
1102
- originalIndex: index
1600
+ networkRetryRef.current = 0;
1601
+ setQualityLevels([]);
1602
+ setCurrentQuality("auto");
1603
+ const clearRetryTimer = () => {
1604
+ if (retryTimerRef.current) {
1605
+ window.clearTimeout(retryTimerRef.current);
1606
+ retryTimerRef.current = undefined;
1607
+ }
1608
+ };
1609
+ const attachNative = () => {
1610
+ setHlsInstance(null);
1611
+ videoElement.src = source;
1612
+ videoElement.load();
1613
+ const handleLoadedMetadata = () => {
1614
+ try {
1615
+ const mediaTracks = videoElement?.videoTracks;
1616
+ if (mediaTracks && mediaTracks.length > 0) {
1617
+ const levels = Array.from(mediaTracks).map((track, index) => ({
1618
+ height: track.height ?? 0,
1619
+ bitrate: track.bandwidth ?? 0,
1620
+ originalIndex: index,
1103
1621
  }));
1104
- setQualityLevels(tracks);
1105
- console.log('✅ Native HLS quality levels:', tracks);
1106
- }
1107
- else {
1108
- // Fallback quality levels for native HLS
1109
- const defaultLevels = [
1110
- { height: 360, bitrate: 800000, originalIndex: 0 },
1111
- { height: 480, bitrate: 1400000, originalIndex: 1 },
1112
- { height: 720, bitrate: 2800000, originalIndex: 2 },
1113
- { height: 1080, bitrate: 5000000, originalIndex: 3 },
1114
- ];
1115
- setQualityLevels(defaultLevels);
1116
- console.log('✅ Native HLS fallback quality levels:', defaultLevels);
1622
+ setQualityLevels(levels);
1117
1623
  }
1118
- // Even for native HLS, set a mock HLS instance to indicate it's HLS
1119
- // This allows the quality manager to know we're dealing with HLS
1120
- setHlsInstance(null); // null indicates native HLS, not HLS.js
1121
- };
1122
- videoRef.addEventListener('loadedmetadata', handleLoadedMetadata);
1123
- return () => {
1124
- videoRef.removeEventListener('loadedmetadata', handleLoadedMetadata);
1125
- };
1624
+ }
1625
+ catch (_error) {
1626
+ setQualityLevels([]);
1627
+ }
1628
+ };
1629
+ videoElement.addEventListener("loadedmetadata", handleLoadedMetadata);
1630
+ return () => {
1631
+ videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata);
1632
+ };
1633
+ };
1634
+ if (!Hls.isSupported() &&
1635
+ videoElement.canPlayType("application/vnd.apple.mpegurl")) {
1636
+ return attachNative();
1637
+ }
1638
+ if (!Hls.isSupported()) {
1639
+ setHlsInstance(null);
1640
+ videoElement.src = source;
1641
+ videoElement.load();
1642
+ return;
1643
+ }
1644
+ const hls = new Hls(HLS_CONFIG);
1645
+ setHlsInstance(hls);
1646
+ const updateQualityLevels = () => {
1647
+ const levels = hls.levels ?? [];
1648
+ const parsedLevels = levels.map((level, index) => ({
1649
+ height: level.height ?? 0,
1650
+ bitrate: level.bitrate ?? 0,
1651
+ originalIndex: index,
1652
+ }));
1653
+ setQualityLevels(parsedLevels);
1654
+ };
1655
+ const handleManifestParsed = () => {
1656
+ networkRetryRef.current = 0;
1657
+ updateQualityLevels();
1658
+ const { activeQuality } = useVideoStore.getState();
1659
+ if (activeQuality && activeQuality.startsWith("hls-")) {
1660
+ const levelIndex = parseInt(activeQuality.replace("hls-", ""), 10);
1661
+ if (!Number.isNaN(levelIndex) && levelIndex >= 0) {
1662
+ hls.loadLevel = levelIndex;
1663
+ hls.nextLevel = levelIndex;
1664
+ hls.currentLevel = levelIndex;
1665
+ setCurrentQuality(activeQuality);
1666
+ return;
1667
+ }
1126
1668
  }
1127
- // HLS.js support (Chrome/Firefox/etc)
1128
- else if (Hls.isSupported()) {
1129
- console.log('🔧 Using HLS.js for HLS streaming');
1130
- const hls = new Hls({
1131
- // HLS.js configuration for optimal performance
1132
- enableWorker: true,
1133
- lowLatencyMode: true,
1134
- backBufferLength: 90
1135
- });
1136
- hls.loadSource(trackSrc);
1137
- hls.attachMedia(videoRef);
1138
- console.log('✅ HLS.js instance created and attached');
1139
- setHlsInstance(hls);
1140
- // Extract quality levels when manifest is parsed
1141
- hls.on(Hls.Events.MANIFEST_PARSED, () => {
1142
- const levels = hls.levels.map((level, index) => ({
1143
- height: level.height,
1144
- bitrate: level.bitrate,
1145
- originalIndex: index
1146
- }));
1147
- setQualityLevels(levels);
1148
- console.log('✅ HLS.js quality levels:', levels);
1149
- });
1150
- // Log level switches for debugging
1151
- hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
1152
- console.log('🔄 HLS level switched to:', data.level, hls.levels?.[data.level]);
1153
- });
1154
- // Error handling
1155
- hls.on(Hls.Events.ERROR, (event, data) => {
1156
- console.error('❌ HLS.js error:', data);
1157
- });
1158
- // Cleanup
1159
- return () => {
1160
- hls.destroy();
1161
- console.log('🧹 HLS.js instance destroyed');
1162
- };
1669
+ setCurrentQuality("auto");
1670
+ hls.currentLevel = -1;
1671
+ hls.loadLevel = -1;
1672
+ hls.nextLevel = -1;
1673
+ };
1674
+ const handleLevelsUpdated = () => {
1675
+ updateQualityLevels();
1676
+ };
1677
+ const handleLevelSwitched = (_event, data) => {
1678
+ if (typeof data?.level === "number" && data.level >= 0) {
1679
+ setCurrentQuality(`hls-${data.level}`);
1163
1680
  }
1164
- else {
1165
- // Fallback when HLS.js is not supported
1166
- console.log('📱 Using fallback HLS (direct src)');
1167
- videoRef.src = trackSrc;
1168
- setHlsInstance(null); // null indicates native HLS fallback
1169
- // Set fallback quality levels
1170
- const defaultLevels = [
1171
- { height: 360, bitrate: 800000, originalIndex: 0 },
1172
- { height: 480, bitrate: 1400000, originalIndex: 1 },
1173
- { height: 720, bitrate: 2800000, originalIndex: 2 },
1174
- { height: 1080, bitrate: 5000000, originalIndex: 3 },
1175
- ];
1176
- setQualityLevels(defaultLevels);
1177
- console.log('✅ HLS fallback quality levels:', defaultLevels);
1178
- }
1179
- }
1180
- // Handle DASH streams
1181
- else if (contentType === "dash") {
1182
- // DASH.js support
1183
- if (dashjs.supportsMediaSource()) {
1184
- console.log('🔧 Using DASH.js for DASH streaming');
1185
- const player = dashjs.MediaPlayer().create();
1186
- // DASH.js configuration for optimal performance
1187
- player.updateSettings({
1188
- streaming: {
1189
- buffer: {
1190
- fastSwitchEnabled: true, // Enable fast quality switching
1191
- bufferTimeAtTopQuality: 30, // Buffer 30s at top quality
1192
- bufferTimeAtTopQualityLongForm: 60 // Buffer 60s for long content
1193
- },
1194
- // Note: Some ABR settings may vary by DASH.js version
1195
- // Check documentation for your specific version
1196
- }
1197
- });
1198
- player.initialize(videoRef, trackSrc, true);
1199
- console.log('✅ DASH.js instance created and initialized');
1200
- setDashInstance(player);
1201
- // Extract quality levels when manifest is loaded
1202
- const handleManifestLoaded = () => {
1203
- try {
1204
- const representations = player.getRepresentationsByType('video');
1205
- if (representations && representations.length > 0) {
1206
- const levels = representations.map((rep, index) => ({
1207
- height: rep.height || Math.round(rep.bandwidth / 1000) || 0,
1208
- bitrate: rep.bandwidth,
1209
- originalIndex: index,
1210
- id: rep.id
1211
- }));
1212
- setQualityLevels(levels);
1213
- console.log('✅ DASH.js quality levels:', levels);
1681
+ };
1682
+ const scheduleRestart = () => {
1683
+ if (networkRetryRef.current >= MAX_HLS_NETWORK_RETRIES) {
1684
+ return;
1685
+ }
1686
+ const delay = Math.min(2000 * (networkRetryRef.current + 1), 10000);
1687
+ clearRetryTimer();
1688
+ retryTimerRef.current = window.setTimeout(() => {
1689
+ try {
1690
+ hls.startLoad();
1691
+ }
1692
+ catch (_err) {
1693
+ // Ignore
1694
+ }
1695
+ }, delay);
1696
+ networkRetryRef.current += 1;
1697
+ };
1698
+ const handleError = (_event, data) => {
1699
+ if (!data)
1700
+ return;
1701
+ const HLS_ERROR_TYPES = Hls.ErrorTypes ?? {};
1702
+ if (data.fatal) {
1703
+ switch (data.type) {
1704
+ case HLS_ERROR_TYPES.NETWORK_ERROR ?? "networkError":
1705
+ scheduleRestart();
1706
+ break;
1707
+ case HLS_ERROR_TYPES.MEDIA_ERROR ?? "mediaError":
1708
+ try {
1709
+ hls.recoverMediaError();
1214
1710
  }
1215
- else {
1216
- console.warn('⚠️ No DASH video representations found');
1217
- setQualityLevels([]);
1711
+ catch (_err) {
1712
+ scheduleRestart();
1218
1713
  }
1219
- }
1220
- catch (error) {
1221
- console.error('❌ Error getting DASH quality levels:', error);
1222
- setQualityLevels([]);
1223
- }
1224
- };
1225
- // Listen for manifest loaded event
1226
- player.on('manifestLoaded', handleManifestLoaded);
1227
- // Log quality changes for debugging
1228
- player.on('qualityChange', (e) => {
1229
- console.log('🔄 DASH quality changed to:', e.newQuality, e);
1230
- });
1231
- // Error handling
1232
- player.on('error', (e) => {
1233
- console.error('❌ DASH.js error:', e);
1234
- });
1235
- // Cleanup
1236
- return () => {
1237
- player.reset();
1238
- console.log('🧹 DASH.js instance reset');
1239
- };
1714
+ break;
1715
+ default:
1716
+ clearRetryTimer();
1717
+ hls.destroy();
1718
+ setHlsInstance(null);
1719
+ break;
1720
+ }
1240
1721
  }
1241
- else {
1242
- console.warn('⚠️ DASH.js not supported in this browser');
1722
+ else if (data.type === (HLS_ERROR_TYPES.NETWORK_ERROR ?? "networkError")) {
1723
+ scheduleRestart();
1243
1724
  }
1244
- }
1245
- // Fallback for unsupported formats
1246
- videoRef.src = trackSrc;
1247
- setQualityLevels([]);
1248
- }, [trackSrc, videoRef, type, setQualityLevels, setHlsInstance, setDashInstance, setStreamType]);
1249
- };
1250
-
1251
- const useSubtitles = (subtitles) => {
1252
- const { videoRef, activeSubtitle, setSubtitles } = useVideoStore();
1253
- useEffect(() => {
1254
- if (videoRef) {
1255
- const tracks = videoRef.getElementsByTagName("track");
1256
- while (tracks.length > 0) {
1257
- videoRef.removeChild(tracks[0]);
1725
+ };
1726
+ hls.attachMedia(videoElement);
1727
+ hls.loadSource(source);
1728
+ const HLS_EVENTS = Hls.Events ?? {};
1729
+ hls.on(HLS_EVENTS.MANIFEST_PARSED ?? "manifestParsed", handleManifestParsed);
1730
+ hls.on(HLS_EVENTS.LEVELS_UPDATED ?? "levelsUpdated", handleLevelsUpdated);
1731
+ hls.on(HLS_EVENTS.LEVEL_SWITCHED ?? "levelSwitched", handleLevelSwitched);
1732
+ hls.on(HLS_EVENTS.ERROR ?? "error", handleError);
1733
+ return () => {
1734
+ clearRetryTimer();
1735
+ hls.off(HLS_EVENTS.MANIFEST_PARSED ?? "manifestParsed", handleManifestParsed);
1736
+ hls.off(HLS_EVENTS.LEVELS_UPDATED ?? "levelsUpdated", handleLevelsUpdated);
1737
+ hls.off(HLS_EVENTS.LEVEL_SWITCHED ?? "levelSwitched", handleLevelSwitched);
1738
+ hls.off(HLS_EVENTS.ERROR ?? "error", handleError);
1739
+ hls.destroy();
1740
+ setHlsInstance(null);
1741
+ setQualityLevels([]);
1742
+ setCurrentQuality("auto");
1743
+ };
1744
+ }, [
1745
+ enabled,
1746
+ source,
1747
+ videoElement,
1748
+ setHlsInstance,
1749
+ setQualityLevels,
1750
+ setCurrentQuality,
1751
+ ]);
1752
+ };
1753
+ const useDashEngine = ({ enabled, source, videoElement, setDashInstance, setQualityLevels, setCurrentQuality, }) => {
1754
+ const restartCountRef = useRef(0);
1755
+ const restartTimerRef = useRef(undefined);
1756
+ useEffect(() => {
1757
+ if (!enabled || !videoElement) {
1758
+ return;
1759
+ }
1760
+ if (!dashjs.supportsMediaSource()) {
1761
+ setDashInstance(null);
1762
+ setQualityLevels([]);
1763
+ videoElement.src = source;
1764
+ videoElement.load();
1765
+ return;
1766
+ }
1767
+ restartCountRef.current = 0;
1768
+ setQualityLevels([]);
1769
+ setCurrentQuality("auto");
1770
+ const player = dashjs.MediaPlayer().create();
1771
+ setDashInstance(player);
1772
+ const dashPlayer = player;
1773
+ const clearRestartTimer = () => {
1774
+ if (restartTimerRef.current) {
1775
+ window.clearTimeout(restartTimerRef.current);
1776
+ restartTimerRef.current = undefined;
1258
1777
  }
1259
- Array.from(videoRef.textTracks).forEach((track) => {
1260
- track.mode = "disabled";
1261
- });
1262
- if (activeSubtitle && subtitles) {
1263
- const index = subtitles.findIndex((s) => s.label === activeSubtitle.label);
1264
- if (index !== -1) {
1265
- const trackElement = document.createElement("track");
1266
- trackElement.kind = "subtitles";
1267
- trackElement.label = activeSubtitle.label;
1268
- trackElement.srclang = activeSubtitle.lang;
1269
- trackElement.src = activeSubtitle.url;
1270
- trackElement.default = false;
1271
- videoRef.appendChild(trackElement);
1272
- Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1273
- const handleTrackLoad = () => {
1274
- const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1275
- if (textTrack) {
1276
- textTrack.mode = "showing";
1277
- console.log("Subtitle track loaded for custom rendering:", activeSubtitle.label);
1278
- }
1279
- };
1280
- trackElement.addEventListener("load", handleTrackLoad);
1281
- const attempts = [100, 500, 1000];
1282
- attempts.forEach((delay) => {
1283
- setTimeout(() => {
1284
- const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
1285
- if (textTrack && textTrack.mode !== "showing") {
1286
- textTrack.mode = "showing";
1287
- }
1288
- }, delay);
1289
- });
1778
+ };
1779
+ const applySettings = () => {
1780
+ player.updateSettings(DASH_SETTINGS);
1781
+ };
1782
+ const updateQualityLevels = () => {
1783
+ try {
1784
+ const levels = dashPlayer.getBitrateInfoListFor?.("video") ?? [];
1785
+ const mapped = Array.from(levels).map((info) => ({
1786
+ height: info.height ?? 0,
1787
+ bitrate: info.bitrate,
1788
+ originalIndex: info.qualityIndex ?? info.index ?? 0,
1789
+ id: info.id,
1790
+ }));
1791
+ setQualityLevels(mapped);
1792
+ }
1793
+ catch (_error) {
1794
+ setQualityLevels([]);
1795
+ }
1796
+ };
1797
+ const handleStreamInitialized = () => {
1798
+ restartCountRef.current = 0;
1799
+ updateQualityLevels();
1800
+ const { activeQuality } = useVideoStore.getState();
1801
+ if (activeQuality && activeQuality.startsWith("dash-")) {
1802
+ const levelIndex = parseInt(activeQuality.replace("dash-", ""), 10);
1803
+ if (!Number.isNaN(levelIndex) && levelIndex >= 0) {
1804
+ dashPlayer.setAutoSwitchQualityFor?.("video", false);
1805
+ dashPlayer.setQualityFor?.("video", levelIndex);
1806
+ setCurrentQuality(activeQuality);
1807
+ return;
1290
1808
  }
1291
1809
  }
1810
+ dashPlayer.setAutoSwitchQualityFor?.("video", true);
1811
+ const current = dashPlayer.getQualityFor?.("video");
1812
+ if (typeof current === "number" && current >= 0) {
1813
+ setCurrentQuality(`dash-${current}`);
1814
+ }
1292
1815
  else {
1293
- Array.from(videoRef.textTracks).forEach((track) => {
1294
- track.mode = "disabled";
1816
+ setCurrentQuality("auto");
1817
+ }
1818
+ };
1819
+ const handleManifestLoaded = () => {
1820
+ updateQualityLevels();
1821
+ };
1822
+ const handleQualityRendered = (event) => {
1823
+ if (event?.mediaType === "video") {
1824
+ const current = dashPlayer.getQualityFor?.("video");
1825
+ if (typeof current === "number" && current >= 0) {
1826
+ setCurrentQuality(`dash-${current}`);
1827
+ }
1828
+ }
1829
+ };
1830
+ const bindEvents = () => {
1831
+ player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, handleStreamInitialized);
1832
+ player.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, handleManifestLoaded);
1833
+ player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, handleQualityRendered);
1834
+ player.on(dashjs.MediaPlayer.events.ERROR, handleError);
1835
+ };
1836
+ const detachEvents = () => {
1837
+ player.off(dashjs.MediaPlayer.events.STREAM_INITIALIZED, handleStreamInitialized);
1838
+ player.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, handleManifestLoaded);
1839
+ player.off(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, handleQualityRendered);
1840
+ player.off(dashjs.MediaPlayer.events.ERROR, handleError);
1841
+ };
1842
+ const restartPlayer = () => {
1843
+ if (restartCountRef.current >= MAX_DASH_RESTARTS) {
1844
+ return;
1845
+ }
1846
+ restartCountRef.current += 1;
1847
+ clearRestartTimer();
1848
+ const delay = Math.min(1500 * restartCountRef.current, 6000);
1849
+ restartTimerRef.current = window.setTimeout(() => {
1850
+ detachEvents();
1851
+ player.reset();
1852
+ setQualityLevels([]);
1853
+ applySettings();
1854
+ bindEvents();
1855
+ player.initialize(videoElement, source, videoElement.autoplay ?? false);
1856
+ }, delay);
1857
+ };
1858
+ const handleError = (event) => {
1859
+ if (!event)
1860
+ return;
1861
+ const errorToken = typeof event?.error === "string"
1862
+ ? event.error
1863
+ : typeof event?.event === "object" && event.event
1864
+ ? event.event.id
1865
+ : undefined;
1866
+ const normalized = errorToken?.toString().toLowerCase();
1867
+ const shouldRecover = normalized &&
1868
+ (normalized.includes("download") ||
1869
+ normalized.includes("manifest") ||
1870
+ normalized.includes("mediasource") ||
1871
+ normalized.includes("capability") ||
1872
+ normalized.includes("fragment"));
1873
+ if (shouldRecover) {
1874
+ restartPlayer();
1875
+ }
1876
+ };
1877
+ applySettings();
1878
+ bindEvents();
1879
+ player.initialize(videoElement, source, videoElement.autoplay ?? false);
1880
+ return () => {
1881
+ clearRestartTimer();
1882
+ detachEvents();
1883
+ player.reset();
1884
+ setDashInstance(null);
1885
+ setQualityLevels([]);
1886
+ setCurrentQuality("auto");
1887
+ };
1888
+ }, [
1889
+ enabled,
1890
+ source,
1891
+ videoElement,
1892
+ setDashInstance,
1893
+ setQualityLevels,
1894
+ setCurrentQuality,
1895
+ ]);
1896
+ };
1897
+ const useVideoSource = (trackSrc, type) => {
1898
+ const { videoRef, setQualityLevels, setHlsInstance, setDashInstance, setStreamType, setActiveQuality, setCurrentQuality, } = useVideoStore(useShallow((state) => ({
1899
+ videoRef: state.videoRef,
1900
+ setQualityLevels: state.setQualityLevels,
1901
+ setHlsInstance: state.setHlsInstance,
1902
+ setDashInstance: state.setDashInstance,
1903
+ setStreamType: state.setStreamType,
1904
+ setActiveQuality: state.setActiveQuality,
1905
+ setCurrentQuality: state.setCurrentQuality,
1906
+ })));
1907
+ const streamType = useMemo(() => resolveStreamType(type, trackSrc), [type, trackSrc]);
1908
+ useEffect(() => {
1909
+ if (!trackSrc)
1910
+ return;
1911
+ setStreamType(streamType);
1912
+ setActiveQuality("auto");
1913
+ setCurrentQuality("auto");
1914
+ setQualityLevels([]);
1915
+ }, [
1916
+ trackSrc,
1917
+ streamType,
1918
+ setStreamType,
1919
+ setActiveQuality,
1920
+ setCurrentQuality,
1921
+ setQualityLevels,
1922
+ ]);
1923
+ useEffect(() => {
1924
+ if (streamType !== "dash") {
1925
+ setDashInstance(null);
1926
+ }
1927
+ if (streamType !== "hls") {
1928
+ setHlsInstance(null);
1929
+ }
1930
+ }, [streamType, setDashInstance, setHlsInstance]);
1931
+ useEffect(() => {
1932
+ if (!videoRef)
1933
+ return;
1934
+ if (streamType === "mp4" || streamType === "other") {
1935
+ videoRef.src = trackSrc;
1936
+ videoRef.load();
1937
+ }
1938
+ else {
1939
+ // Adaptive engines will attach their own source; ensure no stale src lingers
1940
+ videoRef.removeAttribute("src");
1941
+ }
1942
+ }, [videoRef, trackSrc, streamType]);
1943
+ useHlsEngine({
1944
+ enabled: streamType === "hls",
1945
+ source: trackSrc,
1946
+ videoElement: videoRef,
1947
+ setHlsInstance,
1948
+ setQualityLevels,
1949
+ setCurrentQuality,
1950
+ });
1951
+ useDashEngine({
1952
+ enabled: streamType === "dash",
1953
+ source: trackSrc,
1954
+ videoElement: videoRef,
1955
+ setDashInstance,
1956
+ setQualityLevels,
1957
+ setCurrentQuality,
1958
+ });
1959
+ useEffect(() => {
1960
+ if (!videoRef)
1961
+ return;
1962
+ return () => {
1963
+ videoRef.pause();
1964
+ videoRef.removeAttribute("src");
1965
+ videoRef.load();
1966
+ const { setIsPlaying, setBufferedProgress } = useVideoStore.getState();
1967
+ setIsPlaying(false);
1968
+ setBufferedProgress(0);
1969
+ };
1970
+ }, [videoRef, trackSrc]);
1971
+ };
1972
+
1973
+ const useSubtitles = (subtitles) => {
1974
+ const { videoRef, activeSubtitle, setSubtitles } = useVideoStore();
1975
+ const timeoutIdsRef = useRef([]);
1976
+ useEffect(() => {
1977
+ // Clear any pending timeouts from previous effect runs
1978
+ timeoutIdsRef.current.forEach((id) => clearTimeout(id));
1979
+ timeoutIdsRef.current = [];
1980
+ if (!videoRef)
1981
+ return;
1982
+ const tracks = videoRef.getElementsByTagName("track");
1983
+ while (tracks.length > 0) {
1984
+ videoRef.removeChild(tracks[0]);
1985
+ }
1986
+ Array.from(videoRef.textTracks).forEach((track) => {
1987
+ track.mode = "disabled";
1988
+ });
1989
+ let trackElement = null;
1990
+ let handleTrackLoad = null;
1991
+ if (activeSubtitle && subtitles) {
1992
+ const index = subtitles.findIndex((s) => s.label === activeSubtitle.label);
1993
+ if (index !== -1) {
1994
+ trackElement = document.createElement("track");
1995
+ trackElement.kind = "subtitles";
1996
+ trackElement.label = activeSubtitle.label;
1997
+ trackElement.srclang = activeSubtitle.lang;
1998
+ trackElement.src = activeSubtitle.url;
1999
+ trackElement.default = false;
2000
+ videoRef.appendChild(trackElement);
2001
+ handleTrackLoad = () => {
2002
+ const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
2003
+ if (textTrack) {
2004
+ textTrack.mode = "showing";
2005
+ }
2006
+ };
2007
+ trackElement.addEventListener("load", handleTrackLoad);
2008
+ // Fallback attempts with proper cleanup tracking
2009
+ const attempts = [100, 500, 1000];
2010
+ attempts.forEach((delay) => {
2011
+ const timeoutId = setTimeout(() => {
2012
+ const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
2013
+ if (textTrack && textTrack.mode !== "showing") {
2014
+ textTrack.mode = "showing";
2015
+ }
2016
+ }, delay);
2017
+ timeoutIdsRef.current.push(timeoutId);
1295
2018
  });
1296
2019
  }
1297
2020
  }
2021
+ // Cleanup function
2022
+ return () => {
2023
+ // Clear all pending timeouts
2024
+ timeoutIdsRef.current.forEach((id) => clearTimeout(id));
2025
+ timeoutIdsRef.current = [];
2026
+ // Remove event listener if it was added
2027
+ if (trackElement && handleTrackLoad) {
2028
+ trackElement.removeEventListener("load", handleTrackLoad);
2029
+ }
2030
+ };
1298
2031
  }, [videoRef, activeSubtitle, subtitles]);
1299
2032
  useEffect(() => {
1300
2033
  if (subtitles) {
@@ -1349,29 +2082,31 @@ const useSubtitleStyling = (config) => {
1349
2082
  };
1350
2083
 
1351
2084
  const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
1352
- const { videoRef, setIsPlaying, setShowCountdown } = useVideoStore();
1353
- const startTime = useRef(null);
2085
+ const { videoRef, setShowCountdown } = useVideoStore();
1354
2086
  const isViewCounted = useRef(false);
2087
+ const lastVideoSrcRef = useRef(null);
2088
+ // Reset view count when video source changes
1355
2089
  useEffect(() => {
1356
2090
  if (!videoRef)
1357
2091
  return;
2092
+ const currentSrc = videoRef.src || videoRef.currentSrc;
2093
+ // If video source changed, reset the view count
2094
+ if (lastVideoSrcRef.current !== currentSrc) {
2095
+ isViewCounted.current = false;
2096
+ lastVideoSrcRef.current = currentSrc;
2097
+ }
2098
+ }, [videoRef?.src, videoRef?.currentSrc, videoRef]);
2099
+ useEffect(() => {
2100
+ if (!videoRef)
2101
+ return;
2102
+ // Only handle view tracking on play - setIsPlaying is handled by useVideoEvents
1358
2103
  const onPlay = () => {
1359
2104
  if (!isViewCounted.current) {
1360
2105
  isViewCounted.current = true;
1361
2106
  tracking?.onViewed?.();
1362
2107
  }
1363
- startTime.current = Date.now();
1364
- setIsPlaying(true);
1365
- };
1366
- const onPause = () => {
1367
- if (startTime.current) {
1368
- const elapsedTime = (Date.now() - startTime.current) / 1000;
1369
- const getCurrentTime = localStorage.getItem("current_time");
1370
- localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
1371
- startTime.current = null;
1372
- }
1373
- setIsPlaying(false);
1374
2108
  };
2109
+ // Handle episode end logic - playback state is handled by useVideoEvents
1375
2110
  const onEnded = () => {
1376
2111
  if (episodeList &&
1377
2112
  episodeList.length > 0 &&
@@ -1389,11 +2124,9 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
1389
2124
  }
1390
2125
  };
1391
2126
  videoRef.addEventListener("play", onPlay);
1392
- videoRef.addEventListener("pause", onPause);
1393
2127
  videoRef.addEventListener("ended", onEnded);
1394
2128
  return () => {
1395
2129
  videoRef.removeEventListener("play", onPlay);
1396
- videoRef.removeEventListener("pause", onPause);
1397
2130
  videoRef.removeEventListener("ended", onEnded);
1398
2131
  };
1399
2132
  }, [
@@ -1402,29 +2135,8 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
1402
2135
  currentEpisodeIndex,
1403
2136
  onClose,
1404
2137
  tracking,
1405
- setIsPlaying,
1406
2138
  setShowCountdown,
1407
2139
  ]);
1408
- useEffect(() => {
1409
- const handleUnload = () => {
1410
- if (startTime.current) {
1411
- const elapsedTime = (Date.now() - startTime.current) / 1000;
1412
- const getCurrentTime = localStorage.getItem("current_time");
1413
- localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
1414
- }
1415
- const totalTimeWatched = Number(localStorage.getItem("current_time") || 0);
1416
- if (totalTimeWatched >= 30) {
1417
- tracking?.onWatchTimeUpdated?.({
1418
- watchTime: totalTimeWatched,
1419
- });
1420
- }
1421
- localStorage.setItem("current_time", "0");
1422
- };
1423
- window.addEventListener("unload", handleUnload);
1424
- return () => {
1425
- window.removeEventListener("unload", handleUnload);
1426
- };
1427
- }, [tracking]);
1428
2140
  };
1429
2141
 
1430
2142
  const useIntroSkip = (intro) => {
@@ -1497,24 +2209,99 @@ const useEpisodes = (episodeList, currentEpisodeIndex, nextEpisodeConfig) => {
1497
2209
  };
1498
2210
 
1499
2211
  const useVideoEvents = () => {
1500
- const { setCurrentTime, setDuration, setBufferedProgress, setIsPlaying } = useVideoStore();
2212
+ const { setCurrentTime, setDuration, setBufferedProgress, setIsPlaying } = useVideoStore(useShallow((state) => ({
2213
+ setCurrentTime: state.setCurrentTime,
2214
+ setDuration: state.setDuration,
2215
+ setBufferedProgress: state.setBufferedProgress,
2216
+ setIsPlaying: state.setIsPlaying,
2217
+ })));
2218
+ // Cache the most recent values so we can short-circuit duplicate store writes that were causing needless renders.
2219
+ const lastTimeUpdateRef = useRef(0);
2220
+ const lastBufferedProgressRef = useRef(0);
2221
+ const pendingTimeRef = useRef(null);
2222
+ const pendingBufferedRef = useRef(null);
2223
+ const timeUpdateRafRef = useRef(null);
2224
+ const bufferedRafRef = useRef(null);
2225
+ const stopMediaElement = (media) => {
2226
+ if (!media)
2227
+ return;
2228
+ try {
2229
+ media.pause();
2230
+ }
2231
+ catch (_error) {
2232
+ // Ignored: the element might already be paused or unavailable.
2233
+ }
2234
+ try {
2235
+ media.currentTime = 0;
2236
+ }
2237
+ catch (_error) {
2238
+ // Some streams throw while seeking when metadata is missing; safe to ignore.
2239
+ }
2240
+ media.removeAttribute("src");
2241
+ media.load();
2242
+ };
2243
+ const flushPendingTimeUpdate = (time) => {
2244
+ if (timeUpdateRafRef.current !== null) {
2245
+ cancelAnimationFrame(timeUpdateRafRef.current);
2246
+ timeUpdateRafRef.current = null;
2247
+ }
2248
+ pendingTimeRef.current = null;
2249
+ lastTimeUpdateRef.current = time;
2250
+ setCurrentTime(time);
2251
+ };
2252
+ const scheduleTimeUpdate = (time) => {
2253
+ pendingTimeRef.current = time;
2254
+ if (timeUpdateRafRef.current !== null) {
2255
+ return;
2256
+ }
2257
+ // Coalesce multiple rapid events into a single rAF-aligned update to reduce layout thrash.
2258
+ timeUpdateRafRef.current = requestAnimationFrame(() => {
2259
+ timeUpdateRafRef.current = null;
2260
+ const nextTime = pendingTimeRef.current;
2261
+ if (typeof nextTime === "number") {
2262
+ pendingTimeRef.current = null;
2263
+ lastTimeUpdateRef.current = nextTime;
2264
+ setCurrentTime(nextTime);
2265
+ }
2266
+ });
2267
+ };
2268
+ const scheduleBufferedUpdate = (bufferedProgress) => {
2269
+ pendingBufferedRef.current = bufferedProgress;
2270
+ if (bufferedRafRef.current !== null) {
2271
+ return;
2272
+ }
2273
+ bufferedRafRef.current = requestAnimationFrame(() => {
2274
+ bufferedRafRef.current = null;
2275
+ const nextBuffered = pendingBufferedRef.current;
2276
+ if (typeof nextBuffered === "number") {
2277
+ pendingBufferedRef.current = null;
2278
+ lastBufferedProgressRef.current = nextBuffered;
2279
+ setBufferedProgress(nextBuffered);
2280
+ }
2281
+ });
2282
+ };
1501
2283
  const onRightClick = (e) => {
1502
2284
  e.preventDefault();
1503
2285
  };
1504
2286
  const onSeeked = (e) => {
1505
- if (e?.currentTarget?.currentTime) {
1506
- setCurrentTime(e?.currentTarget?.currentTime);
2287
+ const time = e?.currentTarget?.currentTime;
2288
+ if (typeof time === "number" && !Number.isNaN(time)) {
2289
+ flushPendingTimeUpdate(time);
1507
2290
  }
1508
2291
  };
1509
2292
  const onTimeUpdate = (e) => {
1510
- if (e?.currentTarget?.currentTime) {
1511
- setCurrentTime(e?.currentTarget?.currentTime);
2293
+ const time = e?.currentTarget?.currentTime;
2294
+ if (typeof time === "number" && !Number.isNaN(time)) {
2295
+ if (Math.abs(time - lastTimeUpdateRef.current) >= 0.1 || time === 0) {
2296
+ // Reduce the frequency of global state updates and batch them on the next animation frame for smoother playback.
2297
+ scheduleTimeUpdate(time);
2298
+ }
1512
2299
  }
1513
2300
  };
1514
2301
  const onLoadedMetadata = (e) => {
1515
- if (e?.currentTarget?.duration) {
1516
- localStorage.setItem("current_time", "0");
1517
- setDuration(e?.currentTarget?.duration);
2302
+ const duration = e?.currentTarget?.duration;
2303
+ if (typeof duration === "number" && !Number.isNaN(duration)) {
2304
+ setDuration(duration);
1518
2305
  }
1519
2306
  };
1520
2307
  const onProgress = (e) => {
@@ -1534,18 +2321,56 @@ const useVideoEvents = () => {
1534
2321
  bufferedEnd = video.buffered.end(video.buffered.length - 1);
1535
2322
  }
1536
2323
  const bufferedProgress = Math.min((bufferedEnd / video.duration) * 100, 100);
1537
- setBufferedProgress(bufferedProgress);
2324
+ if (Math.abs(bufferedProgress - lastBufferedProgressRef.current) >= 1) {
2325
+ // Skip tiny buffer deltas and dispatch the update on the next animation frame to avoid blocking the UI thread.
2326
+ scheduleBufferedUpdate(bufferedProgress);
2327
+ }
1538
2328
  }
1539
2329
  };
1540
2330
  const onPlay = () => {
1541
- setIsPlaying(true);
2331
+ const state = useVideoStore.getState();
2332
+ if (state.adVideoRef) {
2333
+ // Defensive guard: ensure any ad media tears down before the primary stream resumes so stray audio cannot continue.
2334
+ stopMediaElement(state.adVideoRef);
2335
+ state.setAdVideoRef(null);
2336
+ }
2337
+ if (state.isAdPlaying || state.currentAd) {
2338
+ state.setIsAdPlaying(false);
2339
+ state.setCurrentAd(null);
2340
+ state.setAdType(null);
2341
+ state.setAdCurrentTime(0);
2342
+ state.setCanSkipAd(false);
2343
+ state.setSkipCountdown(0);
2344
+ }
2345
+ if (!state.isPlaying) {
2346
+ setIsPlaying(true);
2347
+ }
1542
2348
  };
1543
2349
  const onPause = () => {
1544
- setIsPlaying(false);
2350
+ const state = useVideoStore.getState();
2351
+ if (state.isPlaying) {
2352
+ setIsPlaying(false);
2353
+ }
1545
2354
  };
1546
2355
  const onEnded = (e) => {
1547
- setIsPlaying(false);
2356
+ const state = useVideoStore.getState();
2357
+ if (state.isPlaying) {
2358
+ setIsPlaying(false);
2359
+ }
1548
2360
  };
2361
+ useEffect(() => {
2362
+ // Cancel any pending animation frame callbacks when the hook unmounts or dependencies change.
2363
+ return () => {
2364
+ if (timeUpdateRafRef.current !== null) {
2365
+ cancelAnimationFrame(timeUpdateRafRef.current);
2366
+ timeUpdateRafRef.current = null;
2367
+ }
2368
+ if (bufferedRafRef.current !== null) {
2369
+ cancelAnimationFrame(bufferedRafRef.current);
2370
+ bufferedRafRef.current = null;
2371
+ }
2372
+ };
2373
+ }, []);
1549
2374
  return {
1550
2375
  onRightClick,
1551
2376
  onSeeked,
@@ -1558,47 +2383,1216 @@ const useVideoEvents = () => {
1558
2383
  };
1559
2384
  };
1560
2385
 
1561
- var css_248z = ".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";
2386
+ const useAdManager = (adConfig) => {
2387
+ const { videoRef, setPlaying, setIsPlaying, currentTime, duration, isAdPlaying, setIsAdPlaying, currentAd, setCurrentAd, adType, setAdType, adVideoRef, setAdVideoRef, setAdCurrentTime, setCanSkipAd, setSkipCountdown, playedAdBreaks, addPlayedAdBreak, midRollQueue, setMidRollQueue, } = useVideoStore(useShallow((state) => ({
2388
+ videoRef: state.videoRef,
2389
+ setPlaying: state.setPlaying,
2390
+ setIsPlaying: state.setIsPlaying,
2391
+ currentTime: state.currentTime,
2392
+ duration: state.duration,
2393
+ isAdPlaying: state.isAdPlaying,
2394
+ setIsAdPlaying: state.setIsAdPlaying,
2395
+ currentAd: state.currentAd,
2396
+ setCurrentAd: state.setCurrentAd,
2397
+ adType: state.adType,
2398
+ setAdType: state.setAdType,
2399
+ adVideoRef: state.adVideoRef,
2400
+ setAdVideoRef: state.setAdVideoRef,
2401
+ setAdCurrentTime: state.setAdCurrentTime,
2402
+ setCanSkipAd: state.setCanSkipAd,
2403
+ setSkipCountdown: state.setSkipCountdown,
2404
+ playedAdBreaks: state.playedAdBreaks,
2405
+ addPlayedAdBreak: state.addPlayedAdBreak,
2406
+ midRollQueue: state.midRollQueue,
2407
+ setMidRollQueue: state.setMidRollQueue,
2408
+ })));
2409
+ const preRollPlayedRef = useRef(false);
2410
+ const postRollPlayedRef = useRef(false);
2411
+ const resumeAfterAdRef = useRef(false);
2412
+ // Track maximum time reached to prevent ad replay when seeking backward
2413
+ const maxTimeReachedRef = useRef(0);
2414
+ // Throttle ad checking to prevent performance issues
2415
+ const adCheckThrottleRef = useRef(null);
2416
+ // Track if we're currently processing an ad to prevent race conditions
2417
+ const isProcessingAdRef = useRef(false);
2418
+ const stopMediaElement = useCallback((media) => {
2419
+ if (!media)
2420
+ return;
2421
+ try {
2422
+ media.pause();
2423
+ }
2424
+ catch (_error) { }
2425
+ try {
2426
+ media.currentTime = 0;
2427
+ }
2428
+ catch (_error) { }
2429
+ media.removeAttribute("src");
2430
+ media.load();
2431
+ }, []);
2432
+ useEffect(() => {
2433
+ if (!adConfig?.midRoll || adConfig.midRoll.length === 0) {
2434
+ setMidRollQueue([]);
2435
+ return;
2436
+ }
2437
+ // Filter out invalid ads and ensure all required fields are present
2438
+ const validAds = adConfig.midRoll.filter((ad) => ad &&
2439
+ typeof ad.time === "number" &&
2440
+ ad.time >= 0 &&
2441
+ ad.time < Number.MAX_SAFE_INTEGER &&
2442
+ typeof ad.id === "string" &&
2443
+ ad.id.trim() !== "" &&
2444
+ typeof ad.adUrl === "string" &&
2445
+ ad.adUrl.trim() !== "" &&
2446
+ typeof ad.type === "string" &&
2447
+ ad.type === "mid-roll");
2448
+ if (validAds.length === 0) {
2449
+ setMidRollQueue([]);
2450
+ return;
2451
+ }
2452
+ // Sort ads by time to ensure they play in order
2453
+ const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
2454
+ // Remove duplicate IDs (keep first occurrence)
2455
+ const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
2456
+ setMidRollQueue(uniqueAds);
2457
+ }, [adConfig?.midRoll, setMidRollQueue]);
2458
+ // Removed smartPlacement - users should configure exact ad times
2459
+ // This ensures ads appear exactly when specified
2460
+ const playPreRollAd = async () => {
2461
+ if (!adConfig?.preRoll || preRollPlayedRef.current || !videoRef)
2462
+ return;
2463
+ const adBreak = adConfig.preRoll;
2464
+ preRollPlayedRef.current = true;
2465
+ resumeAfterAdRef.current = true;
2466
+ stopMediaElement(useVideoStore.getState().adVideoRef);
2467
+ videoRef.pause();
2468
+ setPlaying(false);
2469
+ setIsPlaying(false);
2470
+ setIsAdPlaying(true);
2471
+ setCurrentAd(adBreak);
2472
+ setAdType("pre-roll");
2473
+ adConfig.onAdStart?.(adBreak);
2474
+ };
2475
+ const playMidRollAd = useCallback(async (adBreak) => {
2476
+ if (!videoRef || isAdPlaying || isProcessingAdRef.current)
2477
+ return;
2478
+ // Prevent duplicate ad playback
2479
+ if (playedAdBreaks.includes(adBreak.id))
2480
+ return;
2481
+ // Mark as processing to prevent race conditions
2482
+ isProcessingAdRef.current = true;
2483
+ const updatedQueue = midRollQueue.filter((ad) => ad.id !== adBreak.id);
2484
+ setMidRollQueue(updatedQueue);
2485
+ addPlayedAdBreak(adBreak.id);
2486
+ const wasPlaying = !videoRef.paused;
2487
+ videoRef.pause();
2488
+ setPlaying(false);
2489
+ setIsPlaying(false);
2490
+ resumeAfterAdRef.current = wasPlaying;
2491
+ stopMediaElement(useVideoStore.getState().adVideoRef);
2492
+ setIsAdPlaying(true);
2493
+ setCurrentAd(adBreak);
2494
+ setAdType("mid-roll");
2495
+ adConfig?.onAdStart?.(adBreak);
2496
+ // Reset processing flag after a short delay
2497
+ setTimeout(() => {
2498
+ isProcessingAdRef.current = false;
2499
+ }, 100);
2500
+ }, [
2501
+ videoRef,
2502
+ isAdPlaying,
2503
+ playedAdBreaks,
2504
+ midRollQueue,
2505
+ setMidRollQueue,
2506
+ addPlayedAdBreak,
2507
+ setPlaying,
2508
+ setIsPlaying,
2509
+ setCurrentAd,
2510
+ setAdType,
2511
+ setIsAdPlaying,
2512
+ adConfig,
2513
+ stopMediaElement,
2514
+ ]);
2515
+ const playPostRollAd = async () => {
2516
+ if (!adConfig?.postRoll || postRollPlayedRef.current || !videoRef)
2517
+ return;
2518
+ const adBreak = adConfig.postRoll;
2519
+ postRollPlayedRef.current = true;
2520
+ resumeAfterAdRef.current = false;
2521
+ stopMediaElement(useVideoStore.getState().adVideoRef);
2522
+ setIsAdPlaying(true);
2523
+ setCurrentAd(adBreak);
2524
+ setAdType("post-roll");
2525
+ adConfig.onAdStart?.(adBreak);
2526
+ };
2527
+ const endAd = useCallback(() => {
2528
+ const currentAdState = useVideoStore.getState().currentAd;
2529
+ const adTypeState = useVideoStore.getState().adType;
2530
+ const videoRefState = useVideoStore.getState().videoRef;
2531
+ const adVideoRefState = useVideoStore.getState().adVideoRef;
2532
+ if (!currentAdState)
2533
+ return;
2534
+ // Reset processing flag
2535
+ isProcessingAdRef.current = false;
2536
+ // Clean up ad video element
2537
+ if (adVideoRefState) {
2538
+ stopMediaElement(adVideoRefState);
2539
+ setAdVideoRef(null);
2540
+ }
2541
+ // Reset ad state
2542
+ setIsAdPlaying(false);
2543
+ setCurrentAd(null);
2544
+ setAdType(null);
2545
+ setAdCurrentTime(0);
2546
+ setCanSkipAd(false);
2547
+ setSkipCountdown(0);
2548
+ // Call end callback
2549
+ adConfig?.onAdEnd?.(currentAdState);
2550
+ // Resume main video if needed
2551
+ if (resumeAfterAdRef.current &&
2552
+ adTypeState !== "post-roll" &&
2553
+ videoRefState) {
2554
+ // Small delay to ensure ad cleanup is complete
2555
+ setTimeout(() => {
2556
+ if (videoRefState && !videoRefState.paused)
2557
+ return;
2558
+ videoRefState.play().catch(() => {
2559
+ // If autoplay fails, user will need to click play
2560
+ setPlaying(false);
2561
+ setIsPlaying(false);
2562
+ });
2563
+ setPlaying(true);
2564
+ setIsPlaying(true);
2565
+ }, 100);
2566
+ }
2567
+ resumeAfterAdRef.current = false;
2568
+ }, [
2569
+ adConfig,
2570
+ setIsAdPlaying,
2571
+ setCurrentAd,
2572
+ setAdType,
2573
+ setAdCurrentTime,
2574
+ setCanSkipAd,
2575
+ setSkipCountdown,
2576
+ setAdVideoRef,
2577
+ setPlaying,
2578
+ setIsPlaying,
2579
+ stopMediaElement,
2580
+ ]);
2581
+ const skipAd = () => {
2582
+ if (!currentAd || !currentAd.skipable)
2583
+ return;
2584
+ adConfig?.onAdSkip?.(currentAd);
2585
+ endAd();
2586
+ };
2587
+ useEffect(() => {
2588
+ if (!adVideoRef || !isAdPlaying)
2589
+ return;
2590
+ const handleAdEnded = () => {
2591
+ endAd();
2592
+ };
2593
+ let forcedEndTriggered = false;
2594
+ const enforceTimedDuration = () => {
2595
+ if (!currentAd)
2596
+ return;
2597
+ const configuredDuration = Number(currentAd.duration);
2598
+ if (!Number.isFinite(configuredDuration) || configuredDuration <= 0) {
2599
+ return;
2600
+ }
2601
+ if (adVideoRef.currentTime >= configuredDuration) {
2602
+ if (adVideoRef.currentTime > configuredDuration) {
2603
+ try {
2604
+ adVideoRef.currentTime = configuredDuration;
2605
+ }
2606
+ catch (_error) { }
2607
+ }
2608
+ if (!forcedEndTriggered) {
2609
+ forcedEndTriggered = true;
2610
+ endAd();
2611
+ }
2612
+ }
2613
+ };
2614
+ adVideoRef.addEventListener("ended", handleAdEnded);
2615
+ adVideoRef.addEventListener("timeupdate", enforceTimedDuration);
2616
+ return () => {
2617
+ adVideoRef.removeEventListener("ended", handleAdEnded);
2618
+ adVideoRef.removeEventListener("timeupdate", enforceTimedDuration);
2619
+ };
2620
+ }, [adVideoRef, isAdPlaying, endAd, currentAd]);
2621
+ useEffect(() => {
2622
+ if (isAdPlaying || !adVideoRef) {
2623
+ return;
2624
+ }
2625
+ stopMediaElement(adVideoRef);
2626
+ setAdVideoRef(null);
2627
+ }, [isAdPlaying, adVideoRef, setAdVideoRef, stopMediaElement]);
2628
+ useEffect(() => {
2629
+ if (!videoRef || !adConfig?.preRoll || preRollPlayedRef.current)
2630
+ return;
2631
+ const handleCanPlay = () => {
2632
+ playPreRollAd();
2633
+ };
2634
+ videoRef.addEventListener("canplay", handleCanPlay, { once: true });
2635
+ if (videoRef.readyState >= 2) {
2636
+ playPreRollAd();
2637
+ }
2638
+ return () => {
2639
+ videoRef.removeEventListener("canplay", handleCanPlay);
2640
+ };
2641
+ }, [videoRef, adConfig?.preRoll]);
2642
+ // Precise mid-roll ad checking with accurate timing
2643
+ useEffect(() => {
2644
+ if (!videoRef || !adConfig?.midRoll || isAdPlaying) {
2645
+ // Clear any pending throttle
2646
+ if (adCheckThrottleRef.current !== null) {
2647
+ cancelAnimationFrame(adCheckThrottleRef.current);
2648
+ adCheckThrottleRef.current = null;
2649
+ }
2650
+ return;
2651
+ }
2652
+ // Precise ad check function
2653
+ const checkMidRollAds = () => {
2654
+ // Clear throttle ref
2655
+ adCheckThrottleRef.current = null;
2656
+ const state = useVideoStore.getState();
2657
+ // Skip if ad is already playing or being processed
2658
+ if (state.isAdPlaying || isProcessingAdRef.current) {
2659
+ return;
2660
+ }
2661
+ // Check if we have mid-roll ads in queue
2662
+ if (!state.midRollQueue || state.midRollQueue.length === 0) {
2663
+ return;
2664
+ }
2665
+ // Get current time directly from video element for maximum accuracy
2666
+ const currentVideoTime = videoRef.currentTime || 0;
2667
+ // Update max time reached (only forward, not backward)
2668
+ if (currentVideoTime > maxTimeReachedRef.current) {
2669
+ maxTimeReachedRef.current = currentVideoTime;
2670
+ }
2671
+ // Find the next ad in queue that should play
2672
+ // Check ads in order, but skip already-played ones
2673
+ for (const ad of state.midRollQueue) {
2674
+ // Skip if already played
2675
+ if (state.playedAdBreaks.includes(ad.id)) {
2676
+ continue;
2677
+ }
2678
+ // Precise timing check: ad should trigger when we reach or pass its time
2679
+ // Use 1 second tolerance to catch ads even if timeupdate fires slightly late
2680
+ const timeDifference = currentVideoTime - ad.time;
2681
+ const shouldTrigger = timeDifference >= 0 && timeDifference <= 1.0;
2682
+ // Also check if we've reached the max time (prevents replay on backward seek)
2683
+ // This ensures ads only play if we've actually watched past them
2684
+ const hasReachedMaxTime = maxTimeReachedRef.current >= ad.time;
2685
+ if (shouldTrigger && hasReachedMaxTime) {
2686
+ // Play the ad and break (only one ad at a time)
2687
+ playMidRollAd(ad);
2688
+ break;
2689
+ }
2690
+ }
2691
+ };
2692
+ // Throttle function using requestAnimationFrame for smooth performance
2693
+ const throttledCheck = () => {
2694
+ if (adCheckThrottleRef.current === null) {
2695
+ adCheckThrottleRef.current = requestAnimationFrame(checkMidRollAds);
2696
+ }
2697
+ };
2698
+ // Listen to timeupdate event (throttled for performance)
2699
+ videoRef.addEventListener("timeupdate", throttledCheck);
2700
+ // Also check immediately on seek to catch rapid seeks past ad times
2701
+ const handleSeeking = () => {
2702
+ // Force immediate check when seeking
2703
+ if (adCheckThrottleRef.current !== null) {
2704
+ cancelAnimationFrame(adCheckThrottleRef.current);
2705
+ adCheckThrottleRef.current = null;
2706
+ }
2707
+ // Check immediately
2708
+ checkMidRollAds();
2709
+ };
2710
+ videoRef.addEventListener("seeking", handleSeeking);
2711
+ videoRef.addEventListener("seeked", handleSeeking);
2712
+ return () => {
2713
+ videoRef.removeEventListener("timeupdate", throttledCheck);
2714
+ videoRef.removeEventListener("seeking", handleSeeking);
2715
+ videoRef.removeEventListener("seeked", handleSeeking);
2716
+ // Clear any pending animation frame
2717
+ if (adCheckThrottleRef.current !== null) {
2718
+ cancelAnimationFrame(adCheckThrottleRef.current);
2719
+ adCheckThrottleRef.current = null;
2720
+ }
2721
+ };
2722
+ }, [videoRef, isAdPlaying, adConfig, playMidRollAd]);
2723
+ useEffect(() => {
2724
+ if (!videoRef || !adConfig?.postRoll || postRollPlayedRef.current)
2725
+ return;
2726
+ const handleVideoEnded = () => {
2727
+ setTimeout(() => {
2728
+ playPostRollAd();
2729
+ }, 500);
2730
+ };
2731
+ videoRef.addEventListener("ended", handleVideoEnded);
2732
+ return () => {
2733
+ videoRef.removeEventListener("ended", handleVideoEnded);
2734
+ };
2735
+ }, [videoRef, adConfig?.postRoll]);
2736
+ useEffect(() => {
2737
+ if (!videoRef?.src)
2738
+ return;
2739
+ // Reset ad state when video source changes
2740
+ preRollPlayedRef.current = false;
2741
+ postRollPlayedRef.current = false;
2742
+ resumeAfterAdRef.current = false;
2743
+ maxTimeReachedRef.current = 0;
2744
+ isProcessingAdRef.current = false;
2745
+ // Clear any pending throttle
2746
+ if (adCheckThrottleRef.current !== null) {
2747
+ cancelAnimationFrame(adCheckThrottleRef.current);
2748
+ adCheckThrottleRef.current = null;
2749
+ }
2750
+ setIsAdPlaying(false);
2751
+ setCurrentAd(null);
2752
+ setAdType(null);
2753
+ // Re-initialize mid-roll queue with strict validation
2754
+ if (adConfig?.midRoll && adConfig.midRoll.length > 0) {
2755
+ // Filter and validate ads
2756
+ const validAds = adConfig.midRoll.filter((ad) => ad &&
2757
+ typeof ad.time === "number" &&
2758
+ ad.time >= 0 &&
2759
+ typeof ad.id === "string" &&
2760
+ ad.id.trim() !== "" &&
2761
+ typeof ad.adUrl === "string" &&
2762
+ ad.adUrl.trim() !== "" &&
2763
+ typeof ad.type === "string" &&
2764
+ ad.type === "mid-roll");
2765
+ if (validAds.length > 0) {
2766
+ // Sort by time and remove duplicates
2767
+ const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
2768
+ const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
2769
+ setMidRollQueue(uniqueAds);
2770
+ }
2771
+ else {
2772
+ setMidRollQueue([]);
2773
+ }
2774
+ }
2775
+ else {
2776
+ setMidRollQueue([]);
2777
+ }
2778
+ // Clean up any lingering ad video
2779
+ const lingeringAdRef = useVideoStore.getState().adVideoRef;
2780
+ if (lingeringAdRef) {
2781
+ stopMediaElement(lingeringAdRef);
2782
+ setAdVideoRef(null);
2783
+ }
2784
+ }, [
2785
+ videoRef?.src,
2786
+ adConfig?.midRoll,
2787
+ setIsAdPlaying,
2788
+ setCurrentAd,
2789
+ setAdType,
2790
+ setMidRollQueue,
2791
+ stopMediaElement,
2792
+ setAdVideoRef,
2793
+ ]);
2794
+ return {
2795
+ isAdPlaying,
2796
+ currentAd,
2797
+ adType,
2798
+ skipAd,
2799
+ endAd,
2800
+ };
2801
+ };
2802
+
2803
+ const usePrimaryVideoLifecycle = ({ hasPreRoll, trackSrc, }) => {
2804
+ const { videoRef, setVideoRef, isAdPlaying, currentAd, adType, setMuted, setPlaying, setIsPlaying, } = useVideoStore(useShallow((state) => ({
2805
+ videoRef: state.videoRef,
2806
+ setVideoRef: state.setVideoRef,
2807
+ isAdPlaying: state.isAdPlaying,
2808
+ currentAd: state.currentAd,
2809
+ adType: state.adType,
2810
+ setMuted: state.setMuted,
2811
+ setPlaying: state.setPlaying,
2812
+ setIsPlaying: state.setIsPlaying,
2813
+ })));
2814
+ const [initialAdStarted, setInitialAdStarted] = useState(!hasPreRoll);
2815
+ const [initialAdFinished, setInitialAdFinished] = useState(!hasPreRoll);
2816
+ const previousIsAdPlayingRef = useRef(isAdPlaying);
2817
+ useEffect(() => {
2818
+ if (hasPreRoll) {
2819
+ setInitialAdStarted(false);
2820
+ setInitialAdFinished(false);
2821
+ }
2822
+ else {
2823
+ setInitialAdStarted(true);
2824
+ setInitialAdFinished(true);
2825
+ }
2826
+ }, [hasPreRoll, trackSrc]);
2827
+ useEffect(() => {
2828
+ if (hasPreRoll &&
2829
+ !initialAdStarted &&
2830
+ isAdPlaying &&
2831
+ adType === "pre-roll") {
2832
+ setInitialAdStarted(true);
2833
+ }
2834
+ }, [hasPreRoll, initialAdStarted, isAdPlaying, adType]);
2835
+ useEffect(() => {
2836
+ const previouslyPlaying = previousIsAdPlayingRef.current;
2837
+ if (hasPreRoll &&
2838
+ initialAdStarted &&
2839
+ previouslyPlaying &&
2840
+ !isAdPlaying &&
2841
+ !initialAdFinished) {
2842
+ setInitialAdFinished(true);
2843
+ }
2844
+ previousIsAdPlayingRef.current = isAdPlaying;
2845
+ }, [hasPreRoll, initialAdStarted, initialAdFinished, isAdPlaying]);
2846
+ useEffect(() => {
2847
+ if (!videoRef) {
2848
+ return;
2849
+ }
2850
+ if (hasPreRoll && !initialAdFinished) {
2851
+ videoRef.pause();
2852
+ return;
2853
+ }
2854
+ }, [videoRef, hasPreRoll, initialAdFinished]);
2855
+ useEffect(() => {
2856
+ if (!videoRef)
2857
+ return;
2858
+ const syncMutedState = () => {
2859
+ setMuted(videoRef.muted);
2860
+ };
2861
+ syncMutedState();
2862
+ videoRef.addEventListener("volumechange", syncMutedState);
2863
+ return () => {
2864
+ videoRef.removeEventListener("volumechange", syncMutedState);
2865
+ };
2866
+ }, [videoRef, setMuted]);
2867
+ useEffect(() => {
2868
+ if (!videoRef)
2869
+ return;
2870
+ videoRef.preload = "auto";
2871
+ }, [videoRef]);
2872
+ useEffect(() => {
2873
+ const element = videoRef;
2874
+ return () => {
2875
+ if (!element)
2876
+ return;
2877
+ try {
2878
+ element.pause();
2879
+ }
2880
+ catch (_error) { }
2881
+ element.removeAttribute("src");
2882
+ element.load();
2883
+ };
2884
+ }, [videoRef]);
2885
+ useEffect(() => {
2886
+ if (!videoRef) {
2887
+ return;
2888
+ }
2889
+ if (hasPreRoll && !initialAdFinished) {
2890
+ return;
2891
+ }
2892
+ if (isAdPlaying) {
2893
+ return;
2894
+ }
2895
+ let cancelled = false;
2896
+ const markPlaying = () => {
2897
+ if (cancelled)
2898
+ return;
2899
+ setPlaying(true);
2900
+ setIsPlaying(true);
2901
+ };
2902
+ const attemptPlayback = () => {
2903
+ if (!videoRef || cancelled)
2904
+ return;
2905
+ if (!videoRef.paused) {
2906
+ markPlaying();
2907
+ return;
2908
+ }
2909
+ const playPromise = videoRef.play();
2910
+ if (playPromise && typeof playPromise.then === "function") {
2911
+ playPromise.then(markPlaying).catch((error) => {
2912
+ if (cancelled)
2913
+ return;
2914
+ const maybeNotAllowed = typeof error === "object" &&
2915
+ error !== null &&
2916
+ "name" in error &&
2917
+ error.name === "NotAllowedError";
2918
+ if (maybeNotAllowed && videoRef.muted === false) {
2919
+ videoRef.muted = true;
2920
+ const retryPromise = videoRef.play();
2921
+ if (retryPromise && typeof retryPromise.then === "function") {
2922
+ retryPromise.then(markPlaying).catch(() => {
2923
+ if (cancelled)
2924
+ return;
2925
+ setPlaying(false);
2926
+ setIsPlaying(false);
2927
+ });
2928
+ return;
2929
+ }
2930
+ }
2931
+ setPlaying(false);
2932
+ setIsPlaying(false);
2933
+ });
2934
+ }
2935
+ else {
2936
+ markPlaying();
2937
+ }
2938
+ };
2939
+ if (videoRef.readyState >= 2) {
2940
+ attemptPlayback();
2941
+ }
2942
+ else {
2943
+ const onCanPlay = () => {
2944
+ attemptPlayback();
2945
+ };
2946
+ videoRef.addEventListener("canplay", onCanPlay, { once: true });
2947
+ return () => {
2948
+ cancelled = true;
2949
+ videoRef.removeEventListener("canplay", onCanPlay);
2950
+ };
2951
+ }
2952
+ return () => {
2953
+ cancelled = true;
2954
+ };
2955
+ }, [
2956
+ videoRef,
2957
+ hasPreRoll,
2958
+ initialAdFinished,
2959
+ isAdPlaying,
2960
+ setPlaying,
2961
+ setIsPlaying,
2962
+ ]);
2963
+ useEffect(() => {
2964
+ if (!videoRef)
2965
+ return;
2966
+ const wrapper = useVideoStore.getState().videoWrapperRef;
2967
+ if (wrapper) {
2968
+ wrapper.dataset.ready = (!hasPreRoll || initialAdFinished).toString();
2969
+ }
2970
+ }, [videoRef, hasPreRoll, initialAdFinished]);
2971
+ const registerVideoRef = useCallback((node) => {
2972
+ setVideoRef(node);
2973
+ }, [setVideoRef]);
2974
+ const shouldCoverMainVideo = useMemo(() => hasPreRoll && !initialAdFinished, [hasPreRoll, initialAdFinished]);
2975
+ const shouldShowPlaceholder = useMemo(() => shouldCoverMainVideo && !isAdPlaying, [shouldCoverMainVideo, isAdPlaying]);
2976
+ return {
2977
+ registerVideoRef,
2978
+ videoRef,
2979
+ isAdPlaying,
2980
+ currentAd,
2981
+ adType,
2982
+ initialAdFinished,
2983
+ shouldCoverMainVideo,
2984
+ shouldShowPlaceholder,
2985
+ };
2986
+ };
2987
+
2988
+ const getErrorType = (code) => {
2989
+ // MediaError codes: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
2990
+ switch (code) {
2991
+ case 1: // MEDIA_ERR_ABORTED
2992
+ return "unknown";
2993
+ case 2: // MEDIA_ERR_NETWORK
2994
+ return "network";
2995
+ case 3: // MEDIA_ERR_DECODE
2996
+ return "decode";
2997
+ case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
2998
+ return "src";
2999
+ default:
3000
+ return "unknown";
3001
+ }
3002
+ };
3003
+ const getErrorMessage = (code) => {
3004
+ switch (code) {
3005
+ case 1:
3006
+ return "Video playback was aborted.";
3007
+ case 2:
3008
+ return "A network error occurred while loading the video.";
3009
+ case 3:
3010
+ return "An error occurred while decoding the video.";
3011
+ case 4:
3012
+ return "The video format is not supported.";
3013
+ default:
3014
+ return "An unknown error occurred.";
3015
+ }
3016
+ };
3017
+ const useVideoError = () => {
3018
+ const { setError, clearError, error } = useVideoStore();
3019
+ const handleVideoError = useCallback((e) => {
3020
+ const video = e.currentTarget;
3021
+ const mediaError = video.error;
3022
+ if (mediaError) {
3023
+ const errorData = {
3024
+ code: mediaError.code,
3025
+ message: mediaError.message || getErrorMessage(mediaError.code),
3026
+ type: getErrorType(mediaError.code),
3027
+ };
3028
+ setError(errorData);
3029
+ }
3030
+ else {
3031
+ setError({
3032
+ code: 0,
3033
+ message: "An unknown error occurred.",
3034
+ type: "unknown",
3035
+ });
3036
+ }
3037
+ }, [setError]);
3038
+ const retry = useCallback(() => {
3039
+ clearError();
3040
+ const { videoRef } = useVideoStore.getState();
3041
+ if (videoRef) {
3042
+ videoRef.load();
3043
+ videoRef.play().catch(() => undefined);
3044
+ }
3045
+ }, [clearError]);
3046
+ return {
3047
+ error,
3048
+ handleVideoError,
3049
+ clearError,
3050
+ retry,
3051
+ };
3052
+ };
3053
+
3054
+ const AdOverlay = React__default.memo(({ adBreak, onSkip, config }) => {
3055
+ const { adVideoRef, setAdVideoRef, adCurrentTime, setAdCurrentTime, canSkipAd, setCanSkipAd, skipCountdown, setSkipCountdown, videoRef, muted, setIsPlaying, } = useVideoStore(useShallow((state) => ({
3056
+ adVideoRef: state.adVideoRef,
3057
+ setAdVideoRef: state.setAdVideoRef,
3058
+ adCurrentTime: state.adCurrentTime,
3059
+ setAdCurrentTime: state.setAdCurrentTime,
3060
+ canSkipAd: state.canSkipAd,
3061
+ setCanSkipAd: state.setCanSkipAd,
3062
+ skipCountdown: state.skipCountdown,
3063
+ setSkipCountdown: state.setSkipCountdown,
3064
+ videoRef: state.videoRef,
3065
+ muted: state.muted,
3066
+ setIsPlaying: state.setIsPlaying,
3067
+ })));
3068
+ const [showControls, setShowControls] = useState(true);
3069
+ const [isHovered, setIsHovered] = useState(false);
3070
+ const [adDuration, setAdDuration] = useState(0);
3071
+ const [requiresInteraction, setRequiresInteraction] = useState(false);
3072
+ const [adLoadError, setAdLoadError] = useState(false);
3073
+ const controlsTimeoutRef = useRef(null);
3074
+ const loadTimeoutRef = useRef(null);
3075
+ const safelySetCanSkipAd = useCallback((value) => {
3076
+ if (useVideoStore.getState().canSkipAd !== value) {
3077
+ setCanSkipAd(value);
3078
+ }
3079
+ }, [setCanSkipAd]);
3080
+ const safelySetSkipCountdown = useCallback((value) => {
3081
+ if (useVideoStore.getState().skipCountdown !== value) {
3082
+ setSkipCountdown(value);
3083
+ }
3084
+ }, [setSkipCountdown]);
3085
+ useEffect(() => {
3086
+ if (isHovered) {
3087
+ setShowControls(true);
3088
+ if (controlsTimeoutRef.current) {
3089
+ clearTimeout(controlsTimeoutRef.current);
3090
+ }
3091
+ return;
3092
+ }
3093
+ controlsTimeoutRef.current = setTimeout(() => {
3094
+ setShowControls(false);
3095
+ }, 3000);
3096
+ return () => {
3097
+ if (controlsTimeoutRef.current) {
3098
+ clearTimeout(controlsTimeoutRef.current);
3099
+ }
3100
+ };
3101
+ }, [isHovered]);
3102
+ const skipAfter = useMemo(() => {
3103
+ const rawSkipAfter = Number.isFinite(adBreak.skipAfter)
3104
+ ? Math.max(0, Number(adBreak.skipAfter))
3105
+ : 0;
3106
+ if (adDuration > 0) {
3107
+ return Math.min(rawSkipAfter, adDuration);
3108
+ }
3109
+ return rawSkipAfter;
3110
+ }, [adBreak.skipAfter, adDuration]);
3111
+ const sponsoredUrl = adBreak.sponsoredUrl;
3112
+ useEffect(() => {
3113
+ setAdDuration(0);
3114
+ setRequiresInteraction(false);
3115
+ setAdLoadError(false);
3116
+ setAdCurrentTime(0);
3117
+ if (loadTimeoutRef.current) {
3118
+ clearTimeout(loadTimeoutRef.current);
3119
+ loadTimeoutRef.current = null;
3120
+ }
3121
+ if (adBreak.skipable !== undefined) {
3122
+ setCanSkipAd(false);
3123
+ setSkipCountdown(0);
3124
+ }
3125
+ }, [
3126
+ adBreak.id,
3127
+ adBreak.skipable,
3128
+ setAdCurrentTime,
3129
+ setCanSkipAd,
3130
+ setSkipCountdown,
3131
+ ]);
3132
+ useEffect(() => {
3133
+ if (!adBreak.skipable) {
3134
+ safelySetCanSkipAd(false);
3135
+ safelySetSkipCountdown(0);
3136
+ return;
3137
+ }
3138
+ safelySetCanSkipAd(false);
3139
+ safelySetSkipCountdown(Math.max(Math.ceil(skipAfter), 0));
3140
+ if (skipAfter <= 0) {
3141
+ safelySetCanSkipAd(true);
3142
+ safelySetSkipCountdown(0);
3143
+ }
3144
+ }, [
3145
+ adBreak.id,
3146
+ adBreak.skipable,
3147
+ skipAfter,
3148
+ safelySetCanSkipAd,
3149
+ safelySetSkipCountdown,
3150
+ ]);
3151
+ const attemptAdPlayback = useCallback(() => {
3152
+ if (!adVideoRef)
3153
+ return;
3154
+ setRequiresInteraction(false);
3155
+ setAdLoadError(false);
3156
+ if (!adVideoRef.src && adBreak.adUrl) {
3157
+ adVideoRef.src = adBreak.adUrl;
3158
+ adVideoRef.load();
3159
+ return;
3160
+ }
3161
+ const playPromise = adVideoRef.play();
3162
+ if (playPromise && "catch" in playPromise) {
3163
+ playPromise.catch((error) => {
3164
+ console.warn("Ad play failed:", error);
3165
+ setRequiresInteraction(true);
3166
+ setIsPlaying(false);
3167
+ });
3168
+ }
3169
+ }, [adVideoRef, adBreak.adUrl, setIsPlaying]);
3170
+ const timeUpdateRafRef = useRef(null);
3171
+ const lastUpdateTimeRef = useRef(0);
3172
+ useEffect(() => {
3173
+ if (!adVideoRef)
3174
+ return;
3175
+ const handleTimeUpdate = () => {
3176
+ if (timeUpdateRafRef.current !== null)
3177
+ return;
3178
+ timeUpdateRafRef.current = requestAnimationFrame(() => {
3179
+ timeUpdateRafRef.current = null;
3180
+ const currentTime = adVideoRef.currentTime;
3181
+ if (Math.abs(currentTime - lastUpdateTimeRef.current) < 0.1) {
3182
+ return;
3183
+ }
3184
+ lastUpdateTimeRef.current = currentTime;
3185
+ setAdCurrentTime(currentTime);
3186
+ if (adBreak.skipable) {
3187
+ const remaining = skipAfter - currentTime;
3188
+ if (remaining <= 0) {
3189
+ safelySetCanSkipAd(true);
3190
+ safelySetSkipCountdown(0);
3191
+ }
3192
+ else {
3193
+ const remainingForDisplay = Math.max(Math.ceil(remaining), 0);
3194
+ safelySetSkipCountdown(remainingForDisplay);
3195
+ if (canSkipAd) {
3196
+ safelySetCanSkipAd(false);
3197
+ }
3198
+ }
3199
+ }
3200
+ });
3201
+ };
3202
+ const handleLoadedMetadata = () => {
3203
+ if (loadTimeoutRef.current) {
3204
+ clearTimeout(loadTimeoutRef.current);
3205
+ loadTimeoutRef.current = null;
3206
+ }
3207
+ const duration = Number.isFinite(adVideoRef.duration)
3208
+ ? adVideoRef.duration
3209
+ : 0;
3210
+ setAdDuration(duration);
3211
+ setAdLoadError(false);
3212
+ setIsPlaying(!adVideoRef.paused);
3213
+ attemptAdPlayback();
3214
+ };
3215
+ if (loadTimeoutRef.current) {
3216
+ clearTimeout(loadTimeoutRef.current);
3217
+ }
3218
+ loadTimeoutRef.current = setTimeout(() => {
3219
+ if (adVideoRef && adVideoRef.readyState < 2) {
3220
+ console.warn("Ad load timeout:", adBreak.id);
3221
+ setAdLoadError(true);
3222
+ setRequiresInteraction(true);
3223
+ }
3224
+ }, 30000);
3225
+ const handlePlay = () => {
3226
+ setIsPlaying(true);
3227
+ setRequiresInteraction(false);
3228
+ };
3229
+ const handlePause = () => {
3230
+ setIsPlaying(false);
3231
+ };
3232
+ const handleWaiting = () => {
3233
+ setIsPlaying(false);
3234
+ };
3235
+ const handlePlaying = () => {
3236
+ setIsPlaying(true);
3237
+ setRequiresInteraction(false);
3238
+ };
3239
+ const handleError = (e) => {
3240
+ if (loadTimeoutRef.current) {
3241
+ clearTimeout(loadTimeoutRef.current);
3242
+ loadTimeoutRef.current = null;
3243
+ }
3244
+ const error = e.target;
3245
+ const errorCode = error.error?.code;
3246
+ const errorMessage = error.error?.message || "Unknown ad error";
3247
+ console.error("Ad playback error:", {
3248
+ adId: adBreak.id,
3249
+ errorCode,
3250
+ errorMessage,
3251
+ src: adVideoRef.src,
3252
+ });
3253
+ setAdLoadError(true);
3254
+ setRequiresInteraction(true);
3255
+ setIsPlaying(false);
3256
+ };
3257
+ adVideoRef.addEventListener("timeupdate", handleTimeUpdate);
3258
+ adVideoRef.addEventListener("loadedmetadata", handleLoadedMetadata);
3259
+ adVideoRef.addEventListener("play", handlePlay);
3260
+ adVideoRef.addEventListener("pause", handlePause);
3261
+ adVideoRef.addEventListener("waiting", handleWaiting);
3262
+ adVideoRef.addEventListener("playing", handlePlaying);
3263
+ adVideoRef.addEventListener("error", handleError);
3264
+ return () => {
3265
+ adVideoRef.removeEventListener("timeupdate", handleTimeUpdate);
3266
+ adVideoRef.removeEventListener("loadedmetadata", handleLoadedMetadata);
3267
+ adVideoRef.removeEventListener("play", handlePlay);
3268
+ adVideoRef.removeEventListener("pause", handlePause);
3269
+ adVideoRef.removeEventListener("waiting", handleWaiting);
3270
+ adVideoRef.removeEventListener("playing", handlePlaying);
3271
+ adVideoRef.removeEventListener("error", handleError);
3272
+ if (loadTimeoutRef.current) {
3273
+ clearTimeout(loadTimeoutRef.current);
3274
+ loadTimeoutRef.current = null;
3275
+ }
3276
+ if (timeUpdateRafRef.current !== null) {
3277
+ cancelAnimationFrame(timeUpdateRafRef.current);
3278
+ timeUpdateRafRef.current = null;
3279
+ }
3280
+ lastUpdateTimeRef.current = 0;
3281
+ };
3282
+ }, [
3283
+ adVideoRef,
3284
+ adBreak.skipable,
3285
+ adBreak.id,
3286
+ skipAfter,
3287
+ canSkipAd,
3288
+ setAdCurrentTime,
3289
+ setIsPlaying,
3290
+ safelySetSkipCountdown,
3291
+ safelySetCanSkipAd,
3292
+ attemptAdPlayback,
3293
+ ]);
3294
+ useEffect(() => {
3295
+ if (!adVideoRef || !videoRef)
3296
+ return;
3297
+ // Sync volume and muted state
3298
+ adVideoRef.volume = videoRef.volume;
3299
+ adVideoRef.muted = muted;
3300
+ // Check if src needs to be updated
3301
+ const currentSrc = adVideoRef.src || adVideoRef.currentSrc || "";
3302
+ const needsReload = !currentSrc || currentSrc !== adBreak.adUrl;
3303
+ // Load ad if needed
3304
+ if (needsReload && adBreak.adUrl) {
3305
+ // Clear previous src
3306
+ try {
3307
+ adVideoRef.pause();
3308
+ adVideoRef.removeAttribute("src");
3309
+ adVideoRef.src = "";
3310
+ // Set new src
3311
+ adVideoRef.src = adBreak.adUrl;
3312
+ adVideoRef.load();
3313
+ }
3314
+ catch (error) {
3315
+ console.warn("Error loading ad:", error);
3316
+ setAdLoadError(true);
3317
+ }
3318
+ }
3319
+ const handleCanPlay = () => {
3320
+ if (!adVideoRef || adVideoRef.paused === false)
3321
+ return;
3322
+ attemptAdPlayback();
3323
+ };
3324
+ const handleLoadedData = () => {
3325
+ // Ensure volume is synced after load
3326
+ if (videoRef && adVideoRef) {
3327
+ try {
3328
+ adVideoRef.volume = videoRef.volume;
3329
+ adVideoRef.muted = muted;
3330
+ }
3331
+ catch (error) {
3332
+ // Ignore errors during cleanup
3333
+ }
3334
+ }
3335
+ };
3336
+ adVideoRef.addEventListener("canplay", handleCanPlay);
3337
+ adVideoRef.addEventListener("loadeddata", handleLoadedData);
3338
+ // Try to play if already ready and src matches
3339
+ if (adVideoRef.readyState >= 3 && !needsReload) {
3340
+ attemptAdPlayback();
3341
+ }
3342
+ return () => {
3343
+ if (adVideoRef) {
3344
+ adVideoRef.removeEventListener("canplay", handleCanPlay);
3345
+ adVideoRef.removeEventListener("loadeddata", handleLoadedData);
3346
+ }
3347
+ };
3348
+ }, [adVideoRef, videoRef, muted, adBreak.adUrl, attemptAdPlayback]);
3349
+ useEffect(() => {
3350
+ if (!adVideoRef)
3351
+ return;
3352
+ try {
3353
+ // Sync muted state
3354
+ adVideoRef.muted = muted;
3355
+ // Sync volume with main video
3356
+ if (videoRef) {
3357
+ adVideoRef.volume = videoRef.volume;
3358
+ }
3359
+ }
3360
+ catch (error) {
3361
+ // Ignore errors during state sync
3362
+ }
3363
+ }, [adVideoRef, muted, videoRef]);
3364
+ const handleSkip = () => {
3365
+ if (canSkipAd && onSkip) {
3366
+ onSkip();
3367
+ }
3368
+ };
3369
+ const progressPercent = adDuration > 0 ? (adCurrentTime / adDuration) * 100 : 0;
3370
+ 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: () => {
3371
+ setIsHovered(true);
3372
+ setShowControls(true);
3373
+ } },
3374
+ React__default.createElement("div", { className: "relative flex-1 w-full flex items-center justify-center" },
3375
+ React__default.createElement("video", { ref: (ref) => {
3376
+ if (!ref)
3377
+ return;
3378
+ if (ref !== adVideoRef) {
3379
+ setAdVideoRef(ref);
3380
+ }
3381
+ ref.muted = muted;
3382
+ if (videoRef) {
3383
+ ref.volume = videoRef.volume;
3384
+ }
3385
+ if (adBreak.adUrl) {
3386
+ const currentSrc = ref.src || ref.currentSrc || "";
3387
+ if (currentSrc !== adBreak.adUrl) {
3388
+ ref.src = adBreak.adUrl;
3389
+ }
3390
+ }
3391
+ }, className: "w-full h-full object-contain", autoPlay: true, playsInline: true, muted: muted, preload: "auto", key: adBreak.id }),
3392
+ (requiresInteraction || adLoadError) && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
3393
+ React__default.createElement("div", { className: "flex flex-col items-center gap-4" },
3394
+ adLoadError && (React__default.createElement("p", { className: "text-red-400 text-sm" }, "Ad failed to load")),
3395
+ 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"))))),
3396
+ React__default.createElement("div", { className: `absolute inset-0 transition-all duration-300 ${showControls ? "opacity-100" : "opacity-0 pointer-events-none"}` },
3397
+ 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" },
3398
+ React__default.createElement("div", { className: "shrink-0 relative" },
3399
+ React__default.createElement(ControlsHeader, { config: {
3400
+ title: config?.config?.headerConfig?.config?.title ||
3401
+ "Advertisement",
3402
+ isTrailer: config?.config?.headerConfig?.config?.isTrailer,
3403
+ onClose: config?.config?.headerConfig?.config?.onClose,
3404
+ } })),
3405
+ React__default.createElement("div", { className: "flex-1 flex items-center justify-center" },
3406
+ React__default.createElement(MiddleControls, null)),
3407
+ React__default.createElement("div", { className: "shrink-0 relative" },
3408
+ adBreak.skipable && (React__default.createElement("div", { className: "px-10 pb-3 flex justify-end" },
3409
+ React__default.createElement("button", { onClick: handleSkip, disabled: !canSkipAd, className: `flex items-center gap-2 px-4 py-2 rounded transition-all duration-200 ${canSkipAd
3410
+ ? "bg-white/20 hover:bg-white/30 text-white cursor-pointer hover:scale-105 active:scale-95 shadow-md hover:shadow-lg border border-white/30 hover:border-white/50 backdrop-blur-md"
3411
+ : "bg-black/60 text-gray-400 cursor-not-allowed border border-gray-700/60"}`, style: { borderRadius: "4px" } },
3412
+ React__default.createElement(SkipForward, { className: "w-4 h-4" }),
3413
+ React__default.createElement("span", { className: "text-sm font-medium" }, canSkipAd
3414
+ ? "Skip Ad"
3415
+ : `Skip in ${Math.max(skipCountdown, 0)}s`)))),
3416
+ React__default.createElement("div", { className: "px-10 pb-4" },
3417
+ React__default.createElement("div", { className: "relative h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none select-none" },
3418
+ 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}%` } }),
3419
+ 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)` } }))),
3420
+ sponsoredUrl && (React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-end" },
3421
+ 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"))))))));
3422
+ });
3423
+ AdOverlay.displayName = "AdOverlay";
3424
+
3425
+ const ErrorOverlay = React__default.memo(({ error, onRetry }) => {
3426
+ const getIcon = () => {
3427
+ switch (error.type) {
3428
+ case "network":
3429
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3430
+ 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" })));
3431
+ case "src":
3432
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3433
+ 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" }),
3434
+ React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 3l18 18" })));
3435
+ default:
3436
+ return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3437
+ 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" })));
3438
+ }
3439
+ };
3440
+ return (React__default.createElement("div", { className: "absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90" },
3441
+ React__default.createElement("div", { className: "flex flex-col items-center gap-4 p-6 text-center" },
3442
+ getIcon(),
3443
+ React__default.createElement("h3", { className: "text-xl font-semibold text-white" }, error.type === "network"
3444
+ ? "Network Error"
3445
+ : error.type === "src"
3446
+ ? "Video Unavailable"
3447
+ : "Playback Error"),
3448
+ React__default.createElement("p", { className: "text-sm text-gray-400 max-w-md" }, error.message),
3449
+ 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" },
3450
+ React__default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
3451
+ 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" })),
3452
+ "Retry"))));
3453
+ });
3454
+ ErrorOverlay.displayName = "ErrorOverlay";
3455
+
3456
+ 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";
3457
+ styleInject(css_248z$1,{"insertAt":"top"});
3458
+
3459
+ 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";
1562
3460
  styleInject(css_248z,{"insertAt":"top"});
1563
3461
 
1564
- 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, }) => {
1565
- const { setVideoRef, setVideoWrapperRef, videoRef } = useVideoStore();
3462
+ const VideoPlayer = React__default.memo(({ video, style, events, features }) => {
3463
+ const { src: trackSrc, title: trackTitle, poster: trackPoster, type, isTrailer, showControls = true, isMute = false, startFrom, } = video;
3464
+ const { className, width, height, subtitleStyle } = style || {};
3465
+ const { onEnded, onError, onClose, onWatchHistoryUpdate } = events || {};
3466
+ const { timeCodes, getPreviewScreenUrl, tracking, subtitles, episodeList, currentEpisodeIndex = 0, intro, nextEpisodeConfig, ads, } = features || {};
3467
+ const { setVideoWrapperRef } = useVideoStore(useShallow((state) => ({
3468
+ setVideoWrapperRef: state.setVideoWrapperRef,
3469
+ })));
3470
+ const effectiveAds = React__default.useMemo(() => (isTrailer ? undefined : ads), [ads, isTrailer]);
3471
+ const hasPreRoll = React__default.useMemo(() => Boolean(effectiveAds?.preRoll), [effectiveAds?.preRoll]);
3472
+ const { registerVideoRef, videoRef, isAdPlaying, currentAd, initialAdFinished, shouldCoverMainVideo, shouldShowPlaceholder, } = usePrimaryVideoLifecycle({
3473
+ hasPreRoll,
3474
+ trackSrc,
3475
+ });
3476
+ const onWatchHistoryUpdateRef = React__default.useRef(onWatchHistoryUpdate);
3477
+ React__default.useEffect(() => {
3478
+ onWatchHistoryUpdateRef.current = onWatchHistoryUpdate;
3479
+ }, [onWatchHistoryUpdate]);
3480
+ const getWatchHistoryData = React__default.useCallback(() => {
3481
+ const video = useVideoStore.getState().videoRef;
3482
+ if (!video || !video.duration || isNaN(video.duration))
3483
+ return null;
3484
+ const currentTime = video.currentTime || 0;
3485
+ const duration = video.duration;
3486
+ const progress = Math.round((currentTime / duration) * 100);
3487
+ const isCompleted = progress >= 90;
3488
+ return {
3489
+ currentTime,
3490
+ duration,
3491
+ progress,
3492
+ isCompleted,
3493
+ watchedAt: Date.now(),
3494
+ };
3495
+ }, []);
3496
+ const handleClose = React__default.useCallback(() => {
3497
+ const historyData = getWatchHistoryData();
3498
+ if (historyData && onWatchHistoryUpdate) {
3499
+ onWatchHistoryUpdate(historyData);
3500
+ }
3501
+ onClose?.();
3502
+ }, [getWatchHistoryData, onWatchHistoryUpdate, onClose]);
3503
+ const overlayConfig = React__default.useMemo(() => ({
3504
+ headerConfig: {
3505
+ config: {
3506
+ isTrailer: isTrailer,
3507
+ title: trackTitle,
3508
+ onClose: handleClose,
3509
+ videoRef: videoRef,
3510
+ },
3511
+ },
3512
+ bottomConfig: {
3513
+ config: {
3514
+ seekBarConfig: {
3515
+ timeCodes: timeCodes,
3516
+ trackColor: "red",
3517
+ getPreviewScreenUrl,
3518
+ },
3519
+ },
3520
+ },
3521
+ }), [
3522
+ isTrailer,
3523
+ trackTitle,
3524
+ handleClose,
3525
+ videoRef,
3526
+ timeCodes,
3527
+ getPreviewScreenUrl,
3528
+ ]);
3529
+ const adOverlayConfig = React__default.useMemo(() => ({
3530
+ config: {
3531
+ headerConfig: {
3532
+ config: {
3533
+ isTrailer: isTrailer,
3534
+ title: trackTitle,
3535
+ onClose: handleClose,
3536
+ },
3537
+ },
3538
+ bottomConfig: {
3539
+ config: {
3540
+ seekBarConfig: {
3541
+ timeCodes: timeCodes,
3542
+ trackColor: "red",
3543
+ getPreviewScreenUrl,
3544
+ },
3545
+ },
3546
+ },
3547
+ },
3548
+ }), [isTrailer, trackTitle, handleClose, timeCodes, getPreviewScreenUrl]);
1566
3549
  useVideoSource(trackSrc, type);
1567
3550
  useSubtitles(subtitles);
1568
3551
  useSubtitleStyling(subtitleStyle);
1569
- useVideoTracking(tracking, episodeList, currentEpisodeIndex, onClose);
3552
+ useVideoTracking(tracking, episodeList, currentEpisodeIndex, handleClose);
1570
3553
  const { showSkipIntro, handleSkipIntro } = useIntroSkip(intro);
1571
3554
  useEpisodes(episodeList, currentEpisodeIndex, nextEpisodeConfig);
1572
3555
  const { onSeeked, onTimeUpdate, onLoadedMetadata, onProgress, onPlay, onPause, onEnded: onEndedHook, } = useVideoEvents();
3556
+ const { skipAd } = useAdManager(effectiveAds);
3557
+ const { error, handleVideoError, retry } = useVideoError();
3558
+ const hasResumedRef = React__default.useRef(false);
3559
+ React__default.useEffect(() => {
3560
+ return () => {
3561
+ const historyData = getWatchHistoryData();
3562
+ if (historyData && onWatchHistoryUpdateRef.current) {
3563
+ onWatchHistoryUpdateRef.current(historyData);
3564
+ }
3565
+ };
3566
+ }, [getWatchHistoryData]);
3567
+ React__default.useEffect(() => {
3568
+ if (!videoRef || !startFrom || hasResumedRef.current)
3569
+ return;
3570
+ const handleCanPlay = () => {
3571
+ if (!hasResumedRef.current && startFrom > 0) {
3572
+ videoRef.currentTime = startFrom;
3573
+ hasResumedRef.current = true;
3574
+ }
3575
+ };
3576
+ videoRef.addEventListener("canplay", handleCanPlay);
3577
+ return () => videoRef.removeEventListener("canplay", handleCanPlay);
3578
+ }, [videoRef, startFrom]);
1573
3579
  return (React__default.createElement("div", { ref: setVideoWrapperRef, className: `video-player ${height || "h-full"} ${width || "w-full"} mx-auto absolute` },
1574
3580
  trackPoster && (React__default.createElement("div", { className: "pip-poster absolute inset-0 bg-center bg-cover hidden", style: { backgroundImage: `url(${trackPoster})` } })),
1575
- React__default.createElement("video", { autoPlay: true, playsInline: true, preload: "metadata", ref: setVideoRef, 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) => {
3581
+ 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) => {
1576
3582
  onEndedHook(e);
1577
3583
  onEnded?.(e);
1578
3584
  }, onError: (e) => {
3585
+ handleVideoError(e);
1579
3586
  onError?.(e);
1580
- }, muted: isMute, className: `w-full h-full relative ${className}` }),
1581
- showControls && (React__default.createElement(Overlay, { config: {
1582
- headerConfig: {
1583
- config: {
1584
- isTrailer: isTrailer,
1585
- title: trackTitle,
1586
- onClose: onClose,
1587
- videoRef: videoRef,
1588
- },
1589
- },
1590
- bottomConfig: {
1591
- config: {
1592
- seekBarConfig: {
1593
- timeCodes: timeCodes,
1594
- trackColor: "red",
1595
- getPreviewScreenUrl,
1596
- },
1597
- },
1598
- },
1599
- } })),
3587
+ }, autoPlay: !hasPreRoll, muted: isMute, className: `w-full h-full relative ${className || ""} ${shouldCoverMainVideo ? "opacity-0" : "opacity-100"} transition-opacity duration-200 ease-out` }),
3588
+ shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm" },
3589
+ React__default.createElement(Loader, { className: "w-24 h-24 lg:w-32 lg:h-32 animate-spin text-white" }))),
3590
+ showControls && initialAdFinished && (React__default.createElement(Overlay, { config: overlayConfig })),
1600
3591
  React__default.createElement(SubtitleOverlay, { styleConfig: subtitleStyle }),
1601
- showSkipIntro && (React__default.createElement(VideoActionButton, { text: "Skip Intro", onClick: handleSkipIntro, position: "left" }))));
1602
- };
3592
+ showSkipIntro && !isAdPlaying && initialAdFinished && (React__default.createElement(VideoActionButton, { text: "Skip Intro", onClick: handleSkipIntro, position: "left" })),
3593
+ isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config: adOverlayConfig })),
3594
+ error && onError && React__default.createElement(ErrorOverlay, { error: error, onRetry: retry })));
3595
+ });
3596
+ VideoPlayer.displayName = "VideoPlayer";
1603
3597
 
1604
3598
  export { VideoPlayer, useVideoStore };