mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-06 07:16:12 +00:00
Scaffold web
This commit is contained in:
278
packages/react-native-video/src/core/VideoPlayer.web.ts
Normal file
278
packages/react-native-video/src/core/VideoPlayer.web.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { NitroModules } from 'react-native-nitro-modules';
|
||||
import { type VideoPlayer as VideoPlayerImpl } from '../spec/nitro/VideoPlayer.nitro';
|
||||
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 { createPlayer } from './utils/playerFactory';
|
||||
import { createSource } from './utils/sourceFactory';
|
||||
import { VideoPlayerEvents } from './VideoPlayerEvents';
|
||||
|
||||
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
protected player: VideoPlayerImpl;
|
||||
|
||||
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
|
||||
const hybridSource = createSource(source);
|
||||
const player = createPlayer(hybridSource);
|
||||
|
||||
// Initialize events
|
||||
super(player.eventEmitter);
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up player's native resources and releases native state.
|
||||
* After calling this method, the player is no longer usable.
|
||||
* @internal
|
||||
*/
|
||||
__destroy() {
|
||||
this.clearAllEvents();
|
||||
this.player.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the native (hybrid) player instance.
|
||||
* Should not be used outside of the module.
|
||||
* @internal
|
||||
*/
|
||||
__getNativePlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles parsing native errors to VideoRuntimeError and calling onError if provided
|
||||
* @internal
|
||||
*/
|
||||
private throwError(error: unknown) {
|
||||
const parsedError = tryParseNativeVideoError(error);
|
||||
|
||||
if (
|
||||
parsedError instanceof VideoRuntimeError
|
||||
&& this.triggerEvent('onError', parsedError)
|
||||
) {
|
||||
// We don't throw errors if onError is provided
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Status
|
||||
get status(): VideoPlayerStatus {
|
||||
return this.player.status;
|
||||
}
|
||||
|
||||
// Duration
|
||||
get duration(): number {
|
||||
return this.player.duration;
|
||||
}
|
||||
|
||||
// Volume
|
||||
get volume(): number {
|
||||
return this.player.volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
this.player.volume = value;
|
||||
}
|
||||
|
||||
// Current Time
|
||||
get currentTime(): number {
|
||||
return this.player.currentTime;
|
||||
}
|
||||
|
||||
set currentTime(value: number) {
|
||||
this.player.currentTime = value;
|
||||
}
|
||||
|
||||
// Muted
|
||||
get muted(): boolean {
|
||||
return this.player.muted;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
this.player.muted = value;
|
||||
}
|
||||
|
||||
// Loop
|
||||
get loop(): boolean {
|
||||
return this.player.loop;
|
||||
}
|
||||
|
||||
set loop(value: boolean) {
|
||||
this.player.loop = value;
|
||||
}
|
||||
|
||||
// Rate
|
||||
get rate(): number {
|
||||
return this.player.rate;
|
||||
}
|
||||
|
||||
set rate(value: number) {
|
||||
this.player.rate = value;
|
||||
}
|
||||
|
||||
// Mix Audio Mode
|
||||
get mixAudioMode(): MixAudioMode {
|
||||
return this.player.mixAudioMode;
|
||||
}
|
||||
|
||||
set mixAudioMode(value: MixAudioMode) {
|
||||
this.player.mixAudioMode = value;
|
||||
}
|
||||
|
||||
// Ignore Silent Switch Mode
|
||||
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
|
||||
return this.player.ignoreSilentSwitchMode;
|
||||
}
|
||||
|
||||
set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) {
|
||||
if (__DEV__ && !['ios'].includes(Platform.OS)) {
|
||||
console.warn(
|
||||
'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;
|
||||
}
|
||||
|
||||
set playInBackground(value: boolean) {
|
||||
this.player.playInBackground = value;
|
||||
}
|
||||
|
||||
// Play When Inactive
|
||||
get playWhenInactive(): boolean {
|
||||
return this.player.playWhenInactive;
|
||||
}
|
||||
|
||||
set playWhenInactive(value: boolean) {
|
||||
this.player.playWhenInactive = value;
|
||||
}
|
||||
|
||||
// Is Playing
|
||||
get isPlaying(): boolean {
|
||||
return this.player.isPlaying;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the player's native resources and releases native state.
|
||||
* After calling this method, the player is no longer usable.
|
||||
* Accessing any properties or methods of the player after calling this method will throw an error.
|
||||
* If you want to clean player resource use `replaceSourceAsync` with `null` instead.
|
||||
*/
|
||||
release(): void {
|
||||
this.__destroy();
|
||||
}
|
||||
|
||||
play(): void {
|
||||
try {
|
||||
this.player.play();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
try {
|
||||
this.player.pause();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
seekBy(time: number): void {
|
||||
try {
|
||||
this.player.seekBy(time);
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
seekTo(time: number): void {
|
||||
try {
|
||||
this.player.seekTo(time);
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async replaceSourceAsync(
|
||||
source: VideoSource | VideoConfig | NoAutocomplete<VideoPlayerSource> | null
|
||||
): Promise<void> {
|
||||
await this.wrapPromise(
|
||||
this.player.replaceSourceAsync(
|
||||
source === null ? null : createSource(source)
|
||||
)
|
||||
);
|
||||
|
||||
NitroModules.updateMemorySize(this.player);
|
||||
}
|
||||
|
||||
// Text Track Management
|
||||
getAvailableTextTracks(): TextTrack[] {
|
||||
try {
|
||||
return this.player.getAvailableTextTracks();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
selectTextTrack(textTrack: TextTrack | null): void {
|
||||
try {
|
||||
this.player.selectTextTrack(textTrack);
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Selected Text Track
|
||||
get selectedTrack(): TextTrack | undefined {
|
||||
return this.player.selectedTrack;
|
||||
}
|
||||
}
|
||||
|
||||
export { VideoPlayer };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { VideoPlayer } from '../VideoPlayer';
|
||||
import { type AllPlayerEvents } from '../types/Events';
|
||||
import type { AllPlayerEvents } from '../types/Events';
|
||||
import type { VideoPlayer } from '../VideoPlayer';
|
||||
|
||||
/**
|
||||
* Attaches an event listener to a `VideoPlayer` instance for a specified event.
|
||||
@@ -18,6 +18,5 @@ export const useEvent = <T extends keyof AllPlayerEvents>(
|
||||
player.addEventListener(event, callback);
|
||||
|
||||
return () => player.removeEventListener(event, callback);
|
||||
;
|
||||
}, [player, event, callback]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { NoAutocomplete } from '../types/Utils';
|
||||
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
|
||||
import { isVideoPlayerSource } from '../utils/sourceFactory';
|
||||
import { VideoPlayer } from '../VideoPlayer';
|
||||
import { useManagedInstance } from './useManagedInstance';
|
||||
|
||||
const sourceEqual = <T extends VideoConfig | VideoSource | VideoPlayerSource>(
|
||||
a: T,
|
||||
b?: T
|
||||
) => {
|
||||
if (isVideoPlayerSource(a) && isVideoPlayerSource(b)) {
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a `VideoPlayer` instance and manages its lifecycle.
|
||||
*
|
||||
* @param source - The source of the video to play
|
||||
* @param setup - A function to setup the player
|
||||
* @returns The `VideoPlayer` instance
|
||||
*/
|
||||
export const useVideoPlayer = (
|
||||
source: VideoConfig | VideoSource | NoAutocomplete<VideoPlayerSource>,
|
||||
setup?: (player: VideoPlayer) => void
|
||||
) => {
|
||||
return useManagedInstance(
|
||||
{
|
||||
factory: () => {
|
||||
const player = new VideoPlayer(source);
|
||||
setup?.(player);
|
||||
return player;
|
||||
},
|
||||
cleanup: (player) => {
|
||||
player.__destroy();
|
||||
},
|
||||
dependenciesEqualFn: sourceEqual,
|
||||
},
|
||||
[JSON.stringify(source)]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import * as React from 'react';
|
||||
import type { ViewProps, ViewStyle } from 'react-native';
|
||||
import { NitroModules } from 'react-native-nitro-modules';
|
||||
import type {
|
||||
VideoViewViewManager,
|
||||
VideoViewViewManagerFactory,
|
||||
} from '../../spec/nitro/VideoViewViewManager.nitro';
|
||||
import type { VideoViewEvents } from '../types/Events';
|
||||
import type { ResizeMode } from '../types/ResizeMode';
|
||||
import { tryParseNativeVideoError, VideoError } from '../types/VideoError';
|
||||
import type { VideoPlayer } from '../VideoPlayer';
|
||||
import { NativeVideoView } from './NativeVideoView';
|
||||
|
||||
export interface VideoViewProps extends Partial<VideoViewEvents>, ViewProps {
|
||||
/**
|
||||
* The player to play the video - {@link VideoPlayer}
|
||||
*/
|
||||
player: VideoPlayer;
|
||||
/**
|
||||
* The style of the video view - {@link ViewStyle}
|
||||
*/
|
||||
style?: ViewStyle;
|
||||
/**
|
||||
* Whether to show the controls. Defaults to false.
|
||||
*/
|
||||
controls?: boolean;
|
||||
/**
|
||||
* Whether to enable & show the picture in picture button in native controls. Defaults to false.
|
||||
*/
|
||||
pictureInPicture?: boolean;
|
||||
/**
|
||||
* Whether to automatically enter picture in picture mode when the video is playing. Defaults to false.
|
||||
*/
|
||||
autoEnterPictureInPicture?: boolean;
|
||||
/**
|
||||
* How the video should be resized to fit the view. Defaults to 'none'.
|
||||
* - 'contain': Scale the video uniformly (maintain aspect ratio) so that it fits entirely within the view
|
||||
* - 'cover': Scale the video uniformly (maintain aspect ratio) so that it fills the entire view (may crop)
|
||||
* - 'stretch': Scale the video to fill the entire view without maintaining aspect ratio
|
||||
* - 'none': Do not resize the video
|
||||
*/
|
||||
resizeMode?: ResizeMode;
|
||||
/**
|
||||
* Whether to keep the screen awake while the video view is mounted. Defaults to true.
|
||||
*/
|
||||
keepScreenAwake?: boolean;
|
||||
}
|
||||
|
||||
export interface VideoViewRef {
|
||||
/**
|
||||
* Enter fullscreen mode
|
||||
*/
|
||||
enterFullscreen: () => void;
|
||||
/**
|
||||
* Exit fullscreen mode
|
||||
*/
|
||||
exitFullscreen: () => void;
|
||||
/**
|
||||
* Enter picture in picture mode
|
||||
*/
|
||||
enterPictureInPicture: () => void;
|
||||
/**
|
||||
* Exit picture in picture mode
|
||||
*/
|
||||
exitPictureInPicture: () => void;
|
||||
/**
|
||||
* Check if picture in picture mode is supported
|
||||
* @returns true if picture in picture mode is supported, false otherwise
|
||||
*/
|
||||
canEnterPictureInPicture: () => boolean;
|
||||
}
|
||||
|
||||
let nitroIdCounter = 1;
|
||||
const VideoViewViewManagerFactory =
|
||||
NitroModules.createHybridObject<VideoViewViewManagerFactory>(
|
||||
'VideoViewViewManagerFactory'
|
||||
);
|
||||
|
||||
const wrapNativeViewManagerFunction = <T,>(
|
||||
manager: VideoViewViewManager | null,
|
||||
func: (manager: VideoViewViewManager) => T
|
||||
) => {
|
||||
try {
|
||||
if (manager === null) {
|
||||
throw new VideoError('view/not-found', 'View manager not found');
|
||||
}
|
||||
|
||||
return func(manager);
|
||||
} catch (error) {
|
||||
throw tryParseNativeVideoError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProps = (manager: VideoViewViewManager, props: VideoViewProps) => {
|
||||
manager.player = props.player.__getNativePlayer();
|
||||
manager.controls = props.controls ?? false;
|
||||
manager.pictureInPicture = props.pictureInPicture ?? false;
|
||||
manager.autoEnterPictureInPicture = props.autoEnterPictureInPicture ?? false;
|
||||
manager.resizeMode = props.resizeMode ?? 'none';
|
||||
manager.onPictureInPictureChange = props.onPictureInPictureChange;
|
||||
manager.onFullscreenChange = props.onFullscreenChange;
|
||||
manager.willEnterFullscreen = props.willEnterFullscreen;
|
||||
manager.willExitFullscreen = props.willExitFullscreen;
|
||||
manager.willEnterPictureInPicture = props.willEnterPictureInPicture;
|
||||
manager.willExitPictureInPicture = props.willExitPictureInPicture;
|
||||
manager.keepScreenAwake = props.keepScreenAwake ?? true;
|
||||
};
|
||||
|
||||
/**
|
||||
* VideoView is a component that allows you to display a video from a {@link VideoPlayer}.
|
||||
*
|
||||
* @param player - The player to play the video - {@link VideoPlayer}
|
||||
* @param controls - Whether to show the controls. Defaults to false.
|
||||
* @param style - The style of the video view - {@link ViewStyle}
|
||||
* @param pictureInPicture - Whether to show the picture in picture button. Defaults to false.
|
||||
* @param autoEnterPictureInPicture - Whether to automatically enter picture in picture mode
|
||||
* when the video is playing. Defaults to false.
|
||||
* @param resizeMode - How the video should be resized to fit the view. Defaults to 'none'.
|
||||
*/
|
||||
const VideoView = React.forwardRef<VideoViewRef, VideoViewProps>(
|
||||
(
|
||||
{
|
||||
player,
|
||||
controls = false,
|
||||
pictureInPicture = false,
|
||||
autoEnterPictureInPicture = false,
|
||||
resizeMode = 'none',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const nitroId = React.useMemo(() => nitroIdCounter++, []);
|
||||
const nitroViewManager = React.useRef<VideoViewViewManager | null>(null);
|
||||
|
||||
const setupViewManager = React.useCallback(
|
||||
(id: number) => {
|
||||
try {
|
||||
if (nitroViewManager.current === null) {
|
||||
nitroViewManager.current =
|
||||
VideoViewViewManagerFactory.createViewManager(id);
|
||||
|
||||
// Should never happen
|
||||
if (!nitroViewManager.current) {
|
||||
throw new VideoError(
|
||||
'view/not-found',
|
||||
'Failed to create View Manager'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates props to native view
|
||||
updateProps(nitroViewManager.current, {
|
||||
...props,
|
||||
player: player,
|
||||
controls: controls,
|
||||
pictureInPicture: pictureInPicture,
|
||||
autoEnterPictureInPicture: autoEnterPictureInPicture,
|
||||
resizeMode: resizeMode,
|
||||
});
|
||||
} catch (error) {
|
||||
throw tryParseNativeVideoError(error);
|
||||
}
|
||||
},
|
||||
[
|
||||
props,
|
||||
player,
|
||||
controls,
|
||||
pictureInPicture,
|
||||
autoEnterPictureInPicture,
|
||||
resizeMode,
|
||||
]
|
||||
);
|
||||
|
||||
const onNitroIdChange = React.useCallback(
|
||||
(event: { nativeEvent: { nitroId: number } }) => {
|
||||
setupViewManager(event.nativeEvent.nitroId);
|
||||
},
|
||||
[setupViewManager]
|
||||
);
|
||||
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
enterFullscreen: () => {
|
||||
wrapNativeViewManagerFunction(nitroViewManager.current, (manager) => {
|
||||
manager.enterFullscreen();
|
||||
});
|
||||
},
|
||||
exitFullscreen: () => {
|
||||
wrapNativeViewManagerFunction(nitroViewManager.current, (manager) => {
|
||||
manager.exitFullscreen();
|
||||
});
|
||||
},
|
||||
enterPictureInPicture: () => {
|
||||
wrapNativeViewManagerFunction(nitroViewManager.current, (manager) => {
|
||||
manager.enterPictureInPicture();
|
||||
});
|
||||
},
|
||||
exitPictureInPicture: () => {
|
||||
wrapNativeViewManagerFunction(nitroViewManager.current, (manager) => {
|
||||
manager.exitPictureInPicture();
|
||||
});
|
||||
},
|
||||
canEnterPictureInPicture: () => {
|
||||
return wrapNativeViewManagerFunction(
|
||||
nitroViewManager.current,
|
||||
(manager) => {
|
||||
return manager.canEnterPictureInPicture();
|
||||
}
|
||||
);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!nitroViewManager.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Updates props to native view
|
||||
updateProps(nitroViewManager.current, {
|
||||
...props,
|
||||
player: player,
|
||||
controls: controls,
|
||||
pictureInPicture: pictureInPicture,
|
||||
autoEnterPictureInPicture: autoEnterPictureInPicture,
|
||||
resizeMode: resizeMode,
|
||||
});
|
||||
}, [
|
||||
player,
|
||||
controls,
|
||||
pictureInPicture,
|
||||
autoEnterPictureInPicture,
|
||||
resizeMode,
|
||||
props,
|
||||
]);
|
||||
|
||||
return (
|
||||
<NativeVideoView
|
||||
nitroId={nitroId}
|
||||
onNitroIdChange={onNitroIdChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
VideoView.displayName = 'VideoView';
|
||||
|
||||
export default React.memo(VideoView);
|
||||
@@ -1,26 +1,27 @@
|
||||
export { useEvent } from './core/hooks/useEvent';
|
||||
export { useVideoPlayer } from './core/hooks/useVideoPlayer';
|
||||
export * from './core/types/Events';
|
||||
export type * from './core/types/Events';
|
||||
export type { IgnoreSilentSwitchMode } from './core/types/IgnoreSilentSwitchMode';
|
||||
export type { MixAudioMode } from './core/types/MixAudioMode';
|
||||
export type { ResizeMode } from './core/types/ResizeMode';
|
||||
export type { TextTrack } from './core/types/TextTrack';
|
||||
export type { VideoConfig, VideoSource } from './core/types/VideoConfig';
|
||||
export {
|
||||
type LibraryError,
|
||||
type PlayerError,
|
||||
type SourceError,
|
||||
type UnknownError,
|
||||
type VideoComponentError,
|
||||
type VideoError,
|
||||
type VideoErrorCode,
|
||||
type VideoRuntimeError,
|
||||
type VideoViewError,
|
||||
export type {
|
||||
LibraryError,
|
||||
PlayerError,
|
||||
SourceError,
|
||||
UnknownError,
|
||||
VideoComponentError,
|
||||
VideoError,
|
||||
VideoErrorCode,
|
||||
VideoRuntimeError,
|
||||
VideoViewError,
|
||||
} from './core/types/VideoError';
|
||||
export type { VideoPlayerStatus } from './core/types/VideoPlayerStatus';
|
||||
|
||||
export { VideoPlayer } from './core/VideoPlayer';
|
||||
export {
|
||||
default as VideoView,
|
||||
type VideoViewProps,
|
||||
type VideoViewRef,
|
||||
} from './core/video-view/VideoView';
|
||||
export { VideoPlayer } from './core/VideoPlayer';
|
||||
|
||||
Reference in New Issue
Block a user