Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEM-1261 #144

Merged
merged 5 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 6 additions & 3 deletions Classes/PKStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ class BasicStateMachine<T: StateProtocol> {
PKLog.error("\(String(describing: type(of: self))) was set to initial state, this is not allowed")
return
}
self.state = state
DispatchQueue.main.async {
self.onStateChange?(state)
// only set state when changed
if self.state != state {
self.state = state
DispatchQueue.main.async {
self.onStateChange?(state)
}
}
}
}
Expand Down
63 changes: 58 additions & 5 deletions Plugins/IMA/AdsEnabledPlayerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,36 @@ import UIKit
import AVFoundation
import AVKit

/// `AdsPlayerState` represents `AdsEnabledPlayerController` state machine states.
enum AdsPlayerState: Int, StateProtocol {
/// initial state.
case start = 0
/// when prepare was requested for the first time and it is stalled until ad started (preroll) / faliure or content resume
case waitingForPrepare
/// a moment before we called prepare until prepare() was finished (the sychornos code only not async tasks)
case preparing
/// Indicates when prepare() was finished (the sychornos code only not async tasks)
case prepared
}

class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPluginDataSource {

enum PlayType {
case play, resume
}

/// The ads player state machine.
private var stateMachine = BasicStateMachine(initialState: AdsPlayerState.start, allowTransitionToInitialState: true)

/// The media config to prepare the player with.
/// Uses @NSCopying in order to make a copy whenever set with new value.
@NSCopying private var prepareMediaConfig: MediaConfig!

/// indicates if play was used, if `play()` or `resume()` was called we set this to true.
var isPlayEnabled = false
private var isPlayEnabled = false

/// a semaphore to make sure prepare calling will not be reached from 2 threads by mistake.
private let prepareSemaphore = DispatchSemaphore(value: 1)

/// when playing post roll google sends content resume when finished.
/// In our case we need to prevent sending play/resume to the player because the content already ended.
Expand Down Expand Up @@ -48,10 +70,10 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl
}
}

// TODO:: finilize prepare
override func prepare(_ config: MediaConfig) {
super.prepare(config)

self.stop()
self.stateMachine.set(state: .waitingForPrepare)
self.prepareMediaConfig = config
self.adsPlugin.requestAds()
}

Expand All @@ -75,8 +97,9 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl
}

override func stop() {
self.adsPlugin.destroyManager()
self.stateMachine.set(state: .start)
super.stop()
self.adsPlugin.destroyManager()
self.isPlayEnabled = false
self.shouldPreventContentResume = false
}
Expand All @@ -95,18 +118,24 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl
return self.delegate!.playerShouldPlayAd(self)
}

var adsPluginStartTime: TimeInterval {
return self.prepareMediaConfig?.startTime ?? 0
}

/************************************************************/
// MARK: - AdsPluginDelegate
/************************************************************/

func adsPlugin(_ adsPlugin: AdsPlugin, loaderFailedWith error: String) {
if self.isPlayEnabled {
self.preparePlayerIfNeeded()
super.play()
self.adsPlugin.didPlay()
}
}

func adsPlugin(_ adsPlugin: AdsPlugin, managerFailedWith error: String) {
self.preparePlayerIfNeeded()
super.play()
self.adsPlugin.didPlay()
}
Expand All @@ -117,9 +146,15 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl
super.pause()
case let e where type(of: e) == AdEvent.adDidRequestResume:
if !self.shouldPreventContentResume {
self.preparePlayerIfNeeded()
super.resume()
}
case let e where type(of: e) == AdEvent.adResumed: self.isPlayEnabled = true
case let e where type(of: e) == AdEvent.adStarted:
// when starting to play pre roll start preparing the player.
if event.adInfo?.positionType == .preRoll {
self.preparePlayerIfNeeded()
}
case let e where type(of: e) == AdEvent.adLoaded || type(of: e) == AdEvent.adBreakReady:
if self.shouldPreventContentResume == true { return } // no need to handle twice if already true
if event.adInfo?.positionType == .postRoll {
Expand All @@ -132,12 +167,30 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl

func adsRequestTimedOut(shouldPlay: Bool) {
if shouldPlay {
self.preparePlayerIfNeeded()
self.play()
}
}

func play(_ playType: PlayType) {
self.preparePlayerIfNeeded()
playType == .play ? super.play() : super.resume()
self.adsPlugin.didPlay()
}

/************************************************************/
// MARK: - Private
/************************************************************/

/// prepare the player only if wasn't prepared yet.
private func preparePlayerIfNeeded() {
self.prepareSemaphore.wait() // use semaphore to make sure will not be called from more than one thread by mistake.
if self.stateMachine.getState() == .waitingForPrepare {
self.stateMachine.set(state: .preparing)
PKLog.debug("will prepare player")
super.prepare(self.prepareMediaConfig)
self.stateMachine.set(state: .prepared)
}
self.prepareSemaphore.signal()
}
}
8 changes: 8 additions & 0 deletions Plugins/IMA/AdsPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import AVKit

