@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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
|
|
2
|
+
import Slider from '@react-native-community/slider';
|
|
3
|
+
import { isWeb, isElectron } from '../../../utils/runtimePlatform';
|
|
4
|
+
import { ASPECT_OPTIONS, PLAYBACK_SPEEDS, formatTime } from './playerConstants';
|
|
5
|
+
import { PlayerIcon, usePlayerColors } from '../../customization';
|
|
6
|
+
|
|
7
|
+
function SeekControls({ isLive, controls, insets, scale, isSeeking, sliderPos, currentTime, duration, onValueChange, onSlidingStart, onSlidingComplete }) {
|
|
8
|
+
const palette = usePlayerColors();
|
|
9
|
+
if (isLive || controls.seek === false) return null;
|
|
10
|
+
const seeking = Boolean(isSeeking?.current);
|
|
11
|
+
const displayTime = seeking ? sliderPos : currentTime;
|
|
12
|
+
const remaining = duration > 0 ? formatTime(Math.max(0, duration - displayTime)) : '--:--';
|
|
13
|
+
return (
|
|
14
|
+
<View style={[styles.bottomBar, { paddingHorizontal: Math.max(insets?.left || 0, insets?.right || 0, 20) }]} pointerEvents="box-none">
|
|
15
|
+
<View style={styles.timeRow}>
|
|
16
|
+
<Text style={[styles.timeText, seeking && styles.timeTextSeeking, { color: seeking ? palette.accentColor : palette.controlColor, fontSize: scale?.timeFont, fontWeight: seeking ? scale?.timeSeekingWeight : scale?.timeWeight }]}>{formatTime(displayTime)}</Text>
|
|
17
|
+
<Text style={[styles.timeText, { color: palette.controlColor, fontSize: scale?.timeFont, fontWeight: scale?.timeWeight }]}>{remaining}</Text>
|
|
18
|
+
</View>
|
|
19
|
+
<Slider style={styles.slider} minimumValue={0} maximumValue={duration > 0 ? duration : 1} value={sliderPos} minimumTrackTintColor={palette.accentColor} maximumTrackTintColor="rgba(255,255,255,0.3)" thumbTintColor={palette.accentColor} onValueChange={onValueChange} onSlidingStart={onSlidingStart} onSlidingComplete={onSlidingComplete} />
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function AspectRatioControl({ controls, open, aspectRatio, onToggle, onSelect }) {
|
|
25
|
+
const palette = usePlayerColors();
|
|
26
|
+
if (controls.aspectRatio === false) return null;
|
|
27
|
+
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
28
|
+
return (
|
|
29
|
+
<View style={styles.speedButtonContainer}>
|
|
30
|
+
{open ? <View style={[styles.speedPickerPopup, { right: 'auto', left: 0, backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
31
|
+
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Aspect Ratio</Text>
|
|
32
|
+
{ASPECT_OPTIONS.map((option) => {
|
|
33
|
+
const selected = aspectRatio === option.value;
|
|
34
|
+
return <TouchableOpacity key={option.label} style={styles.speedOption} onPress={(event) => { event.stopPropagation(); onSelect(option.value); }} activeOpacity={0.7}>
|
|
35
|
+
<PlayerIcon name="check" size={15} color={selected ? palette.accentColor : 'transparent'} />
|
|
36
|
+
<Text style={[styles.speedOptionText, { color: selected ? palette.accentColor : palette.controlColor }, selected && styles.speedOptionTextActive]}>{option.label}</Text>
|
|
37
|
+
</TouchableOpacity>;
|
|
38
|
+
})}
|
|
39
|
+
</View> : null}
|
|
40
|
+
<TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, open && selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }}>
|
|
41
|
+
<PlayerIcon pack="material" name="aspect-ratio" size={18} color={palette.controlColor} />
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AudioOnlyModeControl({ isAudioOnly, onToggle }) {
|
|
48
|
+
const palette = usePlayerColors();
|
|
49
|
+
const selectedStyle = isAudioOnly ? { backgroundColor: palette.surfaceColor, borderWidth: 1, borderColor: palette.accentColor } : null;
|
|
50
|
+
return <TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }} accessibilityLabel="Audio-Only Mode"><PlayerIcon name="headphones" size={18} color={isAudioOnly ? palette.accentColor : palette.controlColor} /></TouchableOpacity>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function VideoOnlyControl({ isVideoOnly, onToggle }) {
|
|
54
|
+
const palette = usePlayerColors();
|
|
55
|
+
const selectedStyle = isVideoOnly ? { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor } : null;
|
|
56
|
+
return <TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle?.(); }} accessibilityLabel={isVideoOnly ? 'Enable audio' : 'Video only'}><PlayerIcon name="video" size={18} color={isVideoOnly ? palette.accentColor : palette.controlColor} /></TouchableOpacity>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SpeedControl({ enabled, open, playbackRate, onToggle, onSelect }) {
|
|
60
|
+
const palette = usePlayerColors();
|
|
61
|
+
if (!enabled) return null;
|
|
62
|
+
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
63
|
+
return (
|
|
64
|
+
<View style={styles.speedButtonContainer}>
|
|
65
|
+
{open ? <View style={[styles.speedPickerPopup, { backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
66
|
+
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Playback speed</Text>
|
|
67
|
+
{[...PLAYBACK_SPEEDS].reverse().map((speed) => {
|
|
68
|
+
const selected = playbackRate === speed;
|
|
69
|
+
return <TouchableOpacity key={speed} style={styles.speedOption} onPress={(event) => { event.stopPropagation(); onSelect(speed); }} activeOpacity={0.7}>
|
|
70
|
+
<PlayerIcon name="check" size={15} color={selected ? palette.accentColor : 'transparent'} />
|
|
71
|
+
<Text style={[styles.speedOptionText, { color: selected ? palette.accentColor : palette.controlColor }, selected && styles.speedOptionTextActive]}>{speed === 1 ? 'Normal' : `${speed}x`}</Text>
|
|
72
|
+
</TouchableOpacity>;
|
|
73
|
+
})}
|
|
74
|
+
</View> : null}
|
|
75
|
+
<TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, open && selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }}>
|
|
76
|
+
<Text style={[styles.speedButtonText, { color: palette.controlColor }]}>{playbackRate}x</Text>
|
|
77
|
+
</TouchableOpacity>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function AudioTrackMenu({ open, audioTracks, selectedAudioTrack, onSelect, palette }) {
|
|
83
|
+
if (!open) return null;
|
|
84
|
+
if (audioTracks.length <= 1) return <View style={[styles.speedPickerPopup, { backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}><Text style={[styles.speedOptionText, { color: palette.controlColor }]}>No other audio available</Text></View>;
|
|
85
|
+
return (
|
|
86
|
+
<View style={[styles.speedPickerPopup, { backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
87
|
+
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Audio</Text>
|
|
88
|
+
{audioTracks.map((track) => {
|
|
89
|
+
const selected = String(track.id) === String(selectedAudioTrack);
|
|
90
|
+
return <TouchableOpacity key={`audio-${track.id}`} style={styles.speedOption} onPress={(event) => { event.stopPropagation(); onSelect(track.id); }} activeOpacity={0.7}>
|
|
91
|
+
<PlayerIcon name="check" size={15} color={selected ? palette.accentColor : 'transparent'} />
|
|
92
|
+
<Text style={[styles.speedOptionText, { color: selected ? palette.accentColor : palette.controlColor }, selected && styles.speedOptionTextActive]} numberOfLines={1}>{track.name || track.language || `Track ${track.id}`}</Text>
|
|
93
|
+
</TouchableOpacity>;
|
|
94
|
+
})}
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function AudioTracksControl({ enabled, open, audioTracks, selectedAudioTrack, onToggle, onSelect }) {
|
|
100
|
+
const palette = usePlayerColors();
|
|
101
|
+
if (!enabled) return null;
|
|
102
|
+
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
103
|
+
return (
|
|
104
|
+
<View style={styles.speedButtonContainer}>
|
|
105
|
+
<AudioTrackMenu open={open} audioTracks={audioTracks} selectedAudioTrack={selectedAudioTrack} onSelect={onSelect} palette={palette} />
|
|
106
|
+
<TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, open && selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }}>
|
|
107
|
+
<PlayerIcon pack="material" name="audiotrack" size={18} color={palette.controlColor} />
|
|
108
|
+
</TouchableOpacity>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function FullscreenControl({ enabled, isFullscreen, onToggle }) {
|
|
114
|
+
const palette = usePlayerColors();
|
|
115
|
+
if (!enabled) return null;
|
|
116
|
+
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
117
|
+
return <TouchableOpacity style={[styles.fullscreenButton, { backgroundColor: palette.controlBackground }, isFullscreen && selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }} hitSlop={10}><PlayerIcon name={isFullscreen ? 'fullscreen-exit' : 'fullscreen'} size={20} color={palette.controlColor} /></TouchableOpacity>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const PlayerBottomBar = ({
|
|
121
|
+
isLive,
|
|
122
|
+
insets,
|
|
123
|
+
scale,
|
|
124
|
+
isSeeking,
|
|
125
|
+
sliderPos,
|
|
126
|
+
currentTime,
|
|
127
|
+
duration,
|
|
128
|
+
isAudioOnlyFeatureEnabled,
|
|
129
|
+
isAudioOnly,
|
|
130
|
+
showAspectPicker,
|
|
131
|
+
aspectRatio,
|
|
132
|
+
showSpeedPicker,
|
|
133
|
+
playbackRate,
|
|
134
|
+
showAudioPicker,
|
|
135
|
+
audioTracks = [],
|
|
136
|
+
selectedAudioTrack,
|
|
137
|
+
isFullscreen,
|
|
138
|
+
onSliderValueChange,
|
|
139
|
+
onSliderSlidingStart,
|
|
140
|
+
onSliderSlidingComplete,
|
|
141
|
+
onToggleAudioOnly,
|
|
142
|
+
onToggleAspectPicker,
|
|
143
|
+
onSelectAspectRatio,
|
|
144
|
+
onToggleSpeedPicker,
|
|
145
|
+
onSelectSpeed,
|
|
146
|
+
onToggleAudioPicker,
|
|
147
|
+
onSelectAudioTrack,
|
|
148
|
+
onToggleFullscreen,
|
|
149
|
+
controls = {},
|
|
150
|
+
isVideoOnly = false,
|
|
151
|
+
onToggleVideoOnly,
|
|
152
|
+
}) => {
|
|
153
|
+
return (
|
|
154
|
+
<View
|
|
155
|
+
style={[
|
|
156
|
+
styles.bottomContainer,
|
|
157
|
+
{
|
|
158
|
+
paddingBottom: Math.max(insets?.bottom || 0, 16),
|
|
159
|
+
paddingHorizontal: Math.max(insets?.left || 0, insets?.right || 0, 20),
|
|
160
|
+
},
|
|
161
|
+
]}
|
|
162
|
+
pointerEvents="box-none"
|
|
163
|
+
>
|
|
164
|
+
<SeekControls isLive={isLive} controls={controls} insets={insets} scale={scale} isSeeking={isSeeking} sliderPos={sliderPos} currentTime={currentTime} duration={duration} onValueChange={onSliderValueChange} onSlidingStart={onSliderSlidingStart} onSlidingComplete={onSliderSlidingComplete} />
|
|
165
|
+
|
|
166
|
+
{/* ASPECT RATIO PICKER & RIGHT ACTIONS (PLAYBACK SPEED & FULLSCREEN) */}
|
|
167
|
+
<View style={styles.bottomControlsRow} pointerEvents="box-none">
|
|
168
|
+
<View style={styles.leftActionsContainer}>
|
|
169
|
+
{isAudioOnlyFeatureEnabled && controls.audioOnly !== false ? <AudioOnlyModeControl isAudioOnly={isAudioOnly} onToggle={onToggleAudioOnly} /> : null}
|
|
170
|
+
<AspectRatioControl controls={controls} open={showAspectPicker} aspectRatio={aspectRatio} onToggle={onToggleAspectPicker} onSelect={onSelectAspectRatio} />
|
|
171
|
+
{controls.videoOnly ? <VideoOnlyControl isVideoOnly={isVideoOnly} onToggle={onToggleVideoOnly} /> : null}
|
|
172
|
+
</View>
|
|
173
|
+
|
|
174
|
+
{/* RIGHT ACTIONS: Playback Speed, Audio, Fullscreen */}
|
|
175
|
+
<View style={styles.rightActionsContainer}>
|
|
176
|
+
<SpeedControl enabled={!isLive && controls.playbackRate !== false} open={showSpeedPicker} playbackRate={playbackRate} onToggle={onToggleSpeedPicker} onSelect={onSelectSpeed} />
|
|
177
|
+
|
|
178
|
+
{/* Audio Tracks Picker */}
|
|
179
|
+
<AudioTracksControl enabled={controls.audioTracks !== false && !isLive && (!isWeb() || isElectron())} open={showAudioPicker} audioTracks={audioTracks} selectedAudioTrack={selectedAudioTrack} onToggle={onToggleAudioPicker} onSelect={onSelectAudioTrack} />
|
|
180
|
+
|
|
181
|
+
{/* Fullscreen Button - comes after playback speed (web only) */}
|
|
182
|
+
<FullscreenControl enabled={isWeb() && controls.fullscreen !== false} isFullscreen={isFullscreen} onToggle={onToggleFullscreen} />
|
|
183
|
+
</View>
|
|
184
|
+
</View>
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const styles = StyleSheet.create({
|
|
190
|
+
bottomContainer: {
|
|
191
|
+
paddingVertical: 8,
|
|
192
|
+
gap: 8,
|
|
193
|
+
zIndex: 70,
|
|
194
|
+
elevation: 70,
|
|
195
|
+
},
|
|
196
|
+
bottomBar: {
|
|
197
|
+
gap: 6,
|
|
198
|
+
},
|
|
199
|
+
timeRow: {
|
|
200
|
+
flexDirection: 'row',
|
|
201
|
+
justifyContent: 'space-between',
|
|
202
|
+
paddingHorizontal: 4,
|
|
203
|
+
},
|
|
204
|
+
timeText: {
|
|
205
|
+
color: '#FFF',
|
|
206
|
+
fontVariant: ['tabular-nums'],
|
|
207
|
+
},
|
|
208
|
+
timeTextSeeking: {
|
|
209
|
+
color: '#FFD60A',
|
|
210
|
+
},
|
|
211
|
+
slider: {
|
|
212
|
+
width: '100%',
|
|
213
|
+
height: 36,
|
|
214
|
+
},
|
|
215
|
+
bottomControlsRow: {
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
justifyContent: 'space-between',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
width: '100%',
|
|
220
|
+
paddingHorizontal: 10,
|
|
221
|
+
minHeight: 44,
|
|
222
|
+
},
|
|
223
|
+
speedButtonContainer: {
|
|
224
|
+
position: 'relative',
|
|
225
|
+
zIndex: 100,
|
|
226
|
+
},
|
|
227
|
+
leftActionsContainer: {
|
|
228
|
+
flexDirection: 'row',
|
|
229
|
+
alignItems: 'center',
|
|
230
|
+
gap: 8,
|
|
231
|
+
zIndex: 100,
|
|
232
|
+
},
|
|
233
|
+
rightActionsContainer: {
|
|
234
|
+
flexDirection: 'row',
|
|
235
|
+
alignItems: 'center',
|
|
236
|
+
gap: 8,
|
|
237
|
+
zIndex: 100,
|
|
238
|
+
},
|
|
239
|
+
fullscreenButton: {
|
|
240
|
+
paddingHorizontal: 12,
|
|
241
|
+
paddingVertical: 6,
|
|
242
|
+
borderRadius: 18,
|
|
243
|
+
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
justifyContent: 'center',
|
|
246
|
+
},
|
|
247
|
+
speedButton: {
|
|
248
|
+
paddingHorizontal: 14,
|
|
249
|
+
paddingVertical: 6,
|
|
250
|
+
borderRadius: 18,
|
|
251
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
252
|
+
},
|
|
253
|
+
speedButtonText: {
|
|
254
|
+
color: '#FFF',
|
|
255
|
+
fontWeight: '700',
|
|
256
|
+
},
|
|
257
|
+
speedPickerPopup: {
|
|
258
|
+
position: 'absolute',
|
|
259
|
+
right: 0,
|
|
260
|
+
bottom: 44,
|
|
261
|
+
backgroundColor: 'rgba(15, 15, 15, 0.96)',
|
|
262
|
+
borderRadius: 12,
|
|
263
|
+
paddingVertical: 4,
|
|
264
|
+
minWidth: 168,
|
|
265
|
+
zIndex: 200,
|
|
266
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
267
|
+
borderColor: 'rgba(255, 255, 255, 0.12)',
|
|
268
|
+
overflow: 'hidden',
|
|
269
|
+
},
|
|
270
|
+
speedPickerTitle: {
|
|
271
|
+
color: 'rgba(255,255,255,0.45)',
|
|
272
|
+
fontSize: 10,
|
|
273
|
+
fontWeight: '700',
|
|
274
|
+
textTransform: 'uppercase',
|
|
275
|
+
letterSpacing: 0.9,
|
|
276
|
+
paddingHorizontal: 16,
|
|
277
|
+
paddingTop: 10,
|
|
278
|
+
paddingBottom: 6,
|
|
279
|
+
},
|
|
280
|
+
speedOption: {
|
|
281
|
+
flexDirection: 'row',
|
|
282
|
+
alignItems: 'center',
|
|
283
|
+
gap: 10,
|
|
284
|
+
paddingHorizontal: 16,
|
|
285
|
+
paddingVertical: 11,
|
|
286
|
+
},
|
|
287
|
+
speedOptionText: {
|
|
288
|
+
color: 'rgba(255, 255, 255, 0.85)',
|
|
289
|
+
fontSize: 14,
|
|
290
|
+
fontWeight: '500',
|
|
291
|
+
},
|
|
292
|
+
speedOptionTextActive: {
|
|
293
|
+
fontWeight: '700',
|
|
294
|
+
},
|
|
295
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View, Text, TouchableOpacity, useWindowDimensions } from 'react-native';
|
|
3
|
+
import { isAndroid, isIOS } from '../../../utils/runtimePlatform';
|
|
4
|
+
import { cleanPlayerTitle } from '../../../utils/mediaUtils';
|
|
5
|
+
import { PlayerIcon, usePlayerColors } from '../../customization';
|
|
6
|
+
|
|
7
|
+
function shouldShowEpisodeSubtitle(isMobile, episodeLabel, displayTitle) {
|
|
8
|
+
if (isMobile || !episodeLabel) return false;
|
|
9
|
+
const episode = episodeLabel.trim().toLowerCase();
|
|
10
|
+
const title = displayTitle.toLowerCase();
|
|
11
|
+
return Boolean(episode && title !== episode && !title.includes(episode));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function BackButton({ visible, palette, scale, onClose }) {
|
|
15
|
+
if (!visible) return <View style={{ width: 12 }} />;
|
|
16
|
+
return (
|
|
17
|
+
<TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onClose(); }} hitSlop={12}>
|
|
18
|
+
<PlayerIcon name="arrow-left" size={22} color={palette.controlColor} />
|
|
19
|
+
<Text style={[styles.backText, { color: palette.controlColor, fontSize: scale?.backFont, fontWeight: scale?.backWeight }]}>Back</Text>
|
|
20
|
+
</TouchableOpacity>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function RecordingControls({ isLive, enabled, status, loading, controls, onStart, onResume, onPause, onStop, palette }) {
|
|
25
|
+
if (!isLive || !enabled || controls.recording === false) return null;
|
|
26
|
+
if (status === 'idle') {
|
|
27
|
+
return (
|
|
28
|
+
<TouchableOpacity style={[styles.pill, loading && { opacity: 0.45 }]} onPress={onStart} hitSlop={12} disabled={loading} accessibilityLabel="Start recording">
|
|
29
|
+
<PlayerIcon name="record-rec" size={22} color={palette.errorColor} />
|
|
30
|
+
</TouchableOpacity>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const isPaused = status === 'paused';
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<TouchableOpacity style={[styles.pill, isPaused ? styles.recPausePill : styles.recActivePill]} onPress={isPaused ? onResume : onPause} hitSlop={12} accessibilityLabel={isPaused ? 'Resume recording' : 'Pause recording'}>
|
|
37
|
+
<PlayerIcon name={isPaused ? 'play' : 'pause'} size={20} color={palette.controlColor} />
|
|
38
|
+
</TouchableOpacity>
|
|
39
|
+
<TouchableOpacity style={[styles.pill, styles.recStopPill]} onPress={onStop} hitSlop={12} accessibilityLabel="Stop recording">
|
|
40
|
+
<PlayerIcon name="stop" size={20} color={palette.errorColor} />
|
|
41
|
+
</TouchableOpacity>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function LiveServiceControls({ isLive, controls, showLiveChat, drawerTab, showChat, showEpg, showDiagnostics, onToggle, palette }) {
|
|
47
|
+
const renderServiceButton = (name, tab, enabled, activeName) => {
|
|
48
|
+
if (!isLive || !enabled || controls[name] === false) return null;
|
|
49
|
+
const active = showLiveChat && drawerTab === tab;
|
|
50
|
+
return (
|
|
51
|
+
<TouchableOpacity key={tab} style={[styles.pill, active && { backgroundColor: palette.surfaceColor, borderColor: palette.accentColor, borderWidth: 1 }]} onPress={(event) => { event.stopPropagation(); onToggle(tab); }} hitSlop={12}>
|
|
52
|
+
<PlayerIcon name={active ? activeName : ({ chat: 'comment-text-multiple-outline', epg: 'television-classic', diagnostics: 'pulse' }[tab])} size={20} color={active ? palette.accentColor : palette.controlColor} />
|
|
53
|
+
</TouchableOpacity>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
return <>{renderServiceButton('liveChat', 'chat', showChat, 'comment-text-multiple')}{renderServiceButton('epg', 'epg', showEpg, 'television-guide')}{renderServiceButton('diagnostics', 'diagnostics', showDiagnostics, 'pulse')}</>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function PlaybackSessionControls({ isLive, controls, muted, onRestart, onMute, onLock, onMinimize, palette }) {
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
{!isLive && controls.restart !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onRestart(); }} hitSlop={12}><PlayerIcon name="restart" size={20} color={palette.controlColor} /></TouchableOpacity> : null}
|
|
63
|
+
{controls.mute !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onMute(); }} hitSlop={12}><PlayerIcon name={muted ? 'volume-off' : 'volume-high'} size={20} color={muted ? palette.errorColor : palette.controlColor} /></TouchableOpacity> : null}
|
|
64
|
+
{controls.lock !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onLock(); }} hitSlop={12}><PlayerIcon name="lock-open-variant" size={20} color={palette.controlColor} /></TouchableOpacity> : null}
|
|
65
|
+
{controls.minimize && typeof onMinimize === 'function' ? <TouchableOpacity style={styles.pill} onPress={onMinimize} hitSlop={12} accessibilityLabel="Minimize player"><PlayerIcon name="arrow-collapse" size={20} color={palette.controlColor} /></TouchableOpacity> : null}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function PlayerTitle({ isPortrait, displayTitle, episodeLabel, showEpisodeSubtitle, palette, scale }) {
|
|
71
|
+
if (isPortrait) return <View style={styles.headerSpacer} />;
|
|
72
|
+
return (
|
|
73
|
+
<View style={styles.titleBlock}>
|
|
74
|
+
<View style={styles.titleRow}>
|
|
75
|
+
<Text style={[styles.titleText, { color: palette.controlColor, fontSize: scale?.titleFont, fontWeight: scale?.titleWeight }]} numberOfLines={1}>{displayTitle}</Text>
|
|
76
|
+
</View>
|
|
77
|
+
{showEpisodeSubtitle ? <Text style={[styles.episodeLabelText, { color: palette.mutedColor, fontSize: scale?.backFont }]} numberOfLines={1}>{episodeLabel}</Text> : null}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function PortraitPlayerTitle({ isPortrait, displayTitle, episodeLabel, showEpisodeSubtitle, palette, scale }) {
|
|
83
|
+
if (!isPortrait) return null;
|
|
84
|
+
return (
|
|
85
|
+
<View style={styles.portraitTitleBlock}>
|
|
86
|
+
<Text style={[styles.titleText, styles.portraitTitleText, { color: palette.controlColor, fontSize: scale?.titleFont, fontWeight: scale?.titleWeight }]} numberOfLines={2}>{displayTitle}</Text>
|
|
87
|
+
{showEpisodeSubtitle ? <Text style={[styles.episodeLabelText, styles.portraitEpisodeLabel, { color: palette.mutedColor, fontSize: scale?.backFont }]} numberOfLines={1}>{episodeLabel}</Text> : null}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const PlayerTopBar = ({
|
|
93
|
+
insets,
|
|
94
|
+
scale,
|
|
95
|
+
title,
|
|
96
|
+
episodeLabel,
|
|
97
|
+
isLive,
|
|
98
|
+
isScreenRecorderEnabled,
|
|
99
|
+
recStatus,
|
|
100
|
+
isLoading,
|
|
101
|
+
showLiveChat,
|
|
102
|
+
drawerTab,
|
|
103
|
+
isLiveCommentsEnabled,
|
|
104
|
+
isEpgEnabled,
|
|
105
|
+
diagnosticsOverlayEnabled,
|
|
106
|
+
muted,
|
|
107
|
+
onClose,
|
|
108
|
+
onStartRecording,
|
|
109
|
+
onResumeRecording,
|
|
110
|
+
onPauseRecording,
|
|
111
|
+
onStopRecording,
|
|
112
|
+
onToggleChatTab,
|
|
113
|
+
onRestart,
|
|
114
|
+
onToggleMute,
|
|
115
|
+
onToggleLock,
|
|
116
|
+
onMinimize,
|
|
117
|
+
controls = {},
|
|
118
|
+
}) => {
|
|
119
|
+
const { width, height } = useWindowDimensions();
|
|
120
|
+
const palette = usePlayerColors();
|
|
121
|
+
const isPortrait = height >= width;
|
|
122
|
+
const isMobile = isAndroid() || isIOS() || Math.min(width, height) < 600;
|
|
123
|
+
|
|
124
|
+
const displayTitle = useMemo(() => {
|
|
125
|
+
return cleanPlayerTitle(title, episodeLabel, isMobile);
|
|
126
|
+
}, [title, episodeLabel, isMobile]);
|
|
127
|
+
|
|
128
|
+
const showEpisodeSubtitle = useMemo(
|
|
129
|
+
() => shouldShowEpisodeSubtitle(isMobile, episodeLabel, displayTitle),
|
|
130
|
+
[isMobile, episodeLabel, displayTitle],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<View
|
|
135
|
+
style={[
|
|
136
|
+
styles.topBar,
|
|
137
|
+
isPortrait && styles.portraitTopBar,
|
|
138
|
+
{
|
|
139
|
+
paddingTop: Math.max(insets?.top || 0, 24),
|
|
140
|
+
paddingHorizontal: Math.max(insets?.left || 0, insets?.right || 0, 20),
|
|
141
|
+
},
|
|
142
|
+
]}
|
|
143
|
+
pointerEvents="box-none"
|
|
144
|
+
>
|
|
145
|
+
<View style={[styles.headerRow, !isPortrait && styles.landscapeHeaderRow, isPortrait && styles.portraitHeaderRow]}>
|
|
146
|
+
<BackButton visible={controls.back !== false} palette={palette} scale={scale} onClose={onClose} />
|
|
147
|
+
<PlayerTitle isPortrait={isPortrait} displayTitle={displayTitle} episodeLabel={episodeLabel} showEpisodeSubtitle={showEpisodeSubtitle} palette={palette} scale={scale} />
|
|
148
|
+
|
|
149
|
+
<View style={styles.topRightActions}>
|
|
150
|
+
<RecordingControls isLive={isLive} enabled={isScreenRecorderEnabled} status={recStatus} loading={isLoading} controls={controls} onStart={onStartRecording} onResume={onResumeRecording} onPause={onPauseRecording} onStop={onStopRecording} palette={palette} />
|
|
151
|
+
<LiveServiceControls isLive={isLive} controls={controls} showLiveChat={showLiveChat} drawerTab={drawerTab} showChat={isLiveCommentsEnabled} showEpg={isEpgEnabled} showDiagnostics={diagnosticsOverlayEnabled} onToggle={onToggleChatTab} palette={palette} />
|
|
152
|
+
<PlaybackSessionControls isLive={isLive} controls={controls} muted={muted} onRestart={onRestart} onMute={onToggleMute} onLock={onToggleLock} onMinimize={onMinimize} palette={palette} />
|
|
153
|
+
</View>
|
|
154
|
+
</View>
|
|
155
|
+
<PortraitPlayerTitle isPortrait={isPortrait} displayTitle={displayTitle} episodeLabel={episodeLabel} showEpisodeSubtitle={showEpisodeSubtitle} palette={palette} scale={scale} />
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const styles = StyleSheet.create({
|
|
161
|
+
topBar: {
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'space-between',
|
|
165
|
+
paddingVertical: 10,
|
|
166
|
+
gap: 12,
|
|
167
|
+
zIndex: 70,
|
|
168
|
+
elevation: 70,
|
|
169
|
+
},
|
|
170
|
+
portraitTopBar: {
|
|
171
|
+
flexDirection: 'column',
|
|
172
|
+
alignItems: 'stretch',
|
|
173
|
+
justifyContent: 'flex-start',
|
|
174
|
+
gap: 8,
|
|
175
|
+
},
|
|
176
|
+
headerRow: {
|
|
177
|
+
flexDirection: 'row',
|
|
178
|
+
alignItems: 'center',
|
|
179
|
+
justifyContent: 'space-between',
|
|
180
|
+
gap: 12,
|
|
181
|
+
},
|
|
182
|
+
landscapeHeaderRow: {
|
|
183
|
+
flex: 1,
|
|
184
|
+
},
|
|
185
|
+
portraitHeaderRow: {
|
|
186
|
+
width: '100%',
|
|
187
|
+
},
|
|
188
|
+
headerSpacer: {
|
|
189
|
+
flex: 1,
|
|
190
|
+
},
|
|
191
|
+
titleBlock: {
|
|
192
|
+
flex: 1,
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center',
|
|
195
|
+
},
|
|
196
|
+
portraitTitleBlock: {
|
|
197
|
+
width: '100%',
|
|
198
|
+
alignItems: 'center',
|
|
199
|
+
paddingHorizontal: 4,
|
|
200
|
+
},
|
|
201
|
+
titleRow: {
|
|
202
|
+
flexDirection: 'row',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
gap: 8,
|
|
206
|
+
maxWidth: '100%',
|
|
207
|
+
},
|
|
208
|
+
titleText: {
|
|
209
|
+
color: '#FFF',
|
|
210
|
+
textAlign: 'center',
|
|
211
|
+
},
|
|
212
|
+
portraitTitleText: {
|
|
213
|
+
width: '100%',
|
|
214
|
+
textAlign: 'center',
|
|
215
|
+
},
|
|
216
|
+
episodeLabelText: {
|
|
217
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
218
|
+
marginTop: 2,
|
|
219
|
+
},
|
|
220
|
+
portraitEpisodeLabel: {
|
|
221
|
+
textAlign: 'center',
|
|
222
|
+
},
|
|
223
|
+
topRightActions: {
|
|
224
|
+
flexDirection: 'row',
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
gap: 8,
|
|
227
|
+
},
|
|
228
|
+
pill: {
|
|
229
|
+
flexDirection: 'row',
|
|
230
|
+
alignItems: 'center',
|
|
231
|
+
gap: 6,
|
|
232
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
233
|
+
paddingHorizontal: 12,
|
|
234
|
+
paddingVertical: 8,
|
|
235
|
+
borderRadius: 20,
|
|
236
|
+
},
|
|
237
|
+
backText: {
|
|
238
|
+
color: '#FFF',
|
|
239
|
+
},
|
|
240
|
+
recActivePill: {
|
|
241
|
+
backgroundColor: 'rgba(255, 82, 82, 0.28)',
|
|
242
|
+
borderWidth: 1,
|
|
243
|
+
borderColor: '#FF5252',
|
|
244
|
+
},
|
|
245
|
+
recPausePill: {
|
|
246
|
+
backgroundColor: 'rgba(255, 193, 7, 0.22)',
|
|
247
|
+
borderWidth: 1,
|
|
248
|
+
borderColor: '#FFC107',
|
|
249
|
+
},
|
|
250
|
+
recStopPill: {
|
|
251
|
+
backgroundColor: 'rgba(255, 82, 82, 0.16)',
|
|
252
|
+
borderWidth: 1,
|
|
253
|
+
borderColor: 'rgba(255, 82, 82, 0.7)',
|
|
254
|
+
},
|
|
255
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import { UIManager, NativeModules } from 'react-native';
|
|
3
|
+
import { isWeb } from '../../../utils/runtimePlatform';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether the native VLC view manager is registered. It is absent in
|
|
7
|
+
* Expo Go and can fail on new-architecture builds — in both cases we
|
|
8
|
+
* fall back to expo-video instead of crashing with
|
|
9
|
+
* "ViewConfig not found for component rctvlcplayer".
|
|
10
|
+
*/
|
|
11
|
+
export const VLC_AVAILABLE =
|
|
12
|
+
isWeb() ||
|
|
13
|
+
(() => {
|
|
14
|
+
try {
|
|
15
|
+
return !!(
|
|
16
|
+
UIManager?.getViewManagerConfig?.('RCTVLCPlayer') ||
|
|
17
|
+
NativeModules?.RCTVLCPlayer ||
|
|
18
|
+
NativeModules?.VLCPlayer
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render-phase error boundary: if VLCPlayer throws during render (e.g. the
|
|
27
|
+
* native view manager isn't registered on this build/Fabric), swap to the
|
|
28
|
+
* expo-video fallback instead of crashing with "View config not found".
|
|
29
|
+
*/
|
|
30
|
+
export class VLCBoundary extends Component {
|
|
31
|
+
state = { failed: false };
|
|
32
|
+
|
|
33
|
+
static getDerivedStateFromError() {
|
|
34
|
+
return { failed: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
if (this.state.failed) return this.props.fallback || null;
|
|
39
|
+
return this.props.children;
|
|
40
|
+
}
|
|
41
|
+
}
|