> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kontext.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Guides

> Recipes beyond the first ad: loading several ads at once, your own auction, no-fill, the player, the next episode, the viewer, Compose, Java.

## Loading several ads at once

To have several formats ready at once, ask for them and load them concurrently. Each format is requested and rendered in parallel, and one format without an ad leaves the others untouched:

```kotlin theme={null}
suspend fun prepare(format: AdFormat): VideoAd? = try {
    ads.format(format).also {
        it.load()
        it.listener = this
    }
} catch (e: VideoAdException) {
    null                                   // no fill for this format this time
}

lifecycleScope.launch {
    val overlay = async { prepare(AdFormat.OVERLAY) }
    val pause = async { prepare(AdFormat.PAUSE) }
    val linear = async { prepare(AdFormat.LINEAR) }
    overlayAd = overlay.await()
    pauseAd = pause.await()
    linearAd = linear.await()
}
```

## Running your own auction

`VideoAd` carries the price before anything is rendered, so you can compare it with other networks and decide:

```kotlin theme={null}
val ad = ads.format(AdFormat.PAUSE)
if (ad.price.value >= otherNetworkCpm) {
    ad.load()
    pauseAd = ad
} else {
    ad.reject(VideoAd.RejectReason.Outbid(winningPrice = otherNetworkCpm, winner = "Other network"))
}
```

`price.value` is a CPM in `price.currency`. `price.precision` says whether it is a DSP's clearing price (`EXACT`) or a forecast for house demand (`ESTIMATED`).

`reject` tells the server at once, and the server tells the advertiser its win produced no impression. Use `Outbid` when another network won on price, with the winning price and the network, so we can optimize our bids for your traffic; `NotUsed` (the default) for any other reason: the moment passed, the viewer left. Rejecting is optional; an ad you never use is settled when it expires or when you call `destroy()`.

## Handling no-fill

`format()` throws `VideoAdException.NoFill` when the server has nothing for the format. It is not a failure and the SDK never retries by itself; ask again at a later moment.

```kotlin theme={null}
try {
    pauseAd = ads.format(AdFormat.PAUSE)
} catch (e: VideoAdException.NoFill) {
    when (e.reason) {
        NoFillReason.NoAdMatch -> {}           // no campaign fits this episode or viewer right now
        NoFillReason.GeoRestricted -> {}       // no ads served in the viewer's country
        else -> {}                             // FrequencyCap, ContentUnsafe, TemporaryError, …
    }
}
```

All of them mean the same for your code: no ad this time, ask again at the next moment. Log the reason if you want to see why fill is low.

## Giving the SDK your player

Every request and event carries where the viewer is in the episode, and an overlay follows the playback: it shows over playing content only and goes away when the viewer pauses. Give the SDK your player once the episode starts playing.

With a Media3 player (`ExoPlayer`, `MediaController`), add `so.kontext:video-media3` and hand it over; the SDK reads its time and listens to its playback. The player must run on the main looper:

```kotlin theme={null}
import so.kontext.video.media3.track

ads.track(exoPlayer)
```

With any other player, implement `VideoAdsPlayer`: its position in milliseconds, and whether it plays. Buffering and seeking count as playing.

```kotlin theme={null}
class MyPlayerAdapter(player: MyPlayer) : VideoAdsPlayer {
    private val player = WeakReference(player)
    override val currentPositionMillis: Long get() = player.get()?.positionMillis ?: 0
    override val isPlaying: Boolean get() = player.get()?.let { it.state == State.PLAYING || it.state == State.BUFFERING } ?: false
}

ads.track(MyPlayerAdapter(myPlayer))
```

If your player has its own listeners, implement `addListener` and `removeListener` and call the `VideoAdsPlayer.Listener` on each change (`onIsPlayingChanged`, `onSeek`, `onContentChanged`): an overlay then ends the moment the viewer pauses. Otherwise the SDK checks `isPlaying` four times a second while an overlay is on screen. The SDK holds the adapter until `destroy()` or the next `track`, so let the adapter hold your player weakly.

Call `track` when this episode starts playing, not when you create the `VideoAds`: until then the player's time belongs to the previous episode. Before that the SDK sends 0, which is right for a prefetch.

## Keeping the ad clear of your controls

An overlay strip sits at the bottom or top of the container, a pause card at the top; the SDK keeps both clear of the system bars and display cutout that overlap the container. If your player draws its own controls there (a title, a progress bar, buttons), pass the space they take as `insets`:

```kotlin theme={null}
// Your control bar is controlBar.height pixels tall, at the bottom of the player.
overlayAd?.show(playerContainer, Insets.of(0, 0, 0, controlBar.height))
```

* `Insets` is `androidx.core.graphics.Insets`, in pixels. They add to the system bars and cutout: measure from their edge, not from the screen's.
* They stay while the ad is shown, through resizes and rotations. If your controls change size while an ad is on screen, the ad keeps the insets it was shown with.
* The ad still has to fit at half its size or more. Insets count against the space, so `ContainerTooSmall` includes them.
* A full-screen pause ad ignores them: it fills the container, and its own controls keep clear of the edges.

## The next episode

Create the next episode's `VideoAds` while the current one plays, prefetch its ads, and destroy the current one when the viewer leaves. Any number can be alive at once; the SDK keeps at most one ad on screen in the whole app.

