@cinecrew/cinecrew-player 0.1.5 → 0.1.7
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 +19 -2
- package/package.json +5 -3
- package/src/native/InlineLivePlayer.js +11 -1
- package/src/native/media/WebVideoPlayer.js +75 -8
- package/src/native/media/web/useWebDashPlayback.native.js +3 -0
- package/src/native/media/web/useWebDashPlayback.web.js +84 -0
- package/src/native/media/web/useWebFlvPlayback.web.js +156 -0
- package/src/native/media/web/useWebHlsPlayback.web.js +25 -2
- package/src/native/media/web/useWebMpegTsPlayback.native.js +1 -1
- package/src/native/media/web/useWebMpegTsPlayback.web.js +72 -22
- package/src/native/media/web/useWebOgvPlayback.native.js +3 -0
- package/src/native/media/web/useWebOgvPlayback.web.js +312 -0
- package/src/utils/playerError.js +34 -0
- package/src/utils/webRecording.js +17 -5
- package/src/web/index.js +221 -75
- package/src/web/styles.css +37 -4
- package/types/index.d.ts +20 -2
- package/vendor/mpegts.js/mpegts.js +3 -2
package/src/web/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useWebVideoAspectRatio } from '../native/media/web/useWebVideoAspectRatio';
|
|
3
3
|
import { useWebMpegTsPlayback } from '../native/media/web/useWebMpegTsPlayback.web';
|
|
4
|
+
import { useWebFlvPlayback } from '../native/media/web/useWebFlvPlayback.web';
|
|
4
5
|
import { useWebHlsPlayback } from '../native/media/web/useWebHlsPlayback.web';
|
|
6
|
+
import { useWebDashPlayback } from '../native/media/web/useWebDashPlayback.web';
|
|
7
|
+
import { useWebOgvPlayback } from '../native/media/web/useWebOgvPlayback.web';
|
|
5
8
|
import { useWebAc3AudioPlayback } from '../native/media/web/useWebAc3AudioPlayback.web';
|
|
6
9
|
import { getWebRuntimePlatform, useResolvedPlayerSource } from '../utils/sourceUtils';
|
|
7
10
|
import { EMOJI_GROUPS, searchEmojis } from '../data/emoji';
|
|
8
11
|
import { createRecordingDownloadLink, createVideoRecordingStream, createScreenRecordingStream, downloadRecording, getRecordingMimeType } from '../utils/webRecording';
|
|
9
12
|
import { invokePlayerAction } from '../utils/invokePlayerAction.js';
|
|
10
13
|
import { emitProgressBarTime } from '../utils/progressBarTime.js';
|
|
14
|
+
import { getPlayerErrorMessage } from '../utils/playerError.js';
|
|
11
15
|
import './styles.css';
|
|
12
16
|
|
|
13
17
|
const h = React.createElement;
|
|
@@ -311,19 +315,10 @@ function WebBottomControls(props) {
|
|
|
311
315
|
|
|
312
316
|
function getRecordingOverlayContent({ status, elapsed, error, downloadLink, onPause, onResume, onStop, onDownload, onDismiss }) {
|
|
313
317
|
if (status === 'complete') {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
children.push(h('a', {
|
|
319
|
-
className: 'cinecrew-player__recording-download',
|
|
320
|
-
href: downloadLink.url,
|
|
321
|
-
download: downloadLink.filename,
|
|
322
|
-
onClick: onDownload,
|
|
323
|
-
}, 'Download recording'));
|
|
324
|
-
}
|
|
325
|
-
children.push(h('button', { type: 'button', onClick: onDismiss, 'aria-label': 'Dismiss recording controls' }, 'Done'));
|
|
326
|
-
return h(React.Fragment, null, ...children);
|
|
318
|
+
return h(React.Fragment, null,
|
|
319
|
+
error ? h('span', null, error) : null,
|
|
320
|
+
h('strong', { className: 'cinecrew-player__recording-ready' }, 'Recording ready'),
|
|
321
|
+
h('button', { type: 'button', onClick: onDismiss, 'aria-label': 'Dismiss recording controls' }, 'Done'));
|
|
327
322
|
}
|
|
328
323
|
if (status === 'finalizing') {
|
|
329
324
|
return h('strong', { className: 'cinecrew-player__recording-ready' }, 'Preparing recording…');
|
|
@@ -394,6 +389,51 @@ function WebPlayerControls({ locked, buffering, overrides, theme, icons, unlocke
|
|
|
394
389
|
bottomControls);
|
|
395
390
|
}
|
|
396
391
|
|
|
392
|
+
function getDrawerResizedVideoStyle(drawerResize, videoStyle) {
|
|
393
|
+
if (!drawerResize) return {};
|
|
394
|
+
if (videoStyle.width === 'auto') {
|
|
395
|
+
return {
|
|
396
|
+
left: 'calc(var(--cinecrew-media-width, 64%) / 2)',
|
|
397
|
+
top: '50%',
|
|
398
|
+
right: 'auto',
|
|
399
|
+
bottom: 'auto',
|
|
400
|
+
maxWidth: 'var(--cinecrew-media-width, 64%)',
|
|
401
|
+
transform: 'translate(-50%, -50%)',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
width: 'var(--cinecrew-media-width, 64%)',
|
|
406
|
+
height: '100%',
|
|
407
|
+
left: 0,
|
|
408
|
+
top: 0,
|
|
409
|
+
right: 'auto',
|
|
410
|
+
bottom: 0,
|
|
411
|
+
transform: 'none',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getVideoSurfaceStyle(videoStyle, resizedVideoStyle, audioOnly) {
|
|
416
|
+
return {
|
|
417
|
+
...videoStyle,
|
|
418
|
+
...resizedVideoStyle,
|
|
419
|
+
opacity: audioOnly ? 0 : 1,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getOgvStageStyle(drawerResize, audioOnly) {
|
|
424
|
+
return {
|
|
425
|
+
position: 'absolute',
|
|
426
|
+
top: 0,
|
|
427
|
+
left: 0,
|
|
428
|
+
right: drawerResize ? 'auto' : 0,
|
|
429
|
+
bottom: 0,
|
|
430
|
+
width: drawerResize ? 'var(--cinecrew-media-width, 64%)' : '100%',
|
|
431
|
+
height: '100%',
|
|
432
|
+
backgroundColor: '#000',
|
|
433
|
+
opacity: audioOnly ? 0 : 1,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
397
437
|
function WebPlayerSurface({
|
|
398
438
|
title,
|
|
399
439
|
streamUrl,
|
|
@@ -419,30 +459,40 @@ function WebPlayerSurface({
|
|
|
419
459
|
onError,
|
|
420
460
|
handleError,
|
|
421
461
|
corsMode,
|
|
462
|
+
useOgv,
|
|
463
|
+
mediaType,
|
|
464
|
+
ogvResourceBase,
|
|
465
|
+
pausedRef,
|
|
466
|
+
onErrorRef,
|
|
422
467
|
}) {
|
|
468
|
+
const ogvContainerRef = useRef(null);
|
|
469
|
+
useWebOgvPlayback({
|
|
470
|
+
activeUrl: streamUrl,
|
|
471
|
+
type: mediaType,
|
|
472
|
+
videoRef,
|
|
473
|
+
playerContainerRef: ogvContainerRef,
|
|
474
|
+
playerStyle: videoStyle,
|
|
475
|
+
pausedRef,
|
|
476
|
+
onErrorRef,
|
|
477
|
+
onBufferingRef: bufferingRef,
|
|
478
|
+
resourceBase: ogvResourceBase,
|
|
479
|
+
});
|
|
423
480
|
if (!streamUrl) return h('div', { className: 'cinecrew-player__empty' });
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
};
|
|
434
|
-
} else if (drawerResize) {
|
|
435
|
-
resizedVideoStyle = {
|
|
436
|
-
width: 'var(--cinecrew-media-width, 64%)',
|
|
437
|
-
height: '100%',
|
|
438
|
-
left: 0,
|
|
439
|
-
top: 0,
|
|
440
|
-
right: 'auto',
|
|
441
|
-
bottom: 0,
|
|
442
|
-
transform: 'none',
|
|
443
|
-
};
|
|
481
|
+
const resizedVideoStyle = getDrawerResizedVideoStyle(drawerResize, videoStyle);
|
|
482
|
+
const surfaceStyle = getVideoSurfaceStyle(videoStyle, resizedVideoStyle, audioOnly);
|
|
483
|
+
if (useOgv) {
|
|
484
|
+
return h('div', {
|
|
485
|
+
ref: ogvContainerRef,
|
|
486
|
+
className: 'cinecrew-player__video',
|
|
487
|
+
style: getOgvStageStyle(drawerResize, audioOnly),
|
|
488
|
+
onClick: inlinePreview ? onPromotePreview : undefined,
|
|
489
|
+
'data-stream-mode': 'ogv',
|
|
490
|
+
});
|
|
444
491
|
}
|
|
445
492
|
return h('video', {
|
|
493
|
+
// MSE-backed engines (HLS, DASH, MPEG-TS, FLV) own this element. Changing
|
|
494
|
+
// the CORS mode must not replace it after an engine has attached to it.
|
|
495
|
+
key: directVideoSource ? `${directVideoSource}:${corsMode || 'nocors'}` : 'managed-media',
|
|
446
496
|
ref: videoRef,
|
|
447
497
|
className: 'cinecrew-player__video',
|
|
448
498
|
src: directVideoSource,
|
|
@@ -452,11 +502,7 @@ function WebPlayerSurface({
|
|
|
452
502
|
playsInline: true,
|
|
453
503
|
preload: 'auto',
|
|
454
504
|
crossOrigin: corsMode,
|
|
455
|
-
style:
|
|
456
|
-
...videoStyle,
|
|
457
|
-
...resizedVideoStyle,
|
|
458
|
-
opacity: audioOnly ? 0 : 1,
|
|
459
|
-
},
|
|
505
|
+
style: surfaceStyle,
|
|
460
506
|
onClick: inlinePreview ? onPromotePreview : undefined,
|
|
461
507
|
onError: (event) => {
|
|
462
508
|
if (!directVideoSource) return;
|
|
@@ -466,7 +512,8 @@ function WebPlayerSurface({
|
|
|
466
512
|
});
|
|
467
513
|
}
|
|
468
514
|
|
|
469
|
-
function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo }) {
|
|
515
|
+
function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo, hasVideo, isPaused }) {
|
|
516
|
+
const bars = [1, 2, 3, 4, 5, 6, 7];
|
|
470
517
|
return h('div', {
|
|
471
518
|
className: 'cinecrew-player__audio-card',
|
|
472
519
|
style: { background: '#07090E', color: theme.controlColor, borderColor: theme.accentColor },
|
|
@@ -475,13 +522,18 @@ function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo }) {
|
|
|
475
522
|
? h('img', { className: 'cinecrew-player__audio-poster', src: poster, alt: '' })
|
|
476
523
|
: h('div', { className: 'cinecrew-player__audio-placeholder', role: 'img', 'aria-label': 'Audio artwork placeholder' },
|
|
477
524
|
h(Icon, { name: 'audioOnly', icons, color: theme.accentColor })),
|
|
478
|
-
h('
|
|
525
|
+
h('div', {
|
|
526
|
+
className: `cinecrew-player__audio-wave${!isPaused ? ' is-playing' : ''}`,
|
|
527
|
+
'aria-hidden': true,
|
|
528
|
+
}, bars.map((b) => h('span', { key: b, style: { background: theme.accentColor } }))),
|
|
479
529
|
h('strong', null, title || 'Audio only'),
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
530
|
+
hasVideo && onSwitchToVideo
|
|
531
|
+
? h('button', {
|
|
532
|
+
type: 'button',
|
|
533
|
+
onClick: onSwitchToVideo,
|
|
534
|
+
style: { color: theme.accentColor, borderColor: theme.accentColor },
|
|
535
|
+
}, 'Switch to video')
|
|
536
|
+
: null);
|
|
485
537
|
}
|
|
486
538
|
|
|
487
539
|
function WebPlayerError({ error, theme, renderBackButton }) {
|
|
@@ -492,8 +544,8 @@ function WebPlayerError({ error, theme, renderBackButton }) {
|
|
|
492
544
|
renderBackButton());
|
|
493
545
|
}
|
|
494
546
|
|
|
495
|
-
function getDirectVideoSource({ mpegTs, useHls, sourceType, activeUrl }) {
|
|
496
|
-
if (mpegTs || useHls || /mpegurl|mpeg-ts/.test(sourceType)) return undefined;
|
|
547
|
+
function getDirectVideoSource({ mpegTs, useHls, useDash, useOgv, sourceType, activeUrl }) {
|
|
548
|
+
if (mpegTs || useHls || useDash || useOgv || /mpegurl|mpeg-ts|dash|flv|video\/ogg|ogv/.test(sourceType)) return undefined;
|
|
497
549
|
return activeUrl;
|
|
498
550
|
}
|
|
499
551
|
|
|
@@ -522,6 +574,9 @@ function stopBrowserRecorder(recorderRef, completionRef, finalizerRef) {
|
|
|
522
574
|
return;
|
|
523
575
|
}
|
|
524
576
|
recorder.addEventListener('error', (event) => reject(event.error || new Error('Recording failed.')), { once: true });
|
|
577
|
+
try {
|
|
578
|
+
if (recorder.state === 'recording') recorder.requestData();
|
|
579
|
+
} catch {}
|
|
525
580
|
recorder.stop();
|
|
526
581
|
if (completion) completion.then(resolve, reject);
|
|
527
582
|
else resolve();
|
|
@@ -543,16 +598,21 @@ async function performRecordingStop({ stop, action, recordingElapsed, streamUrl,
|
|
|
543
598
|
let stopCore = () => stopBrowserRecorder(recorderRef, completionRef, finalizerRef);
|
|
544
599
|
if (stop) stopCore = () => stop();
|
|
545
600
|
await action('onRecordingStop', stopCore, { elapsedMs: recordingElapsed, source: streamUrl, title });
|
|
546
|
-
if (!stop) await waitForRecordingFinalization(completionRef.current);
|
|
601
|
+
if (!stop) return await waitForRecordingFinalization(completionRef.current);
|
|
602
|
+
return null;
|
|
547
603
|
}
|
|
548
604
|
|
|
549
|
-
function updateRecordingStopState({ clock,
|
|
605
|
+
function updateRecordingStopState({ clock, completedBlob, stop, setRecordingElapsed, setRecordingStatus, setRecordingError }) {
|
|
550
606
|
if (clock.startedAt) clock.accumulatedMs += Date.now() - clock.startedAt;
|
|
551
607
|
setRecordingElapsed(clock.accumulatedMs);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
608
|
+
if (completedBlob) {
|
|
609
|
+
setRecordingStatus('idle');
|
|
610
|
+
setRecordingError('');
|
|
611
|
+
} else if (!stop) {
|
|
612
|
+
setRecordingStatus('idle');
|
|
555
613
|
setRecordingError((current) => current || 'The browser returned no recorded media data for this source. Try a local or CORS-enabled video while it is playing.');
|
|
614
|
+
} else {
|
|
615
|
+
setRecordingStatus('idle');
|
|
556
616
|
}
|
|
557
617
|
}
|
|
558
618
|
|
|
@@ -614,7 +674,7 @@ function getPlaybackStatus(error, buffering, paused) {
|
|
|
614
674
|
|
|
615
675
|
function WebPlayerLayout(props) {
|
|
616
676
|
let controlLayer = null;
|
|
617
|
-
if (!props.error
|
|
677
|
+
if (!props.error) {
|
|
618
678
|
controlLayer = h(WebPlayerControls, {
|
|
619
679
|
locked: props.locked,
|
|
620
680
|
buffering: props.buffering,
|
|
@@ -633,11 +693,19 @@ function WebPlayerLayout(props) {
|
|
|
633
693
|
let loadingNotice = null;
|
|
634
694
|
if (props.buffering && !props.error) {
|
|
635
695
|
loadingNotice = h('div', { className: 'cinecrew-player__status', style: { color: props.theme.controlColor } },
|
|
636
|
-
h('span', { className: 'cinecrew-player__spinner', style: { borderTopColor: props.theme.accentColor } }), 'Loading stream…')
|
|
696
|
+
h('span', { className: 'cinecrew-player__spinner', style: { borderTopColor: props.theme.accentColor } }), 'Loading stream…');
|
|
637
697
|
}
|
|
638
698
|
let audioCard = null;
|
|
639
699
|
if (props.audioOnly) {
|
|
640
|
-
audioCard = h(WebAudioOnlyCard, {
|
|
700
|
+
audioCard = h(WebAudioOnlyCard, {
|
|
701
|
+
poster: props.poster,
|
|
702
|
+
title: props.title,
|
|
703
|
+
theme: props.theme,
|
|
704
|
+
icons: props.icons,
|
|
705
|
+
onSwitchToVideo: props.onSwitchToVideo,
|
|
706
|
+
hasVideo: props.hasVideo,
|
|
707
|
+
isPaused: props.isPaused,
|
|
708
|
+
});
|
|
641
709
|
}
|
|
642
710
|
let panelNode = null;
|
|
643
711
|
if (props.activePanel && props.webPanel) {
|
|
@@ -649,23 +717,26 @@ function WebPlayerLayout(props) {
|
|
|
649
717
|
}
|
|
650
718
|
return h('div', {
|
|
651
719
|
ref: props.playerRef,
|
|
652
|
-
className: `cinecrew-player${props.inlinePreview ? ' cinecrew-player--inline-preview' : ''}${props.drawerMode === 'resize' && props.activePanel ? ' cinecrew-player--drawer-resize' : ''} ${props.className}`.trim(),
|
|
720
|
+
className: `cinecrew-player${props.audioOnly ? ' cinecrew-player--audio-mode' : ''}${props.inlinePreview ? ' cinecrew-player--inline-preview' : ''}${props.drawerMode === 'resize' && props.activePanel ? ' cinecrew-player--drawer-resize' : ''} ${props.className}`.trim(),
|
|
653
721
|
style: { ...props.rootStyle, ...props.style, background: props.theme.backgroundColor, borderRadius: props.theme.borderRadius, '--cinecrew-accent': props.theme.accentColor, '--cinecrew-text': props.theme.controlColor, '--cinecrew-surface': props.theme.surfaceColor, '--cinecrew-media-width': '64%' },
|
|
654
722
|
onWheel: props.onWheel,
|
|
655
723
|
'data-stream-mode': props.streamMode,
|
|
656
724
|
},
|
|
657
725
|
props.mediaSurface,
|
|
726
|
+
audioCard,
|
|
658
727
|
h('div', { className: 'cinecrew-player__shade', style: { background: 'linear-gradient(180deg, rgba(0,0,0,.48), transparent 28%, transparent 68%, rgba(0,0,0,.64))' } }),
|
|
659
728
|
loadingNotice,
|
|
660
729
|
h(WebPlayerError, { error: props.error, theme: props.theme, renderBackButton: props.locked ? () => null : props.renderBackButton }),
|
|
661
730
|
controlLayer,
|
|
662
|
-
audioCard,
|
|
663
731
|
panelNode);
|
|
664
732
|
}
|
|
665
733
|
|
|
666
|
-
function getStreamMode(mpegTs, useHls) {
|
|
734
|
+
function getStreamMode({ mpegTs, isFlv, useHls, useDash, useOgv }) {
|
|
735
|
+
if (isFlv) return 'flv';
|
|
667
736
|
if (mpegTs) return 'mpegts';
|
|
668
737
|
if (useHls) return 'hls';
|
|
738
|
+
if (useDash) return 'dash';
|
|
739
|
+
if (useOgv) return 'ogv';
|
|
669
740
|
return 'native';
|
|
670
741
|
}
|
|
671
742
|
|
|
@@ -1231,7 +1302,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1231
1302
|
|
|
1232
1303
|
const handleError = useCallback((detail) => {
|
|
1233
1304
|
const message = typeof detail === 'string' ? detail : detail?.message || 'Unable to play this media source.';
|
|
1234
|
-
// If
|
|
1305
|
+
// If playback failed while crossOrigin="anonymous" was set, retry without
|
|
1235
1306
|
// CORS so playback still works (recording will fall back to screen capture).
|
|
1236
1307
|
if (detail?.isCorsCandidate && corsModeRef.current === 'anonymous') {
|
|
1237
1308
|
setCorsMode(undefined);
|
|
@@ -1240,7 +1311,16 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1240
1311
|
}
|
|
1241
1312
|
setError(message);
|
|
1242
1313
|
setBuffering(false);
|
|
1243
|
-
|
|
1314
|
+
if (detail instanceof Error) {
|
|
1315
|
+
errorRef.current?.(detail);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const actualMessage = getPlayerErrorMessage(detail);
|
|
1319
|
+
errorRef.current?.({
|
|
1320
|
+
...detail,
|
|
1321
|
+
message,
|
|
1322
|
+
...(actualMessage ? { actualMessage } : {}),
|
|
1323
|
+
});
|
|
1244
1324
|
}, []);
|
|
1245
1325
|
const onErrorRef = useRef(handleError);
|
|
1246
1326
|
onErrorRef.current = handleError;
|
|
@@ -1312,6 +1392,19 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1312
1392
|
onErrorRef,
|
|
1313
1393
|
onBufferingRef,
|
|
1314
1394
|
});
|
|
1395
|
+
const useFlv = useWebFlvPlayback({
|
|
1396
|
+
activeUrl,
|
|
1397
|
+
type: media.type || media.mimeType,
|
|
1398
|
+
isLive,
|
|
1399
|
+
videoRef,
|
|
1400
|
+
pausedRef: pausedStateRef,
|
|
1401
|
+
onErrorRef,
|
|
1402
|
+
onBufferingRef,
|
|
1403
|
+
onAutoplayBlocked: () => {
|
|
1404
|
+
pausedStateRef.current = true;
|
|
1405
|
+
setIsPaused(true);
|
|
1406
|
+
},
|
|
1407
|
+
});
|
|
1315
1408
|
const useHls = useWebHlsPlayback({
|
|
1316
1409
|
activeUrl,
|
|
1317
1410
|
isLive,
|
|
@@ -1319,7 +1412,22 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1319
1412
|
pausedRef: pausedStateRef,
|
|
1320
1413
|
onErrorRef,
|
|
1321
1414
|
});
|
|
1322
|
-
const
|
|
1415
|
+
const useDash = useWebDashPlayback({
|
|
1416
|
+
activeUrl,
|
|
1417
|
+
type: media.type || media.mimeType,
|
|
1418
|
+
videoRef,
|
|
1419
|
+
pausedRef: pausedStateRef,
|
|
1420
|
+
onErrorRef,
|
|
1421
|
+
});
|
|
1422
|
+
const useOgv = Boolean(/\.ogv(?:$|[?#])/i.test(activeUrl)
|
|
1423
|
+
|| /(?:video\/ogg|\bogv\b)/i.test(String(media.type || media.mimeType || '')));
|
|
1424
|
+
const streamMode = getStreamMode({
|
|
1425
|
+
mpegTs: mpegTs.useMpegTs,
|
|
1426
|
+
isFlv: mpegTs.isFlv,
|
|
1427
|
+
useHls,
|
|
1428
|
+
useDash,
|
|
1429
|
+
useOgv,
|
|
1430
|
+
});
|
|
1323
1431
|
const ac3AudioPlayback = useWebAc3AudioPlayback({
|
|
1324
1432
|
active: mpegTs.useAc3Fallback,
|
|
1325
1433
|
streamUrl,
|
|
@@ -1328,17 +1436,39 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1328
1436
|
volume: volume * 100,
|
|
1329
1437
|
videoRef,
|
|
1330
1438
|
});
|
|
1439
|
+
const isManagedPlayback = mpegTs.useMpegTs || useFlv || useHls || useDash || useOgv;
|
|
1331
1440
|
|
|
1332
1441
|
useEffect(() => {
|
|
1333
1442
|
const video = videoRef.current;
|
|
1334
1443
|
if (!video) return;
|
|
1335
|
-
if (isPaused)
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1444
|
+
if (isPaused) {
|
|
1445
|
+
video.pause();
|
|
1446
|
+
} else if (isManagedPlayback && video.readyState < 2) {
|
|
1447
|
+
// Let the streaming engine start playback once its MediaSource has
|
|
1448
|
+
// produced a decodable frame. Calling play() before then can reject and
|
|
1449
|
+
// incorrectly trigger the native-source CORS retry path.
|
|
1450
|
+
return;
|
|
1451
|
+
} else {
|
|
1452
|
+
const playPromise = video.play();
|
|
1453
|
+
if (playPromise !== undefined) {
|
|
1454
|
+
playPromise.catch((playError) => {
|
|
1455
|
+
if (playError?.name === 'NotAllowedError') {
|
|
1456
|
+
if (useFlv) {
|
|
1457
|
+
video.pause();
|
|
1458
|
+
pausedStateRef.current = true;
|
|
1459
|
+
setIsPaused(true);
|
|
1460
|
+
} else {
|
|
1461
|
+
video.muted = true;
|
|
1462
|
+
setMuted(true);
|
|
1463
|
+
video.play().catch(() => {});
|
|
1464
|
+
}
|
|
1465
|
+
} else if (playError?.name !== 'AbortError') {
|
|
1466
|
+
handleError({ message: playError?.message || 'Unable to start playback.', err: playError });
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1339
1469
|
}
|
|
1340
|
-
}
|
|
1341
|
-
}, [isPaused, streamUrl, handleError]);
|
|
1470
|
+
}
|
|
1471
|
+
}, [isPaused, streamUrl, corsMode, handleError, isManagedPlayback, useFlv]);
|
|
1342
1472
|
|
|
1343
1473
|
useEffect(() => {
|
|
1344
1474
|
const video = videoRef.current;
|
|
@@ -1375,14 +1505,15 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1375
1505
|
|
|
1376
1506
|
const startBuiltinRecording = useCallback(async () => {
|
|
1377
1507
|
const video = videoRef.current;
|
|
1378
|
-
const mimeType = getRecordingMimeType();
|
|
1379
|
-
if (typeof MediaRecorder === 'undefined' || !mimeType) throw new Error('This browser does not support WebM audio/video recording.');
|
|
1380
1508
|
if (!video) throw new Error('The media element is not ready to record.');
|
|
1381
1509
|
let capture = createVideoRecordingStream(video, playerRef.current, () => aspectRatioRef.current, ac3AudioPlayback.getRecordingAudioStream());
|
|
1382
1510
|
// Fallback: if direct capture failed (CORS / tainted), use screen capture.
|
|
1383
1511
|
if (!capture) {
|
|
1384
1512
|
capture = await createScreenRecordingStream();
|
|
1385
1513
|
}
|
|
1514
|
+
const hasAudio = Boolean(capture.stream?.getAudioTracks?.().length);
|
|
1515
|
+
const mimeType = getRecordingMimeType(hasAudio);
|
|
1516
|
+
if (typeof MediaRecorder === 'undefined' || !mimeType) throw new Error('This browser does not support WebM audio/video recording.');
|
|
1386
1517
|
const recorder = new MediaRecorder(capture.stream, { mimeType });
|
|
1387
1518
|
recordingCaptureRef.current = capture;
|
|
1388
1519
|
recordingChunksRef.current = [];
|
|
@@ -1404,11 +1535,18 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1404
1535
|
if (!downloadLink) throw new Error('The recording is empty. Play the video briefly, then stop and download again.');
|
|
1405
1536
|
recordingDownloadLinkRef.current = downloadLink;
|
|
1406
1537
|
setRecordingDownloadLink(downloadLink);
|
|
1407
|
-
setRecordingStatus('complete');
|
|
1408
1538
|
try {
|
|
1409
|
-
if (!downloadRecording(blob, title)) throw new Error('The browser did not start the recording download.
|
|
1539
|
+
if (!downloadRecording(blob, title)) throw new Error('The browser did not start the recording download. Try a local or CORS-enabled source.');
|
|
1540
|
+
// Auto-dismiss back to idle after a successful download.
|
|
1541
|
+
setRecordingStatus('idle');
|
|
1542
|
+
setRecordingError('');
|
|
1543
|
+
recordingBlobRef.current = null;
|
|
1544
|
+
if (recordingDownloadLinkRef.current?.url) URL.revokeObjectURL(recordingDownloadLinkRef.current.url);
|
|
1545
|
+
recordingDownloadLinkRef.current = null;
|
|
1546
|
+
setRecordingDownloadLink(null);
|
|
1410
1547
|
} catch (downloadError) {
|
|
1411
|
-
setRecordingError(downloadError?.message || 'The recording could not be downloaded.
|
|
1548
|
+
setRecordingError(downloadError?.message || 'The recording could not be downloaded.');
|
|
1549
|
+
setRecordingStatus('idle');
|
|
1412
1550
|
}
|
|
1413
1551
|
} catch (error_) {
|
|
1414
1552
|
setRecordingError(error_?.message || 'Could not finish the recording.');
|
|
@@ -1491,7 +1629,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1491
1629
|
const stopRecording = useCallback(async () => {
|
|
1492
1630
|
try {
|
|
1493
1631
|
const stop = integrations.recording?.stop;
|
|
1494
|
-
await performRecordingStop({
|
|
1632
|
+
const completedBlob = await performRecordingStop({
|
|
1495
1633
|
stop,
|
|
1496
1634
|
action,
|
|
1497
1635
|
recordingElapsed,
|
|
@@ -1504,7 +1642,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1504
1642
|
});
|
|
1505
1643
|
updateRecordingStopState({
|
|
1506
1644
|
clock: recordingClockRef.current,
|
|
1507
|
-
|
|
1645
|
+
completedBlob,
|
|
1508
1646
|
stop,
|
|
1509
1647
|
setRecordingElapsed,
|
|
1510
1648
|
setRecordingStatus,
|
|
@@ -1616,8 +1754,10 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1616
1754
|
const hasDiagnostics = Boolean(features.diagnostics) || typeof actions.onDiagnosticsOpen === 'function';
|
|
1617
1755
|
const sourceType = String(media.type || media.mimeType || '').toLowerCase();
|
|
1618
1756
|
const directVideoSource = getDirectVideoSource({
|
|
1619
|
-
mpegTs: mpegTs.useMpegTs,
|
|
1757
|
+
mpegTs: mpegTs.useMpegTs || useFlv,
|
|
1620
1758
|
useHls,
|
|
1759
|
+
useDash,
|
|
1760
|
+
useOgv,
|
|
1621
1761
|
sourceType,
|
|
1622
1762
|
activeUrl,
|
|
1623
1763
|
});
|
|
@@ -1758,6 +1898,11 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1758
1898
|
onError: handleError,
|
|
1759
1899
|
handleError,
|
|
1760
1900
|
corsMode,
|
|
1901
|
+
useOgv,
|
|
1902
|
+
mediaType: media.type || media.mimeType,
|
|
1903
|
+
ogvResourceBase: props.ogvResourceBase,
|
|
1904
|
+
pausedRef: pausedStateRef,
|
|
1905
|
+
onErrorRef,
|
|
1761
1906
|
});
|
|
1762
1907
|
const bottomControlProps = {
|
|
1763
1908
|
isLive,
|
|
@@ -1812,6 +1957,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1812
1957
|
error,
|
|
1813
1958
|
buffering,
|
|
1814
1959
|
audioOnly,
|
|
1960
|
+
hasVideo: !audioOnly,
|
|
1815
1961
|
activePanel,
|
|
1816
1962
|
webPanel,
|
|
1817
1963
|
renderBackButton: () => control('back', 'Close player', () => action('onBack', undefined, { title, source: media }), { icon: 'close' }),
|
package/src/web/styles.css
CHANGED
|
@@ -273,13 +273,13 @@
|
|
|
273
273
|
.cinecrew-player__audio-card {
|
|
274
274
|
position: absolute;
|
|
275
275
|
inset: 0;
|
|
276
|
-
z-index:
|
|
276
|
+
z-index: 2;
|
|
277
277
|
box-sizing: border-box;
|
|
278
278
|
display: flex;
|
|
279
279
|
flex-direction: column;
|
|
280
280
|
align-items: center;
|
|
281
281
|
justify-content: center;
|
|
282
|
-
gap:
|
|
282
|
+
gap: 16px;
|
|
283
283
|
padding: 24px;
|
|
284
284
|
border: 1px solid;
|
|
285
285
|
border-radius: inherit;
|
|
@@ -287,6 +287,10 @@
|
|
|
287
287
|
box-shadow: 0 18px 60px rgba(0, 0, 0, .32);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
.cinecrew-player--audio-mode .cinecrew-player__controls {
|
|
291
|
+
opacity: 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
290
294
|
.cinecrew-player__audio-poster {
|
|
291
295
|
width: min(30vw, 160px);
|
|
292
296
|
aspect-ratio: 1;
|
|
@@ -312,8 +316,37 @@
|
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
.cinecrew-player__audio-wave {
|
|
315
|
-
|
|
316
|
-
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: center;
|
|
322
|
+
gap: 5px;
|
|
323
|
+
height: 24px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.cinecrew-player__audio-wave span {
|
|
327
|
+
display: inline-block;
|
|
328
|
+
width: 4px;
|
|
329
|
+
height: 8px;
|
|
330
|
+
border-radius: 2px;
|
|
331
|
+
background: var(--cinecrew-accent, #00E5FF);
|
|
332
|
+
transition: height 0.2s ease, opacity 0.2s ease;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.cinecrew-player__audio-wave.is-playing span {
|
|
336
|
+
animation: cinecrew-audio-bar 0.9s ease-in-out infinite alternate;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(1) { height: 14px; animation-delay: 0.1s; }
|
|
340
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(2) { height: 22px; animation-delay: 0.3s; }
|
|
341
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(3) { height: 10px; animation-delay: 0.5s; }
|
|
342
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(4) { height: 24px; animation-delay: 0.2s; }
|
|
343
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(5) { height: 16px; animation-delay: 0.4s; }
|
|
344
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(6) { height: 20px; animation-delay: 0.6s; }
|
|
345
|
+
.cinecrew-player__audio-wave.is-playing span:nth-child(7) { height: 12px; animation-delay: 0.15s; }
|
|
346
|
+
|
|
347
|
+
@keyframes cinecrew-audio-bar {
|
|
348
|
+
0% { transform: scaleY(0.35); opacity: 0.45; }
|
|
349
|
+
100% { transform: scaleY(1.25); opacity: 1; }
|
|
317
350
|
}
|
|
318
351
|
|
|
319
352
|
.cinecrew-player__audio-card strong {
|
package/types/index.d.ts
CHANGED
|
@@ -80,6 +80,20 @@ export interface PlayerActionContext {
|
|
|
80
80
|
video?: unknown | null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Structured playback failure. `message` is suitable for the player UI; `actualMessage` and `cause`/`err` preserve the underlying engine diagnostic when available. */
|
|
84
|
+
export interface PlayerErrorDetails {
|
|
85
|
+
message: string;
|
|
86
|
+
actualMessage?: string;
|
|
87
|
+
cause?: unknown;
|
|
88
|
+
err?: unknown;
|
|
89
|
+
errorType?: string;
|
|
90
|
+
errorDetail?: string;
|
|
91
|
+
httpStatus?: number | null;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type PlayerError = Error | PlayerErrorDetails;
|
|
96
|
+
|
|
83
97
|
/** App callback for a player action. Back is app-owned and has no built-in behavior. */
|
|
84
98
|
export type PlayerAction = (payload?: Record<string, unknown>, context?: PlayerActionContext) => unknown;
|
|
85
99
|
|
|
@@ -234,6 +248,8 @@ export interface CineCrewPlayerProps {
|
|
|
234
248
|
videoOnly?: boolean;
|
|
235
249
|
/** Optional resolver for share pages or other non-media links. */
|
|
236
250
|
resolveSource?: (source: PlayerSource, context: { platform: 'web' | 'native' | 'electron' }) => PlayerSource | string | Promise<PlayerSource | string>;
|
|
251
|
+
/** Optional base URL for ogv.js worker/WASM assets; defaults to the versioned jsDelivr distribution. */
|
|
252
|
+
ogvResourceBase?: string;
|
|
237
253
|
audioTracks?: AudioTrack[];
|
|
238
254
|
selectedAudioTrack?: string | number;
|
|
239
255
|
resumePosition?: number;
|
|
@@ -265,7 +281,8 @@ export interface CineCrewPlayerProps {
|
|
|
265
281
|
onProgressBarChange?: (time: string) => void;
|
|
266
282
|
onPlaying?: (event: unknown) => void;
|
|
267
283
|
onBuffering?: (buffering: boolean) => void;
|
|
268
|
-
|
|
284
|
+
/** Receives the player-facing error plus the underlying engine diagnostic (`actualMessage`, `cause`, `err`) when available. */
|
|
285
|
+
onError?: (error: PlayerError) => void;
|
|
269
286
|
onEnded?: () => void;
|
|
270
287
|
onPlaybackRoute?: (url: string) => void;
|
|
271
288
|
onNextEpisode?: (episode: Record<string, unknown>) => void;
|
|
@@ -292,7 +309,8 @@ export interface InlineLivePlayerProps {
|
|
|
292
309
|
icons?: PlayerIcons;
|
|
293
310
|
style?: unknown;
|
|
294
311
|
initialMuted?: boolean;
|
|
295
|
-
|
|
312
|
+
/** Receives the player-facing error plus the underlying engine diagnostic (`actualMessage`, `cause`, `err`) when available. */
|
|
313
|
+
onError?: (error: PlayerError) => void;
|
|
296
314
|
onPlaying?: (event: unknown) => void;
|
|
297
315
|
}
|
|
298
316
|
|