[251113] feat: MediaPanel ref Video Control
🕐 커밋 시간: 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()
This commit is contained in:
@@ -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 (
|
||||
<TPanel
|
||||
isTabActivated={false}
|
||||
{...props}
|
||||
className={appliedClassNames}
|
||||
handleCancel={onClickBack}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<Container
|
||||
spotlightRestrict={containerSpotlightRestrict}
|
||||
spotlightId="spotlightId-media-video-container"
|
||||
// ✅ 이벤트 리스너 정리
|
||||
if (mediaEventListenersRef.current && mediaEventListenersRef.current.length > 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 (
|
||||
<TPanel
|
||||
isTabActivated={false}
|
||||
{...props}
|
||||
className={appliedClassNames}
|
||||
handleCancel={onClickBack}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
{currentPlayingUrl && (
|
||||
<div
|
||||
className={classNames(
|
||||
css.videoPlayerWrapper,
|
||||
shouldDisableIframeInteraction && css.youtubeSafe
|
||||
)}
|
||||
>
|
||||
<MediaPlayerV2
|
||||
setApiProvider={getPlayer}
|
||||
disabled={panelInfo.modal}
|
||||
onEnded={onEnded}
|
||||
autoPlay={!panelInfo?.isPaused}
|
||||
noAutoShowMediaControls={panelInfo.modal} // modal 모드에서는 자동으로 controls가 올라오지 않도록 설정
|
||||
autoCloseTimeout={3000}
|
||||
onBackButton={onClickBack}
|
||||
onClick={onVideoClick}
|
||||
spotlightDisabled={panelInfo.modal}
|
||||
isYoutube={isYoutube}
|
||||
src={currentPlayingUrl}
|
||||
loop={panelInfo.continuousPlay || false}
|
||||
style={panelInfo.modal ? modalStyle : {}}
|
||||
modalScale={panelInfo.modal ? modalScale : 1}
|
||||
modalClassName={panelInfo.modal ? panelInfo.modalClassName : undefined}
|
||||
onError={mediainfoHandler}
|
||||
onTimeUpdate={mediainfoHandler}
|
||||
onLoadedData={mediainfoHandler}
|
||||
onLoadedMetadata={mediainfoHandler}
|
||||
onDurationChange={mediainfoHandler}
|
||||
reactPlayerConfig={reactPlayerSubtitleConfig}
|
||||
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
|
||||
videoComponent={
|
||||
(typeof window === 'object' && !window.PalmSystem) || isYoutube ? TReactPlayer : Media
|
||||
}
|
||||
// VideoOverlay props - 간소화
|
||||
type="MEDIA"
|
||||
panelInfo={panelInfo}
|
||||
captionEnable={false}
|
||||
setIsSubtitleActive={setIsSubtitleActive}
|
||||
setCurrentTime={setCurrentTime}
|
||||
setIsVODPaused={setIsVODPaused}
|
||||
// PlayerOverlayContents props (빈 배열로 전달하여 null 처리)
|
||||
playListInfo={[]}
|
||||
selectedIndex={0}
|
||||
videoVerticalVisible={false}
|
||||
sideContentsVisible={false}
|
||||
setSideContentsVisible={setSideContentsVisible}
|
||||
handleIndicatorDownClick={handleIndicatorDownClick}
|
||||
handleIndicatorUpClick={handleIndicatorUpClick}
|
||||
>
|
||||
{typeof window === 'object' && window.PalmSystem && (
|
||||
<source src={currentPlayingUrl} type={videoType} />
|
||||
<Container
|
||||
spotlightRestrict={containerSpotlightRestrict}
|
||||
spotlightId="spotlightId-media-video-container"
|
||||
>
|
||||
{currentPlayingUrl && (
|
||||
<div
|
||||
className={classNames(
|
||||
css.videoPlayerWrapper,
|
||||
shouldDisableIframeInteraction && css.youtubeSafe
|
||||
)}
|
||||
{isSubtitleActive &&
|
||||
!panelInfo.modal &&
|
||||
typeof window === 'object' &&
|
||||
window.PalmSystem &&
|
||||
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />}
|
||||
</MediaPlayerV2>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</TPanel>
|
||||
);
|
||||
};
|
||||
>
|
||||
<MediaPlayerV2
|
||||
setApiProvider={getPlayer}
|
||||
disabled={panelInfo.modal}
|
||||
onEnded={onEnded}
|
||||
autoPlay={!panelInfo?.isPaused}
|
||||
noAutoShowMediaControls={panelInfo.modal} // modal 모드에서는 자동으로 controls가 올라오지 않도록 설정
|
||||
autoCloseTimeout={3000}
|
||||
onBackButton={onClickBack}
|
||||
onClick={onVideoClick}
|
||||
spotlightDisabled={panelInfo.modal}
|
||||
isYoutube={isYoutube}
|
||||
src={currentPlayingUrl}
|
||||
loop={panelInfo.continuousPlay || false}
|
||||
style={panelInfo.modal ? modalStyle : {}}
|
||||
modalScale={panelInfo.modal ? modalScale : 1}
|
||||
modalClassName={panelInfo.modal ? panelInfo.modalClassName : undefined}
|
||||
onError={mediainfoHandler}
|
||||
onTimeUpdate={mediainfoHandler}
|
||||
onLoadedData={mediainfoHandler}
|
||||
onLoadedMetadata={mediainfoHandler}
|
||||
onDurationChange={mediainfoHandler}
|
||||
reactPlayerConfig={reactPlayerSubtitleConfig}
|
||||
thumbnailUrl={videoLoaded ? '' : videoThumbnailUrl}
|
||||
videoComponent={
|
||||
(typeof window === 'object' && !window.PalmSystem) || isYoutube
|
||||
? TReactPlayer
|
||||
: Media
|
||||
}
|
||||
// VideoOverlay props - 간소화
|
||||
type="MEDIA"
|
||||
panelInfo={panelInfo}
|
||||
captionEnable={false}
|
||||
setIsSubtitleActive={setIsSubtitleActive}
|
||||
setCurrentTime={setCurrentTime}
|
||||
setIsVODPaused={setIsVODPaused}
|
||||
// PlayerOverlayContents props (빈 배열로 전달하여 null 처리)
|
||||
playListInfo={[]}
|
||||
selectedIndex={0}
|
||||
videoVerticalVisible={false}
|
||||
sideContentsVisible={false}
|
||||
setSideContentsVisible={setSideContentsVisible}
|
||||
handleIndicatorDownClick={handleIndicatorDownClick}
|
||||
handleIndicatorUpClick={handleIndicatorUpClick}
|
||||
>
|
||||
{typeof window === 'object' && window.PalmSystem && (
|
||||
<source src={currentPlayingUrl} type={videoType} />
|
||||
)}
|
||||
{isSubtitleActive &&
|
||||
!panelInfo.modal &&
|
||||
typeof window === 'object' &&
|
||||
window.PalmSystem &&
|
||||
currentSubtitleUrl && <track kind="subtitles" src={currentSubtitleUrl} default />}
|
||||
</MediaPlayerV2>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</TPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default React.memo(MediaPanel);
|
||||
|
||||
Reference in New Issue
Block a user