@cinecrew/cinecrew-player 0.1.6 → 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 +74 -7
- 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 +215 -76
- 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,31 +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', {
|
|
446
|
-
|
|
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',
|
|
447
496
|
ref: videoRef,
|
|
448
497
|
className: 'cinecrew-player__video',
|
|
449
498
|
src: directVideoSource,
|
|
@@ -453,11 +502,7 @@ function WebPlayerSurface({
|
|
|
453
502
|
playsInline: true,
|
|
454
503
|
preload: 'auto',
|
|
455
504
|
crossOrigin: corsMode,
|
|
456
|
-
style:
|
|
457
|
-
...videoStyle,
|
|
458
|
-
...resizedVideoStyle,
|
|
459
|
-
opacity: audioOnly ? 0 : 1,
|
|
460
|
-
},
|
|
505
|
+
style: surfaceStyle,
|
|
461
506
|
onClick: inlinePreview ? onPromotePreview : undefined,
|
|
462
507
|
onError: (event) => {
|
|
463
508
|
if (!directVideoSource) return;
|
|
@@ -467,7 +512,8 @@ function WebPlayerSurface({
|
|
|
467
512
|
});
|
|
468
513
|
}
|
|
469
514
|
|
|
470
|
-
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];
|
|
471
517
|
return h('div', {
|
|
472
518
|
className: 'cinecrew-player__audio-card',
|
|
473
519
|
style: { background: '#07090E', color: theme.controlColor, borderColor: theme.accentColor },
|
|
@@ -476,13 +522,18 @@ function WebAudioOnlyCard({ poster, title, theme, icons, onSwitchToVideo }) {
|
|
|
476
522
|
? h('img', { className: 'cinecrew-player__audio-poster', src: poster, alt: '' })
|
|
477
523
|
: h('div', { className: 'cinecrew-player__audio-placeholder', role: 'img', 'aria-label': 'Audio artwork placeholder' },
|
|
478
524
|
h(Icon, { name: 'audioOnly', icons, color: theme.accentColor })),
|
|
479
|
-
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 } }))),
|
|
480
529
|
h('strong', null, title || 'Audio only'),
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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);
|
|
486
537
|
}
|
|
487
538
|
|
|
488
539
|
function WebPlayerError({ error, theme, renderBackButton }) {
|
|
@@ -493,8 +544,8 @@ function WebPlayerError({ error, theme, renderBackButton }) {
|
|
|
493
544
|
renderBackButton());
|
|
494
545
|
}
|
|
495
546
|
|
|
496
|
-
function getDirectVideoSource({ mpegTs, useHls, sourceType, activeUrl }) {
|
|
497
|
-
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;
|
|
498
549
|
return activeUrl;
|
|
499
550
|
}
|
|
500
551
|
|
|
@@ -523,6 +574,9 @@ function stopBrowserRecorder(recorderRef, completionRef, finalizerRef) {
|
|
|
523
574
|
return;
|
|
524
575
|
}
|
|
525
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 {}
|
|
526
580
|
recorder.stop();
|
|
527
581
|
if (completion) completion.then(resolve, reject);
|
|
528
582
|
else resolve();
|
|
@@ -544,16 +598,21 @@ async function performRecordingStop({ stop, action, recordingElapsed, streamUrl,
|
|
|
544
598
|
let stopCore = () => stopBrowserRecorder(recorderRef, completionRef, finalizerRef);
|
|
545
599
|
if (stop) stopCore = () => stop();
|
|
546
600
|
await action('onRecordingStop', stopCore, { elapsedMs: recordingElapsed, source: streamUrl, title });
|
|
547
|
-
if (!stop) await waitForRecordingFinalization(completionRef.current);
|
|
601
|
+
if (!stop) return await waitForRecordingFinalization(completionRef.current);
|
|
602
|
+
return null;
|
|
548
603
|
}
|
|
549
604
|
|
|
550
|
-
function updateRecordingStopState({ clock,
|
|
605
|
+
function updateRecordingStopState({ clock, completedBlob, stop, setRecordingElapsed, setRecordingStatus, setRecordingError }) {
|
|
551
606
|
if (clock.startedAt) clock.accumulatedMs += Date.now() - clock.startedAt;
|
|
552
607
|
setRecordingElapsed(clock.accumulatedMs);
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
608
|
+
if (completedBlob) {
|
|
609
|
+
setRecordingStatus('idle');
|
|
610
|
+
setRecordingError('');
|
|
611
|
+
} else if (!stop) {
|
|
612
|
+
setRecordingStatus('idle');
|
|
556
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');
|
|
557
616
|
}
|
|
558
617
|
}
|
|
559
618
|
|
|
@@ -615,7 +674,7 @@ function getPlaybackStatus(error, buffering, paused) {
|
|
|
615
674
|
|
|
616
675
|
function WebPlayerLayout(props) {
|
|
617
676
|
let controlLayer = null;
|
|
618
|
-
if (!props.error
|
|
677
|
+
if (!props.error) {
|
|
619
678
|
controlLayer = h(WebPlayerControls, {
|
|
620
679
|
locked: props.locked,
|
|
621
680
|
buffering: props.buffering,
|
|
@@ -634,11 +693,19 @@ function WebPlayerLayout(props) {
|
|
|
634
693
|
let loadingNotice = null;
|
|
635
694
|
if (props.buffering && !props.error) {
|
|
636
695
|
loadingNotice = h('div', { className: 'cinecrew-player__status', style: { color: props.theme.controlColor } },
|
|
637
|
-
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…');
|
|
638
697
|
}
|
|
639
698
|
let audioCard = null;
|
|
640
699
|
if (props.audioOnly) {
|
|
641
|
-
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
|
+
});
|
|
642
709
|
}
|
|
643
710
|
let panelNode = null;
|
|
644
711
|
if (props.activePanel && props.webPanel) {
|
|
@@ -650,23 +717,26 @@ function WebPlayerLayout(props) {
|
|
|
650
717
|
}
|
|
651
718
|
return h('div', {
|
|
652
719
|
ref: props.playerRef,
|
|
653
|
-
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(),
|
|
654
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%' },
|
|
655
722
|
onWheel: props.onWheel,
|
|
656
723
|
'data-stream-mode': props.streamMode,
|
|
657
724
|
},
|
|
658
725
|
props.mediaSurface,
|
|
726
|
+
audioCard,
|
|
659
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))' } }),
|
|
660
728
|
loadingNotice,
|
|
661
729
|
h(WebPlayerError, { error: props.error, theme: props.theme, renderBackButton: props.locked ? () => null : props.renderBackButton }),
|
|
662
730
|
controlLayer,
|
|
663
|
-
audioCard,
|
|
664
731
|
panelNode);
|
|
665
732
|
}
|
|
666
733
|
|
|
667
|
-
function getStreamMode(mpegTs, useHls) {
|
|
734
|
+
function getStreamMode({ mpegTs, isFlv, useHls, useDash, useOgv }) {
|
|
735
|
+
if (isFlv) return 'flv';
|
|
668
736
|
if (mpegTs) return 'mpegts';
|
|
669
737
|
if (useHls) return 'hls';
|
|
738
|
+
if (useDash) return 'dash';
|
|
739
|
+
if (useOgv) return 'ogv';
|
|
670
740
|
return 'native';
|
|
671
741
|
}
|
|
672
742
|
|
|
@@ -1232,7 +1302,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1232
1302
|
|
|
1233
1303
|
const handleError = useCallback((detail) => {
|
|
1234
1304
|
const message = typeof detail === 'string' ? detail : detail?.message || 'Unable to play this media source.';
|
|
1235
|
-
// If
|
|
1305
|
+
// If playback failed while crossOrigin="anonymous" was set, retry without
|
|
1236
1306
|
// CORS so playback still works (recording will fall back to screen capture).
|
|
1237
1307
|
if (detail?.isCorsCandidate && corsModeRef.current === 'anonymous') {
|
|
1238
1308
|
setCorsMode(undefined);
|
|
@@ -1241,7 +1311,16 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1241
1311
|
}
|
|
1242
1312
|
setError(message);
|
|
1243
1313
|
setBuffering(false);
|
|
1244
|
-
|
|
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
|
+
});
|
|
1245
1324
|
}, []);
|
|
1246
1325
|
const onErrorRef = useRef(handleError);
|
|
1247
1326
|
onErrorRef.current = handleError;
|
|
@@ -1313,6 +1392,19 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1313
1392
|
onErrorRef,
|
|
1314
1393
|
onBufferingRef,
|
|
1315
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
|
+
});
|
|
1316
1408
|
const useHls = useWebHlsPlayback({
|
|
1317
1409
|
activeUrl,
|
|
1318
1410
|
isLive,
|
|
@@ -1320,7 +1412,22 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1320
1412
|
pausedRef: pausedStateRef,
|
|
1321
1413
|
onErrorRef,
|
|
1322
1414
|
});
|
|
1323
|
-
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
|
+
});
|
|
1324
1431
|
const ac3AudioPlayback = useWebAc3AudioPlayback({
|
|
1325
1432
|
active: mpegTs.useAc3Fallback,
|
|
1326
1433
|
streamUrl,
|
|
@@ -1329,17 +1436,39 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1329
1436
|
volume: volume * 100,
|
|
1330
1437
|
videoRef,
|
|
1331
1438
|
});
|
|
1439
|
+
const isManagedPlayback = mpegTs.useMpegTs || useFlv || useHls || useDash || useOgv;
|
|
1332
1440
|
|
|
1333
1441
|
useEffect(() => {
|
|
1334
1442
|
const video = videoRef.current;
|
|
1335
1443
|
if (!video) return;
|
|
1336
|
-
if (isPaused)
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
+
});
|
|
1340
1469
|
}
|
|
1341
|
-
}
|
|
1342
|
-
}, [isPaused, streamUrl, handleError]);
|
|
1470
|
+
}
|
|
1471
|
+
}, [isPaused, streamUrl, corsMode, handleError, isManagedPlayback, useFlv]);
|
|
1343
1472
|
|
|
1344
1473
|
useEffect(() => {
|
|
1345
1474
|
const video = videoRef.current;
|
|
@@ -1376,14 +1505,15 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1376
1505
|
|
|
1377
1506
|
const startBuiltinRecording = useCallback(async () => {
|
|
1378
1507
|
const video = videoRef.current;
|
|
1379
|
-
const mimeType = getRecordingMimeType();
|
|
1380
|
-
if (typeof MediaRecorder === 'undefined' || !mimeType) throw new Error('This browser does not support WebM audio/video recording.');
|
|
1381
1508
|
if (!video) throw new Error('The media element is not ready to record.');
|
|
1382
1509
|
let capture = createVideoRecordingStream(video, playerRef.current, () => aspectRatioRef.current, ac3AudioPlayback.getRecordingAudioStream());
|
|
1383
1510
|
// Fallback: if direct capture failed (CORS / tainted), use screen capture.
|
|
1384
1511
|
if (!capture) {
|
|
1385
1512
|
capture = await createScreenRecordingStream();
|
|
1386
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.');
|
|
1387
1517
|
const recorder = new MediaRecorder(capture.stream, { mimeType });
|
|
1388
1518
|
recordingCaptureRef.current = capture;
|
|
1389
1519
|
recordingChunksRef.current = [];
|
|
@@ -1405,17 +1535,18 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1405
1535
|
if (!downloadLink) throw new Error('The recording is empty. Play the video briefly, then stop and download again.');
|
|
1406
1536
|
recordingDownloadLinkRef.current = downloadLink;
|
|
1407
1537
|
setRecordingDownloadLink(downloadLink);
|
|
1408
|
-
setRecordingStatus('complete');
|
|
1409
1538
|
try {
|
|
1410
|
-
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.');
|
|
1411
1540
|
// Auto-dismiss back to idle after a successful download.
|
|
1412
1541
|
setRecordingStatus('idle');
|
|
1542
|
+
setRecordingError('');
|
|
1413
1543
|
recordingBlobRef.current = null;
|
|
1414
1544
|
if (recordingDownloadLinkRef.current?.url) URL.revokeObjectURL(recordingDownloadLinkRef.current.url);
|
|
1415
1545
|
recordingDownloadLinkRef.current = null;
|
|
1416
1546
|
setRecordingDownloadLink(null);
|
|
1417
1547
|
} catch (downloadError) {
|
|
1418
|
-
setRecordingError(downloadError?.message || 'The recording could not be downloaded.
|
|
1548
|
+
setRecordingError(downloadError?.message || 'The recording could not be downloaded.');
|
|
1549
|
+
setRecordingStatus('idle');
|
|
1419
1550
|
}
|
|
1420
1551
|
} catch (error_) {
|
|
1421
1552
|
setRecordingError(error_?.message || 'Could not finish the recording.');
|
|
@@ -1498,7 +1629,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1498
1629
|
const stopRecording = useCallback(async () => {
|
|
1499
1630
|
try {
|
|
1500
1631
|
const stop = integrations.recording?.stop;
|
|
1501
|
-
await performRecordingStop({
|
|
1632
|
+
const completedBlob = await performRecordingStop({
|
|
1502
1633
|
stop,
|
|
1503
1634
|
action,
|
|
1504
1635
|
recordingElapsed,
|
|
@@ -1511,7 +1642,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1511
1642
|
});
|
|
1512
1643
|
updateRecordingStopState({
|
|
1513
1644
|
clock: recordingClockRef.current,
|
|
1514
|
-
|
|
1645
|
+
completedBlob,
|
|
1515
1646
|
stop,
|
|
1516
1647
|
setRecordingElapsed,
|
|
1517
1648
|
setRecordingStatus,
|
|
@@ -1623,8 +1754,10 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1623
1754
|
const hasDiagnostics = Boolean(features.diagnostics) || typeof actions.onDiagnosticsOpen === 'function';
|
|
1624
1755
|
const sourceType = String(media.type || media.mimeType || '').toLowerCase();
|
|
1625
1756
|
const directVideoSource = getDirectVideoSource({
|
|
1626
|
-
mpegTs: mpegTs.useMpegTs,
|
|
1757
|
+
mpegTs: mpegTs.useMpegTs || useFlv,
|
|
1627
1758
|
useHls,
|
|
1759
|
+
useDash,
|
|
1760
|
+
useOgv,
|
|
1628
1761
|
sourceType,
|
|
1629
1762
|
activeUrl,
|
|
1630
1763
|
});
|
|
@@ -1765,6 +1898,11 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1765
1898
|
onError: handleError,
|
|
1766
1899
|
handleError,
|
|
1767
1900
|
corsMode,
|
|
1901
|
+
useOgv,
|
|
1902
|
+
mediaType: media.type || media.mimeType,
|
|
1903
|
+
ogvResourceBase: props.ogvResourceBase,
|
|
1904
|
+
pausedRef: pausedStateRef,
|
|
1905
|
+
onErrorRef,
|
|
1768
1906
|
});
|
|
1769
1907
|
const bottomControlProps = {
|
|
1770
1908
|
isLive,
|
|
@@ -1819,6 +1957,7 @@ export const CineCrewPlayer = forwardRef(function CineCrewPlayer(props, ref) {
|
|
|
1819
1957
|
error,
|
|
1820
1958
|
buffering,
|
|
1821
1959
|
audioOnly,
|
|
1960
|
+
hasVideo: !audioOnly,
|
|
1822
1961
|
activePanel,
|
|
1823
1962
|
webPanel,
|
|
1824
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
|
|