diff --git a/packages/react-native-video/.eslintrc.js b/packages/react-native-video/.eslintrc.js index 56aa5e85..e79c3bf9 100644 --- a/packages/react-native-video/.eslintrc.js +++ b/packages/react-native-video/.eslintrc.js @@ -3,6 +3,6 @@ module.exports = { extends: ["../../config/.eslintrc.js"], parserOptions: { tsconfigRootDir: __dirname, - project: true, + project: ['./tsconfig.json', './tsconfig.web.json'], }, }; diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index ddfff9f7..429f5857 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -326,7 +326,6 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedTrack(): TextTrack | undefined { return this.player.selectedTrack; } - } export { VideoPlayer }; diff --git a/packages/react-native-video/src/core/VideoPlayer.web.ts b/packages/react-native-video/src/core/VideoPlayer.web.ts index 8dba8e8d..d08b1304 100644 --- a/packages/react-native-video/src/core/VideoPlayer.web.ts +++ b/packages/react-native-video/src/core/VideoPlayer.web.ts @@ -1,38 +1,38 @@ -import type { AudioTrack } from "./types/AudioTrack"; -import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode"; -import type { MixAudioMode } from "./types/MixAudioMode"; -import type { TextTrack } from "./types/TextTrack"; -import type { VideoTrack } from "./types/VideoTrack"; -import type { NoAutocomplete } from "./types/Utils"; +import type { AudioTrack } from './types/AudioTrack'; +import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode'; +import type { MixAudioMode } from './types/MixAudioMode'; +import type { TextTrack } from './types/TextTrack'; +import type { VideoTrack } from './types/VideoTrack'; +import type { NoAutocomplete } from './types/Utils'; import type { NativeVideoConfig, VideoConfig, VideoSource, -} from "./types/VideoConfig"; -import type { WebVideoPlayer } from "./types/WebVideoPlayer"; -import type { VideoPlayerSourceBase } from "./types/VideoPlayerSourceBase"; -import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; -import { VideoPlayerEvents } from "./events/VideoPlayerEvents"; -import { MediaSessionHandler } from "./web/MediaSession"; -import { WebEventEmitter } from "./web/WebEventEmitter"; -import type { VideoStore } from "./web/VideoStore"; +} from './types/VideoConfig'; +import type { WebVideoPlayer } from './types/WebVideoPlayer'; +import type { VideoPlayerSourceBase } from './types/VideoPlayerSourceBase'; +import type { VideoPlayerStatus } from './types/VideoPlayerStatus'; +import { VideoPlayerEvents } from './events/VideoPlayerEvents'; +import { MediaSessionHandler } from './web/MediaSession'; +import { WebEventEmitter } from './web/WebEventEmitter'; +import type { VideoStore } from './web/VideoStore'; function setExternalSubtitles( video: HTMLVideoElement, - subtitles: NativeVideoConfig["externalSubtitles"], + subtitles: NativeVideoConfig['externalSubtitles'] ) { - video.querySelectorAll("track").forEach((t) => t.remove()); + video.querySelectorAll('track').forEach((t) => t.remove()); for (const sub of subtitles ?? []) { - const track = document.createElement("track"); - track.kind = "subtitles"; + const track = document.createElement('track'); + track.kind = 'subtitles'; track.src = sub.uri; - track.srclang = sub.language ?? "und"; + track.srclang = sub.language ?? 'und'; track.label = sub.label; video.appendChild(track); } } -type TrackType = "textTracks" | "audioTracks" | "videoTracks"; +type TrackType = 'textTracks' | 'audioTracks' | 'videoTracks'; /** * Reads tracks from HTMLVideoElement. @@ -41,17 +41,25 @@ type TrackType = "textTracks" | "audioTracks" | "videoTracks"; */ function getTracks( video: HTMLVideoElement, - prop: TrackType, + prop: TrackType ): Array<{ id: string; label: string; language?: string; selected: boolean }> { const tracks = (video as any)[prop]; if (!tracks) return []; const result = []; for (let i = 0; i < tracks.length; i++) { const t = tracks[i]!; - const selected = prop === "textTracks" - ? t.mode === "showing" - : prop === "audioTracks" ? t.enabled : t.selected; - result.push({ id: t.id || t.label, label: t.label, language: t.language, selected }); + const selected = + prop === 'textTracks' + ? t.mode === 'showing' + : prop === 'audioTracks' + ? t.enabled + : t.selected; + result.push({ + id: t.id || t.label, + label: t.label, + language: t.language, + selected, + }); } return result; } @@ -59,15 +67,15 @@ function getTracks( function selectTrack( video: HTMLVideoElement, prop: TrackType, - trackId: string | null, + trackId: string | null ): void { const tracks = (video as any)[prop]; if (!tracks) return; for (let i = 0; i < tracks.length; i++) { const id = tracks[i]!.id || tracks[i]!.label; - if (prop === "textTracks") { - tracks[i]!.mode = id === trackId ? "showing" : "disabled"; - } else if (prop === "audioTracks") { + if (prop === 'textTracks') { + tracks[i]!.mode = id === trackId ? 'showing' : 'disabled'; + } else if (prop === 'audioTracks') { tracks[i]!.enabled = id === trackId; } else { tracks[i]!.selected = id === trackId; @@ -97,18 +105,20 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { * VideoView later mounts it into the DOM and connects the video.js store. */ constructor(source: VideoSource | VideoConfig | VideoPlayerSourceBase) { - if (typeof window === "undefined") { - throw new Error("[react-native-video] VideoPlayer cannot be created in SSR environment."); + if (typeof window === 'undefined') { + throw new Error( + '[react-native-video] VideoPlayer cannot be created in SSR environment.' + ); } - const video = document.createElement("video"); + const video = document.createElement('video'); video.playsInline = true; super(new WebEventEmitter(null, () => video)); this.video = video; (this.eventEmitter as WebEventEmitter).addOnErrorListener((error) => { - this.triggerJSEvent("onError", error); + this.triggerJSEvent('onError', error); }); this.replaceSourceAsync(source); @@ -145,8 +155,8 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { get source(): VideoPlayerSourceBase { return { - uri: this._source?.uri ?? "", - config: this._source ?? { uri: "" }, + uri: this._source?.uri ?? '', + config: this._source ?? { uri: '' }, getAssetInformationAsync: async () => ({ bitrate: NaN, width: this.video.videoWidth, @@ -155,32 +165,49 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { fileSize: -1n, isHDR: false, isLive: false, - orientation: "landscape" as const, + orientation: 'landscape' as const, }), }; } get status(): VideoPlayerStatus { - if (this.media.error) return "error"; - if (this.video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) return "readyToPlay"; - if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING) return "loading"; - if (this.video.src) return "loading"; - return "idle"; + if (this.media.error) return 'error'; + if (this.video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) + return 'readyToPlay'; + if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING) return 'loading'; + if (this.video.src) return 'loading'; + return 'idle'; } - get duration(): number { return this.media.duration || NaN; } - get currentTime(): number { return this.media.currentTime; } - get volume(): number { return this.media.volume; } - get muted(): boolean { return this.media.muted; } - get loop(): boolean { return this.video.loop; } - get rate(): number { return this.media.playbackRate; } - get isPlaying(): boolean { return !this.media.paused; } + get duration(): number { + return this.media.duration || NaN; + } + get currentTime(): number { + return this.media.currentTime; + } + get volume(): number { + return this.media.volume; + } + get muted(): boolean { + return this.media.muted; + } + get loop(): boolean { + return this.video.loop; + } + get rate(): number { + return this.media.playbackRate; + } + get isPlaying(): boolean { + return !this.media.paused; + } // --- Playback state (write through store or video element) --- set volume(v: number) { const clamped = Math.max(0, Math.min(1, v)); - this._store ? this._store.setVolume(clamped) : (this.video.volume = clamped); + this._store + ? this._store.setVolume(clamped) + : (this.video.volume = clamped); } set currentTime(v: number) { @@ -188,22 +215,36 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { } // video.js store has toggleMuted() but no direct setter - set muted(v: boolean) { this.video.muted = v; } - set loop(v: boolean) { this.video.loop = v; } + set muted(v: boolean) { + this.video.muted = v; + } + set loop(v: boolean) { + this.video.loop = v; + } set rate(v: number) { - this._store ? this._store.setPlaybackRate(v) : (this.video.playbackRate = v); + this._store + ? this._store.setPlaybackRate(v) + : (this.video.playbackRate = v); } // --- Unsupported on web (no-op) --- - get mixAudioMode(): MixAudioMode { return "auto"; } + get mixAudioMode(): MixAudioMode { + return 'auto'; + } set mixAudioMode(_: MixAudioMode) {} - get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode { return "auto"; } + get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode { + return 'auto'; + } set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) {} - get playInBackground(): boolean { return true; } + get playInBackground(): boolean { + return true; + } set playInBackground(_: boolean) {} - get playWhenInactive(): boolean { return true; } + get playWhenInactive(): boolean { + return true; + } set playWhenInactive(_: boolean) {} // --- Media Session --- @@ -214,7 +255,10 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { set showNotificationControls(value: boolean) { if (!this.mediaSession) return; - if (!value) { this.mediaSession.disable(); return; } + if (!value) { + this.mediaSession.disable(); + return; + } this.mediaSession.enable(); this.mediaSession.updateMediaSession(this._source?.metadata); } @@ -224,13 +268,19 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { async initialize(): Promise {} async preload(): Promise { - this.video.preload = "auto"; + this.video.preload = 'auto'; this.video.load(); } - release(): void { this.__destroy(); } - play(): void { this.media.play()?.catch(() => {}); } - pause(): void { this.media.pause(); } + release(): void { + this.__destroy(); + } + play(): void { + this.media.play()?.catch(() => {}); + } + pause(): void { + this.media.pause(); + } seekTo(time: number): void { this._store ? this._store.seek(time) : (this.video.currentTime = time); @@ -247,26 +297,30 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { | VideoSource | VideoConfig | NoAutocomplete - | null, + | null ): Promise { if (!source) { - this.video.removeAttribute("src"); + this.video.removeAttribute('src'); this.video.load(); this._source = undefined; return; } - if (typeof source === "string") { + if (typeof source === 'string') { source = { uri: source }; } - if (typeof source === "number" || typeof source.uri === "number") { - console.error("A source uri must be a string. Numbers are only supported on native."); + if (typeof source === 'number' || typeof source.uri === 'number') { + console.error( + 'A source uri must be a string. Numbers are only supported on native.' + ); return; } this._source = source as NativeVideoConfig; - this._store ? this._store.loadSource(source.uri) : (this.video.src = source.uri); + this._store + ? this._store.loadSource(source.uri) + : (this.video.src = source.uri); if (this.mediaSession?.enabled) { this.mediaSession.updateMediaSession(source.metadata); @@ -279,18 +333,36 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer { // --- Tracks --- - getAvailableTextTracks(): TextTrack[] { return getTracks(this.video, "textTracks"); } - selectTextTrack(t: TextTrack | null): void { selectTrack(this.video, "textTracks", t?.id ?? null); } - get selectedTrack(): TextTrack | undefined { return this.getAvailableTextTracks().find((x) => x.selected); } + getAvailableTextTracks(): TextTrack[] { + return getTracks(this.video, 'textTracks'); + } + selectTextTrack(t: TextTrack | null): void { + selectTrack(this.video, 'textTracks', t?.id ?? null); + } + get selectedTrack(): TextTrack | undefined { + return this.getAvailableTextTracks().find((x) => x.selected); + } // Audio/video tracks: web-only, ~16% browser support (Safari only, flags in Chrome/Firefox) - getAvailableAudioTracks(): AudioTrack[] { return getTracks(this.video, "audioTracks"); } - selectAudioTrack(t: AudioTrack | null): void { selectTrack(this.video, "audioTracks", t?.id ?? null); } - get selectedAudioTrack(): AudioTrack | undefined { return this.getAvailableAudioTracks().find((x) => x.selected); } + getAvailableAudioTracks(): AudioTrack[] { + return getTracks(this.video, 'audioTracks'); + } + selectAudioTrack(t: AudioTrack | null): void { + selectTrack(this.video, 'audioTracks', t?.id ?? null); + } + get selectedAudioTrack(): AudioTrack | undefined { + return this.getAvailableAudioTracks().find((x) => x.selected); + } - getAvailableVideoTracks(): VideoTrack[] { return getTracks(this.video, "videoTracks"); } - selectVideoTrack(t: VideoTrack | null): void { selectTrack(this.video, "videoTracks", t?.id ?? null); } - get selectedVideoTrack(): VideoTrack | undefined { return this.getAvailableVideoTracks().find((x) => x.selected); } + getAvailableVideoTracks(): VideoTrack[] { + return getTracks(this.video, 'videoTracks'); + } + selectVideoTrack(t: VideoTrack | null): void { + selectTrack(this.video, 'videoTracks', t?.id ?? null); + } + get selectedVideoTrack(): VideoTrack | undefined { + return this.getAvailableVideoTracks().find((x) => x.selected); + } } export { VideoPlayer }; diff --git a/packages/react-native-video/src/core/events/VideoPlayerEvents.native.ts b/packages/react-native-video/src/core/events/VideoPlayerEvents.native.ts index 6daecbeb..ad71751f 100644 --- a/packages/react-native-video/src/core/events/VideoPlayerEvents.native.ts +++ b/packages/react-native-video/src/core/events/VideoPlayerEvents.native.ts @@ -5,7 +5,7 @@ import { VideoPlayerEventsBase } from './VideoPlayerEventsBase'; export class VideoPlayerEvents extends VideoPlayerEventsBase { addEventListener( event: Event, - callback: PlayerEvents[Event], + callback: PlayerEvents[Event] ): ListenerSubscription { switch (event) { // ----------------- Native-only Events ----------------- diff --git a/packages/react-native-video/src/core/events/VideoPlayerEvents.web.ts b/packages/react-native-video/src/core/events/VideoPlayerEvents.web.ts index 395276c2..00d0e473 100644 --- a/packages/react-native-video/src/core/events/VideoPlayerEvents.web.ts +++ b/packages/react-native-video/src/core/events/VideoPlayerEvents.web.ts @@ -1,11 +1,11 @@ -import type { AllPlayerEvents as PlayerEvents } from "../types/Events"; -import type { ListenerSubscription } from "../types/EventEmitter"; -import { VideoPlayerEventsBase } from "./VideoPlayerEventsBase"; +import type { AllPlayerEvents as PlayerEvents } from '../types/Events'; +import type { ListenerSubscription } from '../types/EventEmitter'; +import { VideoPlayerEventsBase } from './VideoPlayerEventsBase'; export class VideoPlayerEvents extends VideoPlayerEventsBase { addEventListener( event: Event, - callback: PlayerEvents[Event], + callback: PlayerEvents[Event] ): ListenerSubscription { switch (event) { // Web-only events will be added here diff --git a/packages/react-native-video/src/core/types/EventEmitter.ts b/packages/react-native-video/src/core/types/EventEmitter.ts index 63129aaf..83c4c591 100644 --- a/packages/react-native-video/src/core/types/EventEmitter.ts +++ b/packages/react-native-video/src/core/types/EventEmitter.ts @@ -24,50 +24,50 @@ export interface ListenerSubscription { export interface VideoPlayerEventEmitterBase { addOnAudioBecomingNoisyListener(listener: () => void): ListenerSubscription; addOnAudioFocusChangeListener( - listener: (hasAudioFocus: boolean) => void, + listener: (hasAudioFocus: boolean) => void ): ListenerSubscription; addOnBandwidthUpdateListener( - listener: (data: BandwidthData) => void, + listener: (data: BandwidthData) => void ): ListenerSubscription; addOnBufferListener( - listener: (buffering: boolean) => void, + listener: (buffering: boolean) => void ): ListenerSubscription; addOnControlsVisibleChangeListener( - listener: (visible: boolean) => void, + listener: (visible: boolean) => void ): ListenerSubscription; addOnEndListener(listener: () => void): ListenerSubscription; addOnExternalPlaybackChangeListener( - listener: (externalPlaybackActive: boolean) => void, + listener: (externalPlaybackActive: boolean) => void ): ListenerSubscription; addOnLoadListener(listener: (data: onLoadData) => void): ListenerSubscription; addOnLoadStartListener( - listener: (data: onLoadStartData) => void, + listener: (data: onLoadStartData) => void ): ListenerSubscription; addOnPlaybackStateChangeListener( - listener: (data: onPlaybackStateChangeData) => void, + listener: (data: onPlaybackStateChangeData) => void ): ListenerSubscription; addOnPlaybackRateChangeListener( - listener: (rate: number) => void, + listener: (rate: number) => void ): ListenerSubscription; addOnProgressListener( - listener: (data: onProgressData) => void, + listener: (data: onProgressData) => void ): ListenerSubscription; addOnReadyToDisplayListener(listener: () => void): ListenerSubscription; addOnSeekListener(listener: (position: number) => void): ListenerSubscription; addOnStatusChangeListener( - listener: (status: VideoPlayerStatus) => void, + listener: (status: VideoPlayerStatus) => void ): ListenerSubscription; addOnTimedMetadataListener( - listener: (data: TimedMetadata) => void, + listener: (data: TimedMetadata) => void ): ListenerSubscription; addOnTextTrackDataChangedListener( - listener: (data: string[]) => void, + listener: (data: string[]) => void ): ListenerSubscription; addOnTrackChangeListener( - listener: (track: TextTrack | null) => void, + listener: (track: TextTrack | null) => void ): ListenerSubscription; addOnVolumeChangeListener( - listener: (data: onVolumeChangeData) => void, + listener: (data: onVolumeChangeData) => void ): ListenerSubscription; clearAllListeners(): void; } diff --git a/packages/react-native-video/src/core/utils/sourceUtils.ts b/packages/react-native-video/src/core/utils/sourceUtils.ts index ac2b184b..02f50a11 100644 --- a/packages/react-native-video/src/core/utils/sourceUtils.ts +++ b/packages/react-native-video/src/core/utils/sourceUtils.ts @@ -8,4 +8,3 @@ export const isVideoPlayerSource = (obj: any): obj is VideoPlayerSource => { obj.name === 'VideoPlayerSource' // obj.name is 'VideoPlayerSource' ); }; - diff --git a/packages/react-native-video/src/core/video-view/VideoView.tsx b/packages/react-native-video/src/core/video-view/VideoView.tsx index 8f6acb6b..19917f17 100644 --- a/packages/react-native-video/src/core/video-view/VideoView.tsx +++ b/packages/react-native-video/src/core/video-view/VideoView.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import type { ViewStyle } from 'react-native'; import { NitroModules } from 'react-native-nitro-modules'; import type { ListenerSubscription } from '../../spec/nitro/VideoPlayerEventEmitter.nitro'; import type { diff --git a/packages/react-native-video/src/core/video-view/VideoView.web.tsx b/packages/react-native-video/src/core/video-view/VideoView.web.tsx index e34a9479..330b402b 100644 --- a/packages/react-native-video/src/core/video-view/VideoView.web.tsx +++ b/packages/react-native-video/src/core/video-view/VideoView.web.tsx @@ -6,16 +6,21 @@ import { useImperativeHandle, useRef, type CSSProperties, -} from "react"; -import { View } from "react-native"; -import type { VideoPlayer } from "../VideoPlayer.web"; -import type { VideoViewEvents } from "../types/Events"; -import type { ListenerSubscription } from "../types/EventEmitter"; -import type { VideoViewProps, VideoViewRef } from "./VideoViewProps"; -import { createPlayer, videoFeatures, usePlayerContext, useMediaAttach } from "@videojs/react"; -import { VideoSkin } from "@videojs/react/video"; -import "@videojs/react/video/skin.css"; -import type { VideoStore } from "../web/VideoStore"; +} from 'react'; +import { View } from 'react-native'; +import type { VideoPlayer } from '../VideoPlayer.web'; +import type { VideoViewEvents } from '../types/Events'; +import type { ListenerSubscription } from '../types/EventEmitter'; +import type { VideoViewProps, VideoViewRef } from './VideoViewProps'; +import { + createPlayer, + videoFeatures, + usePlayerContext, + useMediaAttach, +} from '@videojs/react'; +import { VideoSkin } from '@videojs/react/video'; +import '@videojs/react/video/skin.css'; +import type { VideoStore } from '../web/VideoStore'; const Player = createPlayer({ features: videoFeatures }); @@ -38,7 +43,11 @@ function PlayerBridge({ player }: { player: VideoPlayer }) { return () => { player.__setStore(null); - try { detach?.(); } catch { /* store may already be destroyed by Provider */ } + try { + detach?.(); + } catch { + /* store may already be destroyed by Provider */ + } setMedia?.(null); }; }, [store, player, setMedia, container]); @@ -51,20 +60,26 @@ function PlayerBridge({ player }: { player: VideoPlayer }) { * The element is created in VideoPlayer constructor so it already has * source and event listeners attached. */ -function VideoElement({ player, objectFit }: { player: VideoPlayer; objectFit: string }) { +function VideoElement({ + player, + objectFit, +}: { + player: VideoPlayer; + objectFit: string; +}) { const mountRef = useCallback( (container: HTMLDivElement | null) => { if (!container) return; const video = player.__getMedia(); - Object.assign(video.style, { width: "100%", height: "100%", objectFit }); + Object.assign(video.style, { width: '100%', height: '100%', objectFit }); if (video.parentNode !== container) { container.appendChild(video); } }, - [player, objectFit], + [player, objectFit] ); - return
; + return
; } const VideoView = forwardRef( @@ -72,24 +87,25 @@ const VideoView = forwardRef( { player: nPlayer, controls = false, - resizeMode = "none", - pictureInPicture = false, - autoEnterPictureInPicture = false, - keepScreenAwake = true, + resizeMode = 'none', + // Destructured to exclude from ...props (not used on web) + pictureInPicture: _pip = false, + autoEnterPictureInPicture: _autoPip = false, + keepScreenAwake: _keepAwake = true, ...props }, - ref, + ref ) => { const player = nPlayer as unknown as VideoPlayer; const containerRef = useRef(null); - const objectFitMap: Record = { - contain: "contain", - cover: "cover", - stretch: "fill", - none: "contain", + const objectFitMap: Record = { + contain: 'contain', + cover: 'cover', + stretch: 'fill', + none: 'contain', }; - const objectFit = objectFitMap[resizeMode] ?? "contain"; + const objectFit = objectFitMap[resizeMode] ?? 'contain'; useImperativeHandle( ref, @@ -110,12 +126,12 @@ const VideoView = forwardRef( document.pictureInPictureEnabled ?? false, addEventListener: ( _event: Event, - _callback: VideoViewEvents[Event], + _callback: VideoViewEvents[Event] ): ListenerSubscription => { return { remove: () => {} }; }, }), - [player], + [player] ); const videoContent = ; @@ -126,22 +142,24 @@ const VideoView = forwardRef( {controls ? {videoContent} : videoContent} ); - }, + } ); -VideoView.displayName = "VideoView"; +VideoView.displayName = 'VideoView'; export default memo(VideoView); diff --git a/packages/react-native-video/src/core/web/MediaSession.ts b/packages/react-native-video/src/core/web/MediaSession.ts index 19f3f7be..9b786fc9 100644 --- a/packages/react-native-video/src/core/web/MediaSession.ts +++ b/packages/react-native-video/src/core/web/MediaSession.ts @@ -1,8 +1,8 @@ -import type { CustomVideoMetadata } from "../types/VideoConfig"; -import type { MediaSessionStore } from "./VideoStore"; +import type { CustomVideoMetadata } from '../types/VideoConfig'; +import type { MediaSessionStore } from './VideoStore'; function getMediaSession(): MediaSession | undefined { - if (typeof window === "undefined") return undefined; + if (typeof window === 'undefined') return undefined; return window.navigator?.mediaSession; } @@ -20,25 +20,55 @@ export class MediaSessionHandler { const defaultSkipTime = 15; - const actionHandlers: Array<[MediaSessionAction, MediaSessionActionHandler]> = [ - ["play", () => { this.store.play(); }], - ["pause", () => { this.store.pause(); }], - ["stop", () => { - this.store.pause(); - this.store.seek(0); - }], - ["seekbackward", (details) => { - const offset = (details as MediaSessionActionDetails).seekOffset || defaultSkipTime; - this.store.seek(Math.max(0, this.store.currentTime - offset)); - }], - ["seekforward", (details) => { - const offset = (details as MediaSessionActionDetails).seekOffset || defaultSkipTime; - this.store.seek(Math.min(this.store.duration, this.store.currentTime + offset)); - }], - ["seekto", (details) => { - const seekTime = (details as MediaSessionActionDetails).seekTime; - if (seekTime != null) this.store.seek(seekTime); - }], + const actionHandlers: Array< + [MediaSessionAction, MediaSessionActionHandler] + > = [ + [ + 'play', + () => { + this.store.play(); + }, + ], + [ + 'pause', + () => { + this.store.pause(); + }, + ], + [ + 'stop', + () => { + this.store.pause(); + this.store.seek(0); + }, + ], + [ + 'seekbackward', + (details) => { + const offset = + (details as MediaSessionActionDetails).seekOffset || + defaultSkipTime; + this.store.seek(Math.max(0, this.store.currentTime - offset)); + }, + ], + [ + 'seekforward', + (details) => { + const offset = + (details as MediaSessionActionDetails).seekOffset || + defaultSkipTime; + this.store.seek( + Math.min(this.store.duration, this.store.currentTime + offset) + ); + }, + ], + [ + 'seekto', + (details) => { + const seekTime = (details as MediaSessionActionDetails).seekTime; + if (seekTime != null) this.store.seek(seekTime); + }, + ], ]; for (const [action, handler] of actionHandlers) { @@ -51,9 +81,12 @@ export class MediaSessionHandler { // Subscribe to store for playback state and position updates const unsubscribe = this.store.subscribe(() => { - mediaSession.playbackState = this.store.paused ? "paused" : "playing"; + mediaSession.playbackState = this.store.paused ? 'paused' : 'playing'; - if ("setPositionState" in mediaSession && Number.isFinite(this.store.duration)) { + if ( + 'setPositionState' in mediaSession && + Number.isFinite(this.store.duration) + ) { try { mediaSession.setPositionState({ duration: this.store.duration, diff --git a/packages/react-native-video/src/core/web/VideoStore.ts b/packages/react-native-video/src/core/web/VideoStore.ts index d8716ec9..1b3e81e1 100644 --- a/packages/react-native-video/src/core/web/VideoStore.ts +++ b/packages/react-native-video/src/core/web/VideoStore.ts @@ -19,7 +19,12 @@ export interface VideoStore { readonly source: string | null; readonly buffered: [number, number][]; readonly error: { code: number; message: string } | null; - readonly textTrackList: Array<{ kind: string; label: string; language: string; mode: string }>; + readonly textTrackList: Array<{ + kind: string; + label: string; + language: string; + mode: string; + }>; readonly destroyed: boolean; readonly target: unknown; readonly pipAvailability: string; @@ -38,7 +43,10 @@ export interface VideoStore { // Lifecycle subscribe(callback: () => void): () => void; - attach(target: { media: HTMLVideoElement; container: HTMLElement | null }): () => void; + attach(target: { + media: HTMLVideoElement; + container: HTMLElement | null; + }): () => void; destroy(): void; } @@ -47,5 +55,12 @@ export interface VideoStore { */ export type MediaSessionStore = Pick< VideoStore, - "paused" | "currentTime" | "duration" | "playbackRate" | "play" | "pause" | "seek" | "subscribe" + | 'paused' + | 'currentTime' + | 'duration' + | 'playbackRate' + | 'play' + | 'pause' + | 'seek' + | 'subscribe' >; diff --git a/packages/react-native-video/src/core/web/WebEventEmitter.ts b/packages/react-native-video/src/core/web/WebEventEmitter.ts index 758d98e8..da9e95c8 100644 --- a/packages/react-native-video/src/core/web/WebEventEmitter.ts +++ b/packages/react-native-video/src/core/web/WebEventEmitter.ts @@ -6,8 +6,8 @@ import type { onProgressData, onVolumeChangeData, TimedMetadata, -} from "../types/Events"; -import type { TextTrack } from "../types/TextTrack"; +} from '../types/Events'; +import type { TextTrack } from '../types/TextTrack'; import { type LibraryError, type PlayerError, @@ -15,14 +15,14 @@ import { type UnknownError, VideoError, type VideoRuntimeError, -} from "../types/VideoError"; -import type { VideoPlayerStatus } from "../types/VideoPlayerStatus"; +} from '../types/VideoError'; +import type { VideoPlayerStatus } from '../types/VideoPlayerStatus'; import type { ListenerSubscription, VideoPlayerEventEmitterBase, -} from "../types/EventEmitter"; +} from '../types/EventEmitter'; -import type { VideoStore } from "./VideoStore"; +import type { VideoStore } from './VideoStore'; /** * WebEventEmitter bridges HTML5 media events to our event system. @@ -38,7 +38,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase { constructor( store: VideoStore | null, - private getMedia: () => HTMLVideoElement | null, + private getMedia: () => HTMLVideoElement | null ) { // Attach to video element immediately if available this._attachMediaListeners(); @@ -72,124 +72,159 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase { const cleanups: Array<() => void> = []; - cleanups.push(on("play", () => { - this._emit("onPlaybackStateChange", { - isPlaying: true, - isBuffering: this._isBuffering, - }); - })); - - cleanups.push(on("pause", () => { - this._emit("onPlaybackStateChange", { - isPlaying: false, - isBuffering: this._isBuffering, - }); - })); - - cleanups.push(on("waiting", () => { - this._isBuffering = true; - this._emit("onBuffer", true); - this._emit("onStatusChange", "loading"); - })); - - cleanups.push(on("canplay", () => { - this._isBuffering = false; - this._emit("onBuffer", false); - this._emit("onStatusChange", "readyToPlay"); - })); - - cleanups.push(on("timeupdate", () => { - const buffered = video.buffered; - const lastBuffered = buffered.length > 0 ? buffered.end(buffered.length - 1) : 0; - this._emit("onProgress", { - currentTime: video.currentTime, - bufferDuration: lastBuffered, - }); - })); - - cleanups.push(on("durationchange", () => { - if (video.duration > 0) { - this._emit("onLoad", { - currentTime: video.currentTime, - duration: video.duration, - width: video.videoWidth, - height: video.videoHeight, - orientation: "unknown", + cleanups.push( + on('play', () => { + this._emit('onPlaybackStateChange', { + isPlaying: true, + isBuffering: this._isBuffering, }); - } - })); + }) + ); - cleanups.push(on("ended", () => { - this._emit("onEnd"); - this._emit("onStatusChange", "idle"); - })); + cleanups.push( + on('pause', () => { + this._emit('onPlaybackStateChange', { + isPlaying: false, + isBuffering: this._isBuffering, + }); + }) + ); - cleanups.push(on("ratechange", () => { - this._emit("onPlaybackRateChange", video.playbackRate); - })); + cleanups.push( + on('waiting', () => { + this._isBuffering = true; + this._emit('onBuffer', true); + this._emit('onStatusChange', 'loading'); + }) + ); - cleanups.push(on("loadeddata", () => { - this._emit("onReadyToDisplay"); - })); + cleanups.push( + on('canplay', () => { + this._isBuffering = false; + this._emit('onBuffer', false); + this._emit('onStatusChange', 'readyToPlay'); + }) + ); - cleanups.push(on("seeked", () => { - this._emit("onSeek", video.currentTime); - })); + cleanups.push( + on('timeupdate', () => { + const buffered = video.buffered; + const lastBuffered = + buffered.length > 0 ? buffered.end(buffered.length - 1) : 0; + this._emit('onProgress', { + currentTime: video.currentTime, + bufferDuration: lastBuffered, + }); + }) + ); - cleanups.push(on("volumechange", () => { - this._emit("onVolumeChange", { - volume: video.volume, - muted: video.muted, - }); - })); - - cleanups.push(on("loadstart", () => { - this._emit("onLoadStart", { - sourceType: "network", - source: { - uri: video.currentSrc || video.src, - config: { - uri: video.currentSrc || video.src, - externalSubtitles: [], - }, - getAssetInformationAsync: async () => ({ - duration: video.duration || NaN, + cleanups.push( + on('durationchange', () => { + if (video.duration > 0) { + this._emit('onLoad', { + currentTime: video.currentTime, + duration: video.duration, width: video.videoWidth, height: video.videoHeight, - orientation: "unknown", - bitrate: NaN, - fileSize: -1n, - isHDR: false, - isLive: false, - }), - }, - }); - })); + orientation: 'unknown', + }); + } + }) + ); - cleanups.push(on("error", () => { - this._emit("onStatusChange", "error"); - const err = video.error; - if (!err) { - console.error("Unknown error occurred in player"); - return; - } - const codeMap: Record = { - 1: "player/asset-not-initialized", - 2: "player/not-initialized", - 3: "player/invalid-source", - 4: "source/unsupported-content-type", - }; - this._emit("onError", new VideoError(codeMap[err.code] ?? "unknown/unknown", err.message)); - })); + cleanups.push( + on('ended', () => { + this._emit('onEnd'); + this._emit('onStatusChange', 'idle'); + }) + ); - this._mediaCleanup = () => { cleanups.forEach((fn) => fn()); }; + cleanups.push( + on('ratechange', () => { + this._emit('onPlaybackRateChange', video.playbackRate); + }) + ); + + cleanups.push( + on('loadeddata', () => { + this._emit('onReadyToDisplay'); + }) + ); + + cleanups.push( + on('seeked', () => { + this._emit('onSeek', video.currentTime); + }) + ); + + cleanups.push( + on('volumechange', () => { + this._emit('onVolumeChange', { + volume: video.volume, + muted: video.muted, + }); + }) + ); + + cleanups.push( + on('loadstart', () => { + this._emit('onLoadStart', { + sourceType: 'network', + source: { + uri: video.currentSrc || video.src, + config: { + uri: video.currentSrc || video.src, + externalSubtitles: [], + }, + getAssetInformationAsync: async () => ({ + duration: video.duration || NaN, + width: video.videoWidth, + height: video.videoHeight, + orientation: 'unknown', + bitrate: NaN, + fileSize: -1n, + isHDR: false, + isLive: false, + }), + }, + }); + }) + ); + + cleanups.push( + on('error', () => { + this._emit('onStatusChange', 'error'); + const err = video.error; + if (!err) { + console.error('Unknown error occurred in player'); + return; + } + const codeMap: Record< + number, + LibraryError | PlayerError | SourceError | UnknownError + > = { + 1: 'player/asset-not-initialized', + 2: 'player/not-initialized', + 3: 'player/invalid-source', + 4: 'source/unsupported-content-type', + }; + this._emit( + 'onError', + new VideoError(codeMap[err.code] ?? 'unknown/unknown', err.message) + ); + }) + ); + + this._mediaCleanup = () => { + cleanups.forEach((fn) => fn()); + }; } // --- Listener infrastructure --- private _addListener( event: string, - listener: (...args: any[]) => void, + listener: (...args: any[]) => void ): ListenerSubscription { if (!this._listeners.has(event)) { this._listeners.set(event, new Set()); @@ -209,111 +244,111 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase { // --- Listener registration (implements VideoPlayerEventEmitterBase) --- addOnAudioBecomingNoisyListener(listener: () => void): ListenerSubscription { - return this._addListener("onAudioBecomingNoisy", listener); + return this._addListener('onAudioBecomingNoisy', listener); } addOnAudioFocusChangeListener( - listener: (hasAudioFocus: boolean) => void, + listener: (hasAudioFocus: boolean) => void ): ListenerSubscription { - return this._addListener("onAudioFocusChange", listener); + return this._addListener('onAudioFocusChange', listener); } addOnBandwidthUpdateListener( - listener: (data: BandwidthData) => void, + listener: (data: BandwidthData) => void ): ListenerSubscription { - return this._addListener("onBandwidthUpdate", listener); + return this._addListener('onBandwidthUpdate', listener); } addOnBufferListener( - listener: (buffering: boolean) => void, + listener: (buffering: boolean) => void ): ListenerSubscription { - return this._addListener("onBuffer", listener); + return this._addListener('onBuffer', listener); } addOnControlsVisibleChangeListener( - listener: (visible: boolean) => void, + listener: (visible: boolean) => void ): ListenerSubscription { - return this._addListener("onControlsVisibleChange", listener); + return this._addListener('onControlsVisibleChange', listener); } addOnEndListener(listener: () => void): ListenerSubscription { - return this._addListener("onEnd", listener); + return this._addListener('onEnd', listener); } addOnExternalPlaybackChangeListener( - listener: (externalPlaybackActive: boolean) => void, + listener: (externalPlaybackActive: boolean) => void ): ListenerSubscription { - return this._addListener("onExternalPlaybackChange", listener); + return this._addListener('onExternalPlaybackChange', listener); } addOnLoadListener( - listener: (data: onLoadData) => void, + listener: (data: onLoadData) => void ): ListenerSubscription { - return this._addListener("onLoad", listener); + return this._addListener('onLoad', listener); } addOnLoadStartListener( - listener: (data: onLoadStartData) => void, + listener: (data: onLoadStartData) => void ): ListenerSubscription { - return this._addListener("onLoadStart", listener); + return this._addListener('onLoadStart', listener); } addOnPlaybackStateChangeListener( - listener: (data: onPlaybackStateChangeData) => void, + listener: (data: onPlaybackStateChangeData) => void ): ListenerSubscription { - return this._addListener("onPlaybackStateChange", listener); + return this._addListener('onPlaybackStateChange', listener); } addOnPlaybackRateChangeListener( - listener: (rate: number) => void, + listener: (rate: number) => void ): ListenerSubscription { - return this._addListener("onPlaybackRateChange", listener); + return this._addListener('onPlaybackRateChange', listener); } addOnProgressListener( - listener: (data: onProgressData) => void, + listener: (data: onProgressData) => void ): ListenerSubscription { - return this._addListener("onProgress", listener); + return this._addListener('onProgress', listener); } addOnReadyToDisplayListener(listener: () => void): ListenerSubscription { - return this._addListener("onReadyToDisplay", listener); + return this._addListener('onReadyToDisplay', listener); } addOnSeekListener( - listener: (position: number) => void, + listener: (position: number) => void ): ListenerSubscription { - return this._addListener("onSeek", listener); + return this._addListener('onSeek', listener); } addOnStatusChangeListener( - listener: (status: VideoPlayerStatus) => void, + listener: (status: VideoPlayerStatus) => void ): ListenerSubscription { - return this._addListener("onStatusChange", listener); + return this._addListener('onStatusChange', listener); } addOnTimedMetadataListener( - listener: (data: TimedMetadata) => void, + listener: (data: TimedMetadata) => void ): ListenerSubscription { - return this._addListener("onTimedMetadata", listener); + return this._addListener('onTimedMetadata', listener); } addOnTextTrackDataChangedListener( - listener: (data: string[]) => void, + listener: (data: string[]) => void ): ListenerSubscription { - return this._addListener("onTextTrackDataChanged", listener); + return this._addListener('onTextTrackDataChanged', listener); } addOnTrackChangeListener( - listener: (track: TextTrack | null) => void, + listener: (track: TextTrack | null) => void ): ListenerSubscription { - return this._addListener("onTrackChange", listener); + return this._addListener('onTrackChange', listener); } addOnVolumeChangeListener( - listener: (data: onVolumeChangeData) => void, + listener: (data: onVolumeChangeData) => void ): ListenerSubscription { - return this._addListener("onVolumeChange", listener); + return this._addListener('onVolumeChange', listener); } clearAllListeners(): void { @@ -321,8 +356,8 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase { } addOnErrorListener( - listener: (error: VideoRuntimeError) => void, + listener: (error: VideoRuntimeError) => void ): ListenerSubscription { - return this._addListener("onError", listener); + return this._addListener('onError', listener); } } diff --git a/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts b/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts index 83bc3068..cf026e1d 100644 --- a/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts +++ b/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts @@ -5,8 +5,7 @@ import type { VideoPlayerEventEmitter } from './VideoPlayerEventEmitter.nitro'; import type { VideoPlayerSource } from './VideoPlayerSource.nitro'; export interface VideoPlayer - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }>, - VideoPlayerBase { + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }>, VideoPlayerBase { // Override with (hybrid) VideoPlayerSource readonly source: VideoPlayerSource; @@ -49,7 +48,9 @@ export interface VideoPlayer release(): void; } -export interface VideoPlayerFactory - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { +export interface VideoPlayerFactory extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { createPlayer(source: VideoPlayerSource): VideoPlayer; } diff --git a/packages/react-native-video/src/spec/nitro/VideoPlayerEventEmitter.nitro.ts b/packages/react-native-video/src/spec/nitro/VideoPlayerEventEmitter.nitro.ts index 5f0cc4c9..32552c96 100644 --- a/packages/react-native-video/src/spec/nitro/VideoPlayerEventEmitter.nitro.ts +++ b/packages/react-native-video/src/spec/nitro/VideoPlayerEventEmitter.nitro.ts @@ -18,8 +18,10 @@ export interface ListenerSubscription { remove(): void; } -export interface VideoPlayerEventEmitter - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { +export interface VideoPlayerEventEmitter extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { /** * Adds a listener for the `onAudioBecomingNoisy` event. * @see {@link VideoPlayerEvents.onAudioBecomingNoisy} diff --git a/packages/react-native-video/src/spec/nitro/VideoPlayerSource.nitro.ts b/packages/react-native-video/src/spec/nitro/VideoPlayerSource.nitro.ts index df6b4656..1bbcd87c 100644 --- a/packages/react-native-video/src/spec/nitro/VideoPlayerSource.nitro.ts +++ b/packages/react-native-video/src/spec/nitro/VideoPlayerSource.nitro.ts @@ -8,11 +8,14 @@ import type { VideoPlayerSourceBase } from '../../core/types/VideoPlayerSourceBa * It provides functions to get information about the asset. */ export interface VideoPlayerSource - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }>, + extends + HybridObject<{ ios: 'swift'; android: 'kotlin' }>, VideoPlayerSourceBase {} -export interface VideoPlayerSourceFactory - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { +export interface VideoPlayerSourceFactory extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { fromUri(uri: string): VideoPlayerSource; fromVideoConfig(config: NativeVideoConfig): VideoPlayerSource; } diff --git a/packages/react-native-video/src/spec/nitro/VideoViewViewManager.nitro.ts b/packages/react-native-video/src/spec/nitro/VideoViewViewManager.nitro.ts index d0836066..7562a07d 100644 --- a/packages/react-native-video/src/spec/nitro/VideoViewViewManager.nitro.ts +++ b/packages/react-native-video/src/spec/nitro/VideoViewViewManager.nitro.ts @@ -6,8 +6,10 @@ import type { ListenerSubscription } from './VideoPlayerEventEmitter.nitro'; export type SurfaceType = 'surface' | 'texture'; // @internal -export interface VideoViewViewManager - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { +export interface VideoViewViewManager extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { player?: VideoPlayer; controls: boolean; pictureInPicture: boolean; @@ -86,7 +88,9 @@ export interface VideoViewViewManager } // @internal -export interface VideoViewViewManagerFactory - extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { +export interface VideoViewViewManagerFactory extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { createViewManager(nitroId: number): VideoViewViewManager; } diff --git a/packages/react-native-video/tsconfig.web.json b/packages/react-native-video/tsconfig.web.json index 9d0989e2..265c0599 100644 --- a/packages/react-native-video/tsconfig.web.json +++ b/packages/react-native-video/tsconfig.web.json @@ -3,5 +3,6 @@ "compilerOptions": { "lib": ["ESNext", "dom"] }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [] }