쉬운 설명 · Bugs · 사건일 2026-09-28

진행자 화면만 0초에 멈춘 모델링영상

마지막 업데이트 2026-09-29

진행자는 "아동 영상이 시작되지 않는다"고 보고 강제퇴장을 했습니다. 하지만 아동 화면의 영상은 재생되고 있었습니다. 멈춘 것은 진행자 화면에 띄우는 복제본이었습니다.

수업: 임새아 2회기 · Chrome 153 / Windows영상: video_episode_family_0_low (86.5초)시각: 09:09:03Z (KST 18:09:03)

문제 정의. 영상 스텝으로 넘어갈 때 "스텝 변경"과 "재생 시작" 이벤트가 렌더 사이 없이 연달아 도착하면, 진행자 모니터가 스텝 변경의 setIsVideoPlaying(false)를 updater 안에서 늦게 적용해 true를 덮어쓰고, 그래서 아동 영상은 정상 재생되는데 진행자 화면의 영상만 0초에 멈춥니다(use-host-socket.ts:770).
쉬운 말로. 진행자 화면에 쪽지 두 장이 1ms 간격으로 도착했습니다. ① "다음 스텝으로" (여기에 "재생 끄기"가 들어 있음), ② "재생 시작". 화면이 ①의 "재생 끄기"를 나중에 적용하는 바람에 ②의 "재생 켜기"가 지워졌고, 영상은 0초에 멈춘 채 남았습니다.

01화면은 두 개, 멈춘 쪽은 하나

진행자 화면은 아동 화면을 녹화해서 받는 것이 아니라, 같은 영상 파일을 따로 재생합니다.

아동 화면 ▶ 재생 중 진행자 화면 (복제본) ⏸ 0:00 정지 같은 파일 따로 재생 "영상이 안 나와요" → 강제퇴장
그림 1. 아동 쪽 로그에는 재생 시작, 2초 뒤 재생 위치 1.9초·오디오 정상, 멈춤 경고 0건이 남아 있습니다. 진행자 쪽 로그에는 "준비 완료" 뒤 재생 시도가 없습니다.

02쪽지 두 장이 거의 동시에 도착

서버는 순서를 지키려고 ① 스텝 변경 → ② 재생 시작을 연달아 보냅니다.

아동 09:09:02.904 스텝 전환 09:09:02.944 재생 시작 서버 스텝 확정까지 재생 신호 보류 그다음 ①② 연달아 전송 진행자 화면 03.314 ① 도착 03.315 ② 도착 ① 다음 스텝 ② 재생 시작 1ms
그림 2. 여기까지는 의도한 동작입니다. 9/2 #1058에서 "재생 시작이 스텝보다 먼저 와서 지워지는" 문제를 막으려고 서버가 순서를 맞추게 했습니다.

03왜 "켜기"가 지워졌나 — 나중에 펼치는 쪽지

① 쪽지 안의 "재생 끄기"는 봉투에 넣어 두었다가 화면을 다시 그릴 때 꺼내 적용합니다.

03.314 03.315 화면 다시 그리기 결과 ① 다음 스텝 ✉ 봉투에 보관 (안에 "재생 끄기") ② 재생 시작 재생 = 켜짐 바로 적용 ✉ 봉투 개봉 재생 = 꺼짐 마지막에 적용 → 덮어씀 ⏸ 꺼짐 43ms·148ms 간격일 때는? 두 쪽지 사이에 화면을 한 번 다시 그립니다 → 봉투가 먼저 열려 "꺼짐" → 뒤이어 온 ②가 "켜짐"으로 바꿈 → 정상 재생
그림 3. "봉투" = React의 setState(updater). updater 함수는 React가 화면을 다시 그릴 때 실행될 수 있습니다. 그 안에서 부른 setIsVideoPlaying(false)가 먼저 들어온 true보다 뒤에 적용됩니다.
한 번 꺼지면 되살릴 방법이 없습니다. 진행자 영상이 재생되는 길은 세 가지인데, 셋 다 "재생 = 켜짐"이어야 동작합니다. 켜짐 상태가 바뀔 때, 영상 준비가 끝났을 때(canplay), 서버 시계로 재생 위치를 맞췄을 때입니다. 그래서 강제퇴장 후 재접속으로 상태를 새로 받을 때까지 멈춰 있었습니다.

04간격만 달랐던 세 번

파일·장치·브라우저는 모두 같았습니다. 달랐던 것은 쪽지 두 장의 도착 간격뿐입니다.

진행자 화면 도착 간격 (① → ②) 09:09:03 모델링영상 1ms ⏸ 0:00 정지 09:09:48 재접속 후 같은 영상 43ms ▶ 정상 09:14:59 다음 영상(bridge) 148ms ▶ 정상
그림 4. 1ms라는 기준이 따로 있는 것이 아닙니다. 두 쪽지 사이에 화면 다시 그리기가 끼었느냐가 결과를 가릅니다.

05로그로 보기 (UTC · 세션 로그 줄 번호)

파일: 세션 로그 임새아_2회기.json (JSONL, 12,841줄). 아동과 진행자는 다른 PC라 시계가 어긋날 수 있어 간격은 진행자 시각끼리만 비교했습니다.

