The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager, Google AdMob, VAST tags via SDK extensions developed by Storyteller, and Ads from other sources via custom implementation provided by the integrator.
Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.
If Ads do not appear, follow the shared Ads troubleshooting route first to identify whether the last successful boundary belongs to the tenant strategy, app extension, external provider, or SDK rendering.
If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.
To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.
You will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager.
To use this extension, first install it using Swift Package Manager or Cocoapods.
For Swift Package Manager, it is available on Github here:
A closure that returns the ID of the Ad unit in Google Ad Manager that will be used to serve the Storyteller Ads for the specific Ad request. This can be used for custom Ad units depending on the request context.
bottomBannerAdUnit
Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this nil if you don't plan to serve Clips bottom banner Ads.
Then pass the newly created instance of the extension to the modules property on the Storyteller instance:
This example adds a host-owned GAM targeting value. You can also use configureAdRequest to await your app's Sliide, APS, Nimbus, or other bidder adapter and apply its response to the request before Storyteller starts loading the Ad.
Parameter Name
Description
customNativeTemplateIds
If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct.
publisherProvidedId
Optional closure that returns the Publisher Provided ID (PPID) for Google Ad Manager audience targeting. Return the identifier your GAM setup expects, or nil to omit PPID.
customKvps
A closure that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. Note that the SDK will not inherit any KVPs being set in the rest of your app. Do not pass PPID here; use publisherProvidedId for PPID.
configureAdRequest
Optional async main-actor closure called after Storyteller applies its request setup, default KVPs, customKvps, and publisherProvidedId, but before it loads the Google request. Storyteller awaits this closure before calling the Google load API. Use it for host-owned bidder setup or Google request fields that cannot be expressed as KVPs or PPID.
Storyteller default KVPs and customKvps are only sent when ad tracking is enabled. publisherProvidedId and configureAdRequest are host-controlled request customizations; return nil for PPID and avoid mutating the request when your app should not send those values. configureAdRequest runs on the main actor for full-screen native, custom-template, and banner-fallback GAM requests, and for Clips bottom-banner GAM requests. It is not automatically privacy-gated by Storyteller. Apply any consent, limited-ad-tracking, or regional privacy checks required by your app before mutating the request. Because Storyteller awaits this callback before loading the Google request, keep bidder work bounded and handle any timeout or cancellation fallback inside your app. Storyteller-owned GAM KVP names beginning with st are reserved; do not overwrite or remove them in the callback. If configureAdRequest is omitted, Storyteller builds and loads the request using the existing request behavior.
The Storyteller GAM SDK automatically sends a set of key-value pairs (KVPs) to Google Ad Manager to enable content-based targeting. These KVPs are only sent when ad tracking is enabled (enableAdTracking == true in the StorytellerEventTrackingOptions you pass to Storyteller.shared.initialize(...)).
The Storyteller VAST SDK extension requests vendor-neutral HTTPS VAST tags, resolves compatible inline and wrapper responses, selects playable linear media, and returns Storyteller-rendered fullscreen Ads through the standard module system.
The module is generic VAST support, not an ITV-specific integration. ITV is the first validated production-shaped scenario for the iOS VAST module, but the same module can be configured for any compatible VAST server.
To use this extension, first install it using Swift Package Manager or Cocoapods.
For Swift Package Manager, it is available on Github here:
Closure called for each Ad request. Return the key-value parameters your VAST provider needs for the specific Stories or Clips request context.
urlFormat
Optional serialization strategy. Use .pathSegment to append parameters as /key=value path segments, or .queryString to append them as query parameters. Defaults to .pathSegment.
diagnosticsHandler
Optional closure that receives request, parse, wrapper, media-selection, mapping, completion, and failure diagnostics.
The generated VAST tag URL must use HTTPS and be no longer than 2,048 bytes. The module supports compatible VAST 2.x, 3.x, and 4.x linear video responses, including wrappers up to the module depth limit. Unsupported creative types such as VPAID JavaScript are ignored during media selection.
The VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.
If you use Google Ad Manager to serve VAST video tags, use StorytellerGAMVASTModule. This module is part of StorytellerVASTIntegration; it does not use the Google Mobile Ads SDK or IMA SDK. It builds a GAM VAST tag request and then uses the same Storyteller VAST request, parsing, tracking, and fullscreen Player flow as StorytellerVASTModule.
Required closure that returns the GAM Ad Unit path for the current Ad request. This becomes the GAM iu parameter.
descriptionUrl
Required closure that returns the canonical HTTPS URL describing the video content or Player context. This becomes the GAM description_url parameter.
contentUrl
Optional closure that returns the page or content URL to send as the GAM url parameter. Return nil to omit it.
customParams
Optional closure that returns GAM custom targeting parameters. The SDK serializes these into GAM's cust_params value, so pass unencoded keys and values.
tagParameters
Optional closure that returns extra top-level GAM VAST tag parameters. These are applied after the SDK-generated GAM parameters, so a matching key overrides the generated value.
diagnosticsHandler
Optional closure that receives the same VAST diagnostics events as StorytellerVASTModule.
StorytellerGAMVASTModule generates a request to https://pubads.g.doubleclick.net/gampad/ads with query-string parameters. It supplies iu, output=vast, env=vp, gdfp_req=1, sz, correlator, description_url, optional url, vpa=auto, vpmute, optional cust_params, and the SDK-owned VAST bid parameters listed below. Use tagParameters for any additional GAM VAST tag parameters required by your ad server setup.
The SDK serializes sz as <width>x<height> in physical pixels, with a lowercase x and no spaces or px suffix. It derives this from the active fullscreen Player window or screen and omits sz only when it cannot resolve a reliable non-zero size. customParams are serialized inside cust_params; for example, ["sliide_content_category": "sports"] becomes sliide_content_category=sports inside the cust_params value before the final request URL is encoded.
GAM VAST is VAST-backed, not Google Mobile Ads or IMA-backed. The module sets adSource to .custom("vast"), so Google paid ad analytics events are not emitted for GAM VAST requests.
See VAST and GAM VAST Parameter Tables for the parameters the SDK sets internally, where client-provided values are applied, and which values can be overridden.
The GAM VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.
The SDK builds the Sliide VAST bid parameters first, then merges the returned key-value pairs and serializes the final result using the configured urlFormat. It does not add GAM-specific parameters.
Matching keys in requestParameters override SDK-set Sliide VAST bid parameters.
vw and vh are separate integer values in physical pixels. Do not include px, point units, spaces, or an x separator in those values. For a fullscreen player measured as 1080 by 1920 physical pixels, the SDK sends vw=1080 and vh=1920.
Pure generic VAST does not use GAM cust_params. If you need GAM custom targeting such as a content category, pass it through GAM VAST customParams so the SDK encodes it into cust_params.
GAM VAST Top-Level Parameter
Set By Default
Default Source
Value Type
Client Override
output
Yes
SDK sets vast.
Static
tagParameters["output"]
env
Yes
SDK sets vp.
Static
tagParameters["env"]
gdfp_req
Yes
SDK sets 1.
Static
tagParameters["gdfp_req"]
iu
Yes
adUnit closure.
Dynamic per Ad request
tagParameters["iu"]
sz
When a reliable non-zero size is available.
SDK derives active fullscreen Player window or screen physical pixels as <width>x<height>.
Dynamic per request context
tagParameters["sz"]
correlator
Yes
SDK generates a fresh value for each request.
Dynamic per Ad request
tagParameters["correlator"]
description_url
Yes
descriptionUrl closure.
Dynamic per Ad request
tagParameters["description_url"]
url
When non-nil
contentUrl closure.
Dynamic per Ad request
tagParameters["url"]
vpa
Yes
SDK sets auto.
Static
tagParameters["vpa"]
vpmute
Yes
SDK sets 1 when the Player is muted and 0 when unmuted.
Dynamic per request context
tagParameters["vpmute"]
cust_params
When customParams returns at least one key-value pair.
Encoded customParams output.
Dynamic per Ad request
tagParameters["cust_params"]
GAM VAST Custom Parameter
Recommended Client Path
Encoded Location
Notes
sliide_content_category
customParams["sliide_content_category"]
Inside cust_params.
Use this for Sliide content category targeting.
Additional custom KVPs
customParams
Inside cust_params.
Pass unencoded keys and values; the SDK handles GAM cust_params encoding.
When a VAST response contains multiple URLs for the same supported tracking event, the SDK preserves and fires all of them for the matching Storyteller playback event.
VAST skip tracking maps to the Storyteller skipped-Ad flow. VAST skipoffset is converted to an ad-specific non-skippable duration in the Storyteller Player. When skipoffset is present, it controls the countdown for that VAST Ad; when it is absent, the SDK uses the tenant's CMS-configured non-skippable Ads behavior.
VAST <Error> URLs are reported by the VAST module when the SDK cannot request, parse, resolve, select media for, or map a VAST Ad. These failures cause the module to fail safely so Storyteller can continue through the normal module fallback flow.
VAST ads can provide Storyteller presentation metadata through Extension type="storyteller:ad-ui". The SDK reads CtaText and AdvertiserName from the StorytellerAdUi child element:
CTA text is only shown when the VAST ad has a non-empty <ClickThrough> destination. VAST <Icon> elements are reserved for industry, privacy, or program overlays and are not used as brand logos.
For fullscreen Story and Clip VAST ads, the SDK renders one compatible static-resource <Icon> overlay per ad only when the icon has program="AdChoices". Non-AdChoices icons, <IFrameResource> icons, and <HTMLResource> icons are parsed for diagnostics but are not rendered.
The icon is rendered inside the video frame, including landscape videos that are aspect-fitted inside the Player. The SDK honors VAST xPosition values of left, right, or a numeric x-coordinate, and yPosition values of top, bottom, or a numeric y-coordinate. The overlay has a fixed 24pt height and preserves the static resource aspect ratio for its width. VAST offset and duration values control when the icon appears, IconViewTracking is fired once when it first becomes visible, and IconClickTracking is fired when the icon is tapped. If IconClickThrough is present, tapping the icon opens that destination externally.
AdMob support uses the same StorytellerGAMIntegration artifact but a different module entry point. Only one ads integration module can be used at a time.
Due to AdMob limitations, banner Ads cannot be served from the same Ad unit as native Ads. adUnit is always used for native Ads, while bannerAdUnit is used for fullscreen banner Ads.
Default (enableBannerAdPriority = false): the module tries to load a native Ad from adUnit first. If native loading fails and bannerAdUnit is configured, it falls back to a fullscreen banner Ad from bannerAdUnit.
Banner priority enabled (enableBannerAdPriority = true): if bannerAdUnit is configured, the module tries to load a fullscreen banner Ad first. If banner loading fails, it falls back to a native Ad from adUnit.
enableBannerAdPriority only affects this fullscreen fallback order. It has no effect when bannerAdUnit is nil, and it does not change Clips bottom banner behavior configured through bottomBannerAdUnit.
Required closure that returns the native Ad unit ID.
bannerAdUnit
Optional closure that returns a fullscreen banner Ad unit ID. If supplied, the module can use it as the banner fallback path or the banner-first path when enableBannerAdPriority is enabled.
bottomBannerAdUnit
Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this nil if you don't plan to serve Clips bottom banner Ads.
customKvps
Optional closure that returns custom key-value pairs to attach to AdMob requests for targeting.
enableBannerAdPriority
Optional flag that changes the fullscreen Ad loading order to banner first, then native fallback. This flag only has an effect when bannerAdUnit is configured.
For a complete integration example, see our Showcase app code here.
StorytellerGAMModule and StorytellerAdMobModule are mutually exclusive. Configure only one of them at a time.
StorytellerVASTModule and StorytellerGAMVASTModule are separate fullscreen Ads modules. If you use either with any other Ads module, order the Storyteller.shared.modules array deliberately because the SDK asks modules for Ads in order and falls back to the next module when one throws.
The Clips Player supports bottom banner Ads rendered as standard banner views added to the hierarchy below the video view. Bottom banner Ads are disabled when StorytellerClipCollectionConfiguration.adConfiguration is omitted or set to nil, and for new StorytellerClipsAdConfiguration instances. To opt a Clips presentation into bottom banner Ads, pass StorytellerClipsAdConfiguration(bottomBannerEnabled: true) through StorytellerClipCollectionConfiguration.adConfiguration.
When using the GAM or AdMob module, supply bottomBannerAdUnit in the corresponding configuration to fetch bottom banner Ads. Opting in locally does not force Ads on: the tenant feed response must still enable the placement with showBottomBannerAd == true, and the active Ads module must support it.
Storyteller First Party Ads, StorytellerVASTModule, and StorytellerGAMVASTModule do not support bottom banner Ads.
If your tenant is configured for Clips Ads with initialIndex = 0, individual Clips presentations can opt into an opening fullscreen Ad before the first Clip is played by passing StorytellerClipsAdConfiguration(preRollEnabled: true) through StorytellerClipCollectionConfiguration.adConfiguration.
Opening pre-roll is disabled when StorytellerClipCollectionConfiguration.adConfiguration is omitted or set to nil, and for new StorytellerClipsAdConfiguration instances. Opting in locally does not force Ads on: Clips Ads must still be available for the tenant, and the remote Clips Ad strategy must use initialIndex = 0. The request uses the first opened content Clip as the current Clip context and sends adIndex = 1, which maps to the default stAdIndex = 1 value for Google Ad Manager and AdMob integrations.
When a CMS-configured opening pre-roll timeout is reached before the Ad loads, the Player starts the content Clip and ignores any late opening pre-roll result. Later between-Clip Ads continue to use the normal Clips ad cadence and increment from the next Ad index.
Note: This section is only relevant if you're implementing a custom Ads solution or building VAST request parameters. If you're using Storyteller First Party Ads, StorytellerGAMModule, or StorytellerAdMobModule, you don't need to work with this directly.
If your StorytellerModule (or StorytellerDelegate) provides integrating-app ads, set adSource to declare the source used by your implementation.
For custom ad implementations, use .custom("myNetwork").
For Google modules, use .gam for GAM and .admob for AdMob.
The VAST and GAM VAST modules set .custom("vast") automatically.
.storyteller is reserved for Storyteller First Party ads.
Setting adSource to .gam or .admob enables Google paid ad analytics events. See Ad Events.
StorytellerGAMModule, StorytellerAdMobModule, StorytellerVASTModule, and StorytellerGAMVASTModule set adSource automatically.
When implementing custom Ads, you'll receive context about the Ad request through the StorytellerAdRequestInfo enum. This provides information about what content the Ad will be displayed for.
Our Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS.
{"slug": "ads", "page_title": "Integrate Ads", "page_url": "Ads/", "canonical_url": "/ios/Ads/", "markdown": "# Ads\n\n## Introduction\n\nThe Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager, Google AdMob, VAST tags via SDK extensions developed by Storyteller, and Ads from other sources via custom implementation provided by the integrator.\n\nWhich source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.\n\nIf Ads do not appear, follow the shared [Ads troubleshooting route](Troubleshooting.md#ads-do-not-appear) first to identify whether the last successful boundary belongs to the tenant strategy, app extension, external provider, or SDK rendering.\n\n## Storyteller First Party Ads\n\nIf your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.\n\n## Storyteller GAM SDK\n\nTo use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.\n\nYou will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager.\n\nTo use this extension, first install it using Swift Package Manager or Cocoapods.\n\nFor Swift Package Manager, it is available on [Github](https://github.com/getstoryteller/storyteller-gam-module-swift) here:\n\n```bash\nhttps://github.com/getstoryteller/storyteller-gam-module-swift\n```\n\nFor Cocoapods, first make sure to specify the sources for Cocoapods:\n\n```bash\nsource 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n```\n\nThe StorytellerGAMIntegration is available by importing this pod:\n\n```bash\npod 'StorytellerGAMIntegration'\n```\n\n### Basic Setup\n\nMake sure to import the same version of `StorytellerSDK` and `StorytellerGAMIntegration`.\n\nNow initialize the extension as follows:\n\n<!-- storyteller-swift-example: id=ads-01 target=gam-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n adUnit: { requestInfo in\n return \"YOUR_AD_UNIT_ID\"\n }\n)\n```\n\nYou will need to supply the following parameter:\n\n| Parameter Name | Description |\n|----------------|-------------|\n| `adUnit` | A closure that returns the ID of the Ad unit in Google Ad Manager that will be used to serve the Storyteller Ads for the specific Ad request. This can be used for custom Ad units depending on the request context. |\n| `bottomBannerAdUnit` | Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this `nil` if you don't plan to serve Clips bottom banner Ads. |\n\nThen pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance:\n\n<!-- storyteller-swift-example: id=ads-02 target=gam-ios context=statements -->\n\n```swift\nlet configuration = StorytellerGAMModuleConfiguration(\n adUnit: { _ in \"YOUR_AD_UNIT_ID\" }\n)\nStoryteller.shared.modules = [StorytellerGAMModule(configuration: configuration)]\n```\n\nOur Showcase app uses this module to integrate ads - see the GAM module configuration in [`AppDelegate.setupStoryteller`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/ShowcaseApp.swift#L95).\n\n### Setup with Dynamic Ad Unit Changes\n\nExample for dynamic Ad unit changes when you want to use different Ad units for Stories and Clips:\n\n<!-- storyteller-swift-example: id=ads-03 target=gam-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n adUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"/33813572/storyteller/stories\"\n case .clips:\n return \"/33813572/storyteller/clips\"\n @unknown default:\n return \"/33813572/storyteller/default\"\n }\n }\n)\n```\n\n### Setup with Additional Parameters\n\nYou can also supply optional parameters `customNativeTemplateIds`, `publisherProvidedId`, `customKvps`, and `configureAdRequest` if needed:\n\n<!-- storyteller-swift-example: id=ads-04 target=gam-ios context=statements -->\n\n```swift\nimport GoogleMobileAds\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n adUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"/33813572/storyteller/stories\"\n case .clips:\n return \"/33813572/storyteller/clips\"\n @unknown default:\n return \"/33813572/storyteller/default\"\n }\n },\n customNativeTemplateIds: StorytellerGAMModuleConfiguration.CustomNativeTemplateIds(\n stories: \"YOUR_STORIES_TEMPLATE_ID\",\n clips: \"YOUR_CLIPS_TEMPLATE_ID\"\n ),\n publisherProvidedId: {\n \"YOUR_PUBLISHER_PROVIDED_ID\"\n },\n customKvps: {\n [\n \"YOUR_CUSTOM_KEY\": \"YOUR_CUSTOM_VALUE\",\n \"YOUR_PRIVACY_KEY\": \"YOUR_PRIVACY_VALUE\"\n ]\n },\n configureAdRequest: { _, request in\n var targeting = request.customTargeting ?? [:]\n targeting[\"host_app_context\"] = \"sports\"\n request.customTargeting = targeting\n }\n)\n```\n\nThis example adds a host-owned GAM targeting value. You can also use `configureAdRequest` to await your app's Sliide, APS, Nimbus, or other bidder adapter and apply its response to the request before Storyteller starts loading the Ad.\n\n| Parameter Name | Description |\n|----------------|-------------|\n| `customNativeTemplateIds` | If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct. |\n| `publisherProvidedId` | Optional closure that returns the Publisher Provided ID (PPID) for Google Ad Manager audience targeting. Return the identifier your GAM setup expects, or `nil` to omit PPID. |\n| `customKvps` | A closure that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. Note that the SDK will not inherit any KVPs being set in the rest of your app. Do not pass PPID here; use `publisherProvidedId` for PPID. |\n| `configureAdRequest` | Optional async main-actor closure called after Storyteller applies its request setup, default KVPs, `customKvps`, and `publisherProvidedId`, but before it loads the Google request. Storyteller awaits this closure before calling the Google load API. Use it for host-owned bidder setup or Google request fields that cannot be expressed as KVPs or PPID. |\n\nStoryteller default KVPs and `customKvps` are only sent when ad tracking is enabled. `publisherProvidedId` and `configureAdRequest` are host-controlled request customizations; return `nil` for PPID and avoid mutating the request when your app should not send those values. `configureAdRequest` runs on the main actor for full-screen native, custom-template, and banner-fallback GAM requests, and for Clips bottom-banner GAM requests. It is not automatically privacy-gated by Storyteller. Apply any consent, limited-ad-tracking, or regional privacy checks required by your app before mutating the request. Because Storyteller awaits this callback before loading the Google request, keep bidder work bounded and handle any timeout or cancellation fallback inside your app. Storyteller-owned GAM KVP names beginning with `st` are reserved; do not overwrite or remove them in the callback. If `configureAdRequest` is omitted, Storyteller builds and loads the request using the existing request behavior.\n\n#### Default KVPs\n\nThe Storyteller GAM SDK automatically sends a set of key-value pairs (KVPs) to Google Ad Manager to enable content-based targeting. These KVPs are only sent when ad tracking is enabled (`enableAdTracking == true` in the `StorytellerEventTrackingOptions` you pass to `Storyteller.shared.initialize(...)`).\n\n**For Stories:**\n\n| KVP Key | Description |\n|---------|-------------|\n| `stCategories` | Categories associated with the current story |\n| `stCurrentCategory` | Categories of the list containing the story |\n| `stPlacement` | The placement identifier of the story |\n| `stApiKey` | The current Storyteller API key |\n| `stAdIndex` | The order of the ad within the story |\n\n**For Clips:**\n\n| KVP Key | Description |\n|---------|-------------|\n| `stCollection` | The identifier of the clip collection |\n| `stClipCategories` | Categories associated with the current clip |\n| `stNextClipCategories` | Categories of the next clip (if available) |\n| `stApiKey` | The current Storyteller API key |\n| `stAdIndex` | The order of the ad within the clip collection |\n\n## Storyteller VAST SDK\n\nThe Storyteller VAST SDK extension requests vendor-neutral HTTPS VAST tags, resolves compatible inline and wrapper responses, selects playable linear media, and returns Storyteller-rendered fullscreen Ads through the standard module system.\n\nThe module is generic VAST support, not an ITV-specific integration. ITV is the first validated production-shaped scenario for the iOS VAST module, but the same module can be configured for any compatible VAST server.\n\nTo use this extension, first install it using Swift Package Manager or Cocoapods.\n\nFor Swift Package Manager, it is available on [Github](https://github.com/getstoryteller/storyteller-vast-module-swift) here:\n\n```bash\nhttps://github.com/getstoryteller/storyteller-vast-module-swift\n```\n\nFor Cocoapods, first make sure to specify the sources for Cocoapods:\n\n```bash\nsource 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n```\n\nThe StorytellerVASTIntegration is available by importing this pod:\n\n```bash\npod 'StorytellerVASTIntegration'\n```\n\n### Basic Setup\n\nMake sure to import the same version of `StorytellerSDK` and `StorytellerVASTIntegration`.\n\n<!-- storyteller-swift-example: id=ads-05 target=vast-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerVASTIntegration\n\nlet configuration = StorytellerVASTModuleConfiguration(\n baseUrl: \"https://ads.example.com/vast\",\n requestParameters: { _ in\n [\n \"placement\": \"storyteller\"\n ]\n },\n urlFormat: .queryString\n)\n\nStoryteller.shared.modules = [StorytellerVASTModule(configuration: configuration)]\n```\n\nYou will need to supply the following parameters:\n\n| Parameter Name | Description |\n|----------------|-------------|\n| `baseUrl` | HTTPS base URL of the VAST tag endpoint. |\n| `requestParameters` | Closure called for each Ad request. Return the key-value parameters your VAST provider needs for the specific Stories or Clips request context. |\n| `urlFormat` | Optional serialization strategy. Use `.pathSegment` to append parameters as `/key=value` path segments, or `.queryString` to append them as query parameters. Defaults to `.pathSegment`. |\n| `diagnosticsHandler` | Optional closure that receives request, parse, wrapper, media-selection, mapping, completion, and failure diagnostics. |\n\nThe generated VAST tag URL must use HTTPS and be no longer than 2,048 bytes. The module supports compatible VAST 2.x, 3.x, and 4.x linear video responses, including wrappers up to the module depth limit. Unsupported creative types such as VPAID JavaScript are ignored during media selection.\n\nThe VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.\n\n### GAM VAST Setup\n\nIf you use Google Ad Manager to serve VAST video tags, use `StorytellerGAMVASTModule`. This module is part of `StorytellerVASTIntegration`; it does not use the Google Mobile Ads SDK or IMA SDK. It builds a GAM VAST tag request and then uses the same Storyteller VAST request, parsing, tracking, and fullscreen Player flow as `StorytellerVASTModule`.\n\n<!-- storyteller-swift-example: id=ads-06 target=vast-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerVASTIntegration\n\nlet configuration = StorytellerGAMVASTModuleConfiguration(\n adUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"/33813572/storyteller/stories_vast\"\n case .clips:\n return \"/33813572/storyteller/clips_vast\"\n @unknown default:\n return \"/33813572/storyteller/default_vast\"\n }\n },\n descriptionUrl: { _ in\n \"https://example.com/storyteller-video\"\n },\n contentUrl: { _ in\n \"https://example.com/storyteller-video\"\n },\n customParams: { _ in\n [\n \"sliide_content_category\": \"sports\"\n ]\n }\n)\n\nStoryteller.shared.modules = [StorytellerGAMVASTModule(configuration: configuration)]\n```\n\nYou will need to supply the following parameters:\n\n| Parameter Name | Description |\n|----------------|-------------|\n| `adUnit` | Required closure that returns the GAM Ad Unit path for the current Ad request. This becomes the GAM `iu` parameter. |\n| `descriptionUrl` | Required closure that returns the canonical HTTPS URL describing the video content or Player context. This becomes the GAM `description_url` parameter. |\n| `contentUrl` | Optional closure that returns the page or content URL to send as the GAM `url` parameter. Return `nil` to omit it. |\n| `customParams` | Optional closure that returns GAM custom targeting parameters. The SDK serializes these into GAM's `cust_params` value, so pass unencoded keys and values. |\n| `tagParameters` | Optional closure that returns extra top-level GAM VAST tag parameters. These are applied after the SDK-generated GAM parameters, so a matching key overrides the generated value. |\n| `diagnosticsHandler` | Optional closure that receives the same VAST diagnostics events as `StorytellerVASTModule`. |\n\n`StorytellerGAMVASTModule` generates a request to `https://pubads.g.doubleclick.net/gampad/ads` with query-string parameters. It supplies `iu`, `output=vast`, `env=vp`, `gdfp_req=1`, `sz`, `correlator`, `description_url`, optional `url`, `vpa=auto`, `vpmute`, optional `cust_params`, and the SDK-owned VAST bid parameters listed below. Use `tagParameters` for any additional GAM VAST tag parameters required by your ad server setup.\n\nThe SDK serializes `sz` as `<width>x<height>` in physical pixels, with a lowercase `x` and no spaces or `px` suffix. It derives this from the active fullscreen Player window or screen and omits `sz` only when it cannot resolve a reliable non-zero size. `customParams` are serialized inside `cust_params`; for example, `[\"sliide_content_category\": \"sports\"]` becomes `sliide_content_category=sports` inside the `cust_params` value before the final request URL is encoded.\n\nGAM VAST is VAST-backed, not Google Mobile Ads or IMA-backed. The module sets `adSource` to `.custom(\"vast\")`, so Google paid ad analytics events are not emitted for GAM VAST requests.\n\nSee [VAST and GAM VAST Parameter Tables](#vast-and-gam-vast-parameter-tables) for the parameters the SDK sets internally, where client-provided values are applied, and which values can be overridden.\n\nThe GAM VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.\n\n### VAST and GAM VAST Parameter Tables\n\nUse these tables to decide whether a value belongs in generic VAST `requestParameters`, GAM VAST `customParams`, or GAM VAST `tagParameters`.\n\n| Surface | Client Parameter Path | SDK Behavior | Override Behavior |\n|---------|-----------------------|--------------|-------------------|\n| Generic VAST tag parameters | `StorytellerVASTModuleConfiguration.requestParameters` | The SDK builds the Sliide VAST bid parameters first, then merges the returned key-value pairs and serializes the final result using the configured `urlFormat`. It does not add GAM-specific parameters. | Matching keys in `requestParameters` override SDK-set Sliide VAST bid parameters. |\n| GAM VAST custom targeting | `StorytellerGAMVASTModuleConfiguration.customParams` | The SDK encodes the returned key-value pairs into GAM's `cust_params` value. | Change individual custom targeting values in `customParams`, or replace the generated `cust_params` by returning `cust_params` from `tagParameters`. |\n| GAM VAST top-level parameters | `StorytellerGAMVASTModuleConfiguration.tagParameters` | The SDK builds GAM VAST parameters and Sliide VAST bid parameters first, then applies these top-level parameters. | Matching keys in `tagParameters` are the final override layer. |\n\n| Sliide VAST Bid Parameter | Applies To | Value / Source | Static or Dynamic | Client Override |\n|---------------------------|------------|----------------|-------------------|-----------------|\n| `adtype` | Generic VAST and GAM VAST | `13` | Static | Generic: `requestParameters[\"adtype\"]`; GAM VAST: `tagParameters[\"adtype\"]` |\n| `plcmt` | Generic VAST and GAM VAST | `3` | Static | Generic: `requestParameters[\"plcmt\"]`; GAM VAST: `tagParameters[\"plcmt\"]` |\n| `vw` | Generic VAST and GAM VAST | Player width in physical pixels, for example `1080` | Dynamic; omitted when no reliable size is available | Generic: `requestParameters[\"vw\"]`; GAM VAST: `tagParameters[\"vw\"]` |\n| `vh` | Generic VAST and GAM VAST | Player height in physical pixels, for example `1920` | Dynamic; omitted when no reliable size is available | Generic: `requestParameters[\"vh\"]`; GAM VAST: `tagParameters[\"vh\"]` |\n| `vminl` | Generic VAST and GAM VAST | `5` | Static | Generic: `requestParameters[\"vminl\"]`; GAM VAST: `tagParameters[\"vminl\"]` |\n| `vmaxl` | Generic VAST and GAM VAST | `30` | Static | Generic: `requestParameters[\"vmaxl\"]`; GAM VAST: `tagParameters[\"vmaxl\"]` |\n| `vfmt` | Generic VAST and GAM VAST | `1` for MP4 playable media format on iOS | Static | Generic: `requestParameters[\"vfmt\"]`; GAM VAST: `tagParameters[\"vfmt\"]` |\n| `vadFmt` | Generic VAST and GAM VAST | `2+3+8` | Static | Generic: `requestParameters[\"vadFmt\"]`; GAM VAST: `tagParameters[\"vadFmt\"]` |\n| `vplay` | Generic VAST and GAM VAST | `6` for muted autoplay, `5` for sound-on autoplay | Dynamic; falls back to `6` when mute state is unavailable | Generic: `requestParameters[\"vplay\"]`; GAM VAST: `tagParameters[\"vplay\"]` |\n| `vskip` | Generic VAST and GAM VAST | `1` | Static | Generic: `requestParameters[\"vskip\"]`; GAM VAST: `tagParameters[\"vskip\"]` |\n| `vpos` | Generic VAST and GAM VAST | `0` | Static | Generic: `requestParameters[\"vpos\"]`; GAM VAST: `tagParameters[\"vpos\"]` |\n| `vcom` | Generic VAST and GAM VAST | `0` | Static | Generic: `requestParameters[\"vcom\"]`; GAM VAST: `tagParameters[\"vcom\"]` |\n| `vcont` | Generic VAST and GAM VAST | `1` | Static | Generic: `requestParameters[\"vcont\"]`; GAM VAST: `tagParameters[\"vcont\"]` |\n| `vtype` | Generic VAST and GAM VAST | `1` | Static | Generic: `requestParameters[\"vtype\"]`; GAM VAST: `tagParameters[\"vtype\"]` |\n| `vminbtr` | Generic VAST and GAM VAST | `600` | Static | Generic: `requestParameters[\"vminbtr\"]`; GAM VAST: `tagParameters[\"vminbtr\"]` |\n| `vmaxbtr` | Generic VAST and GAM VAST | `8000` | Static | Generic: `requestParameters[\"vmaxbtr\"]`; GAM VAST: `tagParameters[\"vmaxbtr\"]` |\n\n`vw` and `vh` are separate integer values in physical pixels. Do not include `px`, point units, spaces, or an `x` separator in those values. For a fullscreen player measured as 1080 by 1920 physical pixels, the SDK sends `vw=1080` and `vh=1920`.\n\nPure generic VAST does not use GAM `cust_params`. If you need GAM custom targeting such as a content category, pass it through GAM VAST `customParams` so the SDK encodes it into `cust_params`.\n\n| GAM VAST Top-Level Parameter | Set By Default | Default Source | Value Type | Client Override |\n|------------------------------|----------------|----------------|------------|-----------------|\n| `output` | Yes | SDK sets `vast`. | Static | `tagParameters[\"output\"]` |\n| `env` | Yes | SDK sets `vp`. | Static | `tagParameters[\"env\"]` |\n| `gdfp_req` | Yes | SDK sets `1`. | Static | `tagParameters[\"gdfp_req\"]` |\n| `iu` | Yes | `adUnit` closure. | Dynamic per Ad request | `tagParameters[\"iu\"]` |\n| `sz` | When a reliable non-zero size is available. | SDK derives active fullscreen Player window or screen physical pixels as `<width>x<height>`. | Dynamic per request context | `tagParameters[\"sz\"]` |\n| `correlator` | Yes | SDK generates a fresh value for each request. | Dynamic per Ad request | `tagParameters[\"correlator\"]` |\n| `description_url` | Yes | `descriptionUrl` closure. | Dynamic per Ad request | `tagParameters[\"description_url\"]` |\n| `url` | When non-`nil` | `contentUrl` closure. | Dynamic per Ad request | `tagParameters[\"url\"]` |\n| `vpa` | Yes | SDK sets `auto`. | Static | `tagParameters[\"vpa\"]` |\n| `vpmute` | Yes | SDK sets `1` when the Player is muted and `0` when unmuted. | Dynamic per request context | `tagParameters[\"vpmute\"]` |\n| `cust_params` | When `customParams` returns at least one key-value pair. | Encoded `customParams` output. | Dynamic per Ad request | `tagParameters[\"cust_params\"]` |\n\n| GAM VAST Custom Parameter | Recommended Client Path | Encoded Location | Notes |\n|---------------------------|-------------------------|------------------|-------|\n| `sliide_content_category` | `customParams[\"sliide_content_category\"]` | Inside `cust_params`. | Use this for Sliide content category targeting. |\n| Additional custom KVPs | `customParams` | Inside `cust_params`. | Pass unencoded keys and values; the SDK handles GAM `cust_params` encoding. |\n\n### VAST URL Formats\n\nUse `.pathSegment` when your VAST server expects parameters appended as path segments:\n\n<!-- storyteller-swift-example: id=ads-07 target=vast-ios context=statements -->\n\n```swift\nlet configuration = StorytellerVASTModuleConfiguration(\n baseUrl: \"https://ads.example.com/vast\",\n requestParameters: { _ in\n [\n \"placement\": \"stories\",\n \"adIndex\": \"1\"\n ]\n },\n urlFormat: .pathSegment\n)\n```\n\nThis produces a request shaped like:\n\n```text\nhttps://ads.example.com/vast/placement=stories/adIndex=1\n```\n\nUse `.queryString` when your VAST server expects standard query parameters:\n\n<!-- storyteller-swift-example: id=ads-08 target=vast-ios context=statements -->\n\n```swift\nlet configuration = StorytellerVASTModuleConfiguration(\n baseUrl: \"https://ads.example.com/vast\",\n requestParameters: { _ in\n [\n \"placement\": \"stories\",\n \"adIndex\": \"1\"\n ]\n },\n urlFormat: .queryString\n)\n```\n\nThis produces a request shaped like:\n\n```text\nhttps://ads.example.com/vast?placement=stories&adIndex=1\n```\n\nThe SDK encodes parameter keys and values for the selected format. Pass unencoded values from your callback.\n\n### VAST Support in the First Release\n\nThe first iOS release supports:\n\n- Inline linear fullscreen video Ads for Stories and Clips\n- Compatible VAST 2.x, 3.x, and 4.x parsing\n- Wrapper resolution and fallback to the first playable Ad in the response\n- Media file selection for compatible video media\n- Click-through and click tracking\n- Impression, creative view, start, quartile, complete, pause, resume, mute, unmute, close, and skip tracking\n- Multiple tracking URLs for the same VAST event\n- Deferred VAST error reporting when request, parse, wrapper, media selection, or mapping fails\n- Storyteller presentation metadata through `Extension type=\"storyteller:ad-ui\"`\n- One compatible static-resource VAST `<Icon>` overlay for fullscreen Story and Clip Ads\n\nThe first iOS release does not support:\n\n- Nonlinear Ads\n- Companion Ads\n- OMID / verification rendering\n- VPAID or SIMID\n- Server-side ad insertion (SSAI)\n- Clips bottom banner Ads through VAST\n- VAST ad caption rendering\n- Rendering VAST `<IFrameResource>` or `<HTMLResource>` icons\n\n### VAST Tracking and Skip Behavior\n\nWhen a VAST response contains multiple URLs for the same supported tracking event, the SDK preserves and fires all of them for the matching Storyteller playback event.\n\nVAST `skip` tracking maps to the Storyteller skipped-Ad flow. VAST `skipoffset` is converted to an ad-specific non-skippable duration in the Storyteller Player. When `skipoffset` is present, it controls the countdown for that VAST Ad; when it is absent, the SDK uses the tenant's CMS-configured non-skippable Ads behavior.\n\nVAST `<Error>` URLs are reported by the VAST module when the SDK cannot request, parse, resolve, select media for, or map a VAST Ad. These failures cause the module to fail safely so Storyteller can continue through the normal module fallback flow.\n\n### VAST Branded Presentation Metadata\n\nVAST ads can provide Storyteller presentation metadata through `Extension type=\"storyteller:ad-ui\"`. The SDK reads `CtaText` and `AdvertiserName` from the `StorytellerAdUi` child element:\n\n```xml\n<Extension type=\"storyteller:ad-ui\">\n <StorytellerAdUi>\n <CtaText>Shop now<\/CtaText>\n <AdvertiserName>Example Brand<\/AdvertiserName>\n <\/StorytellerAdUi>\n<\/Extension>\n```\n\nThe SDK resolves the final presentation values with this priority:\n\n| Field | Resolution priority |\n| ----- | ------------------- |\n| CTA text | VAST extension `CtaText` -> backend default `ads.presentationDefaults.vast.ctaText` -> `Learn more` |\n| Advertiser display name | VAST extension `AdvertiserName` -> backend default `ads.presentationDefaults.vast.advertiserName` -> VAST `<Advertiser>` -> VAST `<AdTitle>` |\n\nCTA text is only shown when the VAST ad has a non-empty `<ClickThrough>` destination. VAST `<Icon>` elements are reserved for industry, privacy, or program overlays and are not used as brand logos.\n\n### VAST Icon Overlays\n\nFor fullscreen Story and Clip VAST ads, the SDK renders one compatible static-resource `<Icon>` overlay per ad only when the icon has `program=\"AdChoices\"`. Non-AdChoices icons, `<IFrameResource>` icons, and `<HTMLResource>` icons are parsed for diagnostics but are not rendered.\n\nThe icon is rendered inside the video frame, including landscape videos that are aspect-fitted inside the Player. The SDK honors VAST `xPosition` values of `left`, `right`, or a numeric x-coordinate, and `yPosition` values of `top`, `bottom`, or a numeric y-coordinate. The overlay has a fixed 24pt height and preserves the static resource aspect ratio for its width. VAST `offset` and `duration` values control when the icon appears, `IconViewTracking` is fired once when it first becomes visible, and `IconClickTracking` is fired when the icon is tapped. If `IconClickThrough` is present, tapping the icon opens that destination externally.\n\n### Setup with Dynamic Parameters\n\nUse `StorytellerAdRequestInfo` to provide different parameters for Stories and Clips:\n\n<!-- storyteller-swift-example: id=ads-09 target=vast-ios context=statements -->\n\n```swift\nlet configuration = StorytellerVASTModuleConfiguration(\n baseUrl: \"https://ads.example.com/vast\",\n requestParameters: { requestInfo in\n switch requestInfo {\n case let .stories(placement, categories, story, adIndex):\n return [\n \"placement\": placement,\n \"categories\": categories.joined(separator: \",\"),\n \"storyCategories\": story.categories.map(\\.externalId).joined(separator: \",\"),\n \"adIndex\": \"\\(adIndex)\"\n ]\n case let .clips(collection, clip, nextClip, adIndex):\n return [\n \"collection\": collection,\n \"clipCategories\": clip.categories.map(\\.externalId).joined(separator: \",\"),\n \"nextClipCategories\": nextClip?.categories.map(\\.externalId).joined(separator: \",\") ?? \"\",\n \"adIndex\": \"\\(adIndex)\"\n ]\n @unknown default:\n return [:]\n }\n },\n urlFormat: .queryString\n)\n```\n\n## Storyteller AdMob SDK\n\nAdMob support uses the same StorytellerGAMIntegration artifact but a different module entry point. Only one ads integration module can be used at a time.\n\nDue to AdMob limitations, banner Ads cannot be served from the same Ad unit as native Ads. `adUnit` is always used for native Ads, while `bannerAdUnit` is used for fullscreen banner Ads.\n\n- Default (`enableBannerAdPriority = false`): the module tries to load a native Ad from `adUnit` first. If native loading fails and `bannerAdUnit` is configured, it falls back to a fullscreen banner Ad from `bannerAdUnit`.\n- Banner priority enabled (`enableBannerAdPriority = true`): if `bannerAdUnit` is configured, the module tries to load a fullscreen banner Ad first. If banner loading fails, it falls back to a native Ad from `adUnit`.\n\n`enableBannerAdPriority` only affects this fullscreen fallback order. It has no effect when `bannerAdUnit` is `nil`, and it does not change Clips bottom banner behavior configured through `bottomBannerAdUnit`.\n\n### Basic Setup\n\n<!-- storyteller-swift-example: id=ads-10 target=gam-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n adUnit: { requestInfo in\n return \"YOUR_NATIVE_AD_UNIT_ID\"\n },\n bannerAdUnit: { requestInfo in\n return \"YOUR_BANNER_AD_UNIT_ID\"\n },\n bottomBannerAdUnit: { requestInfo in\n return \"YOUR_BOTTOM_BANNER_AD_UNIT_ID\"\n }\n)\n\nStoryteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)]\n```\n\n| Parameter Name | Description |\n|----------------|-------------|\n| `adUnit` | Required closure that returns the native Ad unit ID. |\n| `bannerAdUnit` | Optional closure that returns a fullscreen banner Ad unit ID. If supplied, the module can use it as the banner fallback path or the banner-first path when `enableBannerAdPriority` is enabled. |\n| `bottomBannerAdUnit` | Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this `nil` if you don't plan to serve Clips bottom banner Ads. |\n| `customKvps` | Optional closure that returns custom key-value pairs to attach to AdMob requests for targeting. |\n| `enableBannerAdPriority` | Optional flag that changes the fullscreen Ad loading order to banner first, then native fallback. This flag only has an effect when `bannerAdUnit` is configured. |\n\nFor a complete integration example, see our Showcase app code [here](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/ShowcaseApp.swift#L95).\n\n### Setup with Banner Priority Enabled\n\n<!-- storyteller-swift-example: id=ads-11 target=gam-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n adUnit: { _ in\n \"YOUR_NATIVE_AD_UNIT_ID\"\n },\n bannerAdUnit: { _ in\n \"YOUR_BANNER_AD_UNIT_ID\"\n },\n enableBannerAdPriority: true\n)\n\nStoryteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)]\n```\n\n### Setup with Dynamic Ad Unit Changes\n\n<!-- storyteller-swift-example: id=ads-12 target=gam-ios context=statements -->\n\n```swift\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n adUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"YOUR_STORIES_NATIVE_AD_UNIT_ID\"\n case .clips:\n return \"YOUR_CLIPS_NATIVE_AD_UNIT_ID\"\n @unknown default:\n return \"YOUR_DEFAULT_NATIVE_AD_UNIT_ID\"\n }\n },\n bannerAdUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"YOUR_STORIES_BANNER_AD_UNIT_ID\"\n case .clips:\n return \"YOUR_CLIPS_BANNER_AD_UNIT_ID\"\n @unknown default:\n return \"YOUR_DEFAULT_BANNER_AD_UNIT_ID\"\n }\n },\n bottomBannerAdUnit: { requestInfo in\n switch requestInfo {\n case .stories:\n return \"YOUR_STORIES_BOTTOM_BANNER_AD_UNIT_ID\"\n case .clips:\n return \"YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT_ID\"\n @unknown default:\n return \"YOUR_DEFAULT_BOTTOM_BANNER_AD_UNIT_ID\"\n }\n },\n customKvps: {\n [\"appmode\": \"prod\"]\n },\n enableBannerAdPriority: true\n)\n```\n\n### AdMob Test IDs for Local Validation\n\nThe Showcase app uses the following sample AdMob unit IDs for local validation. Use your own production IDs outside test and debug flows.\n\n| Placement | Sample AdMob unit ID |\n|-----------|----------------------|\n| Native | `ca-app-pub-3940256099942544/3986624511` |\n| Native video | `ca-app-pub-3940256099942544/2521693316` |\n| Fullscreen banner fallback | `ca-app-pub-3940256099942544/2435281174` |\n| Clips bottom banner | `ca-app-pub-3940256099942544/2934735716` |\n\n### AdMob vs GAM at a Glance\n\n| Concern | GAM | AdMob |\n|---------|-----|-------|\n| Module entry point | `StorytellerGAMModule` | `StorytellerAdMobModule` |\n| Native-specific options | Supports `customNativeTemplateIds` for custom native Ads | Uses standard native Ads and does not expose `customNativeTemplateIds` |\n| Fullscreen banner setup | No separate `bannerAdUnit` parameter | Uses optional `bannerAdUnit`; this can be banner fallback or banner-first when `enableBannerAdPriority` is `true` |\n| Shared options | Supports `bottomBannerAdUnit`, `publisherProvidedId`, `customKvps`, and `configureAdRequest` | Supports `bottomBannerAdUnit`, `customKvps`, and `enableBannerAdPriority` |\n\n## Mutual Exclusivity\n\n`StorytellerGAMModule` and `StorytellerAdMobModule` are mutually exclusive. Configure only one of them at a time.\n\n`StorytellerVASTModule` and `StorytellerGAMVASTModule` are separate fullscreen Ads modules. If you use either with any other Ads module, order the `Storyteller.shared.modules` array deliberately because the SDK asks modules for Ads in order and falls back to the next module when one throws.\n\n## Bottom Banner Ads\n\nThe Clips Player supports bottom banner Ads rendered as standard banner views added to the hierarchy below the video view. Bottom banner Ads are disabled when `StorytellerClipCollectionConfiguration.adConfiguration` is omitted or set to `nil`, and for new `StorytellerClipsAdConfiguration` instances. To opt a Clips presentation into bottom banner Ads, pass `StorytellerClipsAdConfiguration(bottomBannerEnabled: true)` through `StorytellerClipCollectionConfiguration.adConfiguration`.\n\nWhen using the GAM or AdMob module, supply `bottomBannerAdUnit` in the corresponding configuration to fetch bottom banner Ads. Opting in locally does not force Ads on: the tenant feed response must still enable the placement with `showBottomBannerAd == true`, and the active Ads module must support it.\n\nStoryteller First Party Ads, `StorytellerVASTModule`, and `StorytellerGAMVASTModule` do not support bottom banner Ads.\n\n## Clips Opening Pre-Roll Ads\n\nIf your tenant is configured for Clips Ads with `initialIndex = 0`, individual Clips presentations can opt into an opening fullscreen Ad before the first Clip is played by passing `StorytellerClipsAdConfiguration(preRollEnabled: true)` through `StorytellerClipCollectionConfiguration.adConfiguration`.\n\n<!-- storyteller-swift-example: id=ads-13 target=sdk-ios context=statements -->\n\n```swift\nlet configuration = StorytellerClipCollectionConfiguration(\n collectionId: \"top-plays\",\n adConfiguration: StorytellerClipsAdConfiguration(preRollEnabled: true)\n)\n```\n\nOpening pre-roll is disabled when `StorytellerClipCollectionConfiguration.adConfiguration` is omitted or set to `nil`, and for new `StorytellerClipsAdConfiguration` instances. Opting in locally does not force Ads on: Clips Ads must still be available for the tenant, and the remote Clips Ad strategy must use `initialIndex = 0`. The request uses the first opened content Clip as the current Clip context and sends `adIndex = 1`, which maps to the default `stAdIndex = 1` value for Google Ad Manager and AdMob integrations.\n\nWhen a CMS-configured opening pre-roll timeout is reached before the Ad loads, the Player starts the content Clip and ignores any late opening pre-roll result. Later between-Clip Ads continue to use the normal Clips ad cadence and increment from the next Ad index.\n\n## Ad Request Information\n\n**Note: This section is only relevant if you're implementing a custom Ads solution or building VAST request parameters. If you're using Storyteller First Party Ads, `StorytellerGAMModule`, or `StorytellerAdMobModule`, you don't need to work with this directly.**\n\nIf your `StorytellerModule` (or `StorytellerDelegate`) provides integrating-app ads, set `adSource` to declare the source used by your implementation.\n\n- For custom ad implementations, use `.custom(\"myNetwork\")`.\n- For Google modules, use `.gam` for GAM and `.admob` for AdMob.\n- The VAST and GAM VAST modules set `.custom(\"vast\")` automatically.\n- `.storyteller` is reserved for Storyteller First Party ads.\n\nSetting `adSource` to `.gam` or `.admob` enables Google paid ad analytics events. See [Ad Events](Analytics.md#ad-events).\n\n`StorytellerGAMModule`, `StorytellerAdMobModule`, `StorytellerVASTModule`, and `StorytellerGAMVASTModule` set `adSource` automatically.\n\nWhen implementing custom Ads, you'll receive context about the Ad request through the `StorytellerAdRequestInfo` enum. This provides information about what content the Ad will be displayed for.\n\n### StorytellerAdRequestInfo\n\nThe `StorytellerAdRequestInfo` enum has two cases:\n\n- **`stories(placement: String, categories: [String], story: ItemInfo, adIndex: Int)`**\n\n Used when an Ad is requested for display in a Stories Player. The parameters include:\n\n - `placement` - The placement identifier of the Story\n - `categories` - An array of categories associated with the List that the Story is part of\n - `story` - An `ItemInfo` struct containing more information about the specific Story\n - `adIndex` - The order of the ad within the current playback session (starts from 1)\n\n- **`clips(collection: String, clip: ItemInfo, nextClip: ItemInfo?, adIndex: Int)`**\n\n Used when an Ad is requested for display in a Clips Player. The parameters include:\n\n - `collection` - The identifier of the Clip collection\n - `clip` - An `ItemInfo` struct containing detailed information about the current Clip\n - `nextClip` - An optional `ItemInfo` struct containing more information about the Clip that is to appear after the requested Ad\n - `adIndex` - The order of the Ad within the displayed Ads in a Clip collection (1 for the first Ad, 2 for the second, etc.)\n\n#### ItemInfo\n\nEach case includes an `ItemInfo` struct that contains:\n\n- `categories` - An array of `StorytellerCategory` objects representing categories that the Story or Clip is part of.\n\n## Non Skippable Ads\n\nOur Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS.\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}