@zezosoft/react-player 0.0.1
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 +97 -0
- package/dist/VideoPlayer/VideoPlayer.d.ts +21 -0
- package/dist/VideoPlayer/_components/BottomControls.d.ts +5 -0
- package/dist/VideoPlayer/_components/ControlsHeader.d.ts +4 -0
- package/dist/VideoPlayer/_components/MiddleControls.d.ts +3 -0
- package/dist/VideoPlayer/_components/Overlay.d.ts +4 -0
- package/dist/VideoPlayer/_components/TimeLine/TimeLine.d.ts +20 -0
- package/dist/VideoPlayer/_components/TimeLine/_components/hoverTimeWithPreview.d.ts +16 -0
- package/dist/VideoPlayer/_components/TimeLine/_components/thumb.d.ts +9 -0
- package/dist/VideoPlayer/_components/TimeLine/_components/timeCodeItem.d.ts +21 -0
- package/dist/VideoPlayer/_components/TimeLine/_components/timeCodes.d.ts +15 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/getEndTimeByIndex.d.ts +2 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/getHoverTimePosition.d.ts +3 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/getPositionPercent.d.ts +1 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/getTimeScale.d.ts +1 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/isInRange.d.ts +1 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/positionToMs.d.ts +1 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/secondsToTime.d.ts +6 -0
- package/dist/VideoPlayer/_components/TimeLine/utils/timeToTimeString.d.ts +1 -0
- package/dist/VideoPlayer/_components/VideoPlayerControls.d.ts +4 -0
- package/dist/VideoPlayer/utils/index.d.ts +29 -0
- package/dist/components/ui/Popover.d.ts +6 -0
- package/dist/components/ui/tooltip.d.ts +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +648 -0
- package/dist/store/VideoState.d.ts +25 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Zezo React Player
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
To install the `zezo-react-player` package, use the following command:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install zezo-react-player
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Import and use the `VideoPlayer` component in your React project:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useCallback, useRef } from "react";
|
|
17
|
+
import { VideoPlayer } from "zezo-react-player";
|
|
18
|
+
|
|
19
|
+
function App() {
|
|
20
|
+
const previewImage = useRef("");
|
|
21
|
+
const updatePreviewImage = (hoverTime: number) => {
|
|
22
|
+
const url = `https://fakeimg.pl/720x405?text=${hoverTime}`;
|
|
23
|
+
const image = document.createElement("img");
|
|
24
|
+
image.src = url;
|
|
25
|
+
image.onload = () => {
|
|
26
|
+
previewImage.current = url;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleGettingPreview = useCallback((hoverTime: number) => {
|
|
31
|
+
// FIND AND RETURN LOADED!!! VIDEO PREVIEW ACCORDING TO the hoverTime TIME
|
|
32
|
+
updatePreviewImage(hoverTime);
|
|
33
|
+
return previewImage.current;
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="w-[720px]">
|
|
38
|
+
<VideoPlayer
|
|
39
|
+
trackPoster="https://i.ytimg.com/vi/Uh60VDKg348/maxresdefault.jpg"
|
|
40
|
+
trackSrc="https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"
|
|
41
|
+
trackTitle="Mehmaan"
|
|
42
|
+
isTrailer={false}
|
|
43
|
+
width="120px"
|
|
44
|
+
height="180px"
|
|
45
|
+
timeCodes={[
|
|
46
|
+
{
|
|
47
|
+
fromMs: 0,
|
|
48
|
+
description: "This is a very long first part label you could use",
|
|
49
|
+
},
|
|
50
|
+
{ fromMs: 130000, description: "This is the second part" },
|
|
51
|
+
{ fromMs: 270000, description: "One more part label" },
|
|
52
|
+
{ fromMs: 440000, description: "Final battle" },
|
|
53
|
+
{ fromMs: 600000, description: "Cast" },
|
|
54
|
+
]}
|
|
55
|
+
getPreviewScreenUrl={handleGettingPreview}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default App;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Props
|
|
65
|
+
|
|
66
|
+
| Prop Name | Type | Description |
|
|
67
|
+
| --------------------- | ------------------------------------------------ | ------------------------------------------------------- |
|
|
68
|
+
| `trackPoster` | `string` | URL of the video poster image. |
|
|
69
|
+
| `trackSrc` | `string` | Video source URL (MP4, HLS, etc.). |
|
|
70
|
+
| `trackTitle` | `string` | Title of the video. |
|
|
71
|
+
| `isTrailer` | `boolean` | Specifies if the video is a trailer. |
|
|
72
|
+
| `width` | `string` | Width of the video player. |
|
|
73
|
+
| `height` | `string` | Height of the video player. |
|
|
74
|
+
| `timeCodes` | `Array<{ fromMs: number, description: string }>` | List of time-based markers with descriptions. |
|
|
75
|
+
| `getPreviewScreenUrl` | `(timeMs: number) => string` | Function to generate preview screen URLs based on time. |
|
|
76
|
+
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<VideoPlayer
|
|
81
|
+
trackPoster="https://example.com/poster.jpg"
|
|
82
|
+
trackSrc="https://example.com/video.mp4"
|
|
83
|
+
trackTitle="Sample Video"
|
|
84
|
+
isTrailer={true}
|
|
85
|
+
width="640px"
|
|
86
|
+
height="360px"
|
|
87
|
+
timeCodes={[
|
|
88
|
+
{ fromMs: 0, description: "Intro" },
|
|
89
|
+
{ fromMs: 60000, description: "Main Scene" },
|
|
90
|
+
]}
|
|
91
|
+
getPreviewScreenUrl={(timeMs) => `https://example.com/preview?time=${timeMs}`}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TimeCode } from "./_components/TimeLine/TimeLine";
|
|
3
|
+
import { IOnWatchTimeUpdated } from "../types";
|
|
4
|
+
interface Props {
|
|
5
|
+
trackSrc: string;
|
|
6
|
+
trackTitle?: string;
|
|
7
|
+
trackPoster?: string;
|
|
8
|
+
isTrailer?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
type?: "hls" | "mp4" | "other";
|
|
11
|
+
width?: string;
|
|
12
|
+
height?: string;
|
|
13
|
+
timeCodes?: TimeCode[];
|
|
14
|
+
getPreviewScreenUrl?: (hoverTimeValue: number) => string;
|
|
15
|
+
tracking?: {
|
|
16
|
+
onViewed?: () => void;
|
|
17
|
+
onWatchTimeUpdated?: (e: IOnWatchTimeUpdated) => void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
declare const VideoPlayer: React.FC<Props>;
|
|
21
|
+
export default VideoPlayer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TimeCode {
|
|
3
|
+
fromMs: number;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Props {
|
|
7
|
+
max: number;
|
|
8
|
+
currentTime: number;
|
|
9
|
+
bufferTime?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
timeCodes?: TimeCode[];
|
|
12
|
+
hideThumbTooltip?: boolean;
|
|
13
|
+
limitTimeTooltipBySides?: boolean;
|
|
14
|
+
secondsPrefix?: string;
|
|
15
|
+
minutesPrefix?: string;
|
|
16
|
+
onChange: (time: number, offsetTime: number) => void;
|
|
17
|
+
getPreviewScreenUrl?: (hoverTimeValue: number) => string;
|
|
18
|
+
trackColor?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const VideoSeekSlider: React.FC<Props>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
max: number;
|
|
4
|
+
hoverTimeValue: number;
|
|
5
|
+
trackWidth: number;
|
|
6
|
+
seekHoverPosition: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
isThumbActive: boolean;
|
|
9
|
+
limitTimeTooltipBySides: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
secondsPrefix?: string;
|
|
12
|
+
minutesPrefix?: string;
|
|
13
|
+
getPreviewScreenUrl?: (hoverTimeValue: number) => string;
|
|
14
|
+
}
|
|
15
|
+
export declare const HoverTimeWithPreview: React.FC<Props>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TimeCode {
|
|
3
|
+
fromMs: number;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Props {
|
|
7
|
+
currentTime: number;
|
|
8
|
+
seekHoverTime: number;
|
|
9
|
+
bufferTime: number;
|
|
10
|
+
startTime: number;
|
|
11
|
+
endTime: number;
|
|
12
|
+
maxTime: number;
|
|
13
|
+
label?: string;
|
|
14
|
+
isTimePassed?: boolean;
|
|
15
|
+
isBufferPassed?: boolean;
|
|
16
|
+
isHoverPassed?: boolean;
|
|
17
|
+
onHover?: (label: string) => void;
|
|
18
|
+
withGap?: boolean;
|
|
19
|
+
trackColor?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const TimeCodeItem: React.FC<Props>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TimeCode } from "./timeCodeItem";
|
|
3
|
+
export interface Props {
|
|
4
|
+
max: number;
|
|
5
|
+
currentTime: number;
|
|
6
|
+
bufferTime: number;
|
|
7
|
+
seekHoverPosition: number;
|
|
8
|
+
timeCodes: TimeCode[] | undefined;
|
|
9
|
+
trackWidth: number;
|
|
10
|
+
mobileSeeking: boolean;
|
|
11
|
+
label: string;
|
|
12
|
+
setLabel: React.Dispatch<React.SetStateAction<string>>;
|
|
13
|
+
trackColor?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const TimeCodes: React.FC<Props>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getPositionPercent(max: number, current: number): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getTimeScale(currentTime: number, startTime: number, endTime: number, isTimePassed: boolean): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isInRange: (time: number, start: number, end: number) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function positionToMs(max: number, position: number, trackWidth: number): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function timeToTimeString(max: number, seekHoverTime: number, offset?: number, minutesPrefix?: string, secondsPrefix?: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Converts seconds to hh:mm:ss
|
|
3
|
+
* @param seconds
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export declare const timeFormat: (seconds: number) => string;
|
|
7
|
+
/**
|
|
8
|
+
* @description Converts seconds to hh:mm
|
|
9
|
+
* @param seconds
|
|
10
|
+
*/
|
|
11
|
+
export declare const timeFormatForContent: (seconds: number) => string;
|
|
12
|
+
/**
|
|
13
|
+
* @description Converts seconds to milliseconds
|
|
14
|
+
* @param seconds
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
export declare const secondsToMilliseconds: (seconds: number) => number;
|
|
18
|
+
/**
|
|
19
|
+
* @description Converts milliseconds to seconds
|
|
20
|
+
* @param milliseconds
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
23
|
+
export declare const millisecondsToSeconds: (milliseconds: number) => number;
|
|
24
|
+
/**
|
|
25
|
+
* @description get extension from url
|
|
26
|
+
* @param url
|
|
27
|
+
* @returns string | undefined
|
|
28
|
+
*/
|
|
29
|
+
export declare const getExtensionFromUrl: (url: string) => string | undefined;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import React__default, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { Settings, Check, VolumeOff, Volume2, Shrink, Maximize, X } from 'lucide-react';
|
|
5
|
+
import Hls from 'hls.js';
|
|
6
|
+
|
|
7
|
+
function styleInject(css, ref) {
|
|
8
|
+
if ( ref === void 0 ) ref = {};
|
|
9
|
+
var insertAt = ref.insertAt;
|
|
10
|
+
|
|
11
|
+
if (!css || typeof document === 'undefined') { return; }
|
|
12
|
+
|
|
13
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
14
|
+
var style = document.createElement('style');
|
|
15
|
+
style.type = 'text/css';
|
|
16
|
+
|
|
17
|
+
if (insertAt === 'top') {
|
|
18
|
+
if (head.firstChild) {
|
|
19
|
+
head.insertBefore(style, head.firstChild);
|
|
20
|
+
} else {
|
|
21
|
+
head.appendChild(style);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
head.appendChild(style);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (style.styleSheet) {
|
|
28
|
+
style.styleSheet.cssText = css;
|
|
29
|
+
} else {
|
|
30
|
+
style.appendChild(document.createTextNode(css));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var css_248z$1 = "/*! tailwindcss v4.0.14 | MIT License | https://tailwindcss.com */\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-gray-200: oklch(0.928 0.006 264.531);\n --color-gray-900: oklch(0.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-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 --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 --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-font-feature-settings: var(--font-sans--font-feature-settings);\n --default-font-variation-settings: var(\n --font-sans--font-variation-settings\n );\n --default-mono-font-family: var(--font-mono);\n --default-mono-font-feature-settings: var(\n --font-mono--font-feature-settings\n );\n --default-mono-font-variation-settings: var(\n --font-mono--font-variation-settings\n );\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 body {\n line-height: inherit;\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 color: color-mix(in oklab, currentColor 50%, transparent);\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 :-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 .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\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-full {\n right: 100%;\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-full {\n left: 100%;\n }\n .z-50 {\n z-index: 50;\n }\n .mx-1 {\n margin-inline: calc(var(--spacing) * 1);\n }\n .mx-auto {\n margin-inline: auto;\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-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .flex {\n display: flex;\n }\n .inline-block {\n display: inline-block;\n }\n .size-10 {\n width: calc(var(--spacing) * 10);\n height: calc(var(--spacing) * 10);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-full {\n height: 100%;\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\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 .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 .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .gap-1\\.5 {\n gap: calc(var(--spacing) * 1.5);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\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-gray-200 {\n border-color: var(--color-gray-200);\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-white {\n background-color: var(--color-white);\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 .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-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\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-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .text-black {\n color: var(--color-black);\n }\n .text-white {\n color: var(--color-white);\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 .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 .lg\\:size-12 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 12);\n height: calc(var(--spacing) * 12);\n }\n }\n .lg\\:size-15 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 15);\n height: calc(var(--spacing) * 15);\n }\n }\n .lg\\:h-8 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 8);\n }\n }\n .lg\\:w-8 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 8);\n }\n }\n .lg\\:pb-10 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 10);\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-lg {\n @media (width >= 64rem) {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n }\n .lg\\:text-xl {\n @media (width >= 64rem) {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\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 initial-value: rotateX(0);\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: rotateY(0);\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: rotateZ(0);\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n initial-value: skewX(0);\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n initial-value: skewY(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-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-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-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}";
|
|
35
|
+
styleInject(css_248z$1,{"insertAt":"top"});
|
|
36
|
+
|
|
37
|
+
const useVideoStore = create((set) => ({
|
|
38
|
+
videoRef: null,
|
|
39
|
+
setVideoRef: (ref) => set({ videoRef: ref }),
|
|
40
|
+
videoWrapperRef: null,
|
|
41
|
+
setVideoWrapperRef: (ref) => set({ videoWrapperRef: ref }),
|
|
42
|
+
isPlaying: false,
|
|
43
|
+
setIsPlaying: (isPlaying) => set({ isPlaying }),
|
|
44
|
+
controls: false,
|
|
45
|
+
setControls: (controls) => set({ controls }),
|
|
46
|
+
currentTime: 0,
|
|
47
|
+
setCurrentTime: (currentTime) => set({ currentTime }),
|
|
48
|
+
hlsInstance: undefined,
|
|
49
|
+
setHlsInstance: (hlsInstance) => set({ hlsInstance }),
|
|
50
|
+
qualityLevels: undefined,
|
|
51
|
+
setQualityLevels: (qualityLevels) => set({ qualityLevels }),
|
|
52
|
+
activeQuality: "auto",
|
|
53
|
+
setActiveQuality: (activeQuality) => set({ activeQuality }),
|
|
54
|
+
isFullscreen: false,
|
|
55
|
+
setIsFullscreen: (isFullscreen) => set({ isFullscreen }),
|
|
56
|
+
duration: 0,
|
|
57
|
+
setDuration: (duration) => set({ duration }),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @description Converts seconds to hh:mm:ss
|
|
62
|
+
* @param seconds
|
|
63
|
+
* @returns
|
|
64
|
+
*/
|
|
65
|
+
const timeFormat = (seconds) => {
|
|
66
|
+
if (isNaN(seconds)) {
|
|
67
|
+
return `00:00`;
|
|
68
|
+
}
|
|
69
|
+
const date = new Date(seconds * 1000);
|
|
70
|
+
const hh = date.getUTCHours();
|
|
71
|
+
const mm = date.getUTCMinutes();
|
|
72
|
+
const ss = date.getUTCSeconds().toString().padStart(2, "0");
|
|
73
|
+
if (hh) {
|
|
74
|
+
return `${hh}:${mm.toString().padStart(2, "0")}:${ss}`;
|
|
75
|
+
}
|
|
76
|
+
return `${mm}:${ss}`;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* @description Converts seconds to milliseconds
|
|
80
|
+
* @param seconds
|
|
81
|
+
* @returns
|
|
82
|
+
*/
|
|
83
|
+
const secondsToMilliseconds = (seconds) => {
|
|
84
|
+
return seconds * 1000;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* @description get extension from url
|
|
88
|
+
* @param url
|
|
89
|
+
* @returns string | undefined
|
|
90
|
+
*/
|
|
91
|
+
const getExtensionFromUrl = (url) => {
|
|
92
|
+
const extension = url?.split(".")?.pop();
|
|
93
|
+
if (extension === "m3u8") {
|
|
94
|
+
return "hls";
|
|
95
|
+
}
|
|
96
|
+
return extension;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function getPositionPercent(max, current) {
|
|
100
|
+
const divider = max || -1; // prevent division by zero
|
|
101
|
+
return (current * 100) / divider;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isInRange = (time, start, end) => time >= start && time <= end;
|
|
105
|
+
|
|
106
|
+
function getTimeScale(currentTime, startTime, endTime, isTimePassed) {
|
|
107
|
+
const isActiveTime = isInRange(currentTime, startTime, endTime);
|
|
108
|
+
const timeDiff = endTime - startTime;
|
|
109
|
+
const timeDiffWithCurrent = currentTime - startTime;
|
|
110
|
+
const currentScalePercent = isActiveTime ? timeDiffWithCurrent / timeDiff : 0;
|
|
111
|
+
return isTimePassed ? 1 : currentScalePercent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const TimeCodeItem = memo(({ label = "", startTime, maxTime, endTime, currentTime, seekHoverTime, bufferTime, isTimePassed = false, isBufferPassed = false, isHoverPassed = false, onHover = () => undefined, withGap, trackColor, }) => {
|
|
115
|
+
const positionPercent = getPositionPercent(maxTime, startTime);
|
|
116
|
+
const timeDiff = endTime - startTime;
|
|
117
|
+
const widthPercent = (timeDiff / maxTime) * 100;
|
|
118
|
+
const mainClassName = `main${withGap ? " with-gap" : ""}`;
|
|
119
|
+
const currentTimeScale = getTimeScale(currentTime, startTime, endTime, isTimePassed);
|
|
120
|
+
const seekHoverTimeScale = getTimeScale(seekHoverTime, startTime, endTime, isHoverPassed);
|
|
121
|
+
const bufferTimeScale = getTimeScale(bufferTime, startTime, endTime, isBufferPassed);
|
|
122
|
+
const handleMouseMove = () => onHover(label);
|
|
123
|
+
return (React__default.createElement("div", { className: mainClassName, onMouseMove: handleMouseMove, style: {
|
|
124
|
+
width: `${widthPercent}%`,
|
|
125
|
+
left: `${positionPercent}%`,
|
|
126
|
+
} },
|
|
127
|
+
React__default.createElement("div", { className: "inner-seek-block buffered", "data-test-id": "test-buffered", style: { transform: `scaleX(${bufferTimeScale})` } }),
|
|
128
|
+
React__default.createElement("div", { className: "inner-seek-block seek-hover", "data-test-id": "test-seek-hover", style: { transform: `scaleX(${seekHoverTimeScale})` } }),
|
|
129
|
+
React__default.createElement("div", { className: "inner-seek-block connect", style: {
|
|
130
|
+
transform: `scaleX(${currentTimeScale})`,
|
|
131
|
+
backgroundColor: trackColor || "#ff0000",
|
|
132
|
+
} })));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function positionToMs(max, position, trackWidth) {
|
|
136
|
+
const percent = (position * 100) / trackWidth;
|
|
137
|
+
return Math.floor(+(percent * (max / 100)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const getEndTimeByIndex = (timeCodes, index, max) => (index + 1 < timeCodes.length ? timeCodes[index + 1].fromMs : max);
|
|
141
|
+
|
|
142
|
+
const TimeCodes = ({ max = 1000, currentTime = 0, bufferTime = 0, seekHoverPosition = 0, timeCodes, trackWidth, mobileSeeking, label, setLabel, trackColor, }) => {
|
|
143
|
+
const hoverTimeValue = positionToMs(max, seekHoverPosition, trackWidth);
|
|
144
|
+
const handleLabelChange = useCallback((currentLabel) => {
|
|
145
|
+
if (label !== currentLabel) {
|
|
146
|
+
setLabel(currentLabel);
|
|
147
|
+
}
|
|
148
|
+
}, [label]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!mobileSeeking) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const currentCode = timeCodes?.find(({ fromMs }, index) => {
|
|
154
|
+
const endTime = getEndTimeByIndex(timeCodes, index, max);
|
|
155
|
+
return isInRange(currentTime, fromMs, endTime);
|
|
156
|
+
});
|
|
157
|
+
if (currentCode?.description !== label) {
|
|
158
|
+
setLabel(currentCode?.description || "");
|
|
159
|
+
}
|
|
160
|
+
}, [currentTime, label, max, timeCodes]);
|
|
161
|
+
return (React__default.createElement(React__default.Fragment, null, timeCodes?.map(({ fromMs, description }, index) => {
|
|
162
|
+
const endTime = getEndTimeByIndex(timeCodes, index, max);
|
|
163
|
+
const isTimePassed = endTime <= currentTime;
|
|
164
|
+
const isBufferPassed = endTime <= bufferTime;
|
|
165
|
+
const isHoverPassed = endTime <= hoverTimeValue;
|
|
166
|
+
let inRange = isInRange(currentTime, fromMs, endTime);
|
|
167
|
+
const newCurrentTime = isTimePassed || !inRange ? 0 : currentTime;
|
|
168
|
+
inRange = isInRange(bufferTime, fromMs, endTime);
|
|
169
|
+
const newBufferTime = isBufferPassed || !inRange ? 0 : bufferTime;
|
|
170
|
+
inRange = isInRange(hoverTimeValue, fromMs, endTime);
|
|
171
|
+
const newHoverTime = isHoverPassed || !inRange ? 0 : hoverTimeValue;
|
|
172
|
+
return (React__default.createElement(TimeCodeItem, { key: fromMs, label: description, maxTime: max, startTime: fromMs, endTime: endTime, isTimePassed: isTimePassed, isBufferPassed: isBufferPassed, isHoverPassed: isHoverPassed, currentTime: newCurrentTime, bufferTime: newBufferTime, seekHoverTime: newHoverTime, onHover: handleLabelChange, withGap: true, trackColor: trackColor }));
|
|
173
|
+
})));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function getHoverTimePosition(seekHoverPosition, hoverTimeElement, trackWidth, limitTimeTooltipBySides) {
|
|
177
|
+
let position = 0;
|
|
178
|
+
if (hoverTimeElement) {
|
|
179
|
+
position = seekHoverPosition - hoverTimeElement.offsetWidth / 2;
|
|
180
|
+
if (limitTimeTooltipBySides) {
|
|
181
|
+
if (position < 0) {
|
|
182
|
+
position = 0;
|
|
183
|
+
}
|
|
184
|
+
else if (position + hoverTimeElement.offsetWidth > trackWidth) {
|
|
185
|
+
position = trackWidth - hoverTimeElement.offsetWidth;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { transform: `translateX(${position}px)` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function millisecondsToTime(ms, offset = 0) {
|
|
193
|
+
const roundedSeconds = Math.round(ms / 1000 + offset);
|
|
194
|
+
const hours = Math.floor(roundedSeconds / 3600);
|
|
195
|
+
const divirsForMinutes = roundedSeconds % 3600;
|
|
196
|
+
const minutes = Math.floor(divirsForMinutes / 60);
|
|
197
|
+
const sec = Math.ceil(divirsForMinutes % 60);
|
|
198
|
+
return {
|
|
199
|
+
hh: hours.toString(),
|
|
200
|
+
mm: minutes < 10 ? `0${minutes}` : minutes.toString(),
|
|
201
|
+
ss: sec < 10 ? `0${sec}` : sec.toString(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function timeToTimeString(max, seekHoverTime, offset = 0, minutesPrefix = "", secondsPrefix = "") {
|
|
206
|
+
const times = millisecondsToTime(seekHoverTime, offset);
|
|
207
|
+
if (max + offset < 60 * 1000) {
|
|
208
|
+
return secondsPrefix + times.ss;
|
|
209
|
+
}
|
|
210
|
+
if (max + offset < 3600 * 1000) {
|
|
211
|
+
return `${minutesPrefix + times.mm}:${times.ss}`;
|
|
212
|
+
}
|
|
213
|
+
return `${times.hh}:${times.mm}:${times.ss}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const HoverTimeWithPreview = ({ max, hoverTimeValue, offset, trackWidth, seekHoverPosition, isThumbActive, limitTimeTooltipBySides, label, minutesPrefix, secondsPrefix, getPreviewScreenUrl, }) => {
|
|
217
|
+
const hoverTimeElement = useRef(null);
|
|
218
|
+
const hoverTimeClassName = isThumbActive ? "hover-time active" : "hover-time";
|
|
219
|
+
const hoverTimePosition = getHoverTimePosition(seekHoverPosition, hoverTimeElement?.current, trackWidth, limitTimeTooltipBySides);
|
|
220
|
+
const hoverTimeString = timeToTimeString(max, hoverTimeValue, offset, minutesPrefix, secondsPrefix);
|
|
221
|
+
return (React__default.createElement("div", { className: hoverTimeClassName, style: hoverTimePosition, ref: hoverTimeElement, "data-testid": "hover-time" },
|
|
222
|
+
isThumbActive && getPreviewScreenUrl && (React__default.createElement("div", { className: "preview-screen", style: {
|
|
223
|
+
backgroundImage: `url(${getPreviewScreenUrl(hoverTimeValue)})`,
|
|
224
|
+
} })),
|
|
225
|
+
label && React__default.createElement("div", null, label),
|
|
226
|
+
hoverTimeString));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const Thumb = ({ max, currentTime, isThumbActive, trackColor, }) => {
|
|
230
|
+
const getThumbHandlerPosition = () => {
|
|
231
|
+
const thumbConstantOffset = -6;
|
|
232
|
+
const leftPosition = (currentTime / max) * 100;
|
|
233
|
+
return {
|
|
234
|
+
left: `calc(${leftPosition}% + ${thumbConstantOffset}px)`,
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
return (React__default.createElement("div", { className: isThumbActive ? "thumb active" : "thumb active", "data-testid": "testThumb", style: getThumbHandlerPosition() },
|
|
238
|
+
React__default.createElement("div", { className: "handler", style: {
|
|
239
|
+
backgroundColor: trackColor || "#ff0000",
|
|
240
|
+
} })));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThumbTooltip = false, offset = 0, secondsPrefix = "", minutesPrefix = "", limitTimeTooltipBySides = true, timeCodes, onChange = () => undefined, getPreviewScreenUrl, trackColor, }) => {
|
|
244
|
+
const [seekHoverPosition, setSeekHoverPosition] = useState(0);
|
|
245
|
+
const [label, setLabel] = useState("");
|
|
246
|
+
const seeking = useRef(false);
|
|
247
|
+
const mobileSeeking = useRef(false);
|
|
248
|
+
const trackElement = useRef(null);
|
|
249
|
+
const trackWidth = trackElement.current?.offsetWidth || 0;
|
|
250
|
+
const isThumbActive = seekHoverPosition > 0 || seeking.current;
|
|
251
|
+
const hoverTimeValue = positionToMs(max, seekHoverPosition, trackWidth);
|
|
252
|
+
const changeCurrentTimePosition = (pageX) => {
|
|
253
|
+
const clientRect = trackElement.current?.getBoundingClientRect();
|
|
254
|
+
const left = clientRect?.left || 0;
|
|
255
|
+
const width = clientRect?.width || 0;
|
|
256
|
+
let position = pageX - left;
|
|
257
|
+
position = position < 0 ? 0 : position;
|
|
258
|
+
position = position > width ? width : position;
|
|
259
|
+
const percent = (position * 100) / width;
|
|
260
|
+
const time = +(percent * (max / 100)).toFixed(0);
|
|
261
|
+
setSeekHoverPosition(position);
|
|
262
|
+
onChange(time, time + offset);
|
|
263
|
+
};
|
|
264
|
+
const handleTouchSeeking = (event) => {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
event.stopPropagation();
|
|
267
|
+
if (!mobileSeeking.current) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const { changedTouches } = event;
|
|
271
|
+
let pageX = changedTouches?.[changedTouches.length - 1]?.pageX || 0;
|
|
272
|
+
pageX = pageX < 0 ? 0 : pageX;
|
|
273
|
+
changeCurrentTimePosition(pageX);
|
|
274
|
+
};
|
|
275
|
+
const handleSeeking = (event) => {
|
|
276
|
+
if (seeking.current) {
|
|
277
|
+
changeCurrentTimePosition(event.pageX);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const handleTrackHover = (clear, event) => {
|
|
281
|
+
const left = trackElement.current?.getBoundingClientRect().left || 0;
|
|
282
|
+
const position = clear ? 0 : event.pageX - left;
|
|
283
|
+
setSeekHoverPosition(position);
|
|
284
|
+
};
|
|
285
|
+
const setMobileSeeking = (state = true) => {
|
|
286
|
+
mobileSeeking.current = state;
|
|
287
|
+
setSeekHoverPosition(state ? seekHoverPosition : 0);
|
|
288
|
+
};
|
|
289
|
+
const setSeeking = (state, event) => {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
handleSeeking(event);
|
|
292
|
+
seeking.current = state;
|
|
293
|
+
setSeekHoverPosition(state ? seekHoverPosition : 0);
|
|
294
|
+
};
|
|
295
|
+
const mouseSeekingHandler = (event) => {
|
|
296
|
+
setSeeking(false, event);
|
|
297
|
+
};
|
|
298
|
+
const mobileTouchSeekingHandler = () => {
|
|
299
|
+
setMobileSeeking(false);
|
|
300
|
+
};
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!mobileSeeking.current) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const currentCode = timeCodes?.find(({ fromMs }, index) => {
|
|
306
|
+
const endTime = getEndTimeByIndex(timeCodes, index, max);
|
|
307
|
+
return isInRange(currentTime, fromMs, endTime);
|
|
308
|
+
});
|
|
309
|
+
if (currentCode?.description !== label) {
|
|
310
|
+
setLabel(currentCode?.description || "");
|
|
311
|
+
}
|
|
312
|
+
}, [currentTime, label, max, timeCodes]);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
window.addEventListener("mousemove", handleSeeking);
|
|
315
|
+
window.addEventListener("mouseup", mouseSeekingHandler);
|
|
316
|
+
window.addEventListener("touchmove", handleTouchSeeking);
|
|
317
|
+
window.addEventListener("touchend", mobileTouchSeekingHandler);
|
|
318
|
+
return () => {
|
|
319
|
+
window.removeEventListener("mousemove", handleSeeking);
|
|
320
|
+
window.removeEventListener("mouseup", mouseSeekingHandler);
|
|
321
|
+
window.removeEventListener("touchmove", handleTouchSeeking);
|
|
322
|
+
window.removeEventListener("touchend", mobileTouchSeekingHandler);
|
|
323
|
+
};
|
|
324
|
+
}, [max, offset, trackWidth]);
|
|
325
|
+
return (React__default.createElement("div", { className: "ui-video-seek-slider" },
|
|
326
|
+
React__default.createElement("div", { className: isThumbActive ? "track active" : "track", ref: trackElement, onMouseMove: (event) => handleTrackHover(false, event), onMouseLeave: (event) => handleTrackHover(true, event), onMouseDown: (event) => setSeeking(true, event), onTouchStart: () => setMobileSeeking(true), "data-testid": "main-track" },
|
|
327
|
+
Boolean(timeCodes?.length) && (React__default.createElement(TimeCodes, { currentTime: currentTime, max: max, bufferTime: bufferTime, seekHoverPosition: seekHoverPosition, timeCodes: timeCodes, mobileSeeking: mobileSeeking.current, trackWidth: trackWidth, label: label, setLabel: setLabel, trackColor: trackColor })),
|
|
328
|
+
!timeCodes && (React__default.createElement(TimeCodeItem, { maxTime: max, startTime: 0, endTime: max, currentTime: currentTime, bufferTime: bufferTime, seekHoverTime: hoverTimeValue, trackColor: trackColor }))),
|
|
329
|
+
!hideThumbTooltip && (React__default.createElement(HoverTimeWithPreview, { max: max, hoverTimeValue: hoverTimeValue, isThumbActive: isThumbActive, label: label, limitTimeTooltipBySides: limitTimeTooltipBySides, offset: offset, seekHoverPosition: seekHoverPosition, trackWidth: trackWidth, getPreviewScreenUrl: getPreviewScreenUrl, minutesPrefix: minutesPrefix, secondsPrefix: secondsPrefix })),
|
|
330
|
+
React__default.createElement(Thumb, { max: max, currentTime: currentTime, isThumbActive: isThumbActive, trackColor: trackColor })));
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
var css_248z = ".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;\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 }\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, .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 }";
|
|
334
|
+
styleInject(css_248z,{"insertAt":"top"});
|
|
335
|
+
|
|
336
|
+
const BottomControls = ({ config }) => {
|
|
337
|
+
const { videoRef, currentTime, isFullscreen } = useVideoStore();
|
|
338
|
+
const duration = videoRef?.duration;
|
|
339
|
+
return (React__default.createElement("div", { className: "px-10 text-white" },
|
|
340
|
+
React__default.createElement(VideoSeekSlider, { max: secondsToMilliseconds(duration || 0), currentTime: secondsToMilliseconds(currentTime || 0), bufferTime: secondsToMilliseconds(0), onChange: (currentTime) => {
|
|
341
|
+
if (videoRef) {
|
|
342
|
+
videoRef.currentTime = currentTime / 1000;
|
|
343
|
+
}
|
|
344
|
+
}, secondsPrefix: "00:00:", minutesPrefix: "00:", getPreviewScreenUrl: config?.seekBarConfig?.getPreviewScreenUrl, timeCodes: config?.seekBarConfig?.timeCodes, trackColor: config?.seekBarConfig?.trackColor }),
|
|
345
|
+
React__default.createElement("div", { className: `pt-6 ${isFullscreen ? "pb-10" : "pb-16"} lg:pb-10 flex gap-2 items-center` },
|
|
346
|
+
React__default.createElement("p", { className: "lg:text-xl font-semibold" }, timeFormat(currentTime || 0)),
|
|
347
|
+
" ",
|
|
348
|
+
React__default.createElement("p", { className: "lg:text-2xl" }, "/"),
|
|
349
|
+
" ",
|
|
350
|
+
React__default.createElement("p", { className: "lg:text-xl font-semibold" }, timeFormat(duration || 0)))));
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const Tooltip = ({ children, title, position = "top", }) => {
|
|
354
|
+
const [visible, setVisible] = useState(false);
|
|
355
|
+
const positionStyles = {
|
|
356
|
+
top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
|
|
357
|
+
bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
|
|
358
|
+
left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
|
|
359
|
+
right: "left-full top-1/2 transform -translate-y-1/2 ml-2",
|
|
360
|
+
};
|
|
361
|
+
return (React__default.createElement("div", { className: "relative inline-block cursor-pointer", onMouseEnter: () => setVisible(true), onMouseLeave: () => setVisible(false) },
|
|
362
|
+
children,
|
|
363
|
+
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 ${positionStyles[position]}` }, title))));
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const Popover = ({ button, children, }) => {
|
|
367
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
368
|
+
const popoverRef = useRef(null);
|
|
369
|
+
// Close popover when clicking outside
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
const handleClickOutside = (event) => {
|
|
372
|
+
if (popoverRef.current &&
|
|
373
|
+
!popoverRef.current.contains(event.target)) {
|
|
374
|
+
setIsOpen(false);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
378
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
379
|
+
}, []);
|
|
380
|
+
return (React__default.createElement("div", { className: "relative inline-block", ref: popoverRef },
|
|
381
|
+
React__default.createElement("div", { onClick: () => setIsOpen(!isOpen) }, button),
|
|
382
|
+
isOpen && (React__default.createElement("div", { className: "absolute left-0 mt-2 w-fit bg-white shadow-lg rounded-lg border border-gray-200 z-50 p-4" }, children))));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const ControlsHeader = ({ config }) => {
|
|
386
|
+
const className = "w-5 h-5 lg:w-8 lg:h-8";
|
|
387
|
+
const { videoWrapperRef, videoRef, qualityLevels, hlsInstance, setActiveQuality, activeQuality, } = useVideoStore();
|
|
388
|
+
const handleFullscreen = () => {
|
|
389
|
+
if (document.fullscreenElement) {
|
|
390
|
+
document.exitFullscreen();
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
videoWrapperRef?.requestFullscreen();
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const isFullscreen = document.fullscreenElement !== null;
|
|
397
|
+
const handleMute = () => {
|
|
398
|
+
if (videoRef?.muted) {
|
|
399
|
+
if (videoRef) {
|
|
400
|
+
videoRef.muted = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
if (videoRef) {
|
|
405
|
+
videoRef.muted = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return (React.createElement("div", { className: "flex items-center justify-between p-10 bg-gradient-to-b from-black" },
|
|
410
|
+
React.createElement("div", null,
|
|
411
|
+
config?.title && (React.createElement("h1", { className: "text-white lg:text-2xl font-bold" }, config.title)),
|
|
412
|
+
config?.isTrailer && (React.createElement("h1", { className: "text-white lg:text-lg" }, "Trailer"))),
|
|
413
|
+
React.createElement("div", { className: "flex items-center gap-7 text-white" },
|
|
414
|
+
React.createElement("div", null,
|
|
415
|
+
React.createElement(Tooltip, { title: "Settings" },
|
|
416
|
+
React.createElement(Popover, { button: React.createElement(Settings, { className: className }) },
|
|
417
|
+
React.createElement("div", { className: "text-black" },
|
|
418
|
+
React.createElement("div", null,
|
|
419
|
+
React.createElement("p", { className: "p-2 font-bold" }, "Quality"),
|
|
420
|
+
React.createElement("p", { onClick: () => {
|
|
421
|
+
if (hlsInstance) {
|
|
422
|
+
hlsInstance.currentLevel = -1;
|
|
423
|
+
setActiveQuality("auto");
|
|
424
|
+
}
|
|
425
|
+
}, className: "p-2 cursor-pointer flex items-center gap-1.5" },
|
|
426
|
+
activeQuality === "auto" && React.createElement(Check, null),
|
|
427
|
+
" Auto"),
|
|
428
|
+
qualityLevels
|
|
429
|
+
?.map((level, index) => (React.createElement("p", { key: index, onClick: () => {
|
|
430
|
+
if (hlsInstance) {
|
|
431
|
+
hlsInstance.currentLevel = index;
|
|
432
|
+
setActiveQuality(String(level.height));
|
|
433
|
+
}
|
|
434
|
+
}, className: "p-2 cursor-pointer flex items-center gap-1.5" },
|
|
435
|
+
activeQuality === String(level.height) && React.createElement(Check, null),
|
|
436
|
+
" ",
|
|
437
|
+
level.height,
|
|
438
|
+
"p")))
|
|
439
|
+
.reverse()))))),
|
|
440
|
+
React.createElement("div", { onClick: handleMute }, videoRef?.muted ? (React.createElement(Tooltip, { title: "Unmute" },
|
|
441
|
+
React.createElement(VolumeOff, { className: className }))) : (React.createElement(Tooltip, { title: "Mute" },
|
|
442
|
+
React.createElement(Volume2, { className: className })))),
|
|
443
|
+
React.createElement("div", { onClick: handleFullscreen }, isFullscreen ? (React.createElement(Tooltip, { title: "Exit" },
|
|
444
|
+
React.createElement(Shrink, { className: className }))) : (React.createElement(Tooltip, { title: "Fullscreen" },
|
|
445
|
+
React.createElement(Maximize, { className: className })))),
|
|
446
|
+
config?.onClose && (React.createElement(React.Fragment, null,
|
|
447
|
+
React.createElement("div", { className: "w-[2px] h-10 bg-white mx-1" }),
|
|
448
|
+
React.createElement("div", { onClick: config.onClose },
|
|
449
|
+
React.createElement(Tooltip, { title: "Close" },
|
|
450
|
+
React.createElement(X, { className: className }))))))));
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const MiddleControls = () => {
|
|
454
|
+
const { videoRef, isPlaying, setIsPlaying } = useVideoStore();
|
|
455
|
+
const handlePlayPause = () => {
|
|
456
|
+
if (!videoRef)
|
|
457
|
+
return;
|
|
458
|
+
if (videoRef.paused) {
|
|
459
|
+
videoRef
|
|
460
|
+
.play()
|
|
461
|
+
.catch((error) => console.error("Error playing video:", error));
|
|
462
|
+
setIsPlaying(true);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
videoRef.pause();
|
|
466
|
+
setIsPlaying(false);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const handleBackword = () => {
|
|
470
|
+
if (!videoRef)
|
|
471
|
+
return;
|
|
472
|
+
videoRef.currentTime -= 10;
|
|
473
|
+
};
|
|
474
|
+
const handleForword = () => {
|
|
475
|
+
if (!videoRef)
|
|
476
|
+
return;
|
|
477
|
+
videoRef.currentTime += 10;
|
|
478
|
+
};
|
|
479
|
+
const nextAndPrevIconSize = "size-10 lg:size-12";
|
|
480
|
+
const playPauseIconSize = "size-10 lg:size-15";
|
|
481
|
+
return (React__default.createElement("div", { className: "flex justify-center items-center" },
|
|
482
|
+
React__default.createElement("div", { onClick: handleBackword, className: "w-[15vw] flex justify-center items-center h-full cursor-pointer" },
|
|
483
|
+
React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: nextAndPrevIconSize, fill: "none", viewBox: "0 0 67 67" },
|
|
484
|
+
React__default.createElement("path", { fill: "#fff", 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.04zm.48 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" }))),
|
|
485
|
+
React__default.createElement("div", { className: "w-[10vw] flex justify-center items-center h-full cursor-pointer", onClick: handlePlayPause }, isPlaying ? (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: playPauseIconSize, fill: "none", viewBox: "0 0 67 67" },
|
|
486
|
+
React__default.createElement("path", { fill: "#fff", fillRule: "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", clipRule: "evenodd" }))) : (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: playPauseIconSize, width: "67", height: "67", fill: "none", viewBox: "0 0 67 67" },
|
|
487
|
+
React__default.createElement("path", { fill: "#fff", 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" })))),
|
|
488
|
+
React__default.createElement("div", { className: "w-[15vw] flex justify-center items-center h-full cursor-pointer", onClick: handleForword },
|
|
489
|
+
React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: nextAndPrevIconSize, fill: "none", viewBox: "0 0 67 67" },
|
|
490
|
+
React__default.createElement("path", { fill: "#fff", 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-.04zm.48 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" })))));
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const VideoPlayerControls = ({ config, height, width, }) => {
|
|
494
|
+
return (React.createElement("div", { className: `absolute top-0 left-0 ${height || "h-full"} ${width || "w-full"} bg-[rgba(0,0,0,0.5)] flex flex-col justify-between` },
|
|
495
|
+
React.createElement(ControlsHeader, { config: config?.headerConfig?.config }),
|
|
496
|
+
React.createElement(MiddleControls, null),
|
|
497
|
+
React.createElement(BottomControls, { config: config?.bottomConfig?.config })));
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const Overlay = ({ config }) => {
|
|
501
|
+
const controlsTimerRef = useRef(null);
|
|
502
|
+
const { setControls, controls } = useVideoStore();
|
|
503
|
+
const handleMouseEnter = useCallback(() => {
|
|
504
|
+
const videoPlayerControls = document?.getElementById("videoPlayerControls");
|
|
505
|
+
if (videoPlayerControls) {
|
|
506
|
+
videoPlayerControls.classList.remove("noCursor");
|
|
507
|
+
}
|
|
508
|
+
setControls(true);
|
|
509
|
+
if (controlsTimerRef.current) {
|
|
510
|
+
clearTimeout(controlsTimerRef.current);
|
|
511
|
+
}
|
|
512
|
+
controlsTimerRef.current = setTimeout(() => {
|
|
513
|
+
setControls(false);
|
|
514
|
+
if (videoPlayerControls) {
|
|
515
|
+
videoPlayerControls.classList.add("noCursor");
|
|
516
|
+
}
|
|
517
|
+
}, 3000); // 3 seconds
|
|
518
|
+
}, [setControls]);
|
|
519
|
+
return (React.createElement("div", { id: "videoPlayerControls", className: "absolute inset-0", onMouseMove: handleMouseEnter }, controls && React.createElement(VideoPlayerControls, { config: config })));
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const VideoPlayer = ({ trackSrc, trackTitle, trackPoster, isTrailer, className, type, height, width, timeCodes, getPreviewScreenUrl, tracking, }) => {
|
|
523
|
+
const { setVideoRef, setCurrentTime, setVideoWrapperRef, videoRef, setQualityLevels, setHlsInstance, setDuration, setIsPlaying, } = useVideoStore();
|
|
524
|
+
const onRightClick = (e) => {
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
};
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (!videoRef) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const getVideoExtension = getExtensionFromUrl(trackSrc);
|
|
532
|
+
const contentType = type || getVideoExtension;
|
|
533
|
+
if (contentType === "mp4") {
|
|
534
|
+
videoRef.src = trackSrc;
|
|
535
|
+
setQualityLevels([]);
|
|
536
|
+
}
|
|
537
|
+
else if (contentType === "hls") {
|
|
538
|
+
if (videoRef?.canPlayType("application/vnd.apple.mpegurl")) {
|
|
539
|
+
// Native HLS support (Safari)
|
|
540
|
+
videoRef.src = trackSrc;
|
|
541
|
+
}
|
|
542
|
+
else if (Hls.isSupported()) {
|
|
543
|
+
// Use hls.js for other browsers
|
|
544
|
+
const hls = new Hls();
|
|
545
|
+
hls.loadSource(trackSrc);
|
|
546
|
+
hls.attachMedia(videoRef);
|
|
547
|
+
setHlsInstance(hls);
|
|
548
|
+
// Get quality levels when HLS loads
|
|
549
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
550
|
+
setQualityLevels(hls.levels);
|
|
551
|
+
});
|
|
552
|
+
return () => {
|
|
553
|
+
hls.destroy(); // Cleanup on unmount
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
videoRef.src = trackSrc;
|
|
559
|
+
setQualityLevels([]);
|
|
560
|
+
}
|
|
561
|
+
}, [trackSrc, videoRef]);
|
|
562
|
+
// Analytics Start
|
|
563
|
+
const startTime = useRef(null);
|
|
564
|
+
const isViewCounted = useRef(false);
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
if (videoRef) {
|
|
567
|
+
videoRef.addEventListener("play", () => {
|
|
568
|
+
if (!isViewCounted.current) {
|
|
569
|
+
isViewCounted.current = true;
|
|
570
|
+
if (tracking?.onViewed) {
|
|
571
|
+
tracking.onViewed();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
startTime.current = Date.now();
|
|
575
|
+
setIsPlaying(true);
|
|
576
|
+
});
|
|
577
|
+
videoRef.addEventListener("pause", () => {
|
|
578
|
+
if (startTime.current) {
|
|
579
|
+
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
580
|
+
const getCurrentTime = localStorage.getItem("current_time");
|
|
581
|
+
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
582
|
+
startTime.current = null;
|
|
583
|
+
}
|
|
584
|
+
setIsPlaying(false);
|
|
585
|
+
});
|
|
586
|
+
return () => {
|
|
587
|
+
videoRef.removeEventListener("play", () => { });
|
|
588
|
+
videoRef.removeEventListener("pause", () => { });
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}, [videoRef]);
|
|
592
|
+
const handleUnload = (e) => {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
if (startTime.current) {
|
|
595
|
+
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
596
|
+
const getCurrentTime = localStorage.getItem("current_time");
|
|
597
|
+
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
598
|
+
}
|
|
599
|
+
const totalTimeWatched = Number(localStorage.getItem("current_time") || 0);
|
|
600
|
+
if (totalTimeWatched >= 30) {
|
|
601
|
+
if (tracking?.onWatchTimeUpdated) {
|
|
602
|
+
tracking.onWatchTimeUpdated({
|
|
603
|
+
watchTime: totalTimeWatched,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
localStorage.setItem("current_time", "0");
|
|
608
|
+
};
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
611
|
+
window.addEventListener("unload", handleUnload);
|
|
612
|
+
return () => {
|
|
613
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
614
|
+
window.removeEventListener("unload", handleUnload);
|
|
615
|
+
};
|
|
616
|
+
}, [startTime]);
|
|
617
|
+
return (React__default.createElement("div", { ref: setVideoWrapperRef, className: `${height || "h-full"} ${width || "w-full"} mx-auto relative` },
|
|
618
|
+
React__default.createElement("video", { ref: setVideoRef, className: `w-full h-full ${className}`, poster: trackPoster, onContextMenu: onRightClick, onTimeUpdate: (e) => {
|
|
619
|
+
if (e?.currentTarget?.currentTime) {
|
|
620
|
+
setCurrentTime(e?.currentTarget?.currentTime);
|
|
621
|
+
}
|
|
622
|
+
}, onLoad: (e) => {
|
|
623
|
+
if (e?.currentTarget?.duration) {
|
|
624
|
+
localStorage.setItem("current_time", "0");
|
|
625
|
+
setDuration(e?.currentTarget?.duration);
|
|
626
|
+
}
|
|
627
|
+
} },
|
|
628
|
+
React__default.createElement("track", { kind: "captions", srcLang: "en", label: "English", default: true, src: "https://res.cloudinary.com/dm4uaqlio/raw/upload/v1742096015/sintel-captions-en_ehel5s.vtt" })),
|
|
629
|
+
React__default.createElement(Overlay, { height: height, width: width, config: {
|
|
630
|
+
headerConfig: {
|
|
631
|
+
config: {
|
|
632
|
+
isTrailer: isTrailer,
|
|
633
|
+
title: trackTitle,
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
bottomConfig: {
|
|
637
|
+
config: {
|
|
638
|
+
seekBarConfig: {
|
|
639
|
+
timeCodes: timeCodes,
|
|
640
|
+
trackColor: "white",
|
|
641
|
+
getPreviewScreenUrl,
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
} })));
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
export { VideoPlayer, useVideoStore };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Hls from "hls.js";
|
|
2
|
+
interface VideoState {
|
|
3
|
+
videoRef: HTMLVideoElement | null;
|
|
4
|
+
setVideoRef: (ref: HTMLVideoElement) => void;
|
|
5
|
+
videoWrapperRef: HTMLDivElement | null;
|
|
6
|
+
setVideoWrapperRef: (ref: HTMLDivElement) => void;
|
|
7
|
+
isPlaying: boolean;
|
|
8
|
+
setIsPlaying: (isPlaying: boolean) => void;
|
|
9
|
+
controls: boolean;
|
|
10
|
+
setControls: (controls: boolean) => void;
|
|
11
|
+
currentTime: number;
|
|
12
|
+
setCurrentTime: (currentTime: number) => void;
|
|
13
|
+
hlsInstance?: Hls;
|
|
14
|
+
setHlsInstance: (hlsInstance: Hls) => void;
|
|
15
|
+
qualityLevels?: Hls["levels"];
|
|
16
|
+
setQualityLevels: (qualityLevels: Hls["levels"]) => void;
|
|
17
|
+
activeQuality?: string;
|
|
18
|
+
setActiveQuality: (activeQuality: string) => void;
|
|
19
|
+
isFullscreen?: boolean;
|
|
20
|
+
setIsFullscreen: (isFullscreen: boolean) => void;
|
|
21
|
+
duration?: number;
|
|
22
|
+
setDuration: (duration: number) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare const useVideoStore: import("zustand").UseBoundStore<import("zustand").StoreApi<VideoState>>;
|
|
25
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zezosoft/react-player",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "npx rollup -c",
|
|
9
|
+
"dev": "npx rollup -c -w",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"video",
|
|
17
|
+
"player",
|
|
18
|
+
"react",
|
|
19
|
+
"react-player",
|
|
20
|
+
"react-video-player",
|
|
21
|
+
"react-video",
|
|
22
|
+
"react-video-player",
|
|
23
|
+
"react-video-component",
|
|
24
|
+
"react-video-component",
|
|
25
|
+
"react-video-component"
|
|
26
|
+
],
|
|
27
|
+
"author": "Pukhraj Dhamu",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"description": "A lightweight and customizable video player by Zezosoft, built for seamless streaming with advanced controls, adaptive playback, and modern UI. Perfect for web and React applications.",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
32
|
+
"@tailwindcss/postcss": "^4.0.14",
|
|
33
|
+
"@types/react": "^19.0.10",
|
|
34
|
+
"hls.js": "^1.5.20",
|
|
35
|
+
"lucide-react": "^0.481.0",
|
|
36
|
+
"postcss": "^8.5.3",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0",
|
|
39
|
+
"rollup": "^4.35.0",
|
|
40
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
41
|
+
"tailwindcss": "^4.0.14",
|
|
42
|
+
"tslib": "^2.8.1",
|
|
43
|
+
"zustand": "^5.0.3"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"hls.js": "^1.5.20",
|
|
47
|
+
"lucide-react": "^0.481.0",
|
|
48
|
+
"react": "^19.0.0",
|
|
49
|
+
"react-dom": "^19.0.0",
|
|
50
|
+
"zustand": "^5.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|