트랜스코딩 지연으로 SSAI 스티칭이 실패하는 케이스
라이브 스트림에 SSAI로 광고를 끼워 넣다 보면, 스트림 자체는 멀쩡한데 특정 광고 구간에서만 광고가 하나도 안 나가고 슬레이트나 원본 방송으로 채워지는 일을 종종 만납니다. 원인은 여러 가지지만 흔하게 걸리는 건 크리에이티브(실제 광고 영상 소재)가 아직 트랜스코딩을 끝내지 못한 경우입니다. ADS가 VAST 응답으로 광고 소재의 원본 파일을 알려줘도 스트림에 실제로 끼워 넣으려면 그 스트림의 코덱과 비트레이트, 세그먼트 규격에 맞춰 미리 변환해둔 버전이 있어야 합니다. 그 변환이 아직 안 끝난 겁니다.
같은 원인인데도 이걸 어느 단위로 처리하느냐는 플랫폼마다 다릅니다. 한쪽은 손실을 광고 하나에 가두고, 다른 한쪽은 광고 구간 전체를 통째로 날려버립니다.
광고 단위로 처리하는 MediaTailor
AWS MediaTailor는 이 상황을 개별 광고 단위로 처리합니다. 결정 로직을 의사코드로 옮기면 이렇게 생겼습니다.
def build_avail(candidate_ads: list[Ad], avail_duration: float) -> FilledAvail:
creative_ads =
skipped_ads =
for ad in candidate_ads:
if ad.transcode_status == "READY":
creative_ads.append(ad)
else:
# TRANSCODE_IN_PROGRESS, TRANSCODE_ERROR 등은
# 이 광고 하나만 걸러내고 나머지는 그대로 진행한다
skipped_ads.append(SkippedAd(ad, reason="NEW_CREATIVE"))
filled_duration = sum(ad.transcoded_duration for ad in creative_ads)
fill_rate = filled_duration / avail_duration
return FilledAvail(
creative_ads=creative_ads,
skipped_ads=skipped_ads,
fill_rate=fill_rate, # 9/10이 준비됐다면 0.9
)여기서 for 루프는 광고를 하나씩 따로 판정합니다. ADS가 광고 구간 하나를 채울 광고 10개를 반환했는데 그중 9개는 이미 트랜스코딩이 끝나 있고 1개만 아직 처리 중이라면, MediaTailor는 그 1개만 NEW_CREATIVE라는 사유로 걸러내고 나머지 9개는 그대로 스티칭합니다. 이 판정 결과는 MediaTailor/AdDecisionServerInteractions 로그 그룹의 FILLED_AVAIL 이벤트로 그대로 남습니다. 실제 스키마를 간추리면 이런 모양입니다.
{
"eventType": "FILLED_AVAIL",
"eventTimestamp": "2026-09-24T09:12:03Z",
"sessionId": "e039fd39-09f0-46b2-aca9-9871cc116cde",
"avail": {
"availId": "4821",
"originAvailDuration": 300,
"filledDuration": 270,
"fillRate": 0.9,
"numAds": 9,
"creativeAds": [ /* 정상 삽입된 9개 */ ],
"skippedAds": [
{
"creativeUniqueId": "ad-9f21c",
"skippedReason": "NEW_CREATIVE",
"vastDuration": 30
}
]
}
}여기서 볼 값은 avail.fillRate입니다. 1.0이 아니라 0.9로 찍혔습니다. 광고 구간(300초, 30초짜리 10개)에서 1개가 빠져 270초만 채워졌다는 뜻이고 나머지 9개는 그대로 시청자에게 나갑니다. 운영 중에 이 패턴을 직접 확인하고 싶다면 CloudWatch Logs Insights에서 이런 쿼리로 걸러냅니다.
fields @timestamp, avail.availId, avail.fillRate, avail.skippedAds.0.skippedReason
| filter eventType = "FILLED_AVAIL"
| filter avail.skippedAds.0.skippedReason = "NEW_CREATIVE"
| sort @timestamp descfillRate가 1.0 미만이면서 skippedReason이 NEW_CREATIVE인 구간만 걸러내면, 트랜스코딩 타이밍 때문에 놓친 광고가 얼마나 되는지 바로 집계할 수 있습니다.
광고 구간 전체를 포기했던 StreamPackage
Tencent Cloud StreamPackage에서 같은 상황을 들여다보니 처리 단위가 완전히 달랐습니다. 위 MediaTailor 의사코드의 for 루프가 있을 자리에 개념적으로는 이런 판정이 들어가 있었습니다.
def build_avail_old(candidate_ads: list[Ad], avail_duration: float) -> FilledAvail:
if any(ad.transcode_status != "READY" for ad in candidate_ads):
# 하나라도 안 끝났으면 이미 준비된 나머지까지 전부 포기
return FilledAvail(creative_ads=, skipped_ads=candidate_ads, fill_rate=0.0)
creative_ads = candidate_ads
return FilledAvail(creative_ads=creative_ads, skipped_ads=, fill_rate=1.0)any 판정 하나가 전체 결과를 좌우합니다. ADS가 반환한 광고 세트에서 단 하나라도 트랜스코딩이 끝나지 않았으면 이미 준비가 끝난 나머지 광고까지 포함해서 그 광고 구간 전체의 스티칭을 포기해버렸습니다. 10개 중 9개가 재생 가능한 상태여도 1개가 걸리면 fill_rate는 위 MediaTailor 예시의 0.9가 아니라 0.0으로 떨어지고 광고 구간 전체가 슬레이트나 라이브 원본으로 넘어갔습니다. 트랜스코딩 지연은 광고 소재가 새로 들어올 때마다 늘 생깁니다. 그 흔한 지연 하나가 판정 로직의 any 한 줄 때문에 광고 구간 단위 손실로 번진 셈입니다.
실제 운영 로그를 놓고 두 플랫폼의 처리 결과를 나란히 비교해봤습니다. 같은 조건(광고 10개 중 1개만 트랜스코딩 지연)에서 MediaTailor는 9개 몫의 Fill-rate(0.9)를 유지했고 StreamPackage는 Fill-rate가 0으로 떨어졌습니다. 그대로 광고 재고 손실로 이어지는 차이입니다.
개선 이후
이 차이를 확인하고 원인을 재현해 검증한 뒤 최근 업데이트로 StreamPackage의 처리 방식을 개선했습니다. any 한 줄에 전체가 걸려 있던 판정을 걷어내고 MediaTailor의 for 루프와 같은 방향으로 바꿨습니다.
def build_avail_new(candidate_ads: list[Ad], avail_duration: float) -> FilledAvail:
creative_ads = [ad for ad in candidate_ads if ad.transcode_status == "READY"]
skipped_ads = [ad for ad in candidate_ads if ad.transcode_status != "READY"]
filled_duration = sum(ad.transcoded_duration for ad in creative_ads)
fill_rate = filled_duration / avail_duration
return FilledAvail(creative_ads=creative_ads, skipped_ads=skipped_ads, fill_rate=fill_rate)이제는 광고 세트 안에서 트랜스코딩이 끝난 광고만 골라 스티칭하고 아직 준비되지 않은 자리만 슬레이트로 채웁니다. 손실이 개별 광고 단위에서 멈추니 같은 조건(10개 중 1개 지연)에서도 Fill-rate가 꾸준히 0.9 언저리로 나옵니다.
StreamPackage 콘솔에서 확인하는 지표
MediaTailor 쪽은 CloudWatch Logs Insights로 fillRate를 직접 조회할 수 있었습니다. StreamPackage에도 같은 확인을 콘솔에서 그대로 할 수 있는 지표가 있습니다. Ad Insertion Statistics 화면에 들어가면 다음 다섯 가지를 볼 수 있습니다.
| 지표 | 정의 |
|---|---|
| Number of Successful Ad Requests | ADS로부터 성공적으로 받은 VAST 응답의 총합 |
| Number of Failed Ad Requests | 광고를 못 받은 요청 수(VAST 응답에 광고 없음, ADS 타임아웃, ADS 오류 등). ADS에 보낸 요청 수에서 VAST 응답 수를 뺀 값과 같습니다 |
| Exposure Count | 전체 광고 노출 수의 합 |
| Mid-roll Ad Personalization Fill Rate | 라이브 스트리밍의 미드롤 광고 교체 성공률. 채워진 avail 수를 미드롤에서 찾은 광고 마커 수로 나눈 값 |
| Pre-roll Ad Replacement Success Rate | 라이브 스트림의 프리롤 광고 교체 성공률. 채워진 광고 슬롯 수를 프리롤 광고 요청 수로 나눈 값 |
이 중 Mid-roll Ad Personalization Fill Rate가 앞서 의사코드로 다룬 fill_rate에 정확히 대응하는 콘솔 지표입니다. 개선 전에는 트랜스코딩 지연이 하나라도 생기면 이 지표가 통째로 0까지 떨어졌고, 개선 후에는 지연된 광고 하나 몫만큼만 깎이는 수준으로 안정됐습니다. 로그를 직접 조회하는 MediaTailor의 방식과는 다르지만, StreamPackage도 콘솔 화면만으로 같은 개선을 확인할 수 있습니다.
트랜스코딩 지연 자체를 완전히 없앨 수는 없습니다. 광고 소재는 계속 새로 들어오고 변환에는 실제 시간이 걸립니다. 다만 그 지연이 광고 하나의 손실로 끝나는지, 아니면 any 한 줄 때문에 광고 구간 전체의 손실로 번지는지는 판정 로직을 어떻게 설계했느냐에 달려 있습니다. SSAI를 도입할 때는 트랜스코딩 파이프라인의 속도만큼이나 지연이 생겼을 때 실패 범위를 얼마나 좁게 가둬주는지, 그 판정 로직이 광고 하나 단위로 짜여 있는지를 같이 확인해볼 만합니다.