Implement html video properties using headless video

This commit is contained in:
2025-10-03 22:09:55 +02:00
parent f877c1f915
commit 3a2ec54eaa
2 changed files with 119 additions and 128 deletions

View File

@@ -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

View File

@@ -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";