You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
키 출처: internal/config/config.go:114 (YOUTUBE_STREAM_KEY)
사용 지점: internal/session/manager.go:526-533 — 세션마다 이 하나의 키로 egress 생성
ingest URL: internal/session/manager.go:30 에 상수 하드코딩
클라이언트 UI·REST 어디에도 송출 대상을 지정하는 경로가 없음
유튜브 API를 한 번도 호출하지 않는다. RTMP를 밀 뿐이고, 방송이 켜지는 것은 채널 스튜디오 설정의 자동 시작에 의존한다
이 구조에서 파생되는 문제:
사용자별 채널 송출이 불가능하다. 모든 세션이 서버 운영자의 채널 하나로만 나간다.
동시 세션 2개 이상이면 같은 스트림 키로 동시 push되어 충돌한다. 운영 설정은 MAX_SESSIONS=12.
키가 비어 있으면 조건문에 걸려 송출이 조용히 비활성화된다. 실패가 드러나지 않는다.
송출 시작 시점이 트랙 게시 순간으로 고정이라, 사용자가 "오늘은 뷰어에만" 같은 선택을 할 수 없다.
목표
사용자가 자신의 YouTube 계정을 OAuth로 연결하면, 서버가 그 사용자의 토큰으로 방송을 생성하고 ingest 정보를 받아와 해당 세션의 egress에 주입한다. 송출 개시는 명시적 요청으로만 이루어진다.
플랫폼 송출은 opt-in 이다. 연결하지 않은 사용자는 지금과 동일하게 WebRTC 뷰어 경로로만 방송한다.
확정된 설계 결정
1. 명시적 송출 시작 — handleStartStream 501 스텁 실배선
internal/server/server.go:179-186 의 501 스텁과, manager.go:248 에서 idle 로 설정된 뒤 갱신되지 않는 StreamState 를 실제로 배선한다.
근거:
실패 가시성 — start가 동기 HTTP이므로 토큰 만료·API 오류를 4xx로 즉답할 수 있다. 자동 시작 구조에서는 실패가 서버 로그에만 남아 사용자가 원인을 알 수 없다.
방송 중 제어 — InnoLive 뷰어 송출은 유지한 채 플랫폼 송출만 켜고 끌 수 있어야 한다.
스텁이 이미 "트랙 없으면 409" 계약을 갖고 있어(server.go:181-184) 상태 전이 설계 부담이 작다.
계약 변경이지만 위험이 낮은 시점이다. static/client/app.js 는 /stream/start 를 호출하지 않으며(추가 비용은 버튼 1개 + 호출 1건 수준), 전역 키 유실로 현행 자동 송출 동작에 의존하는 사용자가 실존하지 않는다.
2. 방송 라이프사이클 — enableAutoStart / enableAutoStop 사용
liveBroadcasts.insert 시 contentDetails.enableAutoStart=true, enableAutoStop=true 로 생성한다.
transition(live) 는 바인딩된 스트림이 active 여야 하는데, active 는 egress가 프레임을 밀기 시작한 뒤에야 된다. 따라서 transition 을 직접 호출하려면 active 폴링 → 전환 재시도 라는 비동기 상태 머신이 필요하다. autoStart는 이를 제거한다.
부수 효과로 quota 압력이 크게 준다. list 가 1 unit이어도 5초 폴링이면 하루 17,280 units로 기본 한도(10,000)를 넘는다.
포기하는 것은 testing 프리뷰 단계다. fail-closed 설계상 AI 실패 시 검은 프레임이 나가므로 프리뷰의 안전상 가치가 낮다고 판단했다. 필요해지면 enableAutoStart=false + transition 경로를 옵션으로 추가한다.
이 동작은 문서 기재 사항이며 실측 미확인. 착수 전 수동 E2E에서 검증한다.
3. 프로바이더 추상화는 라이프사이클 수준
"스트림 키 조회" 가 아니라 Prepare / Stop 수준으로 추상화한다. YouTube는 Prepare 에서 방송 생성 + bind가 필요하고, 치지직은 키 조회만으로 끝나며 방송 시작/종료 API 자체가 없다. 키 조회 수준으로 추상화하면 두 플랫폼이 한 구조에 들어가지 않는다.
4. 스트림 리소스 프리로딩
liveStreams.insert 시 contentDetails.isReusable=true 로 만들고, OAuth 연결 시점에 1회 생성해 재사용한다. 방송마다는 liveBroadcasts.insert + bind 만 한다.
방송당 API 호출 3회 → 2회 (quota 절감, start 응답 단축)
연결 직후 ingest 정보가 확보되어 연결 상태 UI를 즉시 표시할 수 있다
5. ingest URL은 API 응답값 — RTMPS 전환
manager.go:30 상수를 제거하고 cdn.ingestionInfo.rtmpsIngestionAddress 를 사용한다. 최종 URL은 문서 명시 규칙에 따라 ingestionAddress + "/" + streamName. 현행 평문 RTMP에서 TLS로 전환되는 보안 개선이 함께 따라온다.
6. 전역 YOUTUBE_STREAM_KEY 제거
폴백으로 남기지 않는다. 유실 후 미복구라 실사용이 0이며, 남기면 "사용자별 송출"과 "서버 전역 송출" 두 경로를 계속 부양해야 한다.
7. OAuth 경로 — connect / callback 2단계
POST /auth/youtube/connect (XHR, Bearer 인증)
→ 서버가 state 생성·사용자와 바인딩해 저장
→ authorize URL을 JSON으로 반환
클라이언트가 해당 URL로 이동 → 사용자 동의
→ GET /auth/youtube/callback?code=&state=
→ state 대조로 사용자 복원
콜백은 브라우저 리다이렉트로 도착하므로 Authorization 헤더가 없다. state가 유일한 사용자 바인딩 수단이며, 기존 코드에 state 관리 패턴이 없으므로 신규 작성 영역이다.
/auth/* 는 MountAuthHTTPWithServices 가 만드는 별도 mux다. 인증은 handleWithdrawal(internal/auth/account_withdrawal_http.go:20-34)의 인라인 패턴을 따른다. 단 이 패턴은 DB에서 user status active를 확인하지 않으므로, 서비스 계층에서 상태를 확인하거나 authenticateUser 를 핸들러에 주입해 동등하게 맞춘다.
8. Session 내부 구조
egress를 map[provider]*egressJob{egress, cancel, StreamState} 형태로 보관한다. 동시 송출을 지금 구현하지 않아도(1개 제한 정책) 구조가 막지 않고, StreamState 가 provider별로 자연히 분리되며, start 요청 body의 provider 필드가 그대로 키가 된다.
RunTrack(internal/media/track.go:169)은 단일 *RTMPEgress 인자를 유지한다. 동시 송출이 실요구가 되는 시점에 Enqueue 지점(track.go:351)을 fan-out으로 바꾸는 국소 변경으로 확장한다.
동시 송출 도입 시 결정할 것: 출력별 FFmpeg 대 한 FFmpeg의 tee muxer(인코딩 1회, 출력별 격리 상실) 트레이드오프.
9. 스키마
OAuthAccount 에 provider를 추가하지 않고 별도 테이블로 만든다. OAuthAccount 는 로그인 신원이고 송출 연결은 다른 개념이며(로그인 Google 계정과 송출 YouTube 채널이 다를 수 있다), provider enum이 GORM check constraint와 versioned SQL 양쪽에 박혀 있다.
PK는 별도 UUID, (user_id, provider) 유니크 인덱스
멀티 채널은 지금 허용하지 않는다. 허용하면 "이 세션은 어느 채널로 내보내나" 선택 API/UI가 즉시 범위에 편입된다. 나중에 유니크 인덱스를 드롭하고 선택 채널 포인터를 추가하는 마이그레이션으로 연다
채널 식별자·채널명은 처음부터 저장 (연결 상태 표시에 당장 필요)
refresh token 암호화 저장에 internal/auth/provider_token_cipher.go(AES-256-GCM + 키 버전) 재사용. 단 cmd/server/main.go:229-235 에서 Apple 활성 시에만 생성되므로 배선 변경 필요
치지직은 ingest URL을 API로 제공하지 않으므로 수동 설정값 자리가 필요하다
마이그레이션은 GORM 모델 목록(internal/auth/migrate.go:16-21)과 versioned SQL(000003_*.up/down.sql) 양쪽 모두 갱신한다. 어긋나면 auto 환경과 versioned 환경의 스키마가 달라진다
사용자 제공 값을 변형해 전송하려면 명시적 동의가 필요하다. 얼굴 비식별화가 여기 해당하므로 동의 UI와 동의 기록이 필요하다.
사용자를 대신한 행위(방송 생성·시작·종료)는 사전 명시 동의와 행위 고지가 요구된다.
할당량: 기본 10,000 units/day. 라이브 메서드별 개별 비용이 공식 문서에 없어 실측이 필요하다. 응답 헤더에는 quota 정보가 없으므로 Cloud Console의 Quotas 페이지 사용량 그래프를 관측한다. 방송 중 생존 판정은 API 폴링이 아니라 로컬 신호(FFmpeg 프로세스 생존, egress 프레임 카운트)로 하고, healthStatus 조회는 분 단위 이상 저빈도로 둔다.
앱 검증: YouTube 스코프는 sensitive 등급(restricted 아님 → CASA 보안평가 면제). 미검증 상태에서는 Testing 100명 + refresh token 7일 만료, 프로덕션 미검증은 누적 100명 상한. 검증에는 도메인 소유권·홈페이지·개인정보처리방침·영어 데모 영상이 필요하며 최대 10일 소요.
계정 요건: 채널 인증 필요, 최초 라이브 활성화에 최대 24시간. 미승인 채널의 토큰으로 호출하면 insufficientPermissions 가 반환되므로 사용자 안내에 활용한다.
서비스 계정 플로우는 Live Streaming API와 비호환이다. 사용자 OAuth + refresh token 보관이 필수다.
구현 시 유의
refresh token은 계정 × client_id당 100개 한도. 재로그인마다 새로 발급하면 오래된 것부터 무효화되므로 기존 토큰 재사용 또는 명시적 revoke 설계가 필요하다.
한 사용자가 다중 세션을 열면 access token 갱신이 경합할 수 있다. internal/auth/refresh_store.go:60-160 의 트랜잭션 + 행 락 패턴을 따르거나 사용자 단위로 직렬화한다.
인증 비활성 모드(INNOLIVE_REQUIRE_SESSION_AUTH=false, 벤치마크용)에서는 Session.UserID 가 uuid.Nil 이다. 사용자별 송출 경로는 이 경우를 명시적으로 스킵해야 벤치가 깨지지 않는다.
스트림 키는 비밀번호급으로 취급한다. maskStreamKey(internal/media/egress.go:388-394)는 "마지막 / 뒤가 키"라는 형태를 가정하므로, 다른 형태의 URL을 쓰는 플랫폼에서는 마스킹이 뚫린다. 치지직 연동 시 반드시 함께 처리한다.
착수 전 필수
코드 작성 전에 전 과정을 수동으로 1회 재연한다. OAuth Playground(자체 client_id 사용)로 토큰 발급 → curl로 broadcast/stream insert·bind → ffmpeg CLI로 테스트 송출 → active 확인 → complete. 반나절이면 autoStart 실동작·실제 타이밍·quota 소비량이 전부 확인되며, 설계 불확실성이 사실상 해소된다.
문제
현재 RTMP 송출 대상이 서버 전역 환경변수 하나로 고정되어 있다.
internal/config/config.go:114(YOUTUBE_STREAM_KEY)internal/session/manager.go:526-533— 세션마다 이 하나의 키로 egress 생성internal/session/manager.go:30에 상수 하드코딩이 구조에서 파생되는 문제:
MAX_SESSIONS=12.목표
사용자가 자신의 YouTube 계정을 OAuth로 연결하면, 서버가 그 사용자의 토큰으로 방송을 생성하고 ingest 정보를 받아와 해당 세션의 egress에 주입한다. 송출 개시는 명시적 요청으로만 이루어진다.
플랫폼 송출은 opt-in 이다. 연결하지 않은 사용자는 지금과 동일하게 WebRTC 뷰어 경로로만 방송한다.
확정된 설계 결정
1. 명시적 송출 시작 —
handleStartStream501 스텁 실배선internal/server/server.go:179-186의 501 스텁과,manager.go:248에서idle로 설정된 뒤 갱신되지 않는StreamState를 실제로 배선한다.근거:
server.go:181-184) 상태 전이 설계 부담이 작다.계약 변경이지만 위험이 낮은 시점이다.
static/client/app.js는/stream/start를 호출하지 않으며(추가 비용은 버튼 1개 + 호출 1건 수준), 전역 키 유실로 현행 자동 송출 동작에 의존하는 사용자가 실존하지 않는다.2. 방송 라이프사이클 —
enableAutoStart/enableAutoStop사용liveBroadcasts.insert시contentDetails.enableAutoStart=true,enableAutoStop=true로 생성한다.transition(live)는 바인딩된 스트림이active여야 하는데,active는 egress가 프레임을 밀기 시작한 뒤에야 된다. 따라서transition을 직접 호출하려면 active 폴링 → 전환 재시도 라는 비동기 상태 머신이 필요하다. autoStart는 이를 제거한다.부수 효과로 quota 압력이 크게 준다.
list가 1 unit이어도 5초 폴링이면 하루 17,280 units로 기본 한도(10,000)를 넘는다.포기하는 것은
testing프리뷰 단계다. fail-closed 설계상 AI 실패 시 검은 프레임이 나가므로 프리뷰의 안전상 가치가 낮다고 판단했다. 필요해지면enableAutoStart=false+ transition 경로를 옵션으로 추가한다.3. 프로바이더 추상화는 라이프사이클 수준
"스트림 키 조회" 가 아니라
Prepare/Stop수준으로 추상화한다. YouTube는Prepare에서 방송 생성 + bind가 필요하고, 치지직은 키 조회만으로 끝나며 방송 시작/종료 API 자체가 없다. 키 조회 수준으로 추상화하면 두 플랫폼이 한 구조에 들어가지 않는다.4. 스트림 리소스 프리로딩
liveStreams.insert시contentDetails.isReusable=true로 만들고, OAuth 연결 시점에 1회 생성해 재사용한다. 방송마다는liveBroadcasts.insert+bind만 한다.5. ingest URL은 API 응답값 — RTMPS 전환
manager.go:30상수를 제거하고cdn.ingestionInfo.rtmpsIngestionAddress를 사용한다. 최종 URL은 문서 명시 규칙에 따라ingestionAddress + "/" + streamName. 현행 평문 RTMP에서 TLS로 전환되는 보안 개선이 함께 따라온다.6. 전역
YOUTUBE_STREAM_KEY제거폴백으로 남기지 않는다. 유실 후 미복구라 실사용이 0이며, 남기면 "사용자별 송출"과 "서버 전역 송출" 두 경로를 계속 부양해야 한다.
7. OAuth 경로 — connect / callback 2단계
콜백은 브라우저 리다이렉트로 도착하므로
Authorization헤더가 없다. state가 유일한 사용자 바인딩 수단이며, 기존 코드에 state 관리 패턴이 없으므로 신규 작성 영역이다./auth/*는MountAuthHTTPWithServices가 만드는 별도 mux다. 인증은handleWithdrawal(internal/auth/account_withdrawal_http.go:20-34)의 인라인 패턴을 따른다. 단 이 패턴은 DB에서 user status active를 확인하지 않으므로, 서비스 계층에서 상태를 확인하거나authenticateUser를 핸들러에 주입해 동등하게 맞춘다.8. Session 내부 구조
egress를
map[provider]*egressJob{egress, cancel, StreamState}형태로 보관한다. 동시 송출을 지금 구현하지 않아도(1개 제한 정책) 구조가 막지 않고,StreamState가 provider별로 자연히 분리되며, start 요청 body의provider필드가 그대로 키가 된다.RunTrack(internal/media/track.go:169)은 단일*RTMPEgress인자를 유지한다. 동시 송출이 실요구가 되는 시점에Enqueue지점(track.go:351)을 fan-out으로 바꾸는 국소 변경으로 확장한다.9. 스키마
OAuthAccount에 provider를 추가하지 않고 별도 테이블로 만든다.OAuthAccount는 로그인 신원이고 송출 연결은 다른 개념이며(로그인 Google 계정과 송출 YouTube 채널이 다를 수 있다), provider enum이 GORM check constraint와 versioned SQL 양쪽에 박혀 있다.(user_id, provider)유니크 인덱스internal/auth/provider_token_cipher.go(AES-256-GCM + 키 버전) 재사용. 단cmd/server/main.go:229-235에서 Apple 활성 시에만 생성되므로 배선 변경 필요internal/auth/migrate.go:16-21)과 versioned SQL(000003_*.up/down.sql) 양쪽 모두 갱신한다. 어긋나면 auto 환경과 versioned 환경의 스키마가 달라진다작업 범위
liveStreams.insert(isReusable)연결 시 1회 생성handleStartStream/handleStopStream실배선,StreamState갱신YOUTUBE_STREAM_KEY제거정책 · 제약
개발자 정책 2개 조항이 직접 걸린다.
할당량: 기본 10,000 units/day. 라이브 메서드별 개별 비용이 공식 문서에 없어 실측이 필요하다. 응답 헤더에는 quota 정보가 없으므로 Cloud Console의 Quotas 페이지 사용량 그래프를 관측한다. 방송 중 생존 판정은 API 폴링이 아니라 로컬 신호(FFmpeg 프로세스 생존, egress 프레임 카운트)로 하고,
healthStatus조회는 분 단위 이상 저빈도로 둔다.앱 검증: YouTube 스코프는 sensitive 등급(restricted 아님 → CASA 보안평가 면제). 미검증 상태에서는 Testing 100명 + refresh token 7일 만료, 프로덕션 미검증은 누적 100명 상한. 검증에는 도메인 소유권·홈페이지·개인정보처리방침·영어 데모 영상이 필요하며 최대 10일 소요.
계정 요건: 채널 인증 필요, 최초 라이브 활성화에 최대 24시간. 미승인 채널의 토큰으로 호출하면
insufficientPermissions가 반환되므로 사용자 안내에 활용한다.서비스 계정 플로우는 Live Streaming API와 비호환이다. 사용자 OAuth + refresh token 보관이 필수다.
구현 시 유의
internal/auth/refresh_store.go:60-160의 트랜잭션 + 행 락 패턴을 따르거나 사용자 단위로 직렬화한다.INNOLIVE_REQUIRE_SESSION_AUTH=false, 벤치마크용)에서는Session.UserID가uuid.Nil이다. 사용자별 송출 경로는 이 경우를 명시적으로 스킵해야 벤치가 깨지지 않는다.maskStreamKey(internal/media/egress.go:388-394)는 "마지막/뒤가 키"라는 형태를 가정하므로, 다른 형태의 URL을 쓰는 플랫폼에서는 마스킹이 뚫린다. 치지직 연동 시 반드시 함께 처리한다.착수 전 필수
코드 작성 전에 전 과정을 수동으로 1회 재연한다. OAuth Playground(자체 client_id 사용)로 토큰 발급 → curl로 broadcast/stream insert·bind → ffmpeg CLI로 테스트 송출 → active 확인 → complete. 반나절이면 autoStart 실동작·실제 타이밍·quota 소비량이 전부 확인되며, 설계 불확실성이 사실상 해소된다.
관계
MAX_SESSIONS재실측) — 송출이 실제로 켜지면 세션당 FFmpeg가 늘어난다