diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 0507bedb..a62e7509 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -257,6 +257,7 @@ export const types = { GET_CHAT_LOG: 'GET_CHAT_LOG', GET_SUBTITLE: 'GET_SUBTITLE', CLEAR_PLAYER_INFO: 'CLEAR_PLAYER_INFO', + CLEAR_SUBTITLE_BLOB: 'CLEAR_SUBTITLE_BLOB', UPDATE_VIDEO_PLAY_STATE: 'UPDATE_VIDEO_PLAY_STATE', // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - μž¬μƒ μƒνƒœ diff --git a/com.twin.app.shoptime/src/actions/playActions.js b/com.twin.app.shoptime/src/actions/playActions.js index 594bf9fc..2db5ac53 100644 --- a/com.twin.app.shoptime/src/actions/playActions.js +++ b/com.twin.app.shoptime/src/actions/playActions.js @@ -786,6 +786,28 @@ export const CLEAR_PLAYER_INFO = () => ({ type: types.CLEAR_PLAYER_INFO, }); +// νŠΉμ • μžλ§‰ Blob URL을 ν•΄μ œν•˜λŠ” μ•‘μ…˜ μƒμ„±μž +export const clearSubtitleBlob = (subtitleUrl) => (dispatch, getState) => { + const currentBlobs = getState().play.subTitleBlobs; + const blobUrl = currentBlobs[subtitleUrl]; + + // Blob URL ν•΄μ œ + if (blobUrl && blobUrl.startsWith('blob:')) { + try { + URL.revokeObjectURL(blobUrl); + dlog('[clearSubtitleBlob] Revoked Blob URL:', subtitleUrl); + } catch (error) { + derror('[clearSubtitleBlob] Failed to revoke Blob URL:', error); + } + } + + // Redux μƒνƒœμ—μ„œ 제거 + dispatch({ + type: types.CLEAR_SUBTITLE_BLOB, + payload: { subtitleUrl } + }); +}; + /** * λΉ„λ””μ˜€ μž¬μƒ μƒνƒœλ₯Ό Redux에 μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€. * @param {Object} playState - μ—…λ°μ΄νŠΈν•  μž¬μƒ μƒνƒœ diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx b/com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx index 314ff62b..eac230ac 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx +++ b/com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx @@ -169,6 +169,11 @@ export default function TReactPlayer({ if (typeof videoNode.stopVideo === 'function' && videoNode.stopVideo !== videoNode.pause) { videoNode.stopVideo(); } + // HLS μΈμŠ€ν„΄μŠ€κ°€ μ‘΄μž¬ν•˜λ©΄ λͺ…μ‹œμ μœΌλ‘œ 파괴 + const hls = playerRef.current?.getInternalPlayer?.('hls'); + if (hls && typeof hls.destroy === 'function') { + hls.destroy(); + } } catch (err) { console.warn('[TReactPlayer] cleanup warning:', err); } diff --git a/com.twin.app.shoptime/src/reducers/playReducer.js b/com.twin.app.shoptime/src/reducers/playReducer.js index 78389d86..8827d3d1 100644 --- a/com.twin.app.shoptime/src/reducers/playReducer.js +++ b/com.twin.app.shoptime/src/reducers/playReducer.js @@ -103,6 +103,18 @@ export const playReducer = (state = initialState, action) => { subTitleBlobs: {}, }; } + case types.CLEAR_SUBTITLE_BLOB: { + const { subtitleUrl } = action.payload; + const newSubTitleBlobs = { ...state.subTitleBlobs }; + + // νŠΉμ • URL만 제거 + delete newSubTitleBlobs[subtitleUrl]; + + return { + ...state, + subTitleBlobs: newSubTitleBlobs, + }; + } case types.UPDATE_VIDEO_PLAY_STATE: { const newState = { ...state.videoPlayState, diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx index 2676751f..4e179bab 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx @@ -34,6 +34,7 @@ import * as PanelActions from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions'; import { CLEAR_PLAYER_INFO, + clearSubtitleBlob, getChatLog, getSubTitle, startVideoPlayer, @@ -261,6 +262,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props const liveShowInfos = USE_SELECTOR('liveShowInfos', (state) => state.main.liveShowInfos); const vodSubtitleData = USE_SELECTOR('vodSubtitleData', (state) => state.play.subTitleBlobs); const previousSubtitleBlobRef = useRef(null); + const previousSubtitleUrlRef = useRef(null); const broadcast = USE_SELECTOR('broadcast', (state) => state.common.broadcast); const videoPlayState = USE_SELECTOR('videoPlayState', (state) => state.play.videoPlayState); @@ -1493,6 +1495,13 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props } }, [panelInfo?.shptmBanrTpNm, playListInfo]); + // μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ Job 정리 + useEffect(() => { + return () => { + initialFocusTimeoutJob.current?.stop?.(); + }; + }, []); + // live subtitle Luna API useEffect(() => { if (currentSubtitleBlob) { @@ -1953,7 +1962,23 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props if (currentSubtitleUrl) { dispatch(getSubTitle({ showSubtitleUrl: currentSubtitleUrl })); } - }, [currentSubtitleUrl]); + + // 이전 μžλ§‰ URL 정리 (Redux λ©”λͺ¨λ¦¬ λˆ„μˆ˜ λ°©μ§€) + const prevSubtitleUrl = previousSubtitleUrlRef.current; + if (prevSubtitleUrl && prevSubtitleUrl !== currentSubtitleUrl) { + dispatch(clearSubtitleBlob(prevSubtitleUrl)); + dlog('[PlayerPanel] Clearing previous subtitle URL:', prevSubtitleUrl); + } + + previousSubtitleUrlRef.current = currentSubtitleUrl; + + // μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ λ§ˆμ§€λ§‰ μžλ§‰ URL 정리 + return () => { + if (previousSubtitleUrlRef.current) { + dispatch(clearSubtitleBlob(previousSubtitleUrlRef.current)); + } + }; + }, [currentSubtitleUrl, dispatch]); useEffect(() => { setVideoLoaded(false);