mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-05 23:06:14 +00:00
Implement html video properties using headless video
This commit is contained in:
@@ -1,24 +1,26 @@
|
|||||||
import shaka from 'shaka-player';
|
import shaka from "shaka-player";
|
||||||
import type { VideoPlayerSource } from '../spec/nitro/VideoPlayerSource.nitro';
|
import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro";
|
||||||
import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode';
|
import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode";
|
||||||
import type { MixAudioMode } from './types/MixAudioMode';
|
import type { MixAudioMode } from "./types/MixAudioMode";
|
||||||
import type { TextTrack } from './types/TextTrack';
|
import type { TextTrack } from "./types/TextTrack";
|
||||||
import type { NoAutocomplete } from './types/Utils';
|
import type { NoAutocomplete } from "./types/Utils";
|
||||||
import type { VideoConfig, VideoSource } from './types/VideoConfig';
|
import type { VideoConfig, VideoSource } from "./types/VideoConfig";
|
||||||
import {
|
import {
|
||||||
tryParseNativeVideoError,
|
tryParseNativeVideoError,
|
||||||
VideoRuntimeError,
|
VideoRuntimeError,
|
||||||
} from './types/VideoError';
|
} from "./types/VideoError";
|
||||||
import type { VideoPlayerBase } from './types/VideoPlayerBase';
|
import type { VideoPlayerBase } from "./types/VideoPlayerBase";
|
||||||
import type { VideoPlayerStatus } from './types/VideoPlayerStatus';
|
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
|
||||||
import { VideoPlayerEvents } from './VideoPlayerEvents';
|
import { VideoPlayerEvents } from "./VideoPlayerEvents";
|
||||||
|
|
||||||
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||||
protected player = new shaka.Player();
|
protected player = new shaka.Player();
|
||||||
|
protected video = document.createElement("video");
|
||||||
|
|
||||||
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
|
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
|
||||||
// Initialize events
|
// Initialize events
|
||||||
super(player.eventEmitter);
|
super(player.eventEmitter);
|
||||||
|
this.player.attach(this.video);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,13 +32,8 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
this.player.destroy();
|
this.player.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
__getNativeRef() {
|
||||||
* Returns the native (hybrid) player instance.
|
return this.video;
|
||||||
* Should not be used outside of the module.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
__getNativePlayer() {
|
|
||||||
return this.player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,8 +44,8 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
const parsedError = tryParseNativeVideoError(error);
|
const parsedError = tryParseNativeVideoError(error);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parsedError instanceof VideoRuntimeError
|
parsedError instanceof VideoRuntimeError &&
|
||||||
&& this.triggerEvent('onError', parsedError)
|
this.triggerEvent("onError", parsedError)
|
||||||
) {
|
) {
|
||||||
// We don't throw errors if onError is provided
|
// We don't throw errors if onError is provided
|
||||||
return;
|
return;
|
||||||
@@ -57,18 +54,6 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
throw parsedError;
|
throw parsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a promise to try parsing native errors to VideoRuntimeError
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
private wrapPromise<T>(promise: Promise<T>) {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
promise.then(resolve).catch((error) => {
|
|
||||||
reject(this.throwError(error));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source
|
// Source
|
||||||
get source(): VideoPlayerSource {
|
get source(): VideoPlayerSource {
|
||||||
return this.player.source;
|
return this.player.source;
|
||||||
@@ -76,85 +61,95 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
// Status
|
// Status
|
||||||
get status(): VideoPlayerStatus {
|
get status(): VideoPlayerStatus {
|
||||||
return this.player.status;
|
if (this.video.error) return "error";
|
||||||
|
if (this.video.readyState === HTMLMediaElement.HAVE_NOTHING) return "idle";
|
||||||
|
if (
|
||||||
|
this.video.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
|
||||||
|
this.video.readyState === HTMLMediaElement.HAVE_FUTURE_DATA
|
||||||
|
)
|
||||||
|
return "readyToPlay";
|
||||||
|
return "loading";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
get duration(): number {
|
get duration(): number {
|
||||||
return this.player.duration;
|
return this.video.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume
|
// Volume
|
||||||
get volume(): number {
|
get volume(): number {
|
||||||
return this.player.volume;
|
return this.video.volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
set volume(value: number) {
|
set volume(value: number) {
|
||||||
this.player.volume = value;
|
this.video.volume = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current Time
|
// Current Time
|
||||||
get currentTime(): number {
|
get currentTime(): number {
|
||||||
return this.player.currentTime;
|
return this.video.currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
set currentTime(value: number) {
|
set currentTime(value: number) {
|
||||||
this.player.currentTime = value;
|
this.video.currentTime = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Muted
|
// Muted
|
||||||
get muted(): boolean {
|
get muted(): boolean {
|
||||||
return this.player.muted;
|
return this.video.muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
set muted(value: boolean) {
|
set muted(value: boolean) {
|
||||||
this.player.muted = value;
|
this.video.muted = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop
|
// Loop
|
||||||
get loop(): boolean {
|
get loop(): boolean {
|
||||||
return this.player.loop;
|
return this.video.loop;
|
||||||
}
|
}
|
||||||
|
|
||||||
set loop(value: boolean) {
|
set loop(value: boolean) {
|
||||||
this.player.loop = value;
|
this.video.loop = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate
|
// Rate
|
||||||
get rate(): number {
|
get rate(): number {
|
||||||
return this.player.rate;
|
return this.video.playbackRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
set rate(value: number) {
|
set rate(value: number) {
|
||||||
this.player.rate = value;
|
this.video.playbackRate = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mix Audio Mode
|
// Mix Audio Mode
|
||||||
get mixAudioMode(): MixAudioMode {
|
get mixAudioMode(): MixAudioMode {
|
||||||
return this.player.mixAudioMode;
|
return "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
set mixAudioMode(value: MixAudioMode) {
|
set mixAudioMode(_: MixAudioMode) {
|
||||||
this.player.mixAudioMode = value;
|
if (__DEV__) {
|
||||||
|
console.warn(
|
||||||
|
"mixAudioMode is not supported on this platform, it wont have any effect",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore Silent Switch Mode
|
// Ignore Silent Switch Mode
|
||||||
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
|
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
|
||||||
return this.player.ignoreSilentSwitchMode;
|
return "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) {
|
set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'ignoreSilentSwitchMode is not supported on this platform, it wont have any effect'
|
"ignoreSilentSwitchMode is not supported on this platform, it wont have any effect",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.player.ignoreSilentSwitchMode = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play In Background
|
// Play In Background
|
||||||
get playInBackground(): boolean {
|
get playInBackground(): boolean {
|
||||||
return this.player.playInBackground;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
set playInBackground(value: boolean) {
|
set playInBackground(value: boolean) {
|
||||||
@@ -177,14 +172,10 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
await this.wrapPromise(this.player.initialize());
|
await this.wrapPromise(this.player.initialize());
|
||||||
|
|
||||||
NitroModules.updateMemorySize(this.player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async preload(): Promise<void> {
|
async preload(): Promise<void> {
|
||||||
await this.wrapPromise(this.player.preload());
|
this.player.load(this.media, this.startTime);
|
||||||
|
|
||||||
NitroModules.updateMemorySize(this.player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,7 +190,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
play(): void {
|
play(): void {
|
||||||
try {
|
try {
|
||||||
this.player.play();
|
this.video.play();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwError(error);
|
this.throwError(error);
|
||||||
}
|
}
|
||||||
@@ -207,7 +198,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
pause(): void {
|
pause(): void {
|
||||||
try {
|
try {
|
||||||
this.player.pause();
|
this.video.pause();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwError(error);
|
this.throwError(error);
|
||||||
}
|
}
|
||||||
@@ -215,7 +206,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
seekBy(time: number): void {
|
seekBy(time: number): void {
|
||||||
try {
|
try {
|
||||||
this.player.seekBy(time);
|
this.video.currentTime += time;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwError(error);
|
this.throwError(error);
|
||||||
}
|
}
|
||||||
@@ -223,22 +214,24 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
|||||||
|
|
||||||
seekTo(time: number): void {
|
seekTo(time: number): void {
|
||||||
try {
|
try {
|
||||||
this.player.seekTo(time);
|
this.video.currentTime = time;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwError(error);
|
this.throwError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async replaceSourceAsync(
|
async replaceSourceAsync(
|
||||||
source: VideoSource | VideoConfig | NoAutocomplete<VideoPlayerSource> | null
|
source:
|
||||||
|
| VideoSource
|
||||||
|
| VideoConfig
|
||||||
|
| NoAutocomplete<VideoPlayerSource>
|
||||||
|
| null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.wrapPromise(
|
await this.wrapPromise(
|
||||||
this.player.replaceSourceAsync(
|
this.player.replaceSourceAsync(
|
||||||
source === null ? null : createSource(source)
|
source === null ? null : createSource(source),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
NitroModules.updateMemorySize(this.player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text Track Management
|
// Text Track Management
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
type HTMLProps,
|
memo,
|
||||||
memo,
|
useEffect,
|
||||||
useEffect,
|
useImperativeHandle,
|
||||||
useImperativeHandle,
|
useRef,
|
||||||
useRef,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { ViewProps, ViewStyle } from "react-native";
|
import { View, type ViewStyle } from "react-native";
|
||||||
import { unstable_createElement } from "react-native-web";
|
|
||||||
import { VideoError } from "../types/VideoError";
|
import { VideoError } from "../types/VideoError";
|
||||||
import type { VideoPlayer } from "../VideoPlayer.web";
|
import type { VideoPlayer } from "../VideoPlayer.web";
|
||||||
import type { VideoViewProps, VideoViewRef } from "./ViewViewProps";
|
import type { VideoViewProps, VideoViewRef } from "./ViewViewProps";
|
||||||
|
|
||||||
const Video = (
|
|
||||||
props: Omit<HTMLProps<HTMLVideoElement>, keyof ViewProps> & ViewProps,
|
|
||||||
) => unstable_createElement("video", props);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VideoView is a component that allows you to display a video from a {@link VideoPlayer}.
|
* VideoView is a component that allows you to display a video from a {@link VideoPlayer}.
|
||||||
*
|
*
|
||||||
@@ -28,58 +22,62 @@ const Video = (
|
|||||||
* @param resizeMode - How the video should be resized to fit the view. Defaults to 'none'.
|
* @param resizeMode - How the video should be resized to fit the view. Defaults to 'none'.
|
||||||
*/
|
*/
|
||||||
const VideoView = forwardRef<VideoViewRef, VideoViewProps>(
|
const VideoView = forwardRef<VideoViewRef, VideoViewProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
player,
|
player: nPlayer,
|
||||||
controls = false,
|
controls = false,
|
||||||
resizeMode = "none",
|
resizeMode = "none",
|
||||||
style,
|
// auto pip is unsupported
|
||||||
// auto pip is unsupported
|
pictureInPicture = false,
|
||||||
pictureInPicture = false,
|
autoEnterPictureInPicture = false,
|
||||||
autoEnterPictureInPicture = false,
|
keepScreenAwake = true,
|
||||||
keepScreenAwake = true,
|
...props
|
||||||
...props
|
},
|
||||||
},
|
ref,
|
||||||
ref,
|
) => {
|
||||||
) => {
|
const player = nPlayer as unknown as VideoPlayer;
|
||||||
const vRef = useRef<HTMLVideoElement>(null);
|
const vRef = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const webPlayer = player as unknown as VideoPlayer;
|
const videoElement = player.__getNativeRef();
|
||||||
if (vRef.current) webPlayer.__getNativePlayer().attach(vRef.current);
|
vRef.current?.appendChild(videoElement);
|
||||||
}, [player]);
|
return () => {
|
||||||
|
vRef.current?.removeChild(videoElement);
|
||||||
|
};
|
||||||
|
}, [player]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
enterFullscreen: () => {
|
enterFullscreen: () => {
|
||||||
vRef.current?.requestFullscreen({ navigationUI: "hide" });
|
player.__getNativeRef().requestFullscreen({ navigationUI: "hide" });
|
||||||
},
|
},
|
||||||
exitFullscreen: () => {
|
exitFullscreen: () => {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
},
|
},
|
||||||
enterPictureInPicture: () => {
|
enterPictureInPicture: () => {
|
||||||
vRef.current?.requestPictureInPicture();
|
player.__getNativeRef().requestPictureInPicture();
|
||||||
},
|
},
|
||||||
exitPictureInPicture: () => {
|
exitPictureInPicture: () => {
|
||||||
document.exitPictureInPicture();
|
document.exitPictureInPicture();
|
||||||
},
|
},
|
||||||
canEnterPictureInPicture: () => document.pictureInPictureEnabled,
|
canEnterPictureInPicture: () => document.pictureInPictureEnabled,
|
||||||
}),
|
}),
|
||||||
[],
|
[player],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Video
|
player.__getNativeRef().controls = controls;
|
||||||
ref={vRef}
|
}, [player, controls]);
|
||||||
controls={controls}
|
|
||||||
style={[
|
return (
|
||||||
style,
|
<View {...props}>
|
||||||
{ objectFit: resizeMode === "stretch" ? "fill" : resizeMode },
|
<div
|
||||||
]}
|
ref={vRef}
|
||||||
{...props}
|
style={{ objectFit: resizeMode === "stretch" ? "fill" : resizeMode }}
|
||||||
/>
|
/>
|
||||||
);
|
</View>
|
||||||
},
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
VideoView.displayName = "VideoView";
|
VideoView.displayName = "VideoView";
|
||||||
|
|||||||
Reference in New Issue
Block a user