Skip to content

Multiformat banner ad unit in the rendering and mediation APIs - #1010

Open
mdanylov-sigma wants to merge 3 commits into
masterfrom
feature/999-multiformat-banner
Open

mdanylov-sigma wants to merge 3 commits into
masterfrom
feature/999-multiformat-banner

Conversation

@mdanylov-sigma

Copy link
Copy Markdown
Collaborator

Closes #1008
Related to #999
Android counterpart of prebid/prebid-mobile-ios#1341.

Problem

BannerView and MediationBannerAdUnit could only ever request a single format. Their constructors set AdFormat.BANNER, and setVideoPlacementType(...) replaced it with VAST, so display and outstream video demand could never compete on one impression — unlike InterstitialAdUnit and the original API.

What changed

Public API

setAdUnitFormats(EnumSet<AdUnitFormat>) / getAdUnitFormats() on BannerView and MediationBannerAdUnit. Default stays banner only; a null or empty set is ignored with a warning and the current value is kept.

BannerView bannerView = new BannerView(context, configId, new AdSize(300, 250));
bannerView.setAdUnitFormats(EnumSet.of(AdUnitFormat.BANNER, AdUnitFormat.VIDEO));
bannerView.setVideoPlacementType(VideoPlacementType.IN_BANNER);

setVideoPlacementType(...) no longer overrides formats that were configured explicitly, so the two can be called in either order. Its existing behaviour is unchanged when setAdUnitFormats(...) is not used.

Request and rendering

No renderer changes were needed. BasicParameterBuilder already emits imp.banner and imp.video independently, and PrebidDisplayView already picks the renderer from BidResponse.isVideo(). AdUnitConfiguration.setAdUnitFormats(...) gained an isInterstitial flag so a banner ad unit maps AdUnitFormat.BANNER to AdFormat.BANNER rather than AdFormat.INTERSTITIAL.

Auto refresh

Follows the reworked handling in the iOS PR: rather than cancelling auto refresh when a video bid wins, the refresh tick is skipped while the creative is playing and the timer is rescheduled, so refreshing continues once playback finishes. VideoView.isVideoPlaybackInProgress() reports this from the existing state machine — it stays true across pauses and across a "watch again" replay, matching the iOS window between videoAdDidStart and videoAdDidFinish. The gate sits after the existing "ad failed" escape hatch so a failed load can still retry.

Rendering no longer mutates the ad unit's configuration

This is the least obvious part of the change, and the multiformat feature does not work without it.

The creative pipeline rewrites the configuration it is handed. CreativeModelMakerBids.makeVideoModels() narrows adFormats down to VAST so CreativeFactory can route the creative, and VideoView records the built in video state and clears the refresh delay. DisplayView passed BannerView's own AdUnitConfiguration straight through, so rendering one video creative turned a multiformat ad unit into a video only one permanently.

That is what made auto refresh unrecoverable: once adFormats no longer holds BANNER, BidLoader.setupRefreshTimer() returns early and the timer can never be rescheduled, so deferring a refresh during playback would have stopped it for good rather than resuming it. The next auction would also have silently dropped the banner format.

DisplayView now renders against its own copy of the configuration. The copy carries fingerprint over, because plugin event listeners are registered under it, and broadcastId, because creatives address the event receiver by it. The mutable sub configurations (RewardManager, banner, video and native parameters) are shared by reference on purpose, since they are read back through the ad unit after rendering. A tripwire test fails if a field is added to AdUnitConfiguration without being added to the copy.

The alternative considered was routing CreativeFactory on creativeModel instanceof VideoCreativeModel and leaving adFormats alone. That would additionally have made AdViewManager.handleCreativeDisplay() deduplicate repeat displays of a banner video and silently break "watch again", because lastCreativeShown is never reset.

Notes for reviewers

  • MediationBannerAdUnit deliberately does not stop refreshing when a video bid is returned. Unlike BannerView, it never learns whether the Prebid bid actually won inside the mediation SDK, so cancelling would be wrong when the mediation SDK serves its own ad. This matches iOS. stopRefresh() / resumeRefresh() are documented on the new setter.
  • Interstitials narrow their configuration the same way, but they do not auto refresh, so the impact is limited to a repeated loadAd(). Left out of this PR.
  • BasicParameterBuilder.setVideoImpValues() defaults video.placement to 5 (interstitial) for any rendering API video imp with no placement type, including a banner one. Three existing tests assert this, so it looks intentional and is left alone. Publishers should keep calling setVideoPlacementType(...), as the examples here do.

