diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js index f29d1b28..c44826a3 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js @@ -66,10 +66,12 @@ import TReactPlayer from './TReactPlayer'; import Video from './Video'; import css from './VideoPlayer.module.less'; import { updateVideoPlayState } from '../../actions/playActions'; +import createMemoryMonitor from '../../utils/memoryMonitor'; // 디버그 헬퍼 설정 const DEBUG_MODE = false; const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); +const memoryMonitor = createMemoryMonitor(); const isEnter = is('enter'); const isLeft = is('left'); @@ -826,6 +828,7 @@ const VideoPlayerBase = class extends React.Component { } componentDidMount() { + memoryMonitor.logMemory('[VideoPlayer] componentDidMount'); on('mousemove', this.activityDetected); if (platform.touch) { on('touchmove', this.activityDetected); @@ -1021,6 +1024,7 @@ const VideoPlayerBase = class extends React.Component { } componentWillUnmount() { + memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src }); // console.log('[VideoPlayer] componentWillUnmount - start cleanup', { src: this.props?.src }); off('mousemove', this.activityDetected); if (platform.touch) { @@ -1128,6 +1132,12 @@ const VideoPlayerBase = class extends React.Component { } // 레퍼런스도 해제해 GC 대상이 되도록 함 this.video = null; + // 메모리 모니터링 인터벌 정리 + if (this.memoryMonitoringInterval) { + clearInterval(this.memoryMonitoringInterval); + this.memoryMonitoringInterval = null; + } + memoryMonitor.logMemory('[VideoPlayer] componentWillUnmount - cleanup done'); // console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src }); if (this.floatingLayerController) { this.floatingLayerController.unregister(); @@ -1564,6 +1574,14 @@ const VideoPlayerBase = class extends React.Component { handleEvent = (ev) => { const el = this.video; + // 재생 종료 또는 오류 시 메모리 모니터링 타이머 정리 + if (ev.type === 'ended' || ev.type === 'error') { + if (this.memoryMonitoringInterval) { + clearInterval(this.memoryMonitoringInterval); + this.memoryMonitoringInterval = null; + } + } + const updatedState = { // Standard media properties currentTime: 0, @@ -1798,6 +1816,11 @@ const VideoPlayerBase = class extends React.Component { * @public */ play = () => { + console.log('[TEST] play() method called'); + memoryMonitor.logMemory('[VideoPlayer] play() called', { + currentTime: this.state.currentTime, + duration: this.state.duration, + }); dlog('🟢 [PlayerPanel][VideoPlayer] play() called', { currentTime: this.state.currentTime, duration: this.state.duration, @@ -1819,6 +1842,21 @@ const VideoPlayerBase = class extends React.Component { this.send('play'); this.announce($L('Play')); this.startDelayedMiniFeedbackHide(5000); + // 재생 시작 시 정기적 메모리 모니터링 시작 + if (!this.memoryMonitoringInterval) { + this.memoryMonitoringInterval = setInterval(() => { + try { + const mediaState = this.getMediaState(); + memoryMonitor.logMemory('[VideoPlayer] Playing', { + currentTime: (mediaState?.currentTime ?? 0).toFixed(2), + duration: (mediaState?.duration ?? 0).toFixed(2), + buffered: (this.state?.proportionLoaded ?? 0).toFixed(2), + }); + } catch (err) { + // 타이머 실행 중 오류 발생 시 무시 + } + }, 30000); // 30초마다 메모리 확인 + } // Redux 상태 업데이트 - 재생 상태로 변경 if (this.props.dispatch) { @@ -1842,6 +1880,10 @@ const VideoPlayerBase = class extends React.Component { * @public */ pause = () => { + memoryMonitor.logMemory('[VideoPlayer] pause() called', { + currentTime: this.state.currentTime.toFixed(2), + duration: this.state.duration.toFixed(2), + }); dlog('🔴 [VideoPlayer] pause() called', { currentTime: this.state.currentTime, duration: this.state.duration, @@ -1863,6 +1905,11 @@ const VideoPlayerBase = class extends React.Component { this.send('pause'); this.announce($L('Pause')); this.stopDelayedMiniFeedbackHide(); + // 재생 일시정지 시 정기적 메모리 모니터링 중지 + if (this.memoryMonitoringInterval) { + clearInterval(this.memoryMonitoringInterval); + this.memoryMonitoringInterval = null; + } // Redux 상태 업데이트 - 일시정지 상태로 변경 if (this.props.dispatch) { diff --git a/com.twin.app.shoptime/src/utils/memoryMonitor.js b/com.twin.app.shoptime/src/utils/memoryMonitor.js new file mode 100644 index 00000000..31e2bb63 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/memoryMonitor.js @@ -0,0 +1,191 @@ +/** + * 메모리 모니터링 유틸리티 + * [Memory] 태그를 붙인 로그로 메모리 사용량을 추적합니다 + */ + +let memoryMonitorInstance = null; +let initialized = false; + +export const createMemoryMonitor = (enableInitLog = true) => { + // 싱글톤 패턴: 이미 생성된 인스턴스가 있으면 재사용 + if (memoryMonitorInstance) { + return memoryMonitorInstance; + } + + if (enableInitLog && !initialized) { + initialized = true; + const timestamp = new Date().toISOString(); + console.log(`[Memory Monitor Initialized] ${timestamp}`); + if (typeof performance !== 'undefined' && performance.memory) { + console.log(`[Memory] API Support: YES - performance.memory available`); + } else { + console.log(`[Memory] API Support: NO - performance.memory NOT available (webOS TV 또는 제한된 브라우저)`); + } + } + const getMemoryInfo = () => { + if (typeof performance !== 'undefined' && performance.memory) { + return { + usedJSHeapSize: (performance.memory.usedJSHeapSize / 1048576).toFixed(2), + totalJSHeapSize: (performance.memory.totalJSHeapSize / 1048576).toFixed(2), + jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2), + }; + } + return null; + }; + + const formatMemoryLog = (usedMB, totalMB, limitMB) => { + const percentage = ((usedMB / limitMB) * 100).toFixed(1); + return `[Memory] Used: ${usedMB}MB / Total: ${totalMB}MB / Limit: ${limitMB}MB (${percentage}%)`; + }; + + return { + /** + * 현재 메모리 상태를 로깅 + * @param {string} context - 컨텍스트 설명 + * @param {object} additionalInfo - 추가 정보 + */ + logMemory: (context = '', additionalInfo = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + console.log(`${logMsg} | ${context} ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); + } + }, + + /** + * 메모리 사용량 변화를 추적 + * @param {string} context - 컨텍스트 설명 + * @param {number} previousMB - 이전 메모리 사용량 (MB) + * @returns {number} 현재 메모리 사용량 (MB) + */ + trackMemoryDelta: (context = '', previousMB = 0) => { + const mem = getMemoryInfo(); + if (mem) { + const currentMB = parseFloat(mem.usedJSHeapSize); + const delta = (currentMB - previousMB).toFixed(2); + const deltaSign = delta > 0 ? '+' : ''; + console.log( + `[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB` + ); + return currentMB; + } + return previousMB; + }, + + /** + * 정기적으로 메모리를 모니터링 + * @param {number} intervalMs - 모니터링 간격 (기본값: 10000ms) + * @param {string} label - 모니터링 라벨 + * @returns {function} cleanup 함수 + */ + startPeriodicMonitoring: (intervalMs = 10000, label = 'Periodic') => { + let lastMemory = 0; + const mem = getMemoryInfo(); + if (mem) lastMemory = parseFloat(mem.usedJSHeapSize); + + const intervalId = setInterval(() => { + lastMemory = this.trackMemoryDelta(`${label}:`, lastMemory); + }, intervalMs); + + return () => clearInterval(intervalId); + }, + + /** + * 버퍼 관련 메모리 정보 로깅 + * @param {string} context - 컨텍스트 + * @param {object} bufferInfo - 버퍼 정보 { bufferedSegments, totalDuration, etc } + */ + logBufferMemory: (context = '', bufferInfo = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const bufferStr = JSON.stringify(bufferInfo); + console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`); + } + }, + + /** + * HLS 상태에 따른 메모리 로깅 + * @param {string} context - 컨텍스트 + * @param {object} hlsState - HLS 상태 정보 + */ + logHlsMemory: (context = '', hlsState = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const hlsStr = JSON.stringify(hlsState); + console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`); + } + }, + + /** + * 메모리 정보만 반환 (로깅 없음) + * @returns {object} 메모리 정보 객체 + */ + getMemory: () => getMemoryInfo(), + }; + + // 싱글톤 인스턴스 저장 + memoryMonitorInstance = { + logMemory: (context = '', additionalInfo = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const info = Object.keys(additionalInfo).length > 0 ? JSON.stringify(additionalInfo) : ''; + console.log(`${logMsg} | ${context} ${info}`); + } else { + const timestamp = new Date().toISOString(); + console.log(`[Memory] [${timestamp}] ${context} - Browser does not support performance.memory API (추가정보: ${JSON.stringify(additionalInfo)})`); + } + }, + trackMemoryDelta: (context = '', previousMB = 0) => { + const mem = getMemoryInfo(); + if (mem) { + const currentMB = parseFloat(mem.usedJSHeapSize); + const delta = (currentMB - previousMB).toFixed(2); + const deltaSign = delta > 0 ? '+' : ''; + console.log( + `[Memory] ${context} | Current: ${currentMB}MB (${deltaSign}${delta}MB) | Total: ${mem.totalJSHeapSize}MB / Limit: ${mem.jsHeapSizeLimit}MB` + ); + return currentMB; + } + return previousMB; + }, + startPeriodicMonitoring: (intervalMs = 30000, label = 'Periodic') => { + let lastMemory = 0; + const mem = getMemoryInfo(); + if (mem) lastMemory = parseFloat(mem.usedJSHeapSize); + + const intervalId = setInterval(() => { + this.trackMemoryDelta(`${label}:`, lastMemory); + }, intervalMs); + + return () => clearInterval(intervalId); + }, + logBufferMemory: (context = '', bufferInfo = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const bufferStr = JSON.stringify(bufferInfo); + console.log(`${logMsg} | Buffer: ${context} | Info: ${bufferStr}`); + } + }, + logHlsMemory: (context = '', hlsState = {}) => { + const mem = getMemoryInfo(); + if (mem) { + const logMsg = formatMemoryLog(mem.usedJSHeapSize, mem.totalJSHeapSize, mem.jsHeapSizeLimit); + const hlsStr = JSON.stringify(hlsState); + console.log(`${logMsg} | HLS: ${context} | State: ${hlsStr}`); + } + }, + getMemory: () => getMemoryInfo(), + }; + + return memoryMonitorInstance; +}; + +export default createMemoryMonitor; diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index f12e9602..fcfcb527 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -62,6 +62,7 @@ import css from './PlayerPanel.module.less'; import PlayerTabButton from './PlayerTabContents/TabButton/PlayerTabButton'; import TabContainer from './PlayerTabContents/TabContainer'; import TabContainerV2 from './PlayerTabContents/v2/TabContainer.v2'; +import createMemoryMonitor from '../../utils/memoryMonitor'; // import LiveShowContainer from './PlayerTabContents/v2/LiveShowContainer'; // import ShopNowContainer from './PlayerTabContents/v2/ShopNowContainer'; // import ShopNowButton from './PlayerTabContents/v2/ShopNowButton'; @@ -179,6 +180,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props const focusReturnRef = useRef(null); const modalPrevRef = useRef(panelInfo?.modal); const prevIsTopPanelDetailFromPlayerRef = useRef(false); + const memoryMonitor = useRef(null); const [playListInfo, setPlayListInfo] = USE_STATE('playListInfo', ''); const [shopNowInfo, setShopNowInfo] = USE_STATE('shopNowInfo'); const [backupInitialIndex, setBackupInitialIndex] = USE_STATE('backupInitialIndex', 0); @@ -370,6 +372,14 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props panelInfoRef.current = panelInfo; }, [panelInfo]); + // memoryMonitor 초기화 (마운트 시 한 번만) + useEffect(() => { + if (!memoryMonitor.current) { + memoryMonitor.current = createMemoryMonitor(false); // 초기 로그 비활성화 + console.log('[PlayerPanel] Memory monitor initialized'); + } + }, []); + // PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가 useEffect(() => { dlog('[PlayerPanel] 🔍 isOnTop useEffect 호출:', { @@ -1733,6 +1743,24 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props setCurrentTime(videoPlayer.current?.getMediaState()?.currentTime); break; } + case 'onBuffer': { + // 버퍼링 시작 시 메모리 상태 로깅 + memoryMonitor.current.logMemory('[Video Buffer Start]', { + currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2), + duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2), + proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2), + }); + break; + } + case 'onBufferEnd': { + // 버퍼링 종료 시 메모리 상태 로깅 + memoryMonitor.current.logMemory('[Video Buffer End]', { + currentTime: videoPlayer.current?.getMediaState()?.currentTime?.toFixed(2), + duration: videoPlayer.current?.getMediaState()?.duration?.toFixed(2), + proportionLoaded: videoPlayer.current?.getMediaState()?.proportionLoaded?.toFixed(2), + }); + break; + } case 'error': { dispatch( sendBroadCast({ @@ -1746,6 +1774,23 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props const mediaId = videoPlayer.current?.video?.media?.mediaId; setMediaId(mediaId); setVideoLoaded(true); + // HLS 인스턴스 정보 로깅 + try { + const hlsInstance = videoPlayer.current?.video?.getInternalPlayer?.('hls'); + if (hlsInstance) { + memoryMonitor.current.logHlsMemory('[Video Loaded] HLS Instance', { + hlsVersion: hlsInstance.version, + config: { + maxBufferLength: hlsInstance.config?.maxBufferLength, + maxMaxBufferLength: hlsInstance.config?.maxMaxBufferLength, + backBufferLength: hlsInstance.config?.backBufferLength, + maxBufferSize: hlsInstance.config?.maxBufferSize, + }, + }); + } + } catch (e) { + // HLS 정보 수집 실패는 무시 + } dlog( '[PlayerPanel] 🎬 Video Loaded - shptmBanrTpNm:', panelInfoRef.current?.shptmBanrTpNm @@ -1998,6 +2043,17 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props setVideoLoaded(false); }, [currentPlayingUrl]); + // 메모리 모니터링: 비디오 URL 변경 시 + useEffect(() => { + if (currentPlayingUrl) { + memoryMonitor.current.logMemory(`[Video Change] New URL loaded`, { + url: currentPlayingUrl.substring(0, 50), + isHLS: currentPlayingUrl.includes('.m3u8'), + isDASH: currentPlayingUrl.includes('.mpd'), + }); + } + }, [currentPlayingUrl]); + // 비디오가 새로 선택될 때 타이머 초기화 useEffect(() => { if (currentPlayingUrl) {