저사양 Android 720p 컨텐츠 DECODE_OVERLOAD — lowPerf 플래그 미배선 두 실패 모드원인규명구현완료·검증중

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

2026-07-20 원인규명 2026-07-21 구현·검증 (PPI-1143) 아동 정이안 · 두부핑퐁 시즌2 3회기 roomId 1b0a2d94…abd08a_5 ppi-prod

요약 (TL;DR)

증상(VOC): 수업 중 반복적으로 멈춤·끊김·지연이 발생하고, 양육자가 위치를 옮겨도 매번 재발.

근본 원인: 컨텐츠 활동영상이 저사양 기기(RAM 2GB Android)에서도 480p가 아니라 720p 원본으로 재생됐다. 저사양 게이팅(isLowPerformanceDevice)이 MeetVideo·MeetImage에는 배선돼 있지 않고 아바타에만(그것도 split 모드에서만) 연결돼 있기 때문. 720p@30fps는 H.264 Level 3.1 한계치라 2GB 기기 디코더가 감당 못 해 프레임을 31%까지 버렸다.

두 실패 모드: ① 아바타 없는 긴 영상 = 진행성 과부하(31% 드롭·완주), ② 아바타 있는 영상 = 4~5초 하드 프리즈(숨겨진 아바타 동시 디코딩이 가중). "위치 옮겨도 재발"은 원인이 네트워크가 아니라 기기 디코드라서.

진행자 녹화 멈춤은 별개 원인: 진행자(8GB 정상 기기)의 우주방소개영상 멈춤은 디코드 과부하가 아니라 버퍼 starvation(bufferedAhead 0·dropped 0)이며 게스트 kick보다 먼저 발생 — 게스트 디코드 이슈의 반영이 아닌 모니터 자체 이슈.

시간순 인과 다이어그램

[아동 2GB Android] [진행자 8GB Windows]
     │ │
컨텐츠 720p 재생 (MeetVideo lowPerf 미배선) 컨텐츠 720p 재생 (동일 미배선)
     │ 720p@30 = H.264 Lv3.1 한계(여유0) │
 ┌───┴──────────────┐ 모니터가 story_02 데이터
 │ │ 버퍼링 못함(bufferedAhead=0)
인트로(아바타X) 아바타 있는 영상: │ (재접속 churn 추정)
31% 드롭·진행성 4~5s 하드 프리즈
 │ (아바타 동시 디코딩 가중) 진행자 화면 story_02 정지
 └────────┬─────────┘ (화면녹화에 포착, 원인 별개)
        ▼
수업 내내 멈춤·끊김 (VOC 신고)
        │
  + 네트워크 저하(0.35Mbps): 로딩 46~80초 지연 + story_02 59초 정지 후 kick

세션 환경 (확정)

구분
아동 기기Android 10 · armv81 · Chrome 150 · deviceMemory 2GB · hardwareConcurrency 8 · isLowPerformanceDevice: true
진행자 기기Windows 10 x64 · Chrome 150 · deviceMemory 8GB · hardwareConcurrency 8
네트워크(아동)downlink 0.35Mbps · transport RTT 만성 30~70ms · 피크 1984ms
서버/SFU정상 (room 스코프 error/warn 0건, 진행자 RTT 시종 <100ms)
세션 시각2026-07-20 16:33~16:54 KST (07:33~07:54 UTC)

근본 원인 1 — 컨텐츠 영상이 720p로 재생됨 확신 높음

isLowPerformanceDeviceguest-layout-content.tsx에서 단 한 곳에서만 쓰인다 — 아바타에, 그것도 split 모드에서만.

// guest-layout-content.tsx:203 — 유일한 사용처
<MeetAvatar isLowPerformanceDevice={isSplitMode && isLowPerformanceDevice()} />

// MeetVideo / MeetImage 에는 전달되지 않음 → props.isLowPerformanceDevice = undefined
<MeetVideo ... />   // isLowPerformanceDevice 없음
<MeetImage ... />   // isLowPerformanceDevice 없음

그 결과 meet-video.tsxgetResourceUrl(fileName, undefined)를 호출하고, 서버 /api/resources/urls는 480p 변형 대신 원본(720p)을 반환한다.

