@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,94 @@
1
+ import { StyleSheet, View, TouchableOpacity } from 'react-native';
2
+ import { PlayerIcon, usePlayerColors } from '../../customization';
3
+
4
+ export const CenterControls = ({
5
+ visible = true,
6
+ isLive,
7
+ isPlaying,
8
+ onSeekBy,
9
+ onTogglePlayPause,
10
+ }) => {
11
+ const palette = usePlayerColors();
12
+ if (!visible) return null;
13
+
14
+ return (
15
+ <View style={styles.centerContainer} pointerEvents="box-none">
16
+ <View style={styles.centerRow} pointerEvents="box-none">
17
+ {!isLive && (
18
+ <TouchableOpacity
19
+ style={[styles.pill, { backgroundColor: palette.controlBackground }]}
20
+ onPress={(e) => {
21
+ e.stopPropagation();
22
+ onSeekBy(-10);
23
+ }}
24
+ hitSlop={12}
25
+ >
26
+ <PlayerIcon pack="material" name="replay-10" size={32} color={palette.controlColor} />
27
+ </TouchableOpacity>
28
+ )}
29
+
30
+ <TouchableOpacity
31
+ style={[styles.bigPlayBtn, { backgroundColor: palette.controlColor }]}
32
+ onPress={(e) => {
33
+ e.stopPropagation();
34
+ onTogglePlayPause();
35
+ }}
36
+ >
37
+ <PlayerIcon
38
+ name={isPlaying ? 'pause' : 'play'}
39
+ size={40}
40
+ color={palette.backgroundColor}
41
+ />
42
+ </TouchableOpacity>
43
+
44
+ {!isLive && (
45
+ <TouchableOpacity
46
+ style={[styles.pill, { backgroundColor: palette.controlBackground }]}
47
+ onPress={(e) => {
48
+ e.stopPropagation();
49
+ onSeekBy(10);
50
+ }}
51
+ hitSlop={12}
52
+ >
53
+ <PlayerIcon pack="material" name="forward-10" size={32} color={palette.controlColor} />
54
+ </TouchableOpacity>
55
+ )}
56
+ </View>
57
+ </View>
58
+ );
59
+ };
60
+
61
+ const styles = StyleSheet.create({
62
+ centerContainer: {
63
+ flex: 1,
64
+ justifyContent: 'center',
65
+ alignItems: 'center',
66
+ },
67
+ centerRow: {
68
+ flexDirection: 'row',
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ gap: 28,
72
+ },
73
+ pill: {
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ gap: 6,
77
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
78
+ paddingHorizontal: 12,
79
+ paddingVertical: 8,
80
+ borderRadius: 20,
81
+ zIndex: 70,
82
+ elevation: 70,
83
+ },
84
+ bigPlayBtn: {
85
+ width: 68,
86
+ height: 68,
87
+ borderRadius: 34,
88
+ backgroundColor: '#FFF',
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ zIndex: 70,
92
+ elevation: 70,
93
+ },
94
+ });
@@ -0,0 +1,329 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ useLayoutEffect,
6
+ useRef,
7
+ forwardRef,
8
+ useImperativeHandle,
9
+ } from 'react';
10
+ import { StyleSheet, View, useWindowDimensions } from 'react-native';
11
+ import { useVideoPlayer, VideoView } from 'expo-video';
12
+
13
+ function sendProgress(player, onProgress) {
14
+ if (typeof onProgress !== 'function') return;
15
+ const duration = Number.isFinite(player.duration) && player.duration > 0 ? player.duration : 0;
16
+ const currentTime = Number.isFinite(player.currentTime) && player.currentTime > 0 ? player.currentTime : 0;
17
+ onProgress({ currentTime: currentTime * 1000, duration: duration * 1000, target: currentTime });
18
+ }
19
+
20
+ function handleReadyStatus(player, callbacks) {
21
+ callbacks.onPlaying?.();
22
+ callbacks.onBuffering?.(false);
23
+ if (!callbacks.paused) callbacks.withActivePlayer((activePlayer) => activePlayer.play());
24
+ callbacks.onTracksChanged?.({ audioTracks: player.audioTracks ? Array.from(player.audioTracks) : [] });
25
+ sendProgress(player, callbacks.onProgress);
26
+ }
27
+
28
+ function notifyPlayerStatus(event, callbacks) {
29
+ const { status, error } = event;
30
+ if (status === 'readyToPlay') {
31
+ handleReadyStatus(callbacks.player, callbacks);
32
+ return;
33
+ }
34
+ if (status === 'loading' || status === 'idle') {
35
+ callbacks.onBuffering?.(true);
36
+ return;
37
+ }
38
+ if (status === 'error') callbacks.onError?.({ message: 'Failed to load stream.', error });
39
+ }
40
+
41
+ /**
42
+ * Native fallback player (expo-video) used when the VLC native module is not
43
+ * registered — e.g. running inside Expo Go, where react-native-vlc-media-player
44
+ * is not bundled. Mirrors the imperative API + events MediaPlayerView expects.
45
+ */
46
+ export const ExoVideoFallback = forwardRef(function ExoVideoFallback(
47
+ {
48
+ streamUrl,
49
+ paused,
50
+ muted,
51
+ volume = 100,
52
+ playbackRate = 1,
53
+ videoAspectRatio,
54
+ onProgress,
55
+ onPlaying,
56
+ onBuffering,
57
+ onEnded,
58
+ onError,
59
+ onTracksChanged,
60
+ audioTrack,
61
+ audioOnly = false,
62
+ },
63
+ ref
64
+ ) {
65
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
66
+ const [contentFit, setContentFit] = useState('contain');
67
+ const [boxStyle, setBoxStyle] = useState(undefined);
68
+
69
+ const effectiveStreamUrl = streamUrl || '';
70
+ const player = useVideoPlayer(effectiveStreamUrl, (p) => {
71
+ p.loop = false;
72
+ p.muted = !!muted;
73
+ p.volume = Math.max(0, Math.min(1, (volume ?? 100) / 100));
74
+ p.playbackRate = playbackRate;
75
+ p.staysActiveInBackground = !!audioOnly;
76
+ p.showNowPlayingNotification = !!audioOnly;
77
+ if (audioOnly) {
78
+ p.audioMixingMode = 'doNotMix';
79
+ }
80
+ // iOS default is 0, which disables timeUpdate events entirely.
81
+ p.timeUpdateEventInterval = 0.5;
82
+ });
83
+
84
+ // useVideoPlayer owns the native shared object and releases it during its
85
+ // cleanup. Mark the player inactive in the layout cleanup (before passive
86
+ // effects are torn down) so no command can race that release.
87
+ const activePlayerRef = useRef(null);
88
+ useLayoutEffect(() => {
89
+ activePlayerRef.current = player;
90
+ return () => {
91
+ if (activePlayerRef.current === player) {
92
+ activePlayerRef.current = null;
93
+ }
94
+ };
95
+ }, [player]);
96
+
97
+ const withActivePlayer = useCallback(
98
+ (operation) => {
99
+ if (activePlayerRef.current !== player) return;
100
+
101
+ try {
102
+ const result = operation(player);
103
+ // Expo's native implementation is typed as void, but some versions
104
+ // return a rejected promise when a shared object is released during a
105
+ // native transition. Never let that become an unhandled render error.
106
+ if (result && typeof result.catch === 'function') {
107
+ result.catch(() => {});
108
+ }
109
+ } catch {
110
+ // The hook may release the native object between the active check and
111
+ // the command. Teardown is best-effort and must remain idempotent.
112
+ }
113
+ },
114
+ [player],
115
+ );
116
+
117
+ useEffect(() => {
118
+ withActivePlayer((activePlayer) => {
119
+ if (paused) {
120
+ activePlayer.pause();
121
+ } else {
122
+ activePlayer.play();
123
+ }
124
+ });
125
+ }, [paused, withActivePlayer]);
126
+
127
+ useEffect(() => {
128
+ withActivePlayer((activePlayer) => {
129
+ activePlayer.muted = !!muted;
130
+ activePlayer.volume = Math.max(0, Math.min(1, (volume ?? 100) / 100));
131
+ });
132
+ }, [muted, volume, withActivePlayer]);
133
+
134
+ useEffect(() => {
135
+ withActivePlayer((activePlayer) => {
136
+ activePlayer.playbackRate = playbackRate;
137
+ });
138
+ }, [playbackRate, withActivePlayer]);
139
+
140
+ useEffect(() => {
141
+ withActivePlayer((activePlayer) => {
142
+ activePlayer.staysActiveInBackground = !!audioOnly;
143
+ activePlayer.showNowPlayingNotification = !!audioOnly;
144
+ if (audioOnly) {
145
+ activePlayer.audioMixingMode = 'doNotMix';
146
+ }
147
+ });
148
+ }, [audioOnly, withActivePlayer]);
149
+
150
+ // Apply track selections natively
151
+ useEffect(() => {
152
+ if (audioTrack === undefined || audioTrack === null) return;
153
+
154
+ withActivePlayer((activePlayer) => {
155
+ if (activePlayer.audioTracks) {
156
+ activePlayer.audioTracks.forEach((t) => {
157
+ t.selected = t.id === audioTrack;
158
+ });
159
+ }
160
+ });
161
+ }, [audioTrack, withActivePlayer]);
162
+
163
+ const applyAspect = useCallback(
164
+ (aspect) => {
165
+ let fit = 'contain';
166
+ let style;
167
+
168
+ if (aspect === 'FILL_SCREEN' || aspect === 'FILL') {
169
+ fit = 'cover';
170
+ } else if (aspect === 'STRETCH') {
171
+ fit = 'fill';
172
+ } else if (aspect === 'CENTER') {
173
+ fit = 'none';
174
+ } else if (typeof aspect === 'string' && aspect.includes(':')) {
175
+ const parts = aspect.split(':');
176
+ const ratio = Number(parts[0]) / Number(parts[1]);
177
+ if (Number.isFinite(ratio) && ratio > 0 && windowWidth > 0 && windowHeight > 0) {
178
+ const boxHeight = windowWidth / ratio;
179
+ const dims = boxHeight <= windowHeight ? { width: '100%' } : { height: '100%' };
180
+ fit = 'fill';
181
+ style = { aspectRatio: ratio, ...dims };
182
+ }
183
+ }
184
+
185
+ setContentFit(fit);
186
+ setBoxStyle(style);
187
+ },
188
+ [windowWidth, windowHeight]
189
+ );
190
+
191
+ useEffect(() => {
192
+ applyAspect(videoAspectRatio);
193
+ }, [videoAspectRatio, applyAspect]);
194
+
195
+ useEffect(() => {
196
+ if (activePlayerRef.current !== player) return undefined;
197
+
198
+ let offs;
199
+ try {
200
+ offs = [
201
+ player.addListener('timeUpdate', (event) => {
202
+ if (activePlayerRef.current !== player) return;
203
+
204
+ let cur = 0;
205
+ if (Number.isFinite(event?.currentTime)) {
206
+ cur = event.currentTime;
207
+ } else if (Number.isFinite(player.currentTime)) {
208
+ cur = player.currentTime;
209
+ }
210
+ const dur = Number.isFinite(player.duration) && player.duration > 0 ? player.duration : 0;
211
+ if (typeof onProgress === 'function') {
212
+ onProgress({
213
+ currentTime: cur * 1000,
214
+ duration: dur * 1000,
215
+ target: cur,
216
+ });
217
+ }
218
+ }),
219
+ player.addListener('playingChange', ({ isPlaying }) => {
220
+ if (activePlayerRef.current !== player) return;
221
+ if (isPlaying && typeof onBuffering === 'function') onBuffering(false);
222
+ }),
223
+ player.addListener('statusChange', (event) => {
224
+ if (activePlayerRef.current !== player) return;
225
+ notifyPlayerStatus(event, {
226
+ player,
227
+ paused,
228
+ onPlaying,
229
+ onBuffering,
230
+ onTracksChanged,
231
+ onProgress,
232
+ onError,
233
+ withActivePlayer,
234
+ });
235
+ }),
236
+ player.addListener('playToEnd', () => {
237
+ if (activePlayerRef.current === player && typeof onEnded === 'function') onEnded();
238
+ }),
239
+ ];
240
+ } catch {
241
+ // The shared object can be released while React is replacing the view.
242
+ // There is nothing left to unsubscribe in that case.
243
+ return undefined;
244
+ }
245
+
246
+ return () =>
247
+ offs.forEach((off) => {
248
+ try {
249
+ if (typeof off === 'function') {
250
+ off();
251
+ } else if (off && typeof off.remove === 'function') {
252
+ off.remove();
253
+ }
254
+ } catch {
255
+ // Listener cleanup is also best-effort after native release.
256
+ }
257
+ });
258
+ }, [
259
+ player,
260
+ paused,
261
+ onProgress,
262
+ onPlaying,
263
+ onBuffering,
264
+ onEnded,
265
+ onError,
266
+ onTracksChanged,
267
+ withActivePlayer,
268
+ ]);
269
+
270
+ // Safety net: keep progress flowing even if the timeUpdate event is
271
+ // unreliable in Expo Go.
272
+ useEffect(() => {
273
+ const id = setInterval(() => {
274
+ if (activePlayerRef.current !== player) return;
275
+
276
+ let cur = 0;
277
+ let dur = 0;
278
+ try {
279
+ cur = Number.isFinite(player.currentTime) ? player.currentTime : 0;
280
+ dur = Number.isFinite(player.duration) && player.duration > 0 ? player.duration : 0;
281
+ } catch {
282
+ return;
283
+ }
284
+ if (typeof onProgress === 'function') {
285
+ onProgress({
286
+ currentTime: cur * 1000,
287
+ duration: dur * 1000,
288
+ target: cur,
289
+ });
290
+ }
291
+ }, 500);
292
+ return () => clearInterval(id);
293
+ }, [player, onProgress]);
294
+
295
+ useImperativeHandle(ref, () => ({
296
+ seek(ratio) {
297
+ withActivePlayer((activePlayer) => {
298
+ if (activePlayer.duration && Number.isFinite(activePlayer.duration) && activePlayer.duration > 0) {
299
+ activePlayer.currentTime = Math.max(0, Math.min(1, ratio)) * activePlayer.duration;
300
+ }
301
+ });
302
+ },
303
+ changeVideoAspectRatio(aspect) {
304
+ applyAspect(aspect);
305
+ },
306
+ }), [applyAspect, withActivePlayer]);
307
+
308
+ return (
309
+ <View style={boxStyle || styles.video}>
310
+ {/* TextureView is required here because the player is resized and
311
+ layered under the in-app PiP overlay. */}
312
+ <VideoView
313
+ player={player}
314
+ style={styles.video}
315
+ contentFit={contentFit}
316
+ surfaceType="textureView"
317
+ nativeControls={false}
318
+ allowsFullscreen={false}
319
+ />
320
+ </View>
321
+ );
322
+ });
323
+
324
+ const styles = StyleSheet.create({
325
+ video: {
326
+ width: '100%',
327
+ height: '100%',
328
+ },
329
+ });
@@ -0,0 +1 @@
1
+ export const PipOverlay = () => null;