마지막 업데이트 2026-07-22
로그: 6-019ecf1f-528a-7498-a6f5-5a503e103772_0_06168ae1-…-8fca0dcf3266.json
· 디바이스: 8GB / 6코어 (정상 기기)
| 영상 | duration | DECODE_OVERLOAD | 결과 |
|---|---|---|---|
video_tiki25_movie_01 |
140.544s | 06:35:00+ (반복, 개별 ~1.6s) | ❌ 06:35:35 호스트 수동 |
출처: Grafana ppi-socket Loki 로그 (LogRocket 파일은 부분/혼합 로그로 분석 불가, 소켓 서버 로그로 재조회)
· roomId: c53de64f-b555-43f8-981a-bae18acf9a0d_24
· 디바이스: 8GB / 6코어 (정상 기기)
| 시각 (UTC) | 이벤트 | 결과 |
|---|---|---|
| 11:00:32 | Guest video play started (1번 영상) | ✅ 11:03:57 AI auto-finish 자동 전환 |
| 11:03:57 | Guest video play started (2번 영상) · DECODE_OVERLOAD 11:04:16~ | ❌ 11:04:56 Host changed step |
| 11:06:28 | Guest video play started (3번 영상) · DECODE_OVERLOAD 11:07:12~ | ✅ 11:06:27 AI auto-finish 자동 전환 (전 스텝) |
| 11:10:38 | Guest video play started (4번 영상) · DECODE_OVERLOAD 11:10:40~ | ❌ 11:12:10 Host changed step |
Host changed step 확인 — 아동303·아동074과 동일 패턴.
LogRocket 파일(6-019ed011-…)은 DECODE_OVERLOAD만 있는 부분/혼합 로그로 게스트 세션 로그가 아닌 것으로 판명됨.
본 분석은 Grafana Loki ppi-socket 로그를 직접 조회해 확인.
로그: 6-uuid-0424_0_c0fa9243-…-c660ac550477.json
· 디바이스: 4GB / 8코어 (저사양 판정 경계 ≤4GB)
| 시각 | 영상 | duration | 결과 |
|---|---|---|---|
| 10:00:29 | video_tiki01_intro_01 |
71.573s | ✅ near-end fallback 정상 |
| 10:05:39 | video_tiki01_intro_01 |
71.573s | ❌ 10:06:55 호스트 수동 |
| 10:08:27 | (다른 영상) | - | ❌ 10:09:40 호스트 수동 |
| 10:11:13 | (다른 영상) | - | ❌ 10:12:59 호스트 수동 |
| 10:18:21 | (다른 영상) | - | ❌ 10:18:55 호스트 수동 |
| 10:18:57 | video_tiki01_movie_03 |
9.427s | ✅ Watchdog backstop 정상 (10:19:07) |
arm() 시점부터 첫 tick까지 140초 대기, 첫 tick 이후 호스트 개입까지 단 6초. 이 6초 동안 maxStall(30s)이 누적될 수 없었음.
| 경로 | 로그 토큰 | 발화 |
|---|---|---|
네이티브 ended 이벤트 |
[MEET_VIDEO] Video ended event fired |
❌ 없음 |
| timeupdate near-end fallback (duration−0.1s) | Video onEnd triggered via near-end fallback |
❌ 없음 |
| VideoEndWatchdog backstop | [MEET_VIDEO] Video onEnd triggered via watchdog backstop |
❌ 없음 |
| 소켓 종료 통지 | [GUEST_SOCKET] Notifying video ended |
❌ 없음 |
VideoEndWatchdog.arm()은 정상 재생 중 불필요한 폴링을 피하려고 첫 tick을 예상 종료 시각에 맞게 단 하나만 예약한다. 그 이후부터 1초 폴링을 시작한다.
// video-end-watchdog.ts arm() const firstDelayMs = hasDuration ? Math.max(0, (s.duration - s.currentTime) * 1000) // ← 예상 종료 시점까지 잠들기 : this.pollIntervalMs; this.scheduleNext(firstDelayMs);
이 전략은 정상 재생에는 효율적이지만, DECODE_OVERLOAD로 영상이 늦어지는 경우 첫 tick이 발화할 때는 이미 stall이 한참 진행된 상황이다.
tick()의 maxStall 체크는 마지막 진행 이후 연속 정지 시간만 측정한다.
// video-end-watchdog.ts tick() if (s.currentTime > this.lastTime + PROGRESS_EPSILON_SEC) { this.lastTime = s.currentTime; this.lastProgressAt = this.now(); // ← canplay 후 조금이라도 진행하면 리셋 this.progressed = true; } else if (this.progressed) { const stalledMs = this.now() - this.lastProgressAt; if (remaining <= this.nearEndWindowSec && stalledMs >= this.nearEndStallMs) { fire(); } if (stalledMs >= this.maxStallMs) { fire(); } // 30s 연속 정지 }
DECODE_OVERLOAD는 완전 정지가 아니라 짧은 stall(~1.6s)과 resume이 반복되는 패턴이다.
매 resume 직후 currentTime이 미세하게라도 진행하면 lastProgressAt이 리셋되어 누적 stall이 쌓이지 않는다.
| 지표 | 아동303 25회기 | 아동074 2회기 (10:05) |
|---|---|---|
| 영상 duration | 140.544s | 71.573s |
| arm() → 첫 tick | 140s 대기 | 71.5s 대기 |
| 첫 tick 시점 currentTime | 102.864s (remaining=37.7s) | ~30s 추정 (remaining ~40s) |
| wall clock 누적 지연 | 36s | 미측정 |
| 개별 stall 지속 시간 | ~1.6s | ~0.4–2s |
| 첫 tick → 호스트 개입까지 | 6초 | 64초 (but maxStall 미달) |
| 결과 | 호스트 수동 | 호스트 수동 × 4회 |
VideoEndWatchdog은 두 가지 케이스를 커버한다:
ended 이벤트가 누락됨 → endThreshold(0.3s) 체크로 처리maxStall(30s) 연속 체크로 처리currentTime은 아직 끝에 도달하지 않았고,
연속 stall도 30s에 달하지 않아 두 경로 모두 발화하지 않는다.
arm() 시점의 wall clock과 currentTime을 기록해두고, tick마다 "지금쯤이면 영상이 끝났어야 한다"를 체크한다.
// arm() 에 추가 this.armWallClockMs = this.now(); this.armVideoTimeSec = s.currentTime; // tick() 에 추가 (endThreshold 체크 직후) const wallElapsedSec = (this.now() - this.armWallClockMs) / 1000; const expectedElapsedSec = s.duration - this.armVideoTimeSec; const DRIFT_MARGIN_SEC = 30; // 예상 종료 시각 + 30s 초과 시 발화 if (this.progressed && wallElapsedSec > expectedElapsedSec + DRIFT_MARGIN_SEC) { this.fire(); // "wall clock 기준 오버듀" 발화 return; }
연속 stall이 아닌 총 stall 누적 시간을 별도로 추적하여 임계치 초과 시 발화.
// tick() — stall 구간마다 누적 if (currentlyStalled) { this.totalStalledMs += tickElapsedMs; } if (this.totalStalledMs >= CUMULATIVE_STALL_THRESHOLD_MS) { this.fire(); }
meet-video.tsx에서 DECODE_OVERLOAD 횟수를 카운팅하여 일정 횟수 초과 시 Watchdog의 polling 간격을 단축하거나 별도 타이머를 시작.
근본 해결책은 아니지만 기존 구조를 유지하면서 부분 보완 가능.
Watchdog tick이 발화했으나 아무 조건도 충족 못 해 통과된 경우, 현재는 로그가 전혀 없다. tick 발화 로그 추가 시 "Watchdog이 동작은 했는데 왜 발화 안 했냐" 원인 추적이 용이해진다.
// tick() 마지막에 추가 logger.debug("Watchdog tick: no fire", { remaining, stalledMs, progressed: this.progressed, wallElapsedSec });