PokeClip에서는 생방송을 보다가 지난 장면으로 돌아가 클립 구간을 찾아야 합니다. 지금 장면을 빨리 전달하는 일과 지난 한 시간을 탐색하게 하는 일은 같은 방송에서 출발하지만, 필요한 재생목록의 범위와 저장 방식은 다릅니다. 한 URL에 두 책임을 계속 맡기면 라이브 목록을 줄이는 변경이 되감기를 없애는 변경이 됩니다.
저는 미디어·인프라와 서비스 계약을 맡아 Live와 DVR의 URL을 분리하고, DVR 목록은 DB 장부에서 만들어 S3에 발행하는 방향을 정했습니다. 이후 되감기 재생이 라이브 범위에 도달했을 때 어떤 조건으로 다시 연결할지도 정했습니다. 설계 초안·구현·실측 실행·리뷰에는 AI를 활용했고, 웹의 주 구현은 팀원이 맡았습니다. 이 글은 그 사이의 책임과 판단을 설명합니다.
상태 기준은 2026년 10월 28일 확인한 코드와 10월 27일 작업 기록입니다. 발행 계층과 웹 되감기 기본 연동은 병합됐습니다. 자동 핸드오프는 구현·검증 중이며, 아래의 시간축 문제까지 해결한 상태는 아닙니다.
Live를 짧게 유지하려면 DVR이 먼저 있어야 했습니다
DVR은 이미 방송한 구간을 되돌려 보는 기능입니다. 여기서 매니페스트는 플레이어가 읽을 미디어 조각과 순서·시각을 적은 재생목록을 뜻합니다. Live와 DVR을 나눌 때는 주소뿐 아니라 목록을 만드는 주체와 사용할 미디어도 함께 나눴습니다.
| 경로 | 목록을 만드는 곳 | 맡긴 책임 |
|---|---|---|
| Live | MediaMTX | 현재 송출의 LL-HLS 재생목록과 미디어 제공 |
| DVR | Go 사이드카 | DB 장부에서 최근 1시간 목록을 만들고 S3·CDN으로 제공 |
| 편집 원본 | 녹화·업로드 계층 | 선택할 오디오 6트랙을 보존해 렌더 계층에 전달 |
되감기용 미디어는 영상 1개·오디오 1개로 재포장하고, 편집 원본의 여러 오디오는 따로 보존합니다. 시청 경로를 단순하게 만들면서 편집할 재료를 잃지 않기 위한 구분입니다.
라이브 창은 4초 조각 7개인 약 28초를 선택했습니다. 이 값은 방송 지연 28초를 뜻하지 않습니다. 10월 17일 CloudFront 경유 비교에서는 14초·28초·7초 창의 저지연 재생 지연이 같았고, 탭을 15초 멈췄다 복귀하는 조건에서 28초 창만 살아남았습니다. 30초 정지는 28초 창에서도 실패했습니다. 실험에서 확인한 차이는 복귀 여유였으며, 창 축소로 지연을 개선했다고 해석하지 않았습니다.
적용 순서도 조건으로 뒀습니다. 기존 라이브 목록 900개가 한 시간 되감기를 겸하던 환경에서 먼저 7개로 줄이면 사용자가 과거 장면을 볼 수 없습니다. DVR 전용 URL이 실제 시청 경로에 연결된 뒤 라이브 창을 줄인다는 순서를 지켜야 했습니다.
목록은 시청자가 읽을 때마다 만들지 않기로 했습니다
Go 계층은 파일 색인과 업로드 상태를 DB에 기록하고, 그 상태로 DVR 목록을 렌더합니다. 시청자는 발행된 목록을 읽습니다. 발행 경로가 미디어 요청까지 받도록 만들지는 않았습니다. 같은 목록을 반복 계산하는 요청 경로를 줄이는 대신, 목록을 공개하는 쓰기 경로가 복구를 책임지는 구조입니다.
늦게 끝난 이전 작업이 새 목록을 덮지 않도록 작성 권한, 세대 예약, 객체 버전 확인을 나눴습니다. S3의 If-Match는 예상한 ETag와 현재 객체가 맞는지 검사하지만, DB의 작성 권한까지 확인하지는 않습니다. 그래서 DB 상태와 S3 쓰기 사이의 성공·실패를 다시 판정하는 절차가 필요했습니다. S3 조건부 쓰기 문서
10월 17일에는 compose에서 720p30 영상과 AAC 음성을 90초간 합성 송출하면서 사이드카를 재시작했습니다. 실제 S3에 원본·재포장 조각 각 22개와 init·목록을 발행했고, 재시작 후 새 작성자가 같은 URL로 발행을 이어가며 세대가 역행하지 않는 것을 확인했습니다. 발행 계층은 PR #289로 10월 19일 병합됐습니다. 작성 권한과 복구 실험은 별도 글에 자세히 남겼습니다.
URL을 바꾸기 전에 같은 장면인지 확인해야 했습니다
경로를 분리하면 되감기와 라이브가 만나는 지점이 생깁니다. 송출이 잠깐 끊기거나 대체 화면이 나오는 동안에도 저장된 녹화를 재생하면, 1배속에서도 라이브 범위에 도달할 수 있습니다. 이때 자동 핸드오프, 즉 재생 소스를 DVR에서 Live로 넘기는 동작을 검토했습니다.
핵심은 “두 URL의 끝이 가까운가”보다 전환하면서 아직 보지 않은 녹화를 건너뛰는가였습니다. Live와 DVR의 플레이어 시간이 같은 장면을 가리킨다는 보장부터 필요합니다. HLS의 EXT-X-PROGRAM-DATE-TIME은 미디어 조각의 첫 샘플에 절대 시각을 연결하는 태그입니다. 이 매핑 자체가 어긋나면 시각을 비교하는 전환 조건도 잘못된 결론을 냅니다. RFC 8216의 PDT 정의
제가 확정한 범위와 실패 동작은 다음과 같습니다. 일부는 후속 구현·검수에 반영 중입니다.
| 상황 | 정한 동작과 이유 |
|---|---|
| 같은 방송의 DVR 꼬리가 Live와 만남 | 못 본 녹화를 건너뛰지 않는 조건에서 자동 전환 |
| 방송이 끊겼다가 복귀 | 같은 방송인 경우에만 자동 연결 대상으로 취급 |
| 다른 방송이 시작됨 | 자동으로 넘기지 않고 새 방송 진입은 사용자 선택으로 남김 |
| 전환 확인 실패 | 10초 확인 게이트 실패 시 DVR에 머무르며 LIVE 버튼 안내 |
| 전환 중 사용자가 일시정지 | 되돌림에서도 일시정지를 보존 |
| 이전 요청의 응답이 늦게 도착 | 요청 순번을 대조해 현재 상태를 덮지 못하게 함 |
목록의 ENDLIST도 재생 완료와 구분했습니다. 이 표시는 목록에 조각이 더 추가되지 않는다는 뜻이지 사용자가 끝까지 봤다는 뜻은 아닙니다. 목록 종료, 현재 재생 위치, 방송 세션 종료를 각각 다뤄야 다음 행동을 고를 수 있었습니다. RFC 8216의 ENDLIST 정의
전환 시험에서 먼저 드러난 것은 시간축의 오차였습니다
10월 27일 합성 송출 20분째에 같은 프레임의 시각을 대조했습니다. Live PDT에서 DVR PDT를 뺀 값의 중앙값은 −75.9초였고, 22개 표본은 약 −73~−78초 범위였습니다. ffmpeg 합성 입력의 실험이며 실제 OBS 송출 전체를 검증한 결과는 아닙니다.
원인을 추적하니 장부의 다음 시각을 이전 시각에 조각 길이를 더한 값과 벽시계 중 큰 값으로 정하고 있었습니다. 조각 길이에 영상보다 긴 음성 트랙 길이가 들어오면 작은 차이가 누적됐습니다. 실제 미디어 타임스탬프의 간격과 장부가 붙인 시각이 달라진 것입니다.
저는 녹화기가 남긴 디코딩 시각(DTS)·절대 시각(NTP) 정보인 mtxi를 활용하는 방향을 택했습니다. 다만 시각 보정은 클립의 구간 조회와 재시작 후 재적재에도 영향을 줍니다. 10월 27일 기록에서는 수정 설계가 검토 중이므로 해결 성과로 적지 않습니다. 자동 전환 조건을 만드는 과정이 서버의 시간축 전제를 검증하는 계기가 된 상태입니다.
| 범위 | 확인된 상태 |
|---|---|
| DVR 발행·재시작 | PR #289 병합, 실제 S3 합성 송출 실험 완료 |
| 웹 되감기 기본 연동 | PR #436으로 10월 27일 병합 |
| 자동 핸드오프 | 설계 확정, 일부 구현 커밋 존재, 최종 연결·검수·웹 PR 남음 |
| PDT 보정 | mtxi 기반 방향 선택, 수정 설계 검토 중 |
Live와 DVR을 나눈 이유는 서로 다른 보관·갱신 요구를 각각 다루기 위해서였습니다. 그 대가로 발행 정합성과 두 재생 경로의 연결 조건을 직접 책임져야 했습니다. 지금 확보한 성과는 분리된 발행 구조와 되감기 연동이며, 자동 전환의 완료 기준은 사용자가 보던 장면과 의도가 다음 재생 경로에서도 이어지는지입니다.