마지막 업데이트 2026-07-22
[MEET_VIDEO] 영상 종료 후 다음 스텝 자동 전환 실패 원인 분석 및 수정 기록게스트(아동) 수업 중 일부 단말에서 영상이 재생된 뒤 종료 이벤트가 발생하지 않아, 다음 스텝으로 자동 전환되지 못하고 멈추는 현상이 보고되었다. 호스트(치료사)가 수동으로 다음 스텝으로 넘겨야 진행이 되었다.
[MEET_VIDEO] 로그 태그(apps/web/shared/ui/meet-video.tsx)를 중심으로, LogRocket 세션 JSON 3건을 시계열로 재구성하여 진행했다.
DECODE_OVERLOAD는 onWaiting 핸들러(diagnoseWaitingCase)에서만 찍히는 로그로, 다음을 의미한다.
<video>의 waiting 이벤트 = 다음 프레임을 못 그려 재생이 멈춤(화면 정지 + 오디오 정지).bufferedAheadSec > 1 — 즉 데이터는 이미 버퍼에 있는데 디코더가 못 따라간 상태. 네트워크 문제 아님 단말 성능/디코딩 과부하// meet-video.tsx — 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"; // ← 데이터 있는데 디코드 정체
...
return isBlob ? "BLOB_READ_STALL" : "NETWORK_STALL";
MeetVideo는 영상 종료(→ 다음 스텝 전환)를 다음 3경로로 보장하려 한다. 그런데 세 경로 모두 currentTime이 영상 끝에 도달해야 동작한다.
| 경로 | 트리거 조건 | stall 시 한계 |
|---|---|---|
① ended 이벤트 | 영상이 실제로 끝까지 재생됨 | stall로 끝에 못 가면 미발생 |
| ② near-end (timeUpdate) | currentTime ≥ duration − 0.1 | currentTime이 안 늘면 미발생 |
| ③ duration-timeout fallback | 재생 시작 시 타이머 예약 | 재스케줄 설계 결함 (아래) |
const remaining = video.duration - video.currentTime;
if (remaining > 0.3 && attempt < 5) {
scheduleCheck(remaining * 1000, attempt + 1); // ← 매번 "남은 재생시간"만큼 또 대기
return;
}
영상이 stall되어 currentTime이 거의 안 늘면 remaining도 안 줄어, 매 시도마다 다시 영상 길이만큼 대기한다. 최대 5회 재시도라 발동까지 걸리는 실제 시간이 ≈ (시도 횟수) × remaining으로 늘어나 — 이 폴백이 막아야 할 바로 그 stall 상황에서 발동이 한참 뒤로 밀린다. 그 사이 호스트가 먼저 수동 전환하면 폴백은 발동 기회를 잃는다.
| 케이스 | 같은 이슈? | 근거 |
|---|---|---|
| 아동324 6회기 | 맞음 | 전환영상(video_tiki_bridge_end_event_0)에서 32초간 DECODE_OVERLOAD 반복 → 종료 이벤트 3경로 모두 미발생 → 호스트 수동 전환(17:46:15) |
| 아동040 31회기 | 맞음 | 인트로영상에서 DECODE_OVERLOAD stall → 종료 이벤트 미발생 → 호스트 수동 전환(19:32:24). 아동324와 동일 메커니즘 |
| 아동308 4회기 | 아님(현상만 유사) | DECODE_OVERLOAD는 동일 발생했으나, 영상 종료 fallback(duration-timeout / near-end)이 정상 작동해 자동 진행. 첫인사 stall은 AI 대화 스텝이라 영상 종료에 의존하지 않음 |
| 시각 | 이벤트 |
|---|---|
| 17:45:36.426 | Video play (이후 play 이벤트 없음) |
| 17:45:36 ~ 17:46:08 | Video waiting [DECODE_OVERLOAD] ↔ canplay 6회 반복 (0.5~4초씩 정지) |
| — | ended / near-end / duration-timeout 전부 없음 |
| 17:46:15.413 | [GUEST_PAGE_SESSION] Step changed by host → 호스트 수동 전환 |
아니다. 아동324 단말은 저사양으로 정상 분류되었다.
[LowPerf] result: { isLowPerformanceDevice: True,
ua: 'Android 10; SM-P610', hardwareConcurrency: 8, deviceMemory: 4 }
deviceMemory > 0 && ≤ 4 → 저사양 Trueguest-layout-content.tsx에서 MeetVideo에 isLowPerformanceDevice prop을 전달하지 않는다(MeetAvatar에만 전달). 프리캐시 히트 시엔 480p가 반환되어 무해하나, 캐시 미스 시 풀해상도가 서빙될 수 있다.
결론: 분류·480p 다운그레이드 경로를 모두 거쳤음에도 끊긴 것 → 근본 원인은 저사양 단말이 동시 부하(영상 디코드 + mediasoup + OpenAI Realtime + AI_ENGLISH_GUARD MFCC 2599회)를 못 버틴 것.
아동324 세션은 NAVER Android 인앱 웹뷰 안에서 실행됐다(LogRocket "NAVER, Android" 판별). 이는 디코딩 악화 요인으로 작용한다.
| 요인 | 인앱 웹뷰(NAVER) | 일반 Chrome |
|---|---|---|
| 프로세스 구조 | 호스트 앱 프로세스 내 단일 렌더러, 메모리 공유 | 멀티프로세스 + 전용 GPU 프로세스 |
| 영상 하드웨어 디코딩 | WebView 빌드/설정 따라 제한·SW 폴백 가능 | 풀 하드웨어 가속 |
| 동시 미디어 부하 | 좁은 자원 안에서 경쟁 | 분산 처리 |
Chrome/…이 포함돼 기존 브라우저 판정이 이를 "Chrome"으로 오판 → 브라우저 경고도 안 뜨고, 카카오 전용 인앱 탈출 로직에도 안 걸려 — 경고·탈출 둘 다 빠져나간 채 불안정한 웹뷰 환경으로 그대로 진입했다.
로그 보강 증거: video setSinkId failed: A user gesture is required — 인앱 웹뷰의 엄격한 미디어/제스처 정책의 전형적 증상.
카카오 전용이던 인앱 브라우저 탈출 로직을 일반화하여 NAVER 인앱 브라우저도 외부 브라우저(Chrome)로 리다이렉트하도록 했다. 카카오 동작은 100% 보존.
| 파일 | 변경 |
|---|---|
lib/user-agent-utils.ts | detectInAppBrowser() 추가(kakaotalk/naver 판정). 인앱을 Chrome으로 오판하지 않도록 parseBrowser 선판정 → "네이버/카카오톡 인앱 브라우저"로 표기 → isRecommendedBrowser=false |
hooks/use-in-app-browser-redirect.ts 신규 | 기존 카카오 훅 일반화. 카카오+NAVER 모두 탈출. iOS Chrome 미설치 등 탈출 실패 시 "이동 중" 화면에 갇히지 않도록 폴백 |
client-guest/in-app-browser-redirect-guard.tsx 신규 | 가드 컴포넌트 일반화 (구 kakao-redirect-guard 대체) |
client-guest/layout.tsx, pages/guest-entry.tsx | 새 훅/가드로 교체 |
삭제 use-kakao-browser-redirect.ts, kakao-redirect-guard.tsx | 일반화로 대체 |
export function detectInAppBrowser(userAgent: string): InAppBrowser {
if (!userAgent) return null;
const ua = userAgent.toUpperCase();
if (ua.includes("KAKAOTALK")) return "kakaotalk";
// 정식 브라우저 NAVER Whale("Whale/")과 구분하기 위해 inapp 토큰으로만 판정
if (ua.includes("NAVER(INAPP")) return "naver";
return null;
}
Build/Q…로 잘려 토큰 원문 직접 확인 불가. LogRocket "NAVER" 판별 + 표준 토큰(NAVER(inapp;) 기준으로 작성. 실제 단말 UA로 최종 검증 권장.; wv))는 의도적 제외: 리다이렉트 루프·오탐 위험으로 NAVER만 추가. (인스타/페북/라인 등 확장 가능)remaining*1000 재스케줄 대신 벽시계 기준 절대 데드라인(재생 시작 + duration + buffer)으로 단일 타이머화 → stall 상황에서도 확실히 onEnd 발동.MeetVideo에 isLowPerformanceDevice 전달 누락 수정(캐시 미스 시 풀해상도 서빙 방지).page.tsx, log-user-agent.ts, logrocket-utils.ts)가 git add -A로 함께 포함되었다.