Metadata-Version: 2.4
Name: flet-ima
Version: 0.1.5
Summary: Flet extension for video ads (Google IMA client-side) - a thin 1:1 wrapper over the Flutter interactive_media_ads package. The ad SDK Android TV uses.
Author: Nwokike
License-Expression: MIT
Project-URL: Homepage, https://github.com/Nwokike/flet-ima
Project-URL: Repository, https://github.com/Nwokike/flet-ima
Project-URL: Issues, https://github.com/Nwokike/flet-ima/issues
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: flet>=1.0.1
Dynamic: license-file

# flet-ima

[![pypi](https://img.shields.io/pypi/v/flet-ima.svg)](https://pypi.python.org/pypi/flet-ima)
[![python](https://img.shields.io/badge/python-%3E%3D3.10-%2334D058)](https://pypi.org/project/flet-ima)
[![license](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/Nwokike/flet-ima/blob/main/LICENSE)

A [Flet](https://flet.dev) extension for in-stream video ads — a thin 1:1
wrapper over the Flutter
[`interactive_media_ads`](https://pub.dev/packages/interactive_media_ads)
package (Google IMA, client-side).

> **This is the ad SDK Android TV / Google TV uses.** `flet-ads` wraps AdMob's
> Google Mobile Ads SDK, which [does not support Android
> TV](https://developers.google.com/admob/android/sdk) — for TV (and for
> in-stream video ads anywhere), the correct SDK is IMA, wrapped here.
> The account side is **Google Ad Manager** with a **VAST ad tag**, not an
> AdMob unit ID.

## Platform Support

| Platform | Windows | macOS | Linux | iOS | Android | Web |
|----------|---------|-------|-------|-----|---------|-----|
| Supported|    ❌   |  ❌   |   ❌  |  ✅ |    ✅   |  ❌ |

(Upstream `interactive_media_ads` supports Android and iOS only; on other
platforms the control renders an inline error instead of crashing.)

## Installation

```bash
uv add flet-ima      # or: pip install flet-ima
```

## Usage

```python
import flet as ft
from flet_ima import AdEventType, ImaAdsView

def main(page: ft.Page):
    ads = ImaAdsView(
        ad_tag_url="https://...your ad manager VAST tag...",
        content_title="My video",
        content_duration_ms=120_000,
        on_ad_event=handle_ad_event,
        on_ad_error=lambda e: print("ad error:", e.code, e.message),
        on_ads_loaded=lambda e: print("cue points:", e.cue_points_ms),
    )

    async def handle_ad_event(e):
        if e.type == AdEventType.CONTENT_PAUSE_REQUESTED.value:
            await my_player.pause()          # your content player
        elif e.type == AdEventType.CONTENT_RESUME_REQUESTED.value:
            await my_player.resume()
        elif e.type == AdEventType.LOADED.value:
            await ads.start()

    async def start():
        await ads.request_ads()              # deferred until view is attached

    page.add(ft.Container(height=300, bgcolor="black", content=ads))
    page.run_task(start)
    # while content plays: await ads.set_content_progress(pos_ms, dur_ms) every ~200ms
    # at content end:     await ads.content_complete()

ft.run(main)
```

### Props

`ad_tag_url`, `ads_response` (canned VAST instead of a URL), `content_title`,
`content_duration_ms`, `content_keywords`, `ad_will_autoplay`,
`ad_will_play_muted`, `continuous_playback`, `vast_load_timeout_ms`,
`language`, `enable_preloading`, `bitrate`, `mime_types`,
`load_video_timeout_ms` — each maps 1:1 to `AdsRequest` / `ImaSettings` /
`AdsRenderingSettings`.

### Methods

| Method | Description |
|--------|-------------|
| `request_ads()` | Request ads from the server (queued until the native view attaches). |
| `start()` / `pause()` / `resume()` / `skip()` | `AdsManager` playback control. |
| `set_content_progress(pos_ms, dur_ms)` | Push content position (~every 200ms) so mid-roll cue points fire. |
| `content_complete()` | Content finished — triggers post-rolls. |
| `discard_ad_break()` / `destroy()` | Skip the current break / release everything. |
| `get_ad_cue_points()` | Scheduled break offsets in ms (after `on_ads_loaded`). |

### Events

| Event | Payload |
|-------|---------|
| `on_ad_event` | `e.type` (an `AdEventType` value as string), `e.ad` (`AdInfo`: id, title, duration, skip offset, pod position/total/index), `e.ad_data` |
| `on_ad_error` / `on_ads_load_error` | `e.code`, `e.type`, `e.message` |
| `on_ads_loaded` | `e.cue_points_ms` — delegate attached + `AdsManager.init()` already done by the wrapper |

The wrapper does exactly two glue steps Google's own example does (attach
delegate + `init()` on ads-loaded); everything else is yours to drive — which
is also what keeps D-pad / TV focus handling in your app where it belongs.

**Android TV focus note:** IMA renders and focuses its own native **Skip**
button for the remote — let it. Don't move focus away while an ad is
playing, or the D-pad can no longer reach Skip; `skip()` is for
programmatic skips only (e.g. a settings-menu "skip ad" action).

## Android host requirements (flet 1.0.1 — exact, CI-tested steps)

Three things IMA needs that Flet's Android template doesn't provide by
itself. **These instructions are executed verbatim by this repo's
`example-build` CI job against the real flet template** (tag builds +
manual dispatch).

### 1. Permissions — first-class pyproject knob (no sed)

`INTERNET` is a Flet default (verify it in the merged manifest);
**`ACCESS_NETWORK_STATE` is not**, and IMA's README requires it:

```toml
# pyproject.toml
[tool.flet.android.permission]
"android.permission.ACCESS_NETWORK_STATE" = true
```

### 2. Core-library desugaring — guarded, idempotent patch

IMA requires desugaring; Flet's template does not enable it. Patch
`build/flutter/android/app/build.gradle.kts` **after** `flet build android`
generates the project (the tree is written before gradle runs and persists
even if the first gradle pass fails), then run gradle yourself — same
two-phase pattern as patching the manifest:

```bash
GRADLE=build/flutter/android/app/build.gradle.kts
grep -q "isCoreLibraryDesugaringEnabled" "$GRADLE" || \
  sed -i 's/compileOptions {/compileOptions {\n        isCoreLibraryDesugaringEnabled = true/' "$GRADLE"
grep -q "coreLibraryDesugaring" "$GRADLE" || \
  sed -i 's/dependencies {}/dependencies {\n    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")\n}/' "$GRADLE"
# flet exports this only for gradle runs it launches itself — a manual
# gradlew needs it or serious_python aborts with
# "SERIOUS_PYTHON_SITE_PACKAGES environment variable is not set":
export SERIOUS_PYTHON_SITE_PACKAGES="$PWD/build/site-packages"
# Resume the SAME variant flet's failed pass was building (assembleRelease
# after `flet build apk`) — reusing its caches keeps the build fast and
# avoids doubling disk usage. Use assembleDebug only on a fresh checkout.
(cd build/flutter/android && ./gradlew assembleRelease --no-daemon)
```

(The `grep ||` guards make re-runs safe — plain `sed` would double-insert.
The second pattern replaces `dependencies {}`, so its replacement must
re-add the closing `}` — omitting it breaks the Kotlin script. You'll know
you need step 2 because the first build fails with:
`'com.google.ads.interactivemedia.v3:interactivemedia:...' requires core
library desugaring to be enabled for :app`.)

### 3. minSdk ≥ 24 — read the printed value, patch only if needed

Flet 1.0.1 has **no minSdk option** (no pyproject key, no CLI flag) — the
template falls back to `flutter.minSdkVersion`, and the build prints
`  minSdkVersion: ...` in its log. Capture that value; if it is below 24:

```bash
GRADLE=build/flutter/android/app/build.gradle.kts
MINSDK=$(grep -oE 'minSdkVersion: [0-9]+' build.log | head -1 | grep -oE '[0-9]+' || echo 0)
if [ "$MINSDK" -lt 24 ]; then
  sed -i 's/val resolvedMinSdk = flutter.minSdkVersion/val resolvedMinSdk = 24/' "$GRADLE"
fi
```
(Run `flet build ... 2>&1 | tee build.log` to capture the printed value.)

(Alternative: fork the template with `flet build --template-dir` and set the
`min_sdk_version` cookiecutter variable there.)

Good news for TV: Flet's template already ships the `LEANBACK_LAUNCHER`
category, so your app already appears on Google TV's home.

## Deferred to v1.x (deliberately not wrapped yet)

- `ImaSettings` setters: `setDebugMode`, `setPpid`, `setSessionID`,
  `setMaxRedirects`, `setPlayerType/Version`, `setFeatureFlags`,
  `setAutoPlayAdBreaks` (only `language` is wired today).
- `AdsRenderingSettings.playAdsAfterTime` / `uiElements`.
- `CompanionAdSlot` (fixed-pixel companions are a poor fit for 10-foot UIs
  anyway) and `liveStreamPrefetchMaxWaitTime`.
- DAI (server-side ad insertion) and PAL — separate packages/future work;
  PAL additionally needs its own native dependency.
- Extra `Ad` getters not passed through (`adSystem`, `advertiserName`,
  `creativeId`, `dealId`, companion ads, universal ad ids) — `AdInfo` covers
  id/title/duration/skip/pod metadata.
- **One `ImaAdsView` per screen**: upstream recommends a single `AdsLoader`
  per page; this wrapper binds one loader to one view's native container, so
  keep a single view alive at a time (destroy before pushing another).

## Ad tag notes

- Use a **Google Ad Manager VAST/VMAP tag** for your CTV/Video app — not an
  AdMob unit ID (`flet-ads` covers those, mobile only).
- Google's public test tags (used in the example): the official
  **VMAP pre+mid+post** sample
  (`...iu=/21775744923/external/vmap_ad_samples...cmsid=496&vid=short_onecue`)
  so cue points, mid-roll progress and post-rolls all demo; a single
  skippable pre-roll is kept as a commented alternative in the example.

## Upstream bumps

No logic of its own — every prop/method/event forwards 1:1 to
`interactive_media_ads`. To pick up an upstream release: bump the pin in
`src/flutter/flet_ima/pubspec.yaml`, push a tag, done.

## Development

```bash
uv sync
uv run flet run main.py   # runs the example app (root forwarder)
```

CI (GitHub Actions) runs `dart analyze` against the pinned Flutter package and
publishes the wheel to PyPI on `v*` tags — no local builds needed.

## License

MIT
