diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index ef99920273..c1603b8de6 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -437,13 +437,26 @@ Determine whether the media should continue playing when notifications or the Co ### `poster` +> [!WARNING] +> Value: string with a URL for the poster is deprecated, use `poster` as object instead An image to display while the video is loading -Value: string with a URL for the poster, e.g. "https://baconmockup.com/300/200/" +Value: Props for the `Image` component. The poster is visible when the source attribute is provided. -### `posterResizeMode` +```javascript + +```` +### `posterResizeMode` +> [!WARNING] +> deprecated, use `poster` with `resizeMode` key instead Determines how to resize the poster image when the frame doesn't match the raw video dimensions. @@ -489,6 +502,22 @@ Speed at which the media should play. - **1.0** - Play at normal speed (default) - **Other values** - Slow down or speed up playback +### `renderLoader` + + + +Allows you to create custom components to display while the video is loading. If `renderLoader` is provided, `poster` and `posterResizeMode` will be ignored. + +```javascript + +```` + ### `repeat` diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index b67af9cd2a..243d2fa8f5 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -34,7 +34,7 @@ import Video, { import styles from './styles'; import {type AdditionalSourceInfo} from './types'; import {bufferConfig, srcList, textTracksSelectionBy} from './constants'; -import {Overlay, toast} from './components'; +import {Overlay, toast, VideoLoader} from './components'; type Props = NonNullable; @@ -68,7 +68,7 @@ const VideoPlayer: FC = ({}) => { const [repeat, setRepeat] = useState(false); const [controls, setControls] = useState(false); const [useCache, setUseCache] = useState(false); - const [poster, setPoster] = useState(undefined); + const [showPoster, setShowPoster] = useState(false); const [showNotificationControls, setShowNotificationControls] = useState(false); const [isSeeking, setIsSeeking] = useState(false); @@ -234,7 +234,6 @@ const VideoPlayer: FC = ({}) => { paused={paused} volume={volume} muted={muted} - fullscreen={fullscreen} controls={controls} resizeMode={resizeMode} onFullscreenPlayerWillDismiss={onFullScreenExit} @@ -264,7 +263,7 @@ const VideoPlayer: FC = ({}) => { cacheSizeMB: useCache ? 200 : 0, }} preventsDisplaySleepDuringVideoPlayback={true} - poster={poster} + renderLoader={showPoster ? : undefined} onPlaybackRateChange={onPlaybackRateChange} onPlaybackStateChanged={onPlaybackStateChanged} bufferingStrategy={BufferingStrategyType.DEFAULT} @@ -294,7 +293,7 @@ const VideoPlayer: FC = ({}) => { paused={paused} volume={volume} setControls={setControls} - poster={poster} + showPoster={showPoster} rate={rate} setFullscreen={setFullscreen} setPaused={setPaused} @@ -303,7 +302,7 @@ const VideoPlayer: FC = ({}) => { setIsSeeking={setIsSeeking} repeat={repeat} setRepeat={setRepeat} - setPoster={setPoster} + setShowPoster={setShowPoster} setRate={setRate} setResizeMode={setResizeMode} setShowNotificationControls={setShowNotificationControls} diff --git a/examples/basic/src/components/Indicator.tsx b/examples/basic/src/components/Indicator.tsx index 50b781a270..68691672a8 100644 --- a/examples/basic/src/components/Indicator.tsx +++ b/examples/basic/src/components/Indicator.tsx @@ -1,22 +1,8 @@ -import React, {FC, memo} from 'react'; -import {ActivityIndicator, View} from 'react-native'; -import styles from '../styles.tsx'; +import React, {memo} from 'react'; +import {ActivityIndicator} from 'react-native'; -type Props = { - isLoading: boolean; -}; - -const _Indicator: FC = ({isLoading}) => { - if (!isLoading) { - return ; - } - return ( - - ); +const _Indicator = () => { + return ; }; export const Indicator = memo(_Indicator); diff --git a/examples/basic/src/components/Overlay.tsx b/examples/basic/src/components/Overlay.tsx index d8c4da0239..8d64baf72f 100644 --- a/examples/basic/src/components/Overlay.tsx +++ b/examples/basic/src/components/Overlay.tsx @@ -5,19 +5,11 @@ import React, { type Dispatch, type SetStateAction, } from 'react'; -import {Indicator} from './Indicator.tsx'; import {View} from 'react-native'; import styles from '../styles.tsx'; import ToggleControl from '../ToggleControl.tsx'; -import { - isAndroid, - isIos, - samplePoster, - textTracksSelectionBy, -} from '../constants'; -import MultiValueControl, { - type MultiValueControlPropType, -} from '../MultiValueControl.tsx'; +import {isAndroid, isIos, textTracksSelectionBy} from '../constants'; +import MultiValueControl from '../MultiValueControl.tsx'; import { ResizeMode, VideoRef, @@ -69,8 +61,8 @@ type Props = { setPaused: Dispatch>; repeat: boolean; setRepeat: Dispatch>; - poster: string | undefined; - setPoster: Dispatch>; + showPoster: boolean; + setShowPoster: Dispatch>; muted: boolean; setMuted: Dispatch>; currentTime: number; @@ -108,8 +100,8 @@ const _Overlay = forwardRef((props, ref) => { setPaused, setRepeat, repeat, - setPoster, - poster, + setShowPoster, + showPoster, setMuted, muted, duration, @@ -217,14 +209,12 @@ const _Overlay = forwardRef((props, ref) => { const toggleRepeat = () => setRepeat(prev => !prev); - const togglePoster = () => - setPoster(prev => (prev ? undefined : samplePoster)); + const togglePoster = () => setShowPoster(prev => !prev); const toggleMuted = () => setMuted(prev => !prev); return ( <> - ((props, ref) => { { + return ( + + Loading... + + + ); +}; + +export const VideoLoader = memo(_VideoLoader); diff --git a/examples/basic/src/components/index.ts b/examples/basic/src/components/index.ts index c4ac83b5ac..71c9d7316c 100644 --- a/examples/basic/src/components/index.ts +++ b/examples/basic/src/components/index.ts @@ -1,3 +1,4 @@ +export * from './VideoLoader'; export * from './Indicator'; export * from './Seeker'; export * from './AudioTracksSelector'; diff --git a/examples/basic/src/constants/general.ts b/examples/basic/src/constants/general.ts index 02c39b904c..6331d8a800 100644 --- a/examples/basic/src/constants/general.ts +++ b/examples/basic/src/constants/general.ts @@ -149,10 +149,6 @@ export const srcAndroidList = [ }, ]; -// poster which can be displayed -export const samplePoster = - 'https://upload.wikimedia.org/wikipedia/commons/1/18/React_Native_Logo.png'; - export const srcList: SampleVideoSource[] = srcAllPlatformList.concat( isAndroid ? srcAndroidList : srcIosList, ); diff --git a/examples/basic/src/styles.tsx b/examples/basic/src/styles.tsx index 0dd7239090..61453b7706 100644 --- a/examples/basic/src/styles.tsx +++ b/examples/basic/src/styles.tsx @@ -102,9 +102,15 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: 'red', }, - IndicatorStyle: { - flex: 1, + indicatorContainer: { justifyContent: 'center', + alignItems: 'center', + gap: 10, + width: '100%', + height: '100%', + }, + indicatorText: { + color: 'white', }, seekbarContainer: { flex: 1, diff --git a/src/Video.tsx b/src/Video.tsx index f2f91f3b8c..49ca1e990b 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -8,7 +8,13 @@ import React, { } from 'react'; import type {ElementRef} from 'react'; import {View, StyleSheet, Image, Platform, processColor} from 'react-native'; -import type {StyleProp, ImageStyle, NativeSyntheticEvent} from 'react-native'; +import type { + StyleProp, + ImageStyle, + NativeSyntheticEvent, + ViewStyle, + ImageResizeMode, +} from 'react-native'; import NativeVideoComponent from './specs/VideoNativeComponent'; import type { @@ -67,8 +73,9 @@ const Video = forwardRef( source, style, resizeMode, - posterResizeMode, poster, + posterResizeMode, + renderLoader, drm, textTracks, selectedVideoTrack, @@ -113,25 +120,28 @@ const Video = forwardRef( ref, ) => { const nativeRef = useRef>(null); - const [showPoster, setShowPoster] = useState(!!poster); + + const isPosterDeprecated = typeof poster === 'string'; + + const hasPoster = useMemo(() => { + if (renderLoader) { + return true; + } + + if (isPosterDeprecated) { + return !!poster; + } + + return !!poster?.source; + }, [isPosterDeprecated, poster, renderLoader]); + + const [showPoster, setShowPoster] = useState(hasPoster); + const [ _restoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler, ] = useState(); - const hasPoster = !!poster; - - const posterStyle = useMemo>( - () => ({ - ...StyleSheet.absoluteFillObject, - resizeMode: - posterResizeMode && posterResizeMode !== 'none' - ? posterResizeMode - : 'contain', - }), - [posterResizeMode], - ); - const src = useMemo(() => { if (!source) { return undefined; @@ -598,13 +608,78 @@ const Video = forwardRef( : ViewType.SURFACE; }, [drm, useSecureView, useTextureView, viewType]); + const _renderPoster = useCallback(() => { + if (!hasPoster || !showPoster) { + return null; + } + + // poster resize mode + let _posterResizeMode: ImageResizeMode = 'contain'; + + if (!isPosterDeprecated && poster?.resizeMode) { + _posterResizeMode = poster.resizeMode; + } else if (posterResizeMode && posterResizeMode !== 'none') { + _posterResizeMode = posterResizeMode; + } + + // poster style + const baseStyle: StyleProp = { + ...StyleSheet.absoluteFillObject, + resizeMode: _posterResizeMode, + }; + + let posterStyle: StyleProp = baseStyle; + + if (!isPosterDeprecated && poster?.style) { + const styles = Array.isArray(poster.style) + ? poster.style + : [poster.style]; + posterStyle = [baseStyle, ...styles]; + } + + // render poster + if (renderLoader && (poster || posterResizeMode)) { + console.warn( + 'You provided both `renderLoader` and `poster` or `posterResizeMode` props. `renderLoader` will be used.', + ); + } + + // render loader + if (renderLoader) { + return {renderLoader}; + } + + return ( + + ); + }, [ + hasPoster, + isPosterDeprecated, + poster, + posterResizeMode, + renderLoader, + showPoster, + ]); + + const _style: StyleProp = useMemo( + () => ({ + ...StyleSheet.absoluteFillObject, + ...(showPoster ? {display: 'none'} : {}), + }), + [showPoster], + ); + return ( ( } viewType={_viewType} /> - {hasPoster && showPoster ? ( - - ) : null} + {_renderPoster()} ); }, diff --git a/src/types/video.ts b/src/types/video.ts index 1dbdc9f132..eb075f9c1c 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -1,6 +1,14 @@ import type {ISO639_1} from './language'; import type {ReactVideoEvents} from './events'; -import type {StyleProp, ViewProps, ViewStyle} from 'react-native'; +import type { + ImageProps, + StyleProp, + ViewProps, + ViewStyle, + ImageRequireSource, + ImageURISource, +} from 'react-native'; +import type {ReactNode} from 'react'; import type VideoResizeMode from './ResizeMode'; import type FilterType from './FilterType'; import type ViewType from './ViewType'; @@ -34,6 +42,13 @@ export type ReactVideoSource = Readonly< } >; +export type ReactVideoPosterSource = ImageURISource | ImageRequireSource; + +export type ReactVideoPoster = Omit & { + // prevents giving source in the array + source?: ReactVideoPosterSource; +}; + export type VideoMetadata = Readonly<{ title?: string; subtitle?: string; @@ -243,12 +258,14 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { pictureInPicture?: boolean; // iOS playInBackground?: boolean; playWhenInactive?: boolean; // iOS - poster?: string; + poster?: string | ReactVideoPoster; // string is deprecated + /** @deprecated use **resizeMode** key in **poster** props instead */ posterResizeMode?: EnumValues; preferredForwardBufferDuration?: number; // iOS preventsDisplaySleepDuringVideoPlayback?: boolean; progressUpdateInterval?: number; rate?: number; + renderLoader?: ReactNode; repeat?: boolean; reportBandwidth?: boolean; //Android resizeMode?: EnumValues;