영상 currentTime 앞으로 점프 (SEEK_REBUFFER) → near-end 조기 종료 BUG ROOT CAUSE 앱코드 무관

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

분석 세션: 김서연 4회기 · 2026-07-15 · 김서연_20260715_4회기.json (LogRocket) roomId: 1a61d747-5602-48c6-af74-40eebe095252_4 · appID: tbwewz/ppi-prod 기기: Android 10 / Chrome 150 Mobile / RAM 4GB / 8코어 (저사양) 영상: video_newpingpong04_newstep_02.mp4 (duration 52.97s, 30fps) · 활동2/step0/full:video 관련 파일: apps/web/shared/ui/meet-video.tsx, apps/web/shared/lib/video-end-watchdog.ts

증상 BUG

52.97초짜리 영상이 재생 시작 24.7초 만에 종료 처리됨 (약 28초 조기).
재생 도중 currentTime이 약 1.8초 → 33.8초로 순간 점프(약 32초 건너뜀)하면서, 위치 시계가 끝(52.97s)에 실제 재생 시간보다 훨씬 일찍 도달. 그 시점에 near-end fallback이 onEnd을 호출해 다음 스텝으로 자동 전환됨. 아동은 영상 중간부(약 2~34초 구간)를 보지 못함.
결론: 워치독/near-end 로직의 버그가 아니라, 저사양 Android 기기의 미디어 파이프라인이 스스로 낸 forward seek(currentTime 클럭 점프)가 원인. 앱 코드·영상 파일·네트워크·탭 백그라운드·메인스레드 프리즈는 모두 로그로 배제됨.

시간순 인과 타임라인 (UTC)

11:39:44.736  활동 1→2 자동 전환 (AI 작별 발화 "…그러면 안녕." 직후) → full:video 스텝 진입
              └ 재접속/새로고침 없음, 정상 auto-transition
11:39:47.485  RESOURCE_CACHE cached → blob buffered [0, 52.97] 전체 버퍼링 (네트워크 아님)
11:39:47.546  loadedmetadata: duration=52.97
11:39:47.719  play, currentTime=0  →  watchdog.arm (firstDelay≈52.97s → 첫 tick ~11:40:40 예약)
11:39:48.168  AUDIO_PROBE currentTimeSec=0.277 (정상)
11:39:49.723  Video audio health currentTime=1.802  (정상 1x, 2초 경과 = 2초 재생)
11:39:49.741  AI_SESSION Deferred connection cleanup (safety timeout)   ┐ 스텝 전환 직후
11:39:51.097  mic-audio outbound stats (producer-created)              ┘ AI teardown 경합
              ★ 이 3초 사이(49.7→52.7) currentTime 1.8s → 33.8s 점프 (약 32초)
11:39:52.722  WARN Video waiting [SEEK_REBUFFER]  currentTime=33.826
              buffered [0,52.97] · droppedFrames=4 / totalVideoFrames=149 · RAM 4GB / 8코어
11:39:52.987  canplay  currentTime=33.826  waitDurationMs=268
11:39:52.9~   33.8s → 52.97s 정상 1x 재생 (약 19.4초)
11:40:12.373  Video reached near-end threshold  currentTime=52.97 (≥ duration−0.1)
11:40:12.375  VideoEndWatchdog silenced (disarm, reason=near-end-fallback)
11:40:12.376  Video onEnd via near-end fallback → 다음 스텝 자동 전환
            
              실제 벽시계 소요: 24.66초  /  정상 재생 시: 52.97초  →  약 28초 조기 종료

핵심 증거 — 점프한 것은 "디코더"가 아니라 "위치 시계" ROOT CAUSE

SEEK_REBUFFER 시점의 getVideoPlaybackQuality() 수치가 결정적이다.

