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` ### `poster`
<PlatformsList types={['All']} /> <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 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` ### `posterResizeMode`
> [!WARNING]
> deprecated, use `poster` with `resizeMode` key instead
<PlatformsList types={['All']} /> <PlatformsList types={['All']} />
Determines how to resize the poster image when the frame doesn't match the raw video dimensions. 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) - **1.0** - Play at normal speed (default)
- **Other values** - Slow down or speed up playback - **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` ### `repeat`
<PlatformsList types={['All']} /> <PlatformsList types={['All']} />

View File

@ -34,7 +34,7 @@ import Video, {
import styles from './styles'; import styles from './styles';
import {type AdditionalSourceInfo} from './types'; import {type AdditionalSourceInfo} from './types';
import {bufferConfig, srcList, textTracksSelectionBy} from './constants'; import {bufferConfig, srcList, textTracksSelectionBy} from './constants';
import {Overlay, toast} from './components'; import {Overlay, toast, VideoLoader} from './components';
type Props = NonNullable<unknown>; type Props = NonNullable<unknown>;
@ -68,7 +68,7 @@ const VideoPlayer: FC<Props> = ({}) => {
const [repeat, setRepeat] = useState(false); const [repeat, setRepeat] = useState(false);
const [controls, setControls] = useState(false); const [controls, setControls] = useState(false);
const [useCache, setUseCache] = useState(false); const [useCache, setUseCache] = useState(false);
const [poster, setPoster] = useState<string | undefined>(undefined); const [showPoster, setShowPoster] = useState<boolean>(false);
const [showNotificationControls, setShowNotificationControls] = const [showNotificationControls, setShowNotificationControls] =
useState(false); useState(false);
const [isSeeking, setIsSeeking] = useState(false); const [isSeeking, setIsSeeking] = useState(false);
@ -234,7 +234,6 @@ const VideoPlayer: FC<Props> = ({}) => {
paused={paused} paused={paused}
volume={volume} volume={volume}
muted={muted} muted={muted}
fullscreen={fullscreen}
controls={controls} controls={controls}
resizeMode={resizeMode} resizeMode={resizeMode}
onFullscreenPlayerWillDismiss={onFullScreenExit} onFullscreenPlayerWillDismiss={onFullScreenExit}
@ -264,7 +263,7 @@ const VideoPlayer: FC<Props> = ({}) => {
cacheSizeMB: useCache ? 200 : 0, cacheSizeMB: useCache ? 200 : 0,
}} }}
preventsDisplaySleepDuringVideoPlayback={true} preventsDisplaySleepDuringVideoPlayback={true}
poster={poster} renderLoader={showPoster ? <VideoLoader /> : undefined}
onPlaybackRateChange={onPlaybackRateChange} onPlaybackRateChange={onPlaybackRateChange}
onPlaybackStateChanged={onPlaybackStateChanged} onPlaybackStateChanged={onPlaybackStateChanged}
bufferingStrategy={BufferingStrategyType.DEFAULT} bufferingStrategy={BufferingStrategyType.DEFAULT}
@ -294,7 +293,7 @@ const VideoPlayer: FC<Props> = ({}) => {
paused={paused} paused={paused}
volume={volume} volume={volume}
setControls={setControls} setControls={setControls}
poster={poster} showPoster={showPoster}
rate={rate} rate={rate}
setFullscreen={setFullscreen} setFullscreen={setFullscreen}
setPaused={setPaused} setPaused={setPaused}
@ -303,7 +302,7 @@ const VideoPlayer: FC<Props> = ({}) => {
setIsSeeking={setIsSeeking} setIsSeeking={setIsSeeking}
repeat={repeat} repeat={repeat}
setRepeat={setRepeat} setRepeat={setRepeat}
setPoster={setPoster} setShowPoster={setShowPoster}
setRate={setRate} setRate={setRate}
setResizeMode={setResizeMode} setResizeMode={setResizeMode}
setShowNotificationControls={setShowNotificationControls} setShowNotificationControls={setShowNotificationControls}

View File

@ -1,22 +1,8 @@
import React, {FC, memo} from 'react'; import React, {memo} from 'react';
import {ActivityIndicator, View} from 'react-native'; import {ActivityIndicator} from 'react-native';
import styles from '../styles.tsx';
type Props = { const _Indicator = () => {
isLoading: boolean; return <ActivityIndicator color="#3235fd" size="large" />;
};
const _Indicator: FC<Props> = ({isLoading}) => {
if (!isLoading) {
return <View />;
}
return (
<ActivityIndicator
color="#3235fd"
size="large"
style={styles.IndicatorStyle}
/>
);
}; };
export const Indicator = memo(_Indicator); export const Indicator = memo(_Indicator);

View File

@ -5,19 +5,11 @@ import React, {
type Dispatch, type Dispatch,
type SetStateAction, type SetStateAction,
} from 'react'; } from 'react';
import {Indicator} from './Indicator.tsx';
import {View} from 'react-native'; import {View} from 'react-native';
import styles from '../styles.tsx'; import styles from '../styles.tsx';
import ToggleControl from '../ToggleControl.tsx'; import ToggleControl from '../ToggleControl.tsx';
import { import {isAndroid, isIos, textTracksSelectionBy} from '../constants';
isAndroid, import MultiValueControl from '../MultiValueControl.tsx';
isIos,
samplePoster,
textTracksSelectionBy,
} from '../constants';
import MultiValueControl, {
type MultiValueControlPropType,
} from '../MultiValueControl.tsx';
import { import {
ResizeMode, ResizeMode,
VideoRef, VideoRef,
@ -69,8 +61,8 @@ type Props = {
setPaused: Dispatch<SetStateAction<boolean>>; setPaused: Dispatch<SetStateAction<boolean>>;
repeat: boolean; repeat: boolean;
setRepeat: Dispatch<SetStateAction<boolean>>; setRepeat: Dispatch<SetStateAction<boolean>>;
poster: string | undefined; showPoster: boolean;
setPoster: Dispatch<SetStateAction<string | undefined>>; setShowPoster: Dispatch<SetStateAction<boolean>>;
muted: boolean; muted: boolean;
setMuted: Dispatch<SetStateAction<boolean>>; setMuted: Dispatch<SetStateAction<boolean>>;
currentTime: number; currentTime: number;
@ -108,8 +100,8 @@ const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
setPaused, setPaused,
setRepeat, setRepeat,
repeat, repeat,
setPoster, setShowPoster,
poster, showPoster,
setMuted, setMuted,
muted, muted,
duration, duration,
@ -217,14 +209,12 @@ const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
const toggleRepeat = () => setRepeat(prev => !prev); const toggleRepeat = () => setRepeat(prev => !prev);
const togglePoster = () => const togglePoster = () => setShowPoster(prev => !prev);
setPoster(prev => (prev ? undefined : samplePoster));
const toggleMuted = () => setMuted(prev => !prev); const toggleMuted = () => setMuted(prev => !prev);
return ( return (
<> <>
<Indicator isLoading={isLoading} />
<View style={styles.topControls}> <View style={styles.topControls}>
<View style={styles.resizeModeControl}> <View style={styles.resizeModeControl}>
<TopControl <TopControl
@ -270,7 +260,7 @@ const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
<ToggleControl onPress={toggleFullscreen} text="fullscreen" /> <ToggleControl onPress={toggleFullscreen} text="fullscreen" />
<ToggleControl onPress={openDecoration} text="decoration" /> <ToggleControl onPress={openDecoration} text="decoration" />
<ToggleControl <ToggleControl
isSelected={!!poster} isSelected={showPoster}
onPress={togglePoster} onPress={togglePoster}
selectedText="poster" selectedText="poster"
unselectedText="no 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 './Indicator';
export * from './Seeker'; export * from './Seeker';
export * from './AudioTracksSelector'; 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( export const srcList: SampleVideoSource[] = srcAllPlatformList.concat(
isAndroid ? srcAndroidList : srcIosList, isAndroid ? srcAndroidList : srcIosList,
); );

View File

@ -102,9 +102,15 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
borderColor: 'red', borderColor: 'red',
}, },
IndicatorStyle: { indicatorContainer: {
flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
gap: 10,
width: '100%',
height: '100%',
},
indicatorText: {
color: 'white',
}, },
seekbarContainer: { seekbarContainer: {
flex: 1, flex: 1,

View File

@ -8,7 +8,13 @@ import React, {
} from 'react'; } from 'react';
import type {ElementRef} from 'react'; import type {ElementRef} from 'react';
import {View, StyleSheet, Image, Platform, processColor} from 'react-native'; 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 NativeVideoComponent from './specs/VideoNativeComponent';
import type { import type {
@ -67,8 +73,9 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
source, source,
style, style,
resizeMode, resizeMode,
posterResizeMode,
poster, poster,
posterResizeMode,
renderLoader,
drm, drm,
textTracks, textTracks,
selectedVideoTrack, selectedVideoTrack,
@ -113,25 +120,28 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
ref, ref,
) => { ) => {
const nativeRef = useRef<ElementRef<typeof NativeVideoComponent>>(null); 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 [ const [
_restoreUserInterfaceForPIPStopCompletionHandler, _restoreUserInterfaceForPIPStopCompletionHandler,
setRestoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler,
] = useState<boolean | undefined>(); ] = 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>(() => { const src = useMemo<VideoSrc | undefined>(() => {
if (!source) { if (!source) {
return undefined; return undefined;
@ -598,13 +608,78 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
: ViewType.SURFACE; : ViewType.SURFACE;
}, [drm, useSecureView, useTextureView, viewType]); }, [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 ( return (
<View style={style}> <View style={style}>
<NativeVideoComponent <NativeVideoComponent
ref={nativeRef} ref={nativeRef}
{...rest} {...rest}
src={src} src={src}
style={StyleSheet.absoluteFill} style={_style}
resizeMode={resizeMode} resizeMode={resizeMode}
restoreUserInterfaceForPIPStopCompletionHandler={ restoreUserInterfaceForPIPStopCompletionHandler={
_restoreUserInterfaceForPIPStopCompletionHandler _restoreUserInterfaceForPIPStopCompletionHandler
@ -679,9 +754,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
} }
viewType={_viewType} viewType={_viewType}
/> />
{hasPoster && showPoster ? ( {_renderPoster()}
<Image style={posterStyle} source={{uri: poster}} />
) : null}
</View> </View>
); );
}, },

View File

@ -1,6 +1,14 @@
import type {ISO639_1} from './language'; import type {ISO639_1} from './language';
import type {ReactVideoEvents} from './events'; 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 VideoResizeMode from './ResizeMode';
import type FilterType from './FilterType'; import type FilterType from './FilterType';
import type ViewType from './ViewType'; 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<{ export type VideoMetadata = Readonly<{
title?: string; title?: string;
subtitle?: string; subtitle?: string;
@ -243,12 +258,14 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
pictureInPicture?: boolean; // iOS pictureInPicture?: boolean; // iOS
playInBackground?: boolean; playInBackground?: boolean;
playWhenInactive?: boolean; // iOS playWhenInactive?: boolean; // iOS
poster?: string; poster?: string | ReactVideoPoster; // string is deprecated
/** @deprecated use **resizeMode** key in **poster** props instead */
posterResizeMode?: EnumValues<PosterResizeModeType>; posterResizeMode?: EnumValues<PosterResizeModeType>;
preferredForwardBufferDuration?: number; // iOS preferredForwardBufferDuration?: number; // iOS
preventsDisplaySleepDuringVideoPlayback?: boolean; preventsDisplaySleepDuringVideoPlayback?: boolean;
progressUpdateInterval?: number; progressUpdateInterval?: number;
rate?: number; rate?: number;
renderLoader?: ReactNode;
repeat?: boolean; repeat?: boolean;
reportBandwidth?: boolean; //Android reportBandwidth?: boolean; //Android
resizeMode?: EnumValues<VideoResizeMode>; resizeMode?: EnumValues<VideoResizeMode>;