Add react-native-web support (#3958)

Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
This commit is contained in:
Zoe Roux 2024-11-13 21:19:57 +01:00 committed by GitHub
parent d45300270e
commit 5fa77c4562
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 608 additions and 36 deletions

Binary file not shown.

View File

@ -103,7 +103,7 @@ Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to e
### `onBuffer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Callback function that is called when the player buffers.
@ -219,6 +219,9 @@ Payload: none
Callback function that is called when the media is loaded and ready to play.
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
Payload:
| Property | Type | Description |
@ -292,7 +295,7 @@ Example:
### `onPlaybackStateChanged`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the playback state changes.
@ -463,7 +466,7 @@ Payload: none
### `onSeek`
<PlatformsList types={['Android', 'iOS', 'Windows UWP']} />
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
Callback function that is called when a seek completes.
@ -604,7 +607,7 @@ Example:
### `onVolumeChange`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the volume of player changes.

View File

@ -6,7 +6,7 @@ This page shows the list of available methods
### `dismissFullscreenPlayer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`dismissFullscreenPlayer(): Promise<void>`
@ -17,7 +17,7 @@ Take the player out of fullscreen mode.
### `pause`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`pause(): Promise<void>`
@ -25,7 +25,7 @@ Pause the video.
### `presentFullscreenPlayer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`presentFullscreenPlayer(): Promise<void>`
@ -40,7 +40,7 @@ On Android, this puts the navigation controls in fullscreen mode. It is not a co
### `resume`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`resume(): Promise<void>`
@ -100,7 +100,7 @@ tolerance is the max distance in milliseconds from the seconds position that's a
### `setVolume`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`setVolume(value): Promise<void>`
@ -108,7 +108,7 @@ This function will change the volume exactly like [volume](./props#volume) prope
### `getCurrentPosition`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`getCurrentPosition(): Promise<number>`
@ -127,7 +127,7 @@ Changing source with this function will overide source provided as props.
### `setFullScreen`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`setFullScreen(fullscreen): Promise<void>`
@ -137,6 +137,13 @@ On iOS, this displays the video in a fullscreen view controller with controls.
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
### `nativeHtmlVideoRef`
<PlatformsList types={['web']} />
A ref to the underlying html video element. This can be used if you need to integrate a 3d party, web only video library (like hls.js, shaka, video.js...).
### Example Usage
```tsx
@ -188,7 +195,7 @@ Possible values are:
### `isCodecSupported`
<PlatformsList types={['Android']} />
<PlatformsList types={['Android', 'web']} />
Indicates whether the provided codec is supported level supported by device.

View File

@ -131,7 +131,7 @@ When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured,
### `controls`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Determines whether to show player controls.
@ -300,7 +300,7 @@ Whether this video view should be focusable with a non-touch input device, eg. r
### `fullscreen`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Controls whether the player enters fullscreen on play.
See [presentFullscreenPlayer](#presentfullscreenplayer) for details.
@ -316,7 +316,7 @@ If a preferred [fullscreenOrientation](#fullscreenorientation) is set, causes th
### `fullscreenOrientation`
<PlatformsList types={['iOS', 'visionOS']} />
<PlatformsList types={['iOS', 'visionOS', 'web']} />
- **all (default)** -
- **landscape**
@ -709,6 +709,8 @@ The docs for this prop are incomplete and will be updated as each option is inve
> ⚠️ on iOS, you file name must not contain spaces eg. `my video.mp4` will not work, use `my-video.mp4` instead
<PlatformsList types={['Android', 'iOS', 'visionOS', 'Windows UWP']} />
Example:
Pass directly the asset to play (deprecated)
@ -820,7 +822,7 @@ Example:
#### Start playback at a specific point in time
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Provide an optional `startPosition` for video. Value is in milliseconds. If the `cropStart` prop is applied, it will be applied from that point forward.
(If it is negative or undefined or null, it is ignored)
@ -1048,7 +1050,7 @@ textTracks={[
### `showNotificationControls`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Controls whether to show media controls in the notification area.
For Android each Video component will have its own notification controls and for iOS only one notification control will be shown for the last Active Video component.

View File

@ -8,6 +8,7 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
- Exoplayer for android
- AVplayer for iOS, tvOS and visionOS
- Windows UWP for windows
- HTML5 for web
- Trick mode support
- Subtitles (embeded or side loaded)
- DRM support

View File

@ -181,3 +181,12 @@ Select RCTVideo-tvOS
Run `pod install` in the `visionos` directory of your project
</details>
<details>
<summary>web</summary>
Nothing to do, everything should work out of the box.
Note that only basic video support is present, no hls/dash or ads/drm for now.
</details>

View File

@ -6,7 +6,7 @@ This directory contains examples for `react-native-video` - this is a guide that
- **[`bare`](#bare)** - Main example ([react-native-test-app](https://github.com/microsoft/react-native-test-app) - bare react-native app) that you can run on: iOS, Android, Windows, visionOS
- **[`expo`](#expo)** - Expo example that you can run on: iOS, Android, tvOS, web (support coming soon)
- **[`expo`](#expo)** - Expo example that you can run on: iOS, Android, tvOS, web
### Updating Examples Content
@ -151,7 +151,9 @@ cd examples/expo && yarn install
> Setup for android is not complete yet. Please use bare app for android testing.
- For Web:
Support for web is coming soon.
```bash
yarn web
```
If Metro Bundler is not running (or it did not start), you can start it by running:

View File

@ -78,6 +78,14 @@ export const srcAllPlatformList = [
description: 'another bunny (can be saved)',
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
metadata: {
title: 'Custom Title',
subtitle: 'Custom Subtitle',
artist: 'Custom Artist',
description: 'Custom Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: 'sintel with subtitles',

View File

@ -1,4 +1,4 @@
import {StyleSheet} from 'react-native';
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
container: {
@ -63,6 +63,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
overflow: 'hidden',
paddingBottom: 10,
paddingTop: Platform.OS === 'web' ? 25 : 0,
},
rateControl: {
flex: 1,
@ -146,7 +147,7 @@ const styles = StyleSheet.create({
},
picker: {
flex: 1,
color: 'white',
color: Platform.OS === 'web' ? 'black' : 'white',
flexDirection: 'row',
justifyContent: 'center',
width: 100,

View File

@ -15,13 +15,15 @@
"update-src": "echo 'Updating src from ../bare/src' && rm -r ./src && cp -r ../bare/src ./src && echo 'Updated src from ../bare/src'"
},
"dependencies": {
"@expo/metro-runtime": "^3.2.3",
"@react-native-picker/picker": "2.8.1",
"expo": "~51.0.31",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "npm:react-native-tvos@~0.74.5-0"
"react-native": "npm:react-native-tvos@~0.74.5-0",
"react-native-web": "^0.19.13"
},
"devDependencies": {
"@babel/core": "^7.24.0",

View File

@ -78,6 +78,14 @@ export const srcAllPlatformList = [
description: 'another bunny (can be saved)',
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
metadata: {
title: 'Custom Title',
subtitle: 'Custom Subtitle',
artist: 'Custom Artist',
description: 'Custom Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: 'sintel with subtitles',

View File

@ -1,4 +1,4 @@
import {StyleSheet} from 'react-native';
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
container: {
@ -63,6 +63,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
overflow: 'hidden',
paddingBottom: 10,
paddingTop: Platform.OS === 'web' ? 25 : 0,
},
rateControl: {
flex: 1,
@ -146,7 +147,7 @@ const styles = StyleSheet.create({
},
picker: {
flex: 1,
color: 'white',
color: Platform.OS === 'web' ? 'black' : 'white',
flexDirection: 'row',
justifyContent: 'center',
width: 100,

View File

@ -3,7 +3,7 @@
"version": "6.7.0",
"description": "A <Video /> element for react-native",
"main": "lib/index",
"source": "src/index",
"source": "src/index.ts",
"react-native": "src/index",
"license": "MIT",
"author": "Community Contributors",

13
shell.nix Normal file
View File

@ -0,0 +1,13 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
packages = with pkgs; [
nodejs-18_x
nodePackages.yarn
bun
eslint_d
prettierd
jdk11
(jdt-language-server.override { jdk = jdk11; })
];
}

View File

@ -45,8 +45,7 @@ import {
resolveAssetSourceForVideo,
} from './utils';
import NativeVideoManager from './specs/NativeVideoManager';
import type {VideoSaveData} from './specs/NativeVideoManager';
import {CmcdMode, ViewType} from './types';
import {ViewType, type VideoSaveData, CmcdMode} from './types';
import type {
OnLoadData,
OnTextTracksData,

461
src/Video.web.tsx Normal file
View File

@ -0,0 +1,461 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
type RefObject,
useState,
} from 'react';
import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
// stolen from https://stackoverflow.com/a/77278013/21726244
const isDeepEqual = <T,>(a: T, b: T): boolean => {
if (a === b) {
return true;
}
const bothAreObjects =
a && b && typeof a === 'object' && typeof b === 'object';
return Boolean(
bothAreObjects &&
Object.keys(a).length === Object.keys(b).length &&
Object.entries(a).every(([k, v]) => isDeepEqual(v, b[k as keyof T])),
);
};
const Video = forwardRef<VideoRef, ReactVideoProps>(
(
{
source,
paused,
muted,
volume,
rate,
repeat,
controls,
showNotificationControls = false,
poster,
fullscreen,
fullscreenAutorotate,
fullscreenOrientation,
onBuffer,
onLoad,
onProgress,
onPlaybackRateChange,
onError,
onReadyForDisplay,
onSeek,
onVolumeChange,
onEnd,
onPlaybackStateChanged,
},
ref,
) => {
const nativeRef = useRef<HTMLVideoElement>(null);
const isSeeking = useRef(false);
const seek = useCallback(
async (time: number, _tolerance?: number) => {
if (isNaN(time)) {
throw new Error('Specified time is not a number');
}
if (!nativeRef.current) {
console.warn('Video Component is not mounted');
return;
}
time = Math.max(0, Math.min(time, nativeRef.current.duration));
nativeRef.current.currentTime = time;
onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime});
},
[onSeek],
);
const [src, setSource] = useState(source);
const currentSourceProp = useRef(source);
useEffect(() => {
if (isDeepEqual(source, currentSourceProp.current)) {
return;
}
currentSourceProp.current = source;
setSource(source);
}, [source]);
const pause = useCallback(() => {
if (!nativeRef.current) {
return;
}
nativeRef.current.pause();
}, []);
const resume = useCallback(() => {
if (!nativeRef.current) {
return;
}
nativeRef.current.play();
}, []);
const setVolume = useCallback((vol: number) => {
if (!nativeRef.current) {
return;
}
nativeRef.current.volume = Math.max(0, Math.min(vol, 100)) / 100;
}, []);
const getCurrentPosition = useCallback(async () => {
if (!nativeRef.current) {
throw new Error('Video Component is not mounted');
}
return nativeRef.current.currentTime;
}, []);
const unsupported = useCallback(() => {
throw new Error('This is unsupported on the web');
}, []);
// Stock this in a ref to not invalidate memoization when those changes.
const fsPrefs = useRef({
fullscreenAutorotate,
fullscreenOrientation,
});
fsPrefs.current = {
fullscreenOrientation,
fullscreenAutorotate,
};
const setFullScreen = useCallback(
async (
newVal: boolean,
orientation?: ReactVideoProps['fullscreenOrientation'],
autorotate?: boolean,
) => {
orientation ??= fsPrefs.current.fullscreenOrientation;
autorotate ??= fsPrefs.current.fullscreenAutorotate;
try {
if (newVal) {
await nativeRef.current?.requestFullscreen({
navigationUI: 'hide',
});
if (orientation === 'all' || !orientation || autorotate) {
screen.orientation.unlock();
} else {
await screen.orientation.lock(orientation);
}
} else {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
screen.orientation.unlock();
}
} catch (e) {
// Changing fullscreen status without a button click is not allowed so it throws.
// Some browsers also used to throw when locking screen orientation was not supported.
console.error('Could not toggle fullscreen/screen lock status', e);
}
},
[],
);
useEffect(() => {
setFullScreen(
fullscreen || false,
fullscreenOrientation,
fullscreenAutorotate,
);
}, [
setFullScreen,
fullscreen,
fullscreenAutorotate,
fullscreenOrientation,
]);
const presentFullscreenPlayer = useCallback(
() => setFullScreen(true),
[setFullScreen],
);
const dismissFullscreenPlayer = useCallback(
() => setFullScreen(false),
[setFullScreen],
);
useImperativeHandle(
ref,
() => ({
seek,
setSource,
pause,
resume,
setVolume,
getCurrentPosition,
presentFullscreenPlayer,
dismissFullscreenPlayer,
setFullScreen,
save: unsupported,
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
nativeHtmlVideoRef: nativeRef,
}),
[
seek,
setSource,
pause,
resume,
unsupported,
setVolume,
getCurrentPosition,
nativeRef,
presentFullscreenPlayer,
dismissFullscreenPlayer,
setFullScreen,
],
);
useEffect(() => {
if (paused) {
pause();
} else {
resume();
}
}, [paused, pause, resume]);
useEffect(() => {
if (volume === undefined || isNaN(volume)) {
return;
}
setVolume(volume);
}, [volume, setVolume]);
// we use a ref to prevent triggerring the useEffect when the component rerender with a non-stable `onPlaybackStateChanged`.
const playbackStateRef = useRef(onPlaybackStateChanged);
playbackStateRef.current = onPlaybackStateChanged;
useEffect(() => {
// Not sure about how to do this but we want to wait for nativeRef to be initialized
setTimeout(() => {
if (!nativeRef.current) {
return;
}
// Set play state to the player's value (if autoplay is denied)
// This is useful if our UI is in a play state but autoplay got denied so
// the video is actually in a paused state.
playbackStateRef.current?.({
isPlaying: !nativeRef.current.paused,
isSeeking: isSeeking.current,
});
}, 500);
}, []);
useEffect(() => {
if (!nativeRef.current || rate === undefined) {
return;
}
nativeRef.current.playbackRate = rate;
}, [rate]);
useMediaSession(src?.metadata, nativeRef, showNotificationControls);
return (
<video
ref={nativeRef}
src={src?.uri as string | undefined}
muted={muted}
autoPlay={!paused}
controls={controls}
loop={repeat}
playsInline
poster={
typeof poster === 'object'
? typeof poster.source === 'object'
? poster.source.uri
: undefined
: poster
}
onCanPlay={() => onBuffer?.({isBuffering: false})}
onWaiting={() => onBuffer?.({isBuffering: true})}
onRateChange={() => {
if (!nativeRef.current) {
return;
}
onPlaybackRateChange?.({
playbackRate: nativeRef.current?.playbackRate,
});
}}
onDurationChange={() => {
if (!nativeRef.current) {
return;
}
onLoad?.({
currentTime: nativeRef.current.currentTime,
duration: nativeRef.current.duration,
videoTracks: [],
textTracks: [],
audioTracks: [],
naturalSize: {
width: nativeRef.current.videoWidth,
height: nativeRef.current.videoHeight,
orientation: 'landscape',
},
});
}}
onTimeUpdate={() => {
if (!nativeRef.current) {
return;
}
onProgress?.({
currentTime: nativeRef.current.currentTime,
playableDuration: nativeRef.current.buffered.length
? nativeRef.current.buffered.end(
nativeRef.current.buffered.length - 1,
)
: 0,
seekableDuration: 0,
});
}}
onLoadedData={() => onReadyForDisplay?.()}
onError={() => {
if (!nativeRef.current?.error) {
return;
}
onError?.({
error: {
errorString: nativeRef.current.error.message ?? 'Unknown error',
code: nativeRef.current.error.code,
},
});
}}
onLoadedMetadata={() => {
if (src?.startPosition) {
seek(src.startPosition / 1000);
}
}}
onPlay={() =>
onPlaybackStateChanged?.({
isPlaying: true,
isSeeking: isSeeking.current,
})
}
onPause={() =>
onPlaybackStateChanged?.({
isPlaying: false,
isSeeking: isSeeking.current,
})
}
onSeeking={() => (isSeeking.current = true)}
onSeeked={() => {
// only trigger this if it's from UI seek.
// if it was triggered via ref.seek(), onSeek has already been called
if (isSeeking.current) {
isSeeking.current = false;
onSeek?.({
seekTime: nativeRef.current!.currentTime,
currentTime: nativeRef.current!.currentTime,
});
}
}}
onVolumeChange={() => {
if (!nativeRef.current) {
return;
}
onVolumeChange?.({volume: nativeRef.current.volume});
}}
onEnded={onEnd}
style={videoStyle}
/>
);
},
);
const videoStyle = {
position: 'absolute',
inset: 0,
objectFit: 'contain',
width: '100%',
height: '100%',
} satisfies React.CSSProperties;
const useMediaSession = (
metadata: VideoMetadata | undefined,
nativeRef: RefObject<HTMLVideoElement>,
showNotification: boolean,
) => {
const isPlaying = !nativeRef.current?.paused ?? false;
const progress = nativeRef.current?.currentTime ?? 0;
const duration = Number.isFinite(nativeRef.current?.duration)
? nativeRef.current?.duration
: undefined;
const playbackRate = nativeRef.current?.playbackRate ?? 1;
const enabled = 'mediaSession' in navigator && showNotification;
useEffect(() => {
if (enabled) {
navigator.mediaSession.metadata = new MediaMetadata({
title: metadata?.title,
artist: metadata?.artist,
artwork: metadata?.imageUri ? [{src: metadata.imageUri}] : undefined,
});
}
}, [enabled, metadata]);
useEffect(() => {
if (!enabled) {
return;
}
const seekTo = (time: number) => {
if (nativeRef.current) {
nativeRef.current.currentTime = time;
}
};
const seekRelative = (offset: number) => {
if (nativeRef.current) {
nativeRef.current.currentTime = nativeRef.current.currentTime + offset;
}
};
const mediaActions: [
MediaSessionAction,
MediaSessionActionHandler | null,
][] = [
['play', () => nativeRef.current?.play()],
['pause', () => nativeRef.current?.pause()],
[
'seekbackward',
(evt: MediaSessionActionDetails) =>
seekRelative(evt.seekOffset ? -evt.seekOffset : -10),
],
[
'seekforward',
(evt: MediaSessionActionDetails) =>
seekRelative(evt.seekOffset ? evt.seekOffset : 10),
],
['seekto', (evt: MediaSessionActionDetails) => seekTo(evt.seekTime!)],
];
for (const [action, handler] of mediaActions) {
try {
navigator.mediaSession.setActionHandler(action, handler);
} catch {
// ignored
}
}
}, [enabled, nativeRef]);
useEffect(() => {
if (enabled) {
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
}
}, [isPlaying, enabled]);
useEffect(() => {
if (enabled && duration !== undefined) {
navigator.mediaSession.setPositionState({
position: Math.min(progress, duration),
duration,
playbackRate: playbackRate,
});
}
}, [progress, duration, playbackRate, enabled]);
};
Video.displayName = 'Video';
export default Video;

View File

@ -0,0 +1,34 @@
/// <reference lib="dom" />
import type {VideoDecoderInfoModuleType} from './specs/NativeVideoDecoderInfoModule';
const canPlay = (codec: string): boolean => {
// most chrome based browser (and safari I think) supports matroska but reports they do not.
// for those browsers, only check the codecs and not the container.
if (navigator.userAgent.search('Firefox') === -1) {
codec = codec.replace('video/x-matroska', 'video/mp4');
}
return !!MediaSource.isTypeSupported(codec);
};
export const VideoDecoderProperties = {
async getWidevineLevel() {
return 0;
},
async isCodecSupported(
mimeType: string,
_width: number,
_height: number,
): Promise<'unsupported' | 'hardware' | 'software'> {
// TODO: Figure out if we can get hardware support information
return canPlay(mimeType) ? 'software' : 'unsupported';
},
async isHEVCSupported(): Promise<'unsupported' | 'hardware' | 'software'> {
// Just a dummy vidoe mime type codec with HEVC to check.
return canPlay('video/x-matroska; codecs="hvc1.1.4.L96.BO"')
? 'software'
: 'unsupported';
},
} satisfies VideoDecoderInfoModuleType;

View File

@ -1,6 +1,5 @@
import Video from './Video';
export {VideoDecoderProperties} from './VideoDecoderProperties';
export * from './types';
export type {VideoRef} from './Video';
export {Video};
export default Video;

View File

@ -2,7 +2,7 @@ import {NativeModules} from 'react-native';
import type {Int32} from 'react-native/Libraries/Types/CodegenTypes';
// @TODO rename to "Spec" when applying new arch
interface VideoDecoderInfoModuleType {
export interface VideoDecoderInfoModuleType {
getWidevineLevel: () => Promise<Int32>;
isCodecSupported: (
mimeType: string,

View File

@ -4,10 +4,7 @@ import type {
Float,
UnsafeObject,
} from 'react-native/Libraries/Types/CodegenTypes';
export type VideoSaveData = {
uri: string;
};
import type {VideoSaveData} from '../types/video-ref';
// @TODO rename to "Spec" when applying new arch
export interface VideoManagerType {

View File

@ -285,12 +285,12 @@ type OnReceiveAdEventData = Readonly<{
export type OnVideoErrorData = Readonly<{
error: Readonly<{
errorString?: string; // android
errorString?: string; // android | web
errorException?: string; // android
errorStackTrace?: string; // android
errorCode?: string; // android
error?: string; // ios
code?: Int32; // ios
code?: Int32; // ios | web
localizedDescription?: string; // ios
localizedFailureReason?: string; // ios
localizedRecoverySuggestion?: string; // ios

View File

@ -20,6 +20,8 @@ import type {
OnVolumeChangeData,
} from '../specs/VideoNativeComponent';
export type * from '../specs/VideoNativeComponent';
export type AudioTrack = OnAudioTracksData['audioTracks'][number];
export type TextTrack = OnTextTracksData['textTracks'][number];
export type VideoTrack = OnVideoTracksData['videoTracks'][number];

View File

@ -7,4 +7,4 @@ export {default as ResizeMode} from './ResizeMode';
export {default as TextTrackType} from './TextTrackType';
export {default as ViewType} from './ViewType';
export * from './video';
export * from '../specs/VideoNativeComponent';
export * from './video-ref';

23
src/types/video-ref.ts Normal file
View File

@ -0,0 +1,23 @@
import type {RefObject} from 'react';
import {ReactVideoSource} from './video';
export type VideoSaveData = {
uri: string;
};
export interface VideoRef {
seek: (time: number, tolerance?: number) => void;
resume: () => void;
pause: () => void;
presentFullscreenPlayer: () => void;
dismissFullscreenPlayer: () => void;
restoreUserInterfaceForPictureInPictureStopCompleted: (
restore: boolean,
) => void;
save: (options: object) => Promise<VideoSaveData>;
setVolume: (volume: number) => void;
getCurrentPosition: () => Promise<number>;
setFullScreen: (fullScreen: boolean) => void;
setSource: (source?: ReactVideoSource) => void;
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
}