Доброго вам времени суток! Меня зовут Валерия, я фронтенд-разработчик из компании Doubletapp. Год своей жизни отдала на разработку видеоплеера с рекламными интеграциями. На старте была только спека, дока, единственная статья на Хабре, поясняющая за рекламу, и огроменное желание заказчика накрутить оную на видеоплеер. Пришло время и вас поближе познакомить с рекламой.

Статья подойдет тем, кто хочет разобраться в специфике работы VAST и VPAID, настроить кастомное управление рекламой, разместить видеорекламу отдельным блоком на сайте (out-stream) или разбить видеоролик рекламными интеграциями (in-stream).

Инструментарий, или Основные ингредиенты

Video Ad Serving Template или VAST — это единый стандарт для обмена видеообъявлениями. По факту — файлик формата xml. Он оформлен согласно спецификации, разрабатываемой компанией Interactive Advertising Bureau (IAB).

С помощью VAST провайдеры рекламы создают интеграции в универсальном формате, собирают аналитику по переходам, просмотрам и другим взаимодействиям зрителей; могут предоставлять релевантную возрасту, месту жительства и увлечениям информацию и выстраивать более качественную маркетинговую кампанию. 

Нас же интересует техническая сторона вопроса. Чтобы запустить VAST, достаточно видеоплеера и парсера для xml. Эта статья — концентрат рекламы, поэтому я не буду отвлекаться на создание hls-плеера или работу с MSE — мы возьмем стандартный <video> тег. А вот в парсинге рекламы мы пойдем по хардкору. Google IMA SDK, наверное, единственный инструмент, дающий полный контроль для работы с рекламой и открытый для использования. 

Чисто для справки: есть уже готовые плееры с возможностью воспроизведения рекламы (платные и бесплатные) — Video.js, JW Player, plyr и т. п. Если покопаться, под капотом у них тот же Google IMA SDK, но библиотеки дают к нему ограниченный доступ и свой кастомный интерфейс. Есть еще Video Ads SDK от Яндекса, но, к сожалению, работает инструмент только со своими рекламными партнерами.

Подытожим, IMA SDK даст нам возможность гибко управлять VAST'ами, настраивать сайд-эффекты и при необходимости менять логику воспроизведения под требования заказчика. 

Подробнее о VAST

Этот блок можно пропустить, если вы уже знакомы с темплейтом VAST-файлов, и сразу перейти к разделу с реализацией.

Самый простенький VAST имеет следующую структуру. Ее ни с чем не спутать, так как тело оборачивается в узел <VAST>. В нем могут лежать узлы AD или узел ERROR, если сервер не вернул рекламу.

В узле VAST указывается версия темплейта, на момент публикации статьи последняя — 4.3. При разработке плеера я встречала васты 3.0 и 2.0 версий. Пусть это вас не смущает, реализации не сильно отличаются.

Узлов AD внутри VAST начиная с 3.0 версии может быть несколько. Они будут воспроизводиться в порядке, указанном в атрибуте sequence. Если реклама в первом узле AD упала с ошибкой, загружается следующая в последовательности.

<VAST version=”3.0”>
<AD id=”jDGmdD” sequence=”1”> … </AD>
<AD id=”hrYhfK” sequence=”2”> … </AD>
…
<AD id=”hrYhfK” sequence=”14”> … </AD>
</VAST>

Внутри AD может быть:

  1. Сама реклама — узел InLine.

  2. Информация о посреднике в передаче рекламы — узел Wrapper.

Давайте сначала рассмотрим пример с узлом InLine.

<VAST version="3.0">
  <Ad id="1234567">
    <InLine>
    <AdSystem>Your Ad System</AdSystem>
    <AdTitle>Name of your VAST</AdTitle>
    <Description>Linear Video Ad</Description>
    <Error>https://www.example.com/error</Error>
    <Impression>https://www.example.com/impression</Impression>
      <Creatives>
        <Creative sequence="1">
          <Linear skipoffset=”00:00:05”>
          <Duration>00:01:55</Duration>
            <TrackingEvents>
              <Tracking event="start">https://www.example.com/start</Tracking>
              <Tracking event="firstQuartile">https://www.example.com/firstQuartile</Tracking>
              <Tracking event="midpoint">https://www.example.com/midpoint</Tracking>
              <Tracking event="thirdQuartile">https://www.example.com/thirdQuartile</Tracking>
              <Tracking event="complete">https://www.example.com/complete</Tracking>
              <Tracking event="mute">https://www.example.com/mute</Tracking>
              <Tracking event="unmute">https://www.example.com/unmute</Tracking>
              <Tracking event="rewind">https://www.example.com/rewind</Tracking>
              <Tracking event="pause">https://www.example.com/pause</Tracking>
              <Tracking event="resume">https://www.example.com/resume</Tracking>
              <Tracking event="fullscreen">https://www.example.com/fullscreen</Tracking>
            </TrackingEvents>
            <VideoClicks>
              <ClickThrough id="123">https://google.com</ClickThrough>
              <ClickTracking id="123">https://www.example.com/click</ClickTracking>
            </VideoClicks>
            <MediaFiles>
              <MediaFile delivery="progressive" type="video/mp4"> https://example.com/example.mp4 </MediaFile>
            </MediaFiles>
          </Linear>
        </Creative>
      </Creatives>
    </InLine>
  </Ad>
