FAST 채널은 상업 광고를 통해 수익을 창출합니다. 전체 비즈니스 모델은 단 하나의 가정에 기반을 두고 있습니다. 채널이 "광고로 전환"하라고 지시할 때, 청취하는 모든 다운스트림 시스템(ad server, SSAI splicer, smart-TV player)이 정확히 같은 순간, 정확히 같은 시간 동안 이를 듣는다는 가정입니다. SCTE-35는 이 메시지를 전달하는 표준 프로토콜입니다. 이를 조용히 잘못 처리하면(그리고 침묵은 기본 실패 모드입니다) 광고가 렌더링되지 않거나, 잘못된 프레임에서 시작되거나, 길이가 잘못될 수 있습니다. 시청자는 슬레이트를 보게 되고, 수익은 발생하지 않습니다.
이것은 mStudio의 FAST 채널이 운영자 친화적인 입력에서 표준을 준수하는 SCTE-35 큐를 어떻게 방출하는지, 그리고 왜 우리가 이를 한 번의 광고 삽입 대신 세 개의 큐로 구성했는지에 대한 엔지니어링 이야기입니다.
빠른 개요
| 측면 | 세부 정보 |
|---|---|
| 도메인 | AWS MediaLive FAST 채널을 위한 SCTE-35 광고 중단 신호 |
| 운영자 워크플로우 | 프로그램별 광고 중단 편집기 — 초 단위 위치, 초 단위 길이 |
| 광고 중단당 방출되는 큐 | 세 가지 — 광고 시작 TimeSignal, SpliceInsert, 광고 종료 TimeSignal |
| 길이 제약 | 10–120초, 데이터 계층에서 검증됨 |
| 다운스트림 소비자 | Player, ad server 및 AWS MediaTailor (설계되었으나 아직 연결되지 않음)가 예시입니다. |
| 상태 | SCTE-35 큐 방출은 프로덕션 중; SSAI 통합은 설계 중 |
비즈니스 문제
FAST 채널의 모든 상업 광고는 수익 이벤트입니다. 채널은 "여기에 광고가 표시되며, 30초 동안 지속될 예정이니 준비해 주세요."라고 알리는 마커를 보냅니다. Google Ad Manager, AWS MediaTailor, 지역 ad server, smart-TV player SDK와 같은 다운스트림 시스템은 이 마커를 읽고 무엇을 삽입할지 결정합니다.
마커가 올바르면 광고 중단은 깔끔하게 실행되고, 노출 수가 집계되며, 운영자는 수익을 얻습니다. 마커가 잘못되면(잘못된 길이, 잘못된 형태, 잘못된 형식) 광고 시스템은 이를 거부하거나(광고가 제공되지 않음) 잘못 받아들입니다(잘못된 길이의 광고 또는 다음 프로그램으로 넘어가는 광고). 두 가지 결과 모두 실제 비용을 발생시키며, 두 가지 모두 조용히 실패합니다. 채널은 계속 스트리밍되고, 시청자는 슬레이트 또는 어색한 컷을 보게 됩니다. 수익화 파이프라인은 더 낮은 수치만 생성할 뿐, 추적할 오류 메시지는 없습니다.
엔지니어링 목표는 "광고 지원"이 아닙니다. 그것은: 운영자가 정의한 모든 광고 중단이 표준을 준수하는 SCTE-35 신호로, 운영자가 선택한 정확한 프레임에, 항상 모든 다운스트림 소비자에게 도달해야 한다는 것입니다.
SCTE-35는 실제로 무엇인가 (90초 버전)
SCTE-35는 비디오 스트림에 큐 메시지를 삽입하기 위한 표준입니다. 큐는 광고 콘텐츠를 전달하지 않고, 신호를 전달합니다 — "광고 중단이 여기에서 시작됩니다," "광고 중단이 여기에서 끝납니다," "이 세그먼트는 N초 길이입니다." 다운스트림 시스템은 이러한 신호를 읽고 이에 따라 작동합니다. SSAI 계층은 실제 광고 creative를 HLS manifest에 삽입하고, smart-TV player는 overlay를 트리거하며, ad server는 슬롯 기회를 기록합니다.
당사의 사용 사례에 중요한 두 가지 큐 형태는 다음과 같습니다:
- SpliceInsert — 원본 SCTE-35 큐. SpliceEventId, 초 단위 길이, 그리고 "out of network" 플래그를 전달합니다. 대부분의 기존 ad server가 이를 읽습니다.
- TimeSignal — 더 새롭고 표현력이 풍부한 큐. SegmentationTypeId (52 = Provider Ad Start, 53 = Provider Ad End)와 90,000-tick 단위의 SegmentationDuration을 포함하는 SegmentationDescriptor를 전달합니다. SSAI 시스템과 최신 player는 시작과 끝을 깔끔하게 짝지을 수 있기 때문에 이 형태를 선호합니다.
두 형태 모두 올바른 SCTE-35입니다. 다운스트림 소비자마다 선호하는 형태가 다릅니다. 한 가지 형태만 방출하면 수익을 놓치게 됩니다.
이것이 중요한 이유. 단순한 구현은 광고 중단당 하나의 SpliceInsert를 방출하고 완료되었다고 간주합니다. 이 광고 중단은 레거시 player에서는 작동하지만, SSAI에서는 조용히 실패합니다. 광고 inventory의 절반은 수익화되고, 절반은 그렇지 않습니다. 수익 수치가 낮게 나올 때까지 어떤 부분이 문제인지 알 수 없을 것입니다.
단순한 접근 방식이 실패하는 이유
지름길은 모두 거의 작동하기 때문에 유혹적입니다.
- "인코딩 시점에 원본 비디오에 광고를 삽입." 유연성이 없습니다. 운영자는 A/B-test 배치, deploy 후 광고 변경, 지역 또는 대상 시청자 광고 실행을 할 수 없습니다. FAST 채널이 존재하는 주된 이유는 동일한 content로 여러 방식으로 수익을 창출하는 것인데, encode 시점에 광고를 삽입하면 이러한 가능성이 차단됩니다.
- "클라이언트 측 광고 삽입 활용." ad-blocking이 쉽습니다. device별로 다른 code path가 필요합니다. server-side personalisation이 불가능합니다. 또한 SSAI를 완전히 우회하므로 CPM이 낮아집니다.
- "광고 중단당 하나의 SpliceInsert 만 방출." 가장 흔한 production 실수입니다. 일부 player에서는 작동하지만, TimeSignal 큐로 시작/끝을 짝지으려는 SSAI 시스템에서는 조용히 무시됩니다. 부분적인 수익화만 이루어지며, 조사할 error message가 없습니다.
- "사양에 언급되어 있으니 모든 것을 90 kHz 틱으로 표현." 사양은 그보다 더 까다롭습니다. SpliceInsert.Duration은 초 단위입니다. SegmentationDuration은 90 kHz 틱 단위입니다. 이들을 혼동하면 schema validation을 통과하고 ship out되지만, player에서 malformed duration으로 인해 실패하는 큐가 생성됩니다. encoder는 이를 감지하지 못하고, player는 광고 중단을 그냥 건너뜁니다.
- "MediaLive가 확인하도록 허용." MediaLive는 확인하지 않습니다. MediaLive는 overlapping break, zero-duration break, 그리고 impossible segmentation duration을 불평 없이 받아들입니다. 확인은 MediaLive가 action을 보기 전에 이루어져야 합니다.
우리가 가진 지렛대는 operator UI(ad break가 단순히 {position, duration} 쌍인 곳)와 MediaLive schedule(모든 cue가 perfect해야 하는 곳) 사이의 경계였습니다.
우리의 해결책
ad break를 end-to-end 일급 data object로 처리합니다. operator는 가능한 가장 간단한 용어로 이를 정의합니다. backend는 schema layer에서 한 번 validate합니다. Lambda는 deploy 시 이를 three-cue stack으로 translate하며, 각 cue는 자체 spec이 요구하는 time base에 따라 표현됩니다. stack의 다른 어떤 것도 SCTE-35에 대해 알 필요가 없으며, translation은 다른 모든 schedule action을 소유하는 동일한 Lambda 내의 단일 function에서 이루어집니다.
다이어그램 1 · 종단간 광고 마커 파이프라인

