feat: add ability to define poster
props as Image type and render poster as custom component (#3972)
This commit is contained in:
parent
1ee5811c8e
commit
adbd06e2df
@ -437,13 +437,26 @@ Determine whether the media should continue playing when notifications or the Co
|
||||
### `poster`
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
> [!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
|
||||
<Video>
|
||||
poster={{
|
||||
source: { uri: "https://baconmockup.com/300/200/" },
|
||||
resizeMode: "cover",
|
||||
// ...
|
||||
}}
|
||||
</Video>
|
||||
````
|
||||
|
||||
### `posterResizeMode`
|
||||
|
||||
> [!WARNING]
|
||||
> deprecated, use `poster` with `resizeMode` key instead
|
||||
<PlatformsList types={['All']} />
|
||||
|
||||
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`
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
|
||||
Allows you to create custom components to display while the video is loading. If `renderLoader` is provided, `poster` and `posterResizeMode` will be ignored.
|
||||
|
||||
```javascript
|
||||
<Video>
|
||||
renderLoader={
|
||||
<View>
|
||||
<Text>Custom Loader</Text>
|
||||
</View>
|
||||
}
|
||||
</Video>
|
||||
````
|
||||
|
||||
### `repeat`
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
|
@ -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<unknown>;
|
||||
|
||||
@ -68,7 +68,7 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
const [repeat, setRepeat] = useState(false);
|
||||
const [controls, setControls] = useState(false);
|
||||
const [useCache, setUseCache] = useState(false);
|
||||
const [poster, setPoster] = useState<string | undefined>(undefined);
|
||||
const [showPoster, setShowPoster] = useState<boolean>(false);
|
||||
const [showNotificationControls, setShowNotificationControls] =
|
||||
useState(false);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
@ -234,7 +234,6 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
fullscreen={fullscreen}
|
||||
controls={controls}
|
||||
resizeMode={resizeMode}
|
||||
onFullscreenPlayerWillDismiss={onFullScreenExit}
|
||||
@ -264,7 +263,7 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
cacheSizeMB: useCache ? 200 : 0,
|
||||
}}
|
||||
preventsDisplaySleepDuringVideoPlayback={true}
|
||||
poster={poster}
|
||||
renderLoader={showPoster ? <VideoLoader /> : undefined}
|
||||
onPlaybackRateChange={onPlaybackRateChange}
|
||||
onPlaybackStateChanged={onPlaybackStateChanged}
|
||||
bufferingStrategy={BufferingStrategyType.DEFAULT}
|
||||
@ -294,7 +293,7 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
setControls={setControls}
|
||||
poster={poster}
|
||||
showPoster={showPoster}
|
||||
rate={rate}
|
||||
setFullscreen={setFullscreen}
|
||||
setPaused={setPaused}
|
||||
@ -303,7 +302,7 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
setIsSeeking={setIsSeeking}
|
||||
repeat={repeat}
|
||||
setRepeat={setRepeat}
|
||||
setPoster={setPoster}
|
||||
setShowPoster={setShowPoster}
|
||||
setRate={setRate}
|
||||
setResizeMode={setResizeMode}
|
||||
setShowNotificationControls={setShowNotificationControls}
|
||||
|
@ -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<Props> = ({isLoading}) => {
|
||||
if (!isLoading) {
|
||||
return <View />;
|
||||
}
|
||||
return (
|
||||
<ActivityIndicator
|
||||
color="#3235fd"
|
||||
size="large"
|
||||
style={styles.IndicatorStyle}
|
||||
/>
|
||||
);
|
||||
const _Indicator = () => {
|
||||
return <ActivityIndicator color="#3235fd" size="large" />;
|
||||
};
|
||||
|
||||
export const Indicator = memo(_Indicator);
|
||||
|
@ -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<SetStateAction<boolean>>;
|
||||
repeat: boolean;
|
||||
setRepeat: Dispatch<SetStateAction<boolean>>;
|
||||
poster: string | undefined;
|
||||
setPoster: Dispatch<SetStateAction<string | undefined>>;
|
||||
showPoster: boolean;
|
||||
setShowPoster: Dispatch<SetStateAction<boolean>>;
|
||||
muted: boolean;
|
||||
setMuted: Dispatch<SetStateAction<boolean>>;
|
||||
currentTime: number;
|
||||
@ -108,8 +100,8 @@ const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
|
||||
setPaused,
|
||||
setRepeat,
|
||||
repeat,
|
||||
setPoster,
|
||||
poster,
|
||||
setShowPoster,
|
||||
showPoster,
|
||||
setMuted,
|
||||
muted,
|
||||
duration,
|
||||
@ -217,14 +209,12 @@ const _Overlay = forwardRef<VideoRef, Props>((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 (
|
||||
<>
|
||||
<Indicator isLoading={isLoading} />
|
||||
<View style={styles.topControls}>
|
||||
<View style={styles.resizeModeControl}>
|
||||
<TopControl
|
||||
@ -270,7 +260,7 @@ const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
|
||||
<ToggleControl onPress={toggleFullscreen} text="fullscreen" />
|
||||
<ToggleControl onPress={openDecoration} text="decoration" />
|
||||
<ToggleControl
|
||||
isSelected={!!poster}
|
||||
isSelected={showPoster}
|
||||
onPress={togglePoster}
|
||||
selectedText="poster"
|
||||
unselectedText="no poster"
|
||||
|
15
examples/basic/src/components/VideoLoader.tsx
Normal file
15
examples/basic/src/components/VideoLoader.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import {Text, View} from 'react-native';
|
||||
import {Indicator} from './Indicator.tsx';
|
||||
import React, {memo} from 'react';
|
||||
import styles from '../styles.tsx';
|
||||
|
||||
const _VideoLoader = () => {
|
||||
return (
|
||||
<View style={styles.indicatorContainer}>
|
||||
<Text style={styles.indicatorText}>Loading...</Text>
|
||||
<Indicator />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const VideoLoader = memo(_VideoLoader);
|
@ -1,3 +1,4 @@
|
||||
export * from './VideoLoader';
|
||||
export * from './Indicator';
|
||||
export * from './Seeker';
|
||||
export * from './AudioTracksSelector';
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
|
113
src/Video.tsx
113
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<VideoRef, ReactVideoProps>(
|
||||
source,
|
||||
style,
|
||||
resizeMode,
|
||||
posterResizeMode,
|
||||
poster,
|
||||
posterResizeMode,
|
||||
renderLoader,
|
||||
drm,
|
||||
textTracks,
|
||||
selectedVideoTrack,
|
||||
@ -113,25 +120,28 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
ref,
|
||||
) => {
|
||||
const nativeRef = useRef<ElementRef<typeof NativeVideoComponent>>(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<boolean | undefined>();
|
||||
|
||||
const hasPoster = !!poster;
|
||||
|
||||
const posterStyle = useMemo<StyleProp<ImageStyle>>(
|
||||
() => ({
|
||||
...StyleSheet.absoluteFillObject,
|
||||
resizeMode:
|
||||
posterResizeMode && posterResizeMode !== 'none'
|
||||
? posterResizeMode
|
||||
: 'contain',
|
||||
}),
|
||||
[posterResizeMode],
|
||||
);
|
||||
|
||||
const src = useMemo<VideoSrc | undefined>(() => {
|
||||
if (!source) {
|
||||
return undefined;
|
||||
@ -598,13 +608,78 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
: 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<ImageStyle> = {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
resizeMode: _posterResizeMode,
|
||||
};
|
||||
|
||||
let posterStyle: StyleProp<ImageStyle> = 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 <View style={StyleSheet.absoluteFill}>{renderLoader}</View>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...(isPosterDeprecated ? {} : poster)}
|
||||
source={isPosterDeprecated ? {uri: poster} : poster?.source}
|
||||
style={posterStyle}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
hasPoster,
|
||||
isPosterDeprecated,
|
||||
poster,
|
||||
posterResizeMode,
|
||||
renderLoader,
|
||||
showPoster,
|
||||
]);
|
||||
|
||||
const _style: StyleProp<ViewStyle> = useMemo(
|
||||
() => ({
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...(showPoster ? {display: 'none'} : {}),
|
||||
}),
|
||||
[showPoster],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<NativeVideoComponent
|
||||
ref={nativeRef}
|
||||
{...rest}
|
||||
src={src}
|
||||
style={StyleSheet.absoluteFill}
|
||||
style={_style}
|
||||
resizeMode={resizeMode}
|
||||
restoreUserInterfaceForPIPStopCompletionHandler={
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler
|
||||
@ -679,9 +754,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
}
|
||||
viewType={_viewType}
|
||||
/>
|
||||
{hasPoster && showPoster ? (
|
||||
<Image style={posterStyle} source={{uri: poster}} />
|
||||
) : null}
|
||||
{_renderPoster()}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
|
@ -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<ImageProps, 'source'> & {
|
||||
// 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<PosterResizeModeType>;
|
||||
preferredForwardBufferDuration?: number; // iOS
|
||||
preventsDisplaySleepDuringVideoPlayback?: boolean;
|
||||
progressUpdateInterval?: number;
|
||||
rate?: number;
|
||||
renderLoader?: ReactNode;
|
||||
repeat?: boolean;
|
||||
reportBandwidth?: boolean; //Android
|
||||
resizeMode?: EnumValues<VideoResizeMode>;
|
||||
|
Loading…
Reference in New Issue
Block a user