</VAST>

*это пример файла. Мы не гарантируем, что внутри VAST будет содержаться ссылка на существующий видеофайл

AdSystem — название рекламного сервиса, предоставляющего рекламу
AdTitle, Description — название и описание рекламы
Errorurl, который вызовется в случае ошибки
Impressionurl, который вызовется после показа первого фрейма креатива
Creatives — набор креативов для показа. Один креатив = один медиафайл для воспроизведения.
Creative — креатив также имеет атрибут sequence по аналогии с узлом AD. Их может быть несколько, но спецификация просит не путать Creative sequence с Ad sequence. Так как в этом случае sequence — это составные части одного рекламного объявления.
Linear — тип рекламы. Может иметь атрибут skipoffset, который задает время, через которое можно пропустить рекламу.

Всего IAB предоставляет три вида рекламы Linear, NonLinearAds и CompanionAds. Надеюсь, иллюстрация поможет понять, в чем разница между ними.

В этой статье мы более подробно рассмотрим Linear тип рекламы:

Duration — продолжительность рекламы
TrackingEvents — контейнер для Tracking
Tracking — содержит url, который будет вызываться при наступлении события, указанного в атрибуте event
VideoClicks — может содержать узлы ClickThrough, ClickTracking и CustomClick. В нем задается поведение при клике на рекламный блок
ClickThrough — содержит ссылку, по которой будет совершен переход при взаимодействии с рекламным блоком (лендинг, сайт)
ClickTracking — отслеживает статистику кликов, описанных в узле VideoClicks (переходы на сайт или кастомные клики)
MediaFiles — контейнер для MediaFile
MediaFile — содержит ссылку на рекламу. Их может быть несколько в разном качестве, с указанием атрибутов width и height. Используется для подбора разрешения на разных устройствах. Атрибут delivery указывает протокол для скачивания файла — streaming или progressive.

Ранее я отмечала, что есть интеграции, обернутые во Wrapper. Это значит, что исходный VAST имеет посредника, который тоже обвешал рекламу своими событиями для мониторинга показов. В этой обертке ссылка на истинный VAST лежит в узле VASTAdTagURI. Если пройти по ссылке в нем, то вы увидите тот же InLine xml-файл, похожий на пример выше.

<VAST xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vast.xsd" version="3.0">
  <Ad id="5368884870">
    <Wrapper>
    <AdSystem>GDFP</AdSystem>
    <VASTAdTagURI>
      <![CDATA[ https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml ]]>
    </VASTAdTagURI>
    <Error>https://www.example.com/error</Error>
    <Impression>https://www.example.com/impression</Impression>
      <Creatives>
        <Creative id="138311401271" sequence="1">
          <Linear>
            <TrackingEvents>
            …
            </TrackingEvents>
            <VideoClicks>
            …
            </VideoClicks>
          </Linear>
        </Creative>
      </Creatives>
      <Extensions>
        <Extension type="geo">
          <Country>RU</Country>
        </Extension>
      </Extensions>
    </Wrapper>
  </Ad>
</VAST>

*это пример файла, взятый из репозитория IAB https://github.com/InteractiveAdvertisingBureau/VAST_Samples. Внутри VAST может содержаться ссылка на несуществующий видеофайл.

В целом этих знаний достаточно для начала разработки. Однако, если ваша душа требует подробностей, можете обратиться к документации на сайте IAB или ознакомиться с неплохой статьей на Хабре

Запускаем рекламный блок на странице (out-stream)

Все примеры я опубликовала в этом репозитории. В нем вы можете проверить работоспособность примеров, поэкспериментировать с настройками или вдохновиться кодом. Готовьте попкорн и не забудьте выключить AdBlock!)

Прошу проследовать в каталог simpleExampleVast. Я советую параллельно поглядывать в код, чтобы видеть общую картину и не теряться в частях кода. А еще это мог бы быть неплохой воркшоп)

Создадим базовый index.html и стили для него. IMA рекомендует располагать на одном уровне видеоэлемент и контейнер для рекламы. VAST-спецификация предназначена для передачи рекламных объявлений видеоплеерам, поэтому подбирать рекламу мы будем, смотря на исходные размеры тега <video>. adContainer располагаем абсолютом и перекрываем им весь блок видео элемента.

/* ./src/simpleExampleVast/index.html **/
<div id="videoContainer">
    <video id="videoElement"></video>
    <div id="adContainer"></div>
