diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx index 32d749c2..69331956 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx +++ b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx @@ -11,15 +11,18 @@ import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; import classNames from 'classnames'; +import DurationFmt from 'ilib/lib/DurationFmt'; import PropTypes from 'prop-types'; import { platform } from '@enact/core/platform'; +import { memoize } from '@enact/core/util'; import Spotlight from '@enact/spotlight'; import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import { Spottable } from '@enact/spotlight/Spottable'; import Touchable from '@enact/ui/Touchable'; import Loader from '../Loader/Loader'; +import { MediaSlider, Times, secondsToTime } from '../MediaPlayer'; import Overlay from './Overlay'; import Media from './Media'; import TReactPlayer from './TReactPlayer'; @@ -35,6 +38,20 @@ const RootContainer = SpotlightContainerDecorator( 'div' ); +// DurationFmt memoization +const memoGetDurFmt = memoize( + () => new DurationFmt({ + length: 'medium', + style: 'clock', + useNative: false, + }) +); + +const getDurFmt = () => { + if (typeof window === 'undefined') return null; + return memoGetDurFmt(); +}; + /** * MediaPlayer.v2 컴포넌트 */ @@ -96,6 +113,8 @@ const MediaPlayerV2 = forwardRef((props, ref) => { const [error, setError] = useState(null); const [controlsVisible, setControlsVisible] = useState(false); const [sourceUnavailable, setSourceUnavailable] = useState(true); + const [proportionLoaded, setProportionLoaded] = useState(0); + const [proportionPlayed, setProportionPlayed] = useState(0); // ========== Refs ========== const videoRef = useRef(null); @@ -147,14 +166,21 @@ const MediaPlayerV2 = forwardRef((props, ref) => { const el = videoRef.current; if (!el) return; + const newCurrentTime = el.currentTime || 0; + const newDuration = el.duration || 0; + // 상태 업데이트 - setCurrentTime(el.currentTime || 0); - setDuration(el.duration || 0); + setCurrentTime(newCurrentTime); + setDuration(newDuration); setPaused(el.paused); setLoading(el.loading || false); setError(el.error || null); setSourceUnavailable((el.loading && sourceUnavailable) || el.error); + // Proportion 계산 + setProportionLoaded(el.proportionLoaded || 0); + setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0); + // 콜백 호출 if (ev.type === 'timeupdate' && onTimeUpdate) { onTimeUpdate(ev); @@ -247,8 +273,31 @@ const MediaPlayerV2 = forwardRef((props, ref) => { error, playbackRate: videoRef.current?.playbackRate || 1, proportionPlayed: duration > 0 ? currentTime / duration : 0, + proportionLoaded, }; - }, [currentTime, duration, paused, loading, error]); + }, [currentTime, duration, paused, loading, error, proportionLoaded]); + + // ========== Slider Event Handlers ========== + const handleSliderChange = useCallback(({ value }) => { + const time = value * duration; + seek(time); + }, [duration, seek]); + + const handleKnobMove = useCallback((ev) => { + if (!videoRef.current) return; + + const seconds = Math.floor(ev.proportion * videoRef.current.duration); + if (!isNaN(seconds)) { + // Scrub 시 시간 표시 업데이트 + // 필요시 onScrub 콜백 호출 가능 + } + }, []); + + const handleSliderKeyDown = useCallback((ev) => { + // Spotlight 키 이벤트 처리 + // 위/아래 키로 controls 이동 등 + // 기본 동작은 MediaSlider 내부에서 처리 + }, []); // ========== Video Click Handler (Modal 전환) ========== const handleVideoClick = useCallback(() => { @@ -398,34 +447,69 @@ const MediaPlayerV2 = forwardRef((props, ref) => { )} - {/* Simple Controls */} + {/* Controls with MediaSlider */} {controlsVisible && !isModal && ( -
- +
+ {/* Slider Section */} +
+ {/* Times - Total */} + - {onBackButton && ( + {/* Times - Current */} + + + {/* MediaSlider */} + +
+ + {/* Buttons Section */} +
- )} + + {onBackButton && ( + + )} +
)} diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less index 499d98ec..b7e287a9 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.module.less @@ -757,25 +757,43 @@ }); } -// ========== MediaPlayer.v2 Simple Controls ========== -.simpleControls { +// ========== MediaPlayer.v2 Controls ========== +.controlsContainer { position: absolute; - bottom: 40px; - left: 50%; - transform: translateX(-50%); + bottom: 0; + left: 0; + right: 0; + padding: 20px 40px 30px; + background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, transparent 100%); + z-index: 10; + display: flex; + flex-direction: column; + gap: 16px; +} + +.sliderContainer { + display: flex; + align-items: center; + gap: 12px; + width: 100%; +} + +.times { + min-width: 80px; + text-align: center; +} + +.controlsButtons { display: flex; gap: 20px; + justify-content: center; align-items: center; - z-index: 10; - padding: 20px; - background: rgba(0, 0, 0, 0.7); - border-radius: 12px; } .playPauseBtn { - width: 80px; - height: 80px; - font-size: 32px; + width: 60px; + height: 60px; + font-size: 24px; background: rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.6); border-radius: 50%;