Skip to content

StreamPackage Click-through, 스티칭된 광고에 없는 버튼을 만드는 법 ​

클라이언트에서 VAST를 직접 파싱하는 일반 광고 플레이어는 클릭형 광고를 어렵지 않게 처리합니다. VAST 응답에 ClickThrough 요소가 들어 있고 플레이어가 그 URL을 알고 있으니 광고 위에 클릭 가능한 오버레이를 그리고 클릭이 들어오면 그 주소로 이동시키면 그만입니다. SSAI는 이 전제를 깨뜨립니다. 광고가 서버에서 스트림에 이미 합쳐진 채로 나오기 때문에, 플레이어는 지금 재생 중인 구간이 광고인지 본편인지조차 구분하지 못합니다. VAST 응답은 서버가 소비하고 끝입니다. 그렇다면 스티칭된 광고에는 애초에 "누를 버튼"이 없는 셈인데, StreamPackage는 이 문제를 어떻게 풀까요.

세션부터 열어야 시작되는 이야기 ​

StreamPackage에서 SSAI를 쓰려면 먼저 콘솔에서 채널을 만들고 Content Source를 지정합니다. 저는 채널 이름을 justin_ssai로 두고 실제 원본 스트림 URL을 Content Source로 넣었습니다. 이렇게 해두면 StreamPackage의 SSAI 모듈이 그 주소를 원본으로 보고 그 채널 밑에 세션 초기화용 엔드포인트를 자동으로 열어줍니다.

플레이어가 재생을 시작하려면 이 엔드포인트에 POST 요청을 먼저 보내야 합니다.

bash
curl -XPOST 'https://{ChannelId}.ap-seoul.streampackage.example.com/v1/ssai/session/{ChannelId}/main.m3u8'

바디에 광고 개인화 파라미터(지역, 기기 정보 같은 것들)를 실어 보낼 수도 있습니다. 응답은 이렇게 돌아옵니다.

json
{
  "manifestUrl": "/v1/ssai/master/{ChannelId}/main.m3u8?session_id={SessionId}",
  "trackingUrl": "/v1/ssai/tracking/{ChannelId}/{SessionId}"
}

manifestUrl은 플레이어가 실제로 재생할 마스터 플레이리스트입니다. 여기까지는 Tailor 계열 SSAI를 써본 사람에게 익숙한 흐름입니다. AWS MediaTailor의 세션 초기화 API도 정확히 같은 모양(manifestUrl + trackingUrl)으로 응답을 돌려줍니다. 정작 재미있는 쪽은 두 번째 필드 trackingUrl입니다.

클릭은 트래킹 URL을 계속 두드려서 잡아낸다 ​

플레이어는 재생을 시작한 뒤 이 trackingUrl을 주기적으로 GET 요청으로 폴링합니다. 권장 주기는 재생 중인 HLS 플레이리스트의 targetDuration과 맞춥니다. 보통 6초에서 10초 사이입니다. 그때 돌아오는 응답이 이렇습니다.

json
{
  "avails": [
    {
      "startTime": "PT0.0S",
      "startTimeInSeconds": 0,
      "ads": [
        {
          "adId": "ad_001",
          "adSystem": "Advertisement system name",
          "title": "Product Promotion",
          "startTimeInSeconds": 0,
          "durationInSeconds": 30,
          "trackingEvents": [
            {
              "eventType": "clickThrough",
              "beaconUrls": ["https://advertiser.com/landing-page"],
              "startTimeInSeconds": 0
            }
          ]
        }
      ]
    }
  ]
}

노출이나 25/50/75% 구간 같은 표준 재생 이벤트는 서버가 알아서 처리합니다. 그래서 이 API로 돌아오는 이벤트 타입은 사실상 clickThrough 하나뿐입니다. 노출이나 구간 이벤트는 서버가 세그먼트 요청만 보고도 판단할 수 있지만 "사용자가 지금 화면을 눌렀다"는 사실만큼은 서버가 알 길이 없습니다. 그 판단은 플레이어의 UI 레이어에서만 일어납니다. 그래서 StreamPackage는 여기에 필요한 재료(지금 광고가 재생 중인지, 눌렀을 때 어디로 보내야 하는지)만 트래킹 API로 미리 쥐여주는 쪽을 택했습니다.

폴링 응답은 증분식입니다. 한 번 돌려준 이벤트는 다시 오지 않으니 이미 처리한 adId는 플레이어 쪽에서 직접 기억해둬야 합니다. 폴링 루프를 의사코드로 옮기면 이렇게 됩니다.

javascript
const seenAdIds = new Set;
let currentClickThrough = null;

async function pollTracking(trackingUrl, playheadSeconds) {
  const { avails } = await fetch(trackingUrl).then((r) => r.json);

  for (const avail of avails) {
    for (const ad of avail.ads) {
      if (seenAdIds.has(ad.adId)) continue;
      seenAdIds.add(ad.adId);

      const clickEvent = ad.trackingEvents.find((e) => e.eventType === "clickThrough");
      if (!clickEvent) continue;

      // 이 광고가 재생되는 구간에만 클릭 오버레이를 활성화한다
      const adStart = ad.startTimeInSeconds;
      const adEnd = adStart + ad.durationInSeconds;

      scheduleOverlay(adStart, adEnd,  => {
        currentClickThrough = clickEvent.beaconUrls[0];
      });
    }
  }
}

function onOverlayClicked {
  if (currentClickThrough) window.open(currentClickThrough, "_blank");
}

scheduleOverlay가 이 루프의 핵심입니다. 재생 헤드가 adStart와 adEnd 사이에 있을 때만 클릭 오버레이를 화면에 띄우고 그 구간을 벗어나면 다시 내립니다. 서버는 "이 시간대에 이 광고가 있고, 클릭하면 여기로 보내라"는 정보만 줄 뿐, 오버레이를 언제 그리고 지울지는 전부 플레이어 몫입니다.

노출 집계와는 다른 문제 ​

이 글의 클릭은 트랜스코딩 지연으로 SSAI 스티칭이 실패하는 케이스에서 살펴본 Fill-rate나 노출 수 같은 집계 지표와는 결이 다른 문제입니다. 그쪽은 "광고가 얼마나 채워졌는가"를 서버 혼자서도 계산할 수 있는 집계의 영역입니다. 클릭은 다릅니다. "지금 이 순간 사용자가 뭘 눌렀는가"는 서버가 원천적으로 알 수 없는 상호작용의 영역입니다. 스티칭이 광고와 본편의 경계를 지워버리는 SSAI 구조에서 StreamPackage는 그 경계를 다시 플레이어에게 알려주는 최소한의 창구로 트래킹 폴링을 열어둔 셈입니다.

基于 VitePress 构建 · 部署于腾讯云 EdgeOne Pages