diff --git a/docs/bun.lockb b/docs/bun.lockb
index fac7725a5d..92719d68b4 100755
Binary files a/docs/bun.lockb and b/docs/bun.lockb differ
diff --git a/docs/pages/component/events.mdx b/docs/pages/component/events.mdx
index 2c2f0a1d79..6e0c149855 100644
--- a/docs/pages/component/events.mdx
+++ b/docs/pages/component/events.mdx
@@ -103,7 +103,7 @@ Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to e
### `onBuffer`
-
+
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`
-
+
Callback function that is called when the playback state changes.
@@ -463,7 +466,7 @@ Payload: none
### `onSeek`
-
+
Callback function that is called when a seek completes.
@@ -604,7 +607,7 @@ Example:
### `onVolumeChange`
-
+
Callback function that is called when the volume of player changes.
diff --git a/docs/pages/component/methods.mdx b/docs/pages/component/methods.mdx
index b3b0ebc091..b3d89a0467 100644
--- a/docs/pages/component/methods.mdx
+++ b/docs/pages/component/methods.mdx
@@ -6,7 +6,7 @@ This page shows the list of available methods
### `dismissFullscreenPlayer`
-
+
`dismissFullscreenPlayer(): Promise`
@@ -17,7 +17,7 @@ Take the player out of fullscreen mode.
### `pause`
-
+
`pause(): Promise`
@@ -25,7 +25,7 @@ Pause the video.
### `presentFullscreenPlayer`
-
+
`presentFullscreenPlayer(): Promise`
@@ -40,7 +40,7 @@ On Android, this puts the navigation controls in fullscreen mode. It is not a co
### `resume`
-
+
`resume(): Promise`
@@ -100,7 +100,7 @@ tolerance is the max distance in milliseconds from the seconds position that's a
### `setVolume`
-
+
`setVolume(value): Promise`
@@ -108,7 +108,7 @@ This function will change the volume exactly like [volume](./props#volume) prope
### `getCurrentPosition`
-
+
`getCurrentPosition(): Promise`
@@ -127,7 +127,7 @@ Changing source with this function will overide source provided as props.
### `setFullScreen`
-
+
`setFullScreen(fullscreen): Promise`
@@ -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`
+
+
+
+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`
-
+
Indicates whether the provided codec is supported level supported by device.
diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx
index c5465df745..e205e6bad0 100644
--- a/docs/pages/component/props.mdx
+++ b/docs/pages/component/props.mdx
@@ -131,7 +131,7 @@ When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured,
### `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`
-
+
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`
-
+
- **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
+
+
Example:
Pass directly the asset to play (deprecated)
@@ -820,7 +822,7 @@ Example:
#### Start playback at a specific point in time
-
+
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`
-
+
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.
diff --git a/docs/pages/index.md b/docs/pages/index.md
index dd4d5281fd..33f20810e3 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -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
diff --git a/docs/pages/installation.md b/docs/pages/installation.md
index 26e1207c47..a1c078ba09 100644
--- a/docs/pages/installation.md
+++ b/docs/pages/installation.md
@@ -181,3 +181,12 @@ Select RCTVideo-tvOS
Run `pod install` in the `visionos` directory of your project
+
+
+web
+
+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.
+
+
diff --git a/examples/README.md b/examples/README.md
index 1d1485cd8a..0a98a4cba5 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -5,7 +5,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
@@ -135,7 +135,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:
```bash
diff --git a/examples/bare/src/constants/general.ts b/examples/bare/src/constants/general.ts
index 02a3329d6b..3db84749dc 100644
--- a/examples/bare/src/constants/general.ts
+++ b/examples/bare/src/constants/general.ts
@@ -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',
diff --git a/examples/bare/src/styles.tsx b/examples/bare/src/styles.tsx
index 61453b7706..ddee2d7dca 100644
--- a/examples/bare/src/styles.tsx
+++ b/examples/bare/src/styles.tsx
@@ -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,
diff --git a/examples/expo/package.json b/examples/expo/package.json
index 165b7936af..19d6896dbe 100644
--- a/examples/expo/package.json
+++ b/examples/expo/package.json
@@ -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",
diff --git a/examples/expo/src/constants/general.ts b/examples/expo/src/constants/general.ts
index 02a3329d6b..3db84749dc 100644
--- a/examples/expo/src/constants/general.ts
+++ b/examples/expo/src/constants/general.ts
@@ -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',
diff --git a/examples/expo/src/styles.tsx b/examples/expo/src/styles.tsx
index 61453b7706..ddee2d7dca 100644
--- a/examples/expo/src/styles.tsx
+++ b/examples/expo/src/styles.tsx
@@ -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,
diff --git a/package.json b/package.json
index c1cdfb88e9..00839de3ed 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "6.7.0",
"description": "A element for react-native",
"main": "lib/index",
- "source": "src/index",
+ "source": "src/index.ts",
"react-native": "src/index",
"license": "MIT",
"author": "Community Contributors",
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000000..bb15d05908
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,13 @@
+{pkgs ? import {}}:
+ pkgs.mkShell {
+ packages = with pkgs; [
+ nodejs-18_x
+ nodePackages.yarn
+ bun
+ eslint_d
+ prettierd
+ jdk11
+ (jdt-language-server.override { jdk = jdk11; })
+ ];
+ }
+
diff --git a/src/Video.tsx b/src/Video.tsx
index 9571493f97..7d9b68269e 100644
--- a/src/Video.tsx
+++ b/src/Video.tsx
@@ -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,
diff --git a/src/Video.web.tsx b/src/Video.web.tsx
new file mode 100644
index 0000000000..ee06a516dd
--- /dev/null
+++ b/src/Video.web.tsx
@@ -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 = (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(
+ (
+ {
+ 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(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 (
+