@cinecrew/cinecrew-player 0.1.1 → 0.1.3
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 +20 -16
- package/package.json +1 -2
- package/src/data/README.md +5 -0
- package/src/data/emoji-data.json +1 -0
- package/src/data/emoji.js +30 -0
- package/src/native/InlineLivePlayer.js +1 -1
- package/src/native/MediaPlayerView.js +29 -13
- package/src/native/customization.js +2 -1
- package/src/native/media/EmojiPickerModal.js +217 -21
- package/src/native/media/LiveChatDrawer.js +261 -103
- package/src/native/media/YouTubeVideoPlayer.web.js +64 -10
- package/src/native/media/player/PlayerBottomBar.js +6 -4
- package/src/native/media/player/PlayerTopBar.js +3 -5
- package/src/native/media/web/useWebVideoAspectRatio.js +11 -1
- package/src/web/index.js +506 -114
- package/src/web/styles.css +476 -20
- package/types/index.d.ts +22 -7
package/src/web/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useWebVideoAspectRatio } from '../native/media/web/useWebVideoAspectRatio';
|
|
3
3
|
import { useWebMpegTsPlayback } from '../native/media/web/useWebMpegTsPlayback.web';
|
|
4
4
|
import { useWebHlsPlayback } from '../native/media/web/useWebHlsPlayback.web';
|
|
5
5
|
import { useWebAc3AudioPlayback } from '../native/media/web/useWebAc3AudioPlayback.web';
|
|
6
6
|
import { YouTubeVideoPlayer } from '../native/media/YouTubeVideoPlayer.web.js';
|
|
7
7
|
import { getYouTubeVideoId, getWebRuntimePlatform, useResolvedPlayerSource } from '../utils/sourceUtils';
|
|
8
|
+
import { EMOJI_GROUPS, searchEmojis } from '../data/emoji';
|
|
8
9
|
import './styles.css';
|
|
9
10
|
|
|
10
11
|
const h = React.createElement;
|
|
@@ -20,8 +21,9 @@ const DEFAULT_THEME = {
|
|
|
20
21
|
const DEFAULT_ICONS = {
|
|
21
22
|
play: 'play', pause: 'pause', restart: 'restart', lock: 'lock', unlock: 'lock-open',
|
|
22
23
|
mute: 'volume-mute', unmute: 'volume-high', aspectRatio: 'aspect-ratio', videoOnly: 'video',
|
|
23
|
-
audio: 'music-note',
|
|
24
|
-
liveChat: 'comment-text-multiple-outline', epg: 'television-classic',
|
|
24
|
+
audio: 'music-note', audioOnly: 'headphones', back: 'arrow-left', recording: 'record-rec', stop: 'stop',
|
|
25
|
+
liveChat: 'comment-text-multiple-outline', epg: 'television-classic', diagnostics: 'logs',
|
|
26
|
+
fullscreen: 'fullscreen', close: 'close',
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
// Inline SVG keeps the React DOM/Electron entry independent of React Native,
|
|
@@ -32,12 +34,14 @@ const WEB_ICON_PATHS = {
|
|
|
32
34
|
restart: 'M12 5V1L7 6l5 5V7a6 6 0 1 1-5.65 8H4.26A9 9 0 1 0 12 5z',
|
|
33
35
|
lock: 'M18 8h-1V6a5 5 0 0 0-10 0v2H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2zM9 6a3 3 0 0 1 6 0v2H9zm3 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z',
|
|
34
36
|
'lock-open': 'M18 8h-1V6a5 5 0 0 0-9.8-1H9a3 3 0 0 1 6 .8V8H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2zm-6 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4z',
|
|
35
|
-
'volume-mute': 'M3 9v6h4l5 5V4L7 9H3zm13.
|
|
37
|
+
'volume-mute': 'M3 9v6h4l5 5V4L7 9H3zm13.59 3 2.12-2.12 1.41 1.41L19 13.41l2.12 2.12-1.41 1.41-2.12-2.12-2.12 2.12-1.41-1.41 2.12-2.12-2.12-2.12 1.41-1.41z',
|
|
36
38
|
'volume-high': 'M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4.03v8.05A4.5 4.5 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06a9 9 0 0 0 0-17.54z',
|
|
37
39
|
'aspect-ratio': 'M3 5h18v14H3zm2 2v10h14V7zm2 2h4v2H9v4H7zm10 6h-4v-2h2V9h2z',
|
|
38
40
|
video: 'M18 7V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2l4 4V5zm-2 12H4V5h12z',
|
|
39
41
|
'music-note': 'M12 3v12.26A4 4 0 1 0 14 19V7h6V3z',
|
|
42
|
+
headphones: 'M12 3a9 9 0 0 0-9 9v6a3 3 0 0 0 3 3h3v-9H5v-1a7 7 0 0 1 14 0v1h-4v9h3a3 3 0 0 0 3-3v-6a9 9 0 0 0-9-9z',
|
|
40
43
|
'arrow-collapse': 'M4 4h6v2H6v4H4zm10 0h6v6h-2V6h-4zM4 14h2v4h4v2H4zm14 0h2v6h-6v-2h4z',
|
|
44
|
+
logs: 'M3 3h18v18H3zm2 2v14h14V5zm3 3h2v2H8zm4 0h7v2h-7zm-4 4h2v2H8zm4 0h7v2h-7zm-4 4h2v2H8zm4 0h7v2h-7z',
|
|
41
45
|
'arrow-left': 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z',
|
|
42
46
|
'record-rec': 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 15a5 5 0 1 1 0-10 5 5 0 0 1 0 10z',
|
|
43
47
|
stop: 'M6 6h12v12H6z',
|
|
@@ -45,6 +49,13 @@ const WEB_ICON_PATHS = {
|
|
|
45
49
|
'television-classic': 'M21 3H3a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h7v2h4v-2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 15H3V5h18zM5 7h14v9H5z',
|
|
46
50
|
fullscreen: 'M4 4h6v2H6v4H4zm10 0h6v6h-2V6h-4zM4 14h2v4h4v2H4zm14 0h2v6h-6v-2h4z',
|
|
47
51
|
close: 'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
|
|
52
|
+
send: 'M2 21 23 12 2 3v7l15 2-15 2z',
|
|
53
|
+
network: 'M12 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM4 16a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm16 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM10.7 7.4 5.3 16.7l1.7 1 5.4-9.3zm2.6 0 5.4 9.3-1.7 1-5.4-9.3z',
|
|
54
|
+
speedometer: 'M12 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-2.05-4.95l1.42-1.42A8.96 8.96 0 0 0 12 3zm7.7 1.3-6.4 6.4a2.5 2.5 0 1 0 1.4 1.4l6.4-6.4z',
|
|
55
|
+
buffer: 'M4 5h16v3H4zm0 5.5h16v3H4zm0 5.5h16v3H4z',
|
|
56
|
+
clock: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm1-13h-2v6l5 3 1-1.7-4-2.3z',
|
|
57
|
+
display: 'M3 4h18v13H3zm2 2v9h14V6zm5 13h4v2h-4z',
|
|
58
|
+
frames: 'M4 4h16v12H4zm2 2v8h12V6zm-4 13h16v2H2zm4-3h16v2H6z',
|
|
48
59
|
};
|
|
49
60
|
|
|
50
61
|
function getSource(source, url) {
|
|
@@ -114,17 +125,64 @@ function getPanelActionName(panel) {
|
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
function WebSeekControl({ currentTime, duration, theme, onSeek }) {
|
|
128
|
+
const [seekDraft, setSeekDraft] = useState(currentTime);
|
|
129
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
130
|
+
const seekDraftRef = useRef(currentTime);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isDragging) {
|
|
134
|
+
seekDraftRef.current = currentTime;
|
|
135
|
+
setSeekDraft(currentTime);
|
|
136
|
+
}
|
|
137
|
+
}, [currentTime, isDragging]);
|
|
138
|
+
|
|
139
|
+
const updateDraft = (event) => {
|
|
140
|
+
const nextTime = Number(event.currentTarget.value);
|
|
141
|
+
seekDraftRef.current = nextTime;
|
|
142
|
+
setSeekDraft(nextTime);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const finishSeek = (event) => {
|
|
146
|
+
if (!isDragging) return;
|
|
147
|
+
const nextTime = event?.currentTarget && event.type !== 'blur'
|
|
148
|
+
? Number(event.currentTarget.value)
|
|
149
|
+
: seekDraftRef.current;
|
|
150
|
+
seekDraftRef.current = nextTime;
|
|
151
|
+
setSeekDraft(nextTime);
|
|
152
|
+
setIsDragging(false);
|
|
153
|
+
onSeek(nextTime);
|
|
154
|
+
};
|
|
155
|
+
const displayTime = Math.min(isDragging ? seekDraft : currentTime, duration);
|
|
156
|
+
const remainingTime = Math.max(0, duration - displayTime);
|
|
157
|
+
|
|
117
158
|
return h('div', { className: 'cinecrew-player__seek' },
|
|
118
|
-
h('span', null, formatTime(
|
|
159
|
+
h('span', null, formatTime(displayTime)),
|
|
119
160
|
h('input', {
|
|
120
161
|
type: 'range',
|
|
162
|
+
'aria-label': 'Seek video',
|
|
121
163
|
min: 0,
|
|
122
164
|
max: duration,
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
step: 'any',
|
|
166
|
+
value: displayTime,
|
|
167
|
+
onChange: updateDraft,
|
|
168
|
+
onPointerDown: (event) => {
|
|
169
|
+
event.stopPropagation();
|
|
170
|
+
setIsDragging(true);
|
|
171
|
+
seekDraftRef.current = Number(event.currentTarget.value);
|
|
172
|
+
setSeekDraft(seekDraftRef.current);
|
|
173
|
+
},
|
|
174
|
+
onPointerUp: finishSeek,
|
|
175
|
+
onPointerCancel: finishSeek,
|
|
176
|
+
onBlur: finishSeek,
|
|
177
|
+
onKeyDown: (event) => {
|
|
178
|
+
if (['ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) setIsDragging(true);
|
|
179
|
+
},
|
|
180
|
+
onKeyUp: (event) => {
|
|
181
|
+
if (['ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) finishSeek(event);
|
|
182
|
+
},
|
|
125
183
|
style: { accentColor: theme.accentColor },
|
|
126
184
|
}),
|
|
127
|
-
h('span', null, formatTime(
|
|
185
|
+
h('span', null, `−${formatTime(remainingTime)}`));
|
|
128
186
|
}
|
|
129
187
|
|
|
130
188
|
function WebAspectRatioMenu({ open, theme, icons, onToggle, onSelect }) {
|
|
@@ -138,7 +196,7 @@ function WebAspectRatioMenu({ open, theme, icons, onToggle, onSelect }) {
|
|
|
138
196
|
onClick: () => onSelect(ratio),
|
|
139
197
|
}, ratio)));
|
|
140
198
|
}
|
|
141
|
-
return h('div', { className: 'cinecrew-player__menu-wrap', key: 'aspectRatio' },
|
|
199
|
+
return h('div', { className: 'cinecrew-player__menu-wrap cinecrew-player__menu-wrap--aspect', key: 'aspectRatio' },
|
|
142
200
|
h(PlayerButton, { name: 'aspectRatio', label: 'Aspect ratio', icons, theme, onClick: onToggle, active: open }),
|
|
143
201
|
menu);
|
|
144
202
|
}
|
|
@@ -147,11 +205,13 @@ function WebAudioTrackMenu({ open, tracks, theme, icons, onToggle, onSelect }) {
|
|
|
147
205
|
let menu = null;
|
|
148
206
|
if (open) {
|
|
149
207
|
menu = h('div', { className: 'cinecrew-player__menu', style: { background: theme.surfaceColor } },
|
|
150
|
-
tracks.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
208
|
+
tracks.length
|
|
209
|
+
? tracks.map((track, index) => h('button', {
|
|
210
|
+
key: track.id ?? index,
|
|
211
|
+
type: 'button',
|
|
212
|
+
onClick: () => onSelect(track.id),
|
|
213
|
+
}, track.name || track.language || `Track ${index + 1}`))
|
|
214
|
+
: h('div', { className: 'cinecrew-player__menu-empty', role: 'status' }, 'No audio tracks available'));
|
|
155
215
|
}
|
|
156
216
|
return h('div', { className: 'cinecrew-player__menu-wrap', key: 'audioTracks' },
|
|
157
217
|
h(PlayerButton, { name: 'audio', label: 'Audio tracks', icons, theme, onClick: onToggle, active: open }),
|
|
@@ -170,8 +230,8 @@ function WebPlaybackRateControl({ value, onChange }) {
|
|
|
170
230
|
|
|
171
231
|
function WebBottomControls(props) {
|
|
172
232
|
const {
|
|
173
|
-
isLive, overrides, theme, icons, currentTime, duration, seekTo,
|
|
174
|
-
|
|
233
|
+
isLive, overrides, theme, icons, currentTime, duration, seekTo,
|
|
234
|
+
showAspectMenu, setShowAspectMenu, selectAspect, videoOnly,
|
|
175
235
|
setVideoOnlyMode, audioOnly, setAudioOnlyMode, availableTracks,
|
|
176
236
|
showAudioMenu, setShowAudioMenu, selectAudio, playbackRate,
|
|
177
237
|
setPlaybackRateAction, fullscreen, toggleFullscreen,
|
|
@@ -194,7 +254,7 @@ function WebBottomControls(props) {
|
|
|
194
254
|
});
|
|
195
255
|
}
|
|
196
256
|
let audioTracks = null;
|
|
197
|
-
if (
|
|
257
|
+
if (isControlEnabled(overrides, 'audioTracks', true)) {
|
|
198
258
|
audioTracks = h(WebAudioTrackMenu, {
|
|
199
259
|
open: showAudioMenu,
|
|
200
260
|
tracks: availableTracks,
|
|
@@ -211,27 +271,28 @@ function WebBottomControls(props) {
|
|
|
211
271
|
if (isControlEnabled(overrides, 'playbackRate', true) && !isLive) {
|
|
212
272
|
playbackRateControl = h(WebPlaybackRateControl, { value: playbackRate, onChange: setPlaybackRateAction });
|
|
213
273
|
}
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const videoOnlyLabel = videoOnly ? 'Enable audio' : 'Video only';
|
|
217
|
-
const audioOnlyLabel = audioOnly ? 'Switch to audio card' : 'Audio only';
|
|
274
|
+
const videoOnlyLabel = 'Video only';
|
|
275
|
+
const audioOnlyLabel = 'Audio only';
|
|
218
276
|
const fullscreenLabel = fullscreen ? 'Exit full screen' : 'Full screen';
|
|
219
277
|
|
|
220
278
|
return h('div', { className: 'cinecrew-player__bottom-controls' },
|
|
221
279
|
seek,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
280
|
+
h('div', { className: 'cinecrew-player__bottom-actions' },
|
|
281
|
+
h('div', { className: 'cinecrew-player__bottom-left-actions' },
|
|
282
|
+
renderControlButton({ name: 'audioOnly', label: audioOnlyLabel, callback: () => setAudioOnlyMode(true), options: { active: audioOnly }, overrides, icons, theme }),
|
|
283
|
+
renderControlButton({ name: 'videoOnly', label: videoOnlyLabel, callback: () => setVideoOnlyMode(!videoOnly), options: { active: videoOnly, defaultVisible: false }, overrides, icons, theme }),
|
|
284
|
+
aspect),
|
|
285
|
+
h('div', { className: 'cinecrew-player__bottom-right-actions' },
|
|
286
|
+
audioTracks,
|
|
287
|
+
playbackRateControl,
|
|
288
|
+
renderControlButton({ name: 'fullscreen', label: fullscreenLabel, callback: toggleFullscreen, options: { icon: 'fullscreen' }, overrides, icons, theme }))));
|
|
229
289
|
}
|
|
230
290
|
|
|
231
|
-
function WebPlayerControls({ locked, overrides, theme, icons, unlockedControls, toggleLock, paused, togglePlay, bottomProps }) {
|
|
232
|
-
let
|
|
291
|
+
function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlockedControls, toggleLock, paused, togglePlay, bottomProps }) {
|
|
292
|
+
let leftControls = locked ? null : unlockedControls.left;
|
|
293
|
+
let rightControls = unlockedControls.right;
|
|
233
294
|
if (locked) {
|
|
234
|
-
|
|
295
|
+
rightControls = renderControlButton({
|
|
235
296
|
name: 'lock', label: 'Unlock controls', callback: toggleLock,
|
|
236
297
|
options: { active: true, defaultVisible: true }, overrides, icons, theme,
|
|
237
298
|
});
|
|
@@ -239,14 +300,18 @@ function WebPlayerControls({ locked, overrides, theme, icons, unlockedControls,
|
|
|
239
300
|
let centerControls = null;
|
|
240
301
|
let bottomControls = null;
|
|
241
302
|
if (!locked) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
303
|
+
if (!buffering) {
|
|
304
|
+
const playLabel = paused ? 'Play' : 'Pause';
|
|
305
|
+
const playIcon = paused ? 'play' : 'pause';
|
|
306
|
+
centerControls = h('div', { className: 'cinecrew-player__center-controls' },
|
|
307
|
+
renderControlButton({ name: 'playPause', label: playLabel, callback: togglePlay, options: { icon: playIcon }, overrides, icons, theme }));
|
|
308
|
+
}
|
|
246
309
|
bottomControls = h(WebBottomControls, bottomProps);
|
|
247
310
|
}
|
|
248
311
|
return h('div', { className: 'cinecrew-player__controls', style: { color: theme.controlColor } },
|
|
249
|
-
h('div', { className: 'cinecrew-player__top-controls' },
|
|
312
|
+
h('div', { className: 'cinecrew-player__top-controls' },
|
|
313
|
+
h('div', { className: 'cinecrew-player__top-left-actions' }, leftControls),
|
|
314
|
+
h('div', { className: 'cinecrew-player__top-right-actions' }, rightControls)),
|
|
250
315
|
centerControls,
|
|
251
316
|
bottomControls);
|
|
252
317
|
}
|
|
@@ -265,6 +330,7 @@ function WebPlayerSurface({
|
|
|
265
330
|
playbackRate,
|
|
266
331
|
videoOnly,
|
|
267
332
|
videoStyle,
|
|
333
|
+
drawerResize,
|
|
268
334
|
audioOnly,
|
|
269
335
|
inlinePreview,
|
|
270
336
|
onPromotePreview,
|
|
@@ -292,9 +358,31 @@ function WebPlayerSurface({
|
|
|
292
358
|
onStateChange,
|
|
293
359
|
onError,
|
|
294
360
|
onEnded,
|
|
361
|
+
style: drawerResize ? { width: 'var(--cinecrew-media-width, 64%)', height: '100%', inset: '0 auto 0 0' } : undefined,
|
|
295
362
|
});
|
|
296
363
|
}
|
|
297
364
|
if (!streamUrl) return h('div', { className: 'cinecrew-player__empty' });
|
|
365
|
+
let resizedVideoStyle = {};
|
|
366
|
+
if (drawerResize && videoStyle.width === 'auto') {
|
|
367
|
+
resizedVideoStyle = {
|
|
368
|
+
left: 'calc(var(--cinecrew-media-width, 64%) / 2)',
|
|
369
|
+
top: '50%',
|
|
370
|
+
right: 'auto',
|
|
371
|
+
bottom: 'auto',
|
|
372
|
+
maxWidth: 'var(--cinecrew-media-width, 64%)',
|
|
373
|
+
transform: 'translate(-50%, -50%)',
|
|
374
|
+
};
|
|
375
|
+
} else if (drawerResize) {
|
|
376
|
+
resizedVideoStyle = {
|
|
377
|
+
width: 'var(--cinecrew-media-width, 64%)',
|
|
378
|
+
height: '100%',
|
|
379
|
+
left: 0,
|
|
380
|
+
top: 0,
|
|
381
|
+
right: 'auto',
|
|
382
|
+
bottom: 0,
|
|
383
|
+
transform: 'none',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
298
386
|
return h('video', {
|
|
299
387
|
ref: videoRef,
|
|
300
388
|
className: 'cinecrew-player__video',
|
|
@@ -304,21 +392,29 @@ function WebPlayerSurface({
|
|
|
304
392
|
muted: muted || videoOnly,
|
|
305
393
|
playsInline: true,
|
|
306
394
|
preload: 'auto',
|
|
307
|
-
style: {
|
|
395
|
+
style: {
|
|
396
|
+
...videoStyle,
|
|
397
|
+
...resizedVideoStyle,
|
|
398
|
+
opacity: audioOnly ? 0 : 1,
|
|
399
|
+
},
|
|
308
400
|
onClick: inlinePreview ? onPromotePreview : undefined,
|
|
309
401
|
onError: (event) => {
|
|
402
|
+
if (!directVideoSource) return;
|
|
310
403
|
const mediaError = event.currentTarget?.error;
|
|
311
404
|
handleError({ message: mediaError?.message || 'The browser could not load this stream. Check URL, codec and CORS support.', code: mediaError?.code, cause: mediaError });
|
|
312
405
|
},
|
|
313
406
|
});
|
|
314
407
|
}
|
|
315
408
|
|
|
316
|
-
function WebAudioOnlyCard({ poster, title, theme, onSwitchToVideo }) {
|
|
409
|
+
function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo }) {
|
|
317
410
|
return h('div', {
|
|
318
411
|
className: 'cinecrew-player__audio-card',
|
|
319
412
|
style: { background: theme.surfaceColor, color: theme.controlColor, borderColor: theme.accentColor },
|
|
320
413
|
},
|
|
321
|
-
poster
|
|
414
|
+
poster
|
|
415
|
+
? h('img', { className: 'cinecrew-player__audio-poster', src: poster, alt: '' })
|
|
416
|
+
: h('div', { className: 'cinecrew-player__audio-placeholder', role: 'img', 'aria-label': 'Audio artwork placeholder' },
|
|
417
|
+
h(Icon, { name: 'audioOnly', icons, color: theme.accentColor })),
|
|
322
418
|
h('span', { className: 'cinecrew-player__audio-wave', style: { color: theme.accentColor }, 'aria-hidden': true }, '•••••••'),
|
|
323
419
|
h('strong', null, title || 'Audio only'),
|
|
324
420
|
h('button', {
|
|
@@ -346,7 +442,9 @@ function isAudioPlaybackEnabled(muted, videoOnly) {
|
|
|
346
442
|
}
|
|
347
443
|
|
|
348
444
|
function getPanelIntegration(activePanel, integrations) {
|
|
349
|
-
|
|
445
|
+
if (activePanel === 'chat') return integrations.liveChat;
|
|
446
|
+
if (activePanel === 'epg') return integrations.epg;
|
|
447
|
+
return null;
|
|
350
448
|
}
|
|
351
449
|
|
|
352
450
|
function getActiveMediaUrl(youtubeVideoId, streamUrl) {
|
|
@@ -359,15 +457,11 @@ function getInlinePlayerStyle(inlinePreview, rect) {
|
|
|
359
457
|
}
|
|
360
458
|
|
|
361
459
|
function buildUnlockedControls({
|
|
362
|
-
control,
|
|
460
|
+
control, onBack, isLive, restart, toggleLock, toggleMute, muted, hasRecording,
|
|
363
461
|
recording, integrations, action, videoRef, streamUrl, title, activePanel,
|
|
364
|
-
hasChat, hasEpg,
|
|
462
|
+
hasChat, hasEpg, hasDiagnostics, handleOpenPanel, setRecording, backVisible,
|
|
365
463
|
}) {
|
|
366
|
-
const
|
|
367
|
-
control('back', 'Back', onBack, { defaultVisible: backVisible }),
|
|
368
|
-
...(!isLive ? [control('restart', 'Restart', restart)] : []),
|
|
369
|
-
control('lock', 'Lock controls', toggleLock, { active: false, defaultVisible: true }),
|
|
370
|
-
];
|
|
464
|
+
const leftControls = [control('back', 'Back', onBack, { defaultVisible: backVisible })];
|
|
371
465
|
let recordingControl = null;
|
|
372
466
|
if (hasRecording) {
|
|
373
467
|
const recordingLabel = recording ? 'Stop recording' : 'Start recording';
|
|
@@ -382,19 +476,39 @@ function buildUnlockedControls({
|
|
|
382
476
|
}
|
|
383
477
|
const chatLabel = activePanel === 'chat' ? 'Close live chat' : 'Live chat';
|
|
384
478
|
const epgLabel = activePanel === 'epg' ? 'Close programme guide' : 'Programme guide';
|
|
385
|
-
|
|
386
|
-
...controls,
|
|
479
|
+
const rightControls = [
|
|
387
480
|
recordingControl,
|
|
388
481
|
control('liveChat', chatLabel, () => handleOpenPanel('chat'), { active: activePanel === 'chat', defaultVisible: hasChat }),
|
|
389
482
|
control('epg', epgLabel, () => handleOpenPanel('epg'), { active: activePanel === 'epg', defaultVisible: hasEpg }),
|
|
390
|
-
control('
|
|
483
|
+
...(!isLive ? [control('restart', 'Restart', restart)] : []),
|
|
391
484
|
];
|
|
485
|
+
const muteLabel = muted ? 'Unmute' : 'Mute';
|
|
486
|
+
rightControls.push(
|
|
487
|
+
control('mute', muteLabel, toggleMute, { icon: muted ? 'mute' : 'unmute' }),
|
|
488
|
+
control('diagnostics', 'Stream diagnostics', () => handleOpenPanel('diagnostics'), { active: activePanel === 'diagnostics', defaultVisible: hasDiagnostics }),
|
|
489
|
+
control('lock', 'Lock controls', toggleLock, { active: false, defaultVisible: true }),
|
|
490
|
+
);
|
|
491
|
+
return { left: leftControls, right: rightControls };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getDrawerLabel(activePanel) {
|
|
495
|
+
if (activePanel === 'chat') return 'Live chat drawer';
|
|
496
|
+
if (activePanel === 'epg') return 'Programme guide drawer';
|
|
497
|
+
return 'Stream diagnostics drawer';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getPlaybackStatus(error, buffering, paused) {
|
|
501
|
+
if (error) return 'Error';
|
|
502
|
+
if (buffering) return 'Buffering';
|
|
503
|
+
return paused ? 'Paused' : 'Playing';
|
|
392
504
|
}
|
|
393
505
|
|
|
394
506
|
function WebPlayerLayout(props) {
|
|
395
|
-
|
|
396
|
-
|
|
507
|
+
let controlLayer = null;
|
|
508
|
+
if (!props.error && !props.audioOnly) {
|
|
509
|
+
controlLayer = h(WebPlayerControls, {
|
|
397
510
|
locked: props.locked,
|
|
511
|
+
buffering: props.buffering,
|
|
398
512
|
overrides: props.controlOverrides,
|
|
399
513
|
theme: props.theme,
|
|
400
514
|
icons: props.icons,
|
|
@@ -403,25 +517,33 @@ function WebPlayerLayout(props) {
|
|
|
403
517
|
paused: props.isPaused,
|
|
404
518
|
togglePlay: props.togglePlay,
|
|
405
519
|
bottomProps: props.bottomControlProps,
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
:
|
|
411
|
-
|
|
412
|
-
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
let playerTitle = null;
|
|
523
|
+
if (props.title && !props.audioOnly) {
|
|
524
|
+
playerTitle = h('div', { className: 'cinecrew-player__title', style: { color: props.theme.controlColor } }, props.title);
|
|
525
|
+
}
|
|
526
|
+
let loadingNotice = null;
|
|
527
|
+
if (props.buffering && !props.error) {
|
|
528
|
+
loadingNotice = h('div', { className: 'cinecrew-player__status', style: { color: props.theme.controlColor } },
|
|
413
529
|
h('span', { className: 'cinecrew-player__spinner', style: { borderTopColor: props.theme.accentColor } }), 'Loading stream…')
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
530
|
+
}
|
|
531
|
+
let audioCard = null;
|
|
532
|
+
if (props.audioOnly) {
|
|
533
|
+
audioCard = h(WebAudioOnlyCard, { poster: props.poster, title: props.title, theme: props.theme, icons: props.icons, onSwitchToVideo: props.onSwitchToVideo });
|
|
534
|
+
}
|
|
535
|
+
let panelNode = null;
|
|
536
|
+
if (props.activePanel && props.webPanel) {
|
|
537
|
+
panelNode = h('aside', {
|
|
538
|
+
className: `cinecrew-player__panel${props.drawerMode === 'resize' ? ' is-resizing' : ''}`,
|
|
539
|
+
style: { color: props.theme.controlColor, ...props.drawerStyle },
|
|
540
|
+
'aria-label': getDrawerLabel(props.activePanel),
|
|
541
|
+
}, props.webPanel);
|
|
542
|
+
}
|
|
421
543
|
return h('div', {
|
|
422
544
|
ref: props.playerRef,
|
|
423
|
-
className: `cinecrew-player${props.inlinePreview ? ' cinecrew-player--inline-preview' : ''} ${props.className}`.trim(),
|
|
424
|
-
style: { ...props.rootStyle, ...props.style, background: props.theme.backgroundColor, borderRadius: props.theme.borderRadius, '--cinecrew-accent': props.theme.accentColor, '--cinecrew-text': props.theme.controlColor, '--cinecrew-surface': props.theme.surfaceColor },
|
|
545
|
+
className: `cinecrew-player${props.inlinePreview ? ' cinecrew-player--inline-preview' : ''}${props.drawerMode === 'resize' && props.activePanel ? ' cinecrew-player--drawer-resize' : ''} ${props.className}`.trim(),
|
|
546
|
+
style: { ...props.rootStyle, ...props.style, background: props.theme.backgroundColor, borderRadius: props.theme.borderRadius, '--cinecrew-accent': props.theme.accentColor, '--cinecrew-text': props.theme.controlColor, '--cinecrew-surface': props.theme.surfaceColor, '--cinecrew-media-width': '64%' },
|
|
425
547
|
onWheel: props.onWheel,
|
|
426
548
|
'data-stream-mode': props.streamMode,
|
|
427
549
|
},
|
|
@@ -429,7 +551,7 @@ function WebPlayerLayout(props) {
|
|
|
429
551
|
h('div', { className: 'cinecrew-player__shade', style: { background: 'linear-gradient(180deg, rgba(0,0,0,.48), transparent 28%, transparent 68%, rgba(0,0,0,.64))' } }),
|
|
430
552
|
playerTitle,
|
|
431
553
|
loadingNotice,
|
|
432
|
-
h(WebPlayerError, { error: props.error, theme: props.theme, renderBackButton: props.renderBackButton }),
|
|
554
|
+
h(WebPlayerError, { error: props.error, theme: props.theme, renderBackButton: props.locked ? () => null : props.renderBackButton }),
|
|
433
555
|
controlLayer,
|
|
434
556
|
audioCard,
|
|
435
557
|
panelNode);
|
|
@@ -473,7 +595,13 @@ function getPanelRenderer(activePanel, renderLiveChat, renderEpg, integrations)
|
|
|
473
595
|
return null;
|
|
474
596
|
}
|
|
475
597
|
|
|
476
|
-
function createWebPanelNode({
|
|
598
|
+
function createWebPanelNode({
|
|
599
|
+
renderer, integration, activePanel, integrations, source, title, theme, icons, onClose,
|
|
600
|
+
diagnostics, messagePageSize,
|
|
601
|
+
}) {
|
|
602
|
+
if (activePanel === 'diagnostics') {
|
|
603
|
+
return h(WebDiagnosticsPanel, { title, theme, icons, onClose, ...diagnostics });
|
|
604
|
+
}
|
|
477
605
|
if (renderer) return h(renderer, { title, source, onClose });
|
|
478
606
|
if (!integration) return null;
|
|
479
607
|
const hasLoader = activePanel === 'chat'
|
|
@@ -488,20 +616,247 @@ function createWebPanelNode({ renderer, integration, activePanel, integrations,
|
|
|
488
616
|
title,
|
|
489
617
|
theme,
|
|
490
618
|
onClose,
|
|
619
|
+
messagePageSize,
|
|
491
620
|
});
|
|
492
621
|
}
|
|
493
622
|
|
|
494
|
-
function
|
|
623
|
+
function getBufferedAhead(video, currentTime) {
|
|
624
|
+
if (!video?.buffered) return null;
|
|
625
|
+
for (let index = 0; index < video.buffered.length; index += 1) {
|
|
626
|
+
const start = video.buffered.start(index);
|
|
627
|
+
const end = video.buffered.end(index);
|
|
628
|
+
if (currentTime >= start && currentTime <= end) return Math.max(0, end - currentTime);
|
|
629
|
+
}
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function getFrameQuality(video) {
|
|
634
|
+
try {
|
|
635
|
+
const quality = video?.getVideoPlaybackQuality?.();
|
|
636
|
+
if (quality) return { dropped: quality.droppedVideoFrames, total: quality.totalVideoFrames };
|
|
637
|
+
} catch {
|
|
638
|
+
// Playback quality is optional and may not be exposed by a media engine.
|
|
639
|
+
}
|
|
640
|
+
if (Number.isFinite(video?.webkitDroppedFrameCount) || Number.isFinite(video?.webkitDecodedFrameCount)) {
|
|
641
|
+
return { dropped: video.webkitDroppedFrameCount, total: video.webkitDecodedFrameCount };
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function WebDiagnosticsPanel({ title, theme, icons, onClose, streamMode, status, currentTime, duration, videoRef }) {
|
|
647
|
+
const video = videoRef?.current;
|
|
648
|
+
const resolution = video?.videoWidth && video?.videoHeight
|
|
649
|
+
? `${video.videoWidth} × ${video.videoHeight}`
|
|
650
|
+
: 'Not available';
|
|
651
|
+
const connection = globalThis.navigator?.connection;
|
|
652
|
+
const buffered = getBufferedAhead(video, currentTime);
|
|
653
|
+
const frames = getFrameQuality(video);
|
|
654
|
+
const cards = [
|
|
655
|
+
{ icon: 'display', label: 'Video resolution', value: resolution },
|
|
656
|
+
{ icon: 'network', label: 'Connection rate', value: Number.isFinite(connection?.downlink) ? `${connection.downlink} Mbps` : 'Not reported' },
|
|
657
|
+
{ icon: 'clock', label: 'Network RTT estimate', value: Number.isFinite(connection?.rtt) ? `${connection.rtt} ms` : 'Not reported' },
|
|
658
|
+
{ icon: 'buffer', label: 'Buffered ahead', value: buffered === null ? 'Not reported' : `${buffered.toFixed(1)} sec` },
|
|
659
|
+
{ icon: 'frames', label: 'Dropped / total frames', value: frames && Number.isFinite(frames.dropped) ? `${frames.dropped} / ${frames.total ?? '—'}` : 'Not reported' },
|
|
660
|
+
{ icon: 'speedometer', label: 'Playback position', value: `${formatTime(currentTime)} / ${formatTime(duration)}` },
|
|
661
|
+
{ icon: 'network', label: 'Connection type', value: connection?.effectiveType || 'Not reported' },
|
|
662
|
+
{ icon: 'video', label: 'Stream format', value: streamMode || 'Unknown' },
|
|
663
|
+
];
|
|
664
|
+
return h('section', {
|
|
665
|
+
className: 'cinecrew-player__diagnostics',
|
|
666
|
+
style: { color: theme.controlColor },
|
|
667
|
+
'aria-label': 'Stream diagnostics',
|
|
668
|
+
},
|
|
669
|
+
h('header', { className: 'cinecrew-player__diagnostics-heading' },
|
|
670
|
+
h('div', null,
|
|
671
|
+
h('strong', null, 'Stream diagnostics'),
|
|
672
|
+
h('span', null, title || 'Current playback')),
|
|
673
|
+
h('button', {
|
|
674
|
+
type: 'button',
|
|
675
|
+
className: 'cinecrew-player__diagnostics-close',
|
|
676
|
+
onClick: onClose,
|
|
677
|
+
'aria-label': 'Close stream diagnostics',
|
|
678
|
+
title: 'Close diagnostics',
|
|
679
|
+
}, h(Icon, { name: 'close', icons, color: theme.controlColor }))),
|
|
680
|
+
h('div', { className: 'cinecrew-player__diagnostics-status' },
|
|
681
|
+
h('span', { className: `cinecrew-player__status-dot is-${String(status || 'unknown').toLowerCase()}` }),
|
|
682
|
+
h('span', null, status || 'Unknown'),
|
|
683
|
+
h('span', null, streamMode || 'Unknown format')),
|
|
684
|
+
h('div', { className: 'cinecrew-player__diagnostic-grid' },
|
|
685
|
+
cards.map(({ icon, label, value }) => h('article', { className: 'cinecrew-player__diagnostic-card', key: label },
|
|
686
|
+
h('span', { className: 'cinecrew-player__diagnostic-icon' }, h(Icon, { name: icon, icons, color: theme.accentColor })),
|
|
687
|
+
h('span', { className: 'cinecrew-player__diagnostic-label' }, label),
|
|
688
|
+
h('strong', null, value)))),
|
|
689
|
+
h('small', { className: 'cinecrew-player__diagnostics-note' }, 'Network rate and RTT are browser-reported estimates when supported; no extra ping or stream requests are sent.'));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function getChatMessageKey(message, index = 0) {
|
|
693
|
+
return String(message?.id ?? message?.messageId ?? message?.createdAt ?? message?.timestamp
|
|
694
|
+
?? `${message?.username || message?.userName || ''}:${message?.comment || message?.message || message?.text || index}`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function mergeChatMessages(existing, incoming, prepend = false) {
|
|
698
|
+
const messages = prepend ? [...incoming, ...existing] : [...existing, ...incoming];
|
|
699
|
+
const unique = new Map();
|
|
700
|
+
messages.forEach((message, index) => unique.set(getChatMessageKey(message, index), message));
|
|
701
|
+
return [...unique.values()];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function formatChatTimestamp(value) {
|
|
705
|
+
if (value === null || value === undefined || value === '') return '';
|
|
706
|
+
const date = new Date(value);
|
|
707
|
+
return Number.isNaN(date.getTime())
|
|
708
|
+
? String(value)
|
|
709
|
+
: date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const EMOJI_BATCH_SIZE = 96;
|
|
713
|
+
|
|
714
|
+
function WebEmojiPicker({ onSelect }) {
|
|
715
|
+
const [emojiGroup, setEmojiGroup] = useState(EMOJI_GROUPS[0].name);
|
|
716
|
+
const [emojiQuery, setEmojiQuery] = useState('');
|
|
717
|
+
const [visibleCount, setVisibleCount] = useState(EMOJI_BATCH_SIZE);
|
|
718
|
+
const listRef = useRef(null);
|
|
719
|
+
const activeGroup = EMOJI_GROUPS.find((group) => group.name === emojiGroup) || EMOJI_GROUPS[0];
|
|
720
|
+
const filteredEmojis = useMemo(
|
|
721
|
+
() => emojiQuery.trim() ? searchEmojis(emojiQuery) : activeGroup.items,
|
|
722
|
+
[activeGroup, emojiQuery],
|
|
723
|
+
);
|
|
724
|
+
const visibleEmojis = filteredEmojis.slice(0, visibleCount);
|
|
725
|
+
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
setVisibleCount(EMOJI_BATCH_SIZE);
|
|
728
|
+
if (listRef.current) listRef.current.scrollTop = 0;
|
|
729
|
+
}, [emojiGroup, emojiQuery]);
|
|
730
|
+
|
|
731
|
+
const loadMore = () => setVisibleCount((count) => Math.min(count + EMOJI_BATCH_SIZE, filteredEmojis.length));
|
|
732
|
+
const handleScroll = (event) => {
|
|
733
|
+
const element = event.currentTarget;
|
|
734
|
+
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 32) loadMore();
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
return h('div', { className: 'cinecrew-player__emoji-picker', role: 'dialog', 'aria-label': 'Choose an emoji' },
|
|
738
|
+
h('input', {
|
|
739
|
+
className: 'cinecrew-player__emoji-search',
|
|
740
|
+
type: 'search',
|
|
741
|
+
value: emojiQuery,
|
|
742
|
+
onChange: (event) => setEmojiQuery(event.target.value),
|
|
743
|
+
placeholder: 'Search all emojis',
|
|
744
|
+
'aria-label': 'Search all emojis',
|
|
745
|
+
}),
|
|
746
|
+
h('nav', { className: 'cinecrew-player__emoji-categories', 'aria-label': 'Emoji categories' },
|
|
747
|
+
EMOJI_GROUPS.map((group) => h('button', {
|
|
748
|
+
key: group.name,
|
|
749
|
+
type: 'button',
|
|
750
|
+
className: !emojiQuery.trim() && group.name === emojiGroup ? 'is-active' : '',
|
|
751
|
+
onClick: () => {
|
|
752
|
+
setEmojiGroup(group.name);
|
|
753
|
+
setEmojiQuery('');
|
|
754
|
+
},
|
|
755
|
+
'aria-label': group.name,
|
|
756
|
+
title: group.name,
|
|
757
|
+
'aria-pressed': !emojiQuery.trim() && group.name === emojiGroup,
|
|
758
|
+
}, group.icon))),
|
|
759
|
+
h('div', {
|
|
760
|
+
className: 'cinecrew-player__emoji-grid',
|
|
761
|
+
role: 'grid',
|
|
762
|
+
ref: listRef,
|
|
763
|
+
onScroll: handleScroll,
|
|
764
|
+
},
|
|
765
|
+
visibleEmojis.map((item) => h('button', {
|
|
766
|
+
key: item.codepoints,
|
|
767
|
+
type: 'button',
|
|
768
|
+
onClick: () => onSelect(item.emoji),
|
|
769
|
+
'aria-label': `Insert ${item.short_name}`,
|
|
770
|
+
title: item.short_name,
|
|
771
|
+
}, item.emoji)),
|
|
772
|
+
visibleCount < filteredEmojis.length ? h('button', {
|
|
773
|
+
type: 'button',
|
|
774
|
+
className: 'cinecrew-player__emoji-load-more',
|
|
775
|
+
onClick: loadMore,
|
|
776
|
+
}, 'Load more emojis') : null));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function WebChatComposer({ sending, canSend, onSend }) {
|
|
780
|
+
const [message, setMessage] = useState('');
|
|
781
|
+
const [emojiOpen, setEmojiOpen] = useState(false);
|
|
782
|
+
const handleSubmit = async (event) => {
|
|
783
|
+
event.preventDefault();
|
|
784
|
+
const comment = message.trim();
|
|
785
|
+
if (!comment || sending || !canSend) return;
|
|
786
|
+
const sent = await onSend(comment);
|
|
787
|
+
if (sent) setMessage('');
|
|
788
|
+
};
|
|
789
|
+
const insertEmoji = (emoji) => setMessage((current) => `${current}${emoji}`);
|
|
790
|
+
|
|
791
|
+
return h('form', { className: 'cinecrew-player__chat-form', onSubmit: handleSubmit },
|
|
792
|
+
h('div', { className: 'cinecrew-player__chat-composer' },
|
|
793
|
+
h('button', {
|
|
794
|
+
type: 'button',
|
|
795
|
+
className: 'cinecrew-player__emoji-toggle',
|
|
796
|
+
onClick: () => setEmojiOpen((open) => !open),
|
|
797
|
+
'aria-label': emojiOpen ? 'Close emoji picker' : 'Open emoji picker',
|
|
798
|
+
}, '☺'),
|
|
799
|
+
h('input', {
|
|
800
|
+
value: message,
|
|
801
|
+
onChange: (event) => setMessage(event.target.value),
|
|
802
|
+
placeholder: 'Add a message…',
|
|
803
|
+
'aria-label': 'Chat message',
|
|
804
|
+
maxLength: 1000,
|
|
805
|
+
}),
|
|
806
|
+
h('button', {
|
|
807
|
+
type: 'submit',
|
|
808
|
+
disabled: sending || !message.trim() || !canSend,
|
|
809
|
+
'aria-label': sending ? 'Sending message' : 'Send message',
|
|
810
|
+
title: sending ? 'Sending…' : 'Send message',
|
|
811
|
+
}, h(Icon, { name: 'send', color: '#07111e' }))),
|
|
812
|
+
emojiOpen ? h(WebEmojiPicker, { onSelect: insertEmoji }) : null);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function WebPanelRow({ row, index, isChat }) {
|
|
816
|
+
if (isChat) {
|
|
817
|
+
const timestamp = row.createdAt ?? row.timestamp ?? row.sentAt ?? row.time;
|
|
818
|
+
return h('article', { key: row.id ?? row.messageId ?? index, className: 'cinecrew-player__chat-message' },
|
|
819
|
+
h('header', null,
|
|
820
|
+
h('strong', null, row.username || row.userName || row.name || 'Viewer'),
|
|
821
|
+
timestamp ? h('time', { dateTime: String(timestamp) }, formatChatTimestamp(timestamp)) : null),
|
|
822
|
+
h('span', null, row.comment || row.message || row.text || ''));
|
|
823
|
+
}
|
|
824
|
+
const start = row.startMs ?? row.start ?? row.startTime;
|
|
825
|
+
const end = row.endMs ?? row.end ?? row.endTime;
|
|
826
|
+
return h('article', { key: row.id ?? row.startMs ?? index, className: 'cinecrew-player__epg-item' },
|
|
827
|
+
h('small', null, [formatListingTime(start), formatListingTime(end)].filter(Boolean).join(' – ')),
|
|
828
|
+
h('strong', null, row.title || row.name || 'Programme'),
|
|
829
|
+
row.description ? h('span', null, row.description) : null);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function WebLoadMoreMessages({ loading, onClick }) {
|
|
833
|
+
return h('button', {
|
|
834
|
+
type: 'button',
|
|
835
|
+
className: 'cinecrew-player__load-more',
|
|
836
|
+
onClick,
|
|
837
|
+
disabled: loading,
|
|
838
|
+
}, loading ? 'Loading more…' : 'See more messages');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function WebIntegrationPanel({ kind, integration, integrations, source, title, theme, onClose, messagePageSize = 50 }) {
|
|
495
842
|
const [rows, setRows] = useState([]);
|
|
496
843
|
const [user, setUser] = useState(integrations.user || null);
|
|
497
|
-
const [message, setMessage] = useState('');
|
|
498
844
|
const [loading, setLoading] = useState(true);
|
|
845
|
+
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
846
|
+
const [hasMoreMessages, setHasMoreMessages] = useState(false);
|
|
847
|
+
const [messageOffset, setMessageOffset] = useState(0);
|
|
499
848
|
const [sending, setSending] = useState(false);
|
|
500
849
|
const [error, setError] = useState('');
|
|
850
|
+
const rowsRef = useRef([]);
|
|
501
851
|
const channelId = String(source.streamId || source.mediaId || source.id || '');
|
|
502
852
|
const isChat = kind === 'chat';
|
|
853
|
+
const pageSize = Math.max(1, Math.floor(Number(messagePageSize) || 50));
|
|
503
854
|
const emptyStateMessage = isChat ? 'No messages yet.' : 'No programme information available.';
|
|
504
|
-
|
|
855
|
+
|
|
856
|
+
const updateRows = (nextRows) => {
|
|
857
|
+
rowsRef.current = nextRows;
|
|
858
|
+
setRows(nextRows);
|
|
859
|
+
};
|
|
505
860
|
|
|
506
861
|
const load = useCallback(async () => {
|
|
507
862
|
if (!channelId) {
|
|
@@ -512,15 +867,30 @@ function WebIntegrationPanel({ kind, integration, integrations, source, title, t
|
|
|
512
867
|
try {
|
|
513
868
|
setError('');
|
|
514
869
|
const value = isChat
|
|
515
|
-
? await integration.loadMessages({ channelId, limit:
|
|
870
|
+
? await integration.loadMessages({ channelId, limit: pageSize, offset: 0 })
|
|
516
871
|
: await integration.loadListings({ channelId, limit: integration.limit || 48 });
|
|
517
|
-
|
|
872
|
+
const nextRows = responseRows(value, isChat ? ['messages', 'items', 'comments'] : ['listings', 'programmes', 'epg', 'items']);
|
|
873
|
+
if (isChat) {
|
|
874
|
+
const previousCount = rowsRef.current.length;
|
|
875
|
+
const mergedRows = mergeChatMessages(rowsRef.current, nextRows);
|
|
876
|
+
updateRows(mergedRows);
|
|
877
|
+
const addedCount = Math.max(0, mergedRows.length - previousCount);
|
|
878
|
+
setMessageOffset((offset) => previousCount === 0
|
|
879
|
+
? nextRows.length
|
|
880
|
+
: Math.max(offset, nextRows.length) + addedCount);
|
|
881
|
+
if (previousCount === 0) {
|
|
882
|
+
const explicitHasMore = value?.hasMore ?? value?.pagination?.hasMore;
|
|
883
|
+
setHasMoreMessages(explicitHasMore === undefined ? nextRows.length >= pageSize : Boolean(explicitHasMore));
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
setRows(nextRows);
|
|
887
|
+
}
|
|
518
888
|
} catch (loadError) {
|
|
519
889
|
setError(loadError?.message || `Could not load ${isChat ? 'live chat' : 'the programme guide'}.`);
|
|
520
890
|
} finally {
|
|
521
891
|
setLoading(false);
|
|
522
892
|
}
|
|
523
|
-
}, [channelId, integration, isChat]);
|
|
893
|
+
}, [channelId, integration, isChat, pageSize]);
|
|
524
894
|
|
|
525
895
|
useEffect(() => {
|
|
526
896
|
let cancelled = false;
|
|
@@ -534,16 +904,36 @@ function WebIntegrationPanel({ kind, integration, integrations, source, title, t
|
|
|
534
904
|
|
|
535
905
|
useEffect(() => {
|
|
536
906
|
setLoading(true);
|
|
907
|
+
rowsRef.current = [];
|
|
908
|
+
setRows([]);
|
|
909
|
+
setMessageOffset(0);
|
|
910
|
+
setHasMoreMessages(false);
|
|
537
911
|
load();
|
|
538
912
|
if (!isChat) return undefined;
|
|
539
913
|
const timer = setInterval(load, Math.max(1000, Number(integration.pollIntervalMs) || 5000));
|
|
540
914
|
return () => clearInterval(timer);
|
|
541
915
|
}, [load, isChat, integration.pollIntervalMs]);
|
|
542
916
|
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
917
|
+
const loadOlderMessages = async () => {
|
|
918
|
+
if (!isChat || !hasMoreMessages || loadingOlder) return;
|
|
919
|
+
setLoadingOlder(true);
|
|
920
|
+
try {
|
|
921
|
+
setError('');
|
|
922
|
+
const value = await integration.loadMessages({ channelId, limit: pageSize, offset: messageOffset });
|
|
923
|
+
const olderRows = responseRows(value, ['messages', 'items', 'comments']);
|
|
924
|
+
updateRows(mergeChatMessages(rowsRef.current, olderRows, true));
|
|
925
|
+
setMessageOffset((offset) => offset + olderRows.length);
|
|
926
|
+
const explicitHasMore = value?.hasMore ?? value?.pagination?.hasMore;
|
|
927
|
+
setHasMoreMessages(explicitHasMore === undefined ? olderRows.length >= pageSize : Boolean(explicitHasMore));
|
|
928
|
+
} catch (loadError) {
|
|
929
|
+
setError(loadError?.message || 'Could not load more messages.');
|
|
930
|
+
} finally {
|
|
931
|
+
setLoadingOlder(false);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const send = async (comment) => {
|
|
936
|
+
if (!comment || sending || !integration.sendMessage) return false;
|
|
547
937
|
setSending(true);
|
|
548
938
|
try {
|
|
549
939
|
await integration.sendMessage({
|
|
@@ -552,10 +942,11 @@ function WebIntegrationPanel({ kind, integration, integrations, source, title, t
|
|
|
552
942
|
username: user?.username || 'Viewer',
|
|
553
943
|
comment,
|
|
554
944
|
});
|
|
555
|
-
setMessage('');
|
|
556
945
|
await load();
|
|
946
|
+
return true;
|
|
557
947
|
} catch (sendError) {
|
|
558
948
|
setError(sendError?.message || 'Could not send your message.');
|
|
949
|
+
return false;
|
|
559
950
|
} finally {
|
|
560
951
|
setSending(false);
|
|
561
952
|
}
|
|
@@ -572,29 +963,14 @@ function WebIntegrationPanel({ kind, integration, integrations, source, title, t
|
|
|
572
963
|
loading ? h('div', { className: 'cinecrew-player__panel-state' }, 'Loading…') : null,
|
|
573
964
|
error ? h('div', { className: 'cinecrew-player__panel-state is-error', role: 'status' }, error) : null,
|
|
574
965
|
!loading && !error && rows.length === 0 ? h('div', { className: 'cinecrew-player__panel-state' }, emptyStateMessage) : null,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const end = row.endMs ?? row.end ?? row.endTime;
|
|
584
|
-
return h('article', { key, className: 'cinecrew-player__epg-item' },
|
|
585
|
-
h('small', null, [formatListingTime(start), formatListingTime(end)].filter(Boolean).join(' – ')),
|
|
586
|
-
h('strong', null, row.title || row.name || 'Programme'),
|
|
587
|
-
row.description ? h('span', null, row.description) : null);
|
|
588
|
-
})),
|
|
589
|
-
isChat && typeof integration.sendMessage === 'function' ? h('form', { className: 'cinecrew-player__chat-form', onSubmit: send },
|
|
590
|
-
h('input', {
|
|
591
|
-
value: message,
|
|
592
|
-
onChange: (event) => setMessage(event.target.value),
|
|
593
|
-
placeholder: 'Add a message…',
|
|
594
|
-
'aria-label': 'Chat message',
|
|
595
|
-
maxLength: 1000,
|
|
596
|
-
}),
|
|
597
|
-
h('button', { type: 'submit', disabled: sending || !message.trim() }, sendButtonLabel)) : null);
|
|
966
|
+
h('div', { className: 'cinecrew-player__panel-list' },
|
|
967
|
+
isChat && hasMoreMessages ? h(WebLoadMoreMessages, { loading: loadingOlder, onClick: loadOlderMessages }) : null,
|
|
968
|
+
rows.map((row, index) => h(WebPanelRow, { key: getChatMessageKey(row, index), row, index, isChat }))),
|
|
969
|
+
isChat ? h(WebChatComposer, {
|
|
970
|
+
sending,
|
|
971
|
+
canSend: typeof integration.sendMessage === 'function',
|
|
972
|
+
onSend: send,
|
|
973
|
+
}) : null);
|
|
598
974
|
}
|
|
599
975
|
|
|
600
976
|
/**
|
|
@@ -614,6 +990,10 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
614
990
|
playbackRate: playbackRateProp = 1,
|
|
615
991
|
paused: pausedProp,
|
|
616
992
|
controls: controlOverrides = {},
|
|
993
|
+
features = {},
|
|
994
|
+
drawerMode = 'overlay',
|
|
995
|
+
drawerStyle,
|
|
996
|
+
messagePageSize = 50,
|
|
617
997
|
actions = {},
|
|
618
998
|
theme: themeProp = {},
|
|
619
999
|
icons = {},
|
|
@@ -634,7 +1014,6 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
634
1014
|
onPromotePreview,
|
|
635
1015
|
initialShowLiveChat = false,
|
|
636
1016
|
liveChatNonce = 0,
|
|
637
|
-
onMinimize,
|
|
638
1017
|
onBack,
|
|
639
1018
|
mediaId,
|
|
640
1019
|
onFullscreen,
|
|
@@ -758,6 +1137,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
758
1137
|
pausedRef: pausedStateRef,
|
|
759
1138
|
onErrorRef,
|
|
760
1139
|
});
|
|
1140
|
+
const streamMode = getStreamMode(youtubeVideoId, mpegTs.useMpegTs, useHls);
|
|
761
1141
|
useWebAc3AudioPlayback({
|
|
762
1142
|
active: mpegTs.useAc3Fallback,
|
|
763
1143
|
streamUrl,
|
|
@@ -772,7 +1152,9 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
772
1152
|
if (!video) return;
|
|
773
1153
|
if (isPaused) video.pause();
|
|
774
1154
|
else video.play().catch((playError) => {
|
|
775
|
-
if (playError?.name !== 'NotAllowedError'
|
|
1155
|
+
if (playError?.name !== 'NotAllowedError' && playError?.name !== 'AbortError') {
|
|
1156
|
+
handleError(playError);
|
|
1157
|
+
}
|
|
776
1158
|
});
|
|
777
1159
|
}, [isPaused, streamUrl, youtubeVideoId, handleError]);
|
|
778
1160
|
|
|
@@ -826,7 +1208,6 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
826
1208
|
else youtubeRef.current?.seekTo?.(Math.max(0, Number(seconds) || 0));
|
|
827
1209
|
}, { seconds: Number(seconds) || 0 }), [action]);
|
|
828
1210
|
const handleBack = useCallback(() => action('onBack', onBack || props.onClose, { title, source: media }), [action, onBack, props.onClose, title, media]);
|
|
829
|
-
const handleMinimize = useCallback(() => action('onMinimize', onMinimize, { title, source: media }), [action, onMinimize, title, media]);
|
|
830
1211
|
useImperativeHandle(ref, () => {
|
|
831
1212
|
const api = {
|
|
832
1213
|
play: () => setPaused(false),
|
|
@@ -847,7 +1228,6 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
847
1228
|
seekTo,
|
|
848
1229
|
seekBy: (delta) => seekTo((Number(videoRef.current?.currentTime) || currentTime) + (Number(delta) || 0)),
|
|
849
1230
|
back: handleBack,
|
|
850
|
-
minimize: handleMinimize,
|
|
851
1231
|
getVideoElement: () => videoRef.current,
|
|
852
1232
|
enterFullscreen: () => playerRef.current?.requestFullscreen?.(),
|
|
853
1233
|
exitFullscreen: () => typeof document !== 'undefined' ? document.exitFullscreen?.() : undefined,
|
|
@@ -855,11 +1235,12 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
855
1235
|
};
|
|
856
1236
|
publicPlayerRef.current = api;
|
|
857
1237
|
return api;
|
|
858
|
-
}, [setPaused, togglePlay, restart, toggleMute, setMuted, setAspectRatio, setAudioOnlyMode, setVideoOnlyMode, setPlaybackRateAction, seekTo, handleBack,
|
|
1238
|
+
}, [setPaused, togglePlay, restart, toggleMute, setMuted, setAspectRatio, setAudioOnlyMode, setVideoOnlyMode, setPlaybackRateAction, seekTo, handleBack, availableTracks, tracksProp, currentTime]);
|
|
859
1239
|
|
|
860
1240
|
const hasChat = typeof integrations.liveChat?.loadMessages === 'function' || typeof renderLiveChat === 'function' || typeof integrations.liveChat?.render === 'function' || typeof actions.onLiveChatOpen === 'function';
|
|
861
1241
|
const hasEpg = typeof integrations.epg?.loadListings === 'function' || typeof renderEpg === 'function' || typeof integrations.epg?.render === 'function' || typeof actions.onEpgOpen === 'function';
|
|
862
1242
|
const hasRecording = !!integrations.recording || typeof actions.onRecordingStart === 'function';
|
|
1243
|
+
const hasDiagnostics = Boolean(features.diagnostics) || typeof actions.onDiagnosticsOpen === 'function';
|
|
863
1244
|
const sourceType = String(media.type || media.mimeType || '').toLowerCase();
|
|
864
1245
|
const directVideoSource = getDirectVideoSource({
|
|
865
1246
|
mpegTs: mpegTs.useMpegTs,
|
|
@@ -948,10 +1329,11 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
948
1329
|
actions,
|
|
949
1330
|
onBack: handleBack,
|
|
950
1331
|
backVisible: typeof actions.onBack === 'function' || typeof onBack === 'function' || typeof props.onClose === 'function',
|
|
951
|
-
minimizeVisible: typeof actions.onMinimize === 'function' || typeof onMinimize === 'function',
|
|
952
1332
|
isLive,
|
|
953
1333
|
restart,
|
|
954
1334
|
toggleLock,
|
|
1335
|
+
toggleMute,
|
|
1336
|
+
muted,
|
|
955
1337
|
hasRecording,
|
|
956
1338
|
recording,
|
|
957
1339
|
integrations,
|
|
@@ -962,8 +1344,8 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
962
1344
|
activePanel,
|
|
963
1345
|
hasChat,
|
|
964
1346
|
hasEpg,
|
|
1347
|
+
hasDiagnostics,
|
|
965
1348
|
handleOpenPanel: openPanel,
|
|
966
|
-
handleMinimize,
|
|
967
1349
|
setRecording,
|
|
968
1350
|
});
|
|
969
1351
|
const webPanel = createWebPanelNode({
|
|
@@ -974,7 +1356,16 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
974
1356
|
source: panelSource,
|
|
975
1357
|
title,
|
|
976
1358
|
theme,
|
|
1359
|
+
icons,
|
|
977
1360
|
onClose: () => setActivePanel(null),
|
|
1361
|
+
diagnostics: {
|
|
1362
|
+
streamMode,
|
|
1363
|
+
status: getPlaybackStatus(error, buffering, isPaused),
|
|
1364
|
+
currentTime,
|
|
1365
|
+
duration,
|
|
1366
|
+
videoRef,
|
|
1367
|
+
},
|
|
1368
|
+
messagePageSize,
|
|
978
1369
|
});
|
|
979
1370
|
|
|
980
1371
|
const handleYouTubeProgress = (event) => {
|
|
@@ -996,6 +1387,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
996
1387
|
playbackRate,
|
|
997
1388
|
videoOnly,
|
|
998
1389
|
videoStyle,
|
|
1390
|
+
drawerResize: Boolean(activePanel) && drawerMode === 'resize',
|
|
999
1391
|
audioOnly,
|
|
1000
1392
|
inlinePreview,
|
|
1001
1393
|
onPromotePreview,
|
|
@@ -1008,8 +1400,6 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1008
1400
|
onError: handleError,
|
|
1009
1401
|
handleError,
|
|
1010
1402
|
});
|
|
1011
|
-
const streamMode = getStreamMode(youtubeVideoId, mpegTs.useMpegTs, useHls);
|
|
1012
|
-
|
|
1013
1403
|
const bottomControlProps = {
|
|
1014
1404
|
isLive,
|
|
1015
1405
|
overrides: controlOverrides,
|
|
@@ -1063,6 +1453,8 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1063
1453
|
togglePlay,
|
|
1064
1454
|
bottomControlProps,
|
|
1065
1455
|
onWheel: (event) => onInlinePreviewWheel?.(event.deltaY),
|
|
1456
|
+
drawerMode,
|
|
1457
|
+
drawerStyle,
|
|
1066
1458
|
});
|
|
1067
1459
|
});
|
|
1068
1460
|
|
|
@@ -1111,7 +1503,7 @@ export const InlineLivePlayer = React.memo(function InlineLivePlayer({
|
|
|
1111
1503
|
controls: {
|
|
1112
1504
|
back: false, restart: false, lock: false, recording: false, liveChat: false, epg: false,
|
|
1113
1505
|
seek: false, aspectRatio: false, videoOnly: false, audioOnly: false, audioTracks: false,
|
|
1114
|
-
playbackRate: false,
|
|
1506
|
+
playbackRate: false, playPause: true, mute: true, fullscreen: true,
|
|
1115
1507
|
...controls,
|
|
1116
1508
|
},
|
|
1117
1509
|
actions: {
|