@cinecrew/cinecrew-player 0.1.4 → 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/src/web/index.js CHANGED
@@ -3,10 +3,9 @@ import { useWebVideoAspectRatio } from '../native/media/web/useWebVideoAspectRat
3
3
  import { useWebMpegTsPlayback } from '../native/media/web/useWebMpegTsPlayback.web';
4
4
  import { useWebHlsPlayback } from '../native/media/web/useWebHlsPlayback.web';
5
5
  import { useWebAc3AudioPlayback } from '../native/media/web/useWebAc3AudioPlayback.web';
6
- import { YouTubeVideoPlayer } from '../native/media/YouTubeVideoPlayer.web.js';
7
- import { getYouTubeVideoId, getWebRuntimePlatform, useResolvedPlayerSource } from '../utils/sourceUtils';
6
+ import { getWebRuntimePlatform, useResolvedPlayerSource } from '../utils/sourceUtils';
8
7
  import { EMOJI_GROUPS, searchEmojis } from '../data/emoji';
9
- import { createRecordingDownloadLink, createVideoRecordingStream, downloadRecording, getRecordingMimeType } from '../utils/webRecording';
8
+ import { createRecordingDownloadLink, createVideoRecordingStream, createScreenRecordingStream, downloadRecording, getRecordingMimeType } from '../utils/webRecording';
10
9
  import { invokePlayerAction } from '../utils/invokePlayerAction.js';
11
10
  import { emitProgressBarTime } from '../utils/progressBarTime.js';
12
11
  import './styles.css';
