저사양 Android — 아바타 pause() 후에도 DECODE_OVERLOAD 지속, <video> 언마운트로 디코더 해제원인분석구현완료·검증대기

마지막 업데이트 2026-07-24

저사양 Android — 아바타 pause() 후에도 DECODE_OVER…: 증상: 요약 (TL;DR), 원인: DECODE_OVERLOAD 로그 근거 데이터 확정, 수정·검증: 수정 (PPI-1162) 구현 완료 · 실기기 검증 대기 흐름
동작 흐름 요약
  1. 증상: 요약 (TL;DR)
  2. 원인: DECODE_OVERLOAD 로그 근거 데이터 확정
  3. 수정·검증: 수정 (PPI-1162) 구현 완료 · 실기기 검증 대기
2026-07-24 분석·구현 (PPI-1162) 세션 2026-07-22 20:03~20:23 KST 두부핑퐁 시즌2 2회기 (하랑대화) roomId 3f7a215a…59a2c_2 · lessonIndex 2 ppi-prod

요약 (TL;DR)

증상: 자료(활동) 영상 재생 중 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로 미확정(아래 주의 참조).

시간순 인과 다이어그램

[저사양 4GB Android · Chrome 150 · isLowPerformanceDevice:true]
    │
자료 영상 재생 (blob 완전 다운로드 · networkState idle · bufferedAhead 39~51s)
    │
아바타 idle/talking pause() (PPI-1143 상시 ON)  ──▶  Android MediaCodec 슬롯 미해제(가설)
    │                                          │ (idle+talking <video> 마운트 유지 · src 점유)
    └──────────────────────────┬───────────────────────────┘
                             ▼
자료 영상 디코더가 HW 슬롯/스루풋 확보 실패 (SW 폴백 or 스래싱)
                             ▼
재생 내내 ~50~65% 프레임 드롭 (readyState 2 · buffered 충분)
                             ▼
waiting 이벤트 반복 → DECODE_OVERLOAD 67건 (전 영상 균일)

세션 환경 (로그 확정)

구분
아동 기기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건

DECODE_OVERLOAD 로그 근거 데이터 확정

[MEET_VIDEO] Video waiting [DECODE_OVERLOAD] 이벤트 67건(grep 134 = 로그문+필드 중복 카운트). 특정 영상·특정 스텝·AI 전환과 무관하게 전 구간 균일하게 발생.

영상스텝이벤트피크 드롭
step_01분수대 이동162/141 (44%)
step_02분수대 다행11791/1578 (50%)
step_05다행영상14878/1459 (60%)
step_06우주→하랑3238/372 (64%)
step_07하준찾음7689/1593 (43%)
step_08모카축하9584/900 (65%)
ending_01엔딩10808/1888 (43%)
epilogue_01에필로그12720/1423 (51%)

DECODE_OVERLOAD로 분류되는 조건

// 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 혼재해도 패턴 동일 → 네트워크 무관 확증.

근본 원인 — pause() ≠ MediaCodec 해제 (Android) 유력 가설

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에 남는다.

자료 영상 재생 → HW MediaCodec 슬롯 요청
    │
idle + talking 이 슬롯 점유 중(pause여도 미해제) → HW 슬롯 부족
    │  동시 디코더 요청 최대 3개 (idle 아바타 + talking 아바타 + 자료 영상)
    ▼
자료 영상이 SW 디코드로 폴백(또는 HW 스래싱) → 실시간 디코딩 실패
선행 근거(PPI-1143 실기기 검증): "크롬/사파리가 display:none 영상 디코드를 자동 중단할 것"이라는 예상과 달리, 노트북·iPad 모두 숨김 아바타를 ~60fps로 디코딩 중이었고 pause() ON이 이를 제거했다. 즉 "숨김만으로는 디코드가 안 멈춘다"는 이미 실측된 사실. 이번 건은 일부 Android에선 pause()로도 부족하다는 한 단계 더 나아간 케이스다.

왜 PPI-1143 pause()만으론 부족했나

