import React, { useCallback, useMemo, useRef, useEffect } from 'react'; import ReactPlayer from 'react-player'; import handle from '@enact/core/handle'; var handledMediaEventsMap = [ 'onReady', 'onStart', 'onPlay', 'onProgress', 'onDuration', 'onPause', 'onBuffer', 'onBufferEnd', 'onSeek', 'onEnded', 'onError', ]; export default function TReactPlayer({ mediaEventsMap = handledMediaEventsMap, videoRef, url, dispatch, ...rest }) { const playerRef = useRef(null); // 🔽 [최적화] handleEvent의 주요 의존성 추출 const playing = rest?.playing; const config = rest?.config; const handleEvent = useCallback( (type) => (ev) => { if (type === 'onReady') { if (videoRef) { const videoNode = playerRef.current.getInternalPlayer(); videoRef(videoNode); const iframeEl = typeof playerRef.current?.getInternalPlayer === 'function' ? playerRef.current.getInternalPlayer('iframe') : null; if (iframeEl) { iframeEl.setAttribute('tabIndex', '-1'); iframeEl.setAttribute('aria-hidden', 'true'); } if ( videoNode.tagName && !Object.prototype.hasOwnProperty.call(videoNode, 'proportionPlayed') ) { Object.defineProperties(videoNode, { error: { get: function () { return videoNode.networkState === videoNode.NETWORK_NO_SOURCE; }, }, loading: { get: function () { return videoNode.readyState < videoNode.HAVE_ENOUGH_DATA; }, }, proportionLoaded: { get: function () { return ( videoNode.buffered.length && videoNode.buffered.end(videoNode.buffered.length - 1) / videoNode.duration ); }, }, proportionPlayed: { get: function () { return videoNode.currentTime / videoNode.duration; }, }, }); } else if (!Object.prototype.hasOwnProperty.call(videoNode, 'proportionPlayed')) { videoNode.play = videoNode.playVideo; videoNode.pause = videoNode.pauseVideo; videoNode.seek = videoNode.seekTo; Object.defineProperties(videoNode, { currentTime: { get: function () { return videoNode.getCurrentTime(); }, set: function (time) { videoNode.seekTo(time); }, }, duration: { get: function () { return videoNode.getDuration(); }, }, paused: { get: function () { return videoNode.getPlayerState() !== 1; }, }, error: { get: function () { return !!videoNode?.playerInfo?.videoData?.errorCode; }, }, loading: { get: function () { return !videoNode?.playerInfo?.videoData?.isPlayable; //todo }, }, proportionLoaded: { get: function () { return videoNode?.getVideoBytesLoaded() ?? 0; }, }, proportionPlayed: { get: function () { const duration = videoNode.getDuration(); return duration ? videoNode.getCurrentTime() / duration : 0; }, }, playbackRate: { get: function () { return videoNode?.playerInfo?.playbackRate; }, set: function (playbackRate) { if (videoNode && videoNode.setPlaybackRate) { videoNode.setPlaybackRate(playbackRate); } }, }, }); } } handle.forward('onLoadStart', { type, ev }, rest); } if (type === 'onEnded' && rest?.isYoutube && rest?.type === 'VOD') { // YouTube 재생 종료 시 iframe이 포커스를 가져가는 문제를 방지 const iframeEl = typeof playerRef.current?.getInternalPlayer === 'function' ? playerRef.current.getInternalPlayer('iframe') : null; iframeEl?.blur(); } handle.forward('onUpdate', { type, ev }, rest); }, [videoRef, playing, config] // ✅ 주요 의존성 추가 ); const handledMediaEvents = useMemo(() => { const events = {}; for (let i = 0; i < mediaEventsMap.length; i++) { const eventName = mediaEventsMap[i]; events[eventName] = handleEvent(eventName); } return events; }, [handleEvent, mediaEventsMap]); // 메모리 정리는 VideoPlayer.js componentWillUnmount에서 수행 // TReactPlayer는 react-player 래퍼 역할만 수행 return ( ); }