mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-06 07:16:12 +00:00
Compare commits
2 Commits
f877c1f915
...
d52903709b
| Author | SHA1 | Date | |
|---|---|---|---|
| d52903709b | |||
| 3a2ec54eaa |
@@ -1,24 +1,23 @@
|
||||
import shaka from 'shaka-player';
|
||||
import type { VideoPlayerSource } from '../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode';
|
||||
import type { MixAudioMode } from './types/MixAudioMode';
|
||||
import type { TextTrack } from './types/TextTrack';
|
||||
import type { NoAutocomplete } from './types/Utils';
|
||||
import type { VideoConfig, VideoSource } from './types/VideoConfig';
|
||||
import shaka from "shaka-player";
|
||||
import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro";
|
||||
import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode";
|
||||
import type { MixAudioMode } from "./types/MixAudioMode";
|
||||
import type { TextTrack } from "./types/TextTrack";
|
||||
import type { NoAutocomplete } from "./types/Utils";
|
||||
import type { VideoConfig, VideoSource } from "./types/VideoConfig";
|
||||
import {
|
||||
tryParseNativeVideoError,
|
||||
VideoRuntimeError,
|
||||
} from './types/VideoError';
|
||||
import type { VideoPlayerBase } from './types/VideoPlayerBase';
|
||||
import type { VideoPlayerStatus } from './types/VideoPlayerStatus';
|
||||
import { VideoPlayerEvents } from './VideoPlayerEvents';
|
||||
} from "./types/VideoError";
|
||||
import type { VideoPlayerBase } from "./types/VideoPlayerBase";
|
||||
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
|
||||
|
||||
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
class VideoPlayer extends VideoPlayerEventsWeb implements VideoPlayerBase {
|
||||
protected player = new shaka.Player();
|
||||
protected video = document.createElement("video");
|
||||
|
||||
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
|
||||
// Initialize events
|
||||
super(player.eventEmitter);
|
||||
this.player.attach(this.video);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,13 +29,8 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
this.player.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the native (hybrid) player instance.
|
||||
* Should not be used outside of the module.
|
||||
* @internal
|
||||
*/
|
||||
__getNativePlayer() {
|
||||
return this.player;
|
||||
__getNativeRef() {
|
||||
return this.video;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,8 +41,8 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
const parsedError = tryParseNativeVideoError(error);
|
||||
|
||||
if (
|
||||
parsedError instanceof VideoRuntimeError
|
||||
&& this.triggerEvent('onError', parsedError)
|
||||
parsedError instanceof VideoRuntimeError &&
|
||||
this.triggerEvent("onError", parsedError)
|
||||
) {
|
||||
// We don't throw errors if onError is provided
|
||||
return;
|
||||
@@ -57,18 +51,6 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
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
|
||||
get source(): VideoPlayerSource {
|
||||
return this.player.source;
|
||||
@@ -76,85 +58,95 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
// Status
|
||||
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
|
||||
get duration(): number {
|
||||
return this.player.duration;
|
||||
return this.video.duration;
|
||||
}
|
||||
|
||||
// Volume
|
||||
get volume(): number {
|
||||
return this.player.volume;
|
||||
return this.video.volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
this.player.volume = value;
|
||||
this.video.volume = value;
|
||||
}
|
||||
|
||||
// Current Time
|
||||
get currentTime(): number {
|
||||
return this.player.currentTime;
|
||||
return this.video.currentTime;
|
||||
}
|
||||
|
||||
set currentTime(value: number) {
|
||||
this.player.currentTime = value;
|
||||
this.video.currentTime = value;
|
||||
}
|
||||
|
||||
// Muted
|
||||
get muted(): boolean {
|
||||
return this.player.muted;
|
||||
return this.video.muted;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
this.player.muted = value;
|
||||
this.video.muted = value;
|
||||
}
|
||||
|
||||
// Loop
|
||||
get loop(): boolean {
|
||||
return this.player.loop;
|
||||
return this.video.loop;
|
||||
}
|
||||
|
||||
set loop(value: boolean) {
|
||||
this.player.loop = value;
|
||||
this.video.loop = value;
|
||||
}
|
||||
|
||||
// Rate
|
||||
get rate(): number {
|
||||
return this.player.rate;
|
||||
return this.video.playbackRate;
|
||||
}
|
||||
|
||||
set rate(value: number) {
|
||||
this.player.rate = value;
|
||||
this.video.playbackRate = value;
|
||||
}
|
||||
|
||||
// Mix Audio Mode
|
||||
get mixAudioMode(): MixAudioMode {
|
||||
return this.player.mixAudioMode;
|
||||
return "auto";
|
||||
}
|
||||
|
||||
set mixAudioMode(value: MixAudioMode) {
|
||||
this.player.mixAudioMode = value;
|
||||
set mixAudioMode(_: MixAudioMode) {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
"mixAudioMode is not supported on this platform, it wont have any effect",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore Silent Switch Mode
|
||||
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
|
||||
return this.player.ignoreSilentSwitchMode;
|
||||
return "auto";
|
||||
}
|
||||
|
||||
set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) {
|
||||
set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) {
|
||||
if (__DEV__) {
|
||||
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
|
||||
get playInBackground(): boolean {
|
||||
return this.player.playInBackground;
|
||||
return true;
|
||||
}
|
||||
|
||||
set playInBackground(value: boolean) {
|
||||
@@ -177,14 +169,10 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.wrapPromise(this.player.initialize());
|
||||
|
||||
NitroModules.updateMemorySize(this.player);
|
||||
}
|
||||
|
||||
async preload(): Promise<void> {
|
||||
await this.wrapPromise(this.player.preload());
|
||||
|
||||
NitroModules.updateMemorySize(this.player);
|
||||
this.player.load(this.media, this.startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +187,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
play(): void {
|
||||
try {
|
||||
this.player.play();
|
||||
this.video.play();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
@@ -207,7 +195,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
pause(): void {
|
||||
try {
|
||||
this.player.pause();
|
||||
this.video.pause();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
@@ -215,7 +203,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
seekBy(time: number): void {
|
||||
try {
|
||||
this.player.seekBy(time);
|
||||
this.video.currentTime += time;
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
@@ -223,22 +211,24 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
|
||||
seekTo(time: number): void {
|
||||
try {
|
||||
this.player.seekTo(time);
|
||||
this.video.currentTime = time;
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async replaceSourceAsync(
|
||||
source: VideoSource | VideoConfig | NoAutocomplete<VideoPlayerSource> | null
|
||||
source:
|
||||
| VideoSource
|
||||
| VideoConfig
|
||||
| NoAutocomplete<VideoPlayerSource>
|
||||
| null,
|
||||
): Promise<void> {
|
||||
await this.wrapPromise(
|
||||
this.player.replaceSourceAsync(
|
||||
source === null ? null : createSource(source)
|
||||
)
|
||||
source === null ? null : createSource(source),
|
||||
),
|
||||
);
|
||||
|
||||
NitroModules.updateMemorySize(this.player);
|
||||
}
|
||||
|
||||
// Text Track Management
|
||||
|
||||
@@ -29,7 +29,7 @@ export class VideoPlayerEvents {
|
||||
|
||||
constructor(eventEmitter: VideoPlayerEventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
for (let event of this.supportedEvents){
|
||||
for (const event of this.supportedEvents){
|
||||
// @ts-expect-error we narrow the type of the event
|
||||
this.eventEmitter[event] = this.triggerEvent.bind(this, event);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class VideoPlayerEvents {
|
||||
): boolean {
|
||||
if (!this.eventListeners[event]?.size)
|
||||
return false;
|
||||
for (let fn of this.eventListeners[event]) {
|
||||
for (const fn of this.eventListeners[event]) {
|
||||
fn(...params);
|
||||
}
|
||||
return true;
|
||||
@@ -59,7 +59,7 @@ export class VideoPlayerEvents {
|
||||
event: Event,
|
||||
callback: PlayerEvents[Event]
|
||||
) {
|
||||
this.eventListeners[event]!.delete(callback);
|
||||
this.eventListeners[event]?.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
77
packages/react-native-video/src/core/VideoPlayerEventsWeb.ts
Normal file
77
packages/react-native-video/src/core/VideoPlayerEventsWeb.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { VideoPlayerEvents } from "./VideoPlayerEvents";
|
||||
import type { AllPlayerEvents as PlayerEvents } from "./types/Events";
|
||||
|
||||
class VideoPlayerEventsWeb {
|
||||
protected eventListeners: Partial<
|
||||
Record<keyof PlayerEvents, Set<(...params: any[]) => void>>
|
||||
> = {};
|
||||
|
||||
constructor(private video: HTMLVideoElement) {}
|
||||
|
||||
private eventMap: Record<
|
||||
keyof PlayerEvents,
|
||||
keyof HTMLVideoElementEventMap | null
|
||||
> = {
|
||||
onAudioBecomingNoisy: null,
|
||||
onAudioFocusChange: null,
|
||||
onBandwidthUpdate: null,
|
||||
onBuffer: "waiting",
|
||||
onControlsVisibleChange: null,
|
||||
onEnd: "ended",
|
||||
onExternalPlaybackChange: null,
|
||||
onLoad: "loadeddata",
|
||||
onLoadStart: "loadstart",
|
||||
onPlaybackRateChange: "ratechange",
|
||||
onPlaybackStateChange: null,
|
||||
onProgress: "timeupdate",
|
||||
onReadyToDisplay: "canplay",
|
||||
onSeek: "seeked",
|
||||
onStatusChange: null,
|
||||
onTextTrackDataChanged: null,
|
||||
onTimedMetadata: null,
|
||||
onTrackChange: null,
|
||||
onVolumeChange: "volumechange",
|
||||
onError: "error",
|
||||
};
|
||||
|
||||
addEventListener<Event extends keyof PlayerEvents>(
|
||||
event: Event,
|
||||
callback: PlayerEvents[Event],
|
||||
) {
|
||||
if (!this.eventMap[event]) return;
|
||||
this.eventListeners[event] ??= new Set();
|
||||
this.eventListeners[event].add(callback);
|
||||
this.video.addEventListener(this.eventMap[event], callback);
|
||||
}
|
||||
|
||||
removeEventListener<Event extends keyof PlayerEvents>(
|
||||
event: Event,
|
||||
callback: PlayerEvents[Event],
|
||||
) {
|
||||
if (!this.eventMap[event]) return;
|
||||
this.eventListeners[event]?.delete(callback);
|
||||
this.video.removeEventListener(this.eventMap[event], callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all events from the event emitter.
|
||||
*/
|
||||
clearAllEvents() {
|
||||
for (const [event, callbacks] of Object.entries(this.eventListeners)) {
|
||||
for (const cb of callbacks) {
|
||||
this.removeEventListener(event as keyof PlayerEvents, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a specific event from the event emitter.
|
||||
* @param event - The name of the event to clear.
|
||||
*/
|
||||
clearEvent(event: keyof PlayerEvents) {
|
||||
if (!this.eventListeners[event]) return;
|
||||
for (const cb of this.eventListeners[event]) {
|
||||
this.removeEventListener(event, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
memo,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
forwardRef,
|
||||
memo,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import type { ViewProps, ViewStyle } from "react-native";
|
||||
import { unstable_createElement } from "react-native-web";
|
||||
import { View, type ViewStyle } from "react-native";
|
||||
import { VideoError } from "../types/VideoError";
|
||||
import type { VideoPlayer } from "../VideoPlayer.web";
|
||||
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}.
|
||||
*
|
||||
@@ -28,58 +22,62 @@ const Video = (
|
||||
* @param resizeMode - How the video should be resized to fit the view. Defaults to 'none'.
|
||||
*/
|
||||
const VideoView = forwardRef<VideoViewRef, VideoViewProps>(
|
||||
(
|
||||
{
|
||||
player,
|
||||
controls = false,
|
||||
resizeMode = "none",
|
||||
style,
|
||||
// auto pip is unsupported
|
||||
pictureInPicture = false,
|
||||
autoEnterPictureInPicture = false,
|
||||
keepScreenAwake = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const vRef = useRef<HTMLVideoElement>(null);
|
||||
useEffect(() => {
|
||||
const webPlayer = player as unknown as VideoPlayer;
|
||||
if (vRef.current) webPlayer.__getNativePlayer().attach(vRef.current);
|
||||
}, [player]);
|
||||
(
|
||||
{
|
||||
player: nPlayer,
|
||||
controls = false,
|
||||
resizeMode = "none",
|
||||
// auto pip is unsupported
|
||||
pictureInPicture = false,
|
||||
autoEnterPictureInPicture = false,
|
||||
keepScreenAwake = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const player = nPlayer as unknown as VideoPlayer;
|
||||
const vRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const videoElement = player.__getNativeRef();
|
||||
vRef.current?.appendChild(videoElement);
|
||||
return () => {
|
||||
vRef.current?.removeChild(videoElement);
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
enterFullscreen: () => {
|
||||
vRef.current?.requestFullscreen({ navigationUI: "hide" });
|
||||
},
|
||||
exitFullscreen: () => {
|
||||
document.exitFullscreen();
|
||||
},
|
||||
enterPictureInPicture: () => {
|
||||
vRef.current?.requestPictureInPicture();
|
||||
},
|
||||
exitPictureInPicture: () => {
|
||||
document.exitPictureInPicture();
|
||||
},
|
||||
canEnterPictureInPicture: () => document.pictureInPictureEnabled,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
enterFullscreen: () => {
|
||||
player.__getNativeRef().requestFullscreen({ navigationUI: "hide" });
|
||||
},
|
||||
exitFullscreen: () => {
|
||||
document.exitFullscreen();
|
||||
},
|
||||
enterPictureInPicture: () => {
|
||||
player.__getNativeRef().requestPictureInPicture();
|
||||
},
|
||||
exitPictureInPicture: () => {
|
||||
document.exitPictureInPicture();
|
||||
},
|
||||
canEnterPictureInPicture: () => document.pictureInPictureEnabled,
|
||||
}),
|
||||
[player],
|
||||
);
|
||||
|
||||
return (
|
||||
<Video
|
||||
ref={vRef}
|
||||
controls={controls}
|
||||
style={[
|
||||
style,
|
||||
{ objectFit: resizeMode === "stretch" ? "fill" : resizeMode },
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
useEffect(() => {
|
||||
player.__getNativeRef().controls = controls;
|
||||
}, [player, controls]);
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<div
|
||||
ref={vRef}
|
||||
style={{ objectFit: resizeMode === "stretch" ? "fill" : resizeMode }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
VideoView.displayName = "VideoView";
|
||||
|
||||
Reference in New Issue
Block a user