@8man/react-native-media-console
Version:
Controls for react-native-video
638 lines (624 loc) • 24.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.VideoPlayer = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeVideo = _interopRequireWildcard(require("react-native-video"));
var _hooks = require("./hooks");
var _components = require("./components");
var _OSSupport = require("./OSSupport");
var _utils = require("./utils");
var _styles2 = require("./styles");
var _Gestures = _interopRequireDefault(require("@8man/react-native-media-console/src/components/Gestures"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
const volumeWidth = 150;
const iconOffset = 0;
const AnimatedVideoPlayer = props => {
const {
animations,
toggleResizeModeOnFullscreen,
doubleTapTime = 130,
resizeMode = _reactNativeVideo.ResizeMode.CONTAIN,
isFullscreen = false,
showOnStart = false,
showOnEnd = false,
alwaysShowControls = false,
paused = false,
muted = false,
volume = 1,
title = {
primary: '',
secondary: ''
},
rate = 1,
showDuration = false,
showTimeRemaining = false,
showHours = false,
onSeek,
onError,
onBack,
onEnd,
onEnterFullscreen = () => {},
onExitFullscreen = () => {},
onHideControls = () => {},
onShowControls = () => {},
onPause,
onPlay,
onLoad,
onLoadStart,
onProgress,
controlTimeoutDelay = 15000,
tapAnywhereToPause = false,
videoStyle = {},
containerStyle = {},
seekColor = '',
source,
disableBack = false,
disableVolume = false,
disableFullscreen = false,
disableTimer = false,
disableSeekbar = false,
disablePlayPause = false,
disableSeekButtons = false,
disableOverlay,
navigator,
rewindTime = 15,
pan: {
horizontal: horizontalPan,
inverted: invertedPan
} = {},
testID,
disableGesture = false,
hideAllControlls = false
} = props;
const mounted = (0, _react.useRef)(false);
const _videoRef = (0, _react.useRef)(null);
const controlTimeout = (0, _react.useRef)(setTimeout(() => {})).current;
const tapActionTimeout = (0, _react.useRef)(null);
const [_resizeMode, setResizeMode] = (0, _react.useState)(_reactNativeVideo.ResizeMode.CONTAIN);
const [_paused, setPaused] = (0, _react.useState)(paused);
const [_muted, setMuted] = (0, _react.useState)(muted);
const [_volume, setVolume] = (0, _react.useState)(volume);
const [_isFullscreen, setIsFullscreen] = (0, _react.useState)(isFullscreen || resizeMode === 'cover' || false);
const [_playbackRate, setPlaybackRate] = (0, _react.useState)(rate);
const [_showTimeRemaining, setShowTimeRemaining] = (0, _react.useState)(showTimeRemaining);
const [volumeTrackWidth, setVolumeTrackWidth] = (0, _react.useState)(0);
const [volumeFillWidth, setVolumeFillWidth] = (0, _react.useState)(0);
const [seekerFillWidth, setSeekerFillWidth] = (0, _react.useState)(0);
const [showControls, setShowControls] = (0, _react.useState)(showOnStart);
const [volumePosition, setVolumePositionState] = (0, _react.useState)(0);
const [seekerPosition, setSeekerPositionState] = (0, _react.useState)(0);
const [volumeOffset, setVolumeOffset] = (0, _react.useState)(0);
const [seekerOffset, setSeekerOffset] = (0, _react.useState)(0);
const [seekerWidth, setSeekerWidth] = (0, _react.useState)(0);
const [seeking, setSeeking] = (0, _react.useState)(false);
const [loading, setLoading] = (0, _react.useState)(true);
const [currentTime, setCurrentTime] = (0, _react.useState)(0);
const [error, setError] = (0, _react.useState)(false);
const [duration, setDuration] = (0, _react.useState)(0);
const [buffering, setBuffering] = (0, _react.useState)(false);
const [cachedDuration, setCachedDuration] = (0, _react.useState)(0);
const [cachedPosition, setCachedPosition] = (0, _react.useState)(0);
const videoRef = props.videoRef || _videoRef;
const {
clearControlTimeout,
resetControlTimeout,
setControlTimeout
} = (0, _hooks.useControlTimeout)({
controlTimeout,
controlTimeoutDelay,
mounted: mounted.current,
showControls,
setShowControls,
alwaysShowControls
});
const toggleFullscreen = (0, _react.useCallback)(() => setIsFullscreen(prevState => !prevState), []);
const toggleControls = (0, _react.useCallback)(() => setShowControls(prevState => alwaysShowControls || !prevState), [alwaysShowControls]);
const toggleTimer = (0, _react.useCallback)(() => setShowTimeRemaining(prevState => !prevState), []);
const togglePlayPause = (0, _react.useCallback)(() => {
setPaused(prevState => !prevState);
}, []);
const styles = (0, _react.useMemo)(() => ({
videoStyle,
containerStyle: containerStyle
}), [videoStyle, containerStyle]);
const _onSeek = (0, _react.useCallback)(obj => {
try {
if (!seeking) {
setControlTimeout();
}
// Ensure currentTime is valid
const validCurrentTime = Math.max(0, Math.min(obj.currentTime || 0, duration));
setCurrentTime(validCurrentTime);
console.log('Seek completed:', obj);
if (typeof onSeek === 'function') {
onSeek(obj);
}
} catch (error) {
console.error('Error in _onSeek:', error);
}
}, [seeking, setControlTimeout, onSeek, duration]);
const _onEnd = (0, _react.useCallback)(() => {
if (currentTime < duration) {
setCurrentTime(duration);
setPaused(!props.repeat);
if (showOnEnd) {
setShowControls(!props.repeat);
}
}
if (typeof onEnd === 'function') {
onEnd();
}
}, [currentTime, duration, props.repeat, showOnEnd, onEnd]);
const _onError = (0, _react.useCallback)(() => {
setError(true);
setLoading(false);
}, []);
const _onLoadStart = (0, _react.useCallback)(e => {
setLoading(true);
if (typeof onLoadStart === 'function') {
onLoadStart(e);
}
}, [onLoadStart]);
const _onLoad = (0, _react.useCallback)(data => {
setDuration(data.duration);
setLoading(false);
if (showControls) {
setControlTimeout();
}
if (typeof onLoad === 'function') {
onLoad(data);
}
}, [showControls, setControlTimeout, onLoad]);
const _onProgress = (0, _react.useCallback)(data => {
setLoading(false);
if (!seeking && !buffering) {
const newCurrentTime = data.currentTime;
const newCachedDuration = data.playableDuration;
// Update current time
setCurrentTime(newCurrentTime);
setCachedDuration(newCachedDuration);
// Update seekbar position based on current time and duration
if (duration > 0 && seekerWidth > 0) {
const progress = newCurrentTime / duration;
const position = progress * seekerWidth;
setSeekerPosition(position);
}
// Update cached position for buffer indicator
if (duration > 0 && seekerWidth > 0) {
const bufferProgress = newCachedDuration / duration;
const cachedPos = bufferProgress * seekerWidth;
setCachedPosition(cachedPos);
}
if (typeof onProgress === 'function') {
onProgress(data);
}
}
}, [seeking, buffering, onProgress, duration, seekerWidth]);
const _onScreenTouch = (0, _react.useCallback)(() => {
if (tapActionTimeout.current) {
// This is a double tap - clear timeout and toggle fullscreen
clearTimeout(tapActionTimeout.current);
tapActionTimeout.current = null;
toggleFullscreen();
if (showControls) {
resetControlTimeout();
}
} else {
// This is a single tap - set timeout to handle single tap action
tapActionTimeout.current = setTimeout(() => {
if (tapAnywhereToPause && showControls) {
togglePlayPause();
resetControlTimeout();
} else {
toggleControls();
}
tapActionTimeout.current = null;
}, doubleTapTime);
}
}, [toggleFullscreen, showControls, resetControlTimeout, tapAnywhereToPause, togglePlayPause, toggleControls, doubleTapTime]);
const _onPlaybackRateChange = (0, _react.useCallback)(playBack => {
if (playBack.playbackRate === 0 && !buffering) {
setTimeout(() => {
!buffering && setPaused(true);
});
} else {
setPaused(false);
}
console.log(playBack);
}, [buffering]);
const events = (0, _react.useMemo)(() => ({
onError: onError || _onError,
onBack: onBack || (0, _utils._onBack)(navigator),
onEnd: _onEnd,
onScreenTouch: _onScreenTouch,
onEnterFullscreen,
onExitFullscreen,
onShowControls,
onHideControls,
onLoadStart: _onLoadStart,
onProgress: _onProgress,
onSeek: _onSeek,
onLoad: _onLoad,
onPause,
onPlay,
onPlaybackRateChange: _onPlaybackRateChange
}), [onError, _onError, onBack, navigator, _onEnd, _onScreenTouch, onEnterFullscreen, onExitFullscreen, onShowControls, onHideControls, _onLoadStart, _onProgress, _onSeek, _onLoad, onPause, onPlay, _onPlaybackRateChange]);
const constrainToSeekerMinMax = (0, _react.useCallback)((val = 0) => {
if (val <= 0) {
return 0;
} else if (val >= seekerWidth) {
return seekerWidth;
}
return val;
}, [seekerWidth]);
const constrainToVolumeMinMax = (0, _react.useCallback)((val = 0) => {
if (val <= 0) {
return 0;
} else if (val >= volumeWidth + 9) {
return volumeWidth + 9;
}
return val;
}, []);
const setSeekerPosition = (0, _react.useCallback)((position = 0) => {
const positionValue = constrainToSeekerMinMax(position);
// Batch state updates to prevent excessive re-renders
setSeekerPositionState(positionValue);
setSeekerOffset(positionValue);
setSeekerFillWidth(positionValue);
}, [constrainToSeekerMinMax]);
const setVolumePosition = (0, _react.useCallback)((position = 0) => {
const positionValue = constrainToVolumeMinMax(position);
// Batch state updates
setVolumePositionState(positionValue + iconOffset);
if (positionValue < 0) {
setVolumeFillWidth(0);
} else {
setVolumeFillWidth(positionValue);
}
}, [constrainToVolumeMinMax]);
const seekVideo = (0, _react.useCallback)(time => {
try {
var _videoRef$current, _videoRef$current2;
console.log('seekVideo called with time:', time);
console.log('videoRef.current:', !!(videoRef !== null && videoRef !== void 0 && videoRef.current));
console.log('videoRef.current.seek:', !!(videoRef !== null && videoRef !== void 0 && (_videoRef$current = videoRef.current) !== null && _videoRef$current !== void 0 && _videoRef$current.seek));
if (videoRef !== null && videoRef !== void 0 && (_videoRef$current2 = videoRef.current) !== null && _videoRef$current2 !== void 0 && _videoRef$current2.seek && typeof videoRef.current.seek === 'function') {
console.log('Calling videoRef.current.seek with time:', time);
// Try seeking with tolerance parameter for better compatibility
videoRef.current.seek(time, 100);
} else if (videoRef !== null && videoRef !== void 0 && videoRef.current) {
var _seek, _ref;
// Fallback: try calling seek directly on the ref if available
console.log('Trying fallback seek method');
(_seek = (_ref = videoRef.current).seek) === null || _seek === void 0 || _seek.call(_ref, time);
} else {
var _videoRef$current3;
console.warn('Video seek function not available', {
ref: !!(videoRef !== null && videoRef !== void 0 && videoRef.current),
seekFunction: !!(videoRef !== null && videoRef !== void 0 && (_videoRef$current3 = videoRef.current) !== null && _videoRef$current3 !== void 0 && _videoRef$current3.seek)
});
}
} catch (error) {
console.error('Error seeking video:', error);
}
}, []);
const {
volumePanResponder,
seekPanResponder
} = (0, _hooks.usePanResponders)({
duration,
seekerOffset,
volumeOffset,
loading,
seekerWidth,
seeking,
seekerPosition,
seek: seekVideo,
clearControlTimeout,
setVolumePosition,
setSeekerPosition,
setSeeking,
setControlTimeout,
onEnd: events.onEnd,
horizontal: horizontalPan,
inverted: invertedPan
});
(0, _react.useEffect)(() => {
if (currentTime >= duration && duration > 0) {
var _videoRef$current4;
videoRef === null || videoRef === void 0 || (_videoRef$current4 = videoRef.current) === null || _videoRef$current4 === void 0 || _videoRef$current4.seek(0);
}
}, [currentTime, duration, videoRef]);
(0, _react.useEffect)(() => {
if (toggleResizeModeOnFullscreen) {
setResizeMode(_isFullscreen ? _reactNativeVideo.ResizeMode.CONTAIN : _reactNativeVideo.ResizeMode.COVER);
}
if (mounted.current) {
if (_isFullscreen) {
typeof events.onEnterFullscreen === 'function' && events.onEnterFullscreen();
} else {
typeof events.onExitFullscreen === 'function' && events.onExitFullscreen();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_isFullscreen, toggleResizeModeOnFullscreen]);
(0, _react.useEffect)(() => {
setIsFullscreen(isFullscreen);
}, [isFullscreen]);
(0, _react.useEffect)(() => {
setPaused(paused);
}, [paused]);
(0, _react.useEffect)(() => {
if (_paused) {
typeof events.onPause === 'function' && events.onPause();
} else {
typeof events.onPlay === 'function' && events.onPlay();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_paused]);
// Optimize seekbar position updates with throttling
const updateSeekerPositionRef = (0, _react.useRef)(null);
(0, _react.useEffect)(() => {
if (!seeking && currentTime && duration && seekerWidth) {
if (updateSeekerPositionRef.current) {
cancelAnimationFrame(updateSeekerPositionRef.current);
}
updateSeekerPositionRef.current = requestAnimationFrame(() => {
const percent = currentTime / duration;
const position = seekerWidth * percent;
const cachedPercent = cachedDuration / duration;
const _cachedPosition = seekerWidth * cachedPercent;
const newCachedPosition = constrainToSeekerMinMax(_cachedPosition);
setCachedPosition(newCachedPosition);
setSeekerPosition(position);
updateSeekerPositionRef.current = null;
});
}
return () => {
if (updateSeekerPositionRef.current) {
cancelAnimationFrame(updateSeekerPositionRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTime, duration, seekerWidth, setSeekerPosition, constrainToSeekerMinMax]);
// set current time when seeking
(0, _react.useEffect)(() => {
if (seeking && seekerPosition && seekerWidth && duration) {
const percent = seekerPosition / seekerWidth;
const newTime = duration * percent;
setCurrentTime(newTime);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [seeking, seekerPosition]);
(0, _react.useEffect)(() => {
if (showControls) {
animations.showControlAnimation();
setControlTimeout();
typeof events.onShowControls === 'function' && events.onShowControls();
} else {
animations.hideControlAnimation();
clearControlTimeout();
typeof events.onHideControls === 'function' && events.onHideControls();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showControls, loading]);
(0, _react.useEffect)(() => {
setMuted(muted);
}, [muted]);
// Optimize volume updates with throttling
const updateVolumeRef = (0, _react.useRef)(null);
(0, _react.useEffect)(() => {
if (updateVolumeRef.current) {
cancelAnimationFrame(updateVolumeRef.current);
}
updateVolumeRef.current = requestAnimationFrame(() => {
const newVolume = volumePosition / volumeWidth;
if (newVolume <= 0) {
setMuted(true);
} else {
setMuted(false);
}
setVolume(newVolume);
setVolumeOffset(volumePosition);
const newVolumeTrackWidth = volumeWidth - volumeFillWidth;
if (newVolumeTrackWidth > 150) {
setVolumeTrackWidth(150);
} else {
setVolumeTrackWidth(newVolumeTrackWidth);
}
updateVolumeRef.current = null;
});
return () => {
if (updateVolumeRef.current) {
cancelAnimationFrame(updateVolumeRef.current);
}
};
}, [volumeFillWidth, volumePosition]);
(0, _react.useEffect)(() => {
const position = volumeWidth * _volume;
setVolumePosition(position);
setVolumeOffset(position);
mounted.current = true;
return () => {
mounted.current = false;
clearControlTimeout();
// Clean up any pending animation frames
if (updateSeekerPositionRef.current) {
cancelAnimationFrame(updateSeekerPositionRef.current);
}
if (updateVolumeRef.current) {
cancelAnimationFrame(updateVolumeRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
(0, _react.useEffect)(() => {
setPlaybackRate(rate);
}, [rate]);
const rewind = (0, _react.useCallback)(time => {
var _videoRef$current5;
const newTime = typeof time === 'number' ? currentTime - time : currentTime - rewindTime;
setCurrentTime(newTime);
videoRef === null || videoRef === void 0 || (_videoRef$current5 = videoRef.current) === null || _videoRef$current5 === void 0 || _videoRef$current5.seek(newTime);
}, [currentTime, rewindTime, videoRef]);
const forward = (0, _react.useCallback)(time => {
var _videoRef$current6;
const newTime = typeof time === 'number' ? currentTime + time : currentTime + rewindTime;
setCurrentTime(newTime);
videoRef === null || videoRef === void 0 || (_videoRef$current6 = videoRef.current) === null || _videoRef$current6 === void 0 || _videoRef$current6.seek(newTime);
}, [currentTime, rewindTime, videoRef]);
// Memoize onBuffer callback
const onBuffer = (0, _react.useCallback)(e => {
setBuffering(e.isBuffering);
if (!e.isBuffering) {
setPaused(false);
}
}, []);
// Memoize source URI for dependency comparison - use deep comparison for stability
const sourceUri = (0, _react.useMemo)(() => {
if (!source) return null;
if (typeof source === 'object' && 'uri' in source) {
return source.uri;
}
if (typeof source === 'object') {
// For other source objects, create a stable string representation
return JSON.stringify(source);
}
return String(source);
}, [source]);
// Keep track of previous source to prevent unnecessary resets
const prevSourceUri = (0, _react.useRef)(sourceUri);
const hasInitialized = (0, _react.useRef)(false);
// reset on url change - only reset if source actually changed and component has initialized
(0, _react.useEffect)(() => {
if (hasInitialized.current && sourceUri !== prevSourceUri.current && sourceUri !== null) {
prevSourceUri.current = sourceUri;
setLoading(true);
setSeekerFillWidth(0);
setSeekerPosition(0);
setCachedPosition(0);
setCurrentTime(0);
} else if (!hasInitialized.current) {
prevSourceUri.current = sourceUri;
hasInitialized.current = true;
}
}, [sourceUri, setSeekerPosition]);
return /*#__PURE__*/_react.default.createElement(_OSSupport.PlatformSupport, {
showControls: showControls,
containerStyles: styles.containerStyle,
onScreenTouch: events.onScreenTouch,
testID: testID
}, /*#__PURE__*/_react.default.createElement(_reactNative.View, {
style: [_styles2._styles.player.container, styles.containerStyle]
}, /*#__PURE__*/_react.default.createElement(_reactNativeVideo.default, _extends({
controls: false
}, props, events, {
ref: videoRef || _videoRef,
resizeMode: resizeMode,
volume: _volume,
paused: _paused,
muted: _muted,
rate: _playbackRate,
style: [_styles2._styles.player.video, styles.videoStyle],
source: source,
onBuffer: onBuffer
})), /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_components.Error, {
error: error
}), !hideAllControlls && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, !disableOverlay && /*#__PURE__*/_react.default.createElement(_components.Overlay, {
animations: animations
}), /*#__PURE__*/_react.default.createElement(_components.TopControls, {
title: title,
panHandlers: volumePanResponder.panHandlers,
animations: animations,
disableBack: disableBack,
disableVolume: disableVolume,
volumeFillWidth: volumeFillWidth,
volumeTrackWidth: volumeTrackWidth,
volumePosition: volumePosition,
onBack: events.onBack,
resetControlTimeout: resetControlTimeout,
showControls: showControls
}), loading ? /*#__PURE__*/_react.default.createElement(_components.Loader, {
color: seekColor
}) : /*#__PURE__*/_react.default.createElement(_components.PlayPause, {
animations: animations,
disablePlayPause: disablePlayPause,
disableSeekButtons: disableSeekButtons,
paused: _paused
// pauseLabel={pauseLabel}
,
togglePlayPause: togglePlayPause,
resetControlTimeout: resetControlTimeout,
showControls: showControls,
onPressRewind: rewind,
onPressForward: forward,
buffering: buffering,
primaryColor: seekColor
}), /*#__PURE__*/_react.default.createElement(_Gestures.default, {
forward: forward,
rewind: rewind,
togglePlayPause: togglePlayPause,
doubleTapTime: doubleTapTime,
seekerWidth: seekerWidth,
rewindTime: rewindTime,
toggleControls: toggleControls,
tapActionTimeout: tapActionTimeout,
tapAnywhereToPause: tapAnywhereToPause,
showControls: showControls,
disableGesture: disableGesture,
setPlayback: setPlaybackRate
}), /*#__PURE__*/_react.default.createElement(_components.BottomControls, {
animations: animations,
panHandlers: seekPanResponder.panHandlers,
disableTimer: disableTimer,
disableSeekbar: disableSeekbar,
showHours: showHours,
showDuration: showDuration,
paused: _paused,
showTimeRemaining: _showTimeRemaining,
currentTime: currentTime,
duration: duration,
seekColor: seekColor,
toggleTimer: toggleTimer,
resetControlTimeout: resetControlTimeout,
seekerFillWidth: seekerFillWidth,
seekerPosition: seekerPosition,
setSeekerWidth: setSeekerWidth,
cachedPosition: cachedPosition,
isFullscreen: isFullscreen,
disableFullscreen: disableFullscreen,
toggleFullscreen: toggleFullscreen,
showControls: showControls
})))));
};
const CustomAnimations = ({
useAnimations,
controlAnimationTiming = 450,
...props
}) => {
const animations = useAnimations(controlAnimationTiming);
return /*#__PURE__*/_react.default.createElement(AnimatedVideoPlayer, _extends({
animations: animations
}, props));
};
const JSAnimations = props => {
const animations = (0, _hooks.useJSAnimations)(props.controlAnimationTiming);
return /*#__PURE__*/_react.default.createElement(AnimatedVideoPlayer, _extends({
animations: animations
}, props));
};
const VideoPlayer = props => {
if (props !== null && props !== void 0 && props.useAnimations) {
return /*#__PURE__*/_react.default.createElement(CustomAnimations, _extends({
useAnimations: props === null || props === void 0 ? void 0 : props.useAnimations
}, props));
}
return /*#__PURE__*/_react.default.createElement(JSAnimations, props);
};
exports.VideoPlayer = VideoPlayer;
//# sourceMappingURL=VideoPlayer.js.map