[251011] feat: MediaPlayer 추가
🕐 커밋 시간: 2025. 10. 11. 09:07:17 📊 변경 통계: • 총 파일: 7개 • 추가: +416줄 • 삭제: -518줄 📁 추가된 파일: + com.twin.app.shoptime/src/actions/mediaActions.js + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less + com.twin.app.shoptime/src/views/MediaPanel/README.md 📝 수정된 파일: ~ com.twin.app.shoptime/src/reducers/panelReducer.js ~ com.twin.app.shoptime/src/utils/Config.js ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/MainView/MainView.jsx (javascript): ✅ Added: resetWatchRecord() ❌ Deleted: resetWatchRecord() 📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx (javascript): ✅ Added: MediaPanel() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • 개발 문서 및 가이드 개선 Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
321
com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
Normal file
321
com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
|
||||
import { sendBroadCast } from '../../actions/commonActions';
|
||||
import { pauseModalMedia, resumeModalMedia } from '../../actions/mediaActions';
|
||||
import * as PanelActions from '../../actions/panelActions';
|
||||
import TPanel from '../../components/TPanel/TPanel';
|
||||
import Media from '../../components/VideoPlayer/Media';
|
||||
import TReactPlayer from '../../components/VideoPlayer/TReactPlayer';
|
||||
import { VideoPlayer } from '../../components/VideoPlayer/VideoPlayer';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { panel_names } from '../../utils/Config';
|
||||
import css from './MediaPanel.module.less';
|
||||
|
||||
const Container = SpotlightContainerDecorator(
|
||||
{ enterTo: 'default-element', preserveld: true },
|
||||
'div'
|
||||
);
|
||||
|
||||
const YOUTUBECONFIG = {
|
||||
playerVars: {
|
||||
controls: 0,
|
||||
autoplay: 1,
|
||||
disablekb: 1,
|
||||
enablejsapi: 1,
|
||||
listType: 'user_uploads',
|
||||
fs: 0,
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
loop: 0,
|
||||
iv_load_policy: 3,
|
||||
modestbranding: 1,
|
||||
wmode: 'opaque',
|
||||
cc_lang_pref: 'en',
|
||||
cc_load_policy: 0,
|
||||
playsinline: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const videoPlayer = useRef(null);
|
||||
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);
|
||||
|
||||
// modal/full screen에 따른 일시정지/재생 처리
|
||||
useEffect(() => {
|
||||
console.log('[MediaPanel] isOnTop:', {
|
||||
isOnTop,
|
||||
panelInfo,
|
||||
});
|
||||
|
||||
if (panelInfo && panelInfo.modal) {
|
||||
if (!isOnTop) {
|
||||
console.log('[MediaPanel] Not on top - pausing video');
|
||||
dispatch(pauseModalMedia());
|
||||
} else if (isOnTop && panelInfo.isPaused) {
|
||||
console.log('[MediaPanel] Back on top - resuming video');
|
||||
dispatch(resumeModalMedia());
|
||||
}
|
||||
}
|
||||
}, [isOnTop, panelInfo, dispatch]);
|
||||
|
||||
// videoPlayer ref를 통한 직접 제어
|
||||
useEffect(() => {
|
||||
if (panelInfo?.modal && videoPlayer.current) {
|
||||
if (panelInfo.isPaused) {
|
||||
console.log('[MediaPanel] Executing pause via videoPlayer.current');
|
||||
videoPlayer.current.pause();
|
||||
} else if (panelInfo.isPaused === false) {
|
||||
console.log('[MediaPanel] Executing play via videoPlayer.current');
|
||||
videoPlayer.current.play();
|
||||
}
|
||||
}
|
||||
}, [panelInfo?.isPaused, panelInfo?.modal]);
|
||||
|
||||
const getPlayer = useCallback((ref) => {
|
||||
videoPlayer.current = ref;
|
||||
}, []);
|
||||
|
||||
// modal 스타일 설정
|
||||
useEffect(() => {
|
||||
if (panelInfo.modal && panelInfo.modalContainerId) {
|
||||
const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`);
|
||||
if (node) {
|
||||
const { width, height, top, left } = node.getBoundingClientRect();
|
||||
const style = {
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
position: 'fixed',
|
||||
overflow: 'visible',
|
||||
};
|
||||
setModalStyle(style);
|
||||
let scale = 1;
|
||||
if (typeof window === 'object') {
|
||||
scale = width / window.innerWidth;
|
||||
setModalScale(scale);
|
||||
}
|
||||
} else {
|
||||
setModalStyle(panelInfo.modalStyle || {});
|
||||
setModalScale(panelInfo.modalScale || 1);
|
||||
}
|
||||
} else if (isOnTop && !panelInfo.modal && videoPlayer.current) {
|
||||
if (videoPlayer.current?.getMediaState()?.paused) {
|
||||
videoPlayer.current.play();
|
||||
}
|
||||
|
||||
if (videoPlayer.current.areControlsVisible && !videoPlayer.current.areControlsVisible()) {
|
||||
videoPlayer.current.showControls();
|
||||
}
|
||||
}
|
||||
}, [panelInfo, isOnTop]);
|
||||
|
||||
const onClickBack = useCallback(
|
||||
(ev) => {
|
||||
// modal에서 full로 전환된 경우 다시 modal로 돌아감
|
||||
if (panelInfo.modalContainerId && !panelInfo.modal) {
|
||||
// 다시 modal로 돌리는 로직은 startVideoPlayer 액션을 사용할 수도 있지만
|
||||
// 여기서는 단순히 패널을 pop
|
||||
dispatch(PanelActions.popPanel());
|
||||
ev?.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panelInfo.modal) {
|
||||
dispatch(PanelActions.popPanel());
|
||||
ev?.stopPropagation();
|
||||
return;
|
||||
}
|
||||
},
|
||||
[dispatch, panelInfo]
|
||||
);
|
||||
|
||||
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') {
|
||||
console.log('mediainfoHandler....', type, ev, videoPlayer.current?.getMediaState());
|
||||
}
|
||||
if (ev === 'hlsError' && isNaN(Number(videoPlayer.current?.getMediaState().playbackRate))) {
|
||||
dispatch(
|
||||
sendBroadCast({
|
||||
type: 'videoError',
|
||||
moreInfo: { reason: 'hlsError' },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'timeupdate': {
|
||||
setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
dispatch(
|
||||
sendBroadCast({
|
||||
type: 'videoError',
|
||||
moreInfo: { reason: videoPlayer.current?.getMediaState().error },
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'loadeddata': {
|
||||
setVideoLoaded(true);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onEnded = useCallback(
|
||||
(e) => {
|
||||
console.log('[MediaPanel] Video ended');
|
||||
// 비디오 종료 시 패널 닫기
|
||||
Spotlight.pause();
|
||||
setTimeout(() => {
|
||||
Spotlight.resume();
|
||||
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
|
||||
}, 1500);
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const videoContainer = document.querySelector(`.${css.videoContainer}`);
|
||||
if (videoContainer && panelInfo.thumbnailUrl && !videoLoaded) {
|
||||
videoContainer.style.background = `url(${panelInfo.thumbnailUrl}) center center / contain no-repeat`;
|
||||
videoContainer.style.backgroundColor = 'black';
|
||||
}
|
||||
}, [panelInfo.thumbnailUrl, videoLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
setVideoLoaded(false);
|
||||
}, [currentPlayingUrl]);
|
||||
|
||||
return (
|
||||
<TPanel
|
||||
isTabActivated={false}
|
||||
{...props}
|
||||
className={classNames(
|
||||
css.videoContainer,
|
||||
panelInfo.modal && css.modal,
|
||||
!isOnTop && css.background
|
||||
)}
|
||||
handleCancel={onClickBack}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<Container spotlightRestrict="self-only" spotlightId="spotlightId-media-video-container">
|
||||
{currentPlayingUrl && (
|
||||
<VideoPlayer
|
||||
setApiProvider={getPlayer}
|
||||
disabled={panelInfo.modal}
|
||||
onEnded={onEnded}
|
||||
noAutoPlay={false}
|
||||
autoCloseTimeout={3000}
|
||||
onBackButton={onClickBack}
|
||||
spotlightDisabled={panelInfo.modal}
|
||||
isYoutube={isYoutube}
|
||||
src={currentPlayingUrl}
|
||||
style={panelInfo.modal ? modalStyle : {}}
|
||||
modalScale={panelInfo.modal ? modalScale : 1}
|
||||
modalClassName={panelInfo.modal && panelInfo.modalClassName}
|
||||
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}
|
||||
>
|
||||
{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 />}
|
||||
</VideoPlayer>
|
||||
)}
|
||||
</Container>
|
||||
</TPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MediaPanel);
|
||||
Reference in New Issue
Block a user