Repository navigation
Conversation
Ibad9
force-pushed
the
ima-ads-integration
branch
from
October 2, 2026 20:22
cab1083 to
09efa6a
Compare
Contributor
|
thanks @Ibad9 do you have a version with more commits than 1? it's a huge PR and just one commit |
Contributor
Ibad9
marked this pull request as draft
October 6, 2026 14:37
Author
|
Hi @Okelm currently facing some issues on IOS regarding fullscreen. I have moved this PR to draft for now. I will move the status once tested properly. Thanks in advance. |
Ibad9
force-pushed
the
ima-ads-integration
branch
3 times, most recently
from
October 7, 2026 09:54
d0814cc to
0f2acec
Compare
Adds production client-side pre/mid/post-roll video ad support via
Google's IMA SDK on both platforms, structurally enforced: no content
frame renders before the ad decision resolves, not via a JS-side
check or timer.
- New `VideoAdsConfig` (VideoConfig.ads): adTagUrl, autoActivate,
language, ppid, vastLoadTimeoutMs, mediaLoadTimeoutMs,
adRequestTimeoutMs (a fail-open watchdog independent of the SDK's
own timeouts).
- VideoPlayerBase gains `isPlayingAd`, `adState`
(idle/activating/requesting/playing/content/failed), and
`activateAds()`/`deactivateAds()`/`skipAd()`. Activation is
explicit and separate from construction/play() by design: a
scrollable feed can mount several players at once (preloaded
neighbors) with only the visible one requesting an ad -
`autoActivate: true` is available for the simpler single-player
case.
- 11 new events: onAdsResolved, onAdBreakStart/End, onAdProgress,
onAdStart/Complete/Skipped, onAdClicked, onAdError,
onAllAdsCompleted, onAdStateChange.
New `ios/core/Ads/{AdPlaybackGate,VideoAdController,
GoogleIMAAdController}.swift`. Renders ads through IMA's own internal
player (not by handing the content AVPlayer to IMA) - documented in
GoogleIMAAdController.swift's header, chosen specifically to keep
VideoPlayerObserver's KVO-driven content events (onLoad/onEnd/
onProgress) from misfiring during ad playback, and because the
pre-roll case has `player.currentItem == nil` by construction (the
gate is withholding it) so there is no question of IMA displacing a
nil item. The gate itself lives in `HybridVideoPlayer.commitPlayerItem`,
the single call site that attaches a content item to the player -
withholding it there is what makes "no content frame before the ad
decision" a structural guarantee rather than a convention. PiP is
suppressed for the duration of an ad break (`allowsPictureInPicturePlayback`
toggle) rather than building the full `IMAPictureInPictureProxy`
integration, since this library exposes no app-owned
`AVPictureInPictureController` to hand to it - PiP here is 100%
AVKit-internal.
Two real bugs found and fixed by running an actual vertical feed on
device, not by inspection: a pre-roll played on the first episode of
a feed and never again (deactivate() was permanently disabling the
gate instead of just tearing down the session, so every preloaded-
then-deactivated neighbor cell could never activate again once it
became active); and taps did nothing while an ad was on screen
(onPlaybackStateChange derived isPlaying from the paused content
player instead of the ad's own state, so a play/pause toggle always
picked the wrong branch).
New `android/src/main/java/com/twg/video/core/ads/` (compiled always,
IMA-free interface) plus a separate opt-in `ima` Gradle source set
(`RNVideo_useExoplayerIma` flag) holding the real
`androidx.media3:media3-exoplayer-ima` integration - opt-in because it
pulls `play-services-ads-identifier`, which contributes the AD_ID
permission to the merged manifest, a Data Safety form change that
should not be forced on every consumer of the base package. Enabling
the flag also requires the consuming app to turn on core library
desugaring (`coreLibraryDesugaringEnabled true` +
`desugar_jdk_libs:2.1.5`), which the IMA SDK's own AAR metadata
declares - see example/android/app/build.gradle for the exact setup.
The gate is Media3's own: `AdsMediaSource` withholds its `Timeline`
until the ad decision lands, so ExoPlayer can never create a content
`MediaPeriod` regardless of `playWhenReady`; `HybridVideoPlayer.kt`'s
job is deferring `player.prepare()` itself until `activateAds()` is
called, so the up-to-several preloading neighbors in a feed stay
completely inert (no ImaAdsLoader, no ad request, no decoded frame).
PiP is gated on both entry paths - explicit request (defer-and-retry
once) and the API-31 `autoEnterPictureInPicture` system-driven path,
which bypasses the explicit path entirely.
Two real bugs found the same way: the ad decision was resolving off
ExoPlayer's synchronous placeholder timeline before IMA had actually
answered (opening the gate on nothing); and this media3+IMA version
never dispatches AD_BREAK_STARTED/AD_BREAK_ENDED for
client-side-ad-insertion, only CONTENT_PAUSE_REQUESTED/
CONTENT_RESUME_REQUESTED - both pairs are now handled and deduplicated
so the JS-facing events stay correct regardless of which the SDK
version in use actually fires.
New `example/src/components/AdsManager.tsx`, following the existing
TextTrackManager/VideoQualityManager panel convention: activate/
deactivate/skip buttons, live ad state, and an ad-event log. Wired to
a real public Google sample VAST tag via `videoSource.ts`.
New `docs/docs/player/ads.md`: config reference, activation model
(single-player `autoActivate` vs. feed-driven `activateAds()`/
`deactivateAds()`), ad state, all 11 events with payload shapes,
platform setup for both flags, and a troubleshooting section leading
with the costliest gotcha - forgetting the platform flag produces no
error at all, every ad API just stays present and inert.
Both platforms real-device/simulator verified against Google's public
VAST/VMAP sample tags (not just written and assumed) - confirmed zero
content frames render during pre-roll, full pre/mid/post-roll cycles,
correct fail-open on ad-request failure and on no-fill, and zero
regression against existing playback with no `ads` config present.
Cold builds green on both platforms as of this commit: Android
`assembleDebug` with `useExoplayerIma` both true and false; iOS `pod
install` (IMA linked via `$RNVideoUseGoogleIMA`) + a full `xcodebuild
clean build`. `tsc`/lint clean across the monorepo.
iOS fullscreen: entering or leaving fullscreen while an ad plays no
longer crashes (IMA's child view controller is detached before AVKit
moves the ad container and re-parented afterwards), the ad container is
attached when the ad controller is created after the view, and
fullscreen now follows the phone's rotation (FullscreenOrientationHandler).
iOS: an ad source swapped in while the video view is already on screen no longer skips
the pre-roll (the ad request waits until the ad container has its view controller).
JS: `onAdEvent` is one listener for every ad event, delivered as `{ type, data }`; the
individual onAd* events stay available. The example Ads panel uses it.
`adPodIndex` now means the same on both platforms: the zero-based position of the ad within
its pod (`adPodIndex + 1` of `totalAdsInPod`). iOS sent IMA's one-based `adPosition` and
Android sent the pod's index in the content.
Docs: integration checklist, Google sample tags for testing, and a limitations section.
Agent skill: new `references/v7/ads.md`, and the routing / v6-vs-v7 / events / migration
notes no longer say v7 has no ads.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ibad9
force-pushed
the
ima-ads-integration
branch
from
October 7, 2026 10:52
0f2acec to
9a47a7c
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds client-side video ads (pre-roll, mid-roll, post-roll, VMAP, ad pods) on iOS and Android through Google's IMA SDK. Ads are opt-in per source with
ads: { adTagUrl }and controlled withactivateAds(),deactivateAds(),skipAd(),isPlayingAdandadState. Ad events can be followed with one listener,onAdEvent, which delivers{ type, data }.Motivation
The library had no way to play ads. IMA is the standard ad SDK on both platforms. Playback is gated natively, so no content frame renders before the ad decision is made, and every failure path falls back to content.
Changes
VideoAdsConfig(VideoConfig.ads),adState,isPlayingAd,activateAds/deactivateAds/skipAd, and 11 events (onAdsResolved,onAdBreakStart/End,onAdProgress,onAdStart/Complete/Skipped/Clicked,onAdError,onAllAdsCompleted,onAdStateChange).onAdEventdelivers all of them through one listener.ios/core/Ads/*(IMA through$RNVideoUseGoogleIMA). The ad UI lives in AVKit'scontentOverlayView. Entering or leaving fullscreen while an ad plays does not crash, an ad source swapped in while the view is on screen no longer skips the pre-roll, and fullscreen follows the phone's rotation.src/ima/*(IMA throughRNVideo_useExoplayerIma).adPodIndexmeans the same on both platforms: the zero-based position of the ad within its pod.onAdEventlistener.docs/docs/player/ads.md(setup, integration checklist, sample tags, config, activation, state, events, limitations, troubleshooting). Sidebar positions of Downloading and Analytics shift by one.__tests__/adEvents.test.ts.Platforms affected
Type of change
Test plan
Automated
adEvents.test.ts(unit, 13 tests): each of the 11 ad events is wired to its native listener, forwards its payload and unsubscribes;onAdEventsubscribes to all of them, reports{ type, data }for each, and removes every native listener when unsubscribed. Mutation-tested: breaking one mapping fails a test.bun run test(224 pass),bun lintandbun typecheckpass (run by the pre-commit hook). Lint reports 8 existing warnings inVideoView.web.tsx.httptags.Manual, with the example app's Ads panel and Google's sample tags (release builds of this branch)
(1/1), ad break end, all ads completedonAdErrorfires and content playsskipAd()Media: screen recordings and screenshots of the cases above on both platforms.



Uploading android-fullscreen-during-ad.
Uploading android-preroll-and-events.mp4…
mp4…
ios-preroll-and-events.mp4
ios-fullscreen-during-ad.mp4
Known limits (also in the docs)
VideoViewusessurfaceType="texture"; the default surface works. Not fixed yet.Info.plistallows landscape;Checklist
bun run test,bun lintandbun typecheckpass locally.skills/react-native-video/) to match. (Not done yet: the skill has no ads page.)