마지막 업데이트 2026-07-24
증상: 자료(활동) 영상 재생 중 DECODE_OVERLOAD WARN이 67건 반복 발생. 모든 영상에서 재생 내내 프레임을 ~50~65% 버려 실질 ~15fps로 끊김.
맥락: "자료 영상 재생 중 아바타 영상 pause()" 최적화(PPI-1143, 상시 ON)가 이미 적용된 뒤에도 이 세션에서 DECODE_OVERLOAD가 발생.
근본 원인(유력): 안드로이드 일부 기기에서 HTMLVideoElement.pause()는 재생 클럭만 멈출 뿐 MediaCodec 인스턴스·HW 디코더 슬롯을 해제하지 않는다. 아바타 idle/talking <video>가 src를 문 채 DOM에 남아 슬롯을 점유 → 자료 영상 디코더가 HW 슬롯/스루풋을 확보하지 못해 과부하.
수정(PPI-1162): isLowPerformanceDevice일 때 아바타 숨김 시 idle/talking <video>를 언마운트해 디코더를 실제로 teardown. remount 시 autoPlay로 talking이 발화 아닌데 재생되는 사이드 이펙트는 RMS 루프에서 보완.
확정 vs 가설: 저사양 판정(isLowPerformanceDevice:true)·드롭율·완전버퍼는 로그 확정. "pause가 MediaCodec을 안 놓는다 → SW 폴백/슬롯 고갈"은 이 기기 media-internals 미확인 유력 가설. 콘텐츠 실제 해상도(480p/720p)는 이 export로 미확정(아래 주의 참조).
| 구분 | 값 |
|---|---|
| 아동 기기 | Android 10 · Linux armv81 · Chrome 150 · deviceMemory 4GB · hardwareConcurrency 8 · isLowPerformanceDevice: true ([LowPerf] result 로그 확증) |
| 네트워크 | effectiveType 4g · downlink 1.5Mbps · rtt 50~200ms — 재생과 무관(blob 완전 다운로드, networkState idle) |
| 경로 | V2 /client-guest/session · roomId 3f7a215a-b8ba-4e5d-888e-0123bbf59a2c_2 · lessonIndex=2 |
| 콘텐츠 | 두부핑퐁 시즌2 2회기(하랑대화) · avatar pingpong03 · 영상 video_pingpong_new03_* |
| 세션 시각 | 2026-07-22 11:03~11:23 UTC (20:03~20:23 KST) · 약 20분 |
| 소스 | LogRocket export · appID tbwewz/ppi-prod · logEntries 9,391건 |
[MEET_VIDEO] Video waiting [DECODE_OVERLOAD] 이벤트 67건(grep 134 = 로그문+필드 중복 카운트). 특정 영상·특정 스텝·AI 전환과 무관하게 전 구간 균일하게 발생.
| 영상 | 스텝 | 이벤트 | 피크 드롭 |
|---|---|---|---|
| step_01 | 분수대 이동 | 1 | 62/141 (44%) |
| step_02 | 분수대 다행 | 11 | 791/1578 (50%) |
| step_05 | 다행영상 | 14 | 878/1459 (60%) |
| step_06 | 우주→하랑 | 3 | 238/372 (64%) |
| step_07 | 하준찾음 | 7 | 689/1593 (43%) |
| step_08 | 모카축하 | 9 | 584/900 (65%) |
| ending_01 | 엔딩 | 10 | 808/1888 (43%) |
| epilogue_01 | 에필로그 | 12 | 720/1423 (51%) |
// meet-video.tsx diagnoseWaitingCase — waiting 이벤트 시 호출 if (document.visibilityState === "hidden") return "TAB_BACKGROUNDED"; if (isRecentSeek) return "SEEK_REBUFFER"; if (corruptedVideoFrames > 0) return "CODEC_CORRUPT"; if (bufferedAheadSec > 1 && droppedVideoFrames > 10) return "DECODE_OVERLOAD"; if (bufferedAheadSec > 1) return "DECODE_OVERLOAD"; // 버퍼 충분한데 정지 = 디코드 병목
판정 근거: 모든 이벤트가 readyState 2(HAVE_CURRENT_DATA) · networkState 1(IDLE) · bufferedAheadSec 39~51초. 즉 데이터는 이미 다 받았는데 다음 프레임을 못 만들어 waiting이 뜬 것 = 네트워크가 아니라 디코더 병목. rtt 50/200 혼재해도 패턴 동일 → 네트워크 무관 확증.
HTMLVideoElement.pause()는 재생 클럭만 멈춘다. Chromium(Android WebView/Chrome)에서 실제 디코더는 GPU 프로세스의 MojoVideoDecoder → MediaCodec인데, 이건 element에 src/currentSrc가 붙어 있고 DOM에 마운트된 동안 계속 할당된다. pause 시에도 MediaCodec 인스턴스·입출력 버퍼·예약된 codec HW 슬롯/Surface가 그대로 남는다.
meet-avatar.tsx는 idle / talking 두 개의 <video>를 currentIdleVideo/currentTalkingVideo만 있으면 항상 마운트한다. isVisible은 CSS·play/pause만 제어할 뿐, 숨김 시에도 element는 src를 문 채 DOM에 남는다.
| 단계 | 동작 | 이 기기에서의 실제 |
|---|---|---|
| PPI-1143 | 숨김 아바타 pause() 상시 ON | pause는 됐지만 MediaCodec 슬롯 미해제(가설) → 디코드 부하 잔존 |
| PPI-1162 | 저사양 기기 숨김 아바타 <video> 언마운트 | element 제거 → src 해제 → 디코더 teardown → 슬롯 반환 |
// isVisible=false && 저사양이면 언마운트 → src 완전 해제 = MediaCodec teardown // 리소스는 Cache API blob URL이라 복귀 시 remount해도 네트워크 재다운로드 없음 const mountAvatarVideos = isVisible || !(props.isLowPerformanceDevice ?? false); // JSX {currentIdleVideo && mountAvatarVideos && (<video ... />)} {currentTalkingVideo && mountAvatarVideos && (<video ... />)}
remount 시 <video autoPlay>가 자동 재생을 시작하는데, RMS 루프는 isTalking=false면 pause 분기를 타지 않아 발화 아닌데도 talking이 계속 디코딩되는 사이드 이펙트. RMS 루프에 정지 분기 추가.
if (smoothRMS >= p.rmsThr && !isTalking) { isTalking = true; talkVid.play().catch(()=>{}); } else if (isTalking && smoothRMS < p.rmsThr * 0.4) { isTalking = false; talkVid.pause(); } else if (!isTalking && !talkVid.paused) { talkVid.pause(); } // remount autoPlay 대응
이 분기는 RMS 활성 경로에만 영향(비활성이면 훅 early-return). remount뿐 아니라 최초 마운트 시 talking이 발화 전 재생되던 잠재 케이스도 함께 정리하는 자기수정형. !talkVid.paused 가드로 한 번 pause 후 매 프레임 재호출 없음.
| 파일 | 변경 |
|---|---|
| apps/web/shared/ui/meet-avatar.tsx | mountAvatarVideos = isVisible || !isLowPerformanceDevice 도입. 저사양 기기 숨김 시 idle/talking <video> 언마운트 |
| apps/web/hooks/use-avatar-rms.ts | RMS 루프에 !isTalking && !talkVid.paused → pause() 분기 추가 (remount autoPlay 시 발화 아닌데 재생되는 것 방지) |
브랜치 PPI-1162 · 단일 커밋 · base develop · 라벨 web · PR #923. 타입 검사 통과(변경 파일 기준 신규 에러 0).
MediaCodec 슬롯 반환은 Android 실기기에서만 검증 가능(jsdom 단위테스트 실효 낮음). 확인 페이지: http://localhost:3000/client-guest/session.