[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:
2025-11-25 10:41:26 +09:00
parent 564ff1f69a
commit 89ff921aaa
3 changed files with 102 additions and 77 deletions

View File

@@ -155,33 +155,8 @@ export default function TReactPlayer({
return events;
}, [handleEvent, mediaEventsMap]);
// 🔽 [최적화] URL 변경 또는 언마운트 시 이전 비디오 정리 (메모리 누수 방지)
useEffect(() => {
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 변경 시에도 정리 로직 실행
// 메모리 정리는 VideoPlayer.js componentWillUnmount에서 수행
// TReactPlayer는 react-player 래퍼 역할만 수행
return (
<ReactPlayer

View File

@@ -736,7 +736,7 @@ const VideoPlayerBase = class extends React.Component {
static contextType = FloatingLayerContext;
static defaultProps = {
autoCloseTimeout: 3000,
autoCloseTimeout: 10000,
disableSliderFocus: false,
feedbackHideDelay: 3000,
initialJumpDelay: 400,
@@ -1043,55 +1043,93 @@ const VideoPlayerBase = class extends React.Component {
this.slider5WayPressJob.stop();
// 플레이어 자원 정리: 언마운트 시 디코더/버퍼 해제
try {
// 1. 먼저 중지 (가장 중요)
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. 내부 인스턴스 정리 (마지막)
// 1단계: HLS/YouTube 인스턴스를 먼저 정리 (이벤트 리스너 정리, 버퍼 해제)
if (typeof this.video?.getInternalPlayer === 'function') {
// HLS 재생 중인지 먼저 확인
let hlsDestroyed = false;
try {
const hls = this.video.getInternalPlayer('hls');
if (hls && typeof hls.destroy === 'function') {
hls.destroy();
hlsDestroyed = true;
dlog('[VideoPlayer] HLS instance destroyed');
}
} catch (hlsErr) {
// HLS 정리 실패는 무시
}
// HLS가 아닌 경우에만 YouTube 정리 시도 (이중 destroy 방지)
if (!hlsDestroyed) {
try {
const yt = this.video.getInternalPlayer();
if (yt?.stopVideo) {
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 정리 실패는 무시
}
}
}
// 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) {
// 정리 중 에러는 무시하고 언마운트 진행
// 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 });
if (this.floatingLayerController) {
this.floatingLayerController.unregister();
this.floatingLayerController = null;
}
}
@@ -2327,17 +2365,21 @@ const VideoPlayerBase = class extends React.Component {
} else if (is('down', keyCode)) {
Spotlight.setPointerMode(false);
// TabContainerV2 tabIndex=2일 때 버튼로 포커스 이동
if (this.props.tabContainerVersion === 2 && this.props.tabIndexV2 === 2) {
// TabContainerV2가 열려 있으면 현재 tabIndexV2에 맞는 버튼로 포커스 이동
if (this.props.tabContainerVersion === 2 && this.props.belowContentsVisible) {
const { tabIndexV2 } = this.props;
let focusSuccessful = false;
// 먼저 LiveChannelNext 버튼으로 시도
if (Spotlight.focus('live-channel-next-button')) {
focusSuccessful = true;
if (tabIndexV2 === 0) {
focusSuccessful = Spotlight.focus('shownow_close_button');
} else if (tabIndexV2 === 1) {
focusSuccessful = Spotlight.focus('below-tab-live-channel-button');
} else if (tabIndexV2 === 2) {
// 먼저 LiveChannelNext, 실패하면 ShopNowButton
focusSuccessful = Spotlight.focus('live-channel-next-button');
if (!focusSuccessful) {
focusSuccessful = Spotlight.focus('below-tab-shop-now-button');
}
// 실패하면 ShopNowButton으로 시도
else if (Spotlight.focus('below-tab-shop-now-button')) {
focusSuccessful = true;
}
if (focusSuccessful) {

View File

@@ -2002,14 +2002,17 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
file: {
attributes: {
crossOrigin: 'true',
preload: 'metadata',
},
tracks: [{ kind: 'subtitles', src: currentSubtitleBlob, default: true }],
hlsOptions: {
// 버퍼 길이를 늘려 재버퍼링 감소
maxBufferLength: 60,
maxMaxBufferLength: 180,
liveSyncDuration: 16,
liveMaxLatencyDuration: 32,
// 버퍼 길이를 30초 기준으로 설정
maxBufferLength: 30,
maxMaxBufferLength: 90,
backBufferLength: 0,
maxBufferSize: 30 * 1000 * 1000, // 최대 버퍼 크기 30MB
liveSyncDuration: 8,
liveMaxLatencyDuration: 16,
},
},
youtube: YOUTUBECONFIG,
@@ -2018,11 +2021,16 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return {
youtube: YOUTUBECONFIG,
file: {
attributes: {
preload: 'metadata',
},
hlsOptions: {
maxBufferLength: 60,
maxMaxBufferLength: 180,
liveSyncDuration: 16,
liveMaxLatencyDuration: 32,
maxBufferLength: 30,
maxMaxBufferLength: 90,
backBufferLength: 0,
maxBufferSize: 30 * 1000 * 1000,
liveSyncDuration: 8,
liveMaxLatencyDuration: 16,
},
},
};
@@ -2884,7 +2892,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
disabled={panelInfo.modal}
onEnded={onEnded}
noAutoPlay={cannotPlay}
autoCloseTimeout={3000}
autoCloseTimeout={6000}
onBackButton={onClickBack}
spotlightDisabled={panelInfo.modal}
isYoutube={isYoutube}