Examples

  • PrebidDemoKotlin: InAppMultiformatBannerActivity, registered as an AdFormat.MULTIFORMAT test case.
  • PrebidInternalTestApp: "Multiformat Banner 300x250 (In-App)", picking a display or video config id at random so either creative can win, mirroring the iOS test case.

Testing

1327 unit tests pass. MraidInternalBrowserActionTest > handleInternalBrowserActionFollowUrlSuccessAndIsMraid_StartActionViewActivity fails, and also fails on a clean master, so it is unrelated.

New coverage: format mapping both ways, multiformat request emitting imp.banner + imp.video with instl=0, call order independence between placement and formats, the refresh gate during and after playback, isBuiltInVideo no longer latching, and the configuration copy including the field tripwire.

🤖 Generated with Claude Code

mdanylov-sigma and others added 3 commits September 9, 2026 09:33
BannerView and MediationBannerAdUnit could only request a single format:
their constructors set AdFormat.BANNER, and setVideoPlacementType() replaced
it with VAST, so display and outstream video demand could never compete on
one impression. Interstitials and the original API already support this.

Add setAdUnitFormats(EnumSet<AdUnitFormat>) / getAdUnitFormats() to both ad
units. The request side already emits both imp.banner and imp.video when
several formats are configured, and PrebidDisplayView already picks the
renderer from the winning bid, so no renderer changes are needed.

Alongside that:

- AdUnitConfiguration.setAdUnitFormats() takes an isInterstitial flag so
  banner ad units map AdUnitFormat.BANNER to AdFormat.BANNER rather than
  AdFormat.INTERSTITIAL, and a null or empty set is now ignored with a
  warning instead of throwing.
- setVideoPlacementType() no longer overrides formats that were configured
  explicitly, so placement and formats can be set in either order.
- BannerView cancels auto refresh when a video bid wins. The refresh timer
  is armed as soon as the bid response arrives, before the primary ad server
  answers, so a video creative would otherwise be torn down mid playback.
- PrebidDisplayView derives isBuiltInVideo from the current winning bid
  instead of latching it to true, since a multiformat banner shares one
  configuration across refreshes.

Aligns with prebid/prebid-mobile-ios#1341.

Related to #999

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the reworked auto refresh handling in prebid/prebid-mobile-ios#1341:
instead of cancelling auto refresh for good when a video bid wins, the refresh
tick is skipped while the creative is actually playing and the timer is
rescheduled, so refreshing continues once playback finishes.

VideoView reports isVideoPlaybackInProgress() from its state machine, which
stays true across pauses and across a "watch again" replay, matching the
iOS window between videoAdDidStart and videoAdDidFinish. PrebidDisplayView
and DisplayView surface it, and BannerView consults it in the bid refresh
gate, after the existing "ad failed" escape hatch so a failed load can still
retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The creative pipeline rewrites the configuration it is handed:
CreativeModelMakerBids.makeVideoModels() narrows adFormats down to VAST so
CreativeFactory can route the creative, and VideoView records the built in
video state and clears the refresh delay. DisplayView passed BannerView's own
AdUnitConfiguration straight through, so rendering a video creative turned a
multiformat ad unit into a video only one for good.

That is what made auto refresh unrecoverable: once adFormats no longer holds
BANNER, BidLoader.setupRefreshTimer() returns early and the timer can never be
rescheduled, so deferring a refresh during playback would have stopped it
permanently rather than resuming it. The next auction would also have dropped
the banner format.

DisplayView now renders against its own copy. The alternative, routing
CreativeFactory on the creative model type and leaving adFormats alone, would
additionally have made AdViewManager.handleCreativeDisplay() deduplicate
repeat displays of a banner video and silently break "watch again", since
lastCreativeShown is never reset.

The copy carries fingerprint over, because plugin event listeners are
registered under it, and broadcast id, because creatives address the event
receiver by it. The mutable sub configurations are shared on purpose: reward,
banner, video and native settings are read back through the ad unit after
rendering. A tripwire test fails if a field is added to AdUnitConfiguration
without being added to the copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiformat Banner Ad Unit in the Rendering and Mediation APIs

1 participant