[251125] fix: VideoPlayer 메모리최적화 - 1
🕐 커밋 시간: 2025. 11. 25. 10:41:26 📊 변경 통계: • 총 파일: 3개 • 추가: +101줄 • 삭제: -76줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 중간 규모 기능 개선 • 코드 정리 및 최적화
This commit is contained in:
@@ -155,33 +155,8 @@ export default function TReactPlayer({
|
|||||||
return events;
|
return events;
|
||||||
}, [handleEvent, mediaEventsMap]);
|
}, [handleEvent, mediaEventsMap]);
|
||||||
|
|
||||||
// 🔽 [최적화] URL 변경 또는 언마운트 시 이전 비디오 정리 (메모리 누수 방지)
|
// 메모리 정리는 VideoPlayer.js componentWillUnmount에서 수행
|
||||||
useEffect(() => {
|
// TReactPlayer는 react-player 래퍼 역할만 수행
|
||||||
return () => {
|
|
||||||
// console.log('[TReactPlayer] cleanup - start', { url });
|
|
||||||
const videoNode = playerRef.current?.getInternalPlayer();
|
|
||||||
if (videoNode) {
|
|
||||||
try {
|
|
||||||
// VideoPlayer.js에서 이미 처리하므로 최소한 정리만
|
|
||||||
if (videoNode.pause) {
|
|
||||||
videoNode.pause();
|
|
||||||
}
|
|
||||||
// 중복 정리 방지를 위해 조건 더 엄격하게
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.log('[TReactPlayer] cleanup - done', { url });
|
|
||||||
};
|
|
||||||
}, [url]); // ✅ URL 변경 시에도 정리 로직 실행
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
static contextType = FloatingLayerContext;
|
static contextType = FloatingLayerContext;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoCloseTimeout: 3000,
|
autoCloseTimeout: 10000,
|
||||||
disableSliderFocus: false,
|
disableSliderFocus: false,
|
||||||
feedbackHideDelay: 3000,
|
feedbackHideDelay: 3000,
|
||||||
initialJumpDelay: 400,
|
initialJumpDelay: 400,
|
||||||
@@ -1043,55 +1043,93 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
this.slider5WayPressJob.stop();
|
this.slider5WayPressJob.stop();
|
||||||
// 플레이어 자원 정리: 언마운트 시 디코더/버퍼 해제
|
// 플레이어 자원 정리: 언마운트 시 디코더/버퍼 해제
|
||||||
try {
|
try {
|
||||||
// 1. 먼저 중지 (가장 중요)
|
// 1단계: HLS/YouTube 인스턴스를 먼저 정리 (이벤트 리스너 정리, 버퍼 해제)
|
||||||
if (this.video?.stopVideo) {
|
|
||||||
this.video.stopVideo();
|
|
||||||
} else if (this.video?.pause) {
|
|
||||||
this.video.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 시간 초기화
|
|
||||||
if (this.video?.seekTo) {
|
|
||||||
this.video.seekTo(0);
|
|
||||||
} else if (this.video) {
|
|
||||||
this.video.currentTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 리소스 해제
|
|
||||||
if (this.video) {
|
|
||||||
this.video.src = '';
|
|
||||||
if ('srcObject' in this.video) {
|
|
||||||
this.video.srcObject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 내부 인스턴스 정리 (마지막)
|
|
||||||
if (typeof this.video?.getInternalPlayer === 'function') {
|
if (typeof this.video?.getInternalPlayer === 'function') {
|
||||||
|
// HLS 재생 중인지 먼저 확인
|
||||||
|
let hlsDestroyed = false;
|
||||||
try {
|
try {
|
||||||
const hls = this.video.getInternalPlayer('hls');
|
const hls = this.video.getInternalPlayer('hls');
|
||||||
if (hls && typeof hls.destroy === 'function') {
|
if (hls && typeof hls.destroy === 'function') {
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
|
hlsDestroyed = true;
|
||||||
|
dlog('[VideoPlayer] HLS instance destroyed');
|
||||||
}
|
}
|
||||||
} catch (hlsErr) {
|
} catch (hlsErr) {
|
||||||
// HLS 정리 실패는 무시
|
// HLS 정리 실패는 무시
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// HLS가 아닌 경우에만 YouTube 정리 시도 (이중 destroy 방지)
|
||||||
const yt = this.video.getInternalPlayer();
|
if (!hlsDestroyed) {
|
||||||
if (yt?.stopVideo) {
|
try {
|
||||||
yt.stopVideo();
|
const internalPlayer = this.video.getInternalPlayer();
|
||||||
|
|
||||||
|
// YouTube인 경우
|
||||||
|
if (internalPlayer && typeof internalPlayer.stopVideo === 'function') {
|
||||||
|
internalPlayer.stopVideo();
|
||||||
|
dlog('[VideoPlayer] YouTube stopped');
|
||||||
|
|
||||||
|
// YouTube iframe 제거 (메모리 누수 방지)
|
||||||
|
try {
|
||||||
|
const iframe = this.video.getInternalPlayer('iframe');
|
||||||
|
if (iframe && iframe.parentNode) {
|
||||||
|
iframe.parentNode.removeChild(iframe);
|
||||||
|
dlog('[VideoPlayer] YouTube iframe removed from DOM');
|
||||||
|
}
|
||||||
|
} catch (iframeErr) {
|
||||||
|
// iframe 제거 실패는 무시
|
||||||
|
}
|
||||||
|
|
||||||
|
// YouTube destroy 메서드가 있으면 호출
|
||||||
|
if (typeof internalPlayer.destroy === 'function') {
|
||||||
|
internalPlayer.destroy();
|
||||||
|
dlog('[VideoPlayer] YouTube instance destroyed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ytErr) {
|
||||||
|
// YouTube 정리 실패는 무시
|
||||||
}
|
}
|
||||||
} catch (ytErr) {
|
|
||||||
// YouTube 정리 실패는 무시
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2단계: 실제 video DOM 엘리먼트 정리 (HLS/YouTube 정리 후)
|
||||||
|
// webOS TV 환경에서는 this.video.media, 웹 환경에서는 getInternalPlayer로 video 엘리먼트 접근
|
||||||
|
let videoElement = null;
|
||||||
|
|
||||||
|
if (this.video?.media) {
|
||||||
|
// webOS TV 환경 (Media.js)
|
||||||
|
videoElement = this.video.media;
|
||||||
|
} else if (typeof this.video?.getInternalPlayer === 'function') {
|
||||||
|
// 웹 환경: file player의 경우 video 엘리먼트 반환
|
||||||
|
try {
|
||||||
|
const player = this.video.getInternalPlayer();
|
||||||
|
// video 엘리먼트인지 확인 (tagName이 VIDEO인 경우만)
|
||||||
|
if (player && player.tagName === 'VIDEO') {
|
||||||
|
videoElement = player;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// getInternalPlayer 실패는 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoElement && typeof videoElement.pause === 'function') {
|
||||||
|
videoElement.pause();
|
||||||
|
videoElement.src = '';
|
||||||
|
if ('srcObject' in videoElement) {
|
||||||
|
videoElement.srcObject = null;
|
||||||
|
}
|
||||||
|
videoElement.load();
|
||||||
|
dlog('[VideoPlayer] video element resources cleared');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 정리 중 에러는 무시하고 언마운트 진행
|
// 정리 중 에러는 무시하고 언마운트 진행
|
||||||
// console.warn('[VideoPlayer] cleanup error', err);
|
derror('[VideoPlayer] cleanup error', err);
|
||||||
}
|
}
|
||||||
|
// 레퍼런스도 해제해 GC 대상이 되도록 함
|
||||||
|
this.video = null;
|
||||||
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
|
// console.log('[VideoPlayer] componentWillUnmount - cleanup done', { src: this.props?.src });
|
||||||
if (this.floatingLayerController) {
|
if (this.floatingLayerController) {
|
||||||
this.floatingLayerController.unregister();
|
this.floatingLayerController.unregister();
|
||||||
|
this.floatingLayerController = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2327,17 +2365,21 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
} else if (is('down', keyCode)) {
|
} else if (is('down', keyCode)) {
|
||||||
Spotlight.setPointerMode(false);
|
Spotlight.setPointerMode(false);
|
||||||
|
|
||||||
// TabContainerV2의 tabIndex=2일 때 버튼들로 포커스 이동
|
// TabContainerV2가 열려 있으면 현재 tabIndexV2에 맞는 버튼으로 포커스 이동
|
||||||
if (this.props.tabContainerVersion === 2 && this.props.tabIndexV2 === 2) {
|
if (this.props.tabContainerVersion === 2 && this.props.belowContentsVisible) {
|
||||||
|
const { tabIndexV2 } = this.props;
|
||||||
let focusSuccessful = false;
|
let focusSuccessful = false;
|
||||||
|
|
||||||
// 먼저 LiveChannelNext 버튼으로 시도
|
if (tabIndexV2 === 0) {
|
||||||
if (Spotlight.focus('live-channel-next-button')) {
|
focusSuccessful = Spotlight.focus('shownow_close_button');
|
||||||
focusSuccessful = true;
|
} else if (tabIndexV2 === 1) {
|
||||||
}
|
focusSuccessful = Spotlight.focus('below-tab-live-channel-button');
|
||||||
// 실패하면 ShopNowButton으로 시도
|
} else if (tabIndexV2 === 2) {
|
||||||
else if (Spotlight.focus('below-tab-shop-now-button')) {
|
// 먼저 LiveChannelNext, 실패하면 ShopNowButton
|
||||||
focusSuccessful = true;
|
focusSuccessful = Spotlight.focus('live-channel-next-button');
|
||||||
|
if (!focusSuccessful) {
|
||||||
|
focusSuccessful = Spotlight.focus('below-tab-shop-now-button');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusSuccessful) {
|
if (focusSuccessful) {
|
||||||
|
|||||||
@@ -2002,14 +2002,17 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
file: {
|
file: {
|
||||||
attributes: {
|
attributes: {
|
||||||
crossOrigin: 'true',
|
crossOrigin: 'true',
|
||||||
|
preload: 'metadata',
|
||||||
},
|
},
|
||||||
tracks: [{ kind: 'subtitles', src: currentSubtitleBlob, default: true }],
|
tracks: [{ kind: 'subtitles', src: currentSubtitleBlob, default: true }],
|
||||||
hlsOptions: {
|
hlsOptions: {
|
||||||
// 버퍼 길이를 늘려 재버퍼링 감소
|
// 버퍼 길이를 30초 기준으로 설정
|
||||||
maxBufferLength: 60,
|
maxBufferLength: 30,
|
||||||
maxMaxBufferLength: 180,
|
maxMaxBufferLength: 90,
|
||||||
liveSyncDuration: 16,
|
backBufferLength: 0,
|
||||||
liveMaxLatencyDuration: 32,
|
maxBufferSize: 30 * 1000 * 1000, // 최대 버퍼 크기 30MB
|
||||||
|
liveSyncDuration: 8,
|
||||||
|
liveMaxLatencyDuration: 16,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
youtube: YOUTUBECONFIG,
|
youtube: YOUTUBECONFIG,
|
||||||
@@ -2018,11 +2021,16 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
return {
|
return {
|
||||||
youtube: YOUTUBECONFIG,
|
youtube: YOUTUBECONFIG,
|
||||||
file: {
|
file: {
|
||||||
|
attributes: {
|
||||||
|
preload: 'metadata',
|
||||||
|
},
|
||||||
hlsOptions: {
|
hlsOptions: {
|
||||||
maxBufferLength: 60,
|
maxBufferLength: 30,
|
||||||
maxMaxBufferLength: 180,
|
maxMaxBufferLength: 90,
|
||||||
liveSyncDuration: 16,
|
backBufferLength: 0,
|
||||||
liveMaxLatencyDuration: 32,
|
maxBufferSize: 30 * 1000 * 1000,
|
||||||
|
liveSyncDuration: 8,
|
||||||
|
liveMaxLatencyDuration: 16,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -2884,7 +2892,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
|||||||
disabled={panelInfo.modal}
|
disabled={panelInfo.modal}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
noAutoPlay={cannotPlay}
|
noAutoPlay={cannotPlay}
|
||||||
autoCloseTimeout={3000}
|
autoCloseTimeout={6000}
|
||||||
onBackButton={onClickBack}
|
onBackButton={onClickBack}
|
||||||
spotlightDisabled={panelInfo.modal}
|
spotlightDisabled={panelInfo.modal}
|
||||||
isYoutube={isYoutube}
|
isYoutube={isYoutube}
|
||||||
|
|||||||
Reference in New Issue
Block a user