Multiformat banner ad unit in the rendering and mediation APIs - #1010
Open
mdanylov-sigma wants to merge 3 commits into
Open
mdanylov-sigma wants to merge 3 commits into
mdanylov-sigma wants to merge 3 commits into
Conversation
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>
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.
Closes #1008
Related to #999
Android counterpart of prebid/prebid-mobile-ios#1341.
Problem
BannerViewandMediationBannerAdUnitcould only ever request a single format. Their constructors setAdFormat.BANNER, andsetVideoPlacementType(...)replaced it withVAST, so display and outstream video demand could never compete on one impression — unlikeInterstitialAdUnitand the original API.What changed
Public API
setAdUnitFormats(EnumSet<AdUnitFormat>)/getAdUnitFormats()onBannerViewandMediationBannerAdUnit. Default stays banner only; a null or empty set is ignored with a warning and the current value is kept.setVideoPlacementType(...)no longer overrides formats that were configured explicitly, so the two can be called in either order. Its existing behaviour is unchanged whensetAdUnitFormats(...)is not used.Request and rendering
No renderer changes were needed.
BasicParameterBuilderalready emitsimp.bannerandimp.videoindependently, andPrebidDisplayViewalready picks the renderer fromBidResponse.isVideo().AdUnitConfiguration.setAdUnitFormats(...)gained anisInterstitialflag so a banner ad unit mapsAdUnitFormat.BANNERtoAdFormat.BANNERrather thanAdFormat.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 betweenvideoAdDidStartandvideoAdDidFinish. 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()narrowsadFormatsdown toVASTsoCreativeFactorycan route the creative, andVideoViewrecords the built in video state and clears the refresh delay.DisplayViewpassedBannerView's ownAdUnitConfigurationstraight 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
adFormatsno longer holdsBANNER,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.DisplayViewnow renders against its own copy of the configuration. The copy carriesfingerprintover, because plugin event listeners are registered under it, andbroadcastId, 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 toAdUnitConfigurationwithout being added to the copy.The alternative considered was routing
CreativeFactoryoncreativeModel instanceof VideoCreativeModeland leavingadFormatsalone. That would additionally have madeAdViewManager.handleCreativeDisplay()deduplicate repeat displays of a banner video and silently break "watch again", becauselastCreativeShownis never reset.Notes for reviewers
MediationBannerAdUnitdeliberately does not stop refreshing when a video bid is returned. UnlikeBannerView, 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.loadAd(). Left out of this PR.BasicParameterBuilder.setVideoImpValues()defaultsvideo.placementto5(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 callingsetVideoPlacementType(...), as the examples here do.Examples
PrebidDemoKotlin:InAppMultiformatBannerActivity, registered as anAdFormat.MULTIFORMATtest 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_StartActionViewActivityfails, and also fails on a cleanmaster, so it is unrelated.New coverage: format mapping both ways, multiformat request emitting
imp.banner+imp.videowithinstl=0, call order independence between placement and formats, the refresh gate during and after playback,isBuiltInVideono longer latching, and the configuration copy including the field tripwire.🤖 Generated with Claude Code