실패 — 모델링영상
L3413  09:09:02.904Z  guest    Moving to next step            ← 아동: 모델링영상 스텝
L3438  09:09:02.943Z  guest    Video canplay event fired
L3440  09:09:02.944Z  guest    Notifying video play started   ← 아동: 재생 시작 전송
L3446  09:09:03.314Z  monitor  Guest step updated             ┐
L3449  09:09:03.314Z  monitor  Guest step updated             │ 1ms
L3450  09:09:03.315Z  monitor  Guest video play started       ┘
L3459  09:09:04.035Z  monitor  Video canplay event fired      ← 이후 재생 시도 없음

성공 — 재접속 후 같은 영상
L3581  09:09:48.392Z  guest    Notifying video play started
L3590  09:09:48.700Z  monitor  Guest step updated             ┐ 43ms
L3602  09:09:48.743Z  monitor  Guest video play started       ┘
L3603  09:09:48.746Z  monitor  Attempting programmatic play from playback effect

성공 — 다음 영상(bridge)
L6847  09:14:59.163Z  guest    Notifying video play started
L6855  09:14:59.331Z  monitor  Guest step updated             ┐ 148ms
L6856  09:14:59.479Z  monitor  Guest video play started       ┘
L6868  09:14:59.636Z  monitor  Attempting programmatic play from canplay handler
아동 쪽은 정상이었다는 근거. 09:09:04.944Z에 재생 위치 1.9초, 오디오 디코딩 정상, 볼륨 1·음소거 해제였습니다. 이후 퇴장할 때까지 "재생 멈춤" 경고(timeupdate가 5초 이상 끊기면 남김)가 0건이었고, 09:09:21.717Z에 isPlaying: true 상태로 화면이 내려갔습니다. 진행자도 "모델링영상 스텝에서 영상이 정지 상태였다"고 확인했습니다.

06재현 확인

제품 코드는 건드리지 않았습니다. 프로젝트와 같은 React 19.2.6으로 임시 스크립트를 만들어 같은 호출 순서를 흉내 냈습니다.

케이스최종 재생 상태
A. L3446~L3450 순서를 다시 그리기 없이 연속 처리false — 실패 재현
B. 스텝 변경을 그린 뒤 재생 시작 도착true
C. 별도 태스크로 연달아 도착 (중간에 그리기 가능)true

07코드 위치

위치하는 일
apps/web/entities/host-socket/model/use-host-socket.ts:769-779스텝 변경 처리. setGuestCurrentStep(() => { if (stepChanged) setIsVideoPlaying(false); … }) ← 봉투 속 "재생 끄기"
use-host-socket.ts:988-995재생 시작 처리. setIsVideoPlaying(true)
apps/socket/src/sfu-socket/handlers/session-relay-handlers.ts:934-1063, 541-559서버가 재생 신호를 스텝 확정까지 보류한 뒤 ①→② 순서로 연달아 전송 (#1058)
apps/web/shared/ui/meet-video.tsx:471, 719진행자 영상을 재생하는 두 경로. 둘 다 isPlaying이 true일 때만 동작
apps/web/app/monitor-dashboard/[group]/[roomId]/page.tsx:2206requestVideoElapsed={isVideoPlaying ? … : undefined} — false면 서버 시계 기준 재생 위치 맞추기도 시작되지 않음

08언제부터 있던 버그인가

날짜커밋내용
2026-03-198fc6119d (#302, PPI-676)스텝 변경 updater 안에 setIsVideoPlaying(false) 추가 — 결함 시작
2026-07-28b03871e4스텝이 바뀌었는지 판단하는 코드는 updater 밖으로 뺐지만, "재생 끄기"는 안에 남음
2026-09-023e9f13d8 (#1058)서버가 ①→② 순서를 보장. "② 먼저 와서 지워짐" 문제는 막았지만 모니터 쪽 봉투 문제는 그대로 남음
추정. #1058 이후 서버가 두 쪽지를 일부러 붙여 보내므로 1ms 도착이 오히려 더 자주 생겼을 수 있습니다. 다른 수업에서 같은 패턴(진행자 canplay 후 재생 시도 없음 + 두 쪽지 간격 수 ms 이내)이 몇 번 나오는지 세어 보면 확인할 수 있습니다.

09고치는 방법 (제안 · 아직 적용 안 함)

10원인이 아닌 것

의심판정
영상 파일 video_episode_family_0_low아닙니다. 양쪽 모두 정상 로드(86.517초)됐고, 재접속 후 끝까지 재생됐습니다.
네트워크·연결 (자동 분류기 1순위)아닙니다. 끊김 기록은 강제퇴장 때문에 생긴 결과입니다.
setSinkId("communications") AbortError아닙니다. 아동 페이지에 들어올 때마다 났고, 영상이 정상 재생될 때도 똑같이 났습니다.
오디오 장치 (SSL 2 USB · Voicemeeter)이 증상과는 무관합니다. 다만 "USB"가 들어간 장치를 헤드셋으로 자동 선택하는 규칙 때문에 소리 경로가 나뉘었을 수 있어, 18:05 "소리가 안 들려요" 신고와는 따로 확인이 필요합니다.

관련 문서