# Manifest manipulator for VOD streams

The Pod Serving API provides access to adaptive-bitrate video ad pods prepared
in such a way that they can be stitched directly into a user-facing HLS or
MPEG-DASH media playlist.

> [!WARNING]
> **Warning:** This API is intended for advanced publishers and video technology partners. Using this API at scale requires design and implementation of a sophisticated media serving workflow which is outside the scope of this documentation.

This guide is focused on implementing a basic Pod Serving manifest manipulation
server for VOD streams.

## Receive stream manifest requests

Your manifest manipulator must provide an API endpoint to listen for manifest
requests from the video player client app. At minimum, this endpoint must
collect a stream ID from the client player app. This stream ID is used to
identify the streaming session to Ad Manager in your ad pod requests.

You also need to collect some other information to identify the appropriate
content stream, for example, a content ID.

#### Example manifest request endpoint

    GET /api/stream_id/{stream_id}/video/{content_id}.{format}
    Host: {your_domain}

| Path parameters ||
|---|---|
| `stream_id` | The Ad Manager stream ID from the client video player app. |
| `content_id` | A hypothetical ID corresponding to the content video in your system. |
| `format` | A hypothetical parameter corresponding to the stream format. One of either: |---|---| | `mpd` | For MPEG-DASH streams | | `m3u8` | For HLS streams | |

## Retrieve the content stream

Use the content ID collected from the manifest request to select the content
stream to stitch with ads.

## Request ad pod manifests

To request ads from Ad Manager, your server must make a POST request to the ad
pods endpoint, passing the requested encoding profiles and ad tag. This request
also includes the stream ID that you collected in Step 1.

In return, you receive a list of ad pod objects containing manifest files for
the ad pods requested by the publisher's ad tag and information about when and
where they should be inserted into your content.

    POST /ondemand/pods/api/v1/network/{network_code}/streams/{stream_id}/adpods
    Host: dai.google.com
    Content-Type: application/json

| Path parameters ||
|---|---|
| `network_code` | The publisher's Ad Manager 360 network code. |
| `stream_id` | The stream ID from the client video player app. |

### JSON body

