@viewlift/player-backup 1.0.0 → 1.0.1-lighteningjs
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/.babelrc +8 -0
- package/.browserslistrc +2 -0
- package/README.md +29 -0
- package/babel.config.json +8 -0
- package/dist/index.cjs.css +2 -0
- package/dist/index.cjs.css.map +1 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.esm.css +2 -0
- package/dist/index.esm.css.map +1 -0
- package/dist/index.esm.js +1 -0
- package/dist/main-player/player-ui.d.ts +19 -0
- package/dist/player-components/player-buttons/playerButtons.d.ts +38 -0
- package/dist/player-components/player-seekbar/playerSeekbar.d.ts +25 -0
- package/dist/player-ui/player-ui.d.ts +18 -0
- package/dist/playerInitializer.d.ts +13 -0
- package/package.json +31 -15
- package/rollup.config.mjs +37 -0
- package/src/index.ts +242 -0
- package/src/main-player/player-ui.ts +65 -0
- package/src/player-components/player-buttons/playerButtons.css +0 -0
- package/src/player-components/player-buttons/playerButtons.ts +127 -0
- package/src/player-components/player-seekbar/playerSeekbar.css +75 -0
- package/src/player-components/player-seekbar/playerSeekbar.ts +158 -0
- package/src/player-ui/player-ui.css +8 -0
- package/src/player-ui/player-ui.ts +79 -0
- package/src/playerInitializer.ts +54 -0
- package/tsconfig.json +14 -0
- package/cjs/autoNextVideo-823821e8.js +0 -1
- package/cjs/bundle.css +0 -657
- package/cjs/caption-settings-67212c17.js +0 -1
- package/cjs/caption-settings-d42c056e.js +0 -1
- package/cjs/debugSelection-cbfdabb7.js +0 -1
- package/cjs/dvr_skin-f7f770ae.js +0 -1
- package/cjs/error-26d5a3c3.js +0 -1
- package/cjs/features-164c3cc6.js +0 -1
- package/cjs/generic-8b99b151.js +0 -1
- package/cjs/icons-7afb9c13.js +0 -1
- package/cjs/index-3dfe6459.js +0 -1
- package/cjs/index.js +0 -1
- package/cjs/keyboardShortcuts-cfd998c2.js +0 -1
- package/cjs/loader-393b838a.js +0 -1
- package/cjs/multicam-21c9a90b.js +0 -1
- package/cjs/multicam-a41fe4a6.js +0 -1
- package/cjs/multicamv2-76d8530b.js +0 -1
- package/cjs/multicamv2-c148fa24.js +0 -1
- package/cjs/mwebPotrait-3c11d7cf.js +0 -1
- package/cjs/mwebPotraitTray-b9d5d45a.js +0 -1
- package/cjs/nextEpisodeButton-300afadc.js +0 -1
- package/cjs/nextEpisodeButton-b6e6586f.js +0 -1
- package/cjs/nondvr_skin-ef2cb3b2.js +0 -1
- package/cjs/playlist-925f808b.js +0 -1
- package/cjs/playlist-df87ca90.js +0 -1
- package/cjs/renditions-bc841552.js +0 -1
- package/cjs/resolution-6a9adf54.js +0 -1
- package/cjs/series-82ec79d3.js +0 -1
- package/cjs/series-9c237975.js +0 -1
- package/cjs/series-v2-24855152.js +0 -1
- package/cjs/series-v2-70137d8c.js +0 -1
- package/cjs/settings-icon-a63f46fc.js +0 -1
- package/cjs/settings-icon-e6a8feee.js +0 -1
- package/cjs/skin-225252ff.js +0 -1
- package/cjs/skin-39ab30e2.js +0 -1
- package/cjs/skin-83edff36.js +0 -1
- package/cjs/skin-84083352.js +0 -1
- package/cjs/skin-93a3e5ae.js +0 -1
- package/cjs/skin-983a2ea7.js +0 -1
- package/cjs/skin-afbec6d7.js +0 -1
- package/cjs/skin-ec49b877.js +0 -1
- package/cjs/skin-f225a42e.js +0 -1
- package/cjs/skin-fb394911.js +0 -1
- package/cjs/util-fa21734a.js +0 -1
- package/cjs/videoSelector-01a9ef43.js +0 -1
- package/cjs/videoSelector-fcdf11eb.js +0 -1
- package/esm/autoNextVideo-1672740e.js +0 -1
- package/esm/bundle.css +0 -657
- package/esm/caption-settings-2e47813b.js +0 -1
- package/esm/caption-settings-3eacf3a0.js +0 -1
- package/esm/debugSelection-2601b8b3.js +0 -1
- package/esm/dvr_skin-9cc1b144.js +0 -1
- package/esm/error-2395864b.js +0 -1
- package/esm/features-f86404ce.js +0 -1
- package/esm/generic-7c43020f.js +0 -1
- package/esm/icons-c18c70b7.js +0 -1
- package/esm/index-9f022e82.js +0 -1
- package/esm/index.js +0 -1
- package/esm/keyboardShortcuts-c06f3c7b.js +0 -1
- package/esm/loader-580278fe.js +0 -1
- package/esm/multicam-94da3759.js +0 -1
- package/esm/multicam-de741974.js +0 -1
- package/esm/multicamv2-0105c40f.js +0 -1
- package/esm/multicamv2-d006cffa.js +0 -1
- package/esm/mwebPotrait-3c1805d8.js +0 -1
- package/esm/mwebPotraitTray-5ab39989.js +0 -1
- package/esm/nextEpisodeButton-2ea69b56.js +0 -1
- package/esm/nextEpisodeButton-a1556cdf.js +0 -1
- package/esm/nondvr_skin-64be70e5.js +0 -1
- package/esm/playlist-032fbbf3.js +0 -1
- package/esm/playlist-7ecf432a.js +0 -1
- package/esm/renditions-97276745.js +0 -1
- package/esm/resolution-38cd4f5c.js +0 -1
- package/esm/series-041152aa.js +0 -1
- package/esm/series-977edced.js +0 -1
- package/esm/series-v2-0727ee8c.js +0 -1
- package/esm/series-v2-516c91c2.js +0 -1
- package/esm/settings-icon-0125bffd.js +0 -1
- package/esm/settings-icon-2b713fbf.js +0 -1
- package/esm/skin-2c638dc1.js +0 -1
- package/esm/skin-42c2f81d.js +0 -1
- package/esm/skin-5e2fa9d4.js +0 -1
- package/esm/skin-6093bfb3.js +0 -1
- package/esm/skin-665ec17e.js +0 -1
- package/esm/skin-93e320fc.js +0 -1
- package/esm/skin-b939c35e.js +0 -1
- package/esm/skin-cd537931.js +0 -1
- package/esm/skin-d5510f70.js +0 -1
- package/esm/skin-d6b00653.js +0 -1
- package/esm/util-d9f5a7be.js +0 -1
- package/esm/videoSelector-8889c58e.js +0 -1
- package/esm/videoSelector-8e258d45.js +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Button } from '../player-components/player-buttons/playerButtons';
|
|
2
|
+
import { PlayerSeekbar } from '../player-components/player-seekbar/playerSeekbar';
|
|
3
|
+
|
|
4
|
+
interface VideoPlayerUIOptions {
|
|
5
|
+
player: { video: HTMLVideoElement; play: () => void; pause: () => void };
|
|
6
|
+
width?: string;
|
|
7
|
+
height?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class VideoPlayerUI {
|
|
11
|
+
private player: { video: HTMLVideoElement; play: () => void; pause: () => void };
|
|
12
|
+
private container: HTMLDivElement;
|
|
13
|
+
private playPauseBtn?: Button;
|
|
14
|
+
private seekbar?: PlayerSeekbar;
|
|
15
|
+
|
|
16
|
+
constructor({ player, width = '1280px', height = '720px' }: VideoPlayerUIOptions) {
|
|
17
|
+
this.player = player;
|
|
18
|
+
this.container = document.createElement('div');
|
|
19
|
+
this.container.className = 'video-player-ui';
|
|
20
|
+
if (width) this.container.style.width = width;
|
|
21
|
+
if (height) this.container.style.height = height;
|
|
22
|
+
this.createControls(width, height);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private createControls(width?: string, height?: string) {
|
|
26
|
+
// Play/Pause button using Button class
|
|
27
|
+
this.playPauseBtn = new Button({
|
|
28
|
+
imgSrc: '', // Optionally provide an image
|
|
29
|
+
imgAltText: 'Play/Pause',
|
|
30
|
+
buttonText: 'Play',
|
|
31
|
+
width: 80,
|
|
32
|
+
height: 40,
|
|
33
|
+
klassname: 'play-pause-btn',
|
|
34
|
+
focused: false,
|
|
35
|
+
disabled: false,
|
|
36
|
+
handleClick: () => {
|
|
37
|
+
if (this.player.video.paused) {
|
|
38
|
+
this.player.play();
|
|
39
|
+
if (this.playPauseBtn && this.playPauseBtn.buttonElement) {
|
|
40
|
+
this.playPauseBtn.buttonElement.textContent = 'Pause';
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
this.player.pause();
|
|
44
|
+
if (this.playPauseBtn && this.playPauseBtn.buttonElement) {
|
|
45
|
+
this.playPauseBtn.buttonElement.textContent = 'Play';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.playPauseBtn.mount(this.container);
|
|
51
|
+
// Attach the video element
|
|
52
|
+
if (width) this.player.video.style.width = width;
|
|
53
|
+
if (height) this.player.video.style.height = height;
|
|
54
|
+
this.container.appendChild(this.player.video);
|
|
55
|
+
// Add the seekbar below the video
|
|
56
|
+
this.seekbar = new PlayerSeekbar(this.player.video);
|
|
57
|
+
this.seekbar.mount(this.container);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
attachTo(parent: HTMLElement) {
|
|
61
|
+
parent.appendChild(this.container);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default VideoPlayerUI;
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import './playerButtons.css';
|
|
2
|
+
|
|
3
|
+
type EventHandler = (event?: any) => void;
|
|
4
|
+
|
|
5
|
+
type ButtonProps = {
|
|
6
|
+
imgSrc: string;
|
|
7
|
+
imgAltText: string;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
klassname: string;
|
|
11
|
+
buttonText: string;
|
|
12
|
+
focused: boolean;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
handleClick: EventHandler
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class Button {
|
|
18
|
+
|
|
19
|
+
private imgSrc: string;
|
|
20
|
+
private imgAltText: string;
|
|
21
|
+
private klassname: string;
|
|
22
|
+
private width: number;
|
|
23
|
+
private height: number;
|
|
24
|
+
private focused: boolean;
|
|
25
|
+
private disabled: boolean;
|
|
26
|
+
private handleClick: EventHandler;
|
|
27
|
+
private buttonText: string;
|
|
28
|
+
public buttonElement?: HTMLButtonElement;
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
constructor(props: ButtonProps) {
|
|
33
|
+
this.imgSrc = props.imgSrc || '';
|
|
34
|
+
this.imgAltText = props.imgAltText || '';
|
|
35
|
+
this.buttonText = props.buttonText || '';
|
|
36
|
+
this.klassname = props.klassname || 'default-button';
|
|
37
|
+
this.width = props.width || 100;
|
|
38
|
+
this.height = props.height || 50;
|
|
39
|
+
this.focused = props.focused || false;
|
|
40
|
+
this.disabled = props.disabled || false;
|
|
41
|
+
this.handleClick = props.handleClick || (() => {});
|
|
42
|
+
|
|
43
|
+
if (typeof props.handleClick !== 'function') {
|
|
44
|
+
throw new Error('Button Class: handleClick must be a function');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this.disabled) {
|
|
48
|
+
this.klassname += ' disabled';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.focused) {
|
|
52
|
+
this.klassname += ' focused';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.handleClick = this.handleClick.bind(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mounts the button to the specified mount point
|
|
60
|
+
* @param mountPoint The HTML element to which the button will be appended
|
|
61
|
+
*/
|
|
62
|
+
mount(mountPoint: HTMLElement) {
|
|
63
|
+
// Create the button element
|
|
64
|
+
const button = document.createElement('button');
|
|
65
|
+
button.className = this.klassname;
|
|
66
|
+
button.style.width = `${this.width}px`;
|
|
67
|
+
button.style.height = `${this.height}px`;
|
|
68
|
+
button.disabled = this.disabled;
|
|
69
|
+
button.tabIndex = 0;
|
|
70
|
+
|
|
71
|
+
// Add image or text
|
|
72
|
+
if (this.imgSrc) {
|
|
73
|
+
const img = document.createElement('img');
|
|
74
|
+
img.src = this.imgSrc;
|
|
75
|
+
img.alt = this.imgAltText;
|
|
76
|
+
button.appendChild(img);
|
|
77
|
+
} else {
|
|
78
|
+
const span = document.createElement('span');
|
|
79
|
+
span.textContent = this.buttonText;
|
|
80
|
+
button.appendChild(span);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Attach event listeners
|
|
84
|
+
button.addEventListener('click', this.handleClick);
|
|
85
|
+
button.addEventListener('focus', () => {
|
|
86
|
+
this.focused = true;
|
|
87
|
+
button.classList.add('focused');
|
|
88
|
+
});
|
|
89
|
+
button.addEventListener('blur', () => {
|
|
90
|
+
this.focused = false;
|
|
91
|
+
button.classList.remove('focused');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Store reference and append
|
|
95
|
+
this.buttonElement = button;
|
|
96
|
+
mountPoint.appendChild(button);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes the click event listener from the button
|
|
101
|
+
*/
|
|
102
|
+
unmount() {
|
|
103
|
+
if (this.buttonElement) {
|
|
104
|
+
this.buttonElement.removeEventListener('click', this.handleClick);
|
|
105
|
+
this.buttonElement.removeEventListener('focus', this.handleClick);
|
|
106
|
+
this.buttonElement.removeEventListener('blur', this.handleClick);
|
|
107
|
+
this.buttonElement = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
focus() {
|
|
112
|
+
if (this.buttonElement) {
|
|
113
|
+
this.buttonElement.focus();
|
|
114
|
+
this.buttonElement.classList.add('focused');
|
|
115
|
+
this.focused = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
blur() {
|
|
120
|
+
if (this.buttonElement) {
|
|
121
|
+
this.buttonElement.blur();
|
|
122
|
+
this.buttonElement.classList.remove('focused');
|
|
123
|
+
this.focused = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.seekbar-container {
|
|
2
|
+
position: absolute;
|
|
3
|
+
left: 50%;
|
|
4
|
+
transform: translateX(-50%);
|
|
5
|
+
bottom: 50px;
|
|
6
|
+
height: 12px;
|
|
7
|
+
background: #444;
|
|
8
|
+
width: 90%;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
margin: 0;
|
|
12
|
+
user-select: none;
|
|
13
|
+
z-index: 20;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.seekbar-buffer {
|
|
17
|
+
position: absolute;
|
|
18
|
+
height: 100%;
|
|
19
|
+
background: #888;
|
|
20
|
+
width: 0;
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.seekbar-progress {
|
|
26
|
+
position: absolute;
|
|
27
|
+
height: 100%;
|
|
28
|
+
background: #4169E1;
|
|
29
|
+
width: 0;
|
|
30
|
+
border-radius: 6px;
|
|
31
|
+
z-index: 2;
|
|
32
|
+
transition: width 0.1s linear;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.seekbar-thumb {
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 50%;
|
|
38
|
+
transform: translate(-50%, -50%);
|
|
39
|
+
width: 16px;
|
|
40
|
+
height: 16px;
|
|
41
|
+
background: #fff;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
border: 2px solid #4169E1;
|
|
44
|
+
z-index: 3;
|
|
45
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
transition: background 0.2s;
|
|
48
|
+
}
|
|
49
|
+
.seekbar-thumb:hover {
|
|
50
|
+
background: #ffeaea;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.seekbar-tooltip {
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 24px;
|
|
56
|
+
left: 0;
|
|
57
|
+
transform: translateX(-50%);
|
|
58
|
+
background: #222;
|
|
59
|
+
color: #fff;
|
|
60
|
+
padding: 2px 8px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
z-index: 10;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.seekbar-time-label, .seekbar-duration-label {
|
|
69
|
+
position: relative;
|
|
70
|
+
top: 18px;
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
color: #eee;
|
|
73
|
+
margin-right: 4px;
|
|
74
|
+
z-index: 5;
|
|
75
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import './playerSeekbar.css';
|
|
2
|
+
|
|
3
|
+
export class PlayerSeekbar {
|
|
4
|
+
private container: HTMLDivElement;
|
|
5
|
+
private progress: HTMLDivElement;
|
|
6
|
+
private buffer: HTMLDivElement;
|
|
7
|
+
private thumb: HTMLDivElement;
|
|
8
|
+
private tooltip: HTMLDivElement;
|
|
9
|
+
private video: HTMLVideoElement;
|
|
10
|
+
private timeLabel: HTMLSpanElement;
|
|
11
|
+
private durationLabel: HTMLSpanElement;
|
|
12
|
+
private isDragging: boolean = false;
|
|
13
|
+
private focused: boolean = false;
|
|
14
|
+
|
|
15
|
+
constructor(video: HTMLVideoElement) {
|
|
16
|
+
this.video = video;
|
|
17
|
+
this.container = document.createElement('div');
|
|
18
|
+
this.container.className = 'seekbar-container';
|
|
19
|
+
this.container.tabIndex = 0; // Make focusable
|
|
20
|
+
|
|
21
|
+
this.buffer = document.createElement('div');
|
|
22
|
+
this.buffer.className = 'seekbar-buffer';
|
|
23
|
+
|
|
24
|
+
this.progress = document.createElement('div');
|
|
25
|
+
this.progress.className = 'seekbar-progress';
|
|
26
|
+
|
|
27
|
+
this.thumb = document.createElement('div');
|
|
28
|
+
this.thumb.className = 'seekbar-thumb';
|
|
29
|
+
|
|
30
|
+
this.tooltip = document.createElement('div');
|
|
31
|
+
this.tooltip.className = 'seekbar-tooltip';
|
|
32
|
+
this.tooltip.style.display = 'none';
|
|
33
|
+
|
|
34
|
+
this.timeLabel = document.createElement('span');
|
|
35
|
+
this.timeLabel.className = 'seekbar-time-label';
|
|
36
|
+
this.durationLabel = document.createElement('span');
|
|
37
|
+
this.durationLabel.className = 'seekbar-duration-label';
|
|
38
|
+
|
|
39
|
+
this.container.appendChild(this.buffer);
|
|
40
|
+
this.container.appendChild(this.progress);
|
|
41
|
+
this.container.appendChild(this.thumb);
|
|
42
|
+
this.container.appendChild(this.tooltip);
|
|
43
|
+
this.container.appendChild(this.timeLabel);
|
|
44
|
+
this.container.appendChild(this.durationLabel);
|
|
45
|
+
|
|
46
|
+
// Update UI as video plays
|
|
47
|
+
this.video.addEventListener('timeupdate', () => this.update());
|
|
48
|
+
this.video.addEventListener('progress', () => this.updateBuffer());
|
|
49
|
+
this.video.addEventListener('durationchange', () => this.update());
|
|
50
|
+
|
|
51
|
+
// Seek on click
|
|
52
|
+
this.container.addEventListener('click', (e) => this.seek(e));
|
|
53
|
+
// Dragging
|
|
54
|
+
this.thumb.addEventListener('mousedown', (e) => this.startDrag(e));
|
|
55
|
+
// Tooltip on hover
|
|
56
|
+
this.container.addEventListener('mousemove', (e) => this.showTooltip(e));
|
|
57
|
+
this.container.addEventListener('mouseleave', () => this.hideTooltip());
|
|
58
|
+
|
|
59
|
+
// Focus/blur events for accessibility and visual feedback
|
|
60
|
+
this.container.addEventListener('focus', () => {
|
|
61
|
+
this.focused = true;
|
|
62
|
+
this.container.classList.add('focused');
|
|
63
|
+
});
|
|
64
|
+
this.container.addEventListener('blur', () => {
|
|
65
|
+
this.focused = false;
|
|
66
|
+
this.container.classList.remove('focused');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public focus() {
|
|
71
|
+
this.container.focus();
|
|
72
|
+
this.container.classList.add('focused');
|
|
73
|
+
this.focused = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public blur() {
|
|
77
|
+
this.container.blur();
|
|
78
|
+
this.container.classList.remove('focused');
|
|
79
|
+
this.focused = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private formatTime(seconds: number): string {
|
|
83
|
+
if (isNaN(seconds)) return '0:00';
|
|
84
|
+
const m = Math.floor(seconds / 60);
|
|
85
|
+
const s = Math.floor(seconds % 60);
|
|
86
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private update() {
|
|
90
|
+
const percent = (this.video.currentTime / this.video.duration) * 100;
|
|
91
|
+
this.progress.style.width = percent + '%';
|
|
92
|
+
this.thumb.style.left = percent + '%';
|
|
93
|
+
this.timeLabel.textContent = this.formatTime(this.video.currentTime);
|
|
94
|
+
this.durationLabel.textContent = ' / ' + this.formatTime(this.video.duration);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private updateBuffer() {
|
|
98
|
+
if (this.video.buffered.length) {
|
|
99
|
+
const end = this.video.buffered.end(this.video.buffered.length - 1);
|
|
100
|
+
const percent = (end / this.video.duration) * 100;
|
|
101
|
+
this.buffer.style.width = percent + '%';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private seek(e: MouseEvent) {
|
|
106
|
+
if (this.isDragging) return;
|
|
107
|
+
const rect = this.container.getBoundingClientRect();
|
|
108
|
+
const percent = (e.clientX - rect.left) / rect.width;
|
|
109
|
+
this.video.currentTime = percent * this.video.duration;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private startDrag(e: MouseEvent) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
this.isDragging = true;
|
|
115
|
+
const onMove = (moveEvent: MouseEvent) => {
|
|
116
|
+
const rect = this.container.getBoundingClientRect();
|
|
117
|
+
let percent = (moveEvent.clientX - rect.left) / rect.width;
|
|
118
|
+
percent = Math.max(0, Math.min(1, percent));
|
|
119
|
+
this.progress.style.width = percent * 100 + '%';
|
|
120
|
+
this.thumb.style.left = percent * 100 + '%';
|
|
121
|
+
this.timeLabel.textContent = this.formatTime(percent * this.video.duration);
|
|
122
|
+
};
|
|
123
|
+
const onUp = (upEvent: MouseEvent) => {
|
|
124
|
+
const rect = this.container.getBoundingClientRect();
|
|
125
|
+
let percent = (upEvent.clientX - rect.left) / rect.width;
|
|
126
|
+
percent = Math.max(0, Math.min(1, percent));
|
|
127
|
+
this.video.currentTime = percent * this.video.duration;
|
|
128
|
+
this.isDragging = false;
|
|
129
|
+
document.removeEventListener('mousemove', onMove);
|
|
130
|
+
document.removeEventListener('mouseup', onUp);
|
|
131
|
+
};
|
|
132
|
+
document.addEventListener('mousemove', onMove);
|
|
133
|
+
document.addEventListener('mouseup', onUp);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private showTooltip(e: MouseEvent) {
|
|
137
|
+
const rect = this.container.getBoundingClientRect();
|
|
138
|
+
const percent = (e.clientX - rect.left) / rect.width;
|
|
139
|
+
const time = percent * this.video.duration;
|
|
140
|
+
this.tooltip.textContent = this.formatTime(time);
|
|
141
|
+
this.tooltip.style.display = 'block';
|
|
142
|
+
this.tooltip.style.left = `${percent * 100}%`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private hideTooltip() {
|
|
146
|
+
this.tooltip.style.display = 'none';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public mount(parent: HTMLElement) {
|
|
150
|
+
parent.appendChild(this.container);
|
|
151
|
+
this.update();
|
|
152
|
+
this.updateBuffer();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public getElement() {
|
|
156
|
+
return this.container;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Button } from '../player-components/player-buttons/playerButtons';
|
|
2
|
+
import { PlayerSeekbar } from '../player-components/player-seekbar/playerSeekbar';
|
|
3
|
+
import './player-ui.css';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VideoPlayerUI {
|
|
7
|
+
private player: { video: HTMLVideoElement; play: () => void; pause: () => void };
|
|
8
|
+
private container: HTMLDivElement;
|
|
9
|
+
private playPauseBtn?: Button;
|
|
10
|
+
private seekbar?: PlayerSeekbar;
|
|
11
|
+
|
|
12
|
+
constructor({ player }: { player: { video: HTMLVideoElement; play: () => void; pause: () => void } }) {
|
|
13
|
+
this.player = player;
|
|
14
|
+
this.container = document.createElement('div');
|
|
15
|
+
this.container.className = 'video-player-ui';
|
|
16
|
+
this.createControls();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private createControls() {
|
|
20
|
+
|
|
21
|
+
this.playPauseBtn = new Button({
|
|
22
|
+
imgSrc: '',
|
|
23
|
+
imgAltText: 'Play/Pause',
|
|
24
|
+
buttonText: 'Play',
|
|
25
|
+
width: 80,
|
|
26
|
+
height: 40,
|
|
27
|
+
klassname: 'play-pause-btn',
|
|
28
|
+
focused: false,
|
|
29
|
+
disabled: false,
|
|
30
|
+
handleClick: () => {
|
|
31
|
+
if (this.player.video.paused) {
|
|
32
|
+
this.player.play();
|
|
33
|
+
if (this.playPauseBtn && this.playPauseBtn.buttonElement) {
|
|
34
|
+
this.playPauseBtn.buttonElement.textContent = 'Pause';
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
this.player.pause();
|
|
38
|
+
if (this.playPauseBtn && this.playPauseBtn.buttonElement) {
|
|
39
|
+
this.playPauseBtn.buttonElement.textContent = 'Play';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
this.playPauseBtn.mount(this.container);
|
|
45
|
+
// Attach the video element
|
|
46
|
+
this.container.appendChild(this.player.video);
|
|
47
|
+
// Add the seekbar below the video
|
|
48
|
+
this.seekbar = new PlayerSeekbar(this.player.video);
|
|
49
|
+
this.seekbar.mount(this.container);
|
|
50
|
+
|
|
51
|
+
if (this.playPauseBtn?.buttonElement && this.seekbar) {
|
|
52
|
+
// Down arrow from play button focuses seekbar
|
|
53
|
+
this.playPauseBtn.buttonElement.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
54
|
+
if (e.key === 'ArrowDown') {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
this.seekbar?.focus();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Up arrow from seekbar focuses play button
|
|
61
|
+
this.seekbar?.getElement()?.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
62
|
+
if (e.key === 'ArrowUp') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
this.playPauseBtn?.focus();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
attachTo(parent: HTMLElement) {
|
|
71
|
+
parent.appendChild(this.container);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public getElement() {
|
|
75
|
+
return this.container;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default VideoPlayerUI;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import VideoPlayerUI from './player-ui/player-ui';
|
|
2
|
+
|
|
3
|
+
// Declare Hls type for CDN usage
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
Hls: any;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PlayerInstance {
|
|
11
|
+
video: HTMLVideoElement;
|
|
12
|
+
play: () => void;
|
|
13
|
+
pause: () => void;
|
|
14
|
+
ui: VideoPlayerUI;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createHlsPlayer(
|
|
18
|
+
mountPoint: HTMLElement,
|
|
19
|
+
streamUrl: string
|
|
20
|
+
): PlayerInstance {
|
|
21
|
+
const video = document.createElement('video');
|
|
22
|
+
video.controls = false;
|
|
23
|
+
video.style.width = '100%';
|
|
24
|
+
video.style.height = '100%';
|
|
25
|
+
|
|
26
|
+
let hls: any = null;
|
|
27
|
+
|
|
28
|
+
if (window.Hls && window.Hls.isSupported()) {
|
|
29
|
+
hls = new window.Hls();
|
|
30
|
+
hls.loadSource(streamUrl);
|
|
31
|
+
hls.attachMedia(video);
|
|
32
|
+
hls.on(window.Hls.Events.MANIFEST_PARSED, function () {
|
|
33
|
+
video.play();
|
|
34
|
+
});
|
|
35
|
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
36
|
+
video.src = streamUrl;
|
|
37
|
+
video.addEventListener('loadedmetadata', function () {
|
|
38
|
+
video.play();
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error('HLS is not supported in this browser.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const player = {
|
|
45
|
+
video,
|
|
46
|
+
play: () => video.play(),
|
|
47
|
+
pause: () => video.pause(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ui = new VideoPlayerUI({ player });
|
|
51
|
+
ui.attachTo(mountPoint);
|
|
52
|
+
|
|
53
|
+
return { ...player, ui };
|
|
54
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES5",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["dom", "es2017"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";var e,t=require("./generic-8b99b151.js"),i=require("./util-fa21734a.js"),o=require("./skin-84083352.js"),n=0,d=0,u=function e(i,n,u,s,c){d=i,i>u.length-1||t.load(n,u[i].id,s).then((function(e){o.highlightVideo(d),d++,t.setVideoIdx(d),r(e)})).catch((function(i){r(i),setTimeout((function(){o.highlightVideo(d),d++,t.setVideoIdx(d),e(d,n,u,s)}),5e3)}))},r=function(t){e&&e(t)};exports.autoPlayNext=function(s){clearTimeout(n);var c=null==s?void 0:s.content;e=null==s?void 0:s.cb;var h=s.playerId,l=sessionStorage.getItem("token");i.onVideoEnded(h,(function(){var e=t.getPlayer(h);0!==(null==e?void 0:e.currentTime())&&((d=t.getVideoIdx())>c.length-1||t.load(h,c[d].id,l).then((function(e){o.highlightVideo(d),t.setCurrentVideo(e.response),d++,t.setVideoIdx(d),r(e)})).catch((function(e){r(e),o.highlightVideo(d),d++,t.setVideoIdx(d),n=setTimeout((function(){t.isPaused(h)&&u(d,h,c,l)}),5e3)})))}))},exports.switchToNext=u;
|