</div>
/* ./src/simpleExampleVast/styles.html **/
#videoContainer {
 position: relative;
 width: 800px;
 height: 450px;
 background-color: black;
}

#videoElement {
 width: 100%;
 height: 100%;
}

#adContainer {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
}

Подключаем IMA SDK

В рамках этой статьи я заморочилась и захостила для вас рабочий VAST по этой ссылке. Можете сравнить с примерами выше. В нем нет трекинга событий, но для тренировки этого будет достаточно.

Из инструментов нам понадобится только IMA SDK (вот дока). 

IMA SDK можно подключить напрямую через скрипт, но мы воспользуемся пакетом @alugha/ima. Это даст нам ряд полезных фич:

1. Типизация
2. Динамическая загрузка IMA SDK
3. Проверка на включенный блокировщик рекламы

Загружаем IMA SDK 

import {loadImaSdk} from "@alugha/ima";

async function loadIma() {
 try {
   const ima = await loadImaSdk()
   console.info('IMA SDK successfully loaded. Ima version: ' + ima.VERSION);
 } catch (err) {
   console.error("IMA SDK could not be loaded. Check your ad blocker");
 }
}

loadIma()

loadImaSdk проверяет, включен ли в данный момент блокировщик рекламы на клиенте. Если блокеров нет — загружает инстанс ima, в противном случае триггерит ошибку.

После загрузки управление рекламой будет осуществляться через сущность ima, которую возвращает функция loadImaSdk. Также она будет доступна в глобальных переменных window.google.ima.

Чтобы типизация работала корректно, обновим глобальный интерфейс.

/* ./src/types.d.ts **/
import { ImaSdk } from '@alugha/ima'

declare global {
 interface Window {
   google: { ima: ImaSdk }
 }
}

На основе этих знаний напишем универсальную функцию для загрузки IMA SDK. Она понадобится нам для дальнейшей разработки. 

Наш утиль будет принимать колбэк, который вызовется при успешной загрузке ima.

/* ./src/imaLoader.ts **/
import { loadImaSdk } from '@alugha/ima'

export async function loadIma(onImaLoaded: () => void) {
 const ima = await loadImaSdk().catch((err) =>
   console.error('IMA SDK could not be loaded. Check your ad blocker.')
 )

 if (ima) {
   console.info('IMA SDK successfully loaded.')
   onImaLoaded()
 }
}

Инициализируем

О настройке IMA подробно написано в документации, поэтому я постаралась уделить внимание тому, что осталось за кадром Get Started. Обобщу знания, полученные при разработке подобного плеера. Надеюсь, это поможет вам сократить время на понимание работы инструмента и его особенностей.

С помощью IMA можно:

  • отслеживать события рекламы;

  • описывать сайд-эффекты;

  • получать информацию о рекламе;

  • управлять звуком, паузой, перемоткой и т. п.;

  • подгружать в любой момент новую рекламу;

и многое другое.

Манипулировать мы будем двумя элементами

/* ./src/simpleExampleVast/index.ts **/
const videoElement = document.getElementById('videoContainer')
const adContainer = document.getElementById('adContainer')

Все методы взаимодействия с рекламой буду описывать в кастомном классе ImaManager. Если у вас на странице несколько рекламных блоков (несколько adContainer), для каждого нового контейнера создается новый инстанс imaManager

/* ./src/simpleExampleVast/index.ts **/
const imaManager = new ImaManager(videoElement, adContainer)

Инициализация сводится к тому, что мы передаем информацию о нашем adContainer в ima. Именно в этот элемент IMA будет пробрасывать рекламные блоки. Они могут представлять собой html с медиафайлом в <iframe> или <video>. Также в IMA SDK предусмотрены дополнительные контролы, например, для пропуска рекламы или таймера.

Инициализируем контейнер для рекламы в классе ImaManager.

/* ./src/simpleExampleVast/ImaManager.ts **/
init() {
   const ima = window.google.ima

   const adDisplayContainer = new ima.AdDisplayContainer(
     this.adContainer
   )

   adDisplayContainer.initialize()
 }

и создаем загрузчик рекламы

/* ./src/simpleExampleVast/ImaManager.ts **/
init() {
   if (this.adsLoader) return

   const ima = window.google.ima

   const adDisplayContainer = new ima.AdDisplayContainer(this.adContainer)
   adDisplayContainer.initialize()

   this.adsLoader = new ima.AdsLoader(adDisplayContainer)

   this.adsLoader.addEventListener(
     ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
     this.onAdsManagerLoaded
   )

   this.adsLoader.addEventListener(
     ima.AdErrorEvent.Type.AD_ERROR,
     this.onAdError
   )
 }

Так как объявить adsLoader для каждого ImaManager мы можем один раз, в начало метода я добавила проверку на его существование.

adsLoader запрашивает xml файл и сигнализирует о его получении в событии ADS_MANAGER_LOADED или об ошибке — AD_ERROR.