아키텍처
- frontend ad-break editor — operator는 program start로부터의 position offset(초 단위)과 duration(초 단위)을 지정하여 program별 ad break를 추가합니다. bumper slot은 data model의 일부이며 playout layer에 준비되어 있습니다.
- AdMarker collection (MongoDB) — ad break가 있는 program당 하나의 document로, video를 참조합니다. adBreaks[] array는 Mongoose가 save time에 10 ≤ duration ≤ 120을 강제하는 {position, duration}을 저장합니다. bumper는 downstream pairing을 위한 adBreakId reference를 가집니다.
- NestJS backend (schedule.service.ts) — deploy time에 각 schedule을 해당 AdMarker에 join하고 field 이름을 Lambda의 contract(position → offsetSeconds, duration → durationSeconds)으로 변경합니다. 한 번의 bulk fetch로 N+1 문제를 방지합니다.
- Lambda orchestrator (fastChannel-lambda-fun/index.js) — SCTE-35 translation을 담당합니다. 각 ad break에 대해, input switch 및 watermark를 전달하는 동일한 BatchUpdateScheduleCommand에 three-cue stack을 방출합니다. action 이름은 program별로 version 관리되므로, partial deploy 후에도 orphan sweep이 이를 pairing할 수 있습니다.
- AWS MediaLive — action을 수신하고, operator가 선택한 초에 HLS manifest에 #EXT-SCTE35 marker를 방출합니다.
주요 엔지니어링 결정
1. 광고 중단당 하나의 큐가 아닌 세 개의 큐
모든 ad break는 program 내의 동일한 logical moment에 대해 세 가지 discrete action을 방출합니다.
다이어그램 2 · 단일 광고 중단 라이프사이클