@@ -250,13 +249,14 @@ function WebPlaybackRateControl({ value, onChange }) {
250
249
  function WebBottomControls(props) {
251
250
  const {
252
251
  isLive, overrides, theme, icons, currentTime, duration, seekTo,
252
+ progressBarVisible, inlinePreview, title,
253
253
  showAspectMenu, setShowAspectMenu, selectAspect, aspectRatios, selectedAspectRatio,
254
254
  audioOnly, setAudioOnlyMode, availableTracks, selectedAudioTrack,
255
255
  showAudioMenu, setShowAudioMenu, selectAudio, playbackRate,
256
256
  setPlaybackRateAction, fullscreen, toggleFullscreen,
257
257
  } = props;
258
258
  let seek = null;
259
- if (duration > 0 && isControlEnabled(overrides, 'seek', true)) {
259
+ if (duration > 0 && progressBarVisible && isControlEnabled(overrides, 'seek', true)) {
260
260
  seek = h(WebSeekControl, { currentTime, duration, theme, onSeek: seekTo });
261
261
  }
262
262
  let aspect = null;
@@ -300,6 +300,7 @@ function WebBottomControls(props) {
300
300
  seek,
301
301
  h('div', { className: 'cinecrew-player__bottom-actions' },
302
302
  h('div', { className: 'cinecrew-player__bottom-left-actions' },
303
+ inlinePreview && title ? h('span', { className: 'cinecrew-player__inline-title', title }, title) : null,
303
304
  renderControlButton({ name: 'audioOnly', label: audioOnlyLabel, callback: () => setAudioOnlyMode(true), options: { active: audioOnly }, overrides, icons, theme }),
304
305
  aspect),
305
306
  h('div', { className: 'cinecrew-player__bottom-right-actions' },
@@ -351,7 +352,7 @@ function WebRecordingOverlay(props) {
351
352
  }, getRecordingOverlayContent(props), dismissButton);
352
353
  }
353
354
 
354
- function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlockedControls, toggleLock, paused, title, togglePlay, bottomProps }) {
355
+ function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlockedControls, toggleLock, paused, title, inlinePreview, togglePlay, bottomProps }) {
355
356
  let leftControls = locked ? null : unlockedControls.left;
356
357
  let rightControls = unlockedControls.right;
357
358
  if (locked) {
@@ -375,7 +376,7 @@ function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlocke
375
376
  return h('div', { className: `cinecrew-player__controls${recordingUiVisible ? ' is-recording' : ''}`, style: { color: theme.controlColor } },
376
377
  h('div', { className: 'cinecrew-player__top-controls' },
377
378
  h('div', { className: 'cinecrew-player__top-left-actions' }, leftControls),
378
- !locked && paused && title ? h('div', { className: 'cinecrew-player__title', style: { color: theme.controlColor }, title }, title) : null,
379
+ !locked && !inlinePreview && paused && title ? h('div', { className: 'cinecrew-player__title', style: { color: theme.controlColor }, title }, title) : null,
379
380
  h('div', { className: 'cinecrew-player__top-right-actions' }, rightControls)),
380
381
  centerControls,
381
382
  h(WebRecordingOverlay, {
@@ -394,10 +395,9 @@ function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlocke
394
395
  }
395
396
 
396
397
  function WebPlayerSurface({
397
- youtubeVideoId,
398
+ title,
398
399
  streamUrl,
399
400
  videoRef,
400
- youtubeRef,
401
401
  directVideoSource,
402
402
  poster,
403
403
  autoPlay,
@@ -416,28 +416,10 @@ function WebPlayerSurface({
416
416
  onProgress,
417
417
  onPlaying,
418
418
  onEnded,
419
- onStateChange,
420
419
  onError,
421
420
  handleError,
421
+ corsMode,
422
422
  }) {
423
- if (youtubeVideoId) {
424
- return h(YouTubeVideoPlayer, {
425
- ref: youtubeRef,
426
- videoId: youtubeVideoId,
427
- paused,
428
- muted: muted || videoOnly,
429
- volume,
430
- playbackRate,
431
- onReady,
432
- onProgress,
433
- onPlaying,
434
- onBuffering: bufferingRef.current,
435
- onStateChange,
436
- onError,
437
- onEnded,
438
- style: drawerResize ? { width: 'var(--cinecrew-media-width, 64%)', height: '100%', inset: '0 auto 0 0' } : undefined,
439
- });
440
- }
441
423
  if (!streamUrl) return h('div', { className: 'cinecrew-player__empty' });
442
424
  let resizedVideoStyle = {};
443
425
  if (drawerResize && videoStyle.width === 'auto') {
@@ -469,6 +451,7 @@ function WebPlayerSurface({
469
451
  muted: muted || videoOnly,
470
452
  playsInline: true,
471
453
  preload: 'auto',
454
+ crossOrigin: corsMode,
472
455
  style: {
473
456
  ...videoStyle,
474
457
  ...resizedVideoStyle,
@@ -478,7 +461,7 @@ function WebPlayerSurface({
478
461
  onError: (event) => {
479
462
  if (!directVideoSource) return;
480
463
  const mediaError = event.currentTarget?.error;
481
- handleError({ message: mediaError?.message || 'The browser could not load this stream. Check URL, codec and CORS support.', code: mediaError?.code, cause: mediaError });
464
+ handleError({ message: mediaError?.message || 'The browser could not load this stream. Check URL, codec and CORS support.', code: mediaError?.code, cause: mediaError, isCorsCandidate: corsMode === 'anonymous' });
482
465
  },
483
466
  });
484
467
  }
@@ -486,7 +469,7 @@ function WebPlayerSurface({
486
469
  function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo }) {
487
470
  return h('div', {
488
471
  className: 'cinecrew-player__audio-card',
489
- style: { background: theme.surfaceColor, color: theme.controlColor, borderColor: theme.accentColor },
472
+ style: { background: '#07090E', color: theme.controlColor, borderColor: theme.accentColor },
490
473
  },
491
474
  poster
492
475
  ? h('img', { className: 'cinecrew-player__audio-poster', src: poster, alt: '' })
@@ -524,10 +507,6 @@ function getPanelIntegration(activePanel, integrations) {
524
507
  return null;
525
508
  }
526
509
 
527
- function getActiveMediaUrl(youtubeVideoId, streamUrl) {
528
- return youtubeVideoId ? '' : streamUrl;
529
- }
530
-
531
510
  function getInlinePlayerStyle(inlinePreview, rect) {
532
511
  if (!inlinePreview || !rect) return {};
533
512
  return { position: 'absolute', left: rect.x, top: rect.y, width: rect.width, height: rect.height };
@@ -646,6 +625,7 @@ function WebPlayerLayout(props) {
646
625
  toggleLock: props.toggleLock,
647
626
  paused: props.isPaused,
648
627
  title: props.title,
628
+ inlinePreview: props.inlinePreview,
649
629
  togglePlay: props.togglePlay,
650
630
  bottomProps: props.bottomControlProps,
651
631
  });
@@ -683,8 +663,7 @@ function WebPlayerLayout(props) {
683
663
  panelNode);
684
664
  }
685
665
 
686
- function getStreamMode(youtubeVideoId, mpegTs, useHls) {
687
- if (youtubeVideoId) return 'youtube';
666
+ function getStreamMode(mpegTs, useHls) {
688
667
  if (mpegTs) return 'mpegts';
689
668
  if (useHls) return 'hls';
690
669
  return 'native';
@@ -1157,6 +1136,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1157
1136
  volume: volumeProp = 1,
1158
1137
  playbackRate: playbackRateProp = 1,
1159
1138
  paused: pausedProp,
1139
+ showProgressBar = true,
1160
1140
  controls: controlOverrides = {},
1161
1141
  features = {},
1162
1142
  drawerMode = 'overlay',
@@ -1197,11 +1177,13 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1197
1177
  const resolution = useResolvedPlayerSource(source, url, resolveSource, getWebRuntimePlatform());
1198
1178
  const media = resolution.source || {};
1199
1179
  const streamUrl = String(media.uri || media.url || '');
1200
- const youtubeVideoId = getYouTubeVideoId(media);
1201
1180
  const isLive = liveProp ?? media.isLive ?? false;
1181
+ const progressBarVisible = showProgressBar !== false
1182
+ && isControlEnabled(controlOverrides, 'seek', true)
1183
+ && !isLive;
1184
+ const progressBarCallback = progressBarVisible ? onProgressBarChange : undefined;
1202
1185
  const theme = { ...DEFAULT_THEME, ...themeProp };
1203
1186
  const videoRef = useRef(null);
1204
- const youtubeRef = useRef(null);
1205
1187
  const playerRef = useRef(null);
1206
1188
  const publicPlayerRef = useRef(null);
1207
1189
  const pausedRef = useRef(pausedProp ?? !autoPlay);
@@ -1214,6 +1196,9 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1214
1196
  const [aspectRatio, setAspectRatio] = useState(props.defaultAspectRatio || 'FIT');
1215
1197
  const [locked, setLocked] = useState(false);
1216
1198
  const [buffering, setBuffering] = useState(Boolean(streamUrl) || resolution.loading);
1199
+ const [corsMode, setCorsMode] = useState('anonymous');
1200
+ const corsModeRef = useRef('anonymous');
1201
+ corsModeRef.current = corsMode;
1217
1202
  const [error, setError] = useState('');
1218
1203
  const [currentTime, setCurrentTime] = useState(0);
1219
1204
  const [duration, setDuration] = useState(0);
@@ -1246,6 +1231,13 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1246
1231
 
1247
1232
  const handleError = useCallback((detail) => {
1248
1233
  const message = typeof detail === 'string' ? detail : detail?.message || 'Unable to play this media source.';
1234
+ // If the video failed while crossOrigin="anonymous" was set, retry without
1235
+ // CORS so playback still works (recording will fall back to screen capture).
1236
+ if (detail?.isCorsCandidate && corsModeRef.current === 'anonymous') {
1237
+ setCorsMode(undefined);
1238
+ corsModeRef.current = undefined;
1239
+ return;
1240
+ }
1249
1241
  setError(message);
1250
1242
  setBuffering(false);
1251
1243
  errorRef.current?.(detail instanceof Error ? detail : { ...detail, message });
@@ -1268,6 +1260,8 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1268
1260
  setDuration(0);
1269
1261
  lastProgressBarSecondRef.current = null;
1270
1262
  setAvailableTracks([]);
1263
+ setCorsMode('anonymous');
1264
+ corsModeRef.current = 'anonymous';
1271
1265
  if (resolution.error) {
1272
1266
  handleError(resolution.error);
1273
1267
  return;
@@ -1306,7 +1300,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1306
1300
  if (liveChatNonce) setActivePanel('chat');
1307
1301
  }, [liveChatNonce]);
1308
1302
 
1309
- const activeUrl = getActiveMediaUrl(youtubeVideoId, streamUrl);
1303
+ const activeUrl = streamUrl;
1310
1304
 
1311
1305
  const mpegTs = useWebMpegTsPlayback({
1312
1306
  streamUrl,
@@ -1325,7 +1319,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1325
1319
  pausedRef: pausedStateRef,
1326
1320
  onErrorRef,
1327
1321
  });
1328
- const streamMode = getStreamMode(youtubeVideoId, mpegTs.useMpegTs, useHls);
1322
+ const streamMode = getStreamMode(mpegTs.useMpegTs, useHls);
1329
1323
  const ac3AudioPlayback = useWebAc3AudioPlayback({
1330
1324
  active: mpegTs.useAc3Fallback,
1331
1325
  streamUrl,
@@ -1344,7 +1338,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1344
1338
  handleError(playError);
1345
1339
  }
1346
1340
  });
1347
- }, [isPaused, streamUrl, youtubeVideoId, handleError]);
1341
+ }, [isPaused, streamUrl, handleError]);
1348
1342
 
1349
1343
  useEffect(() => {
1350
1344
  const video = videoRef.current;
@@ -1352,7 +1346,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1352
1346
  video.muted = muted || videoOnly;
1353
1347
  video.volume = volume;
1354
1348
  video.playbackRate = Number(playbackRate) || 1;
1355
- }, [muted, volume, playbackRate, videoOnly, streamUrl, youtubeVideoId]);
1349
+ }, [muted, volume, playbackRate, videoOnly, streamUrl]);
1356
1350
 
1357
1351
  useEffect(() => {
1358
1352
  if (!streamUrl) return undefined;
@@ -1370,20 +1364,25 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1370
1364
  let callback = actions?.[name];
1371
1365
  if (name === 'onAspectRatioChange') callback = callback || props.onAspectRatioChange;
1372
1366
  else if (name === 'onFullscreen') callback = callback || onFullscreen;
1367
+ else if (name === 'onBack') callback = callback || onBack || props.onClose;
1373
1368
  return invokePlayerAction(
1374
1369
  fallback,
1375
1370
  callback,
1376
1371
  payload,
1377
1372
  { video: videoRef.current, player: publicPlayerRef.current },
1378
1373
  );
1379
- }, [actions, props.onAspectRatioChange, onFullscreen]);
1374
+ }, [actions, props.onAspectRatioChange, onFullscreen, onBack, props.onClose]);
1380
1375
 
1381
1376
  const startBuiltinRecording = useCallback(async () => {
1382
1377
  const video = videoRef.current;
1383
- if (!video || youtubeVideoId) throw new Error('This source cannot be recorded from the browser player.');
1384
1378
  const mimeType = getRecordingMimeType();
1385
1379
  if (typeof MediaRecorder === 'undefined' || !mimeType) throw new Error('This browser does not support WebM audio/video recording.');
1386
- const capture = createVideoRecordingStream(video, playerRef.current, () => aspectRatioRef.current, ac3AudioPlayback.getRecordingAudioStream());
1380
+ if (!video) throw new Error('The media element is not ready to record.');
1381
+ let capture = createVideoRecordingStream(video, playerRef.current, () => aspectRatioRef.current, ac3AudioPlayback.getRecordingAudioStream());
1382
+ // Fallback: if direct capture failed (CORS / tainted), use screen capture.
1383
+ if (!capture) {
1384
+ capture = await createScreenRecordingStream();
1385
+ }
1387
1386
  const recorder = new MediaRecorder(capture.stream, { mimeType });
1388
1387
  recordingCaptureRef.current = capture;
1389
1388
  recordingChunksRef.current = [];
@@ -1432,7 +1431,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1432
1431
  setRecordingError(event.error?.message || 'The browser could not record this stream.');
1433
1432
  }, { once: true });
1434
1433
  recorder.start(1000);
1435
- }, [ac3AudioPlayback, title, youtubeVideoId]);
1434
+ }, [ac3AudioPlayback, title]);
1436
1435
 
1437
1436
  const startRecording = useCallback(async () => {
1438
1437
  setRecordingError('');
@@ -1557,10 +1556,9 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1557
1556
  const restart = useCallback(() => action('onRestart', () => {
1558
1557
  const video = videoRef.current;
1559
1558
  if (video) video.currentTime = 0;
1560
- else youtubeRef.current?.seekTo?.(0);
1561
1559
  setPaused(false);
1562
- emitProgressBarTime(0, onProgressBarChange, lastProgressBarSecondRef, { force: true });
1563
- }, { currentTime: Number(videoRef.current?.currentTime) || 0 }), [action, onProgressBarChange, setPaused]);
1560
+ emitProgressBarTime(0, progressBarCallback, lastProgressBarSecondRef, { force: true });
1561
+ }, { currentTime: Number(videoRef.current?.currentTime) || 0 }), [action, progressBarCallback, setPaused]);
1564
1562
  const toggleMute = useCallback(() => action('onMute', () => setMuted((value) => !value), { muted: !muted }), [action, muted]);
1565
1563
  const toggleLock = useCallback(() => action('onLock', () => setLocked((value) => !value), { locked: !locked }), [action, locked]);
1566
1564
  const selectAspect = useCallback((next) => action('onAspectRatioChange', () => setAspectRatio(next), { aspectRatio: next }), [action]);
@@ -1576,10 +1574,9 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1576
1574
  const seekTo = useCallback((seconds) => action('onSeek', () => {
1577
1575
  const targetSeconds = Math.max(0, Number(seconds) || 0);
1578
1576
  if (videoRef.current) videoRef.current.currentTime = targetSeconds;
1579
- else youtubeRef.current?.seekTo?.(targetSeconds);
1580
- emitProgressBarTime(targetSeconds, onProgressBarChange, lastProgressBarSecondRef, { force: true });
1581
- }, { seconds: Number(seconds) || 0 }), [action, onProgressBarChange]);
1582
- const handleBack = useCallback(() => action('onBack', onBack || props.onClose, { title, source: media }), [action, onBack, props.onClose, title, media]);
1577
+ emitProgressBarTime(targetSeconds, progressBarCallback, lastProgressBarSecondRef, { force: true });
1578
+ }, { seconds: Number(seconds) || 0 }), [action, progressBarCallback]);
1579
+ const handleBack = useCallback(() => action('onBack', undefined, { title, source: media }), [action, title, media]);
1583
1580
  useImperativeHandle(ref, () => {
1584
1581
  const api = {
1585
1582
  play: () => setPaused(false),
@@ -1613,8 +1610,9 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1613
1610
 
1614
1611
  const hasChat = typeof integrations.liveChat?.loadMessages === 'function' || typeof renderLiveChat === 'function' || typeof integrations.liveChat?.render === 'function' || typeof actions.onLiveChatOpen === 'function';
1615
1612
  const hasEpg = typeof integrations.epg?.loadListings === 'function' || typeof renderEpg === 'function' || typeof integrations.epg?.render === 'function' || typeof actions.onEpgOpen === 'function';
1613
+ const canRecordFromBrowser = typeof MediaRecorder !== 'undefined';
1616
1614
  const hasRecording = controlOverrides.recording !== false
1617
- && (!!integrations.recording || typeof actions.onRecordingStart === 'function' || (!youtubeVideoId && typeof MediaRecorder !== 'undefined'));
1615
+ && (!!integrations.recording || typeof actions.onRecordingStart === 'function' || canRecordFromBrowser);
1618
1616
  const hasDiagnostics = Boolean(features.diagnostics) || typeof actions.onDiagnosticsOpen === 'function';
1619
1617
  const sourceType = String(media.type || media.mimeType || '').toLowerCase();
1620
1618
  const directVideoSource = getDirectVideoSource({
@@ -1624,11 +1622,6 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1624
1622
  activeUrl,
1625
1623
  });
1626
1624
 
1627
- const handleYouTubeStateChange = useCallback((state) => {
1628
- if (state === 'playing') setIsPaused(false);
1629
- if (state === 'paused' || state === 'ended') setIsPaused(true);
1630
- }, []);
1631
-
1632
1625
  useEffect(() => {
1633
1626
  const video = videoRef.current;
1634
1627
  if (!video) return undefined;
@@ -1636,7 +1629,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1636
1629
  const updateTime = () => {
1637
1630
  setCurrentTime(Number(video.currentTime) || 0);
1638
1631
  if (Number.isFinite(video.duration)) setDuration(video.duration);
1639
- emitProgressBarTime(Number(video.currentTime) || 0, onProgressBarChange, lastProgressBarSecondRef);
1632
+ emitProgressBarTime(Number(video.currentTime) || 0, progressBarCallback, lastProgressBarSecondRef);
1640
1633
  onProgress?.({ currentTime: (Number(video.currentTime) || 0) * 1000, duration: (Number(video.duration) || 0) * 1000, target: video.currentTime });
1641
1634
  };
1642
1635
  const onReadyEvent = () => {
@@ -1664,7 +1657,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1664
1657
  video.removeEventListener('timeupdate', updateTime);
1665
1658
  video.removeEventListener('ended', endedHandler);
1666
1659
  };
1667
- }, [onReady, onPlaying, onProgress, onProgressBarChange, onEnded, tracksProp]);
1660
+ }, [onReady, onPlaying, onProgress, progressBarCallback, onEnded, tracksProp]);
1668
1661
 
1669
1662
  useEffect(() => {
1670
1663
  if (selectedAudioTrack === undefined || selectedAudioTrack === null) return;
@@ -1680,8 +1673,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1680
1673
  }, []);
1681
1674
 
1682
1675
  const toggleFullscreen = () => action('onFullscreen', () => {
1683
- if (inlinePreview && typeof onPromotePreview === 'function') return onPromotePreview({ title, source: media });
1684
- if (document.fullscreenElement) document.exitFullscreen?.();
1676
+ if (document.fullscreenElement === playerRef.current) document.exitFullscreen?.();
1685
1677
  else playerRef.current?.requestFullscreen?.();
1686
1678
  }, { isFullscreen: !fullscreen });
1687
1679
  const openPanel = (panel) => {
@@ -1741,18 +1733,10 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1741
1733
  messagePageSize,
1742
1734
  });