Обрабатываем ошибки

У ima есть критические и некритические ошибки. Подробное описание ошибок и их коды можете посмотреть здесь.

Создадим метод обработки ошибок.

/* ./src/simpleExampleVast/ImaManager.ts **/
private onAdError = (adErrorEvent: google.ima.AdErrorEvent) => {
   console.error(
     adErrorEvent.getError().getErrorCode(),
     adErrorEvent.getError().getMessage()
   )
 }

Обрабатываем событие ADS_MANAGER_LOADED

/* ./src/simpleExampleVast/ImaManager.ts **/
this.adsLoader.addEventListener(
   ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
   this.onAdsManagerLoaded
)

Событие ADS_MANAGER_LOADED тригерится в момент, когда получен xml-файл. На этом этапе мы можем обращаться к adsManager. Он служит для непосредственного «общения» с рекламой. Инстанс adsManager позволяет:

  • получать информацию о рекламе, 

  • подписываться на события текущей рекламы, 

  • влиять на воспроизведение рекламы — остановить, перемотать или пропустить интеграцию.

Также, опционально, можно прокинуть настройки при инициализации adsManager — их список тут

Для примера изменим ожидание загрузки медиафайла на 9 секунд (default = 8 секунд). Это значит, что если видео или баннер не загружаются в течение 9 секунд, IMA его пропустит и загрузит следующий, либо просигнализирует о завершении показа рекламы (об этом в обработке событий рекламы)

/* ./src/simpleExampleVast/ImaManager.ts **/
onAdsManagerLoaded = (
   adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
   console.info('Ads manager loaded')

   const ima = window.google.ima

   const adsRenderingSettings = new ima.AdsRenderingSettings()

   adsRenderingSettings.loadVideoTimeout = 9000

   this.adsManager = adsManagerLoadedEvent.getAdsManager(
     this.videoElement,
     adsRenderingSettings
   )
 }

В момент воспроизведения рекламы также может что-то пойти не так, поэтому добавляем обработку ошибок и инициализируем adsManager. 

/* ./src/simpleExampleVast/ImaManager.ts **/
onAdsManagerLoaded = (
   adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
 ) => {
   // . . . 

   this.adsManager.addEventListener(
     ima.AdErrorEvent.Type.AD_ERROR,
     this.onAdError
   )

   this.adsManager.init(
     this.videoElement.clientWidth,
     this.videoElement.clientHeight,
     ima.ViewMode.NORMAL
   )
 }

При инициализации adsManager нужно передать три обязательных аргумента — ширину и высоту рекламного блока при монтировании и режим отображения («NORMAL» или «FULLSCREEN»).

Запрашиваем и воспроизводим рекламу

В классе ImaManager создадим еще один метод requestAds, который будет получать ссылку на рекламу и запрашивать ее загрузку.

/* ./src/simpleExampleVast/ImaManager.ts **/
requestAds(adTagUrl: string) {
   if (!this.adsLoader) return

   console.info('Start to request ad')

   const adsRequest = new window.google.ima.AdsRequest()

   adsRequest.adTagUrl = adTagUrl

   this.adsLoader.requestAds(adsRequest)
 }

После вызова этой функции adsLoader начнет загрузку рекламы и после задиспатчит событие ADS_MANAGER_LOADED. В нем мы получим adsManager, задача которого — найти в xml .mp4 файл и воспроизвести его в заданном нами контейнере. И… Реклама все равно не будет воспроизводиться. 

Чтобы реклама наконец появилась на странице, нужно дождаться ее загрузки и вызвать метод adsManager.start().

/* ./src/simpleExampleVast/ImaManager.ts **/
private onAdsManagerLoaded = (
   adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
 ) => {
   // . . . 

   this.adsManager.addEventListener(
     ima.AdEvent.Type.LOADED,
     this.adsManager.start
   )

   this.adsManager.addEventListener(
     ima.AdErrorEvent.Type.AD_ERROR,
     this.onAdError
   )

   // . . .
 }

Так выглядит итоговый скрипт с инициализацией imaManager. В последующих примерах функцию init я перенесу в конструктор класса

/* ./src/simpleExampleVast/index.ts **/
import { loadIma } from '../imaLoader'

function createAdService() {
 // Get elements and init ad's manager
 const videoElement = document.getElementById(
   'videoElement'
 ) as HTMLVideoElement

 const adContainer = document.getElementById('adContainer') as HTMLDivElement

 if (videoElement === null || adContainer === null)
   throw Error('VideoElement or AdContainer not included in DOM')

 const imaManager = new ImaManager(videoElement, adContainer)

 // Init ima instance
 imaManager.init()

 // Activate controls
 const startBtn = document.getElementById('startButton')
 
 startBtn?.addEventListener('click', () => {
   // Start to load current ad when start ad button clicked
   imaManager.requestAds(
     'https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml'
   )
 })
}

