@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,44 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { Text } from 'react-native';
3
+ import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
4
+
5
+ const PlayerCustomizationContext = createContext({ icons: {}, theme: {} });
6
+
7
+ export function PlayerCustomizationProvider({ icons, theme, children }) {
8
+ return React.createElement(
9
+ PlayerCustomizationContext.Provider,
10
+ { value: { icons: icons || {}, theme: theme || {} } },
11
+ children,
12
+ );
13
+ }
14
+
15
+ export function usePlayerColors() {
16
+ const { theme = {} } = useContext(PlayerCustomizationContext);
17
+ const colors = theme.colors || {};
18
+ return {
19
+ accentColor: theme.accentColor || theme.brandAccent || colors.brandAccent || colors.primary || '#00E5FF',
20
+ backgroundColor: theme.backgroundColor || colors.background || '#050B14',
21
+ surfaceColor: theme.surfaceColor || colors.surface || '#111B2A',
22
+ controlBackground: theme.controlBackground || 'rgba(0, 0, 0, 0.5)',
23
+ controlColor: theme.controlColor || theme.textColor || colors.onSurfacePrimary || '#FFFFFF',
24
+ mutedColor: theme.mutedTextColor || colors.onSurfaceSecondary || '#B8C2D0',
25
+ errorColor: theme.errorColor || colors.error || '#FF5252',
26
+ borderColor: theme.borderColor || colors.outline || 'rgba(255,255,255,0.18)',
27
+ };
28
+ }
29
+
30
+ export function PlayerIcon({ name, pack = 'community', size = 20, color = '#FFFFFF', style }) {
31
+ const customization = useContext(PlayerCustomizationContext);
32
+ const custom = customization.icons?.[name] || customization.icons?.[`${pack}:${name}`];
33
+ if (React.isValidElement(custom)) {
34
+ return React.cloneElement(custom, { size, color, style, 'aria-hidden': true });
35
+ }
36
+ if (typeof custom === 'function') {
37
+ return React.createElement(custom, { name, size, color, style, 'aria-hidden': true });
38
+ }
39
+ if (typeof custom === 'string' && !/^[a-z0-9-]+$/i.test(custom)) {
40
+ return React.createElement(Text, { style: [{ fontSize: size, color }, style], accessibilityElementsHidden: true }, custom);
41
+ }
42
+ const IconSet = pack === 'material' ? MaterialIcons : MaterialCommunityIcons;
43
+ return React.createElement(IconSet, { name: typeof custom === 'string' ? custom : name, size, color, style });
44
+ }
@@ -0,0 +1,107 @@
1
+ import React, { forwardRef, useImperativeHandle, useRef } from 'react';
2
+ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
3
+ import { PlayerCustomizationProvider } from './customization';
4
+ import { MediaPlayerView } from './MediaPlayerView';
5
+ import { isElectron, isWeb } from '../utils/runtimePlatform';
6
+ import { getYouTubeVideoId, useResolvedPlayerSource } from '../utils/sourceUtils';
7
+
8
+ function getRuntimePlatform() {
9
+ if (isWeb()) return 'web';
10
+ if (isElectron()) return 'electron';
11
+ return 'native';
12
+ }
13
+
14
+ /** Full-screen player for React Native and Electron applications. */
15
+ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
16
+ const {
17
+ source,
18
+ url,
19
+ visible,
20
+ title,
21
+ poster,
22
+ posterUrl,
23
+ mediaType,
24
+ isLive,
25
+ autoPlay = true,
26
+ paused,
27
+ initialPaused,
28
+ muted,
29
+ initialMuted,
30
+ volume = 1,
31
+ playbackRate = 1,
32
+ audioOnly = false,
33
+ controls,
34
+ features,
35
+ actions,
36
+ integrations,
37
+ theme,
38
+ icons,
39
+ style,
40
+ resolveSource,
41
+ onClose,
42
+ onBack,
43
+ ...metadata
44
+ } = props;
45
+ const playerApiRef = useRef({});
46
+ useImperativeHandle(ref, () => playerApiRef.current, []);
47
+ const platform = getRuntimePlatform();
48
+ const resolution = useResolvedPlayerSource(source, url, resolveSource, platform);
49
+ const media = resolution.source || {};
50
+ const streamUrl = String(media.uri || media.url || '');
51
+ const resolvedIsLive = isLive ?? media.isLive ?? (media.mediaType === 'live');
52
+ const resolvedMediaType = mediaType || media.mediaType || (resolvedIsLive ? 'live' : 'movie');
53
+ const youtubeVideoId = getYouTubeVideoId(media);
54
+ const isVisible = visible ?? Boolean(streamUrl || youtubeVideoId);
55
+
56
+ React.useEffect(() => {
57
+ if (resolution.error) props.onError?.(resolution.error);
58
+ }, [resolution.error, props.onError]);
59
+
60
+ if (resolution.loading || resolution.error) {
61
+ const colors = theme?.colors || {};
62
+ return React.createElement(PlayerCustomizationProvider, { icons, theme },
63
+ React.createElement(View, { style: [styles.resolveScreen, style, { backgroundColor: theme?.backgroundColor || colors.background || '#050b14' }] },
64
+ resolution.loading
65
+ ? React.createElement(ActivityIndicator, { color: theme?.accentColor || colors.brandAccent || '#00D4FF' })
66
+ : null,
67
+ React.createElement(Text, { style: { color: theme?.textColor || colors.onSurfacePrimary || '#FFFFFF', marginTop: 12 } },
68
+ resolution.loading ? 'Resolving media source…' : (resolution.error?.message || 'Unable to resolve this media source.'))));
69
+ }
70
+
71
+ return React.createElement(
72
+ PlayerCustomizationProvider,
73
+ { icons, theme },
74
+ React.createElement(MediaPlayerView, {
75
+ ...metadata,
76
+ visible: isVisible,
77
+ streamUrl,
78
+ youtubeVideoId,
79
+ title: title || media.title || '',
80
+ posterUrl: poster || posterUrl || media.poster || media.posterUrl || '',
81
+ mediaType: resolvedMediaType,
82
+ onClose: onClose || onBack || (() => {}),
83
+ initialPaused: paused ?? initialPaused ?? !autoPlay,
84
+ initialMuted: muted ?? initialMuted ?? false,
85
+ initialAudioOnly: audioOnly,
86
+ initialVolume: Math.round(Math.max(0, Math.min(1, Number(volume) || 0)) * 100),
87
+ initialPlaybackRate: playbackRate,
88
+ controls: controls || {},
89
+ features: features || {},
90
+ actions: actions || {},
91
+ integrations: integrations || {},
92
+ theme,
93
+ icons,
94
+ style,
95
+ playerApiRef,
96
+ }),
97
+ );
98
+ });
99
+
100
+ /** Compact inline live preview companion for channel lists and guides. */
101
+ export { InlineLivePlayer } from './InlineLivePlayer';
102
+ export { PlayerCustomizationProvider } from './customization';
103
+ export default CineCrewPlayer;
104
+
105
+ const styles = StyleSheet.create({
106
+ resolveScreen: { flex: 1, minHeight: '100%', alignItems: 'center', justifyContent: 'center', padding: 24 },
107
+ });
@@ -0,0 +1,288 @@
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ const PLAYER_STAGE_ID = 'cinecrew-electron-vlc-stage';
5
+
6
+ function getElectronIpcRenderer() {
7
+ if (typeof window === 'undefined' || typeof window.require !== 'function') return null;
8
+ try {
9
+ return window.require('electron')?.ipcRenderer || null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export const ElectronVideoPlayer = forwardRef(function ElectronVideoPlayer({
16
+ streamUrl,
17
+ paused,
18
+ muted,
19
+ volume = 100,
20
+ playbackRate = 1,
21
+ audioTrack,
22
+ videoAspectRatio,
23
+ isLive,
24
+ audioOnly = false,
25
+ onProgress,
26
+ onPlaying,
27
+ onPlaybackStateChange,
28
+ onBuffering,
29
+ onError,
30
+ onEnded,
31
+ onTracksChanged,
32
+ onPlaybackRoute,
33
+ onClose,
34
+ }, ref) {
35
+ const ipcRef = useRef(null);
36
+ const mountPromiseRef = useRef(null);
37
+ const latestPropsRef = useRef(null);
38
+ const sourceRef = useRef('');
39
+ const tracksRef = useRef([]);
40
+ const stageRef = useRef(null);
41
+
42
+ latestPropsRef.current = {
43
+ onProgress,
44
+ onPlaying,
45
+ onPlaybackStateChange,
46
+ onBuffering,
47
+ onError,
48
+ onEnded,
49
+ onTracksChanged,
50
+ onPlaybackRoute,
51
+ onClose,
52
+ paused,
53
+ muted,
54
+ volume,
55
+ playbackRate,
56
+ audioTrack,
57
+ audioOnly,
58
+ };
59
+
60
+ const invokePlayer = useCallback(async (channel, payload) => {
61
+ const ipc = ipcRef.current;
62
+ const mounted = mountPromiseRef.current;
63
+ if (!ipc || !mounted) return null;
64
+ try {
65
+ const mountResult = await mounted;
66
+ if (!mountResult?.ok) return null;
67
+ const result = await ipc.invoke(channel, payload);
68
+ if (result?.ok === false) throw new Error(result.error || 'Electron VLC request failed.');
69
+ return result;
70
+ } catch (error) {
71
+ latestPropsRef.current?.onError?.(error instanceof Error ? error : new Error(String(error)));
72
+ return null;
73
+ }
74
+ }, []);
75
+
76
+ const handleNativeEvent = useCallback((_event, payload = {}) => {
77
+ const callbacks = latestPropsRef.current || {};
78
+ switch (payload.type) {
79
+ case 'progress':
80
+ callbacks.onProgress?.(payload);
81
+ break;
82
+ case 'playing': {
83
+ const audioTracks = Array.isArray(payload.audioTracks) ? payload.audioTracks : [];
84
+ tracksRef.current = audioTracks;
85
+ callbacks.onPlaybackStateChange?.(true);
86
+ callbacks.onTracksChanged?.({ audioTracks });
87
+ callbacks.onPlaying?.({ audioTracks });
88
+ callbacks.onBuffering?.(false);
89
+ break;
90
+ }
91
+ case 'paused':
92
+ callbacks.onPlaybackStateChange?.(false);
93
+ break;
94
+ case 'tracks': {
95
+ const audioTracks = Array.isArray(payload.audioTracks) ? payload.audioTracks : [];
96
+ tracksRef.current = audioTracks;
97
+ callbacks.onTracksChanged?.({ audioTracks });
98
+ break;
99
+ }
100
+ case 'buffering':
101
+ callbacks.onBuffering?.(!!payload.isBuffering);
102
+ break;
103
+ case 'ended':
104
+ callbacks.onEnded?.();
105
+ break;
106
+ case 'error':
107
+ callbacks.onError?.(new Error(payload.message || 'libVLC could not play this stream.'));
108
+ break;
109
+ case 'route':
110
+ callbacks.onPlaybackRoute?.(payload.url || '');
111
+ break;
112
+ case 'close':
113
+ callbacks.onClose?.();
114
+ break;
115
+ default:
116
+ break;
117
+ }
118
+ }, []);
119
+
120
+ useEffect(() => {
121
+ const ipc = getElectronIpcRenderer();
122
+ if (!ipc) {
123
+ latestPropsRef.current?.onError?.(new Error('CineCrew’s bundled VLC player is available only in the Electron app.'));
124
+ return undefined;
125
+ }
126
+
127
+ let disposed = false;
128
+ let layoutFrame = null;
129
+ const requestLayoutSync = () => {
130
+ if (layoutFrame !== null) return;
131
+ layoutFrame = requestAnimationFrame(() => {
132
+ layoutFrame = null;
133
+ ipc.send('cinecrew:vlc:layout');
134
+ });
135
+ };
136
+
137
+ ipcRef.current = ipc;
138
+ ipc.on('cinecrew:vlc:event', handleNativeEvent);
139
+ const mountPromise = ipc.invoke('cinecrew:vlc:mount', {
140
+ container: `#${PLAYER_STAGE_ID}`,
141
+ }).then((result) => {
142
+ if (!result?.ok) throw new Error(result?.error || 'Could not initialize CineCrew’s bundled VLC player.');
143
+ if (disposed) void ipc.invoke('cinecrew:vlc:unmount').catch(() => {});
144
+ return result;
145
+ }).catch((error) => {
146
+ if (!disposed) {
147
+ latestPropsRef.current?.onBuffering?.(false);
148
+ latestPropsRef.current?.onError?.(error instanceof Error ? error : new Error(String(error)));
149
+ }
150
+ return { ok: false };
151
+ });
152
+ mountPromiseRef.current = mountPromise;
153
+
154
+ window.addEventListener('scroll', requestLayoutSync, true);
155
+ window.addEventListener('resize', requestLayoutSync);
156
+ let resizeObserver = null;
157
+ if (typeof ResizeObserver !== 'undefined' && stageRef.current) {
158
+ resizeObserver = new ResizeObserver(requestLayoutSync);
159
+ resizeObserver.observe(stageRef.current);
160
+ }
161
+
162
+ return () => {
163
+ disposed = true;
164
+ window.removeEventListener('scroll', requestLayoutSync, true);
165
+ window.removeEventListener('resize', requestLayoutSync);
166
+ resizeObserver?.disconnect();
167
+ if (layoutFrame !== null) cancelAnimationFrame(layoutFrame);
168
+ ipc.removeListener('cinecrew:vlc:event', handleNativeEvent);
169
+ ipcRef.current = null;
170
+ mountPromiseRef.current = null;
171
+ void ipc.invoke('cinecrew:vlc:unmount').catch(() => {});
172
+ };
173
+ }, [handleNativeEvent]);
174
+
175
+ useEffect(() => {
176
+ let cancelled = false;
177
+ if (!streamUrl) return undefined;
178
+
179
+ const loadSource = async () => {
180
+ try {
181
+ // Electron uses VLC and should receive the provider URL directly. The
182
+ // Worker redirect resolver is a browser-only compatibility path.
183
+ const source = streamUrl;
184
+ if (!source || cancelled) return;
185
+ sourceRef.current = source;
186
+ latestPropsRef.current?.onBuffering?.(true);
187
+ const result = await invokePlayer('cinecrew:vlc:load', {
188
+ source,
189
+ isLive: !!isLive,
190
+ paused: latestPropsRef.current?.paused === true,
191
+ muted: latestPropsRef.current?.muted === true,
192
+ volume: latestPropsRef.current?.volume,
193
+ playbackRate: latestPropsRef.current?.playbackRate,
194
+ audioOnly: latestPropsRef.current?.audioOnly === true,
195
+ });
196
+ if (!cancelled && result?.ok === true) {
197
+ latestPropsRef.current?.onPlaybackRoute?.(source);
198
+ }
199
+ } catch (error) {
200
+ if (!cancelled) {
201
+ latestPropsRef.current?.onBuffering?.(false);
202
+ latestPropsRef.current?.onError?.(error instanceof Error ? error : new Error(String(error)));
203
+ }
204
+ }
205
+ };
206
+
207
+ void loadSource();
208
+ return () => {
209
+ cancelled = true;
210
+ };
211
+ }, [streamUrl, isLive, invokePlayer]);
212
+
213
+ useEffect(() => {
214
+ void invokePlayer('cinecrew:vlc:set-paused', paused);
215
+ }, [paused, invokePlayer]);
216
+
217
+ useEffect(() => {
218
+ void invokePlayer('cinecrew:vlc:set-volume', { muted, volume });
219
+ }, [muted, volume, invokePlayer]);
220
+
221
+ useEffect(() => {
222
+ void invokePlayer('cinecrew:vlc:set-rate', playbackRate);
223
+ }, [playbackRate, invokePlayer]);
224
+
225
+ useEffect(() => {
226
+ if (audioTrack !== null && audioTrack !== undefined) {
227
+ void invokePlayer('cinecrew:vlc:set-audio-track', audioTrack);
228
+ }
229
+ }, [audioTrack, invokePlayer]);
230
+
231
+ useEffect(() => {
232
+ void invokePlayer('cinecrew:vlc:set-audio-only', audioOnly);
233
+ }, [audioOnly, invokePlayer]);
234
+
235
+ useEffect(() => {
236
+ void invokePlayer('cinecrew:vlc:set-aspect-ratio', videoAspectRatio);
237
+ }, [videoAspectRatio, invokePlayer]);
238
+
239
+ useImperativeHandle(ref, () => ({
240
+ seek(ratio) {
241
+ return invokePlayer('cinecrew:vlc:seek', Math.max(0, Math.min(1, Number(ratio) || 0)));
242
+ },
243
+ seekTo(seconds) {
244
+ return invokePlayer('cinecrew:vlc:seek-to', Math.max(0, Number(seconds) || 0) * 1000);
245
+ },
246
+ reload() {
247
+ return invokePlayer('cinecrew:vlc:seek-to', 0);
248
+ },
249
+ changeVideoAspectRatio(aspect) {
250
+ return invokePlayer('cinecrew:vlc:set-aspect-ratio', aspect);
251
+ },
252
+ setAudioTrack(trackId) {
253
+ return invokePlayer('cinecrew:vlc:set-audio-track', trackId);
254
+ },
255
+ getAudioTracks() {
256
+ return tracksRef.current;
257
+ },
258
+ async requestPictureInPicture() {
259
+ return null;
260
+ },
261
+ async exitPictureInPicture() {
262
+ return null;
263
+ },
264
+ getVideoElement() {
265
+ return null;
266
+ },
267
+ }), [invokePlayer]);
268
+
269
+ return (
270
+ <View
271
+ ref={stageRef}
272
+ nativeID={PLAYER_STAGE_ID}
273
+ collapsable={false}
274
+ pointerEvents="none"
275
+ style={styles.stage}
276
+ />
277
+ );
278
+ });
279
+
280
+ const styles = StyleSheet.create({
281
+ stage: {
282
+ flex: 1,
283
+ width: '100%',
284
+ height: '100%',
285
+ backgroundColor: '#000',
286
+ overflow: 'hidden',
287
+ },
288
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useColorScheme } from 'react-native';
3
+ import {
4
+ EmojiPickerModal as HirakuEmojiPickerModal,
5
+ emojiData,
6
+ } from '@hiraku-ai/react-native-emoji-picker';
7
+
8
+ export const EmojiPickerModal = ({ visible, onClose, onSelectEmoji, colors }) => {
9
+ const systemScheme = useColorScheme();
10
+ const darkMode = colors?.mode ? colors.mode === 'dark' : systemScheme !== 'light';
11
+
12
+ return (
13
+ <HirakuEmojiPickerModal
14
+ visible={visible}
15
+ onClose={onClose}
16
+ onEmojiSelect={(emoji) => {
17
+ onSelectEmoji(emoji);
18
+ }}
19
+ emojis={emojiData}
20
+ darkMode={darkMode}
21
+ modalTitle="Emojis"
22
+ showSearchBar={true}
23
+ showTabs={true}
24
+ showHistoryTab={true}
25
+ showSkinToneSelector={true}
26
+ modalMaxWidth={420}
27
+ modalMaxHeight={380}
28
+ />
29
+ );
30
+ };