From a33213fb8ce2f35562115fe1e1e67bd7978c27fa Mon Sep 17 00:00:00 2001 From: optrader Date: Thu, 13 Nov 2025 20:51:15 +0900 Subject: [PATCH] [251113] feat: MediaPanel ref Video Control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 11. 13. 20:51:15 ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 1๊ฐœ โ€ข ์ถ”๊ฐ€: +41์ค„ โ€ข ์‚ญ์ œ: -3์ค„ ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ: ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx ๐Ÿ”ง ํ•จ์ˆ˜ ๋ณ€๊ฒฝ ๋‚ด์šฉ: ๐Ÿ“„ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx (javascript): โŒ Deleted: MediaPanel() --- .../src/views/MediaPanel/MediaPanel.jsx | 943 +++++++++--------- 1 file changed, 497 insertions(+), 446 deletions(-) diff --git a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx index 17ce6b55..7935dd60 100644 --- a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx +++ b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx @@ -1,4 +1,11 @@ -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; +import React, { + useCallback, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useRef, +} from 'react'; import classNames from 'classnames'; import { useDispatch } from 'react-redux'; @@ -56,498 +63,542 @@ const YOUTUBECONFIG = { }, }; -const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }) => { - const dispatch = useDispatch(); +const MediaPanel = React.forwardRef( + ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }, ref) => { + const dispatch = useDispatch(); - const videoPlayer = useRef(null); - const onEndedTimerRef = useRef(null); // โœ… onEnded ํƒ€์ด๋จธ ๊ด€๋ฆฌ - const mediaEventListenersRef = useRef([]); // โœ… ๋ฏธ๋””์–ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”์  - const [modalStyle, setModalStyle] = React.useState({}); - const [modalScale, setModalScale] = React.useState(1); - const [currentTime, setCurrentTime] = React.useState(0); - const [videoLoaded, setVideoLoaded] = React.useState(false); - const [isSubtitleActive, setIsSubtitleActive] = React.useState(true); + const videoPlayer = useRef(null); + const onEndedTimerRef = useRef(null); // โœ… onEnded ํƒ€์ด๋จธ ๊ด€๋ฆฌ + const mediaEventListenersRef = useRef([]); // โœ… ๋ฏธ๋””์–ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”์  + const [modalStyle, setModalStyle] = React.useState({}); + const [modalScale, setModalScale] = React.useState(1); + const [currentTime, setCurrentTime] = React.useState(0); + const [videoLoaded, setVideoLoaded] = React.useState(false); + const [isSubtitleActive, setIsSubtitleActive] = React.useState(true); - const panelInfoRef = usePrevious(panelInfo); + const panelInfoRef = usePrevious(panelInfo); - // modal/full screen์— ๋”ฐ๋ฅธ ์ผ์‹œ์ •์ง€/์žฌ์ƒ ์ฒ˜๋ฆฌ - useEffect(() => { - // debugLog('[MediaPanel] ========== isOnTop useEffect =========='); - // debugLog('[MediaPanel] isOnTop:', isOnTop); - // debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2)); + // modal/full screen์— ๋”ฐ๋ฅธ ์ผ์‹œ์ •์ง€/์žฌ์ƒ ์ฒ˜๋ฆฌ + useEffect(() => { + // debugLog('[MediaPanel] ========== isOnTop useEffect =========='); + // debugLog('[MediaPanel] isOnTop:', isOnTop); + // debugLog('[MediaPanel] panelInfo:', JSON.stringify(panelInfo, null, 2)); - if (panelInfo && panelInfo.modal) { - if (!isOnTop) { - // debugLog('[MediaPanel] Not on top - pausing video'); - dispatch(pauseModalMedia()); - } else if (isOnTop && panelInfo.isPaused) { - // debugLog('[MediaPanel] Back on top - resuming video'); - dispatch(resumeModalMedia()); - } - } - }, [isOnTop, panelInfo, dispatch]); - - // videoPlayer ref๋ฅผ ํ†ตํ•œ ์ง์ ‘ ์ œ์–ด - useEffect(() => { - if (panelInfo?.modal && videoPlayer.current) { - if (panelInfo.isPaused) { - // debugLog('[MediaPanel] Executing pause via videoPlayer.current'); - videoPlayer.current.pause(); - } else if (panelInfo.isPaused === false) { - // debugLog('[MediaPanel] Executing play via videoPlayer.current'); - videoPlayer.current.play(); - } - } - }, [panelInfo?.isPaused, panelInfo?.modal]); - - useEffect(() => { - if (!videoPlayer.current) return; - if (!isYoutube) return; - if (panelInfo?.modal) return; - - videoPlayer.current.showControls?.(); - }, [isYoutube, panelInfo?.modal]); - - const getPlayer = useCallback((ref) => { - videoPlayer.current = ref; - }, []); - - // โœ… ์•ˆ์ „ํ•œ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ - const safePlayerCall = useCallback((methodName, ...args) => { - if (videoPlayer.current && typeof videoPlayer.current[methodName] === 'function') { - try { - return videoPlayer.current[methodName](...args); - } catch (err) { - if (DEBUG_MODE) console.warn(`[MediaPanel] ${methodName} ํ˜ธ์ถœ ์‹คํŒจ:`, err); - } - } - return null; - }, []); - - // VideoPlayer๊ฐ€ MEDIA ํƒ€์ž…์—์„œ setIsVODPaused๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ๋”๋ฏธ ํ•จ์ˆ˜ ์ œ๊ณต - const setIsVODPaused = useCallback(() => { - // MediaPanel์—์„œ๋Š” VOD pause ์ƒํƒœ ๊ด€๋ฆฌ ๋ถˆํ•„์š” (๋‹จ์ˆœ ์žฌ์ƒ๋งŒ) - }, []); - - // PlayerOverlayContents์—์„œ ํ•„์š”ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜๋“ค - const setSideContentsVisible = useCallback(() => { - // MediaPanel์—์„œ๋Š” ์‚ฌ์ด๋“œ ์ปจํ…์ธ  ์‚ฌ์šฉ ์•ˆ ํ•จ - }, []); - - const handleIndicatorDownClick = useCallback(() => { - // MediaPanel์—์„œ๋Š” indicator ์‚ฌ์šฉ ์•ˆ ํ•จ - }, []); - - const handleIndicatorUpClick = useCallback(() => { - // MediaPanel์—์„œ๋Š” indicator ์‚ฌ์šฉ ์•ˆ ํ•จ - }, []); - - // โœ… modal ์Šคํƒ€์ผ ์„ค์ • - useEffect(() => { - let resizeObserver = null; - - if (panelInfo.modal && panelInfo.modalContainerId) { - // modal ๋ชจ๋“œ: modalContainerId ๊ธฐ๋ฐ˜์œผ๋กœ ์œ„์น˜์™€ ํฌ๊ธฐ ๊ณ„์‚ฐ - const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`); - if (node) { - const { width, height, top, left } = node.getBoundingClientRect(); - - // ProductVideo์˜ padding(6px * 2)๊ณผ ์ถ”๊ฐ€ ์—ฌ์œ ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํฌ๊ธฐ ์กฐ์ • - // ๋น„๋””์˜ค๊ฐ€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋„˜์น˜์ง€ ์•Š๋„๋ก ์ถฉ๋ถ„ํ•œ ์—ฌ์œ  ํ™•๋ณด - const paddingOffset = 6 * 2; // padding ์–‘์ชฝ - const extraMargin = 6 * 2; // ์ถ”๊ฐ€ ์—ฌ์œ  (ํฌ์ปค์Šค ํ…Œ๋‘๋ฆฌ + ๋น„๋””์˜ค ๋น„์œจ ๊ณ ๋ ค) - const totalOffset = paddingOffset + extraMargin; // 24px - - const adjustedWidth = width - totalOffset; - const adjustedHeight = height - totalOffset; - const adjustedTop = top + totalOffset / 2; - const adjustedLeft = left + totalOffset / 2; - - const style = { - width: adjustedWidth + 'px', - height: adjustedHeight + 'px', - maxWidth: adjustedWidth + 'px', - maxHeight: adjustedHeight + 'px', - top: adjustedTop + 'px', - left: adjustedLeft + 'px', - position: 'fixed', - overflow: 'hidden', // visible โ†’ hidden์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋„˜์น˜๋Š” ๋ถ€๋ถ„ ์ˆจ๊น€ - }; - setModalStyle(style); - let scale = 1; - if (typeof window === 'object') { - scale = adjustedWidth / window.innerWidth; - setModalScale(scale); + if (panelInfo && panelInfo.modal) { + if (!isOnTop) { + // debugLog('[MediaPanel] Not on top - pausing video'); + dispatch(pauseModalMedia()); + } else if (isOnTop && panelInfo.isPaused) { + // debugLog('[MediaPanel] Back on top - resuming video'); + dispatch(resumeModalMedia()); } - } else { - setModalStyle(panelInfo.modalStyle || {}); - setModalScale(panelInfo.modalScale || 1); } - } else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) { - // โœ… ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ๋กœ null/undefined ์ฒดํฌ - const mediaState = safePlayerCall('getMediaState'); - if (mediaState?.paused) { - safePlayerCall('play'); + }, [isOnTop, panelInfo, dispatch]); + + // videoPlayer ref๋ฅผ ํ†ตํ•œ ์ง์ ‘ ์ œ์–ด + useEffect(() => { + if (panelInfo?.modal && videoPlayer.current) { + if (panelInfo.isPaused) { + // debugLog('[MediaPanel] Executing pause via videoPlayer.current'); + videoPlayer.current.pause(); + } else if (panelInfo.isPaused === false) { + // debugLog('[MediaPanel] Executing play via videoPlayer.current'); + videoPlayer.current.play(); + } } + }, [panelInfo?.isPaused, panelInfo?.modal]); - const isControlsHidden = - videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible(); - if (isControlsHidden) { - safePlayerCall('showControls'); + useEffect(() => { + if (!videoPlayer.current) return; + if (!isYoutube) return; + if (panelInfo?.modal) return; + + videoPlayer.current.showControls?.(); + }, [isYoutube, panelInfo?.modal]); + + const getPlayer = useCallback((ref) => { + videoPlayer.current = ref; + }, []); + + // โœ… ์•ˆ์ „ํ•œ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ + const safePlayerCall = useCallback((methodName, ...args) => { + if (videoPlayer.current && typeof videoPlayer.current[methodName] === 'function') { + try { + return videoPlayer.current[methodName](...args); + } catch (err) { + if (DEBUG_MODE) console.warn(`[MediaPanel] ${methodName} ํ˜ธ์ถœ ์‹คํŒจ:`, err); + } } - } + return null; + }, []); - // โœ… cleanup: resize observer ์ •๋ฆฌ - return () => { - if (resizeObserver) { - resizeObserver.disconnect(); - } - }; - }, [panelInfo, isOnTop]); + const playViaRef = useCallback(() => safePlayerCall('play'), [safePlayerCall]); + const pauseViaRef = useCallback(() => safePlayerCall('pause'), [safePlayerCall]); + const seekViaRef = useCallback( + (seconds = 0) => safePlayerCall('seek', seconds), + [safePlayerCall] + ); + const showControlsViaRef = useCallback(() => safePlayerCall('showControls'), [safePlayerCall]); + const hideControlsViaRef = useCallback(() => safePlayerCall('hideControls'), [safePlayerCall]); + const toggleControlsViaRef = useCallback( + () => safePlayerCall('toggleControls'), + [safePlayerCall] + ); + const getMediaStateViaRef = useCallback( + () => safePlayerCall('getMediaState'), + [safePlayerCall] + ); - // โœ… ๋น„๋””์˜ค ํด๋ฆญ ์‹œ modal โ†’ fullscreen ์ „ํ™˜ ๋˜๋Š” controls ํ† ๊ธ€ - const onVideoClick = useCallback(() => { - if (panelInfo.modal) { - // debugLog('[MediaPanel] Video clicked - switching to fullscreen'); - dispatch(switchMediaToFullscreen()); - } else { - // ๋น„๋””์˜ค ํด๋ฆญ ์‹œ controls ํ† ๊ธ€ - safePlayerCall('toggleControls'); - } - }, [dispatch, panelInfo.modal, safePlayerCall]); + useImperativeHandle( + ref, + () => ({ + play: playViaRef, + pause: pauseViaRef, + seek: seekViaRef, + showControls: showControlsViaRef, + hideControls: hideControlsViaRef, + toggleControls: toggleControlsViaRef, + getMediaState: getMediaStateViaRef, + getInternalPlayer: () => videoPlayer.current, + }), + [ + playViaRef, + pauseViaRef, + seekViaRef, + showControlsViaRef, + hideControlsViaRef, + toggleControlsViaRef, + getMediaStateViaRef, + ] + ); - const onClickBack = useCallback( - (ev) => { - // โœ… ๋’ค๋กœ๊ฐ€๊ธฐ ์‹œ ๋น„๋””์˜ค ์ •์ง€ - safePlayerCall('pause'); + // VideoPlayer๊ฐ€ MEDIA ํƒ€์ž…์—์„œ setIsVODPaused๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ๋”๋ฏธ ํ•จ์ˆ˜ ์ œ๊ณต + const setIsVODPaused = useCallback(() => { + // MediaPanel์—์„œ๋Š” VOD pause ์ƒํƒœ ๊ด€๋ฆฌ ๋ถˆํ•„์š” (๋‹จ์ˆœ ์žฌ์ƒ๋งŒ) + }, []); - // modal์—์„œ full๋กœ ์ „ํ™˜๋œ ๊ฒฝ์šฐ ๋‹ค์‹œ modal๋กœ ๋Œ์•„๊ฐ - if (panelInfo.modalContainerId && !panelInfo.modal) { - // ๋‹ค์‹œ modal๋กœ ๋Œ๋ฆฌ๋Š” ๋กœ์ง์€ startVideoPlayer ์•ก์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ - // ์—ฌ๊ธฐ์„œ๋Š” ๋‹จ์ˆœํžˆ ํŒจ๋„์„ pop - dispatch(PanelActions.popPanel()); - ev?.stopPropagation(); - return; - } + // PlayerOverlayContents์—์„œ ํ•„์š”ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜๋“ค + const setSideContentsVisible = useCallback(() => { + // MediaPanel์—์„œ๋Š” ์‚ฌ์ด๋“œ ์ปจํ…์ธ  ์‚ฌ์šฉ ์•ˆ ํ•จ + }, []); - if (!panelInfo.modal) { - dispatch(PanelActions.popPanel()); - ev?.stopPropagation(); - return; - } - }, - [dispatch, panelInfo, safePlayerCall] - ); + const handleIndicatorDownClick = useCallback(() => { + // MediaPanel์—์„œ๋Š” indicator ์‚ฌ์šฉ ์•ˆ ํ•จ + }, []); - const currentPlayingUrl = useMemo(() => { - return panelInfo?.showUrl; - }, [panelInfo?.showUrl]); + const handleIndicatorUpClick = useCallback(() => { + // MediaPanel์—์„œ๋Š” indicator ์‚ฌ์šฉ ์•ˆ ํ•จ + }, []); - const isYoutube = useMemo(() => { - if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) { - return true; - } - return false; - }, [currentPlayingUrl]); + // โœ… modal ์Šคํƒ€์ผ ์„ค์ • + useEffect(() => { + let resizeObserver = null; - const currentSubtitleUrl = useMemo(() => { - return panelInfo?.subtitle; - }, [panelInfo?.subtitle]); + if (panelInfo.modal && panelInfo.modalContainerId) { + // modal ๋ชจ๋“œ: modalContainerId ๊ธฐ๋ฐ˜์œผ๋กœ ์œ„์น˜์™€ ํฌ๊ธฐ ๊ณ„์‚ฐ + const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`); + if (node) { + const { width, height, top, left } = node.getBoundingClientRect(); - const reactPlayerSubtitleConfig = useMemo(() => { - if (isSubtitleActive && currentSubtitleUrl) { - return { - file: { - attributes: { - crossOrigin: 'true', - }, - tracks: [{ kind: 'subtitles', src: currentSubtitleUrl, default: true }], - }, - youtube: YOUTUBECONFIG, - }; - } else { - return { - youtube: YOUTUBECONFIG, - }; - } - }, [currentSubtitleUrl, isSubtitleActive]); + // ProductVideo์˜ padding(6px * 2)๊ณผ ์ถ”๊ฐ€ ์—ฌ์œ ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํฌ๊ธฐ ์กฐ์ • + // ๋น„๋””์˜ค๊ฐ€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋„˜์น˜์ง€ ์•Š๋„๋ก ์ถฉ๋ถ„ํ•œ ์—ฌ์œ  ํ™•๋ณด + const paddingOffset = 6 * 2; // padding ์–‘์ชฝ + const extraMargin = 6 * 2; // ์ถ”๊ฐ€ ์—ฌ์œ  (ํฌ์ปค์Šค ํ…Œ๋‘๋ฆฌ + ๋น„๋””์˜ค ๋น„์œจ ๊ณ ๋ ค) + const totalOffset = paddingOffset + extraMargin; // 24px - const videoType = useMemo(() => { - if (currentPlayingUrl) { - if (currentPlayingUrl.toLowerCase().endsWith('.mp4')) { - return 'video/mp4'; - } else if (currentPlayingUrl.toLowerCase().endsWith('.mpd')) { - return 'application/dash+xml'; - } else if (currentPlayingUrl.toLowerCase().endsWith('.m3u8')) { - return 'application/mpegurl'; - } - } - return 'application/mpegurl'; - }, [currentPlayingUrl]); + const adjustedWidth = width - totalOffset; + const adjustedHeight = height - totalOffset; + const adjustedTop = top + totalOffset / 2; + const adjustedLeft = left + totalOffset / 2; - const videoThumbnailUrl = useMemo(() => { - return panelInfo?.thumbnailUrl; - }, [panelInfo?.thumbnailUrl]); - - const mediainfoHandler = useCallback( - (ev) => { - const type = ev.type; - if (type !== 'timeupdate' && type !== 'durationchange') { - debugLog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState()); - } - - // โœ… hlsError ์ฒ˜๋ฆฌ ๊ฐ•ํ™” - if (ev === 'hlsError') { + const style = { + width: adjustedWidth + 'px', + height: adjustedHeight + 'px', + maxWidth: adjustedWidth + 'px', + maxHeight: adjustedHeight + 'px', + top: adjustedTop + 'px', + left: adjustedLeft + 'px', + position: 'fixed', + overflow: 'hidden', // visible โ†’ hidden์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋„˜์น˜๋Š” ๋ถ€๋ถ„ ์ˆจ๊น€ + }; + setModalStyle(style); + let scale = 1; + if (typeof window === 'object') { + scale = adjustedWidth / window.innerWidth; + setModalScale(scale); + } + } else { + setModalStyle(panelInfo.modalStyle || {}); + setModalScale(panelInfo.modalScale || 1); + } + } else if (isOnTop && !panelInfo.modal && !panelInfo.isMinimized && videoPlayer.current) { + // โœ… ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ๋กœ null/undefined ์ฒดํฌ const mediaState = safePlayerCall('getMediaState'); - if (mediaState && isNaN(Number(mediaState.playbackRate))) { - dispatch( - sendBroadCast({ - type: 'videoError', - moreInfo: { reason: 'hlsError' }, - }) - ); + if (mediaState?.paused) { + safePlayerCall('play'); + } + + const isControlsHidden = + videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible(); + if (isControlsHidden) { + safePlayerCall('showControls'); + } + } + + // โœ… cleanup: resize observer ์ •๋ฆฌ + return () => { + if (resizeObserver) { + resizeObserver.disconnect(); + } + }; + }, [panelInfo, isOnTop]); + + // โœ… ๋น„๋””์˜ค ํด๋ฆญ ์‹œ modal โ†’ fullscreen ์ „ํ™˜ ๋˜๋Š” controls ํ† ๊ธ€ + const onVideoClick = useCallback(() => { + if (panelInfo.modal) { + // debugLog('[MediaPanel] Video clicked - switching to fullscreen'); + dispatch(switchMediaToFullscreen()); + } else { + // ๋น„๋””์˜ค ํด๋ฆญ ์‹œ controls ํ† ๊ธ€ + safePlayerCall('toggleControls'); + } + }, [dispatch, panelInfo.modal, safePlayerCall]); + + const onClickBack = useCallback( + (ev) => { + // โœ… ๋’ค๋กœ๊ฐ€๊ธฐ ์‹œ ๋น„๋””์˜ค ์ •์ง€ + safePlayerCall('pause'); + + // modal์—์„œ full๋กœ ์ „ํ™˜๋œ ๊ฒฝ์šฐ ๋‹ค์‹œ modal๋กœ ๋Œ์•„๊ฐ + if (panelInfo.modalContainerId && !panelInfo.modal) { + // ๋‹ค์‹œ modal๋กœ ๋Œ๋ฆฌ๋Š” ๋กœ์ง์€ startVideoPlayer ์•ก์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ + // ์—ฌ๊ธฐ์„œ๋Š” ๋‹จ์ˆœํžˆ ํŒจ๋„์„ pop + dispatch(PanelActions.popPanel()); + ev?.stopPropagation(); return; } - } - switch (type) { - case 'timeupdate': { + if (!panelInfo.modal) { + dispatch(PanelActions.popPanel()); + ev?.stopPropagation(); + return; + } + }, + [dispatch, panelInfo, safePlayerCall] + ); + + const currentPlayingUrl = useMemo(() => { + return panelInfo?.showUrl; + }, [panelInfo?.showUrl]); + + const isYoutube = useMemo(() => { + if (currentPlayingUrl && currentPlayingUrl.includes('youtu')) { + return true; + } + return false; + }, [currentPlayingUrl]); + + const currentSubtitleUrl = useMemo(() => { + return panelInfo?.subtitle; + }, [panelInfo?.subtitle]); + + const reactPlayerSubtitleConfig = useMemo(() => { + if (isSubtitleActive && currentSubtitleUrl) { + return { + file: { + attributes: { + crossOrigin: 'true', + }, + tracks: [{ kind: 'subtitles', src: currentSubtitleUrl, default: true }], + }, + youtube: YOUTUBECONFIG, + }; + } else { + return { + youtube: YOUTUBECONFIG, + }; + } + }, [currentSubtitleUrl, isSubtitleActive]); + + const videoType = useMemo(() => { + if (currentPlayingUrl) { + if (currentPlayingUrl.toLowerCase().endsWith('.mp4')) { + return 'video/mp4'; + } else if (currentPlayingUrl.toLowerCase().endsWith('.mpd')) { + return 'application/dash+xml'; + } else if (currentPlayingUrl.toLowerCase().endsWith('.m3u8')) { + return 'application/mpegurl'; + } + } + return 'application/mpegurl'; + }, [currentPlayingUrl]); + + const videoThumbnailUrl = useMemo(() => { + return panelInfo?.thumbnailUrl; + }, [panelInfo?.thumbnailUrl]); + + const mediainfoHandler = useCallback( + (ev) => { + const type = ev.type; + if (type !== 'timeupdate' && type !== 'durationchange') { + debugLog('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState()); + } + + // โœ… hlsError ์ฒ˜๋ฆฌ ๊ฐ•ํ™” + if (ev === 'hlsError') { const mediaState = safePlayerCall('getMediaState'); - if (mediaState) { - setCurrentTime(mediaState.currentTime || 0); + if (mediaState && isNaN(Number(mediaState.playbackRate))) { + dispatch( + sendBroadCast({ + type: 'videoError', + moreInfo: { reason: 'hlsError' }, + }) + ); + return; } - break; } - case 'error': { - const mediaState = safePlayerCall('getMediaState'); - const errorInfo = mediaState?.error || 'unknown'; - dispatch( - sendBroadCast({ - type: 'videoError', - moreInfo: { reason: errorInfo }, - }) - ); - break; + + switch (type) { + case 'timeupdate': { + const mediaState = safePlayerCall('getMediaState'); + if (mediaState) { + setCurrentTime(mediaState.currentTime || 0); + } + break; + } + case 'error': { + const mediaState = safePlayerCall('getMediaState'); + const errorInfo = mediaState?.error || 'unknown'; + dispatch( + sendBroadCast({ + type: 'videoError', + moreInfo: { reason: errorInfo }, + }) + ); + break; + } + case 'loadeddata': { + setVideoLoaded(true); + break; + } + default: + break; } - case 'loadeddata': { - setVideoLoaded(true); - break; + }, + [dispatch, safePlayerCall] + ); + + const onEnded = useCallback( + (e) => { + debugLog('[MediaPanel] Video ended'); + // continuousPlay๋Š” MediaPlayer(VideoPlayer) ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ loop ์†์„ฑ์œผ๋กœ ์ฒ˜๋ฆฌ + // onEnded๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด loop=false ์ธ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ ํŒจ๋„์„ ๋‹ซ์Œ + Spotlight.pause(); + + // โœ… ์ด์ „ ํƒ€์ด๋จธ๊ฐ€ ์žˆ์œผ๋ฉด ์ •๋ฆฌ + if (onEndedTimerRef.current) { + clearTimeout(onEndedTimerRef.current); } - default: - break; - } - }, - [dispatch, safePlayerCall] - ); - const onEnded = useCallback( - (e) => { - debugLog('[MediaPanel] Video ended'); - // continuousPlay๋Š” MediaPlayer(VideoPlayer) ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ loop ์†์„ฑ์œผ๋กœ ์ฒ˜๋ฆฌ - // onEnded๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด loop=false ์ธ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ ํŒจ๋„์„ ๋‹ซ์Œ - Spotlight.pause(); + // โœ… ์ƒˆ๋กœ์šด ํƒ€์ด๋จธ ์ €์žฅ (cleanup ์‹œ ์ •๋ฆฌ์šฉ) + onEndedTimerRef.current = setTimeout(() => { + Spotlight.resume(); + dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); + onEndedTimerRef.current = null; + }, 1500); - // โœ… ์ด์ „ ํƒ€์ด๋จธ๊ฐ€ ์žˆ์œผ๋ฉด ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - } + e?.stopPropagation(); + e?.preventDefault(); + }, + [dispatch] + ); - // โœ… ์ƒˆ๋กœ์šด ํƒ€์ด๋จธ ์ €์žฅ (cleanup ์‹œ ์ •๋ฆฌ์šฉ) - onEndedTimerRef.current = setTimeout(() => { - Spotlight.resume(); - dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); - onEndedTimerRef.current = null; - }, 1500); - - e?.stopPropagation(); - e?.preventDefault(); - }, - [dispatch] - ); - - // โœ… useLayoutEffect: DOM ์Šคํƒ€์ผ ์„ค์ • (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) - useLayoutEffect(() => { - const videoContainer = document.querySelector(`.${css.videoContainer}`); - if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) { - try { - videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`; - videoContainer.style.backgroundColor = 'black'; - } catch (err) { - if (DEBUG_MODE) console.warn('[MediaPanel] ์ธ๋„ค์ผ ์Šคํƒ€์ผ ์„ค์ • ์‹คํŒจ:', err); - } - } - - // โœ… cleanup: ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ DOM ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” - return () => { - if (videoContainer) { + // โœ… useLayoutEffect: DOM ์Šคํƒ€์ผ ์„ค์ • (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + useLayoutEffect(() => { + const videoContainer = document.querySelector(`.${css.videoContainer}`); + if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) { try { - videoContainer.style.background = ''; - videoContainer.style.backgroundColor = ''; + videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`; + videoContainer.style.backgroundColor = 'black'; } catch (err) { - // ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ + if (DEBUG_MODE) console.warn('[MediaPanel] ์ธ๋„ค์ผ ์Šคํƒ€์ผ ์„ค์ • ์‹คํŒจ:', err); } } - }; - }, [panelInfo.thumbnailUrl, videoLoaded]); - useEffect(() => { - setVideoLoaded(false); - }, [currentPlayingUrl]); - - // โœ… ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ด๋จธ ๋ฐ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ - useEffect(() => { - return () => { - // โœ… onEnded ํƒ€์ด๋จธ ์ •๋ฆฌ - if (onEndedTimerRef.current) { - clearTimeout(onEndedTimerRef.current); - onEndedTimerRef.current = null; - } - - // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ ๋ฐ ์ •๋ฆฌ - if (videoPlayer.current) { - try { - // ์žฌ์ƒ ์ค‘์ด๋ฉด ์ •์ง€ - safePlayerCall('pause'); - - // controls ํƒ€์ž„์•„์›ƒ ์ •๋ฆฌ - safePlayerCall('hideControls'); - } catch (err) { - if (DEBUG_MODE) console.warn('[MediaPanel] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', err); - } - - // ref ์ดˆ๊ธฐํ™” - videoPlayer.current = null; - } - - // โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ - if (mediaEventListenersRef.current && mediaEventListenersRef.current.length > 0) { - mediaEventListenersRef.current.forEach(({ element, event, handler }) => { + // โœ… cleanup: ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ DOM ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” + return () => { + if (videoContainer) { try { - element?.removeEventListener?.(event, handler); + videoContainer.style.background = ''; + videoContainer.style.backgroundColor = ''; } catch (err) { - // ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ์‹คํŒจ๋Š” ๋ฌด์‹œ + // ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ } - }); - mediaEventListenersRef.current = []; - } + } + }; + }, [panelInfo.thumbnailUrl, videoLoaded]); - // โœ… Spotlight ์ƒํƒœ ์ดˆ๊ธฐํ™” - try { - Spotlight.resume?.(); - } catch (err) { - // Spotlight ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ - } - }; - }, [dispatch, safePlayerCall]); + useEffect(() => { + setVideoLoaded(false); + }, [currentPlayingUrl]); - // debugLog('[MediaPanel] ========== Rendering =========='); - // debugLog('[MediaPanel] isOnTop:', isOnTop); - // debugLog('[MediaPanel] panelInfo.modal:', panelInfo.modal); - // debugLog('[MediaPanel] panelInfo.isMinimized:', panelInfo.isMinimized); - // debugLog('[MediaPanel] panelInfo.isPaused:', panelInfo.isPaused); - // debugLog('[MediaPanel] currentPlayingUrl:', currentPlayingUrl); - // debugLog('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current); + // โœ… ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ด๋จธ ๋ฐ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + useEffect(() => { + return () => { + // โœ… onEnded ํƒ€์ด๋จธ ์ •๋ฆฌ + if (onEndedTimerRef.current) { + clearTimeout(onEndedTimerRef.current); + onEndedTimerRef.current = null; + } - // classNames ์ ์šฉ ์ƒํƒœ ํ™•์ธ - // debugLog('[MediaPanel] ========== ClassNames Analysis =========='); - // debugLog('[MediaPanel] css.videoContainer:', css.videoContainer); - // debugLog('[MediaPanel] Condition [panelInfo.modal && !panelInfo.isMinimized]:', panelInfo.modal && !panelInfo.isMinimized); - // debugLog('[MediaPanel] css.modal:', css.modal); - // debugLog('[MediaPanel] Condition [panelInfo.isMinimized]:', panelInfo.isMinimized); - // debugLog('[MediaPanel] css["modal-minimized"]:', css['modal-minimized']); - // debugLog('[MediaPanel] Condition [!isOnTop]:', !isOnTop); - // debugLog('[MediaPanel] css.background:', css.background); + // โœ… ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ •์ง€ ๋ฐ ์ •๋ฆฌ + if (videoPlayer.current) { + try { + // ์žฌ์ƒ ์ค‘์ด๋ฉด ์ •์ง€ + safePlayerCall('pause'); - const appliedClassNames = classNames( - css.videoContainer, - panelInfo.modal && !panelInfo.isMinimized && css.modal, - panelInfo.isMinimized && css['modal-minimized'], - !isOnTop && css.background - ); - // debugLog('[MediaPanel] Final Applied ClassNames:', appliedClassNames); - // debugLog('[MediaPanel] modalStyle:', modalStyle); - // debugLog('[MediaPanel] modalScale:', modalScale); - // debugLog('[MediaPanel] ==============================================='); + // controls ํƒ€์ž„์•„์›ƒ ์ •๋ฆฌ + safePlayerCall('hideControls'); + } catch (err) { + if (DEBUG_MODE) console.warn('[MediaPanel] ๋น„๋””์˜ค ์ •์ง€ ์‹คํŒจ:', err); + } - // minimized ์ƒํƒœ์ผ ๋•Œ๋Š” spotlightRestrict ํ•ด์ œ (ํฌ์ปค์Šค ์ด๋™ ํ—ˆ์šฉ) - const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only'; - const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal; + // ref ์ดˆ๊ธฐํ™” + videoPlayer.current = null; + } - return ( - - 0) { + mediaEventListenersRef.current.forEach(({ element, event, handler }) => { + try { + element?.removeEventListener?.(event, handler); + } catch (err) { + // ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ์‹คํŒจ๋Š” ๋ฌด์‹œ + } + }); + mediaEventListenersRef.current = []; + } + + // โœ… Spotlight ์ƒํƒœ ์ดˆ๊ธฐํ™” + try { + Spotlight.resume?.(); + } catch (err) { + // Spotlight ์ดˆ๊ธฐํ™” ์‹คํŒจ๋Š” ๋ฌด์‹œ + } + }; + }, [dispatch, safePlayerCall]); + + // debugLog('[MediaPanel] ========== Rendering =========='); + // debugLog('[MediaPanel] isOnTop:', isOnTop); + // debugLog('[MediaPanel] panelInfo.modal:', panelInfo.modal); + // debugLog('[MediaPanel] panelInfo.isMinimized:', panelInfo.isMinimized); + // debugLog('[MediaPanel] panelInfo.isPaused:', panelInfo.isPaused); + // debugLog('[MediaPanel] currentPlayingUrl:', currentPlayingUrl); + // debugLog('[MediaPanel] hasVideoPlayer:', !!videoPlayer.current); + + // classNames ์ ์šฉ ์ƒํƒœ ํ™•์ธ + // debugLog('[MediaPanel] ========== ClassNames Analysis =========='); + // debugLog('[MediaPanel] css.videoContainer:', css.videoContainer); + // debugLog('[MediaPanel] Condition [panelInfo.modal && !panelInfo.isMinimized]:', panelInfo.modal && !panelInfo.isMinimized); + // debugLog('[MediaPanel] css.modal:', css.modal); + // debugLog('[MediaPanel] Condition [panelInfo.isMinimized]:', panelInfo.isMinimized); + // debugLog('[MediaPanel] css["modal-minimized"]:', css['modal-minimized']); + // debugLog('[MediaPanel] Condition [!isOnTop]:', !isOnTop); + // debugLog('[MediaPanel] css.background:', css.background); + + const appliedClassNames = classNames( + css.videoContainer, + panelInfo.modal && !panelInfo.isMinimized && css.modal, + panelInfo.isMinimized && css['modal-minimized'], + !isOnTop && css.background + ); + // debugLog('[MediaPanel] Final Applied ClassNames:', appliedClassNames); + // debugLog('[MediaPanel] modalStyle:', modalStyle); + // debugLog('[MediaPanel] modalScale:', modalScale); + // debugLog('[MediaPanel] ==============================================='); + + // minimized ์ƒํƒœ์ผ ๋•Œ๋Š” spotlightRestrict ํ•ด์ œ (ํฌ์ปค์Šค ์ด๋™ ํ—ˆ์šฉ) + const containerSpotlightRestrict = panelInfo.isMinimized ? 'none' : 'self-only'; + const shouldDisableIframeInteraction = isYoutube && !panelInfo.modal; + + return ( + - {currentPlayingUrl && ( -
- - {typeof window === 'object' && window.PalmSystem && ( - + + {currentPlayingUrl && ( +
} - -
- )} -
- - ); -}; + > + + {typeof window === 'object' && window.PalmSystem && ( + + )} + {isSubtitleActive && + !panelInfo.modal && + typeof window === 'object' && + window.PalmSystem && + currentSubtitleUrl && } + +
+ )} +
+
+ ); + } +); export default React.memo(MediaPanel);