런타임 확증: 아동측 RESOURCE_CACHE 로그의 요청 파일명이 전부 _480p 접미사 없는 원본(video_pingpong_new02_story_01.mp4 등). 480p는 별도 파일(…_480p.mp4)로 존재하나 컨텐츠 경로에서 요청되지 않았다.

720p가 왜 2GB 기기를 무너뜨리나

720p 원본480p 대체본
해상도1280×720854×480 (픽셀 44.5%)
매크로블록 처리량 @30fps108,000 MB/s = Lv3.1 한계 (여유 0)≈48,600 MB/s (한계의 45%)
비디오 비트레이트~1.4 Mbps~0.8 Mbps
코덱H.264 Constrained Baseline / B-frame 0 (양쪽 동일, 이미 디코드 친화적)

두 파일의 실질 차이는 해상도(+연동 비트레이트)뿐. 720p는 저사양 디코더에 여유가 0이라 프레임을 버릴 수밖에 없다.

두 실패 모드 — 영상별 디코더 상황 로그 확증

아동측 MEET_VIDEO 전체 타임라인(2400 엔트리 전체 로그)에서 영상별로 두 가지 뚜렷이 다른 실패가 관측됐다.

영상활동/아바타결과드롭(프레임)패턴
story_01 인트로act0 / 아바타 없음완주(0→139.8s)738/2398 → 813/2523 (31~32%)진행성 과부하
story_02 우주방소개act1 / 아바타O3.9s에서 정지6→14 (6초간 7프레임=거의 0fps)하드 프리즈
bridge_01 브릿지act5 / 아바타 없음정상 완주(10s)정상
tutorial_01 튜토리얼act6 / 아바타O4.955s에서 정지14→28하드 프리즈
gacha_06 뽑기영상act8 / 아바타Ocanplay 46.6초 대기 후 완주로딩 지연
ending_01 엔딩act12 / 아바타O3.886s에서 정지15→34하드 프리즈
ending_02 에필로그act12 / 아바타Ocanplay 79.9초 + 4.75s 정지14→14 (완전 정지)로딩지연+프리즈

① 진행성 과부하 (인트로 story_01, 아바타 없음)

프레임을 31% 버리면서도 영상은 계속 전진(81s→85s→완주). 순수 720p 디코드 스루풋 부족. 아바타가 없는데도 발생 → 720p 컨텐츠 자체가 이 기기엔 과함을 단독 입증.

② 하드 프리즈 (아바타 있는 영상)

재생 4~5초 지점에서 완전 정지 — stall 6~7초 동안 디코더가 거의 0프레임만 생산(ending_02는 14→14, 완전 정지). 스루풋 드롭이 아니라 디코더/메인스레드 hang. 하드 프리즈는 전부 아바타 있는 스텝에서 발생 → 컨텐츠 720p + 숨겨진 아바타 idle/talking 동시 디코딩 + avatarState 변경 시 아바타 video 재생성 churn이 메인스레드를 얼린 것으로 추정(상관 강함, 인과는 가설).

waitingCase 판정 로직

// meet-video.tsx:654-677 diagnoseWaitingCase
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";                          // 데이터 있는데 정지 = hang 포함
if (video.currentTime < 1) return "INITIAL_LOAD";
return isBlob ? "BLOB_READ_STALL" : "NETWORK_STALL";
주의: DECODE_OVERLOAD 라벨은 "버퍼에 데이터가 있는데 정지"를 뜻할 뿐 GPU 포화와 메인스레드 정지를 구분하지 못한다. 드롭 프레임 수(진행성=높음, hang=거의 0)로만 구분 가능.

진행자 녹화 멈춤 — 별개 원인(버퍼 starvation) 확신 중~높음

서버 세션 녹화는 오디오 전용 mp3(아동 마이크 + 핑퐁이 TTS만, recordingManager.ts)라 비디오 트랙이 없다. 따라서 "진행자 녹화 영상"은 서버 녹화물이 아니라 진행자 화면 직접 녹화이고, 모니터는 컨텐츠 mp4를 자기 기기에서 독립 로컬 디코드한다.