document.addEventListener('DOMContentLoaded', () => loadIma(createAdService))

Класс ImaManager для этого примера находится в папке simpleExampleVast в репозитории. Запускается код по команде yarn simple-vast. Не забудьте предварительно установить зависимости.

P. S. У adsManager множество методов для работы с рекламой и рекламными блоками (ведь их может быть несколько). Если вы работаете с тайпскриптом, то проанализировать их не составит труда. Для примера, я добавила методы плея, паузы и пропуска рекламы. Важно отметить: время для пропуска рекламы регулируется в атрибуте skipoffset VAST-файла.

Продвинутое управление (въезжаем в обработку событий рекламы)

Сейчас при запуске рекламы мы видим следующую картину. Выглядит все довольно неплохо. Но кое-что мне не нравится. Во-первых, локализация. Во-вторых, если я попытаюсь изменить размеры videoContainer, то реклама к ним не адаптируется.

Дефолтный вид
Дефолтный вид
При увеличении размера контейнера во время воспроизведения
При увеличении размера контейнера во время воспроизведения

Все дальнейшие изменения в репозитории я буду делать на основе предыдущего примера, но уже в каталоге advancedExampleVast.

Локализация

Задать дополнительные настройки ima мы можем глобально для всех adConteiners и точечно для каждого (настройки). 

Пример ниже иллюстрирует локальные и глобальные настройки. Исключение составляет локализация — ее можно задать только для всего проекта.

// global change

   ima.settings.setLocale('ru')

   ima.settings.setAutoPlayAdBreaks(false)
// local change for manual managing current adContainer

   this.adsLoader.getSettings().setAutoPlayAdBreaks(false)

setAutoPlayAdBreaks мы разберем далее при работе с VMAP.

Адаптивность

Следить за размерами адаптивного блока буду через ResizeObserver. Кстати, не забывайте установить к нему полифил для корректной работы в браузерах.

/* ./src/advancedExampleVast/ImaManager.ts **/
private initResizeObserver(): void {
   const resizeObserver = new ResizeObserver(() => {
     this.adsManager?.resize(
       this.videoElement.clientWidth,
       this.videoElement.clientHeight,
       window.google.ima.ViewMode.NORMAL
     )
   })

   resizeObserver.observe(this.videoElement)
 }

Обработка событий

Полный список событий для VAST можно найти здесь.

Подпишемся на некоторые из них и прологгируем.

/* ./src/advancedExampleVast/ImaManager.ts **/
private onAdsManagerLoaded = (
   adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
 ) => {
   // . . .

   const imaEvents: google.ima.AdEvent.Type[] = [
     window.google.ima.AdEvent.Type.LOADED,
     window.google.ima.AdEvent.Type.STARTED,
     window.google.ima.AdEvent.Type.AD_PROGRESS,
     window.google.ima.AdEvent.Type.PAUSED,
     window.google.ima.AdEvent.Type.RESUMED,
     window.google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED,
     window.google.ima.AdEvent.Type.SKIPPED,
     window.google.ima.AdEvent.Type.CLICK,
     window.google.ima.AdEvent.Type.VOLUME_CHANGED,
     window.google.ima.AdEvent.Type.COMPLETE,
     window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
   ]

   imaEvents.forEach((imaEvent) =>
     this.adsManager?.addEventListener(imaEvent, this.onAdEvent)
   )
/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
   adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
 ) => {
   console.info('EVENT: ', adEvent.type)

   switch (adEvent.type) {
     case window.google.ima.AdEvent.Type.LOADED:
       this.adsManager?.start()
       break
   }
 }

В ивентах можно получить данные о рекламе (adEvent.getAd() или adEvent.getAdData()). Возвращаемый объект может различаться в зависимости от события, в котором вы его запросили. К примеру, в событии AD_PROGRESS мы можем получать информацию о продолжительности рекламы, прогрессе и т. п.

Для разнообразия функционала добавим парочку сайд-эффектов, предложенных моим воображаемым заказчиком

  1. «Я кликаю на рекламу, перехожу на сайт, потом обратно, и реклама на паузе. Хочу, чтобы я мог ее возобновить».

Пожалуйста:

/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
   adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
 ) => {
   console.info('EVENT: ', adEvent.type)

   switch (adEvent.type) {
     case window.google.ima.AdEvent.Type.LOADED:
       this.adsManager?.start()
       break
     case window.google.ima.AdEvent.Type.PAUSED:
       this.resumeButton?.classList.remove('hidden')
       break
     case window.google.ima.AdEvent.Type.RESUMED:
       this.resumeButton?.classList.add('hidden')
       break
   }
 }
  1. «Мне не нравится эта пустота после показа рекламы. Пусть воспроизводится еще реклама — бесконечно».

