@cinecrew/cinecrew-player 0.1.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/CineCrewPlayer.podspec +22 -0
- package/LICENSE +21 -0
- package/NOTICE +6 -0
- package/README.md +574 -0
- package/android/build.gradle +48 -0
- package/android/proguard-rules.pro +29 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/yuanzhou/vlc/ReactVlcPlayerPackage.java +28 -0
- package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerView.java +988 -0
- package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerViewManager.java +208 -0
- package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/VideoEventEmitter.java +136 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/app.plugin.cjs +1 -0
- package/assets/cinecrew-player-logo.svg +44 -0
- package/package.json +132 -0
- package/packages/react-native-vlc-media-player/LICENSE +21 -0
- package/packages/react-native-vlc-media-player/VLCPlayer.js +207 -0
- package/packages/react-native-vlc-media-player/expo/android/withGradleTasks.cjs +49 -0
- package/packages/react-native-vlc-media-player/expo/ios/withMobileVlcKit.cjs +28 -0
- package/packages/react-native-vlc-media-player/expo/withVlcMediaPlayer.cjs +10 -0
- package/packages/react-native-vlc-media-player/index.d.ts +354 -0
- package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.h +36 -0
- package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.m +635 -0
- package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.h +5 -0
- package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.m +100 -0
- package/src/native/InlineLivePlayer.js +321 -0
- package/src/native/MediaPlayerView.js +3093 -0
- package/src/native/customization.js +44 -0
- package/src/native/index.js +107 -0
- package/src/native/media/ElectronVideoPlayer.js +288 -0
- package/src/native/media/EmojiPickerModal.js +30 -0
- package/src/native/media/LiveChatDrawer.js +1177 -0
- package/src/native/media/LiveRecordingOverlay.js +319 -0
- package/src/native/media/WebVideoPlayer.js +320 -0
- package/src/native/media/YouTubeVideoPlayer.native.js +120 -0
- package/src/native/media/YouTubeVideoPlayer.web.js +169 -0
- package/src/native/media/player/AudioOnlyView.js +337 -0
- package/src/native/media/player/CenterControls.js +94 -0
- package/src/native/media/player/ExoVideoFallback.js +329 -0
- package/src/native/media/player/PipOverlay.js +1 -0
- package/src/native/media/player/PlayerBottomBar.js +295 -0
- package/src/native/media/player/PlayerTopBar.js +255 -0
- package/src/native/media/player/VLCBoundary.js +41 -0
- package/src/native/media/player/VerticalIndicator.js +184 -0
- package/src/native/media/player/index.js +9 -0
- package/src/native/media/player/playerConstants.js +159 -0
- package/src/native/media/web/useWebAc3AudioPlayback.native.js +6 -0
- package/src/native/media/web/useWebAc3AudioPlayback.web.js +219 -0
- package/src/native/media/web/useWebHlsPlayback.native.js +3 -0
- package/src/native/media/web/useWebHlsPlayback.web.js +66 -0
- package/src/native/media/web/useWebMediaSession.js +90 -0
- package/src/native/media/web/useWebMpegTsPlayback.native.js +3 -0
- package/src/native/media/web/useWebMpegTsPlayback.web.js +356 -0
- package/src/native/media/web/useWebVideoAspectRatio.js +52 -0
- package/src/native/media/web/webPlaybackErrors.js +4 -0
- package/src/utils/layoutUtils.js +12 -0
- package/src/utils/mediaUtils.js +7 -0
- package/src/utils/recording.js +8 -0
- package/src/utils/runtimePlatform.js +19 -0
- package/src/utils/sourceUtils.js +99 -0
- package/src/utils/youtubeHtml.js +34 -0
- package/src/web/index.js +1129 -0
- package/src/web/styles.css +378 -0
- package/types/index.d.ts +268 -0
- package/vendor/mpegts.js/LICENSE +202 -0
- package/vendor/mpegts.js/mpegts.js +3 -0
package/src/web/index.js
ADDED
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import { useWebVideoAspectRatio } from '../native/media/web/useWebVideoAspectRatio';
|
|
3
|
+
import { useWebMpegTsPlayback } from '../native/media/web/useWebMpegTsPlayback.web';
|
|
4
|
+
import { useWebHlsPlayback } from '../native/media/web/useWebHlsPlayback.web';
|
|
5
|
+
import { useWebAc3AudioPlayback } from '../native/media/web/useWebAc3AudioPlayback.web';
|
|
6
|
+
import { YouTubeVideoPlayer } from '../native/media/YouTubeVideoPlayer.web.js';
|
|
7
|
+
import { getYouTubeVideoId, getWebRuntimePlatform, useResolvedPlayerSource } from '../utils/sourceUtils';
|
|
8
|
+
import './styles.css';
|
|
9
|
+
|
|
10
|
+
const h = React.createElement;
|
|
11
|
+
const DEFAULT_THEME = {
|
|
12
|
+
accentColor: '#00E5FF',
|
|
13
|
+
backgroundColor: '#050b14',
|
|
14
|
+
controlBackground: 'rgba(5, 11, 20, 0.76)',
|
|
15
|
+
controlColor: '#ffffff',
|
|
16
|
+
surfaceColor: 'rgba(13, 26, 44, 0.96)',
|
|
17
|
+
errorColor: '#ff647c',
|
|
18
|
+
borderRadius: 14,
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_ICONS = {
|
|
21
|
+
play: 'play', pause: 'pause', restart: 'restart', lock: 'lock', unlock: 'lock-open',
|
|
22
|
+
mute: 'volume-mute', unmute: 'volume-high', aspectRatio: 'aspect-ratio', videoOnly: 'video',
|
|
23
|
+
audio: 'music-note', minimize: 'arrow-collapse', back: 'arrow-left', recording: 'record-rec', stop: 'stop',
|
|
24
|
+
liveChat: 'comment-text-multiple-outline', epg: 'television-classic', fullscreen: 'fullscreen', close: 'close',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Inline SVG keeps the React DOM/Electron entry independent of React Native,
|
|
28
|
+
// Expo, and native icon-font packages. The native entry uses Expo vector icons.
|
|
29
|
+
const WEB_ICON_PATHS = {
|
|
30
|
+
play: 'M8 5v14l11-7z',
|
|
31
|
+
pause: 'M6 5h4v14H6zm8 0h4v14h-4z',
|
|
32
|
+
restart: 'M12 5V1L7 6l5 5V7a6 6 0 1 1-5.65 8H4.26A9 9 0 1 0 12 5z',
|
|
33
|
+
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
|
+
'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.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',
|
|
36
|
+
'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
|
+
'aspect-ratio': 'M3 5h18v14H3zm2 2v10h14V7zm2 2h4v2H9v4H7zm10 6h-4v-2h2V9h2z',
|
|
38
|
+
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
|
+
'music-note': 'M12 3v12.26A4 4 0 1 0 14 19V7h6V3z',
|
|
40
|
+
'arrow-collapse': 'M4 4h6v2H6v4H4zm10 0h6v6h-2V6h-4zM4 14h2v4h4v2H4zm14 0h2v6h-6v-2h4z',
|
|
41
|
+
'arrow-left': 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z',
|
|
42
|
+
'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
|
+
stop: 'M6 6h12v12H6z',
|
|
44
|
+
'comment-text-multiple-outline': 'M4 4h16v12H7l-3 3zm2 2v8h12V6zm2 2h8v2H8zm0 3h6v2H8z',
|
|
45
|
+
'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
|
+
fullscreen: 'M4 4h6v2H6v4H4zm10 0h6v6h-2V6h-4zM4 14h2v4h4v2H4zm14 0h2v6h-6v-2h4z',
|
|
47
|
+
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',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function getSource(source, url) {
|
|
51
|
+
const value = source ?? url ?? '';
|
|
52
|
+
if (typeof value === 'string') return { uri: value };
|
|
53
|
+
return value && typeof value === 'object' ? value : { uri: '' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatTime(seconds) {
|
|
57
|
+
const value = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
58
|
+
const hours = Math.floor(value / 3600);
|
|
59
|
+
const minutes = Math.floor((value % 3600) / 60);
|
|
60
|
+
const remainder = value % 60;
|
|
61
|
+
return hours
|
|
62
|
+
? `${hours}:${String(minutes).padStart(2, '0')}:${String(remainder).padStart(2, '0')}`
|
|
63
|
+
: `${minutes}:${String(remainder).padStart(2, '0')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Icon({ name, icons, color }) {
|
|
67
|
+
const icon = icons?.[name];
|
|
68
|
+
if (React.isValidElement(icon)) return React.cloneElement(icon, { 'aria-hidden': true });
|
|
69
|
+
if (typeof icon === 'function') return h(icon, { size: 18, color, 'aria-hidden': true });
|
|
70
|
+
if (typeof icon === 'string' && !/^[a-z0-9-]+$/i.test(icon)) {
|
|
71
|
+
return h('span', { className: 'cinecrew-player__icon', style: { color }, 'aria-hidden': true }, icon);
|
|
72
|
+
}
|
|
73
|
+
const iconName = icon || DEFAULT_ICONS[name] || name;
|
|
74
|
+
const path = WEB_ICON_PATHS[iconName] || WEB_ICON_PATHS[name];
|
|
75
|
+
return path
|
|
76
|
+
? h('svg', { className: 'cinecrew-player__icon', viewBox: '0 0 24 24', width: 18, height: 18, fill: 'currentColor', style: { color }, 'aria-hidden': true }, h('path', { d: path }))
|
|
77
|
+
: h('span', { className: 'cinecrew-player__icon', style: { color }, 'aria-hidden': true }, '•');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function PlayerButton({ name, label, icons, theme, onClick, active, disabled, children }) {
|
|
81
|
+
return h('button', {
|
|
82
|
+
type: 'button',
|
|
83
|
+
className: `cinecrew-player__button${active ? ' is-active' : ''}`,
|
|
84
|
+
style: { color: theme.controlColor, background: theme.controlBackground },
|
|
85
|
+
'aria-label': label,
|
|
86
|
+
title: label,
|
|
87
|
+
onClick,
|
|
88
|
+
disabled,
|
|
89
|
+
}, h(Icon, { name, icons, color: active ? theme.accentColor : theme.controlColor }), children);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isControlEnabled(overrides, name, fallback = true) {
|
|
93
|
+
return overrides[name] ?? fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderControlButton({ name, label, callback, options = {}, overrides, icons, theme }) {
|
|
97
|
+
if (!isControlEnabled(overrides, name, options.defaultVisible ?? true)) return null;
|
|
98
|
+
return h(PlayerButton, {
|
|
99
|
+
key: name,
|
|
100
|
+
name: options.icon || name,
|
|
101
|
+
label,
|
|
102
|
+
icons,
|
|
103
|
+
theme,
|
|
104
|
+
onClick: callback,
|
|
105
|
+
active: options.active,
|
|
106
|
+
disabled: options.disabled,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getPanelActionName(panel) {
|
|
111
|
+
if (panel === 'chat') return 'onLiveChatOpen';
|
|
112
|
+
if (panel === 'epg') return 'onEpgOpen';
|
|
113
|
+
return 'onDiagnosticsOpen';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function WebSeekControl({ currentTime, duration, theme, onSeek }) {
|
|
117
|
+
return h('div', { className: 'cinecrew-player__seek' },
|
|
118
|
+
h('span', null, formatTime(currentTime)),
|
|
119
|
+
h('input', {
|
|
120
|
+
type: 'range',
|
|
121
|
+
min: 0,
|
|
122
|
+
max: duration,
|
|
123
|
+
value: Math.min(currentTime, duration),
|
|
124
|
+
onChange: (event) => onSeek(Number(event.target.value)),
|
|
125
|
+
style: { accentColor: theme.accentColor },
|
|
126
|
+
}),
|
|
127
|
+
h('span', null, formatTime(duration)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function WebAspectRatioMenu({ open, theme, icons, onToggle, onSelect }) {
|
|
131
|
+
const ratios = ['FIT', 'FILL', 'STRETCH', '16:9', '4:3', '1:1'];
|
|
132
|
+
let menu = null;
|
|
133
|
+
if (open) {
|
|
134
|
+
menu = h('div', { className: 'cinecrew-player__menu', style: { background: theme.surfaceColor } },
|
|
135
|
+
ratios.map((ratio) => h('button', {
|
|
136
|
+
key: ratio,
|
|
137
|
+
type: 'button',
|
|
138
|
+
onClick: () => onSelect(ratio),
|
|
139
|
+
}, ratio)));
|
|
140
|
+
}
|
|
141
|
+
return h('div', { className: 'cinecrew-player__menu-wrap', key: 'aspectRatio' },
|
|
142
|
+
h(PlayerButton, { name: 'aspectRatio', label: 'Aspect ratio', icons, theme, onClick: onToggle, active: open }),
|
|
143
|
+
menu);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function WebAudioTrackMenu({ open, tracks, theme, icons, onToggle, onSelect }) {
|
|
147
|
+
let menu = null;
|
|
148
|
+
if (open) {
|
|
149
|
+
menu = h('div', { className: 'cinecrew-player__menu', style: { background: theme.surfaceColor } },
|
|
150
|
+
tracks.map((track) => h('button', {
|
|
151
|
+
key: track.id,
|
|
152
|
+
type: 'button',
|
|
153
|
+
onClick: () => onSelect(track.id),
|
|
154
|
+
}, track.name)));
|
|
155
|
+
}
|
|
156
|
+
return h('div', { className: 'cinecrew-player__menu-wrap', key: 'audioTracks' },
|
|
157
|
+
h(PlayerButton, { name: 'audio', label: 'Audio tracks', icons, theme, onClick: onToggle, active: open }),
|
|
158
|
+
menu);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function WebPlaybackRateControl({ value, onChange }) {
|
|
162
|
+
const rates = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
163
|
+
return h('select', {
|
|
164
|
+
className: 'cinecrew-player__rate',
|
|
165
|
+
'aria-label': 'Playback speed',
|
|
166
|
+
value,
|
|
167
|
+
onChange: (event) => onChange(Number(event.target.value)),
|
|
168
|
+
}, rates.map((rate) => h('option', { key: rate, value: rate }, `${rate}x`)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function WebBottomControls(props) {
|
|
172
|
+
const {
|
|
173
|
+
isLive, overrides, theme, icons, currentTime, duration, seekTo, muted,
|
|
174
|
+
toggleMute, showAspectMenu, setShowAspectMenu, selectAspect, videoOnly,
|
|
175
|
+
setVideoOnlyMode, audioOnly, setAudioOnlyMode, availableTracks,
|
|
176
|
+
showAudioMenu, setShowAudioMenu, selectAudio, playbackRate,
|
|
177
|
+
setPlaybackRateAction, fullscreen, toggleFullscreen,
|
|
178
|
+
} = props;
|
|
179
|
+
let seek = null;
|
|
180
|
+
if (duration > 0 && isControlEnabled(overrides, 'seek', true)) {
|
|
181
|
+
seek = h(WebSeekControl, { currentTime, duration, theme, onSeek: seekTo });
|
|
182
|
+
}
|
|
183
|
+
let aspect = null;
|
|
184
|
+
if (isControlEnabled(overrides, 'aspectRatio', true)) {
|
|
185
|
+
aspect = h(WebAspectRatioMenu, {
|
|
186
|
+
open: showAspectMenu,
|
|
187
|
+
theme,
|
|
188
|
+
icons,
|
|
189
|
+
onToggle: () => setShowAspectMenu((value) => !value),
|
|
190
|
+
onSelect: (ratio) => {
|
|
191
|
+
selectAspect(ratio);
|
|
192
|
+
setShowAspectMenu(false);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
let audioTracks = null;
|
|
197
|
+
if (availableTracks.length > 1 && isControlEnabled(overrides, 'audioTracks', true)) {
|
|
198
|
+
audioTracks = h(WebAudioTrackMenu, {
|
|
199
|
+
open: showAudioMenu,
|
|
200
|
+
tracks: availableTracks,
|
|
201
|
+
theme,
|
|
202
|
+
icons,
|
|
203
|
+
onToggle: () => setShowAudioMenu((value) => !value),
|
|
204
|
+
onSelect: (id) => {
|
|
205
|
+
selectAudio(id);
|
|
206
|
+
setShowAudioMenu(false);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
let playbackRateControl = null;
|
|
211
|
+
if (isControlEnabled(overrides, 'playbackRate', true) && !isLive) {
|
|
212
|
+
playbackRateControl = h(WebPlaybackRateControl, { value: playbackRate, onChange: setPlaybackRateAction });
|
|
213
|
+
}
|
|
214
|
+
const muteLabel = muted ? 'Unmute' : 'Mute';
|
|
215
|
+
const muteIcon = muted ? 'mute' : 'unmute';
|
|
216
|
+
const videoOnlyLabel = videoOnly ? 'Enable audio' : 'Video only';
|
|
217
|
+
const audioOnlyLabel = audioOnly ? 'Switch to audio card' : 'Audio only';
|
|
218
|
+
const fullscreenLabel = fullscreen ? 'Exit full screen' : 'Full screen';
|
|
219
|
+
|
|
220
|
+
return h('div', { className: 'cinecrew-player__bottom-controls' },
|
|
221
|
+
seek,
|
|
222
|
+
renderControlButton({ name: 'mute', label: muteLabel, callback: toggleMute, options: { icon: muteIcon }, overrides, icons, theme }),
|
|
223
|
+
aspect,
|
|
224
|
+
renderControlButton({ name: 'videoOnly', label: videoOnlyLabel, callback: () => setVideoOnlyMode(!videoOnly), options: { active: videoOnly, defaultVisible: false }, overrides, icons, theme }),
|
|
225
|
+
renderControlButton({ name: 'audioOnly', label: audioOnlyLabel, callback: () => setAudioOnlyMode(true), options: { icon: 'audio', active: audioOnly }, overrides, icons, theme }),
|
|
226
|
+
audioTracks,
|
|
227
|
+
playbackRateControl,
|
|
228
|
+
renderControlButton({ name: 'fullscreen', label: fullscreenLabel, callback: toggleFullscreen, options: { icon: 'fullscreen' }, overrides, icons, theme }));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function WebPlayerControls({ locked, overrides, theme, icons, unlockedControls, toggleLock, paused, togglePlay, bottomProps }) {
|
|
232
|
+
let topControls = unlockedControls;
|
|
233
|
+
if (locked) {
|
|
234
|
+
topControls = renderControlButton({
|
|
235
|
+
name: 'lock', label: 'Unlock controls', callback: toggleLock,
|
|
236
|
+
options: { active: true, defaultVisible: true }, overrides, icons, theme,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
let centerControls = null;
|
|
240
|
+
let bottomControls = null;
|
|
241
|
+
if (!locked) {
|
|
242
|
+
const playLabel = paused ? 'Play' : 'Pause';
|
|
243
|
+
const playIcon = paused ? 'play' : 'pause';
|
|
244
|
+
centerControls = h('div', { className: 'cinecrew-player__center-controls' },
|
|
245
|
+
renderControlButton({ name: 'playPause', label: playLabel, callback: togglePlay, options: { icon: playIcon }, overrides, icons, theme }));
|
|
246
|
+
bottomControls = h(WebBottomControls, bottomProps);
|
|
247
|
+
}
|
|
248
|
+
return h('div', { className: 'cinecrew-player__controls', style: { color: theme.controlColor } },
|
|
249
|
+
h('div', { className: 'cinecrew-player__top-controls' }, topControls),
|
|
250
|
+
centerControls,
|
|
251
|
+
bottomControls);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function WebPlayerSurface({
|
|
255
|
+
youtubeVideoId,
|
|
256
|
+
streamUrl,
|
|
257
|
+
videoRef,
|
|
258
|
+
youtubeRef,
|
|
259
|
+
directVideoSource,
|
|
260
|
+
poster,
|
|
261
|
+
autoPlay,
|
|
262
|
+
paused,
|
|
263
|
+
muted,
|
|
264
|
+
volume,
|
|
265
|
+
playbackRate,
|
|
266
|
+
videoOnly,
|
|
267
|
+
videoStyle,
|
|
268
|
+
audioOnly,
|
|
269
|
+
inlinePreview,
|
|
270
|
+
onPromotePreview,
|
|
271
|
+
bufferingRef,
|
|
272
|
+
onReady,
|
|
273
|
+
onProgress,
|
|
274
|
+
onPlaying,
|
|
275
|
+
onEnded,
|
|
276
|
+
onStateChange,
|
|
277
|
+
onError,
|
|
278
|
+
handleError,
|
|
279
|
+
}) {
|
|
280
|
+
if (youtubeVideoId) {
|
|
281
|
+
return h(YouTubeVideoPlayer, {
|
|
282
|
+
ref: youtubeRef,
|
|
283
|
+
videoId: youtubeVideoId,
|
|
284
|
+
paused,
|
|
285
|
+
muted: muted || videoOnly,
|
|
286
|
+
volume,
|
|
287
|
+
playbackRate,
|
|
288
|
+
onReady,
|
|
289
|
+
onProgress,
|
|
290
|
+
onPlaying,
|
|
291
|
+
onBuffering: bufferingRef.current,
|
|
292
|
+
onStateChange,
|
|
293
|
+
onError,
|
|
294
|
+
onEnded,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (!streamUrl) return h('div', { className: 'cinecrew-player__empty' });
|
|
298
|
+
return h('video', {
|
|
299
|
+
ref: videoRef,
|
|
300
|
+
className: 'cinecrew-player__video',
|
|
301
|
+
src: directVideoSource,
|
|
302
|
+
poster,
|
|
303
|
+
autoPlay,
|
|
304
|
+
muted: muted || videoOnly,
|
|
305
|
+
playsInline: true,
|
|
306
|
+
preload: 'auto',
|
|
307
|
+
style: { ...videoStyle, opacity: audioOnly ? 0 : 1 },
|
|
308
|
+
onClick: inlinePreview ? onPromotePreview : undefined,
|
|
309
|
+
onError: (event) => {
|
|
310
|
+
const mediaError = event.currentTarget?.error;
|
|
311
|
+
handleError({ message: mediaError?.message || 'The browser could not load this stream. Check URL, codec and CORS support.', code: mediaError?.code, cause: mediaError });
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function WebAudioOnlyCard({ poster, title, theme, onSwitchToVideo }) {
|
|
317
|
+
return h('div', {
|
|
318
|
+
className: 'cinecrew-player__audio-card',
|
|
319
|
+
style: { background: theme.surfaceColor, color: theme.controlColor, borderColor: theme.accentColor },
|
|
320
|
+
},
|
|
321
|
+
poster ? h('img', { className: 'cinecrew-player__audio-poster', src: poster, alt: '' }) : null,
|
|
322
|
+
h('span', { className: 'cinecrew-player__audio-wave', style: { color: theme.accentColor }, 'aria-hidden': true }, '•••••••'),
|
|
323
|
+
h('strong', null, title || 'Audio only'),
|
|
324
|
+
h('button', {
|
|
325
|
+
type: 'button',
|
|
326
|
+
onClick: onSwitchToVideo,
|
|
327
|
+
style: { color: theme.accentColor, borderColor: theme.accentColor },
|
|
328
|
+
}, 'Switch to video'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function WebPlayerError({ error, theme, renderBackButton }) {
|
|
332
|
+
if (!error) return null;
|
|
333
|
+
return h('div', { className: 'cinecrew-player__error', style: { color: theme.controlColor, background: theme.surfaceColor } },
|
|
334
|
+
h('strong', { style: { color: theme.errorColor } }, 'Playback error'),
|
|
335
|
+
h('span', null, error),
|
|
336
|
+
renderBackButton());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function getDirectVideoSource({ mpegTs, useHls, sourceType, activeUrl }) {
|
|
340
|
+
if (mpegTs || useHls || /mpegurl|mpeg-ts/.test(sourceType)) return undefined;
|
|
341
|
+
return activeUrl;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isAudioPlaybackEnabled(muted, videoOnly) {
|
|
345
|
+
return !muted && !videoOnly;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getPanelIntegration(activePanel, integrations) {
|
|
349
|
+
return activePanel === 'chat' ? integrations.liveChat : integrations.epg;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getActiveMediaUrl(youtubeVideoId, streamUrl) {
|
|
353
|
+
return youtubeVideoId ? '' : streamUrl;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getInlinePlayerStyle(inlinePreview, rect) {
|
|
357
|
+
if (!inlinePreview || !rect) return {};
|
|
358
|
+
return { position: 'absolute', left: rect.x, top: rect.y, width: rect.width, height: rect.height };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function buildUnlockedControls({
|
|
362
|
+
control, actions, onBack, isLive, restart, toggleLock, hasRecording,
|
|
363
|
+
recording, integrations, action, videoRef, streamUrl, title, activePanel,
|
|
364
|
+
hasChat, hasEpg, handleOpenPanel, handleMinimize, setRecording, backVisible, minimizeVisible,
|
|
365
|
+
}) {
|
|
366
|
+
const controls = [
|
|
367
|
+
control('back', 'Back', onBack, { defaultVisible: backVisible }),
|
|
368
|
+
...(!isLive ? [control('restart', 'Restart', restart)] : []),
|
|
369
|
+
control('lock', 'Lock controls', toggleLock, { active: false, defaultVisible: true }),
|
|
370
|
+
];
|
|
371
|
+
let recordingControl = null;
|
|
372
|
+
if (hasRecording) {
|
|
373
|
+
const recordingLabel = recording ? 'Stop recording' : 'Start recording';
|
|
374
|
+
const recordingAction = recording ? 'onRecordingStop' : 'onRecordingStart';
|
|
375
|
+
const handleRecordingToggle = async () => {
|
|
376
|
+
const handler = recording ? integrations.recording?.stop : integrations.recording?.start;
|
|
377
|
+
const fallback = handler ? () => handler({ getVideoElement: () => videoRef.current, streamUrl, title }) : undefined;
|
|
378
|
+
await action(recordingAction, fallback, { source: streamUrl, title });
|
|
379
|
+
setRecording((value) => !value);
|
|
380
|
+
};
|
|
381
|
+
recordingControl = control('recording', recordingLabel, handleRecordingToggle, { active: recording });
|
|
382
|
+
}
|
|
383
|
+
const chatLabel = activePanel === 'chat' ? 'Close live chat' : 'Live chat';
|
|
384
|
+
const epgLabel = activePanel === 'epg' ? 'Close programme guide' : 'Programme guide';
|
|
385
|
+
return [
|
|
386
|
+
...controls,
|
|
387
|
+
recordingControl,
|
|
388
|
+
control('liveChat', chatLabel, () => handleOpenPanel('chat'), { active: activePanel === 'chat', defaultVisible: hasChat }),
|
|
389
|
+
control('epg', epgLabel, () => handleOpenPanel('epg'), { active: activePanel === 'epg', defaultVisible: hasEpg }),
|
|
390
|
+
control('minimize', 'Minimize player', handleMinimize, { defaultVisible: minimizeVisible }),
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function WebPlayerLayout(props) {
|
|
395
|
+
const controlLayer = !props.error && !props.audioOnly
|
|
396
|
+
? h(WebPlayerControls, {
|
|
397
|
+
locked: props.locked,
|
|
398
|
+
overrides: props.controlOverrides,
|
|
399
|
+
theme: props.theme,
|
|
400
|
+
icons: props.icons,
|
|
401
|
+
unlockedControls: props.unlockedControls,
|
|
402
|
+
toggleLock: props.toggleLock,
|
|
403
|
+
paused: props.isPaused,
|
|
404
|
+
togglePlay: props.togglePlay,
|
|
405
|
+
bottomProps: props.bottomControlProps,
|
|
406
|
+
})
|
|
407
|
+
: null;
|
|
408
|
+
const playerTitle = props.title && !props.audioOnly
|
|
409
|
+
? h('div', { className: 'cinecrew-player__title', style: { color: props.theme.controlColor } }, props.title)
|
|
410
|
+
: null;
|
|
411
|
+
const loadingNotice = props.buffering && !props.error
|
|
412
|
+
? h('div', { className: 'cinecrew-player__status', style: { color: props.theme.controlColor } },
|
|
413
|
+
h('span', { className: 'cinecrew-player__spinner', style: { borderTopColor: props.theme.accentColor } }), 'Loading stream…')
|
|
414
|
+
: null;
|
|
415
|
+
const audioCard = props.audioOnly
|
|
416
|
+
? h(WebAudioOnlyCard, { poster: props.poster, title: props.title, theme: props.theme, onSwitchToVideo: props.onSwitchToVideo })
|
|
417
|
+
: null;
|
|
418
|
+
const panelNode = props.activePanel && props.webPanel
|
|
419
|
+
? h('div', { className: 'cinecrew-player__panel', style: { background: props.theme.surfaceColor, color: props.theme.controlColor } }, props.webPanel)
|
|
420
|
+
: null;
|
|
421
|
+
return h('div', {
|
|
422
|
+
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 },
|
|
425
|
+
onWheel: props.onWheel,
|
|
426
|
+
'data-stream-mode': props.streamMode,
|
|
427
|
+
},
|
|
428
|
+
props.mediaSurface,
|
|
429
|
+
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
|
+
playerTitle,
|
|
431
|
+
loadingNotice,
|
|
432
|
+
h(WebPlayerError, { error: props.error, theme: props.theme, renderBackButton: props.renderBackButton }),
|
|
433
|
+
controlLayer,
|
|
434
|
+
audioCard,
|
|
435
|
+
panelNode);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getStreamMode(youtubeVideoId, mpegTs, useHls) {
|
|
439
|
+
if (youtubeVideoId) return 'youtube';
|
|
440
|
+
if (mpegTs) return 'mpegts';
|
|
441
|
+
if (useHls) return 'hls';
|
|
442
|
+
return 'native';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function normalizeTracks(video, suppliedTracks) {
|
|
446
|
+
if (Array.isArray(suppliedTracks) && suppliedTracks.length) return suppliedTracks;
|
|
447
|
+
const tracks = video?.audioTracks;
|
|
448
|
+
if (!tracks) return [];
|
|
449
|
+
return Array.from(tracks).map((track, index) => ({
|
|
450
|
+
id: track.id ?? index,
|
|
451
|
+
name: track.label || track.language || `Track ${index + 1}`,
|
|
452
|
+
nativeTrack: track,
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function responseRows(value, keys) {
|
|
457
|
+
if (Array.isArray(value)) return value;
|
|
458
|
+
for (const key of keys) if (Array.isArray(value?.[key])) return value[key];
|
|
459
|
+
if (Array.isArray(value?.data)) return value.data;
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatListingTime(value) {
|
|
464
|
+
const date = new Date(Number(value) || value);
|
|
465
|
+
return Number.isNaN(date.getTime())
|
|
466
|
+
? ''
|
|
467
|
+
: date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getPanelRenderer(activePanel, renderLiveChat, renderEpg, integrations) {
|
|
471
|
+
if (activePanel === 'chat') return renderLiveChat || integrations.liveChat?.render;
|
|
472
|
+
if (activePanel === 'epg') return renderEpg || integrations.epg?.render;
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function createWebPanelNode({ renderer, integration, activePanel, integrations, source, title, theme, onClose }) {
|
|
477
|
+
if (renderer) return h(renderer, { title, source, onClose });
|
|
478
|
+
if (!integration) return null;
|
|
479
|
+
const hasLoader = activePanel === 'chat'
|
|
480
|
+
? typeof integration.loadMessages === 'function'
|
|
481
|
+
: typeof integration.loadListings === 'function';
|
|
482
|
+
if (!hasLoader) return null;
|
|
483
|
+
return h(WebIntegrationPanel, {
|
|
484
|
+
kind: activePanel,
|
|
485
|
+
integration,
|
|
486
|
+
integrations,
|
|
487
|
+
source,
|
|
488
|
+
title,
|
|
489
|
+
theme,
|
|
490
|
+
onClose,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function WebIntegrationPanel({ kind, integration, integrations, source, title, theme, onClose }) {
|
|
495
|
+
const [rows, setRows] = useState([]);
|
|
496
|
+
const [user, setUser] = useState(integrations.user || null);
|
|
497
|
+
const [message, setMessage] = useState('');
|
|
498
|
+
const [loading, setLoading] = useState(true);
|
|
499
|
+
const [sending, setSending] = useState(false);
|
|
500
|
+
const [error, setError] = useState('');
|
|
501
|
+
const channelId = String(source.streamId || source.mediaId || source.id || '');
|
|
502
|
+
const isChat = kind === 'chat';
|
|
503
|
+
const emptyStateMessage = isChat ? 'No messages yet.' : 'No programme information available.';
|
|
504
|
+
const sendButtonLabel = sending ? 'Sending…' : 'Send';
|
|
505
|
+
|
|
506
|
+
const load = useCallback(async () => {
|
|
507
|
+
if (!channelId) {
|
|
508
|
+
setError('A channel ID is required to load this panel.');
|
|
509
|
+
setLoading(false);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
setError('');
|
|
514
|
+
const value = isChat
|
|
515
|
+
? await integration.loadMessages({ channelId, limit: 50 })
|
|
516
|
+
: await integration.loadListings({ channelId, limit: integration.limit || 48 });
|
|
517
|
+
setRows(responseRows(value, isChat ? ['messages', 'items', 'comments'] : ['listings', 'programmes', 'epg', 'items']));
|
|
518
|
+
} catch (loadError) {
|
|
519
|
+
setError(loadError?.message || `Could not load ${isChat ? 'live chat' : 'the programme guide'}.`);
|
|
520
|
+
} finally {
|
|
521
|
+
setLoading(false);
|
|
522
|
+
}
|
|
523
|
+
}, [channelId, integration, isChat]);
|
|
524
|
+
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
let cancelled = false;
|
|
527
|
+
if (isChat && !user && integrations.getUser) {
|
|
528
|
+
Promise.resolve(integrations.getUser()).then((value) => {
|
|
529
|
+
if (!cancelled) setUser(value || null);
|
|
530
|
+
}).catch(() => {});
|
|
531
|
+
}
|
|
532
|
+
return () => { cancelled = true; };
|
|
533
|
+
}, [isChat, user, integrations]);
|
|
534
|
+
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
setLoading(true);
|
|
537
|
+
load();
|
|
538
|
+
if (!isChat) return undefined;
|
|
539
|
+
const timer = setInterval(load, Math.max(1000, Number(integration.pollIntervalMs) || 5000));
|
|
540
|
+
return () => clearInterval(timer);
|
|
541
|
+
}, [load, isChat, integration.pollIntervalMs]);
|
|
542
|
+
|
|
543
|
+
const send = async (event) => {
|
|
544
|
+
event.preventDefault();
|
|
545
|
+
const comment = message.trim();
|
|
546
|
+
if (!comment || sending || !integration.sendMessage) return;
|
|
547
|
+
setSending(true);
|
|
548
|
+
try {
|
|
549
|
+
await integration.sendMessage({
|
|
550
|
+
channelId,
|
|
551
|
+
userId: user?.id,
|
|
552
|
+
username: user?.username || 'Viewer',
|
|
553
|
+
comment,
|
|
554
|
+
});
|
|
555
|
+
setMessage('');
|
|
556
|
+
await load();
|
|
557
|
+
} catch (sendError) {
|
|
558
|
+
setError(sendError?.message || 'Could not send your message.');
|
|
559
|
+
} finally {
|
|
560
|
+
setSending(false);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
return h('section', {
|
|
565
|
+
className: 'cinecrew-player__panel-content',
|
|
566
|
+
style: { color: theme.controlColor },
|
|
567
|
+
'aria-label': isChat ? 'Live chat' : 'Programme guide',
|
|
568
|
+
},
|
|
569
|
+
h('header', { className: 'cinecrew-player__panel-heading' },
|
|
570
|
+
h('strong', null, isChat ? 'Live chat' : `${title || 'Channel'} · EPG`),
|
|
571
|
+
h('button', { type: 'button', onClick: onClose, 'aria-label': 'Close panel' }, '×')),
|
|
572
|
+
loading ? h('div', { className: 'cinecrew-player__panel-state' }, 'Loading…') : null,
|
|
573
|
+
error ? h('div', { className: 'cinecrew-player__panel-state is-error', role: 'status' }, error) : null,
|
|
574
|
+
!loading && !error && rows.length === 0 ? h('div', { className: 'cinecrew-player__panel-state' }, emptyStateMessage) : null,
|
|
575
|
+
h('div', { className: 'cinecrew-player__panel-list' }, rows.map((row, index) => {
|
|
576
|
+
const key = row.id ?? row.messageId ?? row.startMs ?? index;
|
|
577
|
+
if (isChat) {
|
|
578
|
+
return h('article', { key, className: 'cinecrew-player__chat-message' },
|
|
579
|
+
h('strong', null, row.username || row.userName || row.name || 'Viewer'),
|
|
580
|
+
h('span', null, row.comment || row.message || row.text || ''));
|
|
581
|
+
}
|
|
582
|
+
const start = row.startMs ?? row.start ?? row.startTime;
|
|
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);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* URL-first browser player. It does not resolve provider URLs or proxy streams;
|
|
602
|
+
* pass a browser-playable URL and provide UI adapters for optional services.
|
|
603
|
+
*/
|
|
604
|
+
export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
605
|
+
const {
|
|
606
|
+
source,
|
|
607
|
+
url,
|
|
608
|
+
title = '',
|
|
609
|
+
poster,
|
|
610
|
+
isLive: liveProp,
|
|
611
|
+
autoPlay = true,
|
|
612
|
+
muted: mutedProp = false,
|
|
613
|
+
volume: volumeProp = 1,
|
|
614
|
+
playbackRate: playbackRateProp = 1,
|
|
615
|
+
paused: pausedProp,
|
|
616
|
+
controls: controlOverrides = {},
|
|
617
|
+
actions = {},
|
|
618
|
+
theme: themeProp = {},
|
|
619
|
+
icons = {},
|
|
620
|
+
style,
|
|
621
|
+
className = '',
|
|
622
|
+
resolveSource,
|
|
623
|
+
videoOnly: videoOnlyProp = false,
|
|
624
|
+
audioOnly: audioOnlyProp = false,
|
|
625
|
+
audioTracks: tracksProp,
|
|
626
|
+
selectedAudioTrack,
|
|
627
|
+
renderLiveChat,
|
|
628
|
+
renderEpg,
|
|
629
|
+
integrations = {},
|
|
630
|
+
onPlayerHostRef,
|
|
631
|
+
onInlinePreviewWheel,
|
|
632
|
+
inlinePreview = false,
|
|
633
|
+
inlinePreviewRect,
|
|
634
|
+
onPromotePreview,
|
|
635
|
+
initialShowLiveChat = false,
|
|
636
|
+
liveChatNonce = 0,
|
|
637
|
+
onMinimize,
|
|
638
|
+
onBack,
|
|
639
|
+
mediaId,
|
|
640
|
+
onFullscreen,
|
|
641
|
+
onReady,
|
|
642
|
+
onProgress,
|
|
643
|
+
onPlaying,
|
|
644
|
+
onBuffering,
|
|
645
|
+
onError,
|
|
646
|
+
onEnded,
|
|
647
|
+
onPlaybackRoute,
|
|
648
|
+
} = props;
|
|
649
|
+
const resolution = useResolvedPlayerSource(source, url, resolveSource, getWebRuntimePlatform());
|
|
650
|
+
const media = resolution.source || {};
|
|
651
|
+
const streamUrl = String(media.uri || media.url || '');
|
|
652
|
+
const youtubeVideoId = getYouTubeVideoId(media);
|
|
653
|
+
const isLive = liveProp ?? media.isLive ?? false;
|
|
654
|
+
const theme = { ...DEFAULT_THEME, ...themeProp };
|
|
655
|
+
const videoRef = useRef(null);
|
|
656
|
+
const youtubeRef = useRef(null);
|
|
657
|
+
const playerRef = useRef(null);
|
|
658
|
+
const publicPlayerRef = useRef(null);
|
|
659
|
+
const pausedRef = useRef(pausedProp ?? !autoPlay);
|
|
660
|
+
const errorRef = useRef(onError);
|
|
661
|
+
const [isPaused, setIsPaused] = useState(pausedProp ?? !autoPlay);
|
|
662
|
+
const [muted, setMuted] = useState(!!mutedProp);
|
|
663
|
+
const [volume, setVolume] = useState(Math.max(0, Math.min(1, Number(volumeProp) || 0)));
|
|
664
|
+
const [videoOnly, setVideoOnly] = useState(!!videoOnlyProp);
|
|
665
|
+
const [audioOnly, setAudioOnly] = useState(!!audioOnlyProp);
|
|
666
|
+
const [aspectRatio, setAspectRatio] = useState('FIT');
|
|
667
|
+
const [locked, setLocked] = useState(false);
|
|
668
|
+
const [buffering, setBuffering] = useState(Boolean(streamUrl) || resolution.loading);
|
|
669
|
+
const [error, setError] = useState('');
|
|
670
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
671
|
+
const [duration, setDuration] = useState(0);
|
|
672
|
+
const [showAspectMenu, setShowAspectMenu] = useState(false);
|
|
673
|
+
const [showAudioMenu, setShowAudioMenu] = useState(false);
|
|
674
|
+
const [activePanel, setActivePanel] = useState(null);
|
|
675
|
+
const [recording, setRecording] = useState(false);
|
|
676
|
+
const [availableTracks, setAvailableTracks] = useState([]);
|
|
677
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
678
|
+
const [playbackRate, setPlaybackRate] = useState(Number(playbackRateProp) || 1);
|
|
679
|
+
const { videoStyle } = useWebVideoAspectRatio(aspectRatio, false);
|
|
680
|
+
const pausedStateRef = pausedRef;
|
|
681
|
+
pausedRef.current = isPaused;
|
|
682
|
+
errorRef.current = onError;
|
|
683
|
+
|
|
684
|
+
const handleError = useCallback((detail) => {
|
|
685
|
+
const message = typeof detail === 'string' ? detail : detail?.message || 'Unable to play this media source.';
|
|
686
|
+
setError(message);
|
|
687
|
+
setBuffering(false);
|
|
688
|
+
errorRef.current?.(detail instanceof Error ? detail : { ...detail, message });
|
|
689
|
+
}, []);
|
|
690
|
+
const onErrorRef = useRef(handleError);
|
|
691
|
+
onErrorRef.current = handleError;
|
|
692
|
+
const onBufferingRef = useRef((next) => {
|
|
693
|
+
setBuffering(!!next);
|
|
694
|
+
onBuffering?.(!!next);
|
|
695
|
+
});
|
|
696
|
+
onBufferingRef.current = (next) => {
|
|
697
|
+
setBuffering(!!next);
|
|
698
|
+
onBuffering?.(!!next);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
setError('');
|
|
703
|
+
setBuffering(Boolean(streamUrl) || resolution.loading);
|
|
704
|
+
setCurrentTime(0);
|
|
705
|
+
setDuration(0);
|
|
706
|
+
setAvailableTracks([]);
|
|
707
|
+
if (resolution.error) {
|
|
708
|
+
handleError(resolution.error);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (!streamUrl) {
|
|
712
|
+
setBuffering(Boolean(resolution.loading));
|
|
713
|
+
}
|
|
714
|
+
}, [streamUrl, resolution.loading, resolution.error, handleError]);
|
|
715
|
+
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
if (pausedProp !== undefined) setIsPaused(!!pausedProp);
|
|
718
|
+
}, [pausedProp]);
|
|
719
|
+
|
|
720
|
+
useEffect(() => setAudioOnly(!!audioOnlyProp), [audioOnlyProp]);
|
|
721
|
+
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
setMuted(!!mutedProp);
|
|
724
|
+
}, [mutedProp]);
|
|
725
|
+
|
|
726
|
+
useEffect(() => setPlaybackRate(Number(playbackRateProp) || 1), [playbackRateProp]);
|
|
727
|
+
useEffect(() => setVolume(Math.max(0, Math.min(1, Number(volumeProp) || 0))), [volumeProp]);
|
|
728
|
+
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
onPlayerHostRef?.(playerRef.current);
|
|
731
|
+
return () => onPlayerHostRef?.(null);
|
|
732
|
+
}, [onPlayerHostRef]);
|
|
733
|
+
|
|
734
|
+
useEffect(() => {
|
|
735
|
+
if (initialShowLiveChat) setActivePanel('chat');
|
|
736
|
+
}, [initialShowLiveChat]);
|
|
737
|
+
|
|
738
|
+
useEffect(() => {
|
|
739
|
+
if (liveChatNonce) setActivePanel('chat');
|
|
740
|
+
}, [liveChatNonce]);
|
|
741
|
+
|
|
742
|
+
const activeUrl = getActiveMediaUrl(youtubeVideoId, streamUrl);
|
|
743
|
+
|
|
744
|
+
const mpegTs = useWebMpegTsPlayback({
|
|
745
|
+
streamUrl,
|
|
746
|
+
activeUrl,
|
|
747
|
+
isLive,
|
|
748
|
+
videoOnly,
|
|
749
|
+
videoRef,
|
|
750
|
+
pausedRef: pausedStateRef,
|
|
751
|
+
onErrorRef,
|
|
752
|
+
onBufferingRef,
|
|
753
|
+
});
|
|
754
|
+
const useHls = useWebHlsPlayback({
|
|
755
|
+
activeUrl,
|
|
756
|
+
isLive,
|
|
757
|
+
videoRef,
|
|
758
|
+
pausedRef: pausedStateRef,
|
|
759
|
+
onErrorRef,
|
|
760
|
+
});
|
|
761
|
+
useWebAc3AudioPlayback({
|
|
762
|
+
active: mpegTs.useAc3Fallback,
|
|
763
|
+
streamUrl,
|
|
764
|
+
enabled: isAudioPlaybackEnabled(muted, videoOnly),
|
|
765
|
+
paused: isPaused,
|
|
766
|
+
volume: volume * 100,
|
|
767
|
+
videoRef,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
useEffect(() => {
|
|
771
|
+
const video = videoRef.current;
|
|
772
|
+
if (!video) return;
|
|
773
|
+
if (isPaused) video.pause();
|
|
774
|
+
else video.play().catch((playError) => {
|
|
775
|
+
if (playError?.name !== 'NotAllowedError') handleError(playError);
|
|
776
|
+
});
|
|
777
|
+
}, [isPaused, streamUrl, youtubeVideoId, handleError]);
|
|
778
|
+
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
const video = videoRef.current;
|
|
781
|
+
if (!video) return;
|
|
782
|
+
video.muted = muted || videoOnly;
|
|
783
|
+
video.volume = volume;
|
|
784
|
+
video.playbackRate = Number(playbackRate) || 1;
|
|
785
|
+
}, [muted, volume, playbackRate, videoOnly, streamUrl, youtubeVideoId]);
|
|
786
|
+
|
|
787
|
+
useEffect(() => {
|
|
788
|
+
if (!streamUrl) return undefined;
|
|
789
|
+
onPlaybackRoute?.(streamUrl);
|
|
790
|
+
return undefined;
|
|
791
|
+
}, [streamUrl, onPlaybackRoute]);
|
|
792
|
+
|
|
793
|
+
const setPaused = useCallback((next) => {
|
|
794
|
+
const value = typeof next === 'boolean' ? next : !pausedRef.current;
|
|
795
|
+
pausedRef.current = value;
|
|
796
|
+
setIsPaused(value);
|
|
797
|
+
}, []);
|
|
798
|
+
|
|
799
|
+
const action = useCallback((name, fallback, payload) => {
|
|
800
|
+
const callback = actions?.[name];
|
|
801
|
+
if (typeof callback === 'function') return callback(payload, { video: videoRef.current, player: publicPlayerRef.current });
|
|
802
|
+
return fallback?.(payload);
|
|
803
|
+
}, [actions]);
|
|
804
|
+
|
|
805
|
+
const togglePlay = useCallback(() => action('onPlayPause', () => setPaused(), { isPlaying: !pausedRef.current }), [action, setPaused]);
|
|
806
|
+
const restart = useCallback(() => action('onRestart', () => {
|
|
807
|
+
const video = videoRef.current;
|
|
808
|
+
if (video) video.currentTime = 0;
|
|
809
|
+
else youtubeRef.current?.seekTo?.(0);
|
|
810
|
+
setPaused(false);
|
|
811
|
+
}, { currentTime: Number(videoRef.current?.currentTime) || 0 }), [action, setPaused]);
|
|
812
|
+
const toggleMute = useCallback(() => action('onMute', () => setMuted((value) => !value), { muted: !muted }), [action, muted]);
|
|
813
|
+
const toggleLock = useCallback(() => action('onLock', () => setLocked((value) => !value), { locked: !locked }), [action, locked]);
|
|
814
|
+
const selectAspect = useCallback((next) => action('onAspectRatioChange', () => setAspectRatio(next), { aspectRatio: next }), [action]);
|
|
815
|
+
const selectAudio = useCallback((id) => action('onAudioTrackChange', () => {
|
|
816
|
+
const track = availableTracks.find((item) => String(item.id) === String(id));
|
|
817
|
+
if (track?.nativeTrack) {
|
|
818
|
+
for (const item of availableTracks) item.nativeTrack.enabled = String(item.id) === String(id);
|
|
819
|
+
}
|
|
820
|
+
}, { trackId: id }), [action, availableTracks]);
|
|
821
|
+
const setVideoOnlyMode = useCallback((next) => action('onVideoOnlyChange', () => setVideoOnly(next), { enabled: next }), [action]);
|
|
822
|
+
const setAudioOnlyMode = useCallback((next) => action('onAudioOnlyChange', () => setAudioOnly(next), { enabled: next }), [action]);
|
|
823
|
+
const setPlaybackRateAction = useCallback((next) => action('onPlaybackRateChange', () => setPlaybackRate(next), { playbackRate: next }), [action]);
|
|
824
|
+
const seekTo = useCallback((seconds) => action('onSeek', () => {
|
|
825
|
+
if (videoRef.current) videoRef.current.currentTime = Math.max(0, Number(seconds) || 0);
|
|
826
|
+
else youtubeRef.current?.seekTo?.(Math.max(0, Number(seconds) || 0));
|
|
827
|
+
}, { seconds: Number(seconds) || 0 }), [action]);
|
|
828
|
+
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
|
+
useImperativeHandle(ref, () => {
|
|
831
|
+
const api = {
|
|
832
|
+
play: () => setPaused(false),
|
|
833
|
+
pause: () => setPaused(true),
|
|
834
|
+
togglePlayPause: togglePlay,
|
|
835
|
+
restart,
|
|
836
|
+
toggleMute,
|
|
837
|
+
mute: () => setMuted(true),
|
|
838
|
+
unmute: () => setMuted(false),
|
|
839
|
+
setMuted,
|
|
840
|
+
setAspectRatio,
|
|
841
|
+
setAudioTrack: (id) => {
|
|
842
|
+
for (const item of availableTracks) if (item.nativeTrack) item.nativeTrack.enabled = String(item.id) === String(id);
|
|
843
|
+
},
|
|
844
|
+
setAudioOnly: setAudioOnlyMode,
|
|
845
|
+
setVideoOnly: setVideoOnlyMode,
|
|
846
|
+
setPlaybackRate: setPlaybackRateAction,
|
|
847
|
+
seekTo,
|
|
848
|
+
seekBy: (delta) => seekTo((Number(videoRef.current?.currentTime) || currentTime) + (Number(delta) || 0)),
|
|
849
|
+
back: handleBack,
|
|
850
|
+
minimize: handleMinimize,
|
|
851
|
+
getVideoElement: () => videoRef.current,
|
|
852
|
+
enterFullscreen: () => playerRef.current?.requestFullscreen?.(),
|
|
853
|
+
exitFullscreen: () => typeof document !== 'undefined' ? document.exitFullscreen?.() : undefined,
|
|
854
|
+
getAudioTracks: () => normalizeTracks(videoRef.current, tracksProp),
|
|
855
|
+
};
|
|
856
|
+
publicPlayerRef.current = api;
|
|
857
|
+
return api;
|
|
858
|
+
}, [setPaused, togglePlay, restart, toggleMute, setMuted, setAspectRatio, setAudioOnlyMode, setVideoOnlyMode, setPlaybackRateAction, seekTo, handleBack, handleMinimize, availableTracks, tracksProp, currentTime]);
|
|
859
|
+
|
|
860
|
+
const hasChat = typeof integrations.liveChat?.loadMessages === 'function' || typeof renderLiveChat === 'function' || typeof integrations.liveChat?.render === 'function' || typeof actions.onLiveChatOpen === 'function';
|
|
861
|
+
const hasEpg = typeof integrations.epg?.loadListings === 'function' || typeof renderEpg === 'function' || typeof integrations.epg?.render === 'function' || typeof actions.onEpgOpen === 'function';
|
|
862
|
+
const hasRecording = !!integrations.recording || typeof actions.onRecordingStart === 'function';
|
|
863
|
+
const sourceType = String(media.type || media.mimeType || '').toLowerCase();
|
|
864
|
+
const directVideoSource = getDirectVideoSource({
|
|
865
|
+
mpegTs: mpegTs.useMpegTs,
|
|
866
|
+
useHls,
|
|
867
|
+
sourceType,
|
|
868
|
+
activeUrl,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const handleYouTubeStateChange = useCallback((state) => {
|
|
872
|
+
if (state === 'playing') setIsPaused(false);
|
|
873
|
+
if (state === 'paused' || state === 'ended') setIsPaused(true);
|
|
874
|
+
}, []);
|
|
875
|
+
|
|
876
|
+
useEffect(() => {
|
|
877
|
+
const video = videoRef.current;
|
|
878
|
+
if (!video) return undefined;
|
|
879
|
+
const updateTracks = () => setAvailableTracks(normalizeTracks(video, tracksProp));
|
|
880
|
+
const updateTime = () => {
|
|
881
|
+
setCurrentTime(Number(video.currentTime) || 0);
|
|
882
|
+
if (Number.isFinite(video.duration)) setDuration(video.duration);
|
|
883
|
+
onProgress?.({ currentTime: (Number(video.currentTime) || 0) * 1000, duration: (Number(video.duration) || 0) * 1000, target: video.currentTime });
|
|
884
|
+
};
|
|
885
|
+
const onReadyEvent = () => {
|
|
886
|
+
setBuffering(false);
|
|
887
|
+
updateTracks();
|
|
888
|
+
onReady?.(video);
|
|
889
|
+
};
|
|
890
|
+
const onPlayingEvent = () => {
|
|
891
|
+
setBuffering(false);
|
|
892
|
+
onPlaying?.(video);
|
|
893
|
+
};
|
|
894
|
+
const onWaitingEvent = () => setBuffering(true);
|
|
895
|
+
video.addEventListener('loadedmetadata', onReadyEvent);
|
|
896
|
+
video.addEventListener('canplay', onReadyEvent);
|
|
897
|
+
video.addEventListener('playing', onPlayingEvent);
|
|
898
|
+
video.addEventListener('waiting', onWaitingEvent);
|
|
899
|
+
video.addEventListener('timeupdate', updateTime);
|
|
900
|
+
const endedHandler = () => onEnded?.();
|
|
901
|
+
video.addEventListener('ended', endedHandler);
|
|
902
|
+
return () => {
|
|
903
|
+
video.removeEventListener('loadedmetadata', onReadyEvent);
|
|
904
|
+
video.removeEventListener('canplay', onReadyEvent);
|
|
905
|
+
video.removeEventListener('playing', onPlayingEvent);
|
|
906
|
+
video.removeEventListener('waiting', onWaitingEvent);
|
|
907
|
+
video.removeEventListener('timeupdate', updateTime);
|
|
908
|
+
video.removeEventListener('ended', endedHandler);
|
|
909
|
+
};
|
|
910
|
+
}, [onReady, onPlaying, onProgress, onEnded, tracksProp]);
|
|
911
|
+
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
if (selectedAudioTrack === undefined || selectedAudioTrack === null) return;
|
|
914
|
+
for (const track of availableTracks) {
|
|
915
|
+
if (track.nativeTrack) track.nativeTrack.enabled = String(track.id) === String(selectedAudioTrack);
|
|
916
|
+
}
|
|
917
|
+
}, [selectedAudioTrack, availableTracks]);
|
|
918
|
+
|
|
919
|
+
useEffect(() => {
|
|
920
|
+
const onFullscreenChange = () => setFullscreen(document.fullscreenElement === playerRef.current);
|
|
921
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
922
|
+
return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
923
|
+
}, []);
|
|
924
|
+
|
|
925
|
+
const toggleFullscreen = () => action('onFullscreen', () => {
|
|
926
|
+
if (typeof onFullscreen === 'function') return onFullscreen({ isFullscreen: !fullscreen });
|
|
927
|
+
if (document.fullscreenElement) document.exitFullscreen?.();
|
|
928
|
+
else playerRef.current?.requestFullscreen?.();
|
|
929
|
+
}, { isFullscreen: !fullscreen });
|
|
930
|
+
const openPanel = (panel) => action(
|
|
931
|
+
getPanelActionName(panel),
|
|
932
|
+
() => setActivePanel((current) => {
|
|
933
|
+
if (current === panel) return null;
|
|
934
|
+
return panel;
|
|
935
|
+
}),
|
|
936
|
+
{ tab: panel, isOpen: activePanel === panel, close: () => setActivePanel(null) },
|
|
937
|
+
);
|
|
938
|
+
const control = (name, label, callback, options = {}) => renderControlButton({
|
|
939
|
+
name, label, callback, options, overrides: controlOverrides, icons, theme,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const panelRenderer = getPanelRenderer(activePanel, renderLiveChat, renderEpg, integrations);
|
|
943
|
+
const panelIntegration = getPanelIntegration(activePanel, integrations);
|
|
944
|
+
const panelSource = mediaId == null ? media : { ...media, mediaId };
|
|
945
|
+
const rootStyle = getInlinePlayerStyle(inlinePreview, inlinePreviewRect);
|
|
946
|
+
const unlockedControls = buildUnlockedControls({
|
|
947
|
+
control,
|
|
948
|
+
actions,
|
|
949
|
+
onBack: handleBack,
|
|
950
|
+
backVisible: typeof actions.onBack === 'function' || typeof onBack === 'function' || typeof props.onClose === 'function',
|
|
951
|
+
minimizeVisible: typeof actions.onMinimize === 'function' || typeof onMinimize === 'function',
|
|
952
|
+
isLive,
|
|
953
|
+
restart,
|
|
954
|
+
toggleLock,
|
|
955
|
+
hasRecording,
|
|
956
|
+
recording,
|
|
957
|
+
integrations,
|
|
958
|
+
action,
|
|
959
|
+
videoRef,
|
|
960
|
+
streamUrl,
|
|
961
|
+
title,
|
|
962
|
+
activePanel,
|
|
963
|
+
hasChat,
|
|
964
|
+
hasEpg,
|
|
965
|
+
handleOpenPanel: openPanel,
|
|
966
|
+
handleMinimize,
|
|
967
|
+
setRecording,
|
|
968
|
+
});
|
|
969
|
+
const webPanel = createWebPanelNode({
|
|
970
|
+
renderer: panelRenderer,
|
|
971
|
+
integration: panelIntegration,
|
|
972
|
+
activePanel,
|
|
973
|
+
integrations,
|
|
974
|
+
source: panelSource,
|
|
975
|
+
title,
|
|
976
|
+
theme,
|
|
977
|
+
onClose: () => setActivePanel(null),
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
const handleYouTubeProgress = (event) => {
|
|
981
|
+
setCurrentTime(Number(event.currentTime) / 1000 || 0);
|
|
982
|
+
setDuration(Number(event.duration) / 1000 || 0);
|
|
983
|
+
onProgress?.(event);
|
|
984
|
+
};
|
|
985
|
+
const mediaSurface = h(WebPlayerSurface, {
|
|
986
|
+
youtubeVideoId,
|
|
987
|
+
streamUrl,
|
|
988
|
+
videoRef,
|
|
989
|
+
youtubeRef,
|
|
990
|
+
directVideoSource,
|
|
991
|
+
poster,
|
|
992
|
+
autoPlay,
|
|
993
|
+
paused: isPaused,
|
|
994
|
+
muted,
|
|
995
|
+
volume,
|
|
996
|
+
playbackRate,
|
|
997
|
+
videoOnly,
|
|
998
|
+
videoStyle,
|
|
999
|
+
audioOnly,
|
|
1000
|
+
inlinePreview,
|
|
1001
|
+
onPromotePreview,
|
|
1002
|
+
bufferingRef: onBufferingRef,
|
|
1003
|
+
onReady,
|
|
1004
|
+
onProgress: handleYouTubeProgress,
|
|
1005
|
+
onPlaying,
|
|
1006
|
+
onEnded,
|
|
1007
|
+
onStateChange: handleYouTubeStateChange,
|
|
1008
|
+
onError: handleError,
|
|
1009
|
+
handleError,
|
|
1010
|
+
});
|
|
1011
|
+
const streamMode = getStreamMode(youtubeVideoId, mpegTs.useMpegTs, useHls);
|
|
1012
|
+
|
|
1013
|
+
const bottomControlProps = {
|
|
1014
|
+
isLive,
|
|
1015
|
+
overrides: controlOverrides,
|
|
1016
|
+
theme,
|
|
1017
|
+
icons,
|
|
1018
|
+
currentTime,
|
|
1019
|
+
duration,
|
|
1020
|
+
seekTo,
|
|
1021
|
+
muted,
|
|
1022
|
+
toggleMute,
|
|
1023
|
+
showAspectMenu,
|
|
1024
|
+
setShowAspectMenu,
|
|
1025
|
+
selectAspect,
|
|
1026
|
+
videoOnly,
|
|
1027
|
+
setVideoOnlyMode,
|
|
1028
|
+
audioOnly,
|
|
1029
|
+
setAudioOnlyMode,
|
|
1030
|
+
availableTracks,
|
|
1031
|
+
showAudioMenu,
|
|
1032
|
+
setShowAudioMenu,
|
|
1033
|
+
selectAudio,
|
|
1034
|
+
playbackRate,
|
|
1035
|
+
setPlaybackRateAction,
|
|
1036
|
+
fullscreen,
|
|
1037
|
+
toggleFullscreen,
|
|
1038
|
+
};
|
|
1039
|
+
return h(WebPlayerLayout, {
|
|
1040
|
+
playerRef,
|
|
1041
|
+
inlinePreview,
|
|
1042
|
+
className,
|
|
1043
|
+
rootStyle,
|
|
1044
|
+
style,
|
|
1045
|
+
theme,
|
|
1046
|
+
streamMode,
|
|
1047
|
+
mediaSurface,
|
|
1048
|
+
title,
|
|
1049
|
+
poster,
|
|
1050
|
+
error,
|
|
1051
|
+
buffering,
|
|
1052
|
+
audioOnly,
|
|
1053
|
+
activePanel,
|
|
1054
|
+
webPanel,
|
|
1055
|
+
renderBackButton: () => control('back', 'Close player', () => action('onBack', props.onBack || props.onClose, { title, source: media }), { icon: 'close' }),
|
|
1056
|
+
onSwitchToVideo: () => setAudioOnlyMode(false),
|
|
1057
|
+
locked,
|
|
1058
|
+
controlOverrides,
|
|
1059
|
+
icons,
|
|
1060
|
+
unlockedControls,
|
|
1061
|
+
toggleLock,
|
|
1062
|
+
isPaused,
|
|
1063
|
+
togglePlay,
|
|
1064
|
+
bottomControlProps,
|
|
1065
|
+
onWheel: (event) => onInlinePreviewWheel?.(event.deltaY),
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
export default CineCrewPlayer;
|
|
1070
|
+
|
|
1071
|
+
export const InlineLivePlayer = React.memo(function InlineLivePlayer({
|
|
1072
|
+
source,
|
|
1073
|
+
url,
|
|
1074
|
+
title = 'Live TV',
|
|
1075
|
+
height = 220,
|
|
1076
|
+
poster,
|
|
1077
|
+
paused = false,
|
|
1078
|
+
isActive = true,
|
|
1079
|
+
onActivate,
|
|
1080
|
+
onFullscreen,
|
|
1081
|
+
controls = {},
|
|
1082
|
+
actions = {},
|
|
1083
|
+
theme,
|
|
1084
|
+
icons,
|
|
1085
|
+
style,
|
|
1086
|
+
initialMuted = true,
|
|
1087
|
+
onError,
|
|
1088
|
+
onPlaying,
|
|
1089
|
+
}) {
|
|
1090
|
+
const media = getSource(source, url);
|
|
1091
|
+
const playbackSource = { ...media, isLive: true, title: title || media.title };
|
|
1092
|
+
if (!isActive) {
|
|
1093
|
+
return h('button', {
|
|
1094
|
+
type: 'button',
|
|
1095
|
+
className: 'cinecrew-player__inline-poster',
|
|
1096
|
+
style: { height, ...style },
|
|
1097
|
+
onClick: onActivate,
|
|
1098
|
+
'aria-label': `Play ${title}`,
|
|
1099
|
+
}, poster || media.poster || media.posterUrl
|
|
1100
|
+
? h('img', { src: poster || media.poster || media.posterUrl, alt: '' })
|
|
1101
|
+
: h('span', null, '▶'));
|
|
1102
|
+
}
|
|
1103
|
+
return h(CineCrewPlayer, {
|
|
1104
|
+
source: playbackSource,
|
|
1105
|
+
title,
|
|
1106
|
+
isLive: true,
|
|
1107
|
+
autoPlay: !paused,
|
|
1108
|
+
paused,
|
|
1109
|
+
muted: initialMuted,
|
|
1110
|
+
poster: poster || media.poster || media.posterUrl,
|
|
1111
|
+
controls: {
|
|
1112
|
+
back: false, restart: false, lock: false, recording: false, liveChat: false, epg: false,
|
|
1113
|
+
seek: false, aspectRatio: false, videoOnly: false, audioOnly: false, audioTracks: false,
|
|
1114
|
+
playbackRate: false, minimize: false, playPause: true, mute: true, fullscreen: true,
|
|
1115
|
+
...controls,
|
|
1116
|
+
},
|
|
1117
|
+
actions: {
|
|
1118
|
+
...actions,
|
|
1119
|
+
...(onFullscreen && !actions.onFullscreen ? { onFullscreen: () => onFullscreen() } : {}),
|
|
1120
|
+
},
|
|
1121
|
+
theme,
|
|
1122
|
+
icons,
|
|
1123
|
+
onError,
|
|
1124
|
+
onPlaying,
|
|
1125
|
+
inlinePreview: true,
|
|
1126
|
+
onPromotePreview: onFullscreen,
|
|
1127
|
+
style: { width: '100%', height, aspectRatio: '16 / 9', ...style },
|
|
1128
|
+
});
|
|
1129
|
+
});
|