07:37:03 [MEET_VIDEO] Video waiting | story_02 | bufferedAheadSec:0, dropped:0/12
07:37:05 [MEET_VIDEO] Video playback stalled (timeupdate gap) | story_02 | bufferedAheadSec:0, dropped:0/12
→ currentTime 0.1s에 얼어 07:39:51까지 정지 (게스트 kick 이전)
07:39:52 게스트 force kick → 진행자 peer 재생성·재렌더 → 07:40:11 pause (프리즈보다 늦음)

시그니처: bufferedAheadSec 0 + dropped 0 + 8GB 정상 기기 → 디코드 과부하가 아니라 데이터가 없어 재생 못 함(buffer/fetch starvation). 재접속 churn으로 모니터가 story_02 blob을 버퍼링하지 못한 것으로 추정.

게스트와 독립: 진행자는 0.1s, 게스트는 3.9s에서 서로 다른 시각·위치에 정지 → 두 플레이어가 독립 로컬 디코드임을 재확인. 진행자 멈춤은 게스트 디코드 이슈의 반영이 아니다.

신뢰도: 정확한 ms/프레임 수치는 LogRocket AI 요약이라 저신뢰. 정성 결론(bufferedAhead≈0, dropped≈0, kick 전 정지)은 robust.

부수 요인 — 네트워크 저하

아동 액세스망 downlink 0.35Mbps, transport RTT 피크 1984ms. 서버·SFU는 정상(진행자 RTT <100ms). 이 저하가 재생 자체를 멈추게 하진 않지만(blob은 재생 전 완전 다운로드) 다음에 기여:

  • 스텝 전환 후 canplay까지 46~80초 로딩 지연 (gacha 46.6s, ending_02 79.9s)
  • story_02 하드 프리즈 구간(59초)에 이어진 순단·재입장(recording 세그먼트 2분할은 정상 동작)

해결책 구현 완료 (PPI-1143)

우선수정효과상태
1 (핵심)MeetVideo·MeetImage에 isLowPerformanceDevice 배선 → 컨텐츠 720p→480p아동측 진행성 과부하 직접 해결. 아바타 유무 무관 모든 영상 스텝 커버구현 · 480p 전환은 2GB Android 미검증
2아바타 480p 게이트에서 isSplitMode 제거 + 숨김(!isVisible) 아바타 idle/talking pause() + RMS 중단영상 스텝의 아바타 동시 디코딩 제거(하드 프리즈 완화)구현·검증완료 (노트북·iPad)
3모니터 리소스 프리캐싱/재접속 시 버퍼 유지 로직 점검진행자 녹화 멈춤(별개 트랙) 해결미착수

실제 구현 (배선)

BeforeisLowPerformanceDevice가 아바타 split 모드에만 연결 → MeetVideo/MeetImage는 undefined → 컨텐츠 720p 원본
After — MeetVideo·MeetImage에 isLowPerformanceDevice() 게이트 없이 전달 + 아바타는 isSplitMode 제거 + isVisible prop으로 숨김 시 pause
주의: 서버 /api/resources/urlsfileType === "video"에만 480p를 적용한다. 이미지는 현재 480p 변형이 없어 배선해도 no-op(향후 대비 배선만). 그리고 480p 전환의 실효(드롭% 개선)는 2GB급 Android 실기기 재측정 필수 — 코드만으로 "해결됨" 단정 금지.

구현 내역 PPI-1143

최종 머지에 남은 코드는 아래 3파일뿐이다(검증용 HUD/오버라이드는 제거 — 아래 표 하단 참조).

파일변경
shared/ui/guest-layout-content.tsxMeetVideo·MeetImage·MeetAvatar에 isLowPerformanceDevice() 전달, 아바타 isSplitMode 게이트 제거, isVisible={isAvatarVisible}(pause 상시 ON)
shared/ui/meet-image.tsxisLowPerformanceDevice prop 추가 → getResourceUrl(fileName, flag) (이미지는 현재 서버상 no-op)
shared/ui/meet-avatar.tsxisVisible prop 추가. 숨김 시 idle/talking pause() + RMS(rmsEnabled) 중단. 표시 복귀 시 idle play(), talking은 RMS 켜진 경우 RMS 루프에 위임(침묵 중 불필요 디코딩 방지)