시작 및 종료 TimeSignal 큐는 SegmentationUpid를 공유하여 downstream system이 이를 deterministically pairing할 수 있도록 합니다. SpliceInsert는 player-level deduplication을 위한 unique SpliceEventId를 가집니다. 서로 다른 consumer는 서로 다른 cue를 읽습니다. 세 가지 모두를 방출함으로써 오늘날 우리가 중요하게 생각하는 모든 contract와 미래의 모든 plausible contract를 포괄합니다.
흔한 production mistake. 하나의 SpliceInsert만 방출하고 나머지 ecosystem이 알아서 처리할 것이라고 가정합니다. SSAI system은 matching TimeSignal 큐가 없는 break를 quietly drop하고; classic ad server는 TimeSignal-only signal을 ignore합니다. 세 가지 큐가 모두 없으면, 모든 ad break는 downstream stack의 일부에 의해 monetise되고 나머지는 missed됩니다 — 그리고 이 사실은 revenue report가 아닌 log를 통해서 알게 됩니다.
2. 하나의 함수에서 조정되는 두 가지 시간 기준
SpliceInsert.Duration은 초 단위입니다. TimeSignal descriptor 내부의 SegmentationDuration은 90,000-tick 단위입니다. 동일한 logical duration을 두 가지 방식으로 encoding하며, 현장에서 가장 흔한 SCTE-35 bug는 이들을 혼동하는 것입니다.
다이어그램 3 · 시간 변환 로직

