iPad 백그라운드 복귀 시 영상 loopback 오디오 무음 — 원인 분석 및 수정 (PPI-1158) 분석수정

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

iPad 백그라운드 복귀 시 영상 loopback 오디오 무음 — 원인 분…: 증상: 1. 증상, 원인: 3. 원인 — 백그라운드 복귀 후 공유 ctx가 suspended로 남음, 수정·검증: 5. 수정 (PR #921 · 브랜치 PPI-1158) 흐름
동작 흐름 요약
  1. 증상: 1. 증상
  2. 원인: 3. 원인 — 백그라운드 복귀 후 공유 ctx가 suspended로 남음
  3. 수정·검증: 5. 수정 (PR #921 · 브랜치 PPI-1158)
작성일: 2026-07-23 사건: 인트로 영상 재생 중 무음 (iPad · CriOS 150) room: 4c6e86e5…_4 PR: #921 상태: 🟡 코드 반영 · 실기기 검증 대기

1. 증상

iPad(iPadOS 26.3 + Chrome/CriOS 150 = WebKit) 게스트가 인트로 영상(오프닝송, video_dubutiki_long_introsong)을 재생하는데 소리가 나오지 않았다. 영상 화면과 재생 시간(currentTime)은 정상적으로 흐르고 muted:false·volume:1인데도 무음이었고, 아동이 도움 메뉴에서 '🙉 소리가 안 들려요'를 신고했다(재생 시작 약 121초 시점).

겉보기엔 전부 정상. loopback established·에러 없음·워치독 폴백 없음·AUDIO_PROBE suspectedSilent:false. 기존 관측만으로는 "앱은 소리가 나간다고 믿는데 실제 출력만 무음"인 상태로, 어느 로그도 무음을 가리키지 않았다.

2. 배경 — iPad에서 영상 오디오는 직접 재생이 아니다

iPad에서는 마이크 EC(voice-processing) 활성 중 <video> 직접 재생이 시스템 덕킹으로 감쇠되는 문제(PPI-1101)를 피하기 위해, 영상 오디오를 WebRTC loopback(통화 카테고리, 덕킹 면제)으로 우회 출력한다. iOS 계열 게스트에서만 활성화된다.

그래프 (iPad 게스트 · video-audio-loopback.ts) <video> ──createMediaElementSource──▶ hub(Gain) ─┬─▶ loopbackDestination ─▶ PC쌍 ─▶ <audio>.srcObject ─▶ 스피커(setSinkId) └─▶ ctx.destination (loopback 미성립 시에만 연결 = "직결 폴백")
핵심 불변식 3가지.createMediaElementSource(video)를 호출하는 순간 video 엘리먼트의 오디오는 스피커 직결에서 영구히 떨어져 WebAudio 그래프로 들어간다(그래서 muted:false여도 직접 소리가 안 난다). ② 이 그래프 전체는 공유 AudioContext가 running일 때만 샘플을 흘린다. ③ loopback established 시점(로그 176.28s)에 직결 폴백(hub→ctx.destination)은 detachDirectOutput()으로 끊긴다 → 소리길은 loopback 단독.

3. 원인 — 백그라운드 복귀 후 공유 ctx가 suspended로 남음

영상 loopback은 AI 세션과 같은 공유 AudioContext(ensureSharedTtsAudioContext)를 탄다. 그런데 이 ctx를 resume하는 트리거가 ①명시적 호출(warmup/attach) ②pointerdown/touchend 제스처 뿐이고, 포그라운드 복귀(visibilitychange)에는 아무 것도 없었다. iOS는 백그라운드 진입 시 AudioContext를 suspend하는데, 복귀 후 아동이 탭하지 않으면 ctx가 suspended로 남아 loopback 그래프가 무음이 된다.

시간순 인과 체인 (room 4c6e86e5…_4) 175.72s Audio warmed up (준비 팝업 탭) ───▶ 공유 ctx resume · running 176.28s loopback established (attempt 1) ─▶ 직결 폴백 detach · 소리길 = loopback 단독 │ (여기까진 ctx running → 정상 출력 가능) 239.85s Tab became hidden ───────────────▶ iOS가 공유 AudioContext를 suspend 259.30s video stalled (gap 5.28s) (백그라운드) 286.10s Tab became visible ──────────────▶ ★ ctx 깨우는 코드가 아무것도 안 불림 │ (복귀 이벤트: tab visible / track unmuted 뿐 · ensure/attach/tap 없음) │ → ctx suspended 유지 → loopback 그래프 무음 지속 (직결 폴백도 이미 끊김) 297.65s 아동 "소리가 안 들려요" 신고 (탭 = 아이러니하게 이때 ctx가 다시 깨어났을 수 있음)
직결 폴백이 무력한 이유. 코드의 직결 폴백은 hub.connect(ctx.destination)같은 suspended ctx를 탄다. 게다가 createMediaElementSource는 되돌릴 수 없어 "WebAudio 안 쓰고 video 직접 재생"으로도 못 돌아간다. → 이 케이스의 유일한 복구 경로는 "공유 ctx를 다시 running으로 만드는 것"뿐.
무음 워치독의 사각지대. 기존 워치독은 source 신호 있음(RMS≥0.01) + remote 무음일 때만 발동한다. ctx가 suspended면 source(hub) RMS도 0이라 sourceActive=false → 카운터 리셋 → 절대 발동 못 함. 그래서 로그에 loopback lost/fallback 경고가 하나도 없이 "정상"처럼 보였다.

4. 진단 근거

신호의미
ISSUE_DIAG media[0]video · paused:false · muted:false · volume:1 · currentTime:76.4 · srcKind:blob인트로 영상은 재생 중(시간 흐름) — 렌더는 정상, 오디오만 우회됨
복귀 이벤트(286.10s)tab visible / track unmuted 만 기록ctx resume / attach / tap 트리거 전무 → suspended 유지
286~297svideo play/pause 이벤트 0건attach()도 안 불려 ctx resume 기회 없음
워치독 경고0건 (silent/lost/failed 없음)source·remote 동시 무음 = 사각지대라 미발동
AUDIO_PROBEsuspectedSilent:false · ctxStateBefore:suspended판정 무의미 — 프로브는 매번 별도 throwaway ctx를 새로 만듦(공유 loopback ctx 상태와 무관, OS 출력계층 동작만 증명)

신뢰도. 공유 ctx의 실제 state가 로그에 직접 안 찍혀 "처음부터 무음" 가능성을 100% 배제하진 못한다. 다만 신고가 백그라운드→포그라운드 복귀(239→286s) 직후이고 복귀 이벤트에 resume 트리거가 전무하다는 점이 "복귀 시 ctx 미복구 → loopback 무음"을 가장 강하게 가리킨다. (후속 개선 ③으로 diag에 공유 ctx state를 함께 기록하면 다음부턴 즉시 확정 가능.)

5. 수정 (PR #921 · 브랜치 PPI-1158)

파일변경
video-audio-loopback.tsresume() 추가 — suspended 공유 ctx resume + 백그라운드 중 정지된 loopback <audio> 재생 복구. 멱등.
meet-video.tsxvisibilitychange→visible 시 loopback.resume() + 재생 중이면 attach() 재확인. (비-iOS는 컨트롤러 없어 no-op)
use-ai-session.ts공유 ctx 소유자에서 visibilitychange→visible 시 ctx resume — 영상뿐 아니라 TTS 무음까지 커버.
video-audio-loopback.test.tsresume() 동작 검증 3건 (suspended 복구 / running 중복 미트리거 / dispose 후 no-op).
// video-audio-loopback.ts — 유일한 복구 경로: ctx를 running으로
resume(): void {
  if (this.disposed) return;
  const ctx = this.options.getAudioContext();
  if (ctx && ctx.state === "suspended") {
    void ctx.resume().catch(() => {});
  }
  const audio = this.audioElement;
  if (audio && audio.srcObject && audio.paused) {
    void Promise.resolve(audio.play()).catch(() => { /* resume 실패 로깅 */ });
  }
}
동작. 포그라운드 복귀 시점에 공유 ctx를 resume하고 정지된 loopback audio를 재생 재개한다. 대부분의 iOS는 미디어 재생 중 복귀 시 제스처 없이도 resume을 허용하므로 탭 없이 소리가 회복된다.

검증: loopback 유닛 테스트 전체 통과(신규 3건 포함) · tsc --noEmit 에러 없음. 실기기 검증은 dev 배포 후 진행.

6. 실기기 재현/검증 시나리오

기기: iPad + Chrome(CriOS) 우선(원 로그가 CriOS 150), Safari 병행.

A. 버그 재현 (수정 전)

B. 수정 검증 (PPI-1158)

탭 없이는 여전히 무음이고 탭하면 회복된다면 → iOS가 제스처 없는 resume을 거부하는 케이스. 로그에 "Video loopback audio resume failed"가 찍혔는지 확인. 이땐 후속 안전망(②)이 필요하다.

7. 후속(미반영) — 실기기 결과에 따라