최종 커밋 473de048 (브랜치 PPI-1143, 3파일 32줄). 단일 커밋. 코드리뷰 반영(resume 시 talking RMS 위임) 포함.

검증 후 제거된 임시 파일: lib/decode-optimization.ts(런타임 튜너블), shared/ui/decode-debug-hud.tsx(디코드 부하 HUD), lib/utils.ts?lowPerf 오버라이드, app/client-guest/page.tsx의 디버그 파라미터 전파 — 모두 실기기 A/B 검증용으로만 도입 후 제거됨. 커밋 이력에 존재.

실기기 검증 — 아바타 pause 2026-07-21

동일 환경에서 아바타 스텝 → 영상 스텝(아바타 숨김) 이동 후, avatarPause OFF vs ON을 HUD로 실측. 핵심 지표는 전체 디코드 fps 합(= 실제 디코딩 중인 프레임/초).

노트북 (Chrome · mem 32GB · core 14)

지표pause OFFpause ON차이
아바타 재생중 · fps합2개 · 59.90개 · 0.0−59.9fps
전체 디코드 fps 합89.930.0−60fps (약 67%↓)
컨텐츠 드롭4/872 (0.5%)0/866 (0.0%)미세 감소

iPad (WebKit)

지표pause OFFpause ON차이
아바타 재생중 · fps합2개 · 59.80개 · 0.0−59.8fps
전체 디코드 fps 합89.630.9−58.7fps (약 66%↓)
컨텐츠 드롭0/1071 (0.0%)0/1011 (0.0%)둘 다 0
결론: 노트북·iPad 양쪽에서 숨김 아바타가 ~60fps로 실제 디코딩 중이었고, pause ON이 이를 완전히 제거(전체 89→30fps). 아바타 pause 최적화 실효 검증 완료.
중요 발견: "크롬/사파리가 display:none 영상 디코드를 자동 중단할 것"이라는 예상과 달리, 양쪽 모두 숨김 아바타를 디코딩하고 있었다(fps합 ≈60). RMS 립싱크 canvas 합성이 hidden 상태에서도 프레임을 강제로 끌어와 디코드를 유발한 것으로 추정 → pause 최적화가 크롬·WebKit 모두에서 유효.
미검증: 두 기기 모두 컨텐츠 드롭 ≈0(720p 여유 있음)이라 Fix 1(480p)·하드 프리즈 개선은 재현 안 됨. 저사양 2GB Android 실기기에서 별도 확인 필요.

검증 도구 — 디코드 부하 HUD 검증 후 제거됨

제거됨: 아래 HUD·런타임 오버라이드(debugDecode·lowPerf·avatarPause)는 실기기 검증 전용 임시 도구로, 검증 완료 후 최종 머지에서 제거됐다. 현재 코드에선 ?debugDecode=1 등이 동작하지 않으며, 아바타 pause는 상시 ON, 480p는 자동 저사양 판정으로 적용된다. 아래는 검증 방법의 기록이며, 필요 시 커밋 이력(PPI-1143 HUD 도입 커밋)에서 되살릴 수 있다.

배포 없이 실기기에서 A/B 하기 위한 런타임 오버라이드 + 화면 내 수치 오버레이. 우선순위: URL 쿼리 > localStorage > 기본값.

// 진입 (mode=test면 세션까지 파라미터 전파)
/client-guest?mode=test&debugDecode=1

// 파라미터
debugDecode=1        # HUD 표시 (필수)
lowPerf=on|off       # 480p 강제 ON / 720p 강제 (없으면 auto 판정)
avatarPause=on|off   # 숨김 아바타 pause 최적화 (기본 on)

HUD 하단 토글 버튼은 localStorage에 기록 후 현재 URL을 새로고침하므로, 세션 안에서 파라미터 재입력 없이 즉시 A/B 가능하다.

HUD 지표 읽는 법

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

관련 문서