단계동작이 기기에서의 실제
PPI-1143숨김 아바타 pause() 상시 ONpause는 됐지만 MediaCodec 슬롯 미해제(가설) → 디코드 부하 잔존
PPI-1162저사양 기기 숨김 아바타 <video> 언마운트element 제거 → src 해제 → 디코더 teardown → 슬롯 반환
콘텐츠 해상도 미확정 (중요): 이 기기는 isLowPerformanceDevice:true라 PPI-1143 배선대로면 콘텐츠가 480p여야 한다. 그러나 이 LogRocket export에는 videoWidth/Height가 없고, 리소스 캐시 키는 base 파일명이라(getResourceUrlisLowPerformanceDevice를 서버로 보내 서버가 480p/720p 키를 결정) 로그의 파일명만으로 실제 해상도를 구분할 수 없다. 만약 콘텐츠가 여전히 720p라면(예: new03_480p 변형 부재), 지배적 레버는 480p 전달이고 PPI-1162(아바타 언마운트)는 보완책이다. 실기기에서 videoWidth / 서버 응답 S3 키로 반드시 확인할 것.

수정 (PPI-1162) 구현 완료 · 실기기 검증 대기

Before — 숨김 시 idle/talking <video>가 마운트 유지된 채 pause()만 됨 → 일부 Android에서 MediaCodec 슬롯 미해제 → 자료 영상과 동시 디코드 경합
After — 저사양 기기 한정, 숨김 시 idle/talking <video>를 언마운트해 src 해제·디코더 teardown. 고성능 기기는 기존대로 마운트 유지 + pause(복귀 즉시 재개, 재버퍼 회피)

1. 저사양 기기 아바타 영상 언마운트 meet-avatar.tsx

// isVisible=false && 저사양이면 언마운트 → src 완전 해제 = MediaCodec teardown
// 리소스는 Cache API blob URL이라 복귀 시 remount해도 네트워크 재다운로드 없음
const mountAvatarVideos = isVisible || !(props.isLowPerformanceDevice ?? false);

// JSX
{currentIdleVideo && mountAvatarVideos && (<video ... />)}
{currentTalkingVideo && mountAvatarVideos && (<video ... />)}

2. RMS talking 자동재생 보완 use-avatar-rms.ts

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 후 매 프레임 재호출 없음.

구현 내역 PPI-1162 · PR #923

파일변경
apps/web/shared/ui/meet-avatar.tsxmountAvatarVideos = isVisible || !isLowPerformanceDevice 도입. 저사양 기기 숨김 시 idle/talking <video> 언마운트
apps/web/hooks/use-avatar-rms.tsRMS 루프에 !isTalking && !talkVid.pausedpause() 분기 추가 (remount autoPlay 시 발화 아닌데 재생되는 것 방지)

브랜치 PPI-1162 · 단일 커밋 · base develop · 라벨 web · PR #923. 타입 검사 통과(변경 파일 기준 신규 에러 0).

실기기 검증 체크리스트 미완

MediaCodec 슬롯 반환은 Android 실기기에서만 검증 가능(jsdom 단위테스트 실효 낮음). 확인 페이지: http://localhost:3000/client-guest/session.

  1. 디코더 반환: 문제 기기(4GB Android, isLowPerformanceDevice:true)에서 자료 영상 재생 시 chrome://media-internals로 아바타 디코더가 사라지고 자료 영상이 MediaCodecVideoDecoder(HW)를 유지하는지
  2. 드롭 감소: DECODE_OVERLOAD 건수·프레임 드롭율이 pause-only 대비 감소하는지
  3. talking 미재생: 아바타 복귀 후 침묵 구간에 talking 영상이 재생되지 않는지, 발화 시 립싱크 정상 재개되는지
  4. 콘텐츠 해상도: 자료 영상 videoWidth가 854(480p)인지 1280(720p)인지 — 720p면 480p 전달 갭 별도 해결 필요(아래 관련 문서)
단정 금지: 코드 적용만으로 "해결됨"이라 하지 말 것. 실기기 4항목 통과 후 판단. 콘텐츠가 720p로 확인되면 PPI-1162는 부분 완화이며 480p 배선(PPI-1143 계열)이 지배적 레버.

판별법 (다음 동일 신고 시)

관련 문서