protocol AdsPluginDataSource : class {
func adsPluginShouldPlayAd(_ adsPlugin: AdsPlugin) -> Bool
/// the player's media config start time.
var adsPluginStartTime: TimeInterval { get }
}

protocol AdsPluginDelegate : class {
Expand All @@ -27,12 +29,18 @@ protocol AdsPlugin: PKPlugin, AVPictureInPictureControllerDelegate {
weak var dataSource: AdsPluginDataSource? { get set }
weak var delegate: AdsPluginDelegate? { get set }
var pipDelegate: AVPictureInPictureControllerDelegate? { get set }
/// is ad playing currently.
var isAdPlaying: Bool { get }

/// request ads from the server.
func requestAds()
/// resume ad
func resume()
/// pause ad
func pause()
/// ad content complete
func contentComplete()
/// destroy the ads manager
func destroyManager()
/// called after player called `super.play()`
func didPlay()
Expand Down
16 changes: 7 additions & 9 deletions Plugins/IMA/IMAConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,20 @@ import GoogleInteractiveMediaAds

@objc public class IMAConfig: NSObject {

@objc public var language: String = "en"
@objc public var enableBackgroundPlayback: Bool {
return true
}
@objc public let enableBackgroundPlayback = true
// defaulted to false, because otherwise ad breaks events will not happen.
// we need to have control on whether ad break will start playing or not using `Loaded` event is not enough.
// (will also need more safety checks because loaded will happen more than once).
@objc public var autoPlayAdBreaks: Bool {
return false
}
// (will also need more safety checks for loaded because loaded will happen more than once).
@objc public let autoPlayAdBreaks = false
@objc public var language: String = "en"

@objc public var videoBitrate = kIMAAutodetectBitrate
@objc public var videoMimeTypes: [Any]?
@objc public var adTagUrl: String = ""
@objc public var companionView: UIView?
@objc public var webOpenerPresentingController: UIViewController?
@objc public var requestTimeoutInterval: TimeInterval = 5
/// ads request timeout interval, when ads request will take more then this time will resume content.
@objc public var requestTimeoutInterval: TimeInterval = IMAPlugin.defaultTimeoutInterval
/// enables debug mode on IMA SDK which will output detailed log information to the console.
/// The default value is false.
@objc public var enableDebugMode: Bool = false
Expand Down
42 changes: 31 additions & 11 deletions Plugins/IMA/IMAPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ enum IMAState: Int, StateProtocol {

@objc public class IMAPlugin: BasePlugin, PKPluginWarmUp, PlayerDecoratorProvider, AdsPlugin, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMAWebOpenerDelegate, IMAContentPlayhead {

/// the default timeout interval for ads request.
static let defaultTimeoutInterval: TimeInterval = 5

weak var dataSource: AdsPluginDataSource? {
didSet {
PKLog.debug("data source set")
Expand All @@ -43,20 +46,19 @@ enum IMAState: Int, StateProtocol {
/// The IMA plugin state machine
private var stateMachine = BasicStateMachine(initialState: IMAState.start, allowTransitionToInitialState: false)

private static var loader: IMAAdsLoader!
private var adsManager: IMAAdsManager?
private var renderingSettings: IMAAdsRenderingSettings! = IMAAdsRenderingSettings()
private static var loader: IMAAdsLoader!

private var pictureInPictureProxy: IMAPictureInPictureProxy?
private var loadingView: UIView?

// we must have config error will be thrown otherwise
private var config: IMAConfig!

private var timer: Timer?
/// timer for checking IMA requests timeout.
private var requestTimeoutTimer: Timer?
/// the request timeout interval
private var requestTimeoutInterval: TimeInterval = 5
private var requestTimeoutInterval: TimeInterval = IMAPlugin.defaultTimeoutInterval

/************************************************************/
// MARK: - IMAContentPlayhead
Expand Down Expand Up @@ -91,6 +93,7 @@ enum IMAState: Int, StateProtocol {
try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus)
if let adsConfig = pluginConfig as? IMAConfig {
self.config = adsConfig
self.requestTimeoutInterval = adsConfig.requestTimeoutInterval
if IMAPlugin.loader == nil {
self.setupLoader(with: adsConfig)
}
Expand Down Expand Up @@ -118,7 +121,6 @@ enum IMAState: Int, StateProtocol {

// TODO:: finilize update config & updateMedia logic
public override func onUpdateMedia(mediaConfig: MediaConfig) {
PKLog.debug("mediaConfig: " + String(describing: mediaConfig))
super.onUpdateMedia(mediaConfig: mediaConfig)
}

Expand Down Expand Up @@ -265,16 +267,20 @@ enum IMAState: Int, StateProtocol {
// Ad break, will be called before each scheduled ad break. Ad breaks may contain more than 1 ad.
// `event.ad` is not available at this point do not use it here.
case .AD_BREAK_READY:
self.notify(event: AdEvent.AdBreakReady())
guard canPlayAd(forState: currentState) else { return }
self.start(adsManager: adsManager)
if shouldDiscardAd() {
PKLog.debug("discard Ad Break")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gal-orlanczyk why self.discardAdBreak is no called here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't discardAdBreak in adBreakReady because no ad have been loaded yet, when we don't call .start() on ad break it is like discarding, if we will call discard here it will discard the next adBreak which is unwanted behavior.

} else {
self.notify(event: AdEvent.AdBreakReady())
guard canPlayAd(forState: currentState) else { return }
self.start(adsManager: adsManager)
}
// single ad only fires `LOADED` without `AD_BREAK_READY`.
case .LOADED:
if shouldDiscard(ad: event.ad, currentState: currentState) {
adsManager.discardAdBreak()
self.discardAdBreak(adsManager: adsManager)
} else {
let adEvent = event.ad != nil ? AdEvent.AdLoaded(adInfo: PKAdInfo(ad: event.ad)) : AdEvent.AdLoaded()
self.notify(event: adEvent)
// single ad only fires `LOADED` without `AD_BREAK_READY`.
// if we have more than one ad don't start the manager, it will be handled in `AD_BREAK_READY`
guard adsManager.adCuePoints.count == 0 else { return }
guard canPlayAd(forState: currentState) else { return }
Expand Down Expand Up @@ -447,15 +453,29 @@ enum IMAState: Int, StateProtocol {
return false
}

private func shouldDiscardAd() -> Bool {
if currentTime < self.dataSource?.adsPluginStartTime ?? 0 {
return true
}
return false
}

private func shouldDiscard(ad: IMAAd, currentState: IMAState) -> Bool {
let adInfo = PKAdInfo(ad: ad)
let isStartTimeInvalid = adInfo.positionType != .postRoll && adInfo.timeOffset < self.dataSource?.adsPluginStartTime ?? 0
let isPreRollInvalid = adInfo.positionType == .preRoll && (currentState == .adsRequestTimedOut || currentState == .contentPlaying)
if isPreRollInvalid {
if isStartTimeInvalid || isPreRollInvalid {
return true
}
return false
}

private func discardAdBreak(adsManager: IMAAdsManager) {
PKLog.debug("discard Ad Break")
adsManager.discardAdBreak()
self.adsManagerDidRequestContentResume(adsManager)
}

/************************************************************/
// MARK: - AVPictureInPictureControllerDelegate
/************************************************************/
Expand Down