<aside> 📌
라이브 스트리밍 시스템은 밴드와 팬을 위한 실시간 오디오 방송 기능을 제공합니다. 핵심 미디어 서버로 MediaMTX를 사용하며 WebRTC(WHIP/WHEP)와 LL-HLS를 통한 저지연 스트리밍을 지원합니다. 시스템은 dual-path 설계를 채택하여, 개별 연주자는 접미사 -m{id}가 붙은 비공개 Member Path로 스트리밍하고, GStreamer Mixer가 이 입력들을 하나의 Main Path로 합성하여 청중에게 전달합니다.
| 서비스 | 역할 | 주요 설정 |
|---|---|---|
| MediaMTX | WebRTC(WHIP/WHEP)와 LL-HLS를 지원하는 핵심 미디어 서버입니다. | WHIP/WHEP 포트 8889, HLS 포트 8888 |
| Spring Backend | AudioStream 상태 전이(SCHEDULED → OPEN → CLOSED)를 관리합니다. | mediamtx.api-url, mixer-token |
| GStreamer Mixer | Member Path에서 RTSP로 스트림을 pull하여 오디오를 합성하는 Python 기반 프로세서입니다. | Python GStreamer supervisor |
| Redis | ZSet 구조로 시청자 프레젠스를 유지하며 score로 마지막 heartbeat 타임스탬프를 저장합니다. | volatile-ttl 삭제 정책 |
| NGINX | SSL termination과 CORS 처리를 담당합니다. | HLS를 위한 CORS 설정 |
graph TB
Broadcaster["방송자 (WHIP)"] -->|"publish"| MediaMTX["MediaMTX"]
CoHost["공동 호스트 (WHIP)"] -->|"publish"| MediaMTX
MediaMTX -->|"RTSP pull"| Mixer["GStreamer Mixer"]
Mixer -->|"Main Path publish (mixerToken)"| MediaMTX
MediaMTX -->|"canPublish / canRead"| Spring["Spring Backend"]
Spring --> Redis["Redis (volatile-ttl)"]
SSE["ViewerSseRegistry"] --> Redis
Nginx["NGINX (SSL / CORS)"] --> MediaMTX
MediaMTX -->|"WHEP / LL-HLS"| Viewer["시청자"]
stateDiagram-v2
[*] --> SCHEDULED : "createStream (scheduledAt 존재)"
SCHEDULED --> OPEN : "enterRoom / syncLiveState"
OPEN --> CLOSED : "leaveRoom / 방송 종료"
SCHEDULED --> CLOSED : "StreamCleanupScheduler (예약 취소)"
OPEN --> CLOSED : "StreamCleanupScheduler (ghost 스트림 종료)"
MediaMtxLivePoller는 몇 초마다 MediaMTX API에서 활성 경로 목록을 가져온 뒤 syncLiveState(319~343번째 줄)를 호출하여, MediaMTX에서는 활성이지만 DB에서는 SCHEDULED로 표시된 경로를 강제로 OPEN으로 전환하고, 실시간 UI 반영을 위한 Redis 기반 라이브 세션 캐시를 갱신합니다.
| 개념 | 코드 엔티티 | 파일 참조 |
|---|---|---|
| Stream Entity | AudioStream | AudioStream.java:18 |
| Member Mapping | StreamMember | StreamMember.java:13 |
| MTX 인증 로직 | canPublish, canRead | StreamServiceImpl.java:143-184 |
| 오디오 믹싱 | mixer.py | mixer/mixer.py:1 |
| 프레젠스 추적 | ViewerSsePresence | ViewerSsePresence.java:15 |
| 헬스 체크 | MediaMtxLivePoller | MediaMtxLivePoller.java:15 |
| 상태 | 설명 |
|---|---|
| INVITED | 방송자가 스트림 생성 시 밴드 멤버를 초대한 상태입니다. |
| UPGRADE_REQUESTED | 청취 중이던 밴드 멤버가 공동 호스트 승격을 요청한 상태입니다. |
| ACCEPTED | 공동 호스트로 확정되어 오디오를 발행할 수 있는 상태입니다. |
| DECLINED | 초대가 거절되었거나 업그레이드 요청이 거부된 상태입니다. |
sequenceDiagram
participant Listener as "청취자"
participant Server as "StreamServiceImpl"
participant Broadcaster as "방송자"
Listener->>Server: "requestCoHostUpgrade"
Server->>Server: "StreamMember 생성 (UPGRADE_REQUESTED)"
Server-->>Broadcaster: "SSE coHostUpgradeRequested"
Broadcaster->>Server: "acceptCoHostUpgrade"
Server->>Server: "status -> ACCEPTED"
Server-->>Listener: "SSE coHostUpgradeAccepted (memberPath 포함)"
| 이벤트명 | 페이로드 | 수신자 | 트리거 |
|---|---|---|---|
| viewerCount | Long | 전체 시청자 | Redis ZCARD 값 변경 |
| coPublisherJoined | CoPublisher DTO | 방송자 | 공동 호스트가 MediaMTX로 발행을 시작함 |
| coHostUpgradeRequested | CoHostUpgradeEvent | 방송자 | requestCoHostUpgrade 호출 |
| coHostUpgradeAccepted | CoHostUpgradeEvent | 요청자 | acceptCoHostUpgrade 호출 |
Member Path 형식은 {mainPath}-m{userId}이며, 이 경로는 coHostUpgradeAccepted SSE 이벤트를 통해 해당 사용자에게만 전달되어 일반 시청자가 발행을 시도하지 못하도록 보장합니다. ViewerSseRegistry는 ConcurrentHashMap과 compute 블록을 사용하며, userLatest 맵으로 멀티탭 시나리오를 처리하여 사용자가 새 탭을 열면 기존 SSE 연결을 명시적으로 종료해 중복 카운팅을 방지합니다.
| 분류 | Method | Endpoint | 설명 |
|---|---|---|---|
| Lifecycle | POST | /lives/{liveId} | 라이브 방에 입장하고 SSE 시청자 추적을 초기화합니다. |
| Lifecycle | POST | /lives/{liveId}/leave | 라이브 방에서 정상적으로 퇴장합니다. |
| Discovery | GET | /lives/home | 통합 대시보드를 조회합니다(Fan/Band 모드별). |
| Discovery | GET | /lives/live-now/all | 활성 스트림 전체를 페이지네이션으로 조회합니다. |
| Replay | POST | /lives/{liveId}/replay | 로컬 녹화본의 S3 업로드를 요청합니다. |
| Replay | GET | /lives/{liveId}/replay | 다시보기 메타데이터와 재생 URL을 조회합니다. |
| Replay | GET | /lives/{liveId}/replay/playlist | HLS .m3u8 매니페스트를 생성합니다. |
| --- | --- | --- |
다시보기 정렬은 LATEST(ID 내림차순)과 POPULAR(조회수 내림차순)을 지원하며, 대용량 VOD 목록 처리를 위해 커서 기반 페이지네이션을 사용합니다. 조회수는 높은 동시성 상황에서 갱신 유실을 방지하기 위해 streamReplayRepository.increaseViewCount(replayId)로 원자적으로 증가시킵니다(97번째 줄).