conversion은 buildProgramActions에 오직 그곳에만 존재합니다. cue duration이 잘못 나올 경우 찾아볼 수 있는 하나의 canonical place가 있고, spec이 evolve할 경우 변경할 수 있는 하나의 place가 있습니다.
우리가 의도적으로 선택한 트레이드오프. AdMarker document에 두 가지 time base를 모두 저장하고 Lambda가 이를 copy through하도록 할 수도 있었습니다. 하지만 의도적으로 그렇게 하지 않았습니다. seconds value만 저장함으로써 operator가 set해야 할 숫자는 하나뿐이고, validate해야 할 숫자도 하나뿐이며, 잘못될 수 있는 숫자도 하나뿐입니다. 90 kHz value는 derived된 것이며 저장되지 않습니다. derived data는 drift하지 않습니다.
3. 위치는 상대적, 발동 시간은 절대적
operator는 "program 시작 720초 지점에 ad break"라고 말합니다. Lambda는 실제 UTC fire time을 programStartTime + offsetSeconds × 1000으로 계산하여 해당 action에 stamp합니다. input switch, watermark on, program end 등 다른 모든 schedule action도 동일한 offset-to-timestamp pipeline을 사용합니다. ad cue는 다른 모든 것과 동일한 frame boundary에 도달하며, 어떤 clock이 timeline을 소유하는지에 대한 ambiguity가 없습니다.
4. 와이어가 아닌 데이터 계층에서의 검증
AdMarker schema는 Mongoose save time에 duration ∈ [10, 120]초를 강제합니다. 범위를 벗어나는 break는 Lambda에 도달하지 않습니다. program end를 넘어 확장될 ad break는 reject되기보다는 build time에 clip됩니다 — operator는 하나의 bad break로 인해 작업을 잃지 않습니다. 발생할 수 있는 bug보다 발생할 수 없는 bug 유형이 더 흥미롭습니다.
5. SSAI와 분리된 큐 방출
오늘날 우리가 ship하는 것은 signalling layer입니다. AWS MediaTailor를 통한 server-side ad insertion(이 cue를 사용하여 실제 ad creative를 HLS manifest에 splice할 것임)은 fully designed되었지만 아직 wired in되지 않았습니다. 의도적인 ordering은 다음과 같습니다: SSAI를 turn on하기 전에 production 환경에서 real channel에 사용되는 cue layer를 먼저 correct하게 만듭니다. integration이 go live되면 cue는 이미 존재할 것입니다. downstream system은 day one부터 clean contract를 consume할 수 있습니다.
이 ordering이 중요한 이유. cue가 wrong할 때 SSAI debugging은 brutal합니다. cue에 fault가 있더라도 모든 failure가 SSAI failure처럼 보이기 때문입니다. cue layer를 isolation 상태로 먼저 ship하고 real player와 ad server를 대상으로 validate함으로써, integration confusion의 entire category가 발생하기 전에 제거했습니다.
결과
- operator가 정의하는 모든 ad break는 channel의 HLS manifest에서 standards-conformant three-cue SCTE-35 stack을 right second에, right time base에 따라 방출합니다.
- translation layer는 하나의 function 내에 존재합니다. spec 변경 또는 new downstream consumer는 codebase 전체를 hunt하는 것이 아니라 single-file change로 처리됩니다.
- duration validation은 어떤 cue가 encoder에 도달하기 전에 data layer에서 실행됩니다. operator mistake는 air time에 silently 나타나는 것이 아니라 UI에 surface됩니다 — malformed cue는 MediaLive에 도달할 수 없습니다.
- cue generation은 deterministic합니다. identical operator input은 identical SCTE-35 action을 생산하므로, same break는 every deploy와 every channel에서 same way로 behave합니다.
- signalling layer는 ship되어 live 상태입니다. SSAI consumer (MediaTailor)는 designed되었으며 wired in될 준비가 되어 있습니다. wired in되면, every existing channel의 cue는 이미 이를 기다리고 있을 것입니다.
기술 스택: AWS MediaLive · AWS Lambda · NestJS · MongoDB · TypeScript · Node 18 · SCTE-35 (Scte35TimeSignalSettings, Scte35SpliceInsertSettings) · AWS SDK v3