play 시작:        11:39:47.719
SEEK_REBUFFER:    11:39:52.722   → 벽시계 경과 = 5.0초
totalVideoFrames = 149            → 30fps × 5.0s ≈ 150프레임  =  디코더는 정상 5초치만 생성
그런데 currentTime = 33.826       → 위치 시계가 디코딩·벽시계보다 약 29초 앞섬
디코더가 뒤처진 게 아니라(=정상 30fps로 5초치 디코딩) 미디어 엘리먼트가 보고하는 재생 위치(currentTime)만 33.8초로 튀었다. 벽시계 3초 동안 위치가 32초 진행한 것은 배속 재생이 아니라 불연속 seek(위치 재배치)이며, native seeking 이벤트가 발생해 SEEK_REBUFFER로 분류된 것이 그 증거다.
SEEK_REBUFFER는 원인이 아니라 결과 라벨이다. diagnoseWaitingCase()는 "최근 3초 내 seeking 이벤트가 있었으면"(lastSeekTimeRef) waiting을 SEEK_REBUFFER로 분류한다. 즉 "건너뛰어서(seek) 그 뒤 재버퍼링이 SEEK_REBUFFER로 찍힌 것"이지, "SEEK_REBUFFER 때문에 건너뛴 것"이 아니다.

원인 배제표

후보판정근거
앱 / 소켓 / 호스트 seek❌ 배제guest 경로의 seek는 monitor 전용(meet-video.tsx:772, requestVideoElapsed)뿐 → "Monitor video seeking" 로그 0건. 소켓/호스트의 guest 영상 위치 조작 경로 없음
영상 파일 결함❌ 배제ffprobe 정상: 30fps · 1589프레임 · start_time 0 · aac. edit list는 media_time 2000/30000 = 0.067초 미세 오프셋뿐
네트워크❌ 배제blob 전체 버퍼링 [0, 52.97], bufferedAhead 19.1s+
탭 백그라운드❌ 배제점프 구간 visibilityState=visible. hidden 이벤트는 11:43 이후에만 존재
메인스레드 프리즈❌ 배제점프 구간(49.7→52.7)에 다른 로그(49.741·51.097)가 정상 발생 → JS 스레드 살아있음
기기 미디어 파이프라인 clock/seek 이상✅ 유력위 전부 배제 후 남는 원인. 저사양 Android(4GB)+Chrome, totalVideoFrames=149 vs currentTime 33.8s 괴리

왜 하필 이 순간이었나 (정황)

한계: Chromium 미디어 스택이 정확히 어떤 내부 규칙으로 currentTime을 33.8초로 튕겼는지는 애플리케이션 로그에 남지 않는다(엔진 내부 동작). 다만 위 모든 증거가 "저사양 Android 기기의 미디어 파이프라인 clock 이상" 한 곳으로 모인다. 우리 코드·파일·네트워크·앱 상태 문제가 아니라는 점은 로그로 확정된다.

VideoEndWatchdog은 오발화하지 않았다 정상

VideoEndWatchdog.arm()은 play 시점(currentTime≈0)에 firstDelay ≈ (duration−currentTime)×1000 ≈ 52.97s 뒤로 첫 tick을 예약했다(→ ~11:40:40). 그러나 그 전에 handleTimeUpdate의 near-end fallback(currentTime ≥ duration−0.1)이 11:40:12.373에 먼저 조건을 만족해 onEnd을 호출하고 워치독을 disarm("near-end-fallback")했다.

즉 로그에 찍힌 "VideoEndWatchdog silenced"는 워치독이 정상적으로 침묵(disarm)당한 기록이지 오발화가 아니다. "조기 종료"의 실제 방아쇠는 currentTime 점프 → near-end 조건 조기 충족이다. near-end은 currentTime을 신뢰하도록 설계됐고, 그 currentTime 자체가 기기 쪽에서 오염됐다.
대비되는 사례: DECODE_OVERLOAD 반복 stall 미발화 건은 정반대로, 긴 영상에서 currentTime이 끝에 도달하지 못해 워치독이 발화하지 못한 케이스다. 본 건(currentTime이 끝에 너무 일찍 도달)과 함께 보면, 두 실패 모드 모두 저사양 기기에서 currentTime과 벽시계가 어긋나는 것이 공통 뿌리다.

개선 방향 (검토용)

본 건은 기기측 미디어 스택 이상이 근본 원인이므로 앱 레벨은 "조기 종료를 완화"하는 방어에 그친다. 재발 시 동일 기기(4GB Android/Chrome)에서의 SEEK_REBUFFER 점프 크기를 대조해 "기기 고유 반복 결함"인지 확인 권장.

관련 문서