Add react-native-web support (#3958)
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
This commit is contained in:
parent
d45300270e
commit
5fa77c4562
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@ -103,7 +103,7 @@ Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to e
|
|||||||
|
|
||||||
### `onBuffer`
|
### `onBuffer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the player buffers.
|
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.
|
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:
|
Payload:
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
@ -292,7 +295,7 @@ Example:
|
|||||||
|
|
||||||
### `onPlaybackStateChanged`
|
### `onPlaybackStateChanged`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the playback state changes.
|
Callback function that is called when the playback state changes.
|
||||||
|
|
||||||
@ -463,7 +466,7 @@ Payload: none
|
|||||||
|
|
||||||
### `onSeek`
|
### `onSeek`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'Windows UWP']} />
|
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when a seek completes.
|
Callback function that is called when a seek completes.
|
||||||
|
|
||||||
@ -604,7 +607,7 @@ Example:
|
|||||||
|
|
||||||
### `onVolumeChange`
|
### `onVolumeChange`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the volume of player changes.
|
Callback function that is called when the volume of player changes.
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ This page shows the list of available methods
|
|||||||
|
|
||||||
### `dismissFullscreenPlayer`
|
### `dismissFullscreenPlayer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`dismissFullscreenPlayer(): Promise<void>`
|
`dismissFullscreenPlayer(): Promise<void>`
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ Take the player out of fullscreen mode.
|
|||||||
|
|
||||||
### `pause`
|
### `pause`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`pause(): Promise<void>`
|
`pause(): Promise<void>`
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ Pause the video.
|
|||||||
|
|
||||||
### `presentFullscreenPlayer`
|
### `presentFullscreenPlayer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`presentFullscreenPlayer(): Promise<void>`
|
`presentFullscreenPlayer(): Promise<void>`
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ On Android, this puts the navigation controls in fullscreen mode. It is not a co
|
|||||||
|
|
||||||
### `resume`
|
### `resume`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`resume(): Promise<void>`
|
`resume(): Promise<void>`
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ tolerance is the max distance in milliseconds from the seconds position that's a
|
|||||||
|
|
||||||
### `setVolume`
|
### `setVolume`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`setVolume(value): Promise<void>`
|
`setVolume(value): Promise<void>`
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ This function will change the volume exactly like [volume](./props#volume) prope
|
|||||||
|
|
||||||
### `getCurrentPosition`
|
### `getCurrentPosition`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`getCurrentPosition(): Promise<number>`
|
`getCurrentPosition(): Promise<number>`
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ Changing source with this function will overide source provided as props.
|
|||||||
|
|
||||||
### `setFullScreen`
|
### `setFullScreen`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`setFullScreen(fullscreen): Promise<void>`
|
`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.
|
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
|
### Example Usage
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@ -188,7 +195,7 @@ Possible values are:
|
|||||||
|
|
||||||
### `isCodecSupported`
|
### `isCodecSupported`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android', 'web']} />
|
||||||
|
|
||||||
Indicates whether the provided codec is supported level supported by device.
|
Indicates whether the provided codec is supported level supported by device.
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured,
|
|||||||
|
|
||||||
### `controls`
|
### `controls`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Determines whether to show player controls.
|
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`
|
### `fullscreen`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Controls whether the player enters fullscreen on play.
|
Controls whether the player enters fullscreen on play.
|
||||||
See [presentFullscreenPlayer](#presentfullscreenplayer) for details.
|
See [presentFullscreenPlayer](#presentfullscreenplayer) for details.
|
||||||
@ -316,7 +316,7 @@ If a preferred [fullscreenOrientation](#fullscreenorientation) is set, causes th
|
|||||||
|
|
||||||
### `fullscreenOrientation`
|
### `fullscreenOrientation`
|
||||||
|
|
||||||
<PlatformsList types={['iOS', 'visionOS']} />
|
<PlatformsList types={['iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
- **all (default)** -
|
- **all (default)** -
|
||||||
- **landscape**
|
- **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
|
> ⚠️ 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:
|
Example:
|
||||||
|
|
||||||
Pass directly the asset to play (deprecated)
|
Pass directly the asset to play (deprecated)
|
||||||
@ -820,7 +822,7 @@ Example:
|
|||||||
|
|
||||||
#### Start playback at a specific point in time
|
#### 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.
|
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)
|
(If it is negative or undefined or null, it is ignored)
|
||||||
@ -1048,7 +1050,7 @@ textTracks={[
|
|||||||
|
|
||||||
### `showNotificationControls`
|
### `showNotificationControls`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
Controls whether to show media controls in the notification area.
|
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.
|
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.
|
||||||
|
@ -8,6 +8,7 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
|
|||||||
- Exoplayer for android
|
- Exoplayer for android
|
||||||
- AVplayer for iOS, tvOS and visionOS
|
- AVplayer for iOS, tvOS and visionOS
|
||||||
- Windows UWP for windows
|
- Windows UWP for windows
|
||||||
|
- HTML5 for web
|
||||||
- Trick mode support
|
- Trick mode support
|
||||||
- Subtitles (embeded or side loaded)
|
- Subtitles (embeded or side loaded)
|
||||||
- DRM support
|
- DRM support
|
||||||
|
@ -181,3 +181,12 @@ Select RCTVideo-tvOS
|
|||||||
Run `pod install` in the `visionos` directory of your project
|
Run `pod install` in the `visionos` directory of your project
|
||||||
|
|
||||||
</details>
|
</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>
|
||||||
|
@ -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
|
- **[`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
|
### 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.
|
> Setup for android is not complete yet. Please use bare app for android testing.
|
||||||
|
|
||||||
- For Web:
|
- 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:
|
If Metro Bundler is not running (or it did not start), you can start it by running:
|
||||||
|
|
||||||
|
@ -78,6 +78,14 @@ export const srcAllPlatformList = [
|
|||||||
description: 'another bunny (can be saved)',
|
description: 'another bunny (can be saved)',
|
||||||
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
|
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
|
||||||
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
|
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',
|
description: 'sintel with subtitles',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {StyleSheet} from 'react-native';
|
import {Platform, StyleSheet} from 'react-native';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -63,6 +63,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
|
paddingTop: Platform.OS === 'web' ? 25 : 0,
|
||||||
},
|
},
|
||||||
rateControl: {
|
rateControl: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -146,7 +147,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
picker: {
|
picker: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
color: 'white',
|
color: Platform.OS === 'web' ? 'black' : 'white',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
@ -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'"
|
"update-src": "echo 'Updating src from ../bare/src' && rm -r ./src && cp -r ../bare/src ./src && echo 'Updated src from ../bare/src'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/metro-runtime": "^3.2.3",
|
||||||
"@react-native-picker/picker": "2.8.1",
|
"@react-native-picker/picker": "2.8.1",
|
||||||
"expo": "~51.0.31",
|
"expo": "~51.0.31",
|
||||||
"expo-splash-screen": "~0.27.5",
|
"expo-splash-screen": "~0.27.5",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.24.0",
|
||||||
|
@ -78,6 +78,14 @@ export const srcAllPlatformList = [
|
|||||||
description: 'another bunny (can be saved)',
|
description: 'another bunny (can be saved)',
|
||||||
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
|
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
|
||||||
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
|
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',
|
description: 'sintel with subtitles',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {StyleSheet} from 'react-native';
|
import {Platform, StyleSheet} from 'react-native';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -63,6 +63,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
|
paddingTop: Platform.OS === 'web' ? 25 : 0,
|
||||||
},
|
},
|
||||||
rateControl: {
|
rateControl: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -146,7 +147,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
picker: {
|
picker: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
color: 'white',
|
color: Platform.OS === 'web' ? 'black' : 'white',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"version": "6.7.0",
|
"version": "6.7.0",
|
||||||
"description": "A <Video /> element for react-native",
|
"description": "A <Video /> element for react-native",
|
||||||
"main": "lib/index",
|
"main": "lib/index",
|
||||||
"source": "src/index",
|
"source": "src/index.ts",
|
||||||
"react-native": "src/index",
|
"react-native": "src/index",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Community Contributors",
|
"author": "Community Contributors",
|
||||||
|
13
shell.nix
Normal file
13
shell.nix
Normal 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; })
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -45,8 +45,7 @@ import {
|
|||||||
resolveAssetSourceForVideo,
|
resolveAssetSourceForVideo,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import NativeVideoManager from './specs/NativeVideoManager';
|
import NativeVideoManager from './specs/NativeVideoManager';
|
||||||
import type {VideoSaveData} from './specs/NativeVideoManager';
|
import {ViewType, type VideoSaveData, CmcdMode} from './types';
|
||||||
import {CmcdMode, ViewType} from './types';
|
|
||||||
import type {
|
import type {
|
||||||
OnLoadData,
|
OnLoadData,
|
||||||
OnTextTracksData,
|
OnTextTracksData,
|
||||||
|
461
src/Video.web.tsx
Normal file
461
src/Video.web.tsx
Normal 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;
|
34
src/VideoDecoderProperties.web.ts
Normal file
34
src/VideoDecoderProperties.web.ts
Normal 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;
|
@ -1,6 +1,5 @@
|
|||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
export {VideoDecoderProperties} from './VideoDecoderProperties';
|
export {VideoDecoderProperties} from './VideoDecoderProperties';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type {VideoRef} from './Video';
|
|
||||||
export {Video};
|
export {Video};
|
||||||
export default Video;
|
export default Video;
|
||||||
|
@ -2,7 +2,7 @@ import {NativeModules} from 'react-native';
|
|||||||
import type {Int32} from 'react-native/Libraries/Types/CodegenTypes';
|
import type {Int32} from 'react-native/Libraries/Types/CodegenTypes';
|
||||||
|
|
||||||
// @TODO rename to "Spec" when applying new arch
|
// @TODO rename to "Spec" when applying new arch
|
||||||
interface VideoDecoderInfoModuleType {
|
export interface VideoDecoderInfoModuleType {
|
||||||
getWidevineLevel: () => Promise<Int32>;
|
getWidevineLevel: () => Promise<Int32>;
|
||||||
isCodecSupported: (
|
isCodecSupported: (
|
||||||
mimeType: string,
|
mimeType: string,
|
||||||
|
@ -4,10 +4,7 @@ import type {
|
|||||||
Float,
|
Float,
|
||||||
UnsafeObject,
|
UnsafeObject,
|
||||||
} from 'react-native/Libraries/Types/CodegenTypes';
|
} from 'react-native/Libraries/Types/CodegenTypes';
|
||||||
|
import type {VideoSaveData} from '../types/video-ref';
|
||||||
export type VideoSaveData = {
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO rename to "Spec" when applying new arch
|
// @TODO rename to "Spec" when applying new arch
|
||||||
export interface VideoManagerType {
|
export interface VideoManagerType {
|
||||||
|
@ -285,12 +285,12 @@ type OnReceiveAdEventData = Readonly<{
|
|||||||
|
|
||||||
export type OnVideoErrorData = Readonly<{
|
export type OnVideoErrorData = Readonly<{
|
||||||
error: Readonly<{
|
error: Readonly<{
|
||||||
errorString?: string; // android
|
errorString?: string; // android | web
|
||||||
errorException?: string; // android
|
errorException?: string; // android
|
||||||
errorStackTrace?: string; // android
|
errorStackTrace?: string; // android
|
||||||
errorCode?: string; // android
|
errorCode?: string; // android
|
||||||
error?: string; // ios
|
error?: string; // ios
|
||||||
code?: Int32; // ios
|
code?: Int32; // ios | web
|
||||||
localizedDescription?: string; // ios
|
localizedDescription?: string; // ios
|
||||||
localizedFailureReason?: string; // ios
|
localizedFailureReason?: string; // ios
|
||||||
localizedRecoverySuggestion?: string; // ios
|
localizedRecoverySuggestion?: string; // ios
|
||||||
|
@ -20,6 +20,8 @@ import type {
|
|||||||
OnVolumeChangeData,
|
OnVolumeChangeData,
|
||||||
} from '../specs/VideoNativeComponent';
|
} from '../specs/VideoNativeComponent';
|
||||||
|
|
||||||
|
export type * from '../specs/VideoNativeComponent';
|
||||||
|
|
||||||
export type AudioTrack = OnAudioTracksData['audioTracks'][number];
|
export type AudioTrack = OnAudioTracksData['audioTracks'][number];
|
||||||
export type TextTrack = OnTextTracksData['textTracks'][number];
|
export type TextTrack = OnTextTracksData['textTracks'][number];
|
||||||
export type VideoTrack = OnVideoTracksData['videoTracks'][number];
|
export type VideoTrack = OnVideoTracksData['videoTracks'][number];
|
||||||
|
@ -7,4 +7,4 @@ export {default as ResizeMode} from './ResizeMode';
|
|||||||
export {default as TextTrackType} from './TextTrackType';
|
export {default as TextTrackType} from './TextTrackType';
|
||||||
export {default as ViewType} from './ViewType';
|
export {default as ViewType} from './ViewType';
|
||||||
export * from './video';
|
export * from './video';
|
||||||
export * from '../specs/VideoNativeComponent';
|
export * from './video-ref';
|
||||||
|
23
src/types/video-ref.ts
Normal file
23
src/types/video-ref.ts
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user