Skip to content

fix: egress 수명을 세션 수명으로 분리해 트랙 교체 시 송출 단절 해소 - #86

Merged
hjbin-25 merged 1 commit into
mainfrom
fix/egress-lifetime/#84
Aug 8, 2026
Merged

fix: egress 수명을 세션 수명으로 분리해 트랙 교체 시 송출 단절 해소#86
hjbin-25 merged 1 commit into
mainfrom
fix/egress-lifetime/#84

Conversation

@itzjb

@itzjb itzjb commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Closes #84

문제

egress의 수명이 세션이 아니라 트랙에 묶여 있었다. manager.go에서 트랙마다 trackCtx를 새로 만들고(:493) egress를 거기에 묶는데(:531), 새 트랙이 도착하면 기존 trackCancel()을 호출한다(:486-491).

따라서 발행자가 방송 중 카메라를 전환하면 egress가 종료된다. 새 트랙의 OnTrack이 egress를 다시 만들어 결과적으로 복구되지만, 그 사이 RTMP 연결이 끊기고 FFmpeg가 재기동되므로 시청자 쪽에서는 스트림이 끊겼다 다시 붙는 것으로 보인다.

현재는 YOUTUBE_STREAM_KEY가 2026-08-02 유실 후 미복구라 manager.go:526에서 egress 생성 자체가 스킵되어 이 결함이 드러나지 않는다. 키가 있던 시점에는 실제로 발생하던 문제다.

변경

egress 수명을 세션 수명으로 이동

  • Sessionegress 참조와 egressCancel 추가. 세션 종료와 별개로 egress만 멈추는 경로(#83의 명시적 stream/stop)를 위해 전용 cancel을 분리했다
  • egress 생성을 OnTrack에서 CreateForUser로 이동. Run은 첫 프레임 수집 전에는 FFmpeg를 스폰하지 않으므로 트랙 없는 세션에서는 고루틴 하나가 대기할 뿐이다
  • OnTracks.mu 하에 기존 egress를 읽어 RunTrack에 재전달한다. RunTrackEnqueue만 호출하고 egress를 종료시키지 않으므로(track.go:351-352) 트랙 교체에도 연결이 유지된다

해상도 재기동

수명이 트랙과 분리되면서 기존에 egress 재생성으로 우연히 얻어지던 해상도 재측정 지점이 사라졌다. writeFrames가 프레임 메타데이터의 width/height를 현재 스폰 해상도와 비교해, 불일치 프레임을 쓰지 않고 반환하면 그 프레임을 시드로 재측정(30프레임) 후 재스폰한다.

  • 감지 신호는 디코더가 키프레임에서 파싱해 모든 프레임에 채우는 값이다(VP8 ffmpeg.go:136, H264 h264.go:48·241-242). JPEG 헤더 재파싱 없이 메타데이터 비교만으로 정확하다
  • 인코더가 죽기 전에 감지해야 한다. 죽은 뒤에는 어느 프레임부터 어떤 해상도였는지 알 수 없어 재기동 기준을 잡을 수 없다
  • fps·비트레이트까지 재산정한다. 트랙 교체는 기기 전환일 수 있어 해상도만 바뀌는 것이 아니다
  • 해상도 재기동은 송출 실패가 아니므로 백오프·ReconnectAttempts·재연결 메트릭에 집계하지 않는다
  • 0×0 프레임은 비교에서 제외한다(재기동 반복 방지)

StreamState 실배선

생성 시 idle로 설정된 뒤 갱신 코드가 전혀 없던 StreamState를 egress 상태 스냅샷으로 합성한다. egress 고루틴이 세션 락을 잡는 역방향 결합을 피하려 pull 모델(전용 뮤텍스 + Status() 게터)을 썼다. 락 순서는 session.mu → egress.statusMu 단방향이다. StopReason은 명시적 중지 개념이 생기는 #83에서 채운다.

범위 밖 (의도적)

YOUTUBE_STREAM_KEY 전역 키 트리거를 그대로 유지했고, 사용자별 송출 로직과 handleStartStream/handleStopStream 실배선은 넣지 않았다. 전부 #83에서 처리한다. 동작 보존이 이 PR의 리뷰 가치다.

NewRTMPEgressRunTrack의 시그니처를 바꾸지 않아 기존 호출부와 egress 테스트 6종은 영향받지 않는다.

검증

  • go test -race -count=1 ./... 전 패키지 통과 (캐시 우회)
  • TestBypassWebRTCEndToEnd 격리 race 실행 통과 (알려진 flaky 대응)
  • 실 ffmpeg 통합 테스트 -race -count=3 통과 — 640x360 스폰 → 해상도 변경 감지 → 1280x720 재스폰, ffprobe로 최종 출력 확인, ReconnectAttempts=0 확인
  • 신규 테스트 8건: 상태 전이(재연결을 넘어 StartedAt 보존 포함) / 시드 수집 / 취소 시 수집 중단 / 해상도 판정 표 테스트 / 스폰 전 취소 종료 / 세션 수명 egress 생성·삭제 / TargetURL 스트림 키 비노출 / 키 없을 때 기존 동작 보존

배포 영향

없다. YOUTUBE_STREAM_KEY가 비어 있어 egress 생성 경로가 실행되지 않으므로 현재 프로덕션 동작은 변하지 않는다.

관계

#83 (사용자별 RTMP 송출 대상)의 선행 작업. 명시적 방송 시작 구조가 성립하려면 egress가 트랙보다 오래 살아야 한다.

egress가 트랙 수명(trackCtx)에 묶여 있어 카메라 전환 시 함께 종료됐다.
새 트랙의 OnTrack이 egress를 다시 만들어 복구되긴 했으나 그 사이 RTMP
연결이 끊기고 FFmpeg가 재기동되어 송출이 단절됐다. egress를 세션 수명으로
옮기고 트랙 교체 시 기존 egress를 재전달해 연결을 유지한다.

세션 종료와 별개로 egress만 멈추는 경로(#83의 명시적 stream/stop)를 위해
전용 cancel을 분리해 뒀다. 수명이 트랙과 분리되면서 사라진 해상도 재측정
지점은 프레임 메타데이터 비교로 대체했다. 해상도 변경은 송출 실패가
아니므로 백오프·재연결 집계 없이 재측정 후 재스폰한다.

생성 시점에 idle로 고정된 채 갱신되지 않던 StreamState는 egress 상태
스냅샷으로 합성한다. StopReason은 명시적 중지 개념이 생기는 #83에서 채운다.

NewRTMPEgress와 RunTrack의 시그니처는 바꾸지 않아 기존 호출부와 테스트는
영향받지 않는다.
@itzjb
itzjb requested a review from a team as a code owner August 8, 2026 19:38
@hjbin-25
hjbin-25 merged commit b9bc5d9 into main Aug 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 트랙 교체 시 RTMP egress가 중단되어 송출이 단절됨

2 participants