Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,11 @@ - (void)videoViewCompletedDisplay {
}

if (self.creativeModel.adConfiguration.presentAsInterstitial) {
// no companion ads so pass this event to the PBMModalManager
// and close video automatically
[self.modalManager creativeDisplayCompleted:self];
if (self.dismissInterstitialModalState) {
// No companion ads, so pass completion to the modal manager and
// close automatically unless the publisher disabled that behavior.
[self.modalManager creativeDisplayCompleted:self];
if (self.creativeModel.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled &&
self.dismissInterstitialModalState) {
self.dismissInterstitialModalState();
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,10 @@ - (void)completeVideoViewDisplayWith:(PBMTrackingEvent)trackingEvent {
self.progressBar.hidden = YES;
}

if (self.adConfiguration.isBuiltInVideo && !self.creative.creativeModel.hasCompanionAd) {
BOOL shouldOfferReplay = self.adConfiguration.isBuiltInVideo ||
(self.adConfiguration.presentAsInterstitial &&
!self.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled);
if (shouldOfferReplay && !self.creative.creativeModel.hasCompanionAd) {
// UI: need to give some time to hide the interstitial before showing the Watch Again
if (self.adConfiguration.presentAsInterstitial) {
@weakify(self);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public class VideoControlsConfiguration: NSObject {

/// This property indicates whether mute controls is visible on the screen.
public var isSoundButtonVisible = false

/// Indicates whether a full-screen video without a companion ad closes automatically when playback completes.
///
/// The default value is `true`, preserving the SDK's existing auto-close behavior. Set this property to `false`
/// to keep the interstitial open and display a **Watch Again** button until the user closes the ad.
public var isAutoCloseOnCompletionEnabled = true

/// Use to initialize video controls with server values.
public func initialize(with ortbAdConfiguration: ORTBAdConfiguration?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ public class InterstitialRenderingAdUnit: NSObject, BaseInterstitialAdUnitProtoc
get { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible }
set { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible = newValue }
}

/// Controls whether full-screen video ads without an end card close when playback completes.
/// Set to `false` to keep the ad open with a **Watch Again** button. The default value is `true`.
public var isAutoCloseOnCompletionEnabled: Bool {
get { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled }
set { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = newValue }
}

// MARK: Private properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ public class RewardedAdUnit: NSObject, BaseInterstitialAdUnitProtocol {
get { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible }
set { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible = newValue }
}

/// Controls whether full-screen video ads without an end card close when playback completes.
/// Set to `false` to keep the ad open with a **Watch Again** button. The default value is `true`.
public var isAutoCloseOnCompletionEnabled: Bool {
Comment thread
mdanylov-sigma marked this conversation as resolved.
Outdated
get { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled }
set { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = newValue }
}

// MARK: Internal Properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public class MediationBaseInterstitialAdUnit : NSObject {
get { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible }
set { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible = newValue }
}

/// Controls whether full-screen video ads without an end card close when playback completes.
/// Set to `false` to keep the ad open with a **Watch Again** button. The default value is `true`.
public var isAutoCloseOnCompletionEnabled: Bool {
get { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled }
set { adUnitConfig.adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = newValue }
}

/// The area for the close button in the video ad.
public var closeButtonArea: Double {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@property (nonatomic, weak, nullable) PBMVideoCreative *creative;
@property (nonatomic, strong) PBMAdViewButtonDecorator * _Nonnull skipButtonDecorator;
@property (nonatomic, strong, nonnull) NSNumber * progressBarDuration;
@property (nonatomic, strong, nullable) UIButton *btnWatchAgain;

- (void)updateControls;
- (CGFloat)requiredVideoDuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ class PBMVideoViewPlaybackStateTest: XCTestCase, CreativeResolutionDelegate {
}
}

func testFullscreenVideoWithAutoCloseDisabledOffersReplay() {
runWithStartedPlayback(
isInterstitial: true,
autoCloseOnCompletion: false,
hasCompanionAd: false,
beforeAssertions: { $0.handleDidPlayToEndTime() },
delayBeforeAssertions: 0.6
) { videoView in
XCTAssertNotNil(videoView.btnWatchAgain)
}
}

func testFullscreenVideoWithAutoCloseEnabledDoesNotOfferReplay() {
runWithStartedPlayback(
isInterstitial: true,
hasCompanionAd: false,
beforeAssertions: { $0.handleDidPlayToEndTime() },
delayBeforeAssertions: 0.6
) { videoView in
XCTAssertNil(videoView.btnWatchAgain)
}
}

// MARK: - App lifecycle transitions

func testResignActiveWhilePlayingSetsPausedByBackground() {
Expand Down Expand Up @@ -369,10 +392,18 @@ class PBMVideoViewPlaybackStateTest: XCTestCase, CreativeResolutionDelegate {
// Loads the video creative, puts it on screen, starts playback and runs
// assertions one second later, when the player is actually playing.
private func runWithStartedPlayback(isInterstitial: Bool = false,
autoCloseOnCompletion: Bool = true,
hasCompanionAd: Bool = true,
beforeAssertions: ((PBMVideoView) -> Void)? = nil,
delayBeforeAssertions: TimeInterval = 0,
file: StaticString = #file,
line: UInt = #line,
assertions: @escaping (PBMVideoView) -> Void) {
setupVideoCreative(isInterstitial: isInterstitial)
setupVideoCreative(
isInterstitial: isInterstitial,
autoCloseOnCompletion: autoCloseOnCompletion,
hasCompanionAd: hasCompanionAd
)
self.videoCreative.creativeModel.displayDurationInSeconds = 6

self.expectationCreativeReady = self.expectation(description: "expectationCreativeReady")
Expand All @@ -396,8 +427,11 @@ class PBMVideoViewPlaybackStateTest: XCTestCase, CreativeResolutionDelegate {
let expectationAssertionsCompleted = expectation(description: "expectationAssertionsCompleted")

DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
assertions(videoView)
expectationAssertionsCompleted.fulfill()
beforeAssertions?(videoView)
DispatchQueue.main.asyncAfter(deadline: .now() + delayBeforeAssertions) {
assertions(videoView)
expectationAssertionsCompleted.fulfill()
}
})

waitForExpectations(timeout: 5, handler: nil)
Expand Down Expand Up @@ -443,7 +477,9 @@ class PBMVideoViewPlaybackStateTest: XCTestCase, CreativeResolutionDelegate {

private func setupVideoCreative(videoFileURL: String = "http://get_video/small.mp4",
localVideoFileName: String = "small.mp4",
isInterstitial: Bool = false) {
isInterstitial: Bool = false,
autoCloseOnCompletion: Bool = true,
hasCompanionAd: Bool = true) {
let rule = MockServerRule(urlNeedle: videoFileURL,
mimeType: MockServerMimeType.MP4.rawValue,
connectionID: connection.internalID,
Expand All @@ -452,8 +488,10 @@ class PBMVideoViewPlaybackStateTest: XCTestCase, CreativeResolutionDelegate {

let adConfiguration = AdConfiguration()
adConfiguration.isInterstitialAd = isInterstitial
adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = autoCloseOnCompletion

let model = CreativeModel(adConfiguration: adConfiguration)
model.hasCompanionAd = hasCompanionAd
model.videoFileURL = videoFileURL

self.expectationDownloadCompleted = self.expectation(description: "expectationDownloadCompleted")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import AVFoundation

@testable @_spi(PBMInternal) import PrebidMobile

private final class CompletionTrackingModalManager: MockModalManager {
private(set) var completedCreative: AbstractCreative?

override func creativeDisplayCompleted(_ creative: AbstractCreative) {
completedCreative = creative
}
}

class VideoCreativeDelegateTest: XCTestCase, CreativeResolutionDelegate, CreativeViewDelegate, PBMVideoViewDelegate {

var videoCreative:PBMVideoCreative!
Expand Down Expand Up @@ -223,6 +231,84 @@ class VideoCreativeDelegateTest: XCTestCase, CreativeResolutionDelegate, Creativ

waitForExpectations(timeout: 1, handler: nil)
}

func testInterstitialWithoutCompanionAutoClosesByDefault() {
let adConfiguration = AdConfiguration()
adConfiguration.isInterstitialAd = true
let model = CreativeModel(adConfiguration: adConfiguration)
model.hasCompanionAd = false

videoCreative = PBMVideoCreative(
creativeModel: model,
transaction: UtilitiesForTesting.createEmptyTransaction(),
videoData: Data()
)

let modalManager = MockModalManager()
videoCreative.modalManager = modalManager
videoCreative.showAsInterstitial(
fromRootViewController: UIViewController(),
displayProperties: InterstitialDisplayProperties()
)
XCTAssertEqual(modalManager.modalStateStack.count, 1)

videoCreative.videoViewCompletedDisplay()

XCTAssertTrue(modalManager.modalStateStack.isEmpty)
}

func testInterstitialWithoutCompanionCanDisableAutoClose() {
let adConfiguration = AdConfiguration()
adConfiguration.isInterstitialAd = true
adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = false
let model = CreativeModel(adConfiguration: adConfiguration)
model.hasCompanionAd = false

videoCreative = PBMVideoCreative(
creativeModel: model,
transaction: UtilitiesForTesting.createEmptyTransaction(),
videoData: Data()
)

let modalManager = MockModalManager()
videoCreative.modalManager = modalManager
videoCreative.showAsInterstitial(
fromRootViewController: UIViewController(),
displayProperties: InterstitialDisplayProperties()
)
XCTAssertEqual(modalManager.modalStateStack.count, 1)

videoCreative.videoViewCompletedDisplay()

XCTAssertEqual(modalManager.modalStateStack.count, 1)
}

func testRewardedCompletionIsReportedWhenAutoCloseIsDisabled() {
let adConfiguration = AdConfiguration()
adConfiguration.isInterstitialAd = true
adConfiguration.isRewarded = true
adConfiguration.videoControlsConfig.isAutoCloseOnCompletionEnabled = false
let model = CreativeModel(adConfiguration: adConfiguration)
model.hasCompanionAd = false

videoCreative = PBMVideoCreative(
creativeModel: model,
transaction: UtilitiesForTesting.createEmptyTransaction(),
videoData: Data()
)

let modalManager = CompletionTrackingModalManager()
videoCreative.modalManager = modalManager
videoCreative.showAsInterstitial(
fromRootViewController: UIViewController(),
displayProperties: InterstitialDisplayProperties()
)

videoCreative.videoViewCompletedDisplay()

XCTAssertTrue(modalManager.completedCreative === videoCreative)
XCTAssertEqual(modalManager.modalStateStack.count, 1)
}

func testSkipOffset() {
let expectation = self.expectation(description: "Should push Modal")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class VideoControlsConfigTests: XCTestCase {
XCTAssertTrue(adConfiguration.isSoundButtonVisible == false)
}

func testAutoCloseOnCompletion() {
let adConfiguration = VideoControlsConfiguration()
XCTAssertTrue(adConfiguration.isAutoCloseOnCompletionEnabled)

adConfiguration.isAutoCloseOnCompletionEnabled = false
XCTAssertFalse(adConfiguration.isAutoCloseOnCompletionEnabled)
}

func testCloseButtonArea() {
let adConfiguration = VideoControlsConfiguration()
XCTAssertEqual(adConfiguration.closeButtonArea, PrebidConstants.BUTTON_AREA_DEFAULT.doubleValue)
Expand Down
Loading