| Body parameters |||
|---|---|---|
| `encoding_profiles` | `Required` | A list of JSON representations of the encoding profiles you want to receive for each ad break. [See details below](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/manipulator#EncodingProfile) To make playback as seamless as possible, this should match the set of encoding profiles used in your content stream. |
| `ad_tag` | `Required` | An ad tag to request VMAP ads. |
| `cuepoints` | `Optional` | A list of cue points within the content stream where midroll ad breaks will be inserted. Cue points are measured in floating point seconds. Required only for VMAP responses that contain mid-rolls using *positional* time offsets. This is uncommon. **Note:** See section 2.3.1 of the IAB's [VMAP standard](https://iabtechlab.com/wp-content/uploads/2016/04/VMAP.pdf) for more information about types of time offset. |
| `content_duration_seconds` | `Optional` | The content duration in seconds. Required only for VMAP responses that contain mid-rolls using *percentage* time offsets. This is uncommon. **Note:** See section 2.3.1 of the IAB's [VMAP standard](https://iabtechlab.com/wp-content/uploads/2016/04/VMAP.pdf) for more information about types of time offset. |
| `manifest_type` | `Optional` | The format of the ad streams being requested, either `hls` or `dash`. The default value is `hls`. |
| `dai_options` | `Optional` | Additional options controlling aspects of how the manifests are rendered. [See details below](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/manipulator#DAIOptions) |
| `profile_name` | `Required` | An identifier for this Encoding Profile. This value can be any string you choose, but you cannot have multiple encoding profiles with the same name on the same stream. |
| `type` | `Required` | The encoding type of the stream described by this encoding profile. Content types are: `media`, `iframe`, `subtitles`. |
| `container_type` | `Required` | The container format used by this encoding profile. Container formats are: `mpeg2ts`, `fmp4cmaf`, `hls_packed_audio` **Note:** Fragmented MP4 profiles must be [demuxed](https://ffmpeg.org/doxygen/4.0/group__lavf__decoding.html). Also, when the `manifest_type` is `dash`, `container_type` must be `fmp4cmaf`. |
| `video_settings` | `Optional` | Required if the encoding profile type is **iframe** . Otherwise, only allowed if the media type contains video. [See details below](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/manipulator#VideoSettings) |
| `audio_settings` | `Optional` | Required if the encoding profile contains audio. Only allowed if the type is media. [See details below](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/manipulator#AudioSettings) |
| `subtitle_settings` | `Optional` | Required if the encoding profile contains subtitles. [See details below](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/manipulator#SubtitleSettings) |
| `codec` | `Required` | The RFC6381 codec string. **Example:** `avc1.4d000c` |
| `bitrate` | `Required` | An integer representing the max video bitrate of this profile in bytes per second. |
| `frames_per_second` | `Required` | The floating point FPS of the video. |
| `resolution` | `Required` | A JSON-encoded value containing video \`width\` and \`height\` in pixels. **Example:** `{"width": 640, "height": 320}` |
| `codec` | `Required` | The RFC6381 codec string. **Example:** `mp4a.40.5` |
| `bitrate` | `Required` | An integer representing the max audio bitrate of this profile in bytes per second. **Example:** `300000` |
| `channels` | `Required` | An integer representing the number of audio channels including low frequency channels. |
| `sample_rate` | `Required` | An integer representing the audio sampling rate in hertz. **Example:** `4800` |
| `format` | `Required` | The file format used by in-band subtitles. Supported values are `webvtt` or `ttml`. |
| `language` | `Optional` | The subtitle language as an RFC5646 language string. If provided, this value is only used for DASH rendering. **Example:** `en-us` |
| `dash_profile` | `Optional` | The MPEG-DASH profile to apply to ad pod manifests. This setting is used for DASH manifests only. Allowed values are `live` or `on-demand`. The default value is `on-demand`. The value `live` corresponds to the [MPEG-DASH profile](https://dashif.org/identifiers/profiles/#profiles) `"urn:mpeg:dash:profile:isoff-live:2011"`. The value `on-demand` corresponds to the [MPEG-DASH profile](https://dashif.org/identifiers/profiles/#profiles) `urn:mpeg:dash:profile:isoff-on-demand:2011`. **Note:** Despite the name, the `live` profile can still be applied to video-on-demand content. This configuration is commonly used in video archives of live streamed events. |
| `ad_pod_timeout` | `Optional` | The maximum time to spend selecting ads and building ad pods, in floating point seconds. After this time has elapsed, Ad Manager returns any ads already selected in the `ad_pods` response and stops processing. |
| `sam_id` | `Optional` | Specifies an alternate debug key that can be used to look up sessions in the [stream activity monitor](https://support.google.com/admanager/answer/7009058). |

### Response

| Response parameters ||
|---|---|
| `valid_for` | Duration for which these ad pod playlists are valid in `dhms` (days, hours, minutes, seconds) format. |
| `valid_until` | The date and time until which these ad pod playlists are valid as an ISO8601 datetime string, in `yyyy-MM-dd'T'hh:mm:ss.sssssssss[+|-]hh:mm` format. |
| `ad_pods` | A list of ad pods selected for this stream. |
| `manifest_uris` | For HLS streams only. A map of encoding profile IDs to HLS manifest URIs. |
| `mpd_uri` | For DASH streams only. The URI of the DASH MPD. |
| `type` | The type of ad pod. Ad pod types are: `pre`, `mid`, or `post`. |
| `start` | For mid-roll ad pods only. The position in the stream where this ad pod should be inserted, in floating point seconds. |
| `duration` | The duration of this ad pod in floating point seconds. |
| `midroll_index` | For mid-roll ad pods only. The index of the current mid-roll ad pod. Indexing begins with `1`. |

### Example request (cURL)

    curl -X POST \
         -d '@request-body.json' \
         -H 'Content-Type: application/json' \
      https://dai.google.com/ondemand/pods/api/v1/network/21775744923/streams/6e69425c-0ac5-43ef-b070-c5143ba68541:CHS/adpods

#### Example request body

This is the contents of `request-body.json` referenced in the cURL call above.

    {
      "encoding_profiles": [
       {
         "profile_name": "1080p",
         "type": "media",
         "container_type": "mpeg2ts",
         "video_settings": {
           "codec": "avc1.4d000c",
           "bitrate": 5000000,
           "frames_per_second": 30.0,
           "resolution": {
             "width": 1920,
             "height": 1080
           }
         },
         "audio_settings": {
           "codec": "mp4a.40.5",
           "bitrate": 300000,
           "channels": 2,
           "sample_rate": 48000
         }
       },
       {
         "profile_name": "360p",
         "type": "media",
         "container_type": "mpeg2ts",
         "video_settings": {
           "codec": "avc1.4d000d",
           "bitrate": 1000000,
           "frames_per_second": 30.0,
           "resolution": {
             "width": 640,
             "height": 360
           }
         },
         "audio_settings": {
           "codec": "mp4a.40.5",
           "bitrate": 64000,
           "channels": 2,
           "sample_rate": 48000
         }
       },
       {
         "profile_name": "subtitles-webvtt",
         "type": "subtitles",
         "subtitle_settings": {
           "format": "webvtt"
         }
       }
     ],
     "ad_tag": "https://pubads.g.doubleclick.net/gampad/ads?...",
     "manifest_type": "hls"
    }

#### Example response

    {
      "valid_for": "8h0m0s",
      "valid_until": "2023-03-24T08:30:26.839717986-07:00",
      "ad_pods": [
        {
          "manifest_urls":{
            "1080p": "https://{...}/pod/0/profile/1080p.m3u8",
            "360p": "https://{...}/pod/0/profile.m3u8",
            "subtitles-webvtt": "https://{...}/pod/0/profile/subtitles-en.vtt"
          },
          "type": "pre",
          "duration": 10.0
        },
        {
          "manifest_urls":{
            "1080p": "https://{...}/pod/1/profile/1080p.m3u8",
            "360p": "https://{...}/pod/1/profile.m3u8",
            "subtitles-webvtt": "https://{...}/pod/1/profile/subtitles-en.vtt"
          },
          "type": "mid",
          "start": 15.0,
          "duration": 15.0,
          "midroll_index": 1
        },
        {
          "manifest_urls":{
            ]"1080p": "https://{...}/pod/2/profile/1080p.m3u8",
            "360p": "https://{...}/pod/2/profile.m3u8",
            "subtitles-webvtt": "https://{...}/pod/0/profile/subtitles-en.vtt""
          },
          "type": "post",
          "duration": 10.0
        }
      ]
    }

## Stitch ad pods into content

The process of stitching ad pods into your content streams varies depending
on your implementation, the stream format, and what features you choose to
implement from the format's specifications. The following workflows are
suggestions for how to handle this process. The precise details of your
implementation might vary, based on your business needs and your content
streams.

### HLS streams

If you're stitching a stream in the HLS format, your content stream will be a
[multivariant
playlist](https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist)
of links to separate stream manifests, one for each encoding profile. Your ad
pods needs to be inserted into each of these variant manifests. One way to
do this is to prepare all the variant manifests and pass them to a [Content
Delivery Network (CDN)](https://support.google.com/admanager/answer/7294300) for
hosting. The final multivariant playlist is a set of links to these CDN-hosted
manifests.

#### Iterate over encoding profiles

For each encoding profile, gather all the associated ad pod manifests from
Ad Manager's response, along with their associated start times. For pre-roll ad
pods, set the start time to `0`. For post-rolls, use the content's duration as
the ad pod's start time. Identify the variant stream in the multivariant
playlist that matches each encoding profile's audio and video settings.

> [!TIP]
> **Tip:** In a properly prepared content playlist, each variant manifest should match one of your ad encoding profiles. If these configurations don't precisely match, it's still possible to stitch segments together, but doing so is beyond the scope of this guide.

##### Example ad pods array

    "ad_pods": [
        {
          "manifest_urls":{
            "1080p": "https://{...}/pod/0/profile/1080p.m3u8",
            "360p": "https://{...}/pod/0/profile/360p.m3u8",
            "subtitles-en": "https://{...}/pod/0/profile/subitles-en.vtt"
          },
          "type": "pre",
          "duration": 10.0
        },
        {
          "manifest_urls":{
            "1080p": "https://{...}/pod/1/profile/1080p.m3u8",
            "360p": "https://{...}/pod/1/profile/360p.m3u8",
            "subtitles-en": "https://{...}/pod/1/profile/subitles-en.vtt"
          },
          "type": "mid",
          "start": 15.0,
          "duration": 15.0,
          "midroll_index": 1
        },
        {
          "manifest_urls":{
            "1080p": "https://{...}/pod/2/profile/1080p.m3u8",
            "360p": "https://{...}/pod/2/profile/360p.m3u8",
            "subtitles-en": "https://{...}/pod/2/profile/subitles-en.vtt"
          },
          "type": "post",
          "duration": 10.0
        }
      ]

##### Example multivariant content playlist

    #EXTM3U
    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs0",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="https://{...}/subitles-en.vtt"
    #EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.4d000c,mp4a.40.5"
    https://{...}/1080p.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS="avc1.4d000d,mp4a.40.5"
    https://{...}/360p.m3u8

##### Example collected variant data

    Encoding profile: "1080p"
    Profile settings: {...}
    Content manifest: https://{...}/1080p.m3u8
    Ad pods (start time -> manifest):
        0 -> https://{...}/pod/0/profile/1080p.m3u8
       15 -> https://{...}/pod/1/profile/1080p.m3u8
      600 -> https://{...}/pod/2/profile/1080p.m3u8

#### Insert ads into each variant manifest

For each variant stream, go through the content manifest's segments, keeping a
running total of the elapsed content time. When you come to the start position
of an ad pod, extract the list of segments from the ad pod's manifest, wrap the
list of segments in two `#EXT-X-DISCONTINUITY` tags, and insert the list at the
current location in the content manifest. Continue this process until all ad
pods and variant streams have been processed.

> [!TIP]
> **Tip:** In a properly prepared variant stream, segments should be timed to position ad pods at the break between two segments. If the variant stream is not prepared this way, you might need to either shift the position of the ad pod to the nearest segment boundary or split a segment around an ad pod. Repositioning ad pods and splitting media segments are outside the scope of this guide.

The resulting manifests must conform to the HLS standard. Therefore, depending
on which features of the specification your content manifest incorporates, you
might need to make a final pass over the combined manifest to fix media
sequence numbers, content duration, discontinuity sequence numbers, and any
other tags that need to be updated to take the new ad segments into account.
Once any discrepancies with the standard have been repaired, push each
user-specific variant manifest to your CDN for hosting.

If your content manifest is encrypted, you need to store the last encryption
key found before the start of the current ad pod in an `#EXT-X-KEY` tag. Then,
you need to add the tag `#EXT-X-KEY:METHOD=NONE` to remove encryption before
the first segment of each ad pod. Finally, you must add a copy of the stored
`#EXT-X-KEY` tag before the first segment of content after each ad pod, to
restore the content encryption.

##### Example collected variant data

    Encoding profile: "1080p"
    Content manifest: https://{...}/1080p.m3u8
    Ad pods (start time -> manifest):
        0 -> https://dai.google.com/{...}pod/0/profile/1080p.m3u8
       15 -> https://dai.google.com/{...}pod/1/profile/1080p.m3u8
      600 -> https://dai.google.com/{...}pod/2/profile/1080p.m3u8

##### Example content manifest

This is the content of the `https://{...}/1080p.m3u8` manifest listed in the
collected variant data.

    #EXTM3U
    {...}
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-0.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-1.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-2.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-3.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-4.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-5.ts
    {...}

##### Example ad pod manifest

This is the contents of the
`https://dai.google.com/{...}/pod/1/profile/1080p.m3u8` manifest
listed in the collected variant data.

    #EXTM3U
    {...}
    #EXTINF:5.000,
    https://dai.google.com/{...}/0.ts
    #EXTINF:5.000,
    https://dai.google.com/{...}/1.ts
    #EXTINF:5.000,
    https://dai.google.com/{...}/2.ts

##### Example stitched variant manifest

This would be the resulting stitched variant manifest, passed to the CDN and
hosted at `https://cdn.{...}/{userid}/1080p.m3u8`.

    #EXTM3U
    {...}
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-0.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-1.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-2.ts
    #EXT-X-DISCONTINUITY
    #EXTINF:5.000,
    https://dai.google.com/{...}/0.ts
    #EXTINF:5.000,
    https://dai.google.com/{...}/1.ts
    #EXTINF:5.000,
    https://dai.google.com/{...}/2.ts
    #EXT-X-DISCONTINUITY
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-3.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-4.ts
    #EXTINF:5.000,
    https://{...}/1080p/content-segment-5.ts
    {...}

#### Build multivariant playlist

Collect the CDN addresses for each completed variant manifest, along with the
matching encoding profile details, and assemble the results into a new
multivariant manifest. This user-specific manifest is returned as the response
to the manifest request that you received in Step 1.

##### Example final multivariant playlist

    #EXTM3U
    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs0",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="https://cdn.{...}-subitles-en.vtt"
    #EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.4d000c,mp4a.40.5"
    https://cdn.{...}/{userid}/1080p.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS="avc1.4d000d,mp4a.40.5"
    https://cdn.{...}/{userid}/360p.m3u8

### MPEG DASH streams

If you are stitching a stream in the MPEG DASH format, you only need to produce
a single file. This can make DASH streams easier to stitch than HLS.

A properly prepared MPEG DASH media presentation description (MPD) file should
consist of several periods, each containing multiple representations. Each
representation should match one of your encoding profiles. Each ad pod returned
from Ad Manager is also an MPD file containing a sequence of periods with
matching representations.

To stitch these MPD files together, start by taking note of the start times for
each ad pod. For pre-roll, insert the preroll ad pod periods before any content
period. For post-rolls, insert the postroll ad pod periods after all the content
periods. Iterate over the periods in the content MPD, keeping track of the
elapsed playtime for all processed content periods. When you reach a boundary
between periods that corresponds to an ad pod's start time, insert the periods
from the matching mid-roll ad pod's MPD file at that boundary.

> [!TIP]
> **Tip:** In a properly prepared content file, ad locations should fall between content periods to simplify the stitching process. If the content is not well prepared, you might need to split content periods and possibly the segments themselves at the point where an ad pod needs to be inserted. Splitting existing ad periods and segments is outside the scope of this guide.

The final stitched MPD file must fully conform to the MPEG_DASH specifications,
so you might need to iterate over the final file one more time correcting any
period start times, fixing the media presentation duration to account for the
newly inserted ad periods, and resolving any other conflicts that could have
arisen from the stitching process.

#### Example content MPD

    <?xml version="1.0" encoding="UTF-8"?>
    <MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500000S" type="static" mediaPresentationDuration="PT0H10M00.000S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">
      <ProgramInformation moreInformationURL="http://.../info">
        <Title>Example Stream</Title>
      </ProgramInformation>
      <Period duration="PT0H0M15.000S" id="content-period-1">
        ...
      </Period>
      <Period duration="PT0H0M15.000S" id="content-period-2">
        ...
      </Period>
      <Period duration="PT0H0M15.000S" id="content-period-3">
        ...
      </Period>
      ...
    </MPD>

#### Example ad pod JSON

    [{
      "mpd_uri": "https://{...}pod/1.mpd",
      "type": "mid",
      "start": 15.0,
      "duration": 15.0,
      "midroll_index": 1
    }]

#### Example ad pod MPD

This is the content of the `mpd_uri` from the ad pod JSON above.

    <?xml version="1.0" encoding="UTF-8"?>
    <MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500000S" type="static" mediaPresentationDuration="PT0H0M15.000S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">
      <ProgramInformation moreInformationURL="http://.../info">
        <Title>Ad Pod 1</Title>
      </ProgramInformation>
      \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-1"\>
    ...
    \</Period\>
    \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-2"\>
    ...
    \</Period\>
    \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-3"\>
    ...
    \</Period\>
      ...
    </MPD>

#### Example stitched MPD

Serve this as your response to the initial stream manifest request.

    <?xml version="1.0" encoding="UTF-8"?>
    <MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500000S" type="static" mediaPresentationDuration="PT0H10M15.000S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">
      <ProgramInformation moreInformationURL="http://.../info">
        <Title>Example Stream</Title>
      </ProgramInformation>
      <Period duration="PT0H0M15.000S" id="content-period-1">
        ...
      </Period>
      \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-1"\>
    ...
    \</Period\>
    \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-2"\>
    ...
    \</Period\>
    \<Period duration="PT0H0M5.000S" id="ad-pod-1-period-3"\>
    ...
    \</Period\>
      <Period duration="PT0H0M15.000S" id="content-period-2">
        ...
      </Period>
      <Period duration="PT0H0M15.000S" id="content-period-3">
        ...
      </Period>
      ...
    </MPD>

## Additional resources

- Pod serving playback with the IMA SDK:
  - [Android](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/android?service=pod)
  - [HTML5](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5?service=pod)
  - [iOS](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/ios?service=pod)
  - [tvOS](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/tvos?service=pod)
- [Pod serving playback with the DAI
  API](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/vod/client)