From 62b965f9e070fb59047e926057ebfb838829f392 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 27 Sep 2025 18:36:25 +0200 Subject: [PATCH] Rework event handler to use add/remove styyle --- docs/docs/events/events.md | 14 +- .../src/core/VideoPlayer.ts | 8 +- .../src/core/VideoPlayerEvents.ts | 212 +++--------------- .../src/core/hooks/useEvent.ts | 32 +-- .../src/core/types/Events.ts | 6 + 5 files changed, 60 insertions(+), 212 deletions(-) diff --git a/docs/docs/events/events.md b/docs/docs/events/events.md index 68ef606c..16339377 100644 --- a/docs/docs/events/events.md +++ b/docs/docs/events/events.md @@ -87,17 +87,17 @@ import { VideoPlayer } from 'react-native-video'; const player = new VideoPlayer('https://example.com/video.mp4'); -player.onLoad = (data) => { +player.addEventListener('onLoad', (data) => { console.log('Video loaded! Duration:', data.duration); -}; +}); -player.onProgress = (data) => { +player.addEventListener('onProgress', (data) => { console.log('Current time:', data.currentTime); -}; +}); -player.onError = (error) => { +player.addEventListener('onError', (error) => { console.error('Player Error:', error.code, error.message); -}; +}); player.play(); ``` @@ -105,4 +105,4 @@ player.play(); ## Clearing Events - The `player.clearEvent(eventName)` method can be used to clear a specific native event handler. -- When a player instance is no longer needed and `player.release()` is called, all event listeners are automatically cleared \ No newline at end of file +- When a player instance is no longer needed and `player.release()` is called, all event listeners are automatically cleared diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index 850affb5..f77d23a5 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -20,8 +20,6 @@ import { VideoPlayerEvents } from './VideoPlayerEvents'; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected player: VideoPlayerImpl; - public onError?: (error: VideoRuntimeError) => void = undefined; - constructor(source: VideoSource | VideoConfig | VideoPlayerSource) { const hybridSource = createSource(source); const player = createPlayer(hybridSource); @@ -57,8 +55,10 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { private throwError(error: unknown) { const parsedError = tryParseNativeVideoError(error); - if (parsedError instanceof VideoRuntimeError && this.onError) { - this.onError(parsedError); + if ( + parsedError instanceof VideoRuntimeError + && this.triggerEvent('onError', parsedError) + ) { // We don't throw errors if onError is provided return; } diff --git a/packages/react-native-video/src/core/VideoPlayerEvents.ts b/packages/react-native-video/src/core/VideoPlayerEvents.ts index 70d93ddc..c7d6a07d 100644 --- a/packages/react-native-video/src/core/VideoPlayerEvents.ts +++ b/packages/react-native-video/src/core/VideoPlayerEvents.ts @@ -1,10 +1,11 @@ import type { VideoPlayerEventEmitter } from '../spec/nitro/VideoPlayerEventEmitter.nitro'; -import type { VideoPlayerEvents as VideoPlayerEventsInterface } from './types/Events'; +import type { AllPlayerEvents as PlayerEvents } from './types/Events'; -export class VideoPlayerEvents implements VideoPlayerEventsInterface { +export class VideoPlayerEvents { protected eventEmitter: VideoPlayerEventEmitter; + protected eventListeners: Partial void>>> = {}; - protected readonly supportedEvents: (keyof VideoPlayerEventsInterface)[] = [ + protected readonly supportedEvents: (keyof PlayerEvents)[] = [ 'onAudioBecomingNoisy', 'onAudioFocusChange', 'onBandwidthUpdate', @@ -28,6 +29,37 @@ export class VideoPlayerEvents implements VideoPlayerEventsInterface { constructor(eventEmitter: VideoPlayerEventEmitter) { this.eventEmitter = eventEmitter; + for (let event of this.supportedEvents){ + // @ts-expect-error we narrow the type of the event + this.eventEmitter[event] = this.triggerEvent.bind(this, event); + } + } + + protected triggerEvent( + event: Event, + ...params: Parameters + ): boolean { + if (!this.eventListeners[event]?.size) + return false; + for (let fn of this.eventListeners[event]) { + fn(...params); + } + return true; + } + + addEventListener( + event: Event, + callback: PlayerEvents[Event] + ) { + this.eventListeners[event] ??= new Set(); + this.eventListeners[event].add(callback); + } + + removeEventListener( + event: Event, + callback: PlayerEvents[Event] + ) { + this.eventListeners[event]!.delete(callback); } /** @@ -43,177 +75,7 @@ export class VideoPlayerEvents implements VideoPlayerEventsInterface { * Clears a specific event from the event emitter. * @param event - The name of the event to clear. */ - clearEvent(event: keyof VideoPlayerEventsInterface) { - this.eventEmitter[event] = VideoPlayerEvents.NOOP; - } - - static NOOP = () => {}; - - set onAudioBecomingNoisy( - value: VideoPlayerEventsInterface['onAudioBecomingNoisy'] - ) { - this.eventEmitter.onAudioBecomingNoisy = value; - } - - get onAudioBecomingNoisy(): VideoPlayerEventsInterface['onAudioBecomingNoisy'] { - return this.eventEmitter.onAudioBecomingNoisy; - } - - set onAudioFocusChange( - value: VideoPlayerEventsInterface['onAudioFocusChange'] - ) { - this.eventEmitter.onAudioFocusChange = value; - } - - get onAudioFocusChange(): VideoPlayerEventsInterface['onAudioFocusChange'] { - return this.eventEmitter.onAudioFocusChange; - } - - set onBandwidthUpdate( - value: VideoPlayerEventsInterface['onBandwidthUpdate'] - ) { - this.eventEmitter.onBandwidthUpdate = value; - } - - get onBandwidthUpdate(): VideoPlayerEventsInterface['onBandwidthUpdate'] { - return this.eventEmitter.onBandwidthUpdate; - } - - set onBuffer(value: VideoPlayerEventsInterface['onBuffer']) { - this.eventEmitter.onBuffer = value; - } - - get onBuffer(): VideoPlayerEventsInterface['onBuffer'] { - return this.eventEmitter.onBuffer; - } - - set onControlsVisibleChange( - value: VideoPlayerEventsInterface['onControlsVisibleChange'] - ) { - this.eventEmitter.onControlsVisibleChange = value; - } - - get onControlsVisibleChange(): VideoPlayerEventsInterface['onControlsVisibleChange'] { - return this.eventEmitter.onControlsVisibleChange; - } - - set onEnd(value: VideoPlayerEventsInterface['onEnd']) { - this.eventEmitter.onEnd = value; - } - - get onEnd(): VideoPlayerEventsInterface['onEnd'] { - return this.eventEmitter.onEnd; - } - - set onExternalPlaybackChange( - value: VideoPlayerEventsInterface['onExternalPlaybackChange'] - ) { - this.eventEmitter.onExternalPlaybackChange = value; - } - - get onExternalPlaybackChange(): VideoPlayerEventsInterface['onExternalPlaybackChange'] { - return this.eventEmitter.onExternalPlaybackChange; - } - - set onLoad(value: VideoPlayerEventsInterface['onLoad']) { - this.eventEmitter.onLoad = value; - } - - get onLoad(): VideoPlayerEventsInterface['onLoad'] { - return this.eventEmitter.onLoad; - } - - set onLoadStart(value: VideoPlayerEventsInterface['onLoadStart']) { - this.eventEmitter.onLoadStart = value; - } - - get onLoadStart(): VideoPlayerEventsInterface['onLoadStart'] { - return this.eventEmitter.onLoadStart; - } - - set onPlaybackStateChange( - value: VideoPlayerEventsInterface['onPlaybackStateChange'] - ) { - this.eventEmitter.onPlaybackStateChange = value; - } - - get onPlaybackStateChange(): VideoPlayerEventsInterface['onPlaybackStateChange'] { - return this.eventEmitter.onPlaybackStateChange; - } - - set onPlaybackRateChange( - value: VideoPlayerEventsInterface['onPlaybackRateChange'] - ) { - this.eventEmitter.onPlaybackRateChange = value; - } - - get onPlaybackRateChange(): VideoPlayerEventsInterface['onPlaybackRateChange'] { - return this.eventEmitter.onPlaybackRateChange; - } - - set onProgress(value: VideoPlayerEventsInterface['onProgress']) { - this.eventEmitter.onProgress = value; - } - - get onProgress(): VideoPlayerEventsInterface['onProgress'] { - return this.eventEmitter.onProgress; - } - - set onReadyToDisplay(value: VideoPlayerEventsInterface['onReadyToDisplay']) { - this.eventEmitter.onReadyToDisplay = value; - } - - get onReadyToDisplay(): VideoPlayerEventsInterface['onReadyToDisplay'] { - return this.eventEmitter.onReadyToDisplay; - } - - set onSeek(value: VideoPlayerEventsInterface['onSeek']) { - this.eventEmitter.onSeek = value; - } - - get onSeek(): VideoPlayerEventsInterface['onSeek'] { - return this.eventEmitter.onSeek; - } - - set onStatusChange(value: VideoPlayerEventsInterface['onStatusChange']) { - this.eventEmitter.onStatusChange = value; - } - - get onStatusChange(): VideoPlayerEventsInterface['onStatusChange'] { - return this.eventEmitter.onStatusChange; - } - - set onTimedMetadata(value: VideoPlayerEventsInterface['onTimedMetadata']) { - this.eventEmitter.onTimedMetadata = value; - } - - get onTimedMetadata(): VideoPlayerEventsInterface['onTimedMetadata'] { - return this.eventEmitter.onTimedMetadata; - } - - set onTextTrackDataChanged( - value: VideoPlayerEventsInterface['onTextTrackDataChanged'] - ) { - this.eventEmitter.onTextTrackDataChanged = value; - } - - get onTextTrackDataChanged(): VideoPlayerEventsInterface['onTextTrackDataChanged'] { - return this.eventEmitter.onTextTrackDataChanged; - } - - set onTrackChange(value: VideoPlayerEventsInterface['onTrackChange']) { - this.eventEmitter.onTrackChange = value; - } - - get onTrackChange(): VideoPlayerEventsInterface['onTrackChange'] { - return this.eventEmitter.onTrackChange; - } - - set onVolumeChange(value: VideoPlayerEventsInterface['onVolumeChange']) { - this.eventEmitter.onVolumeChange = value; - } - - get onVolumeChange(): VideoPlayerEventsInterface['onVolumeChange'] { - return this.eventEmitter.onVolumeChange; + clearEvent(event: keyof PlayerEvents) { + this.eventListeners[event]?.clear(); } } diff --git a/packages/react-native-video/src/core/hooks/useEvent.ts b/packages/react-native-video/src/core/hooks/useEvent.ts index 246a3107..9a9482e2 100644 --- a/packages/react-native-video/src/core/hooks/useEvent.ts +++ b/packages/react-native-video/src/core/hooks/useEvent.ts @@ -1,19 +1,6 @@ import { useEffect } from 'react'; import { VideoPlayer } from '../VideoPlayer'; -import { type VideoPlayerEvents } from '../types/Events'; - -// Omit undefined from events -type NonUndefined = T extends undefined ? never : T; - -// Valid events names -type Events = keyof VideoPlayerEvents | 'onError'; - -// Valid events params -type EventsParams = T extends keyof VideoPlayerEvents - ? // (Native) Events from VideoPlayerEvents - Parameters - : // (JS) Events from Video Player - Parameters>; +import { type AllPlayerEvents } from '../types/Events'; /** * Attaches an event listener to a `VideoPlayer` instance for a specified event. @@ -22,22 +9,15 @@ type EventsParams = T extends keyof VideoPlayerEvents * @param event - The name of the event to attach the callback to * @param callback - The callback for the event */ -export const useEvent = ( +export const useEvent = ( player: VideoPlayer, event: T, - callback: (...args: EventsParams) => void + callback: AllPlayerEvents[T] ) => { useEffect(() => { - // @ts-expect-error we narrow the type of the event - player[event] = callback; + player.addEventListener(event, callback); - return () => { - if (event === 'onError') { - // onError is not native event, so we can set it to undefined - player.onError = undefined; - } else { - player.clearEvent(event); - } - }; + return () => player.removeEventListener(event, callback); + ; }, [player, event, callback]); }; diff --git a/packages/react-native-video/src/core/types/Events.ts b/packages/react-native-video/src/core/types/Events.ts index abf2cf48..b109f61b 100644 --- a/packages/react-native-video/src/core/types/Events.ts +++ b/packages/react-native-video/src/core/types/Events.ts @@ -1,5 +1,6 @@ import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro'; import type { TextTrack } from './TextTrack'; +import type { VideoRuntimeError } from './VideoError'; import type { VideoOrientation } from './VideoOrientation'; import type { VideoPlayerStatus } from './VideoPlayerStatus'; @@ -92,6 +93,11 @@ export interface VideoPlayerEvents { onStatusChange: (status: VideoPlayerStatus) => void; } +export interface AllPlayerEvents extends VideoPlayerEvents { + onError: (error: VideoRuntimeError) => void; +} + + export interface VideoViewEvents { /** * Called when the video view's picture in picture state changes.