@cinecrew/cinecrew-player 0.1.3 → 0.1.5
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/README.md +56 -46
- package/package.json +1 -2
- package/src/native/InlineLivePlayer.js +95 -33
- package/src/native/MediaPlayerView.js +75 -73
- package/src/native/index.js +4 -5
- package/src/native/media/LiveChatDrawer.js +16 -25
- package/src/native/media/WebVideoPlayer.js +13 -0
- package/src/native/media/player/AudioOnlyView.js +3 -40
- package/src/native/media/player/PlayerBottomBar.js +4 -12
- package/src/native/media/player/playerConstants.js +3 -3
- package/src/native/media/web/useWebAc3AudioPlayback.web.js +9 -3
- package/src/native/media/web/useWebHlsPlayback.web.js +1 -0
- package/src/native/media/web/useWebVideoAspectRatio.js +7 -2
- package/src/utils/invokePlayerAction.js +24 -0
- package/src/utils/progressBarTime.js +46 -0
- package/src/utils/sourceUtils.js +0 -25
- package/src/utils/webRecording.js +194 -0
- package/src/web/index.js +523 -156
- package/src/web/styles.css +142 -23
- package/types/index.d.ts +21 -2
- package/src/native/media/YouTubeVideoPlayer.native.js +0 -120
- package/src/native/media/YouTubeVideoPlayer.web.js +0 -223
- package/src/utils/youtubeHtml.js +0 -34
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import VLCPlayer from '../../packages/react-native-vlc-media-player/VLCPlayer.js';
|
|
16
16
|
import { WebVideoPlayer } from './media/WebVideoPlayer';
|
|
17
17
|
import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
|
|
18
|
-
import { YouTubeVideoPlayer } from './media/YouTubeVideoPlayer';
|
|
19
18
|
import { LiveChatDrawer } from './media/LiveChatDrawer';
|
|
20
19
|
import { LiveRecordingNotice, LiveRecordingOverlay } from './media/LiveRecordingOverlay';
|
|
21
20
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
@@ -23,6 +22,8 @@ import { PlayerIcon } from './customization';
|
|
|
23
22
|
import { isWeb, isAndroid, isElectron } from '../utils/runtimePlatform';
|
|
24
23
|
import { getFontSize, getFontWeight } from '../utils/layoutUtils';
|
|
25
24
|
import { isLocalMediaUri } from '../utils/mediaUtils';
|
|
25
|
+
import { invokePlayerAction } from '../utils/invokePlayerAction.js';
|
|
26
|
+
import { emitProgressBarTime } from '../utils/progressBarTime.js';
|
|
26
27
|
import {
|
|
27
28
|
USER_AGENT,
|
|
28
29
|
calculateScreenAspectRatio,
|
|
@@ -69,6 +70,17 @@ function pickShuffleCandidate(candidates, season, episode) {
|
|
|
69
70
|
return candidates[(hash >>> 0) % candidates.length];
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function normalizeAspectOptions(values) {
|
|
74
|
+
if (!Array.isArray(values) || values.length === 0) return ASPECT_OPTIONS;
|
|
75
|
+
const unique = new Map();
|
|
76
|
+
values.forEach((item) => {
|
|
77
|
+
const value = typeof item === 'string' ? item : item?.value;
|
|
78
|
+
if (!value || typeof value !== 'string') return;
|
|
79
|
+
unique.set(value, { value, label: typeof item === 'string' ? item : item.label || value });
|
|
80
|
+
});
|
|
81
|
+
return unique.size ? [...unique.values()] : ASPECT_OPTIONS;
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
// Gesture events can arrive much faster than the native player can consume
|
|
73
85
|
// volume updates. Coalesce them to display-rate updates for the UI and a
|
|
74
86
|
// slower native volume cadence so a drag stays smooth without audio crackle.
|
|
@@ -204,49 +216,16 @@ function handleVerticalGestureMove(touches, gestureState) {
|
|
|
204
216
|
gestureState.commitVolume.current(volume);
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
function handleYouTubePlaybackState(state, playback) {
|
|
208
|
-
if (state === 'playing') {
|
|
209
|
-
playback.isPlayingRef.current = true;
|
|
210
|
-
playback.setIsPlaying(true);
|
|
211
|
-
} else if (state === 'paused' || state === 'ended') {
|
|
212
|
-
playback.isPlayingRef.current = false;
|
|
213
|
-
playback.setIsPlaying(false);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
219
|
function PlatformMediaSurface(props) {
|
|
218
220
|
const {
|
|
219
|
-
|
|
221
|
+
playerStreamUrl, vlcRef, isPlaying, muted, videoOnlyMode,
|
|
220
222
|
volume, playbackRate, aspectRatio, title, posterUrl, isLive, isAudioOnly,
|
|
221
223
|
selectedAudioTrack, handleTracksChanged, handleProgress, handleNativePlaying,
|
|
222
224
|
handleWebBuffering, handleEpisodeEnded, handleWebError, togglePlayPause,
|
|
223
225
|
handleSeekByAction, handlePlaybackRoute, exoFallback, useExoForAndroidLocalMedia,
|
|
224
226
|
nativeSource, computedAspectRatio, handleNativeLoadStart,
|
|
225
|
-
handleNativeOpen, handleNativeBuffering,
|
|
227
|
+
handleNativeOpen, handleNativeBuffering,
|
|
226
228
|
} = props;
|
|
227
|
-
if (youtubeVideoId) {
|
|
228
|
-
return (
|
|
229
|
-
<YouTubeVideoPlayer
|
|
230
|
-
key={`youtube-${youtubeVideoId}`}
|
|
231
|
-
ref={vlcRef}
|
|
232
|
-
videoId={youtubeVideoId}
|
|
233
|
-
paused={!isPlaying}
|
|
234
|
-
muted={muted || videoOnlyMode}
|
|
235
|
-
volume={volume / 100}
|
|
236
|
-
playbackRate={playbackRate}
|
|
237
|
-
onReady={(event) => {
|
|
238
|
-
props.clearBufferingIndicator();
|
|
239
|
-
props.handleNativeOpen(event);
|
|
240
|
-
}}
|
|
241
|
-
onProgress={handleProgress}
|
|
242
|
-
onPlaying={handleNativePlaying}
|
|
243
|
-
onStateChange={(state) => handleYouTubePlaybackState(state, { isPlayingRef, setIsPlaying })}
|
|
244
|
-
onBuffering={handleWebBuffering}
|
|
245
|
-
onEnded={handleEpisodeEnded}
|
|
246
|
-
onError={handleWebError}
|
|
247
|
-
/>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
229
|
if (isElectron()) {
|
|
251
230
|
return (
|
|
252
231
|
<ElectronVideoPlayer
|
|
@@ -590,7 +569,7 @@ function FullscreenControlsPanel(props) {
|
|
|
590
569
|
duration={props.duration}
|
|
591
570
|
isAudioOnlyFeatureEnabled={props.isAudioOnlyFeatureEnabled}
|
|
592
571
|
isAudioOnly={props.isAudioOnly}
|
|
593
|
-
|
|
572
|
+
aspectRatios={props.aspectRatios}
|
|
594
573
|
controls={props.controls}
|
|
595
574
|
showAspectPicker={props.showAspectPicker}
|
|
596
575
|
aspectRatio={props.aspectRatio}
|
|
@@ -611,7 +590,6 @@ function FullscreenControlsPanel(props) {
|
|
|
611
590
|
onToggleAudioPicker={props.onToggleAudioPicker}
|
|
612
591
|
onSelectAudioTrack={props.handleAudioTrackAction}
|
|
613
592
|
onToggleFullscreen={props.handleFullscreenAction}
|
|
614
|
-
onToggleVideoOnly={props.handleVideoOnlyAction}
|
|
615
593
|
/>
|
|
616
594
|
</>
|
|
617
595
|
);
|
|
@@ -695,9 +673,10 @@ function FullscreenChatLayer(props) {
|
|
|
695
673
|
}
|
|
696
674
|
|
|
697
675
|
function FullscreenRecordingLayer(props) {
|
|
676
|
+
const recordingEligible = props.isLive;
|
|
698
677
|
return (
|
|
699
678
|
<>
|
|
700
|
-
{!props.isAudioOnly &&
|
|
679
|
+
{!props.isAudioOnly && recordingEligible && (props.isScreenRecorderEnabled || props.recStatus !== 'idle') ? (
|
|
701
680
|
<LiveRecordingOverlay
|
|
702
681
|
status={props.recStatus}
|
|
703
682
|
elapsedMs={props.recElapsedMs}
|
|
@@ -709,7 +688,7 @@ function FullscreenRecordingLayer(props) {
|
|
|
709
688
|
onStop={props.handleStopRecording}
|
|
710
689
|
/>
|
|
711
690
|
) : null}
|
|
712
|
-
{!props.isAudioOnly &&
|
|
691
|
+
{!props.isAudioOnly && recordingEligible && props.isScreenRecorderEnabled ? (
|
|
713
692
|
<LiveRecordingNotice notice={props.recNotice} colors={props.colors} onDismiss={props.onDismissNotice} />
|
|
714
693
|
) : null}
|
|
715
694
|
</>
|
|
@@ -761,7 +740,7 @@ function getInlinePreviewPositionStyle(rect, isValid) {
|
|
|
761
740
|
export const MediaPlayerView = ({
|
|
762
741
|
visible,
|
|
763
742
|
streamUrl,
|
|
764
|
-
|
|
743
|
+
onBack,
|
|
765
744
|
title,
|
|
766
745
|
mediaType = 'live',
|
|
767
746
|
onClose,
|
|
@@ -790,6 +769,9 @@ export const MediaPlayerView = ({
|
|
|
790
769
|
liveChatNonce = 0,
|
|
791
770
|
messagePageSize = 50,
|
|
792
771
|
drawerStyle,
|
|
772
|
+
aspectRatios,
|
|
773
|
+
defaultAspectRatio = DEFAULT_ASPECT_RATIO,
|
|
774
|
+
onAspectRatioChange,
|
|
793
775
|
genre,
|
|
794
776
|
categoryName,
|
|
795
777
|
controls = {},
|
|
@@ -805,6 +787,8 @@ export const MediaPlayerView = ({
|
|
|
805
787
|
style,
|
|
806
788
|
onReady,
|
|
807
789
|
onProgress,
|
|
790
|
+
showProgressBar = true,
|
|
791
|
+
onProgressBarChange,
|
|
808
792
|
onPlaying,
|
|
809
793
|
onBuffering,
|
|
810
794
|
onError,
|
|
@@ -830,12 +814,16 @@ export const MediaPlayerView = ({
|
|
|
830
814
|
const vlcRef = useRef(null);
|
|
831
815
|
const playerRef = useRef(null);
|
|
832
816
|
const invokeAction = useCallback((name, fallback, payload) => {
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
817
|
+
let callback = actions?.[name];
|
|
818
|
+
if (name === 'onAspectRatioChange') callback = callback || onAspectRatioChange;
|
|
819
|
+
if (name === 'onBack') callback = callback || onBack || onClose;
|
|
820
|
+
return invokePlayerAction(
|
|
821
|
+
fallback,
|
|
822
|
+
callback,
|
|
823
|
+
payload,
|
|
824
|
+
{ player: playerApiRef?.current || null },
|
|
825
|
+
);
|
|
826
|
+
}, [actions, onAspectRatioChange, onBack, onClose, playerApiRef]);
|
|
839
827
|
const handlePlayerHostRef = useCallback((node) => {
|
|
840
828
|
playerRef.current = node;
|
|
841
829
|
onPlayerHostRef?.(node);
|
|
@@ -981,8 +969,9 @@ export const MediaPlayerView = ({
|
|
|
981
969
|
useEffect(() => {
|
|
982
970
|
isLockedRef.current = isLocked;
|
|
983
971
|
}, [isLocked]);
|
|
984
|
-
const [aspectRatio, setAspectRatio] = useState(DEFAULT_ASPECT_RATIO);
|
|
985
|
-
const aspectRatioRef = useRef(DEFAULT_ASPECT_RATIO);
|
|
972
|
+
const [aspectRatio, setAspectRatio] = useState(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
973
|
+
const aspectRatioRef = useRef(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
974
|
+
const aspectOptions = useMemo(() => normalizeAspectOptions(aspectRatios), [aspectRatios]);
|
|
986
975
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
987
976
|
|
|
988
977
|
const [audioTracks, setAudioTracks] = useState([]);
|
|
@@ -1112,7 +1101,7 @@ export const MediaPlayerView = ({
|
|
|
1112
1101
|
let targetRatio = val;
|
|
1113
1102
|
if (val === 'FIT') {
|
|
1114
1103
|
targetRatio = 'FIT';
|
|
1115
|
-
} else if (val === 'FILL_SCREEN') {
|
|
1104
|
+
} else if (val === 'FILL_SCREEN' || val === 'FILL') {
|
|
1116
1105
|
targetRatio = calculateScreenAspectRatio(windowWidth, windowHeight);
|
|
1117
1106
|
}
|
|
1118
1107
|
|
|
@@ -1148,6 +1137,7 @@ export const MediaPlayerView = ({
|
|
|
1148
1137
|
const isSeeking = useRef(false);
|
|
1149
1138
|
const bufferingTimerRef = useRef(null);
|
|
1150
1139
|
const seekCompletedAt = useRef(0);
|
|
1140
|
+
const lastProgressBarSecondRef = useRef(null);
|
|
1151
1141
|
const lastKnownTimeRef = useRef(0);
|
|
1152
1142
|
const lastKnownDurRef = useRef(Number(durationSecs) || 0);
|
|
1153
1143
|
const hasResumedRef = useRef(false);
|
|
@@ -1190,6 +1180,11 @@ export const MediaPlayerView = ({
|
|
|
1190
1180
|
|
|
1191
1181
|
const isLive = mediaType === 'live' || mediaType === 'channel';
|
|
1192
1182
|
const isInlinePreview = !!inlinePreview;
|
|
1183
|
+
const progressBarVisible = showProgressBar !== false
|
|
1184
|
+
&& controls.seek !== false
|
|
1185
|
+
&& !isLive
|
|
1186
|
+
&& !isInlinePreview;
|
|
1187
|
+
const progressBarCallback = progressBarVisible ? onProgressBarChange : undefined;
|
|
1193
1188
|
|
|
1194
1189
|
useEffect(() => {
|
|
1195
1190
|
if (!visible || !initialShowLiveChat || !isLive || !isLiveCommentsEnabled) return;
|
|
@@ -1336,6 +1331,7 @@ export const MediaPlayerView = ({
|
|
|
1336
1331
|
setIsPlaying(!initialPaused);
|
|
1337
1332
|
isPlayingRef.current = !initialPaused;
|
|
1338
1333
|
setVideoOnlyMode(!!videoOnly);
|
|
1334
|
+
lastProgressBarSecondRef.current = null;
|
|
1339
1335
|
setPlaybackUrl(streamUrl || '');
|
|
1340
1336
|
setIsLocked(false);
|
|
1341
1337
|
setIsAudioOnly(!!initialAudioOnly);
|
|
@@ -1374,14 +1370,14 @@ export const MediaPlayerView = ({
|
|
|
1374
1370
|
// Reset aspect ratio & zoom state every time player opens or streamUrl changes
|
|
1375
1371
|
useEffect(() => {
|
|
1376
1372
|
if (visible) {
|
|
1377
|
-
setAspectRatio(DEFAULT_ASPECT_RATIO);
|
|
1378
|
-
aspectRatioRef.current = DEFAULT_ASPECT_RATIO;
|
|
1373
|
+
setAspectRatio(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
1374
|
+
aspectRatioRef.current = defaultAspectRatio || DEFAULT_ASPECT_RATIO;
|
|
1379
1375
|
setZoomScale(1);
|
|
1380
1376
|
zoomScaleRef.current = 1;
|
|
1381
1377
|
setZoomBadgeText('');
|
|
1382
1378
|
setSeekRipple(null);
|
|
1383
1379
|
}
|
|
1384
|
-
}, [visible, streamUrl]);
|
|
1380
|
+
}, [visible, streamUrl, defaultAspectRatio]);
|
|
1385
1381
|
|
|
1386
1382
|
// Auto-hide controls 4 seconds after inactivity
|
|
1387
1383
|
const scheduleHide = useCallback(() => {
|
|
@@ -1455,9 +1451,9 @@ export const MediaPlayerView = ({
|
|
|
1455
1451
|
|
|
1456
1452
|
const handleBackAction = useCallback(() => invokeAction(
|
|
1457
1453
|
'onBack',
|
|
1458
|
-
|
|
1454
|
+
undefined,
|
|
1459
1455
|
{ title, streamUrl, mediaId },
|
|
1460
|
-
), [invokeAction,
|
|
1456
|
+
), [invokeAction, title, streamUrl, mediaId]);
|
|
1461
1457
|
|
|
1462
1458
|
useEffect(() => {
|
|
1463
1459
|
if (!visible || isWeb()) return undefined;
|
|
@@ -1575,8 +1571,9 @@ export const MediaPlayerView = ({
|
|
|
1575
1571
|
setSliderPos(0);
|
|
1576
1572
|
lastKnownTimeRef.current = 0;
|
|
1577
1573
|
seekCompletedAt.current = Date.now();
|
|
1574
|
+
emitProgressBarTime(0, progressBarCallback, lastProgressBarSecondRef, { force: true });
|
|
1578
1575
|
if (!showControls) setShowControls(true);
|
|
1579
|
-
}, [showControls]);
|
|
1576
|
+
}, [progressBarCallback, showControls]);
|
|
1580
1577
|
|
|
1581
1578
|
// Episode finished: auto-advance to the next episode (sequential) or to a
|
|
1582
1579
|
// random episode (shuffle mode). Last episode in the playlist stops playback.
|
|
@@ -1775,6 +1772,7 @@ export const MediaPlayerView = ({
|
|
|
1775
1772
|
lastKnownTimeRef,
|
|
1776
1773
|
setSliderPosition: setSliderPos,
|
|
1777
1774
|
});
|
|
1775
|
+
if (progress) emitProgressBarTime(progress.seconds, progressBarCallback, lastProgressBarSecondRef);
|
|
1778
1776
|
};
|
|
1779
1777
|
|
|
1780
1778
|
const handleNativeOpen = (event) => {
|
|
@@ -1882,10 +1880,14 @@ export const MediaPlayerView = ({
|
|
|
1882
1880
|
setSliderPos(target);
|
|
1883
1881
|
seekCompletedAt.current = Date.now();
|
|
1884
1882
|
debouncedSaveProgress();
|
|
1883
|
+
emitProgressBarTime(target, progressBarCallback, lastProgressBarSecondRef, { force: true });
|
|
1885
1884
|
}, { seconds: Number(seconds) || 0, currentTime: lastKnownTimeRef.current },
|
|
1886
|
-
), [invokeAction, handleSeekTo, debouncedSaveProgress]);
|
|
1885
|
+
), [invokeAction, handleSeekTo, debouncedSaveProgress, progressBarCallback]);
|
|
1887
1886
|
const handleSeekByAction = (deltaSeconds) => invokeAction(
|
|
1888
|
-
'onSeek', () =>
|
|
1887
|
+
'onSeek', () => {
|
|
1888
|
+
handleSeekBy(deltaSeconds);
|
|
1889
|
+
emitProgressBarTime(lastKnownTimeRef.current, progressBarCallback, lastProgressBarSecondRef, { force: true });
|
|
1890
|
+
}, { deltaSeconds: Number(deltaSeconds) || 0, currentTime: lastKnownTimeRef.current },
|
|
1889
1891
|
);
|
|
1890
1892
|
const handleMuteAction = useCallback(() => invokeAction(
|
|
1891
1893
|
'onMute', toggleMute, { muted: !mutedRef.current },
|
|
@@ -1912,14 +1914,17 @@ export const MediaPlayerView = ({
|
|
|
1912
1914
|
event?.stopPropagation?.();
|
|
1913
1915
|
return invokeAction(name, fallback, { title, streamUrl, mediaId });
|
|
1914
1916
|
}, [invokeAction, title, streamUrl, mediaId]);
|
|
1915
|
-
const handlePanelAction = useCallback((tab) =>
|
|
1916
|
-
|
|
1917
|
-
(
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1917
|
+
const handlePanelAction = useCallback((tab) => {
|
|
1918
|
+
const isOpen = !(showLiveChat && drawerTab === tab);
|
|
1919
|
+
return invokeAction(
|
|
1920
|
+
getPanelActionName(tab),
|
|
1921
|
+
() => {
|
|
1922
|
+
if (isOpen) { setDrawerTab(tab); setShowLiveChat(true); }
|
|
1923
|
+
else setShowLiveChat(false);
|
|
1924
|
+
},
|
|
1925
|
+
{ tab, isOpen },
|
|
1926
|
+
);
|
|
1927
|
+
}, [invokeAction, showLiveChat, drawerTab]);
|
|
1923
1928
|
|
|
1924
1929
|
useEffect(() => {
|
|
1925
1930
|
if (!playerApiRef) return undefined;
|
|
@@ -1937,7 +1942,7 @@ export const MediaPlayerView = ({
|
|
|
1937
1942
|
seekTo: handleSeekTo,
|
|
1938
1943
|
seekBy: handleSeekBy,
|
|
1939
1944
|
setPlaybackRate: (rate) => setPlaybackRate(clampNumber(rate, 0.25, 4, 1)),
|
|
1940
|
-
back:
|
|
1945
|
+
back: handleBackAction,
|
|
1941
1946
|
getVideoElement: () => vlcRef.current?.getVideoElement?.() || null,
|
|
1942
1947
|
getAudioTracks: () => audioTracks,
|
|
1943
1948
|
};
|
|
@@ -1945,7 +1950,7 @@ export const MediaPlayerView = ({
|
|
|
1945
1950
|
return () => {
|
|
1946
1951
|
for (const key of Object.keys(api)) delete playerApiRef.current[key];
|
|
1947
1952
|
};
|
|
1948
|
-
}, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy,
|
|
1953
|
+
}, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy, handleBackAction, audioTracks]);
|
|
1949
1954
|
|
|
1950
1955
|
const handleSpeedSelect = useCallback((speed) => invokeAction(
|
|
1951
1956
|
'onPlaybackRateChange', () => {
|
|
@@ -2459,7 +2464,6 @@ export const MediaPlayerView = ({
|
|
|
2459
2464
|
|
|
2460
2465
|
const videoPlayer = (
|
|
2461
2466
|
<PlatformMediaSurface
|
|
2462
|
-
youtubeVideoId={youtubeVideoId}
|
|
2463
2467
|
playerStreamUrl={playerStreamUrl}
|
|
2464
2468
|
vlcRef={vlcRef}
|
|
2465
2469
|
isPlaying={isPlaying}
|
|
@@ -2488,11 +2492,8 @@ export const MediaPlayerView = ({
|
|
|
2488
2492
|
computedAspectRatio={computedAspectRatio}
|
|
2489
2493
|
handleNativeLoadStart={handleNativeLoadStart}
|
|
2490
2494
|
handleNativeOpen={handleNativeOpen}
|
|
2491
|
-
clearBufferingIndicator={clearBufferingIndicator}
|
|
2492
2495
|
handleClose={handleClose}
|
|
2493
2496
|
handleNativeBuffering={handleNativeBuffering}
|
|
2494
|
-
isPlayingRef={isPlayingRef}
|
|
2495
|
-
setIsPlaying={setIsPlaying}
|
|
2496
2497
|
/>
|
|
2497
2498
|
);
|
|
2498
2499
|
const isValidPreviewRect = isUsableInlinePreviewRect(inlinePreviewRect);
|
|
@@ -2568,7 +2569,7 @@ export const MediaPlayerView = ({
|
|
|
2568
2569
|
isEpgEnabled,
|
|
2569
2570
|
diagnosticsOverlayEnabled,
|
|
2570
2571
|
muted,
|
|
2571
|
-
controls,
|
|
2572
|
+
controls: { ...controls, seek: progressBarVisible },
|
|
2572
2573
|
onClose: handleBackAction,
|
|
2573
2574
|
handleRecordingAction,
|
|
2574
2575
|
handleStartRecording,
|
|
@@ -2591,6 +2592,7 @@ export const MediaPlayerView = ({
|
|
|
2591
2592
|
videoOnlyMode,
|
|
2592
2593
|
showAspectPicker,
|
|
2593
2594
|
aspectRatio,
|
|
2595
|
+
aspectRatios: aspectOptions,
|
|
2594
2596
|
showSpeedPicker,
|
|
2595
2597
|
playbackRate,
|
|
2596
2598
|
showAudioPicker,
|
package/src/native/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
|
|
3
3
|
import { PlayerCustomizationProvider } from './customization';
|
|
4
4
|
import { MediaPlayerView } from './MediaPlayerView';
|
|
5
5
|
import { isElectron, isWeb } from '../utils/runtimePlatform';
|
|
6
|
-
import {
|
|
6
|
+
import { useResolvedPlayerSource } from '../utils/sourceUtils';
|
|
7
7
|
|
|
8
8
|
function getRuntimePlatform() {
|
|
9
9
|
if (isWeb()) return 'web';
|
|
@@ -50,8 +50,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
50
50
|
const streamUrl = String(media.uri || media.url || '');
|
|
51
51
|
const resolvedIsLive = isLive ?? media.isLive ?? (media.mediaType === 'live');
|
|
52
52
|
const resolvedMediaType = mediaType || media.mediaType || (resolvedIsLive ? 'live' : 'movie');
|
|
53
|
-
const
|
|
54
|
-
const isVisible = visible ?? Boolean(streamUrl || youtubeVideoId);
|
|
53
|
+
const isVisible = visible ?? Boolean(streamUrl);
|
|
55
54
|
|
|
56
55
|
React.useEffect(() => {
|
|
57
56
|
if (resolution.error) props.onError?.(resolution.error);
|
|
@@ -75,11 +74,11 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
75
74
|
...metadata,
|
|
76
75
|
visible: isVisible,
|
|
77
76
|
streamUrl,
|
|
78
|
-
|
|
77
|
+
onBack,
|
|
79
78
|
title: title || media.title || '',
|
|
80
79
|
posterUrl: poster || posterUrl || media.poster || media.posterUrl || '',
|
|
81
80
|
mediaType: resolvedMediaType,
|
|
82
|
-
onClose: onClose ||
|
|
81
|
+
onClose: onClose || (() => {}),
|
|
83
82
|
initialPaused: paused ?? initialPaused ?? !autoPlay,
|
|
84
83
|
initialMuted: muted ?? initialMuted ?? false,
|
|
85
84
|
initialAudioOnly: audioOnly,
|
|
@@ -124,7 +124,6 @@ function LiveChatPanel({
|
|
|
124
124
|
messages,
|
|
125
125
|
flatListRef,
|
|
126
126
|
renderMessageItem,
|
|
127
|
-
hasMoreMessages,
|
|
128
127
|
loadOlderMessages,
|
|
129
128
|
loadingOlderMessages,
|
|
130
129
|
chatError,
|
|
@@ -161,19 +160,16 @@ function LiveChatPanel({
|
|
|
161
160
|
contentContainerStyle={styles.messagesList}
|
|
162
161
|
showsVerticalScrollIndicator
|
|
163
162
|
keyboardShouldPersistTaps="handled"
|
|
164
|
-
ListHeaderComponent={
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
accessibilityRole="button"
|
|
170
|
-
>
|
|
171
|
-
{loadingOlderMessages ? <ActivityIndicator size="small" color="#00E5FF" /> : null}
|
|
172
|
-
<Text style={styles.loadMoreMessagesText}>
|
|
173
|
-
{loadingOlderMessages ? 'Loading older messages…' : 'See more messages'}
|
|
174
|
-
</Text>
|
|
175
|
-
</TouchableOpacity>
|
|
163
|
+
ListHeaderComponent={loadingOlderMessages ? (
|
|
164
|
+
<View style={styles.loadingOlderMessages}>
|
|
165
|
+
<ActivityIndicator size="small" color="#00E5FF" />
|
|
166
|
+
<Text style={styles.loadingOlderMessagesText}>Loading older messages…</Text>
|
|
167
|
+
</View>
|
|
176
168
|
) : null}
|
|
169
|
+
onScroll={(event) => {
|
|
170
|
+
if (event.nativeEvent.contentOffset.y <= 24) loadOlderMessages();
|
|
171
|
+
}}
|
|
172
|
+
scrollEventThrottle={16}
|
|
177
173
|
ListEmptyComponent={<Text style={styles.epgStatusText}>No messages yet. Start the conversation.</Text>}
|
|
178
174
|
/>
|
|
179
175
|
)}
|
|
@@ -670,7 +666,6 @@ export const LiveChatDrawer = ({
|
|
|
670
666
|
messages={messages}
|
|
671
667
|
flatListRef={flatListRef}
|
|
672
668
|
renderMessageItem={renderMessageItem}
|
|
673
|
-
hasMoreMessages={hasMoreMessages}
|
|
674
669
|
loadOlderMessages={loadOlderMessages}
|
|
675
670
|
loadingOlderMessages={loadingOlderMessages}
|
|
676
671
|
chatError={chatError}
|
|
@@ -994,22 +989,18 @@ const styles = StyleSheet.create({
|
|
|
994
989
|
paddingVertical: 8,
|
|
995
990
|
gap: 8,
|
|
996
991
|
},
|
|
997
|
-
|
|
998
|
-
minHeight:
|
|
992
|
+
loadingOlderMessages: {
|
|
993
|
+
minHeight: 36,
|
|
999
994
|
flexDirection: 'row',
|
|
1000
995
|
alignItems: 'center',
|
|
1001
996
|
justifyContent: 'center',
|
|
1002
|
-
gap:
|
|
1003
|
-
|
|
1004
|
-
borderRadius: 12,
|
|
1005
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
1006
|
-
borderColor: 'rgba(0, 229, 255, 0.35)',
|
|
1007
|
-
backgroundColor: 'rgba(0, 229, 255, 0.08)',
|
|
997
|
+
gap: 7,
|
|
998
|
+
paddingVertical: 6,
|
|
1008
999
|
},
|
|
1009
|
-
|
|
1000
|
+
loadingOlderMessagesText: {
|
|
1010
1001
|
color: '#00E5FF',
|
|
1011
|
-
fontSize:
|
|
1012
|
-
fontWeight: '
|
|
1002
|
+
fontSize: 12,
|
|
1003
|
+
fontWeight: '600',
|
|
1013
1004
|
},
|
|
1014
1005
|
chatErrorText: {
|
|
1015
1006
|
color: '#FF7A8A',
|
|
@@ -39,6 +39,9 @@ export const WebVideoPlayer = forwardRef(({
|
|
|
39
39
|
const playbackErrorRef = useRef(onError);
|
|
40
40
|
const onEndedRef = useRef(onEnded);
|
|
41
41
|
const [activeUrl, setActiveUrl] = useState('');
|
|
42
|
+
const [corsMode, setCorsMode] = useState('anonymous');
|
|
43
|
+
const corsModeRef = useRef('anonymous');
|
|
44
|
+
corsModeRef.current = corsMode;
|
|
42
45
|
pausedRef.current = paused;
|
|
43
46
|
playbackRateRef.current = playbackRate;
|
|
44
47
|
onPlaybackRouteRef.current = onPlaybackRoute;
|
|
@@ -126,6 +129,8 @@ export const WebVideoPlayer = forwardRef(({
|
|
|
126
129
|
return undefined;
|
|
127
130
|
}
|
|
128
131
|
setActiveUrl(streamUrl);
|
|
132
|
+
setCorsMode('anonymous');
|
|
133
|
+
corsModeRef.current = 'anonymous';
|
|
129
134
|
return undefined;
|
|
130
135
|
}, [streamUrl]);
|
|
131
136
|
|
|
@@ -243,6 +248,7 @@ export const WebVideoPlayer = forwardRef(({
|
|
|
243
248
|
muted={muted}
|
|
244
249
|
playsInline
|
|
245
250
|
controls={false}
|
|
251
|
+
crossOrigin={corsMode}
|
|
246
252
|
style={videoStyle}
|
|
247
253
|
onPlaying={() => {
|
|
248
254
|
const video = videoRef.current;
|
|
@@ -280,6 +286,13 @@ export const WebVideoPlayer = forwardRef(({
|
|
|
280
286
|
if (video && video.readyState >= 2) {
|
|
281
287
|
return;
|
|
282
288
|
}
|
|
289
|
+
// If we were trying anonymous CORS and it failed, retry without.
|
|
290
|
+
// Playback will work; recording will fall back to screen capture.
|
|
291
|
+
if (corsModeRef.current === 'anonymous') {
|
|
292
|
+
setCorsMode(undefined);
|
|
293
|
+
corsModeRef.current = undefined;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
283
296
|
playbackErrorRef.current?.({
|
|
284
297
|
message: mediaError?.message || 'Failed to load stream.',
|
|
285
298
|
err: mediaError || e,
|
|
@@ -48,23 +48,6 @@ function SoundWave({ isPlaying, isLandscape, accentColor }) {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function AudioOnlyBackdrop({ posterUrl }) {
|
|
52
|
-
return (
|
|
53
|
-
<>
|
|
54
|
-
{posterUrl ? (
|
|
55
|
-
<Image
|
|
56
|
-
source={{ uri: posterUrl }}
|
|
57
|
-
style={[StyleSheet.absoluteFill, styles.audioOnlyBackdropImage]}
|
|
58
|
-
resizeMode="cover"
|
|
59
|
-
blurRadius={isWeb() ? 40 : 25}
|
|
60
|
-
pointerEvents="none"
|
|
61
|
-
/>
|
|
62
|
-
) : null}
|
|
63
|
-
<View style={styles.audioOnlyBackdropDim} pointerEvents="none" />
|
|
64
|
-
</>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
51
|
function AudioOnlyBadge({ usesAudioProxy, palette, isLandscape }) {
|
|
69
52
|
const message = usesAudioProxy
|
|
70
53
|
? 'Battery Saver Audio Mode • Screen can be locked'
|
|
@@ -101,10 +84,9 @@ export const AudioOnlyView = ({
|
|
|
101
84
|
);
|
|
102
85
|
|
|
103
86
|
return (
|
|
104
|
-
<View style={
|
|
105
|
-
<AudioOnlyBackdrop posterUrl={posterUrl} />
|
|
87
|
+
<View style={styles.audioOnlyContainer} pointerEvents="auto">
|
|
106
88
|
|
|
107
|
-
<View style={[styles.audioOnlyCard, isLandscape && styles.audioOnlyCardLandscape, {
|
|
89
|
+
<View style={[styles.audioOnlyCard, isLandscape && styles.audioOnlyCardLandscape, { borderColor: palette.accentColor }]} pointerEvents="auto">
|
|
108
90
|
<AudioArtwork posterUrl={posterUrl} isLandscape={isLandscape} palette={palette} />
|
|
109
91
|
|
|
110
92
|
<SoundWave isPlaying={isPlaying} isLandscape={isLandscape} accentColor={palette.accentColor} />
|
|
@@ -153,25 +135,6 @@ const styles = StyleSheet.create({
|
|
|
153
135
|
zIndex: 50,
|
|
154
136
|
elevation: 50,
|
|
155
137
|
},
|
|
156
|
-
audioOnlyBackdropImage: {
|
|
157
|
-
position: 'absolute',
|
|
158
|
-
top: 0,
|
|
159
|
-
left: 0,
|
|
160
|
-
right: 0,
|
|
161
|
-
bottom: 0,
|
|
162
|
-
width: '100%',
|
|
163
|
-
height: '100%',
|
|
164
|
-
},
|
|
165
|
-
audioOnlyBackdropDim: {
|
|
166
|
-
position: 'absolute',
|
|
167
|
-
top: 0,
|
|
168
|
-
left: 0,
|
|
169
|
-
right: 0,
|
|
170
|
-
bottom: 0,
|
|
171
|
-
width: '100%',
|
|
172
|
-
height: '100%',
|
|
173
|
-
backgroundColor: 'rgba(7, 9, 14, 0.88)',
|
|
174
|
-
},
|
|
175
138
|
audioOnlyCard: {
|
|
176
139
|
alignItems: 'center',
|
|
177
140
|
justifyContent: 'center',
|
|
@@ -180,7 +143,7 @@ const styles = StyleSheet.create({
|
|
|
180
143
|
maxWidth: 440,
|
|
181
144
|
maxHeight: '92%',
|
|
182
145
|
borderRadius: 20,
|
|
183
|
-
backgroundColor: '
|
|
146
|
+
backgroundColor: '#0B1220',
|
|
184
147
|
borderWidth: 1,
|
|
185
148
|
borderColor: 'rgba(0, 229, 255, 0.25)',
|
|
186
149
|
shadowColor: '#000',
|
|
@@ -21,7 +21,7 @@ function SeekControls({ isLive, controls, insets, scale, isSeeking, sliderPos, c
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function AspectRatioControl({ controls, open, aspectRatio, onToggle, onSelect }) {
|
|
24
|
+
function AspectRatioControl({ controls, open, aspectRatio, aspectRatios = ASPECT_OPTIONS, onToggle, onSelect }) {
|
|
25
25
|
const palette = usePlayerColors();
|
|
26
26
|
if (controls.aspectRatio === false) return null;
|
|
27
27
|
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
@@ -29,7 +29,7 @@ function AspectRatioControl({ controls, open, aspectRatio, onToggle, onSelect })
|
|
|
29
29
|
<View style={styles.speedButtonContainer}>
|
|
30
30
|
{open ? <View style={[styles.speedPickerPopup, { right: 'auto', left: 0, backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
31
31
|
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Aspect Ratio</Text>
|
|
32
|
-
{
|
|
32
|
+
{aspectRatios.map((option) => {
|
|
33
33
|
const selected = aspectRatio === option.value;
|
|
34
34
|
return <TouchableOpacity key={option.label} style={styles.speedOption} onPress={(event) => { event.stopPropagation(); onSelect(option.value); }} activeOpacity={0.7}>
|
|
35
35
|
<PlayerIcon name="check" size={15} color={selected ? palette.accentColor : 'transparent'} />
|
|
@@ -50,12 +50,6 @@ function AudioOnlyModeControl({ isAudioOnly, onToggle }) {
|
|
|
50
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
51
|
}
|
|
52
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 ? 'Disable video-only mode' : 'Video only'}><PlayerIcon name="video" size={18} color={isVideoOnly ? palette.accentColor : palette.controlColor} /></TouchableOpacity>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
53
|
function SpeedControl({ enabled, open, playbackRate, onToggle, onSelect }) {
|
|
60
54
|
const palette = usePlayerColors();
|
|
61
55
|
if (!enabled) return null;
|
|
@@ -129,6 +123,7 @@ export const PlayerBottomBar = ({
|
|
|
129
123
|
isAudioOnly,
|
|
130
124
|
showAspectPicker,
|
|
131
125
|
aspectRatio,
|
|
126
|
+
aspectRatios = ASPECT_OPTIONS,
|
|
132
127
|
showSpeedPicker,
|
|
133
128
|
playbackRate,
|
|
134
129
|
showAudioPicker,
|
|
@@ -147,8 +142,6 @@ export const PlayerBottomBar = ({
|
|
|
147
142
|
onSelectAudioTrack,
|
|
148
143
|
onToggleFullscreen,
|
|
149
144
|
controls = {},
|
|
150
|
-
isVideoOnly = false,
|
|
151
|
-
onToggleVideoOnly,
|
|
152
145
|
}) => {
|
|
153
146
|
return (
|
|
154
147
|
<View
|
|
@@ -167,8 +160,7 @@ export const PlayerBottomBar = ({
|
|
|
167
160
|
<View style={styles.bottomControlsRow} pointerEvents="box-none">
|
|
168
161
|
<View style={styles.leftActionsContainer}>
|
|
169
162
|
{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}
|
|
163
|
+
<AspectRatioControl controls={controls} open={showAspectPicker} aspectRatio={aspectRatio} aspectRatios={aspectRatios} onToggle={onToggleAspectPicker} onSelect={onSelectAspectRatio} />
|
|
172
164
|
</View>
|
|
173
165
|
|
|
174
166
|
{/* RIGHT ACTIONS: Playback Speed, Audio, Fullscreen */}
|
|
@@ -38,8 +38,9 @@ export function nearestPipCorner(x, y, winW, winH, insets) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export const ASPECT_OPTIONS = [
|
|
41
|
-
{ label: 'Fit
|
|
42
|
-
{ label: 'Fill
|
|
41
|
+
{ label: 'Fit', value: 'FIT' },
|
|
42
|
+
{ label: 'Fill', value: 'FILL' },
|
|
43
|
+
{ label: 'Stretch', value: 'STRETCH' },
|
|
43
44
|
{ label: '16:9', value: '16:9' },
|
|
44
45
|
{ label: '4:3', value: '4:3' },
|
|
45
46
|
{ label: '21:9', value: '21:9' },
|
|
@@ -156,4 +157,3 @@ export function applyTrackDefaults(selected, setSelected, setTracks, tracksProp)
|
|
|
156
157
|
const active = tracksProp.find((t) => t.selected || t.active) || tracksProp[0];
|
|
157
158
|
if (active) setSelected(active.id);
|
|
158
159
|
}
|
|
159
|
-
|