feat: add ability to define poster props as Image type and render poster as custom component (#3972)

This commit is contained in:
Kamil Moskała 2024-07-22 22:38:35 +02:00 committed by GitHub
parent 1ee5811c8e
commit adbd06e2df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 184 additions and 72 deletions

View File

@ -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']} />

View File

@ -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}

View File

@ -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);

View File

@ -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"

View 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);

View File

@ -1,3 +1,4 @@
export * from './VideoLoader';
export * from './Indicator';
export * from './Seeker';
export * from './AudioTracksSelector';

View File

@ -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,
);

View File

@ -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,

View File

@ -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>
);
},

View File

@ -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>;