@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.
Files changed (66) hide show
  1. package/CineCrewPlayer.podspec +22 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +6 -0
  4. package/README.md +574 -0
  5. package/android/build.gradle +48 -0
  6. package/android/proguard-rules.pro +29 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/yuanzhou/vlc/ReactVlcPlayerPackage.java +28 -0
  9. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerView.java +988 -0
  10. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerViewManager.java +208 -0
  11. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/VideoEventEmitter.java +136 -0
  12. package/android/src/main/res/values/strings.xml +3 -0
  13. package/app.plugin.cjs +1 -0
  14. package/assets/cinecrew-player-logo.svg +44 -0
  15. package/package.json +132 -0
  16. package/packages/react-native-vlc-media-player/LICENSE +21 -0
  17. package/packages/react-native-vlc-media-player/VLCPlayer.js +207 -0
  18. package/packages/react-native-vlc-media-player/expo/android/withGradleTasks.cjs +49 -0
  19. package/packages/react-native-vlc-media-player/expo/ios/withMobileVlcKit.cjs +28 -0
  20. package/packages/react-native-vlc-media-player/expo/withVlcMediaPlayer.cjs +10 -0
  21. package/packages/react-native-vlc-media-player/index.d.ts +354 -0
  22. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.h +36 -0
  23. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.m +635 -0
  24. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.h +5 -0
  25. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.m +100 -0
  26. package/src/native/InlineLivePlayer.js +321 -0
  27. package/src/native/MediaPlayerView.js +3093 -0
  28. package/src/native/customization.js +44 -0
  29. package/src/native/index.js +107 -0
  30. package/src/native/media/ElectronVideoPlayer.js +288 -0
  31. package/src/native/media/EmojiPickerModal.js +30 -0
  32. package/src/native/media/LiveChatDrawer.js +1177 -0
  33. package/src/native/media/LiveRecordingOverlay.js +319 -0
  34. package/src/native/media/WebVideoPlayer.js +320 -0
  35. package/src/native/media/YouTubeVideoPlayer.native.js +120 -0
  36. package/src/native/media/YouTubeVideoPlayer.web.js +169 -0
  37. package/src/native/media/player/AudioOnlyView.js +337 -0
  38. package/src/native/media/player/CenterControls.js +94 -0
  39. package/src/native/media/player/ExoVideoFallback.js +329 -0
  40. package/src/native/media/player/PipOverlay.js +1 -0
  41. package/src/native/media/player/PlayerBottomBar.js +295 -0
  42. package/src/native/media/player/PlayerTopBar.js +255 -0
  43. package/src/native/media/player/VLCBoundary.js +41 -0
  44. package/src/native/media/player/VerticalIndicator.js +184 -0
  45. package/src/native/media/player/index.js +9 -0
  46. package/src/native/media/player/playerConstants.js +159 -0
  47. package/src/native/media/web/useWebAc3AudioPlayback.native.js +6 -0
  48. package/src/native/media/web/useWebAc3AudioPlayback.web.js +219 -0
  49. package/src/native/media/web/useWebHlsPlayback.native.js +3 -0
  50. package/src/native/media/web/useWebHlsPlayback.web.js +66 -0
  51. package/src/native/media/web/useWebMediaSession.js +90 -0
  52. package/src/native/media/web/useWebMpegTsPlayback.native.js +3 -0
  53. package/src/native/media/web/useWebMpegTsPlayback.web.js +356 -0
  54. package/src/native/media/web/useWebVideoAspectRatio.js +52 -0
  55. package/src/native/media/web/webPlaybackErrors.js +4 -0
  56. package/src/utils/layoutUtils.js +12 -0
  57. package/src/utils/mediaUtils.js +7 -0
  58. package/src/utils/recording.js +8 -0
  59. package/src/utils/runtimePlatform.js +19 -0
  60. package/src/utils/sourceUtils.js +99 -0
  61. package/src/utils/youtubeHtml.js +34 -0
  62. package/src/web/index.js +1129 -0
  63. package/src/web/styles.css +378 -0
  64. package/types/index.d.ts +268 -0
  65. package/vendor/mpegts.js/LICENSE +202 -0
  66. package/vendor/mpegts.js/mpegts.js +3 -0