/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
   adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
 ) => {
   console.info('EVENT: ', adEvent.type)

   switch (adEvent.type) {
     // . . .
     case window.google.ima.AdEvent.Type.SKIPPED:
     case window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
       this.requestAds(
         'https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml'
       )
       break
   }
 }

Класс ImaManager для этого примера находится в папке advancedExampleVast в репозитории. Запускается код по команде yarn advanced-vast.

P. S. Таким образом можно обрабатывать и запускать VAST или VMAP. Но важно отметить, что в случае с VMAP будут запускаться только медиафайлы, отмеченные как preroll. (подробнее в описании VMAP)

Разбиваем видеоролик рекламными паузами (in-stream, VMAP)

Ситуация: представим, что наш заказчик — медиасервис с платной подпиской, но также он предоставляет ресурсы и для бесплатного просмотра. Чтобы оплатить сервера для хостинга такого объема файлов, а еще и обеспечить хорошую пропускную способность, требуются значительные затраты, не считая оплаты лицензий на трансляцию самого контента. Большинство подобных сервисов стараются монетизировать контент, который предоставляют пользователям без подписки, встраивая рекламные блоки. Они могут быть в начале контента, в конце и в середине.

Если бы мы решали эту задачу, имея простой VAST, то нам пришлось бы отслеживать timestamp видео и в нужные промежутки запускать рекламу. Благо все это можно автоматизировать с помощью спецификации VMAP. 

Насколько мне известно, рекламные сервисы обычно отдают VAST файлы, но никто не мешает упаковать их в VMAP.

Подробнее о VMAP

Этот блок можно пропустить, если вы уже знакомы с темплейтом VMAP-файлов, и сразу перейти к разделу с реализацией.

Video Multiple Ad Playlist или VMAP — это единый стандарт для обмена наборами видеообъявлений. Это xml-файл, оформленный согласно спецификации, разрабатываемой компанией Interactive Advertising Bureau (IAB). На момент написания статьи имеет одну версию.

Если проще, то VMAP — это расписание рекламных пауз для видеоплеера, воспроизводящего какой-либо контент. 

Ниже представлена структура VMAP-файла

<vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap" version="1.0">
  <vmap:AdBreak timeOffset="start" breakType="linear" breakId="preroll">
    <vmap:AdSource id="preroll" allowMultipleAds="false" followRedirects="true">
      <vmap:AdTagURI templateType="vast3">
        <![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>
  <vmap:AdBreak timeOffset="00:00:30.000" breakType="linear" breakId="midroll-1">
    <vmap:AdSource id="midroll-ad-1" allowMultipleAds="true" followRedirects="true">
      <vmap:AdTagURI templateType="vast3">
        <![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>
  <vmap:AdBreak timeOffset="00:01:00.000" breakType="linear" breakId="midroll-1">
    <vmap:AdSource id="midroll-ad-2" allowMultipleAds="true" followRedirects="true">
(*1)  <vmap:VASTData>
      	 <VAST version=”3.0”>
      	    . . .
         </VAST>
      </vmap:VASTData>
    </vmap:AdSource>
    <vmap:TrackingEvents>
       <vmap:Tracking event=”breakStart”>
    	 https://www.example.com/breakStart
       </vmap:Tracking>
(*2)</vmap:TrackingEvents>
  </vmap:AdBreak>
  <vmap:AdBreak timeOffset="00:01:30.000" breakType="linear" breakId="midroll-1">
    <vmap:AdSource id="midroll-ad-3" allowMultipleAds="false" followRedirects="true">
      <vmap:AdTagURI templateType="vast3">
        <![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>
  <vmap:AdBreak timeOffset="end" breakType="linear" breakId="postroll">
    <vmap:AdSource id="postroll" allowMultipleAds="false" followRedirects="true">
      <vmap:AdTagURI templateType="vast3">
        <![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>
</vmap:VMAP>

*это пример файла. Мы не гарантируем, что внутри VAST будет содержаться ссылка на существующий видеофайл.

AdBreak — рекламная пауза, обертка для отдельных VAST. 
Атрибуты:
timeOffset — указывает, в какой момент воспроизведения основного видео начнется реклама. Допустимы значения для IMA SDK — “start”, “end” или указание времени до миллисекунд.
breakType — задает тип рекламы.

AdSource — хранит информацию о VAST, который будет воспроизводиться. В него может быть вложена ссылка на VAST (AdTagURI) или целый файл ((1*) VASTData).
Атрибуты:
allowMultipleAds — разрешает или запрещает наличие нескольких узлов Ad в VAST (для формирования рекламного блока), 
followRedirects — разрешает или запрещает наличие VAST с переадресацией на другой VAST. (например, Wrapper)

(2*) Также можно подписаться на события в TrackingEvents

Подключаем VMAP к видеоплееру

В репозитории можно найти каталог exampleVmap. Все дальнейшие действия я буду описывать в нем.

Я была бы не я, если бы не захостила для вас медиафайл и рабочий VMAP (ссылка на него).

Откройте этот файл в браузере. В нем ссылки на 4 рекламных блока — в начале (preroll) и в конце (postroll) воспроизведения, а также два midroll на 5-й и 10-й секундах. 

Здесь загружать рекламу мы будем уже не по кнопке, а при старте контентного видео. 

/* ./src/exampleVmap/index.ts **/
function createAdService() {
 // . . .

 videoElement.addEventListener('play', onVideoPlay)

 function onVideoPlay() {
   imaManager.requestAds(
    'https://lerok007.ams3.digitaloceanspaces.com/vmap%20(2).xml'
   )
   videoElement.removeEventListener('play', onVideoPlay)
 }
}

После этих изменений рекламные паузы будут воспроизводиться по заданному расписанию, но вместе с основным видео, а также перекрывать кликабельные области после воспроизведения. Для таких сайд-эффектов в IMA предусмотрены два события CONTENT_PAUSE_REQUESTED и CONTENT_RESUME_REQUESTED. Они сообщают, когда рекламный блок планирует начать воспроизведение и когда заканчивает. 

/* ./src/exampleVmap/ImaManager.ts **/
onAdEvent = (
   adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
 ) => {
   console.info('EVENT: ', adEvent.type)

   switch (adEvent.type) {
     case window.google.ima.AdEvent.Type.PAUSED:
       this.resumeButton?.classList.remove('hidden')
       break
     case window.google.ima.AdEvent.Type.RESUMED:
       this.resumeButton?.classList.add('hidden')
       break
     case window.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
       this.videoElement.pause()
       this.adContainer.classList.remove('backwards')
       break
     case window.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
       this.videoElement.play()
       this.adContainer.classList.add('backwards')
       break
   }
 }

Класс backwards меняет z-index у элемента на -1.

Обратите внимание, что в этом примере мы уже не запускаем рекламу на событие LOADED. Это происходит автоматически, так как в vast у нас заданы конкретные промежутки воспроизведения интеграций. Поэтому вызвать adsManager?.start() необходимо всего раз после инициализации. Причем Ima будет префетчить следующую рекламу перед воспроизведением для минимальной задержки.

/* ./src/exampleVmap/ImaManager.ts **/
private onAdsManagerLoaded = (
   adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
 ) => {
   // . . .

   this.adsManager.init(
     this.videoElement.clientWidth,
     this.videoElement.clientHeight,
     this.ima.ViewMode.NORMAL
   )

   this.adsManager?.start()
  }

На этом этапе воспроизводятся все рекламы, кроме последней. Это происходит, потому что IMA не знает о том, когда контентное видео заканчивается. Давайте ей поможем.

Проверим наличие postroll внутри VMAP и, если есть, сообщим об этом adsLoader.contentComplete() в момент окончания контентного видео.

/* ./src/exampleVmap/ImaManager.ts **/
private addContentCompletedHandler(adsManager: google.ima.AdsManager) {
   const hasPostroll = adsManager.getCuePoints().slice(-1)[0] === -1

   if (!hasPostroll) return
   
   const onContentEnded = () => {
     if (hasPostroll) {
       this.adsLoader?.contentComplete()
     }
     this.videoElement.removeEventListener('ended', onContentEnded)
   }

   this.videoElement.addEventListener('ended', onContentEnded)
 }

Вызовем эту функцию после получения adsManager в функции onAdsManagerLoaded.

Ручное управление стартом рекламы

Но предположим, мы хотим начать загрузку заранее, чтобы не дожидаться медиафайла и при включении плеера уже иметь возможность ее отобразить.

Этот пример в репозитории я расположила в каталоге advancedExampleVmap.

В настройках adsLoader отключим автоматическое воспроизведение

/* ./src/advancedExampleVmap/ImaManager.ts **/
init() {
   // . . .

   this.adsLoader = new this.ima.AdsLoader(adDisplayContainer)

   this.adsLoader.getSettings().setAutoPlayAdBreaks(false)
 }

Теперь при готовности рекламы к воспроизведению мы будем получать ивент AD_BREAK_READY. И только после этого события запускать интеграцию.

При начале воспроизведения основного видео будем вызывать метод imaManager.startAd(),

/* ./src/advancedExampleVmap/index.ts **/
function onVideoPlay() {

   imaManager.startAd()

   videoElement.removeEventListener('play', onVideoPlay)

 }

 который будет стартовать рекламный блок, 

/* ./src/advancedExampleVmap/ImaManager.ts **/
startAd() {
   if (this.adsReady) {
     this.adsManager?.start()
   }

   this.adCanPlay = true
 }

либо ждать, пока он загрузится

/* ./src/advancedExampleVmap/ImaManager.ts **/
onAdEvent = (
   adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
 ) => {
   console.info('EVENT: ', adEvent.type)

   switch (adEvent.type) {
     case window.google.ima.AdEvent.Type.AD_BREAK_READY:
       if (this.adCanPlay) {
         this.adsManager?.start()
       } else {
         this.adsReady = true
       }
       break
       
    // . . . 
 }

 P. S. Однако с этими настройками нужно глядеть в оба, если вы работаете не с обычным VAST-файлом, а с VPAID. В его коде может быть прописано время ожидания для запуска. Другими словами, после загрузки некоторые разработчики VPAID ставят timeout, через который мы обязаны вызвать команду adsManager.start(), иначе реклама упадет с ошибкой и IMA ее пропустит.

Вы дошли до этого раздела? Поздравляю. А я как раз собиралась рассказать про VPAID.

Особенности VPAID

Давайте вспомним, как выглядит VAST.

И сравните его с VPAID ниже

<VAST version="3.0">
  <Ad id="1234567">
    <InLine>
    <AdSystem>Your Ad System</AdSystem>
    <AdTitle>Name of your VAST</AdTitle>
    <Description>Linear Video Ad</Description>
    <Error>https://www.example.com/error</Error>
    <Impression>https://www.example.com/impression</Impression>
      <Creatives>
        <Creative sequence="1">
          <Linear skipoffset=”00:00:05”>
          <Duration>00:01:55</Duration>
          <TrackingEvents>
          . . . 
          </TrackingEvents>
          <VideoClicks>
          . . . 
          </VideoClicks>
            <MediaFiles>
              <MediaFile maintainAspectRatio="true" scalable="true" delivery="progressive" apiFramework="VPAID" type="application/javascript">    <![CDATA[ https://example.com/vpaidVideoAd.js ]]>
              </MediaFile>
            </MediaFiles>
          </Linear>
        </Creative>
      </Creatives>
    </InLine>
  </Ad>
</VAST>

*это пример файла. Мы не гарантируем, что внутри VAST будет содержаться ссылка на существующий видеофайл

Я подскажу, различие лишь в одном — в MediaFile здесь лежит уже не медиафайл .mp4, а скрипт.

Video Player-Ad Interface Definition (VPAID) — спецификация для интерактивной рекламы. В нее разработчики могут поместить любой скрипт, отрендерить в пределах контейнера любые элементы. По итогу это может быть даже не видеофайл, а баннер с опросом. Мой совет: никогда не пытайтесь обрабатывать поведение этой рекламы и оставляйте ее ровно такой, какой ее описал разработчик VPAID, а значит, по заветам рекламодателя.

Наша задача — VPAID только запустить, и делается это добавлением всего одной строки 

  // for local adContainer
  this.adsLoader
   .getSettings()
   .setVpaidMode(window.google.ima.ImaSdkSettings.VpaidMode.INSECURE)

  // global for all project
  this.ima.settings.setVpaidMode(
    window.google.ima.ImaSdkSettings.VpaidMode.INSECURE
  )

Узнать, что к вам пришел VPAID, можно после загрузки рекламы в событии LOADED

case window.google.ima.AdEvent.Type.LOADED:
   this.isVPAID = adEvent.getAdData().apiFramework === 'VPAID'
   break

Работа VPAID построена на описании методов для различных ивентов рекламы — они приведены в спецификации, которую можно найти тут.

На этом я бы хотела закончить свой «небольшой» рассказ про рекламу. Но еще парочка моментов, которые могут быть вам полезны:

  • автоплей

VAST — это в конечном итоге медиафайл, и как для всех медиаэлементов в браузерах, на них действуют ограничения на воспроизведение. Если видео играет после действия пользователя, то реклама может воспроизводиться со звуком. Автоплеить же рекламу лучше без звука, иначе в некоторых браузерах она может не воспроизвестись. С VAST достаточно поставить 

adsManager.setVolume(0)

сразу после получения adsManager

Однако здесь могут возникнуть проблемы с VPAID, потому что в его скрипте тоже могут переопределяться настройки звука. В таком случае идеально найти способ договориться с представителями рекламных сервисов настроить VPAID для воспроизведения без звука, если в их админках такое невозможно.

  • фулскрин

Можно взять готовые решения вместе с плеером — такие как video.js или plyr. Если же использовать IMA SDK, то обрабатывать fullscreen нужно на родителе videoElement и adContainer.

  • перемотка

При перемотке основного видео IMA воспроизводит рекламный блок, если он попал в интервал перемотки, но не больше одного.

  • предзагрузка VAST

IMA старается заранее подгружать васты перед началом рекламного блока внутри плеера.

На этом точно все, можете снова включать AdBlock! Спасибо, что прочитали эту статью и, надеюсь, что со мной вам было интересно!

Комментарии (1)


  1. andranikTomskiy
    14.08.2023 05:43

    Неужели это свершилось и на данную тему написали статью. А я то думал, что кроме меня никому это не нужно. Но жаль, что только сегодня, когда я уже справился с этой задачей используя Yandex Video Ads SDK