```kotlin theme={null}
val next = VideoAds(nextEpisode)
lifecycleScope.launch {
    nextLinear = try { next.format(AdFormat.LINEAR).apply { load() } } catch (e: VideoAdException) { null }
}

// When the viewer moves on:
ads.destroy()
ads = next
ads.track(player)     // the player now plays the next episode
```

Call `track` when the episode actually starts in the player, not at creation: before that the player's time belongs to the previous episode.

## Who the viewer is

Set what you know on `KontextVideo`; the next request carries it:

```kotlin theme={null}
KontextVideo.user = User { id = "user-1234"; keywords = listOf("romance", "drama") }
KontextVideo.variantId = "B"     // for A/B tests agreed with Kontext
```

> [!NOTE]
>
> Consent is read for you: on every request the SDK reads the IAB keys your consent platform writes to the default `SharedPreferences` (TCF, GPP, US Privacy). Only when the SDK cannot detect a signal, because your app has no such platform or keeps a signal elsewhere, set it yourself. A value your consent platform wrote still wins.
>
> ```kotlin theme={null}
> KontextVideo.regulatory = Regulatory {
>     gdpr = true
>     gdprConsent = "<TCF string>"
>     usPrivacy = "1YNN"
>     coppa = false
> }
> ```
>
> See [TCF & CMP](https://docs.kontext.so/video/guides/tcf-cmp.md).

## Following the screen's lifecycle

A `VideoAds` belongs to the screen that shows the episode: create it when the screen opens, `destroy()` it when the screen goes.

* In an Activity, call `destroy()` in `onDestroy()`; in a Fragment, in `onDestroyView()`.
* A linear ad runs in the SDK's own Activity, over yours: your Activity is stopped, not destroyed, and the ad's end reaches your listener when it returns.
* An Activity recreated on rotation runs `onDestroy()` too, and with it `destroy()`. The [example app](https://docs.kontext.so/video/sdk/kotlin/example.md) keeps its player Activity across rotation with `android:configChanges`.
* The ad holds its listener until `onAdEnded` has run, so a listener that is your Activity stays reachable until the ad ends.

## Using it from Jetpack Compose

The SDK draws overlay and pause ads with Android Views, which Compose hosts with `AndroidView`. A linear ad needs nothing extra: `show(activity)` starts the SDK's Activity over whatever Compose is showing. An overlay or pause ad goes into a `ViewGroup`; give it a `FrameLayout` laid over the player:

```kotlin theme={null}
@Composable
fun PlayerScreen(content: Content, player: ExoPlayer) {
    val context = LocalContext.current
    val adArea = remember { FrameLayout(context) }          // created once, kept for the screen
    val ads = remember(content.id) { VideoAds(content) }
    var pauseAd by remember { mutableStateOf<VideoAd?>(null) }

    DisposableEffect(ads) {
        ads.track(player)
        onDispose { ads.destroy() }
    }
    LaunchedEffect(ads) {
        pauseAd = try { ads.format(AdFormat.PAUSE).apply { load() } } catch (e: VideoAdException) { null }
    }

    Box(Modifier.fillMaxSize()) {
        AndroidView(factory = { PlayerView(it).apply { this.player = player } }, modifier = Modifier.fillMaxSize())
        AndroidView(factory = { adArea }, modifier = Modifier.fillMaxSize())
    }
}

// At the moment:
try { pauseAd?.show(adArea) } catch (e: VideoAdException) { }
```

* Create the `FrameLayout` once and keep it with `remember`: a container that leaves the window takes the ad with it (`INTERRUPTED`).
* Key the `VideoAds` on the episode, and destroy it in the `DisposableEffect`'s `onDispose`, so it goes when the screen or the episode goes.
* For a linear ad, pass the `Activity` that hosts your composition to `show(activity)`.

## From Java

Every type has a builder, and every `suspend` call has a callback variant that delivers on the main thread:

```java theme={null}
KontextVideo.initialize(this, new Configuration.Builder("<your-publisher-token>").build());

VideoAds ads = new VideoAds(new Content.Builder("episode-1234", "en").title("Episode 1").build());
Media3.track(ads, exoPlayer);                       // so.kontext.video.media3.Media3

ads.format(AdFormat.PAUSE, new VideoAdsCallback<VideoAd>() {
    @Override public void onSuccess(VideoAd offer) {
        offer.load(new VideoAdsCallback<VideoAd>() {
            @Override public void onSuccess(VideoAd loaded) {
                loaded.setListener(new VideoAdListener() {     // implement only what you need
                    @Override public void onAdEnded(VideoAd ad, VideoAd.EndReason reason) {
                        if (reason == VideoAd.EndReason.RESUMED) player.play();
                    }
                });
                pauseAd = loaded;
            }
            @Override public void onError(VideoAdException error) { }
        });
    }
    @Override public void onError(VideoAdException error) { }   // NoFill, …
});

// The viewer pauses:
try {
    pauseAd.show(playerContainer);
} catch (VideoAdException e) {
    // not shown
}
```

`show`, `reject` and `dismiss` declare `VideoAdException`, so Java requires the `try`. Sealed values are singletons: `NoFillReason.NoAdMatch.INSTANCE`, `VideoAd.RejectReason.NotUsed.INSTANCE`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.