From ab9d4cbf7e37d1fe0d95c26ac6a38e2bce286c87 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Fri, 11 Jul 2025 14:06:36 +0900 Subject: [PATCH] fix: resolve vimeo player memory leak and unnecessary re-subscriptions - Extract throttleMs from callbackOrThrottle to optimize useEffect dependencies - Add controller.off() cleanup logic when last listener is removed - Prevent memory leaks on component unmount --- .changeset/quiet-bees-happen.md | 9 +++++++ src/VimeoView.tsx | 30 +++++++++++++++++++++- src/hooks/useVimeoEvent.ts | 6 ++--- src/module/VimeoPlayer.ts | 1 + src/module/WebVimeoPlayerController.ts | 6 ++++- src/module/WebviewVimeoPlayerController.ts | 5 ++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 .changeset/quiet-bees-happen.md diff --git a/.changeset/quiet-bees-happen.md b/.changeset/quiet-bees-happen.md new file mode 100644 index 0000000..ce124ba --- /dev/null +++ b/.changeset/quiet-bees-happen.md @@ -0,0 +1,9 @@ +--- +"react-native-vimeo-bridge": patch +--- + +fix: resolve vimeo player memory leak and unnecessary re-subscriptions + +- Extract `throttleMs` from `callbackOrThrottle` to optimize `useEffect` dependencies +- Add `controller.off()` cleanup logic when last listener is removed +- Prevent memory leaks on component unmount diff --git a/src/VimeoView.tsx b/src/VimeoView.tsx index 5ad6903..99e086e 100644 --- a/src/VimeoView.tsx +++ b/src/VimeoView.tsx @@ -173,7 +173,35 @@ function VimeoView({ player, height = 200, width = screenWidth, style, webViewPr getVideoWidth: () => player.getVideoWidth(), getVideoHeight: () => player.getVideoHeight(), getVideoUrl: () => player.getVideoUrl(), - destroy: () => player.destroy(), + destroy: () => { + player.off('play'); + player.off('playing'); + player.off('pause'); + player.off('ended'); + player.off('timeupdate'); + player.off('progress'); + player.off('seeking'); + player.off('seeked'); + player.off('texttrackchange'); + player.off('chapterchange'); + player.off('cuechange'); + player.off('cuepoint'); + player.off('volumechange'); + player.off('playbackratechange'); + player.off('bufferstart'); + player.off('bufferend'); + player.off('error'); + player.off('loaded'); + player.off('durationchange'); + player.off('fullscreenchange'); + player.off('qualitychange'); + player.off('camerachange'); + player.off('resize'); + player.off('enterpictureinpicture'); + player.off('leavepictureinpicture'); + player.destroy(); + }, + off: (event) => player.off(event), } } })(); diff --git a/src/hooks/useVimeoEvent.ts b/src/hooks/useVimeoEvent.ts index 8ade57e..e63b857 100644 --- a/src/hooks/useVimeoEvent.ts +++ b/src/hooks/useVimeoEvent.ts @@ -53,6 +53,7 @@ function useVimeoEvent( deps?: React.DependencyList, ): VimeoPlayerEventMap[T] | null | undefined { const isCallback = typeof callbackOrThrottle === 'function'; + const throttleMs = typeof callbackOrThrottle === 'number' ? callbackOrThrottle : undefined; const callbackRef = useRef | null>(isCallback ? callbackOrThrottle : null); @@ -73,8 +74,7 @@ function useVimeoEvent( } if (!isCallback) { - if (eventType === 'timeupdate' && typeof callbackOrThrottle === 'number') { - const throttleMs = callbackOrThrottle; + if (eventType === 'timeupdate' && throttleMs) { const now = Date.now(); if (now - lastUpdateRef.current < throttleMs) { return; @@ -87,7 +87,7 @@ function useVimeoEvent( }); return unsubscribe; - }, [player, eventType, isCallback, callbackOrThrottle]); + }, [player, eventType, isCallback, throttleMs]); return isCallback ? undefined : data; } diff --git a/src/module/VimeoPlayer.ts b/src/module/VimeoPlayer.ts index 2593dc0..2ccf7b3 100644 --- a/src/module/VimeoPlayer.ts +++ b/src/module/VimeoPlayer.ts @@ -46,6 +46,7 @@ class VimeoPlayer { eventSet?.delete(callback); if (eventSet?.size === 0) { + this.controller?.off(eventType); this.listeners.delete(eventType); } }; diff --git a/src/module/WebVimeoPlayerController.ts b/src/module/WebVimeoPlayerController.ts index d84bd46..3847f41 100644 --- a/src/module/WebVimeoPlayerController.ts +++ b/src/module/WebVimeoPlayerController.ts @@ -1,4 +1,4 @@ -import type { EmbedOptions, VimeoPlayer } from '../types/vimeo'; +import type { EmbedOptions, EventCallback, VimeoPlayer } from '../types/vimeo'; class WebVimeoPlayerController { private player: VimeoPlayer | null = null; @@ -152,6 +152,10 @@ class WebVimeoPlayerController { return this.player?.getVideoUrl() ?? ''; } + off(event: string, callback?: EventCallback): void { + this.player?.off(event, callback); + } + dispose(): void { if (this.player) { try { diff --git a/src/module/WebviewVimeoPlayerController.ts b/src/module/WebviewVimeoPlayerController.ts index 212f6c0..3b91832 100644 --- a/src/module/WebviewVimeoPlayerController.ts +++ b/src/module/WebviewVimeoPlayerController.ts @@ -1,4 +1,5 @@ import type WebView from 'react-native-webview'; +import type { EventCallback } from '../types/vimeo'; class WebviewVimeoPlayerController { private webViewRef: React.RefObject; @@ -96,6 +97,10 @@ class WebviewVimeoPlayerController { await this.executeCommand('destroy'); } + async off(event: string, _callback?: EventCallback): Promise { + await this.executeCommand('off', [event]); + } + private executeCommand( command: string, args: (string | number | boolean | undefined)[] = [],