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