1743
1735
 
1744
- const handleYouTubeProgress = (event) => {
1745
- const seconds = Number(event.currentTime) / 1000 || 0;
1746
- setCurrentTime(seconds);
1747
- setDuration(Number(event.duration) / 1000 || 0);
1748
- emitProgressBarTime(seconds, onProgressBarChange, lastProgressBarSecondRef);
1749
- onProgress?.(event);
1750
- };
1751
1736
  const mediaSurface = h(WebPlayerSurface, {
1752
- youtubeVideoId,
1737
+ title,
1753
1738
  streamUrl,
1754
1739
  videoRef,
1755
- youtubeRef,
1756
1740
  directVideoSource,
1757
1741
  poster,
1758
1742
  autoPlay,
@@ -1768,15 +1752,18 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1768
1752
  onPromotePreview,
1769
1753
  bufferingRef: onBufferingRef,
1770
1754
  onReady,
1771
- onProgress: handleYouTubeProgress,
1755
+ onProgress,
1772
1756
  onPlaying,
1773
1757
  onEnded,
1774
- onStateChange: handleYouTubeStateChange,
1775
1758
  onError: handleError,
1776
1759
  handleError,
1760
+ corsMode,
1777
1761
  });
1778
1762
  const bottomControlProps = {
1779
1763
  isLive,
1764
+ progressBarVisible,
1765
+ inlinePreview,
1766
+ title,
1780
1767
  overrides: controlOverrides,
1781
1768
  theme,
1782
1769
  icons,
@@ -1827,7 +1814,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
1827
1814
  audioOnly,
1828
1815
  activePanel,
1829
1816
  webPanel,
1830
- renderBackButton: () => control('back', 'Close player', () => action('onBack', props.onBack || props.onClose, { title, source: media }), { icon: 'close' }),
1817
+ renderBackButton: () => control('back', 'Close player', () => action('onBack', undefined, { title, source: media }), { icon: 'close' }),
1831
1818
  onSwitchToVideo: () => setAudioOnlyMode(false),
1832
1819
  locked,
1833
1820
  controlOverrides,
@@ -1854,7 +1841,6 @@ export const InlineLivePlayer = React.memo(function InlineLivePlayer({
1854
1841
  paused = false,
1855
1842
  isActive = true,
1856
1843
  onActivate,
1857
- onFullscreen,
1858
1844
  controls = {},
1859
1845
  actions = {},
1860
1846
  theme,
@@ -1897,7 +1883,6 @@ export const InlineLivePlayer = React.memo(function InlineLivePlayer({
1897
1883
  onError,
1898
1884
  onPlaying,
1899
1885
  inlinePreview: true,
1900
- onPromotePreview: onFullscreen,
1901
1886
  style: { width: '100%', height, aspectRatio: '16 / 9', ...style },
1902
1887
  });
1903
1888
  });
@@ -28,8 +28,7 @@
28
28
  background: #000;
29
29
  }
30
30
 
31
- .cinecrew-player__video,
32
- .cinecrew-player__youtube {
31
+ .cinecrew-player__video {
33
32
  transition: width 180ms ease, height 180ms ease, top 180ms ease, left 180ms ease, transform 180ms ease;
34
33
  }
35
34
 
@@ -51,6 +50,18 @@
51
50
  text-shadow: 0 1px 4px #000;
52
51
  }
53
52
 
53
+ .cinecrew-player__inline-title {
54
+ flex: 0 1 auto;
55
+ min-width: 0;
56
+ max-width: min(52vw, 420px);
57
+ overflow: hidden;
58
+ color: var(--cinecrew-text, #fff);
59
+ font-weight: 700;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ text-shadow: 0 1px 4px #000;
63
+ }
64
+
54
65
  .cinecrew-player__controls {
55
66
  position: absolute;
56
67
  inset: 0;
package/types/index.d.ts CHANGED
@@ -80,10 +80,11 @@ export interface PlayerActionContext {
80
80
  video?: unknown | null;
81
81
  }
82
82
 
83
- /** Notification invoked after the player's built-in action has executed. */
83
+ /** App callback for a player action. Back is app-owned and has no built-in behavior. */
84
84
  export type PlayerAction = (payload?: Record<string, unknown>, context?: PlayerActionContext) => unknown;
85
85
 
86
86
  export interface PlayerActions {
87
+ /** User-owned navigation event. The player does not close or navigate on its own. */
87
88
  onBack?: PlayerAction;
88
89
  onPlayPause?: PlayerAction;
89
90
  onSeek?: PlayerAction;
@@ -177,6 +178,7 @@ export interface PlayerIntegrations {
177
178
  render?: (context: { title: string; source: PlayerSource; onClose: () => void }) => React.ReactNode;
178
179
  };
179
180
  recording?: {
181
+ /** Starts host-managed recording for the active direct media source. */
180
182
  start?: (args: { getVideoElement: () => unknown | null; streamUrl: string; title?: string }) => Promise<unknown>;
181
183
  pause?: () => Promise<unknown>;
182
184
  resume?: () => Promise<unknown>;
@@ -209,6 +211,8 @@ export interface CineCrewPlayerProps {
209
211
  volume?: number;
210
212
  playbackRate?: number;
211
213
  controls?: PlayerControls;
214
+ /** Show the playback progress/seek bar. When false, onProgressBarChange is not called. Defaults to true. */
215
+ showProgressBar?: boolean;
212
216
  /** Web/Electron chat and EPG drawer behavior. Overlay keeps the video full-size; resize shrinks it to make room. */
213
217
  drawerMode?: PlayerDrawerMode;
214
218
  /** Web CSS or React Native view-style overrides for the chat, EPG, and diagnostics drawer. */
@@ -243,7 +247,8 @@ export interface CineCrewPlayerProps {
243
247
  playlist?: Array<Record<string, unknown>>;
244
248
  shuffle?: boolean;
245
249
  onClose?: () => void;
246
- onBack?: () => void;
250
+ /** User-owned back event. The player does not close or navigate on its own. */
251
+ onBack?: PlayerAction;
247
252
  onFullscreen?: (state: { isFullscreen: boolean }) => void;
248
253
  /** Invoked after the player applies an aspect ratio selection; actions.onAspectRatioChange takes precedence when both are supplied. */
249
254
  onAspectRatioChange?: PlayerAction;
@@ -279,6 +284,7 @@ export interface InlineLivePlayerProps {
279
284
  paused?: boolean;
280
285
  isActive?: boolean;
281
286
  onActivate?: () => void;
287
+ /** @deprecated Inline fullscreen is self-contained. Use actions.onFullscreen to observe its state. */
282
288
  onFullscreen?: () => void;
283
289
  controls?: Pick<PlayerControls, 'playPause' | 'mute' | 'fullscreen'>;
284
290
  actions?: Pick<PlayerActions, 'onPlayPause' | 'onMute' | 'onFullscreen'>;
@@ -1,120 +0,0 @@
1
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
2
- import { StyleSheet, View } from 'react-native';
3
- import { WebView } from 'react-native-webview';
4
- import { buildYouTubePlayerHtml } from '../../utils/youtubeHtml';
5
-
6
- const stateName = (value) => ({ 0: 'ended', 1: 'playing', 2: 'paused', 3: 'buffering', 5: 'cued' })[value] || 'unstarted';
7
-
8
- export const YouTubeVideoPlayer = forwardRef(function YouTubeVideoPlayer({
9
- videoId,
10
- paused = false,
11
- muted = false,
12
- volume = 1,
13
- playbackRate = 1,
14
- onReady,
15
- onProgress,
16
- onPlaying,
17
- onBuffering,
18
- onStateChange,
19
- onError,
20
- onEnded,
21
- }, ref) {
22
- const webViewRef = useRef(null);
23
- const handlersRef = useRef({});
24
- const latestPropsRef = useRef({});
25
- const readyRef = useRef(false);
26
- handlersRef.current = { onReady, onProgress, onPlaying, onBuffering, onStateChange, onError, onEnded };
27
- latestPropsRef.current = { paused, muted, volume, playbackRate };
28
-
29
- const sendCommand = useCallback((name, value) => {
30
- const serialized = JSON.stringify(value ?? null);
31
- webViewRef.current?.injectJavaScript(
32
- `window.cinecrewPlayerCommand && window.cinecrewPlayerCommand(${JSON.stringify(name)}, ${serialized}); true;`
33
- );
34
- }, []);
35
-
36
- const handleMessage = useCallback((event) => {
37
- let message;
38
- try { message = JSON.parse(event.nativeEvent.data); } catch { return; }
39
- const handlers = handlersRef.current;
40
- if (message.type === 'ready') {
41
- readyRef.current = true;
42
- handlers.onBuffering?.(false);
43
- handlers.onReady?.({ youtube: true });
44
- const latest = latestPropsRef.current;
45
- sendCommand(latest.paused ? 'pause' : 'play');
46
- sendCommand(latest.muted ? 'mute' : 'unmute');
47
- sendCommand('volume', Math.round(Math.max(0, Math.min(1, latest.volume)) * 100));
48
- sendCommand('rate', latest.playbackRate);
49
- return;
50
- }
51
- if (message.type === 'state') {
52
- const state = stateName(Number(message.data?.state));
53
- handlers.onStateChange?.(state);
54
- handlers.onBuffering?.(state === 'buffering');
55
- if (state === 'playing') handlers.onPlaying?.({ state, youtube: true });
56
- if (state === 'ended') handlers.onEnded?.();
57
- return;
58
- }
59
- if (message.type === 'progress') {
60
- const currentTime = Number(message.data?.currentTime) || 0;
61
- const duration = Number(message.data?.duration) || 0;
62
- handlers.onProgress?.({ currentTime: currentTime * 1000, duration: duration * 1000, target: currentTime });
63
- return;
64
- }
65
- if (message.type === 'error') {
66
- handlers.onError?.({ code: message.data?.code, message: `YouTube playback failed (${message.data?.code}).` });
67
- }
68
- }, [sendCommand]);
69
-
70
- useEffect(() => { if (readyRef.current) sendCommand(paused ? 'pause' : 'play'); }, [paused, sendCommand]);
71
- useEffect(() => {
72
- if (!readyRef.current) return;
73
- sendCommand(muted ? 'mute' : 'unmute');
74
- sendCommand('volume', Math.round(Math.max(0, Math.min(1, volume)) * 100));
75
- }, [muted, volume, sendCommand]);
76
- useEffect(() => { if (readyRef.current) sendCommand('rate', playbackRate); }, [playbackRate, sendCommand]);
77
-
78
- const validVideoId = /^[\w-]{11}$/.test(String(videoId || ''));
79
- const html = useMemo(
80
- () => validVideoId ? buildYouTubePlayerHtml(videoId, !paused) : '<!doctype html><html><body style="background:#000"></body></html>',
81
- [validVideoId, videoId],
82
- );
83
- const webViewSource = useMemo(() => ({ html, baseUrl: 'https://www.youtube.com' }), [html]);
84
-
85
- useEffect(() => {
86
- if (!validVideoId) handlersRef.current.onError?.(new Error('A valid YouTube video ID is required.'));
87
- }, [validVideoId]);
88
-
89
- useImperativeHandle(ref, () => ({
90
- play: () => sendCommand('play'),
91
- resume: () => sendCommand('play'),
92
- pause: () => sendCommand('pause'),
93
- seek: (ratio) => sendCommand('seekRatio', Math.max(0, Math.min(1, Number(ratio) || 0))),
94
- seekTo: (seconds) => sendCommand('seek', Math.max(0, Number(seconds) || 0)),
95
- getVideoElement: () => null,
96
- }), [sendCommand]);
97
-
98
- return React.createElement(View, { style: styles.container }, React.createElement(WebView, {
99
- ref: webViewRef,
100
- source: webViewSource,
101
- originWhitelist: ['*'],
102
- javaScriptEnabled: true,
103
- domStorageEnabled: true,
104
- allowsInlineMediaPlayback: true,
105
- mediaPlaybackRequiresUserAction: false,
106
- allowsFullscreenVideo: false,
107
- scrollEnabled: false,
108
- bounces: false,
109
- onMessage: handleMessage,
110
- onError: (event) => handlersRef.current.onError?.(event.nativeEvent || event),
111
- style: styles.webView,
112
- }));
113
- });
114
-
115
- const styles = StyleSheet.create({
116
- container: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000' },
117
- webView: { flex: 1, backgroundColor: '#000' },
118
- });
119
-
120
- export default YouTubeVideoPlayer;