@@ -0,0 +1,100 @@
1
+ #import "RCTVLCPlayerManager.h"
2
+ #import "RCTVLCPlayer.h"
3
+ #import "React/RCTBridge.h"
4
+ #import "React/RCTUIManager.h"
5
+
6
+ @implementation RCTVLCPlayerManager
7
+
8
+ RCT_EXPORT_MODULE();
9
+
10
+ @synthesize bridge = _bridge;
11
+
12
+ - (UIView *)view
13
+ {
14
+ return [[RCTVLCPlayer alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
15
+ }
16
+
17
+ /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
18
+ RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock);
19
+ RCT_EXPORT_VIEW_PROPERTY(onVideoPaused, RCTDirectEventBlock);
20
+ RCT_EXPORT_VIEW_PROPERTY(onVideoStopped, RCTDirectEventBlock);
21
+ RCT_EXPORT_VIEW_PROPERTY(onVideoBuffering, RCTDirectEventBlock);
22
+ RCT_EXPORT_VIEW_PROPERTY(onVideoPlaying, RCTDirectEventBlock);
23
+ RCT_EXPORT_VIEW_PROPERTY(onVideoEnded, RCTDirectEventBlock);
24
+ RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock);
25
+ RCT_EXPORT_VIEW_PROPERTY(onVideoOpen, RCTDirectEventBlock);
26
+ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
27
+ RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock);
28
+ RCT_EXPORT_VIEW_PROPERTY(onRecordingState, RCTDirectEventBlock);
29
+ RCT_EXPORT_VIEW_PROPERTY(onSnapshot, RCTDirectEventBlock);
30
+
31
+ - (dispatch_queue_t)methodQueue
32
+ {
33
+ return dispatch_get_main_queue();
34
+ }
35
+
36
+ RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary);
37
+ RCT_EXPORT_VIEW_PROPERTY(subtitleUri, NSString);
38
+ RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
39
+ RCT_EXPORT_VIEW_PROPERTY(seek, float);
40
+ RCT_EXPORT_VIEW_PROPERTY(rate, float);
41
+ RCT_EXPORT_VIEW_PROPERTY(resume, BOOL);
42
+ RCT_EXPORT_VIEW_PROPERTY(videoAspectRatio, NSString);
43
+ RCT_EXPORT_VIEW_PROPERTY(snapshotPath, NSString);
44
+ RCT_CUSTOM_VIEW_PROPERTY(muted, BOOL, RCTVLCPlayer)
45
+ {
46
+ BOOL isMuted = [RCTConvert BOOL:json];
47
+ [view setMuted:isMuted];
48
+ };
49
+ RCT_EXPORT_VIEW_PROPERTY(audioTrack, int);
50
+ RCT_EXPORT_VIEW_PROPERTY(textTrack, int);
51
+ RCT_EXPORT_VIEW_PROPERTY(autoplay, BOOL);
52
+ RCT_EXPORT_VIEW_PROPERTY(acceptInvalidCertificates, BOOL);
53
+ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
54
+ RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
55
+
56
+ RCT_EXPORT_METHOD(startRecording:(nonnull NSNumber*) reactTag withPath:(NSString *)path) {
57
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
58
+ RCTVLCPlayer *view = viewRegistry[reactTag];
59
+ if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
60
+ RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
61
+ return;
62
+ }
63
+ [view startRecording:path];
64
+ }];
65
+ }
66
+
67
+ RCT_EXPORT_METHOD(stopRecording:(nonnull NSNumber*) reactTag) {
68
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
69
+ RCTVLCPlayer *view = viewRegistry[reactTag];
70
+ if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
71
+ RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
72
+ return;
73
+ }
74
+ [view stopRecording];
75
+ }];
76
+ }
77
+
78
+ RCT_EXPORT_METHOD(stopPlayer:(nonnull NSNumber*) reactTag) {
79
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
80
+ RCTVLCPlayer *view = viewRegistry[reactTag];
81
+ if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
82
+ RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
83
+ return;
84
+ }
85
+ [view stopPlayer];
86
+ }];
87
+ }
88
+
89
+ RCT_EXPORT_METHOD(snapshot:(nonnull NSNumber*) reactTag withPath:(NSString *)path) {
90
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
91
+ RCTVLCPlayer *view = viewRegistry[reactTag];
92
+ if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
93
+ RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
94
+ return;
95
+ }
96
+ [view snapshot:path];
97
+ }];
98
+ }
99
+
100
+ @end
@@ -0,0 +1,321 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import VLCPlayer from '../../packages/react-native-vlc-media-player/VLCPlayer.js';
11
+ import { PlayerCustomizationProvider, PlayerIcon } from './customization';
12
+ import { WebVideoPlayer } from './media/WebVideoPlayer';
13
+ import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
14
+ import { isAndroid, isElectron, isIOS, isWeb } from '../utils/runtimePlatform';
15
+ import { USER_AGENT } from './media/player/playerConstants';
16
+
17
+ function getArtwork(channel) {
18
+ return channel?.logoUrl || channel?.logo || channel?.stream_icon || channel?.posterUrl || channel?.image || '';
19
+ }
20
+
21
+ function createInlinePlatformPlayer({
22
+ shouldRenderVideo,
23
+ streamUrl,
24
+ source,
25
+ muted,
26
+ paused,
27
+ volume,
28
+ title,
29
+ vlcSource,
30
+ onPlaying,
31
+ onError,
32
+ }) {
33
+ if (!shouldRenderVideo || !streamUrl) return null;
34
+
35
+ const commonProps = {
36
+ streamUrl,
37
+ isLive: true,
38
+ paused,
39
+ muted,
40
+ volume: 100,
41
+ onPlaying,
42
+ onError,
43
+ };
44
+ if (isElectron()) return React.createElement(ElectronVideoPlayer, commonProps);
45
+ if (isWeb()) return React.createElement(WebVideoPlayer, { ...commonProps, title });
46
+ if (!isAndroid() && !isIOS()) return null;
47
+
48
+ return React.createElement(VLCPlayer, {
49
+ key: streamUrl,
50
+ style: styles.video,
51
+ source: vlcSource || source,
52
+ autoplay: true,
53
+ paused,
54
+ muted: false,
55
+ volume: muted ? 0 : 100,
56
+ autoAspectRatio: true,
57
+ videoAspectRatio: 'FIT_SCREEN',
58
+ onPlaying,
59
+ onVLCPlaying: onPlaying,
60
+ onOpen: onPlaying,
61
+ onError,
62
+ onVLCError: onError,
63
+ });
64
+ }
65
+
66
+ function InlineControlButton({
67
+ controlName,
68
+ actionName,
69
+ label,
70
+ icon,
71
+ fallback,
72
+ payload,
73
+ active,
74
+ controls,
75
+ actions,
76
+ palette,
77
+ }) {
78
+ if (controls[controlName] === false) return null;
79
+ return React.createElement(Pressable, {
80
+ key: controlName,
81
+ accessibilityRole: 'button',
82
+ accessibilityLabel: label,
83
+ onPress: () => {
84
+ const callback = actions?.[actionName];
85
+ if (typeof callback === 'function') callback(payload, { player: null });
86
+ else fallback?.();
87
+ },
88
+ style: [styles.button, { backgroundColor: palette.controlBackground }, active && { borderColor: palette.accentColor, borderWidth: 1 }],
89
+ }, React.createElement(PlayerIcon, { name: icon, size: 19, color: palette.controlColor }));
90
+ }
91
+
92
+ function InlinePlayerOverlay({
93
+ showControls,
94
+ controls,
95
+ palette,
96
+ title,
97
+ muted,
98
+ paused,
99
+ source,
100
+ fullscreen,
101
+ onToggleControls,
102
+ onMute,
103
+ onPlay,
104
+ onFullscreen,
105
+ }) {
106
+ const button = (controlName, actionName, label, icon, fallback, payload, active = false) => React.createElement(InlineControlButton, {
107
+ key: controlName,
108
+ controlName,
109
+ actionName,
110
+ label,
111
+ icon,
112
+ fallback,
113
+ payload,
114
+ active,
115
+ controls,
116
+ actions: controls.actions || {},
117
+ palette,
118
+ });
119
+ const muteLabel = muted ? 'Unmute' : 'Mute';
120
+ const muteIcon = muted ? 'volume-off' : 'volume-high';
121
+ const playbackLabel = paused ? 'Play' : 'Pause';
122
+ const playbackIcon = paused ? 'play' : 'pause';
123
+
124
+ return React.createElement(View, { pointerEvents: 'box-none', style: StyleSheet.absoluteFill },
125
+ React.createElement(Pressable, {
126
+ style: StyleSheet.absoluteFill,
127
+ onPress: onToggleControls,
128
+ accessibilityLabel: showControls ? 'Hide video controls' : 'Show video controls',
129
+ }),
130
+ showControls ? React.createElement(React.Fragment, null,
131
+ React.createElement(View, { pointerEvents: 'box-none', style: styles.topRow },
132
+ React.createElement(View, { style: styles.liveBadge }, React.createElement(View, { style: styles.liveDot }), React.createElement(Text, { style: styles.liveText }, 'LIVE')),
133
+ React.createElement(Text, { numberOfLines: 1, style: [styles.title, { color: palette.controlColor }] }, title),
134
+ button('mute', 'onMute', muteLabel, muteIcon, onMute, { muted: !muted }),
135
+ ),
136
+ React.createElement(View, { pointerEvents: 'box-none', style: styles.center },
137
+ button('playPause', 'onPlayPause', playbackLabel, playbackIcon, onPlay, { isPlaying: !paused })),
138
+ React.createElement(View, { pointerEvents: 'box-none', style: styles.bottomRow },
139
+ React.createElement(Text, { numberOfLines: 1, style: [styles.title, { color: palette.controlColor }] }, title),
140
+ button('fullscreen', 'onFullscreen', 'Open full player', 'fullscreen', onFullscreen, { source, title, isFullscreen: !fullscreen }))) : null);
141
+ }
142
+
143
+ function InlineLivePlayerView({
144
+ source,
145
+ url,
146
+ title = 'Live TV',
147
+ height = 220,
148
+ onFullscreen,
149
+ paused: externalPaused,
150
+ isActive = true,
151
+ onActivate,
152
+ posterChannel,
153
+ poster,
154
+ controls = {},
155
+ actions = {},
156
+ theme = {},
157
+ style,
158
+ icons,
159
+ initialMuted = true,
160
+ onError,
161
+ onPlaying,
162
+ }) {
163
+ const sourceValue = source ?? url ?? '';
164
+ const sourceObject = typeof sourceValue === 'string' ? { uri: sourceValue } : sourceValue || {};
165
+ const rawUrl = sourceObject.uri || sourceObject.url || '';
166
+ const streamUrl = useMemo(() => rawUrl || '', [rawUrl]);
167
+ const [internallyPaused, setInternallyPaused] = useState(Boolean(externalPaused));
168
+ const [muted, setMuted] = useState(Boolean(initialMuted));
169
+ const [loading, setLoading] = useState(Boolean(streamUrl && isActive));
170
+ const [error, setError] = useState('');
171
+ const [showControls, setShowControls] = useState(true);
172
+ const [fullscreen, setFullscreen] = useState(false);
173
+ const shouldRenderVideo = isActive && !externalPaused;
174
+ const pausedNow = Boolean(externalPaused) || internallyPaused;
175
+ const artwork = poster || getArtwork(posterChannel);
176
+ const palette = {
177
+ accentColor: '#00D4FF',
178
+ controlBackground: 'rgba(5, 11, 20, 0.76)',
179
+ controlColor: '#FFFFFF',
180
+ surfaceColor: '#07111E',
181
+ errorColor: '#FF647C',
182
+ ...theme,
183
+ };
184
+
185
+ useEffect(() => {
186
+ setInternallyPaused(Boolean(externalPaused));
187
+ }, [externalPaused]);
188
+
189
+ useEffect(() => {
190
+ setInternallyPaused(false);
191
+ setMuted(Boolean(initialMuted));
192
+ setLoading(Boolean(streamUrl && isActive));
193
+ setError('');
194
+ setShowControls(true);
195
+ }, [streamUrl, initialMuted]);
196
+
197
+ useEffect(() => {
198
+ if (streamUrl && isActive && !pausedNow) setLoading(true);
199
+ else setLoading(false);
200
+ }, [streamUrl, isActive, pausedNow]);
201
+
202
+ const handleError = useCallback((detail) => {
203
+ const message = typeof detail === 'string' ? detail : detail?.message || 'Could not play this channel.';
204
+ setError(message);
205
+ setLoading(false);
206
+ onError?.(detail instanceof Error ? detail : { ...detail, message });
207
+ }, [onError]);
208
+
209
+ const handlePlaying = useCallback((event) => {
210
+ setLoading(false);
211
+ setError('');
212
+ onPlaying?.(event);
213
+ }, [onPlaying]);
214
+
215
+ const performAction = (name, fallback, payload) => {
216
+ if (typeof actions?.[name] === 'function') return actions[name](payload);
217
+ return fallback?.();
218
+ };
219
+
220
+ const togglePlay = (event) => {
221
+ event?.stopPropagation?.();
222
+ if (!shouldRenderVideo) {
223
+ onActivate?.();
224
+ setInternallyPaused(false);
225
+ return;
226
+ }
227
+ performAction('onPlayPause', () => setInternallyPaused((value) => !value), !pausedNow);
228
+ };
229
+
230
+ const toggleMute = (event) => {
231
+ event?.stopPropagation?.();
232
+ performAction('onMute', () => setMuted((value) => !value), !muted);
233
+ };
234
+
235
+ const openFullscreen = (event) => {
236
+ event?.stopPropagation?.();
237
+ performAction('onFullscreen', () => {
238
+ if (typeof onFullscreen === 'function') onFullscreen();
239
+ else setFullscreen((value) => !value);
240
+ }, { isFullscreen: !fullscreen, source: sourceObject, title });
241
+ };
242
+
243
+ const nativeMediaOptions = useMemo(() => [
244
+ `--user-agent=${USER_AGENT}`,
245
+ `--http-user-agent=${USER_AGENT}`,
246
+ ':http-user-agent=' + USER_AGENT,
247
+ '--network-caching=3000',
248
+ ':network-caching=3000',
249
+ '--drop-late-frames',
250
+ ':drop-late-frames',
251
+ '--skip-frames',
252
+ ':skip-frames',
253
+ '--network-caching=3000',
254
+ ], []);
255
+ const vlcSource = useMemo(() => ({
256
+ ...sourceObject,
257
+ uri: streamUrl,
258
+ initType: sourceObject.initType || 1,
259
+ hwDecoderEnabled: sourceObject.hwDecoderEnabled ?? 1,
260
+ hwDecoderForced: sourceObject.hwDecoderForced ?? 1,
261
+ mediaOptions: sourceObject.mediaOptions || nativeMediaOptions,
262
+ }), [sourceObject, streamUrl, nativeMediaOptions]);
263
+
264
+ const player = createInlinePlatformPlayer({
265
+ shouldRenderVideo,
266
+ streamUrl,
267
+ source: sourceObject,
268
+ muted,
269
+ paused: pausedNow,
270
+ title,
271
+ vlcSource,
272
+ onPlaying: handlePlaying,
273
+ onError: handleError,
274
+ });
275
+
276
+ return React.createElement(
277
+ PlayerCustomizationProvider,
278
+ { icons, theme },
279
+ React.createElement(View, { style: [styles.frame, { height, backgroundColor: palette.surfaceColor }, style] },
280
+ player ? React.createElement(View, { pointerEvents: 'none', style: StyleSheet.absoluteFill }, player) : null,
281
+ !shouldRenderVideo && artwork ? React.createElement(Image, { source: { uri: artwork }, resizeMode: 'contain', style: styles.poster }) : null,
282
+ !shouldRenderVideo && !artwork ? React.createElement(View, { style: styles.emptyPoster }, React.createElement(PlayerIcon, { name: 'television-play', size: 48, color: palette.accentColor })) : null,
283
+ shouldRenderVideo && loading && !error ? React.createElement(View, { pointerEvents: 'none', style: styles.loading }, React.createElement(ActivityIndicator, { size: 'large', color: palette.accentColor })) : null,
284
+ error ? React.createElement(View, { pointerEvents: 'none', style: styles.error }, React.createElement(Text, { style: [styles.errorText, { color: palette.controlColor }] }, error)) : null,
285
+ React.createElement(InlinePlayerOverlay, {
286
+ showControls,
287
+ controls: { ...controls, actions },
288
+ palette,
289
+ title,
290
+ muted,
291
+ paused: pausedNow,
292
+ source: streamUrl,
293
+ fullscreen,
294
+ onToggleControls: () => setShowControls((value) => !value),
295
+ onMute: toggleMute,
296
+ onPlay: togglePlay,
297
+ onFullscreen: openFullscreen,
298
+ }),
299
+ ),
300
+ );
301
+ }
302
+
303
+ export const InlineLivePlayer = React.memo(InlineLivePlayerView);
304
+
305
+ const styles = StyleSheet.create({
306
+ frame: { width: '100%', minHeight: 80, overflow: 'hidden', borderRadius: 14, position: 'relative', justifyContent: 'center' },
307
+ video: { width: '100%', height: '100%' },
308
+ poster: { ...StyleSheet.absoluteFillObject, width: '100%', height: '100%' },
309
+ emptyPoster: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center' },
310
+ loading: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', zIndex: 2 },
311
+ error: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', padding: 16, zIndex: 3 },
312
+ errorText: { textAlign: 'center', fontSize: 13 },
313
+ topRow: { position: 'absolute', top: 8, left: 8, right: 8, flexDirection: 'row', alignItems: 'center', gap: 8, zIndex: 4 },
314
+ liveBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 8, paddingVertical: 5, borderRadius: 6, backgroundColor: '#C62828' },
315
+ liveDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#FFF' },
316
+ liveText: { color: '#FFF', fontSize: 10, fontWeight: '800', letterSpacing: 1 },
317
+ title: { flex: 1, fontWeight: '700', fontSize: 14, textShadowColor: 'rgba(0,0,0,.8)', textShadowRadius: 5 },
318
+ center: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', zIndex: 5 },
319
+ bottomRow: { position: 'absolute', left: 8, right: 8, bottom: 8, flexDirection: 'row', alignItems: 'center', gap: 8, zIndex: 4 },
320
+ button: { minWidth: 38, height: 38, paddingHorizontal: 10, borderRadius: 22, alignItems: 'center', justifyContent: 'center' },
321
+ });