diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx
index ef999202..c1603b8d 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.
+
+```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 b67af9cd..243d2fa8 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 50b781a2..68691672 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 d8c4da02..8d64baf7 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 c4ac83b5..71c9d731 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 02c39b90..6331d8a8 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 0dd72390..61453b77 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 f2f91f3b..49ca1e99 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 1dbdc9f1..eb075f9c 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;