@zezosoft/react-player 0.0.9 → 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.
- package/README.md +536 -146
- package/dist/VideoPlayer/components/ErrorOverlay.d.ts +8 -0
- package/dist/VideoPlayer/hooks/index.d.ts +2 -0
- package/dist/VideoPlayer/hooks/useNetworkSpeed.d.ts +7 -0
- package/dist/VideoPlayer/hooks/useVideoError.d.ts +7 -0
- package/dist/VideoPlayer/hooks/useVideoTracking.d.ts +2 -2
- package/dist/VideoPlayer/types/AdTypes.d.ts +0 -3
- package/dist/VideoPlayer/types/VideoPlayerTypes.d.ts +31 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +851 -296
- package/dist/store/slices/errorSlice.d.ts +5 -0
- package/dist/store/slices/index.d.ts +1 -0
- package/dist/store/types/StoreTypes.d.ts +11 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import React__default, { memo, useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
3
3
|
import { useShallow } from 'zustand/react/shallow';
|
|
4
|
+
import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
|
|
4
5
|
import { create } from 'zustand';
|
|
5
6
|
import { IoVolumeMuteOutline, IoVolumeHighOutline } from 'react-icons/io5';
|
|
6
7
|
import { IoMdClose } from 'react-icons/io';
|
|
7
|
-
import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
|
|
8
8
|
import screenfull from 'screenfull';
|
|
9
9
|
import Hls from 'hls.js';
|
|
10
10
|
import * as dashjs from 'dashjs';
|
|
@@ -36,7 +36,7 @@ function styleInject(css, ref) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-yellow-500: oklch(79.5% 0.184 86.047);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wide: 0.025em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .block {\n display: block;\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-24 {\n height: calc(var(--spacing) * 24);\n }\n .h-full {\n height: 100%;\n }\n .max-h-80 {\n max-height: calc(var(--spacing) * 80);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-24 {\n width: calc(var(--spacing) * 24);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[2px\\] {\n width: 2px;\n }\n .w-\\[10vw\\] {\n width: 10vw;\n }\n .w-\\[15vw\\] {\n width: 15vw;\n }\n .w-\\[720px\\] {\n width: 720px;\n }\n .w-fit {\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .rotate-45 {\n rotate: 45deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-not-allowed {\n cursor: not-allowed;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-3 {\n gap: calc(var(--spacing) * 3);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-\\[5px\\] {\n border-radius: 5px;\n }\n .rounded-\\[7px\\] {\n border-radius: 7px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-gray-600 {\n border-color: var(--color-gray-600);\n }\n .border-gray-700\\/60 {\n border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-gray-700) 60%, transparent);\n }\n }\n .border-white\\/10 {\n border-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .border-white\\/30 {\n border-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .border-white\\/40 {\n border-color: color-mix(in srgb, #fff 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 40%, transparent);\n }\n }\n .bg-\\[\\#2D2F31\\] {\n background-color: #2D2F31;\n }\n .bg-\\[\\#3a4049\\] {\n background-color: #3a4049;\n }\n .bg-\\[\\#454545\\] {\n background-color: #454545;\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/60 {\n background-color: color-mix(in srgb, #000 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 60%, transparent);\n }\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-gray-500 {\n background-color: var(--color-gray-500);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-white\\/5 {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n .bg-white\\/10 {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .bg-white\\/20 {\n background-color: color-mix(in srgb, #fff 20%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 20%, transparent);\n }\n }\n .bg-white\\/80 {\n background-color: color-mix(in srgb, #fff 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 80%, transparent);\n }\n }\n .bg-linear-to-b {\n --tw-gradient-position: to bottom;\n @supports (background-image: linear-gradient(in lab, red, red)) {\n --tw-gradient-position: to bottom in oklab;\n }\n background-image: linear-gradient(var(--tw-gradient-stops));\n }\n .from-black {\n --tw-gradient-from: var(--color-black);\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .from-black\\/80 {\n --tw-gradient-from: color-mix(in srgb, #000 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-from: color-mix(in oklab, var(--color-black) 80%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .via-transparent {\n --tw-gradient-via: transparent;\n --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-via-stops);\n }\n .to-black\\/90 {\n --tw-gradient-to: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-to: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\n }\n .object-contain {\n object-fit: contain;\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-10 {\n padding: calc(var(--spacing) * 10);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-5 {\n padding-inline: calc(var(--spacing) * 5);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .px-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .px-20 {\n padding-inline: calc(var(--spacing) * 20);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-6 {\n padding-top: calc(var(--spacing) * 6);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pb-4 {\n padding-bottom: calc(var(--spacing) * 4);\n }\n .pb-6 {\n padding-bottom: calc(var(--spacing) * 6);\n }\n .pb-10 {\n padding-bottom: calc(var(--spacing) * 10);\n }\n .pb-16 {\n padding-bottom: calc(var(--spacing) * 16);\n }\n .text-left {\n text-align: left;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wide {\n --tw-tracking: var(--tracking-wide);\n letter-spacing: var(--tracking-wide);\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-gray-200 {\n color: var(--color-gray-200);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-900 {\n color: var(--color-gray-900);\n }\n .text-sky-300 {\n color: var(--color-sky-300);\n }\n .text-white {\n color: var(--color-white);\n }\n .text-yellow-500 {\n color: var(--color-yellow-500);\n }\n .uppercase {\n text-transform: uppercase;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .backdrop-blur-md {\n --tw-backdrop-blur: blur(var(--blur-md));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .select-none {\n -webkit-user-select: none;\n user-select: none;\n }\n .hover\\:scale-105 {\n &:hover {\n @media (hover: hover) {\n --tw-scale-x: 105%;\n --tw-scale-y: 105%;\n --tw-scale-z: 105%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n }\n .hover\\:border-white\\/50 {\n &:hover {\n @media (hover: hover) {\n border-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n }\n .hover\\:bg-gray-300 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-gray-300);\n }\n }\n }\n .hover\\:bg-white\\/5 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/10 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/30 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 90%, transparent);\n }\n }\n }\n }\n .hover\\:text-gray-200 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n .hover\\:shadow-lg {\n &:hover {\n @media (hover: hover) {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-gray-400 {\n &:focus {\n --tw-ring-color: var(--color-gray-400);\n }\n }\n .focus\\:ring-offset-1 {\n &:focus {\n --tw-ring-offset-width: 1px;\n --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n .active\\:scale-95 {\n &:active {\n --tw-scale-x: 95%;\n --tw-scale-y: 95%;\n --tw-scale-z: 95%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:bg-white\\/50 {\n &:disabled {\n background-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .lg\\:h-32 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 32);\n }\n }\n .lg\\:w-32 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 32);\n }\n }\n .lg\\:pb-12 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 12);\n }\n }\n .lg\\:text-2xl {\n @media (width >= 64rem) {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n }\n .lg\\:text-3xl {\n @media (width >= 64rem) {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n }\n .lg\\:text-base {\n @media (width >= 64rem) {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\n}\n.icon-class {\n height: calc(var(--spacing) * 14);\n width: calc(var(--spacing) * 14);\n cursor: pointer;\n color: var(--color-gray-400);\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-gradient-position {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-via {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-to {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-via-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 0%;\n}\n@property --tw-gradient-via-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 50%;\n}\n@property --tw-gradient-to-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-backdrop-blur {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-scale-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-gradient-position: initial;\n --tw-gradient-from: #0000;\n --tw-gradient-via: #0000;\n --tw-gradient-to: #0000;\n --tw-gradient-stops: initial;\n --tw-gradient-via-stops: initial;\n --tw-gradient-from-position: 0%;\n --tw-gradient-via-position: 50%;\n --tw-gradient-to-position: 100%;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n }\n }\n}\n";
|
|
39
|
+
var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-red-400: oklch(70.4% 0.191 22.216);\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-red-600: oklch(57.7% 0.245 27.325);\n --color-red-700: oklch(50.5% 0.213 27.518);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --container-md: 28rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wider: 0.05em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .container {\n width: 100%;\n @media (width >= 40rem) {\n max-width: 40rem;\n }\n @media (width >= 48rem) {\n max-width: 48rem;\n }\n @media (width >= 64rem) {\n max-width: 64rem;\n }\n @media (width >= 80rem) {\n max-width: 80rem;\n }\n @media (width >= 96rem) {\n max-width: 96rem;\n }\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mt-4 {\n margin-top: calc(var(--spacing) * 4);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-1 {\n margin-left: calc(var(--spacing) * 1);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-12 {\n height: calc(var(--spacing) * 12);\n }\n .h-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
40
|
styleInject(css_248z$4,{"insertAt":"top"});
|
|
41
41
|
|
|
42
42
|
const createVideoRefsSlice = (set) => ({
|
|
@@ -138,6 +138,12 @@ const createAdsSlice = (set, get) => ({
|
|
|
138
138
|
setAdVideoRef: (adVideoRef) => set({ adVideoRef }),
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
const createErrorSlice = (set) => ({
|
|
142
|
+
error: null,
|
|
143
|
+
setError: (error) => set({ error }),
|
|
144
|
+
clearError: () => set({ error: null }),
|
|
145
|
+
});
|
|
146
|
+
|
|
141
147
|
const createResetSlice = (set, get) => ({
|
|
142
148
|
resetStore: () => {
|
|
143
149
|
const safeStopMediaElement = (media) => {
|
|
@@ -211,6 +217,7 @@ const useVideoStore = create()((set, get, store) => ({
|
|
|
211
217
|
...createEpisodesSlice(set),
|
|
212
218
|
...createIntroSlice(set),
|
|
213
219
|
...createAdsSlice(set),
|
|
220
|
+
...createErrorSlice(set),
|
|
214
221
|
...createResetSlice(set, get),
|
|
215
222
|
}));
|
|
216
223
|
|
|
@@ -635,7 +642,26 @@ const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThum
|
|
|
635
642
|
React__default.createElement(Thumb, { max: max, currentTime: currentTime, isThumbActive: isThumbActive, trackColor: trackColor })));
|
|
636
643
|
};
|
|
637
644
|
|
|
638
|
-
|
|
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 }) => {
|
|
639
665
|
const { videoRef, currentTime, isFullscreen, bufferedProgress, isAdPlaying } = useVideoStore(useShallow((state) => ({
|
|
640
666
|
videoRef: state.videoRef,
|
|
641
667
|
currentTime: state.currentTime,
|
|
@@ -658,20 +684,27 @@ const BottomControls = ({ config }) => {
|
|
|
658
684
|
}
|
|
659
685
|
return secondsToMilliseconds(duration * (bufferedValue / 100));
|
|
660
686
|
}, [bufferedValue, duration]);
|
|
661
|
-
|
|
662
|
-
const
|
|
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]);
|
|
663
695
|
if (isAdPlaying) {
|
|
664
696
|
return null;
|
|
665
697
|
}
|
|
666
698
|
return (React__default.createElement("div", { className: "px-10" },
|
|
667
|
-
React__default.createElement(VideoSeekSlider, { max:
|
|
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 }),
|
|
668
700
|
React__default.createElement("div", { className: `pt-6 ${isFullscreen ? "pb-10" : "pb-16"} lg:pb-12 flex items-center gap-4 text-white` },
|
|
669
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),
|
|
670
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" }, "/"),
|
|
671
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))));
|
|
672
|
-
};
|
|
704
|
+
});
|
|
705
|
+
BottomControls.displayName = "BottomControls";
|
|
673
706
|
|
|
674
|
-
var css_248z$2 = ".icon-button {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.icon-button:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .icon-button {\n width: 32px;\n height: 32px;\n }\n}\n\n.fullscreen-icon {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.fullscreen-icon:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .fullscreen-icon {\n width: 32px;\n height: 32px;\n }\n}\n\n.pip-toggle {\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.pip-toggle:hover {\n color: #e5e7eb;\n}\n\n.pip-icon {\n width: 15px;\n height: 15px;\n}\n\n@media (min-width: 1024px) {\n .pip-icon {\n width: 28px;\n height: 28px;\n }\n}\n";
|
|
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}";
|
|
675
708
|
styleInject(css_248z$2,{"insertAt":"top"});
|
|
676
709
|
|
|
677
710
|
const FullScreenToggle = ({ isFullScreen, onClick, className = "fullscreen-icon", }) => {
|
|
@@ -754,13 +787,44 @@ const Tooltip = ({ children, title, position = "top", className, }) => {
|
|
|
754
787
|
|
|
755
788
|
const Settings = ({ iconClassName }) => {
|
|
756
789
|
const { qualityLevels, activeQuality, currentQuality, subtitles, activeSubtitle, setActiveSubtitle, videoRef, streamType, } = useVideoStore();
|
|
757
|
-
|
|
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());
|
|
758
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]);
|
|
759
816
|
const handleSpeedChange = (newSpeed) => {
|
|
760
817
|
setSpeed(newSpeed);
|
|
761
818
|
if (videoRef) {
|
|
762
819
|
videoRef.playbackRate = newSpeed;
|
|
763
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
|
+
}
|
|
764
828
|
};
|
|
765
829
|
const isAdaptiveStream = streamType === "hls" || streamType === "dash";
|
|
766
830
|
const qualityOptions = React.useMemo(() => {
|
|
@@ -928,7 +992,7 @@ const Settings = ({ iconClassName }) => {
|
|
|
928
992
|
|
|
929
993
|
const ControlsHeader = ({ config }) => {
|
|
930
994
|
const iconClassName = "icon-button";
|
|
931
|
-
const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, } = useVideoStore(useShallow((state) => ({
|
|
995
|
+
const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, adCurrentTime, } = useVideoStore(useShallow((state) => ({
|
|
932
996
|
videoWrapperRef: state.videoWrapperRef,
|
|
933
997
|
videoRef: state.videoRef,
|
|
934
998
|
adVideoRef: state.adVideoRef,
|
|
@@ -938,7 +1002,40 @@ const ControlsHeader = ({ config }) => {
|
|
|
938
1002
|
isAdPlaying: state.isAdPlaying,
|
|
939
1003
|
muted: state.muted,
|
|
940
1004
|
setMuted: state.setMuted,
|
|
1005
|
+
adCurrentTime: state.adCurrentTime,
|
|
941
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]);
|
|
942
1039
|
const [isPipActive, setIsPipActive] = React.useState(false);
|
|
943
1040
|
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
|
944
1041
|
const handleFullscreen = () => {
|
|
@@ -1004,7 +1101,9 @@ const ControlsHeader = ({ config }) => {
|
|
|
1004
1101
|
}
|
|
1005
1102
|
};
|
|
1006
1103
|
const renderAdHeader = () => (React.createElement("div", { className: "flex items-center gap-4" },
|
|
1007
|
-
React.createElement("span", { className: "inline-flex items-center rounded
|
|
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)))));
|
|
1008
1107
|
const renderVideoHeader = () => (React.createElement("div", { className: "flex" },
|
|
1009
1108
|
React.createElement("div", null,
|
|
1010
1109
|
React.createElement("h1", { className: "text-gray-200 text-lg lg:text-2xl font-semibold" }, episodeList.length > 0
|
|
@@ -1062,7 +1161,10 @@ const MiddleControls = () => {
|
|
|
1062
1161
|
setIsPlaying: state.setIsPlaying,
|
|
1063
1162
|
isAdPlaying: state.isAdPlaying,
|
|
1064
1163
|
})));
|
|
1065
|
-
const
|
|
1164
|
+
const { setIsBuffering } = useVideoStore(useShallow((state) => ({
|
|
1165
|
+
setIsBuffering: state.setIsBuffering,
|
|
1166
|
+
})));
|
|
1167
|
+
const [isBuffering, setIsBufferingLocal] = useState(false);
|
|
1066
1168
|
const videoElement = isAdPlaying ? adVideoRef : videoRef;
|
|
1067
1169
|
const resetControlsVisibility = useCallback(() => {
|
|
1068
1170
|
if (typeof window === "undefined") {
|
|
@@ -1103,15 +1205,33 @@ const MiddleControls = () => {
|
|
|
1103
1205
|
useEffect(() => {
|
|
1104
1206
|
if (!videoElement)
|
|
1105
1207
|
return;
|
|
1106
|
-
const handleWaiting = () =>
|
|
1107
|
-
|
|
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
|
+
};
|
|
1108
1224
|
videoElement.addEventListener("waiting", handleWaiting);
|
|
1109
1225
|
videoElement.addEventListener("playing", handlePlaying);
|
|
1226
|
+
videoElement.addEventListener("canplay", handleCanPlay);
|
|
1227
|
+
videoElement.addEventListener("stalled", handleStalled);
|
|
1110
1228
|
return () => {
|
|
1111
1229
|
videoElement.removeEventListener("waiting", handleWaiting);
|
|
1112
1230
|
videoElement.removeEventListener("playing", handlePlaying);
|
|
1231
|
+
videoElement.removeEventListener("canplay", handleCanPlay);
|
|
1232
|
+
videoElement.removeEventListener("stalled", handleStalled);
|
|
1113
1233
|
};
|
|
1114
|
-
}, [videoElement, isAdPlaying]);
|
|
1234
|
+
}, [videoElement, isAdPlaying, setIsBuffering]);
|
|
1115
1235
|
useEffect(() => {
|
|
1116
1236
|
const handleKeyDown = (e) => {
|
|
1117
1237
|
if (!videoElement || isAdPlaying)
|
|
@@ -1143,7 +1263,8 @@ const MiddleControls = () => {
|
|
|
1143
1263
|
]);
|
|
1144
1264
|
if (isAdPlaying) {
|
|
1145
1265
|
return (React__default.createElement("div", { className: "flex justify-center items-center" },
|
|
1146
|
-
React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(
|
|
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)) })));
|
|
1147
1268
|
}
|
|
1148
1269
|
return (React__default.createElement("div", { className: "flex justify-center items-center" },
|
|
1149
1270
|
React__default.createElement(ControlButton, { onClick: handleBackward, className: "w-[15vw]", icon: React__default.createElement(BackwardIcon, null) }),
|
|
@@ -1159,7 +1280,7 @@ const VideoPlayerControls = ({ config }) => {
|
|
|
1159
1280
|
React.createElement(BottomControls, { config: config?.bottomConfig?.config }))));
|
|
1160
1281
|
};
|
|
1161
1282
|
|
|
1162
|
-
const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "left", }) => {
|
|
1283
|
+
const VideoActionButton = React__default.memo(({ text, onClick, icon, disabled = false, position = "left", }) => {
|
|
1163
1284
|
// Increase icon size and apply consistent color to icon
|
|
1164
1285
|
const renderedIcon = icon
|
|
1165
1286
|
? React__default.cloneElement(icon, {
|
|
@@ -1170,9 +1291,10 @@ const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "
|
|
|
1170
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 " },
|
|
1171
1292
|
renderedIcon && React__default.createElement("span", { className: "inline mr-2" }, renderedIcon),
|
|
1172
1293
|
text)));
|
|
1173
|
-
};
|
|
1294
|
+
});
|
|
1295
|
+
VideoActionButton.displayName = "VideoActionButton";
|
|
1174
1296
|
|
|
1175
|
-
const Overlay = ({ config }) => {
|
|
1297
|
+
const Overlay = React__default.memo(({ config }) => {
|
|
1176
1298
|
const controlsTimerRef = useRef(null);
|
|
1177
1299
|
const containerRef = useRef(null);
|
|
1178
1300
|
const { setControls, controls, showCountdown, countdownTime, setShowCountdown, setAutoPlayNext, setCurrentEpisodeIndex, episodeList, setCountdownTime, videoRef, currentEpisodeIndex, isAdPlaying, } = useVideoStore(useShallow((state) => ({
|
|
@@ -1219,7 +1341,9 @@ const Overlay = ({ config }) => {
|
|
|
1219
1341
|
let timer;
|
|
1220
1342
|
if (showCountdown && countdownTime > 0 && episodeList.length > 0) {
|
|
1221
1343
|
timer = setInterval(() => {
|
|
1222
|
-
|
|
1344
|
+
const currentTime = useVideoStore.getState().countdownTime;
|
|
1345
|
+
const next = currentTime - 1;
|
|
1346
|
+
setCountdownTime(next > 0 ? next : 0);
|
|
1223
1347
|
}, 1000);
|
|
1224
1348
|
}
|
|
1225
1349
|
return () => {
|
|
@@ -1239,7 +1363,7 @@ const Overlay = ({ config }) => {
|
|
|
1239
1363
|
window.removeEventListener(CONTROL_INTERACTION_EVENT, handleExternalInteraction);
|
|
1240
1364
|
};
|
|
1241
1365
|
}, [handleControlsInteraction]);
|
|
1242
|
-
const handleNextEpisodeManually = () => {
|
|
1366
|
+
const handleNextEpisodeManually = useCallback(() => {
|
|
1243
1367
|
const nextIndex = currentEpisodeIndex + 1;
|
|
1244
1368
|
if (nextIndex < episodeList.length && videoRef && episodeList[nextIndex]) {
|
|
1245
1369
|
setCurrentEpisodeIndex(nextIndex);
|
|
@@ -1253,63 +1377,89 @@ const Overlay = ({ config }) => {
|
|
|
1253
1377
|
else if (onClose) {
|
|
1254
1378
|
onClose();
|
|
1255
1379
|
}
|
|
1256
|
-
}
|
|
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]);
|
|
1257
1395
|
return (React__default.createElement("div", { id: "videoPlayerControls", ref: containerRef, className: "absolute inset-0", onMouseMove: handleMouseEnter },
|
|
1258
1396
|
controls && !isAdPlaying && React__default.createElement(VideoPlayerControls, { config: config }),
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
};
|
|
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";
|
|
1263
1400
|
|
|
1264
|
-
const SubtitleOverlay = ({ styleConfig }) => {
|
|
1265
|
-
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
|
+
})));
|
|
1266
1406
|
const [currentSubtitle, setCurrentSubtitle] = useState("");
|
|
1267
1407
|
const [isVisible, setIsVisible] = useState(false);
|
|
1408
|
+
const rafRef = useRef(null);
|
|
1268
1409
|
useEffect(() => {
|
|
1269
1410
|
if (!videoRef)
|
|
1270
1411
|
return;
|
|
1271
1412
|
const handleTimeUpdate = () => {
|
|
1272
|
-
if (
|
|
1273
|
-
setCurrentSubtitle("");
|
|
1274
|
-
setIsVisible(false);
|
|
1413
|
+
if (rafRef.current !== null)
|
|
1275
1414
|
return;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
+
}
|
|
1293
1442
|
}
|
|
1294
|
-
|
|
1295
|
-
cueText =
|
|
1443
|
+
catch (_error) {
|
|
1444
|
+
cueText = "";
|
|
1296
1445
|
}
|
|
1446
|
+
setCurrentSubtitle(cueText);
|
|
1447
|
+
setIsVisible(!!cueText);
|
|
1297
1448
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1449
|
+
else {
|
|
1450
|
+
setCurrentSubtitle("");
|
|
1451
|
+
setIsVisible(false);
|
|
1300
1452
|
}
|
|
1301
|
-
setCurrentSubtitle(cueText);
|
|
1302
|
-
setIsVisible(!!cueText);
|
|
1303
|
-
}
|
|
1304
|
-
else {
|
|
1305
|
-
setCurrentSubtitle("");
|
|
1306
|
-
setIsVisible(false);
|
|
1307
1453
|
}
|
|
1308
|
-
}
|
|
1454
|
+
});
|
|
1309
1455
|
};
|
|
1310
1456
|
videoRef.addEventListener("timeupdate", handleTimeUpdate);
|
|
1311
1457
|
return () => {
|
|
1312
1458
|
videoRef.removeEventListener("timeupdate", handleTimeUpdate);
|
|
1459
|
+
if (rafRef.current !== null) {
|
|
1460
|
+
cancelAnimationFrame(rafRef.current);
|
|
1461
|
+
rafRef.current = null;
|
|
1462
|
+
}
|
|
1313
1463
|
};
|
|
1314
1464
|
}, [videoRef, activeSubtitle]);
|
|
1315
1465
|
useEffect(() => {
|
|
@@ -1366,7 +1516,8 @@ const SubtitleOverlay = ({ styleConfig }) => {
|
|
|
1366
1516
|
pointerEvents: "none",
|
|
1367
1517
|
};
|
|
1368
1518
|
return React__default.createElement("div", { style: subtitleStyle }, currentSubtitle);
|
|
1369
|
-
};
|
|
1519
|
+
});
|
|
1520
|
+
SubtitleOverlay.displayName = "SubtitleOverlay";
|
|
1370
1521
|
|
|
1371
1522
|
const HLS_CONFIG = {
|
|
1372
1523
|
enableWorker: true,
|
|
@@ -1821,50 +1972,62 @@ const useVideoSource = (trackSrc, type) => {
|
|
|
1821
1972
|
|
|
1822
1973
|
const useSubtitles = (subtitles) => {
|
|
1823
1974
|
const { videoRef, activeSubtitle, setSubtitles } = useVideoStore();
|
|
1975
|
+
const timeoutIdsRef = useRef([]);
|
|
1824
1976
|
useEffect(() => {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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(() => {
|
|
1845
2012
|
const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
|
|
1846
|
-
if (textTrack) {
|
|
2013
|
+
if (textTrack && textTrack.mode !== "showing") {
|
|
1847
2014
|
textTrack.mode = "showing";
|
|
1848
2015
|
}
|
|
1849
|
-
};
|
|
1850
|
-
|
|
1851
|
-
const attempts = [100, 500, 1000];
|
|
1852
|
-
attempts.forEach((delay) => {
|
|
1853
|
-
setTimeout(() => {
|
|
1854
|
-
const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
|
|
1855
|
-
if (textTrack && textTrack.mode !== "showing") {
|
|
1856
|
-
textTrack.mode = "showing";
|
|
1857
|
-
}
|
|
1858
|
-
}, delay);
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
else {
|
|
1863
|
-
Array.from(videoRef.textTracks).forEach((track) => {
|
|
1864
|
-
track.mode = "disabled";
|
|
2016
|
+
}, delay);
|
|
2017
|
+
timeoutIdsRef.current.push(timeoutId);
|
|
1865
2018
|
});
|
|
1866
2019
|
}
|
|
1867
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
|
+
};
|
|
1868
2031
|
}, [videoRef, activeSubtitle, subtitles]);
|
|
1869
2032
|
useEffect(() => {
|
|
1870
2033
|
if (subtitles) {
|
|
@@ -1919,29 +2082,31 @@ const useSubtitleStyling = (config) => {
|
|
|
1919
2082
|
};
|
|
1920
2083
|
|
|
1921
2084
|
const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
|
|
1922
|
-
const { videoRef,
|
|
1923
|
-
const startTime = useRef(null);
|
|
2085
|
+
const { videoRef, setShowCountdown } = useVideoStore();
|
|
1924
2086
|
const isViewCounted = useRef(false);
|
|
2087
|
+
const lastVideoSrcRef = useRef(null);
|
|
2088
|
+
// Reset view count when video source changes
|
|
1925
2089
|
useEffect(() => {
|
|
1926
2090
|
if (!videoRef)
|
|
1927
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
|
|
1928
2103
|
const onPlay = () => {
|
|
1929
2104
|
if (!isViewCounted.current) {
|
|
1930
2105
|
isViewCounted.current = true;
|
|
1931
2106
|
tracking?.onViewed?.();
|
|
1932
2107
|
}
|
|
1933
|
-
startTime.current = Date.now();
|
|
1934
|
-
setIsPlaying(true);
|
|
1935
|
-
};
|
|
1936
|
-
const onPause = () => {
|
|
1937
|
-
if (startTime.current) {
|
|
1938
|
-
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
1939
|
-
const getCurrentTime = localStorage.getItem("current_time");
|
|
1940
|
-
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
1941
|
-
startTime.current = null;
|
|
1942
|
-
}
|
|
1943
|
-
setIsPlaying(false);
|
|
1944
2108
|
};
|
|
2109
|
+
// Handle episode end logic - playback state is handled by useVideoEvents
|
|
1945
2110
|
const onEnded = () => {
|
|
1946
2111
|
if (episodeList &&
|
|
1947
2112
|
episodeList.length > 0 &&
|
|
@@ -1959,11 +2124,9 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
|
|
|
1959
2124
|
}
|
|
1960
2125
|
};
|
|
1961
2126
|
videoRef.addEventListener("play", onPlay);
|
|
1962
|
-
videoRef.addEventListener("pause", onPause);
|
|
1963
2127
|
videoRef.addEventListener("ended", onEnded);
|
|
1964
2128
|
return () => {
|
|
1965
2129
|
videoRef.removeEventListener("play", onPlay);
|
|
1966
|
-
videoRef.removeEventListener("pause", onPause);
|
|
1967
2130
|
videoRef.removeEventListener("ended", onEnded);
|
|
1968
2131
|
};
|
|
1969
2132
|
}, [
|
|
@@ -1972,29 +2135,8 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
|
|
|
1972
2135
|
currentEpisodeIndex,
|
|
1973
2136
|
onClose,
|
|
1974
2137
|
tracking,
|
|
1975
|
-
setIsPlaying,
|
|
1976
2138
|
setShowCountdown,
|
|
1977
2139
|
]);
|
|
1978
|
-
useEffect(() => {
|
|
1979
|
-
const handleUnload = () => {
|
|
1980
|
-
if (startTime.current) {
|
|
1981
|
-
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
1982
|
-
const getCurrentTime = localStorage.getItem("current_time");
|
|
1983
|
-
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
1984
|
-
}
|
|
1985
|
-
const totalTimeWatched = Number(localStorage.getItem("current_time") || 0);
|
|
1986
|
-
if (totalTimeWatched >= 30) {
|
|
1987
|
-
tracking?.onWatchTimeUpdated?.({
|
|
1988
|
-
watchTime: totalTimeWatched,
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
localStorage.setItem("current_time", "0");
|
|
1992
|
-
};
|
|
1993
|
-
window.addEventListener("unload", handleUnload);
|
|
1994
|
-
return () => {
|
|
1995
|
-
window.removeEventListener("unload", handleUnload);
|
|
1996
|
-
};
|
|
1997
|
-
}, [tracking]);
|
|
1998
2140
|
};
|
|
1999
2141
|
|
|
2000
2142
|
const useIntroSkip = (intro) => {
|
|
@@ -2159,7 +2301,6 @@ const useVideoEvents = () => {
|
|
|
2159
2301
|
const onLoadedMetadata = (e) => {
|
|
2160
2302
|
const duration = e?.currentTarget?.duration;
|
|
2161
2303
|
if (typeof duration === "number" && !Number.isNaN(duration)) {
|
|
2162
|
-
localStorage.setItem("current_time", "0");
|
|
2163
2304
|
setDuration(duration);
|
|
2164
2305
|
}
|
|
2165
2306
|
};
|
|
@@ -2267,8 +2408,13 @@ const useAdManager = (adConfig) => {
|
|
|
2267
2408
|
})));
|
|
2268
2409
|
const preRollPlayedRef = useRef(false);
|
|
2269
2410
|
const postRollPlayedRef = useRef(false);
|
|
2270
|
-
const midRollCheckIntervalRef = useRef(null);
|
|
2271
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);
|
|
2272
2418
|
const stopMediaElement = useCallback((media) => {
|
|
2273
2419
|
if (!media)
|
|
2274
2420
|
return;
|
|
@@ -2288,36 +2434,29 @@ const useAdManager = (adConfig) => {
|
|
|
2288
2434
|
setMidRollQueue([]);
|
|
2289
2435
|
return;
|
|
2290
2436
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
let lastAdTime = 0;
|
|
2306
|
-
for (const ad of ads) {
|
|
2307
|
-
if (ad.time < minGap)
|
|
2308
|
-
continue;
|
|
2309
|
-
if (ad.time > videoDuration - avoidNearEnd)
|
|
2310
|
-
continue;
|
|
2311
|
-
if (ad.time - lastAdTime < minGap)
|
|
2312
|
-
continue;
|
|
2313
|
-
validAds.push(ad);
|
|
2314
|
-
const adLength = Number.isFinite(ad.duration)
|
|
2315
|
-
? ad.duration
|
|
2316
|
-
: 0;
|
|
2317
|
-
lastAdTime = ad.time + adLength;
|
|
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;
|
|
2318
2451
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
|
2321
2460
|
const playPreRollAd = async () => {
|
|
2322
2461
|
if (!adConfig?.preRoll || preRollPlayedRef.current || !videoRef)
|
|
2323
2462
|
return;
|
|
@@ -2333,6 +2472,46 @@ const useAdManager = (adConfig) => {
|
|
|
2333
2472
|
setAdType("pre-roll");
|
|
2334
2473
|
adConfig.onAdStart?.(adBreak);
|
|
2335
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
|
+
]);
|
|
2336
2515
|
const playPostRollAd = async () => {
|
|
2337
2516
|
if (!adConfig?.postRoll || postRollPlayedRef.current || !videoRef)
|
|
2338
2517
|
return;
|
|
@@ -2350,23 +2529,40 @@ const useAdManager = (adConfig) => {
|
|
|
2350
2529
|
const adTypeState = useVideoStore.getState().adType;
|
|
2351
2530
|
const videoRefState = useVideoStore.getState().videoRef;
|
|
2352
2531
|
const adVideoRefState = useVideoStore.getState().adVideoRef;
|
|
2353
|
-
if (!
|
|
2532
|
+
if (!currentAdState)
|
|
2354
2533
|
return;
|
|
2534
|
+
// Reset processing flag
|
|
2535
|
+
isProcessingAdRef.current = false;
|
|
2536
|
+
// Clean up ad video element
|
|
2355
2537
|
if (adVideoRefState) {
|
|
2356
2538
|
stopMediaElement(adVideoRefState);
|
|
2357
2539
|
setAdVideoRef(null);
|
|
2358
2540
|
}
|
|
2541
|
+
// Reset ad state
|
|
2359
2542
|
setIsAdPlaying(false);
|
|
2360
2543
|
setCurrentAd(null);
|
|
2361
2544
|
setAdType(null);
|
|
2362
2545
|
setAdCurrentTime(0);
|
|
2363
2546
|
setCanSkipAd(false);
|
|
2364
2547
|
setSkipCountdown(0);
|
|
2548
|
+
// Call end callback
|
|
2365
2549
|
adConfig?.onAdEnd?.(currentAdState);
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
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);
|
|
2370
2566
|
}
|
|
2371
2567
|
resumeAfterAdRef.current = false;
|
|
2372
2568
|
}, [
|
|
@@ -2443,46 +2639,87 @@ const useAdManager = (adConfig) => {
|
|
|
2443
2639
|
videoRef.removeEventListener("canplay", handleCanPlay);
|
|
2444
2640
|
};
|
|
2445
2641
|
}, [videoRef, adConfig?.preRoll]);
|
|
2642
|
+
// Precise mid-roll ad checking with accurate timing
|
|
2446
2643
|
useEffect(() => {
|
|
2447
2644
|
if (!videoRef || !adConfig?.midRoll || isAdPlaying) {
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2645
|
+
// Clear any pending throttle
|
|
2646
|
+
if (adCheckThrottleRef.current !== null) {
|
|
2647
|
+
cancelAnimationFrame(adCheckThrottleRef.current);
|
|
2648
|
+
adCheckThrottleRef.current = null;
|
|
2451
2649
|
}
|
|
2452
2650
|
return;
|
|
2453
2651
|
}
|
|
2454
|
-
|
|
2652
|
+
// Precise ad check function
|
|
2653
|
+
const checkMidRollAds = () => {
|
|
2654
|
+
// Clear throttle ref
|
|
2655
|
+
adCheckThrottleRef.current = null;
|
|
2455
2656
|
const state = useVideoStore.getState();
|
|
2456
|
-
if
|
|
2457
|
-
|
|
2458
|
-
state.midRollQueue.length === 0) {
|
|
2657
|
+
// Skip if ad is already playing or being processed
|
|
2658
|
+
if (state.isAdPlaying || isProcessingAdRef.current) {
|
|
2459
2659
|
return;
|
|
2460
2660
|
}
|
|
2461
|
-
|
|
2462
|
-
if (
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
state.
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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);
|
|
2479
2712
|
return () => {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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;
|
|
2483
2720
|
}
|
|
2484
2721
|
};
|
|
2485
|
-
}, [videoRef, isAdPlaying, adConfig]);
|
|
2722
|
+
}, [videoRef, isAdPlaying, adConfig, playMidRollAd]);
|
|
2486
2723
|
useEffect(() => {
|
|
2487
2724
|
if (!videoRef || !adConfig?.postRoll || postRollPlayedRef.current)
|
|
2488
2725
|
return;
|
|
@@ -2499,19 +2736,46 @@ const useAdManager = (adConfig) => {
|
|
|
2499
2736
|
useEffect(() => {
|
|
2500
2737
|
if (!videoRef?.src)
|
|
2501
2738
|
return;
|
|
2739
|
+
// Reset ad state when video source changes
|
|
2502
2740
|
preRollPlayedRef.current = false;
|
|
2503
2741
|
postRollPlayedRef.current = false;
|
|
2504
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
|
+
}
|
|
2505
2750
|
setIsAdPlaying(false);
|
|
2506
2751
|
setCurrentAd(null);
|
|
2507
2752
|
setAdType(null);
|
|
2753
|
+
// Re-initialize mid-roll queue with strict validation
|
|
2508
2754
|
if (adConfig?.midRoll && adConfig.midRoll.length > 0) {
|
|
2509
|
-
|
|
2510
|
-
|
|
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
|
+
}
|
|
2511
2774
|
}
|
|
2512
2775
|
else {
|
|
2513
2776
|
setMidRollQueue([]);
|
|
2514
2777
|
}
|
|
2778
|
+
// Clean up any lingering ad video
|
|
2515
2779
|
const lingeringAdRef = useVideoStore.getState().adVideoRef;
|
|
2516
2780
|
if (lingeringAdRef) {
|
|
2517
2781
|
stopMediaElement(lingeringAdRef);
|
|
@@ -2721,13 +2985,93 @@ const usePrimaryVideoLifecycle = ({ hasPreRoll, trackSrc, }) => {
|
|
|
2721
2985
|
};
|
|
2722
2986
|
};
|
|
2723
2987
|
|
|
2724
|
-
const
|
|
2725
|
-
|
|
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
|
+
})));
|
|
2726
3068
|
const [showControls, setShowControls] = useState(true);
|
|
2727
3069
|
const [isHovered, setIsHovered] = useState(false);
|
|
2728
3070
|
const [adDuration, setAdDuration] = useState(0);
|
|
2729
3071
|
const [requiresInteraction, setRequiresInteraction] = useState(false);
|
|
3072
|
+
const [adLoadError, setAdLoadError] = useState(false);
|
|
2730
3073
|
const controlsTimeoutRef = useRef(null);
|
|
3074
|
+
const loadTimeoutRef = useRef(null);
|
|
2731
3075
|
const safelySetCanSkipAd = useCallback((value) => {
|
|
2732
3076
|
if (useVideoStore.getState().canSkipAd !== value) {
|
|
2733
3077
|
setCanSkipAd(value);
|
|
@@ -2768,7 +3112,23 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2768
3112
|
useEffect(() => {
|
|
2769
3113
|
setAdDuration(0);
|
|
2770
3114
|
setRequiresInteraction(false);
|
|
2771
|
-
|
|
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
|
+
]);
|
|
2772
3132
|
useEffect(() => {
|
|
2773
3133
|
if (!adBreak.skipable) {
|
|
2774
3134
|
safelySetCanSkipAd(false);
|
|
@@ -2792,43 +3152,76 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2792
3152
|
if (!adVideoRef)
|
|
2793
3153
|
return;
|
|
2794
3154
|
setRequiresInteraction(false);
|
|
3155
|
+
setAdLoadError(false);
|
|
3156
|
+
if (!adVideoRef.src && adBreak.adUrl) {
|
|
3157
|
+
adVideoRef.src = adBreak.adUrl;
|
|
3158
|
+
adVideoRef.load();
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
2795
3161
|
const playPromise = adVideoRef.play();
|
|
2796
3162
|
if (playPromise && "catch" in playPromise) {
|
|
2797
|
-
playPromise.catch(() => {
|
|
3163
|
+
playPromise.catch((error) => {
|
|
3164
|
+
console.warn("Ad play failed:", error);
|
|
2798
3165
|
setRequiresInteraction(true);
|
|
2799
3166
|
setIsPlaying(false);
|
|
2800
3167
|
});
|
|
2801
3168
|
}
|
|
2802
|
-
}, [adVideoRef, setIsPlaying]);
|
|
3169
|
+
}, [adVideoRef, adBreak.adUrl, setIsPlaying]);
|
|
3170
|
+
const timeUpdateRafRef = useRef(null);
|
|
3171
|
+
const lastUpdateTimeRef = useRef(0);
|
|
2803
3172
|
useEffect(() => {
|
|
2804
3173
|
if (!adVideoRef)
|
|
2805
3174
|
return;
|
|
2806
3175
|
const handleTimeUpdate = () => {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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;
|
|
2814
3183
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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
|
+
}
|
|
2820
3198
|
}
|
|
2821
3199
|
}
|
|
2822
|
-
}
|
|
3200
|
+
});
|
|
2823
3201
|
};
|
|
2824
3202
|
const handleLoadedMetadata = () => {
|
|
3203
|
+
if (loadTimeoutRef.current) {
|
|
3204
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3205
|
+
loadTimeoutRef.current = null;
|
|
3206
|
+
}
|
|
2825
3207
|
const duration = Number.isFinite(adVideoRef.duration)
|
|
2826
3208
|
? adVideoRef.duration
|
|
2827
3209
|
: 0;
|
|
2828
3210
|
setAdDuration(duration);
|
|
3211
|
+
setAdLoadError(false);
|
|
2829
3212
|
setIsPlaying(!adVideoRef.paused);
|
|
2830
3213
|
attemptAdPlayback();
|
|
2831
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);
|
|
2832
3225
|
const handlePlay = () => {
|
|
2833
3226
|
setIsPlaying(true);
|
|
2834
3227
|
setRequiresInteraction(false);
|
|
@@ -2843,7 +3236,21 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2843
3236
|
setIsPlaying(true);
|
|
2844
3237
|
setRequiresInteraction(false);
|
|
2845
3238
|
};
|
|
2846
|
-
const handleError = () => {
|
|
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);
|
|
2847
3254
|
setRequiresInteraction(true);
|
|
2848
3255
|
setIsPlaying(false);
|
|
2849
3256
|
};
|
|
@@ -2862,10 +3269,20 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2862
3269
|
adVideoRef.removeEventListener("waiting", handleWaiting);
|
|
2863
3270
|
adVideoRef.removeEventListener("playing", handlePlaying);
|
|
2864
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;
|
|
2865
3281
|
};
|
|
2866
3282
|
}, [
|
|
2867
3283
|
adVideoRef,
|
|
2868
3284
|
adBreak.skipable,
|
|
3285
|
+
adBreak.id,
|
|
2869
3286
|
skipAfter,
|
|
2870
3287
|
canSkipAd,
|
|
2871
3288
|
setAdCurrentTime,
|
|
@@ -2877,41 +3294,78 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2877
3294
|
useEffect(() => {
|
|
2878
3295
|
if (!adVideoRef || !videoRef)
|
|
2879
3296
|
return;
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
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
|
+
}
|
|
2884
3318
|
}
|
|
2885
3319
|
const handleCanPlay = () => {
|
|
2886
|
-
if (adVideoRef
|
|
3320
|
+
if (!adVideoRef || adVideoRef.paused === false)
|
|
2887
3321
|
return;
|
|
2888
3322
|
attemptAdPlayback();
|
|
2889
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
|
+
};
|
|
2890
3336
|
adVideoRef.addEventListener("canplay", handleCanPlay);
|
|
2891
|
-
|
|
3337
|
+
adVideoRef.addEventListener("loadeddata", handleLoadedData);
|
|
3338
|
+
// Try to play if already ready and src matches
|
|
3339
|
+
if (adVideoRef.readyState >= 3 && !needsReload) {
|
|
2892
3340
|
attemptAdPlayback();
|
|
2893
3341
|
}
|
|
2894
3342
|
return () => {
|
|
2895
|
-
adVideoRef
|
|
3343
|
+
if (adVideoRef) {
|
|
3344
|
+
adVideoRef.removeEventListener("canplay", handleCanPlay);
|
|
3345
|
+
adVideoRef.removeEventListener("loadeddata", handleLoadedData);
|
|
3346
|
+
}
|
|
2896
3347
|
};
|
|
2897
|
-
}, [adVideoRef, videoRef, attemptAdPlayback]);
|
|
3348
|
+
}, [adVideoRef, videoRef, muted, adBreak.adUrl, attemptAdPlayback]);
|
|
2898
3349
|
useEffect(() => {
|
|
2899
|
-
if (adVideoRef)
|
|
3350
|
+
if (!adVideoRef)
|
|
3351
|
+
return;
|
|
3352
|
+
try {
|
|
3353
|
+
// Sync muted state
|
|
2900
3354
|
adVideoRef.muted = muted;
|
|
3355
|
+
// Sync volume with main video
|
|
3356
|
+
if (videoRef) {
|
|
3357
|
+
adVideoRef.volume = videoRef.volume;
|
|
3358
|
+
}
|
|
2901
3359
|
}
|
|
2902
|
-
|
|
3360
|
+
catch (error) {
|
|
3361
|
+
// Ignore errors during state sync
|
|
3362
|
+
}
|
|
3363
|
+
}, [adVideoRef, muted, videoRef]);
|
|
2903
3364
|
const handleSkip = () => {
|
|
2904
3365
|
if (canSkipAd && onSkip) {
|
|
2905
3366
|
onSkip();
|
|
2906
3367
|
}
|
|
2907
3368
|
};
|
|
2908
|
-
const formatTime = (seconds) => {
|
|
2909
|
-
if (isNaN(seconds) || seconds < 0)
|
|
2910
|
-
return "0:00";
|
|
2911
|
-
const mins = Math.floor(seconds / 60);
|
|
2912
|
-
const secs = Math.floor(seconds % 60);
|
|
2913
|
-
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2914
|
-
};
|
|
2915
3369
|
const progressPercent = adDuration > 0 ? (adCurrentTime / adDuration) * 100 : 0;
|
|
2916
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: () => {
|
|
2917
3371
|
setIsHovered(true);
|
|
@@ -2919,23 +3373,31 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2919
3373
|
} },
|
|
2920
3374
|
React__default.createElement("div", { className: "relative flex-1 w-full flex items-center justify-center" },
|
|
2921
3375
|
React__default.createElement("video", { ref: (ref) => {
|
|
2922
|
-
if (ref
|
|
2923
|
-
|
|
3376
|
+
if (!ref)
|
|
3377
|
+
return;
|
|
3378
|
+
if (ref !== adVideoRef) {
|
|
2924
3379
|
setAdVideoRef(ref);
|
|
2925
|
-
|
|
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) {
|
|
2926
3388
|
ref.src = adBreak.adUrl;
|
|
2927
|
-
ref.load();
|
|
2928
3389
|
}
|
|
2929
3390
|
}
|
|
2930
3391
|
}, className: "w-full h-full object-contain", autoPlay: true, playsInline: true, muted: muted, preload: "auto", key: adBreak.id }),
|
|
2931
|
-
requiresInteraction && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
|
|
2932
|
-
React__default.createElement("
|
|
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"))))),
|
|
2933
3396
|
React__default.createElement("div", { className: `absolute inset-0 transition-all duration-300 ${showControls ? "opacity-100" : "opacity-0 pointer-events-none"}` },
|
|
2934
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" },
|
|
2935
3398
|
React__default.createElement("div", { className: "shrink-0 relative" },
|
|
2936
3399
|
React__default.createElement(ControlsHeader, { config: {
|
|
2937
|
-
title:
|
|
2938
|
-
config?.config?.headerConfig?.config?.title ||
|
|
3400
|
+
title: config?.config?.headerConfig?.config?.title ||
|
|
2939
3401
|
"Advertisement",
|
|
2940
3402
|
isTrailer: config?.config?.headerConfig?.config?.isTrailer,
|
|
2941
3403
|
onClose: config?.config?.headerConfig?.config?.onClose,
|
|
@@ -2955,21 +3417,53 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2955
3417
|
React__default.createElement("div", { className: "relative h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none select-none" },
|
|
2956
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}%` } }),
|
|
2957
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)` } }))),
|
|
2958
|
-
React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-
|
|
2959
|
-
React__default.createElement("
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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";
|
|
2965
3455
|
|
|
2966
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";
|
|
2967
3457
|
styleInject(css_248z$1,{"insertAt":"top"});
|
|
2968
3458
|
|
|
2969
|
-
var css_248z = "
|
|
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";
|
|
2970
3460
|
styleInject(css_248z,{"insertAt":"top"});
|
|
2971
3461
|
|
|
2972
|
-
const VideoPlayer = ({
|
|
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 || {};
|
|
2973
3467
|
const { setVideoWrapperRef } = useVideoStore(useShallow((state) => ({
|
|
2974
3468
|
setVideoWrapperRef: state.setVideoWrapperRef,
|
|
2975
3469
|
})));
|
|
@@ -2979,65 +3473,126 @@ const VideoPlayer = ({ trackSrc, trackTitle, intro, onClose, onError, trackPoste
|
|
|
2979
3473
|
hasPreRoll,
|
|
2980
3474
|
trackSrc,
|
|
2981
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]);
|
|
2982
3549
|
useVideoSource(trackSrc, type);
|
|
2983
3550
|
useSubtitles(subtitles);
|
|
2984
3551
|
useSubtitleStyling(subtitleStyle);
|
|
2985
|
-
useVideoTracking(tracking, episodeList, currentEpisodeIndex,
|
|
3552
|
+
useVideoTracking(tracking, episodeList, currentEpisodeIndex, handleClose);
|
|
2986
3553
|
const { showSkipIntro, handleSkipIntro } = useIntroSkip(intro);
|
|
2987
3554
|
useEpisodes(episodeList, currentEpisodeIndex, nextEpisodeConfig);
|
|
2988
3555
|
const { onSeeked, onTimeUpdate, onLoadedMetadata, onProgress, onPlay, onPause, onEnded: onEndedHook, } = useVideoEvents();
|
|
2989
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]);
|
|
2990
3579
|
return (React__default.createElement("div", { ref: setVideoWrapperRef, className: `video-player ${height || "h-full"} ${width || "w-full"} mx-auto absolute` },
|
|
2991
3580
|
trackPoster && (React__default.createElement("div", { className: "pip-poster absolute inset-0 bg-center bg-cover hidden", style: { backgroundImage: `url(${trackPoster})` } })),
|
|
2992
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) => {
|
|
2993
3582
|
onEndedHook(e);
|
|
2994
3583
|
onEnded?.(e);
|
|
2995
3584
|
}, onError: (e) => {
|
|
3585
|
+
handleVideoError(e);
|
|
2996
3586
|
onError?.(e);
|
|
2997
3587
|
}, autoPlay: !hasPreRoll, muted: isMute, className: `w-full h-full relative ${className || ""} ${shouldCoverMainVideo ? "opacity-0" : "opacity-100"} transition-opacity duration-200 ease-out` }),
|
|
2998
|
-
shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex items-center justify-center bg-black" },
|
|
2999
|
-
React__default.createElement(
|
|
3000
|
-
showControls && initialAdFinished && (React__default.createElement(Overlay, { config:
|
|
3001
|
-
headerConfig: {
|
|
3002
|
-
config: {
|
|
3003
|
-
isTrailer: isTrailer,
|
|
3004
|
-
title: trackTitle,
|
|
3005
|
-
onClose: onClose,
|
|
3006
|
-
videoRef: videoRef,
|
|
3007
|
-
},
|
|
3008
|
-
},
|
|
3009
|
-
bottomConfig: {
|
|
3010
|
-
config: {
|
|
3011
|
-
seekBarConfig: {
|
|
3012
|
-
timeCodes: timeCodes,
|
|
3013
|
-
trackColor: "red",
|
|
3014
|
-
getPreviewScreenUrl,
|
|
3015
|
-
},
|
|
3016
|
-
},
|
|
3017
|
-
},
|
|
3018
|
-
} })),
|
|
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 })),
|
|
3019
3591
|
React__default.createElement(SubtitleOverlay, { styleConfig: subtitleStyle }),
|
|
3020
3592
|
showSkipIntro && !isAdPlaying && initialAdFinished && (React__default.createElement(VideoActionButton, { text: "Skip Intro", onClick: handleSkipIntro, position: "left" })),
|
|
3021
|
-
isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config:
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
isTrailer: isTrailer,
|
|
3026
|
-
title: trackTitle,
|
|
3027
|
-
onClose: onClose,
|
|
3028
|
-
},
|
|
3029
|
-
},
|
|
3030
|
-
bottomConfig: {
|
|
3031
|
-
config: {
|
|
3032
|
-
seekBarConfig: {
|
|
3033
|
-
timeCodes: timeCodes,
|
|
3034
|
-
trackColor: "red",
|
|
3035
|
-
getPreviewScreenUrl,
|
|
3036
|
-
},
|
|
3037
|
-
},
|
|
3038
|
-
},
|
|
3039
|
-
},
|
|
3040
|
-
} }))));
|
|
3041
|
-
};
|
|
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";
|
|
3042
3597
|
|
|
3043
3598
|
export { VideoPlayer, useVideoStore };
|