Skip to content
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
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ mParticle.start(with: options)
// Later in your code, when a user begins to engage with your content
let mediaSession = MPMediaSession.init(
coreSDK: mParticle, // mParticle SDK Instance
mediaContentId: '1234567', // Custom media ID
title: 'Funny internet cat video', // Custom media Title
mediaContentId: "1234567", // Custom media ID
title: "Funny internet cat video", // Custom media Title
duration: 120000, // Duration in milliseconds
contentType: .video, // Content Type (Video or Audio)
streamType: .onDemand) // Stream Type (OnDemand, Live, etc.)
streamType: .onDemand // Stream Type (OnDemand, Live, etc.)
)

// OR, optionally exclude ad break time from content time tracking when using `logAdBreakStart` and `logAdBreakEnd`
let mediaSession = MPMediaSession.init(
coreSDK: mParticle,
mediaContentId: "1234567",
title: "Funny internet cat video",
duration: 120000,
contentType: .video,
streamType: .onDemand,
excludeAdBreaksFromContentTime: true // Optional flag (defaults to false)
)

mediaSession.logMediaSessionStart()
mediaSession.logPlay()
Expand Down
53 changes: 46 additions & 7 deletions mParticle-Apple-Media-SDK-Shared/MPMediaSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ let PlayerOvp = "player_ovp"
@objc public var mediaSessionAttributes: [String:Any]
@objc public var adContent: MPMediaAdContent?
@objc public var adBreak: MPMediaAdBreak?
@objc public var excludeAdBreaksFromContentTime: Bool
@objc public var segment: MPMediaSegment?
@objc public var mediaEventListener: ((MPMediaEvent)->Void)?

Expand Down Expand Up @@ -273,7 +274,6 @@ let PlayerOvp = "player_ovp"
private(set) public var currentPlaybackStartTimestamp: Date? //Timestamp for beginning of current playback
private(set) public var storedPlaybackTime: Double = 0 //On Pause calculate playback time and clear currentPlaybackTime
private var sessionSummarySent = false // Ensures we only send summary event once


// MARK: init
/// Creates a media session object. This does not start a session, you can do so by calling `logMediaSessionStart`.
Expand All @@ -284,7 +284,7 @@ let PlayerOvp = "player_ovp"
/// :param: duration The playback time of the media content in milliseconds
/// :param: contentType The type of the media content (e.g. video)
/// :param: streamType The stream type for the media (e.g. on-demand)
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType) {
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, excludeAdBreaksFromContentTime: Bool = false) {
if let coreSDK = coreSDK {
self.coreSDK = coreSDK
} else {
Expand All @@ -296,6 +296,7 @@ let PlayerOvp = "player_ovp"
self.duration = duration
self.contentType = contentType
self.streamType = streamType
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
self.mediaSessionId = NSUUID().uuidString
self.mediaSessionAttributes = [:]
self.logMPEvents = false
Expand All @@ -318,7 +319,7 @@ let PlayerOvp = "player_ovp"
/// :param: logMPEvents Set to true if you would like custom events forwarded to the mParticle SDK
/// :param: logMediaEvents Set to true if you would like media events forwarded to the mParticle SDK
/// :param: completeLimit Int from 1 to 100 denotes percentage of progress needed to be considered "completed"
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int) {
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, excludeAdBreaksFromContentTime: Bool = false, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int) {
if let coreSDK = coreSDK {
self.coreSDK = coreSDK
} else {
Expand All @@ -330,23 +331,45 @@ let PlayerOvp = "player_ovp"
self.duration = duration
self.contentType = contentType
self.streamType = streamType
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
self.mediaSessionId = NSUUID().uuidString
self.mediaSessionAttributes = [:]
self.logMPEvents = logMPEvents
self.logMediaEvents = logMediaEvents
if ( 100 >= completeLimit && completeLimit > 0) {
self.mediaContentCompleteLimit = completeLimit
}
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime

let currentTimestamp = Date()
self.mediaSessionStartTimestamp = currentTimestamp
self.mediaSessionEndTimestamp = currentTimestamp
}

internal convenience init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int, testing: Bool) {
self.init(coreSDK: coreSDK, mediaContentId: mediaContentId, title: title, duration: duration, contentType: contentType, streamType: streamType, logMPEvents: logMPEvents, logMediaEvents: logMediaEvents, completeLimit: completeLimit)

self.sessionSummarySent = true
internal convenience init(
coreSDK: MParticle?,
mediaContentId: String,
title: String,
duration: NSNumber?,
contentType: MPMediaContentType,
streamType: MPMediaStreamType,
logMPEvents: Bool,
logMediaEvents: Bool,
completeLimit: Int,
excludeAdBreaksFromContentTime: Bool = false,
testing: Bool) {
self.init(coreSDK: coreSDK,
mediaContentId: mediaContentId,
title: title,
duration: duration,
contentType: contentType,
streamType: streamType,
excludeAdBreaksFromContentTime: excludeAdBreaksFromContentTime,
logMPEvents: logMPEvents,
logMediaEvents: logMediaEvents,
completeLimit: completeLimit)

self.sessionSummarySent = true
}

deinit {
Expand Down Expand Up @@ -473,6 +496,8 @@ let PlayerOvp = "player_ovp"
// MARK: ad break
/// Logs that a sequence of one or more ads has begun
@objc public func logAdBreakStart(adBreak: MPMediaAdBreak, options: Options? = nil) {
pauseContentTimeIfAdBreakExclusionEnabled()

self.adBreak = adBreak
let mediaEvent = self.makeMediaEvent(name: .adBreakStart, options: options)
mediaEvent.adBreak = self.adBreak
Expand All @@ -481,11 +506,25 @@ let PlayerOvp = "player_ovp"

/// Indicates that the ad break is complete
@objc public func logAdBreakEnd(options: Options? = nil) {
resumeContentTimeIfAdBreakExclusionEnabled()

let mediaEvent = self.makeMediaEvent(name: .adBreakEnd, options: options)
mediaEvent.adBreak = self.adBreak
self.logEvent(mediaEvent: mediaEvent)
self.adBreak = nil
}

// MARK: private helpers (ad break)
private func pauseContentTimeIfAdBreakExclusionEnabled() {
guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp != nil else { return }
storedPlaybackTime += Date().timeIntervalSince(currentPlaybackStartTimestamp!)
currentPlaybackStartTimestamp = nil
}

private func resumeContentTimeIfAdBreakExclusionEnabled() {
guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp == nil else { return }
currentPlaybackStartTimestamp = Date()
}

// MARK: ad content
/// Indicates a given ad creative has started playing
Expand Down
2 changes: 1 addition & 1 deletion mParticle-Apple-Media-SDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
};
};
buildConfigurationList = DBD815E22319A7F400A9809C /* Build configuration list for PBXProject "mParticle-Apple-Media-SDK" */;
compatibilityVersion = "Xcode 9.3";
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Expand Down
74 changes: 72 additions & 2 deletions mParticle-Apple-Media-SDKTests/mParticle_Apple_MediaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
coreSDK = MParticle.sharedInstance()
mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "12345", title: "foo title", duration: 90000, contentType: .video, streamType: .onDemand, logMPEvents: false, logMediaEvents: true, completeLimit: 90, testing: true)
mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "12345", title: "foo title", duration: 90000, contentType: .video, streamType: .onDemand, logMPEvents: false, logMediaEvents: true, completeLimit: 90, excludeAdBreaksFromContentTime: true, testing: true)
MPListenerController.sharedInstance().addSdkListener(self)
}

Expand All @@ -102,6 +102,7 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
XCTAssertEqual(mediaSession?.streamType, .onDemand)
XCTAssertTrue(mediaSession?.mediaSessionAttributes != nil)
XCTAssertTrue(mediaSession?.mediaSessionAttributes.count == 0)
XCTAssertTrue(mediaSession!.excludeAdBreaksFromContentTime)

let mediaEvent1 = mediaSession?.makeMediaEvent(name: .play)

Expand All @@ -113,7 +114,7 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
XCTAssertEqual(mediaEvent1?.streamType, .onDemand)
XCTAssertTrue(mediaEvent1?.customAttributes == nil)

mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "678", title: "foo title 2", duration: 80000, contentType: .audio, streamType: .liveStream, logMPEvents: true, logMediaEvents: false, completeLimit: 90, testing: true)
mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "678", title: "foo title 2", duration: 80000, contentType: .audio, streamType: .liveStream, logMPEvents: true, logMediaEvents: false, completeLimit: 90, excludeAdBreaksFromContentTime: true, testing: true)
mediaSession?.mediaSessionAttributes = ["exampleKey1": "exampleValue1"]

XCTAssertTrue(mediaSession!.logMPEvents, "logMPEvents should have been set to true")
Expand All @@ -125,6 +126,8 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
XCTAssertEqual(mediaSession?.streamType, .liveStream)
XCTAssertEqual(mediaSession?.mediaSessionAttributes["exampleKey1"] as! String, "exampleValue1")
XCTAssertTrue(mediaSession?.mediaSessionAttributes.count == 1)
XCTAssertTrue(mediaSession!.excludeAdBreaksFromContentTime)


let mediaEvent2 = mediaSession?.makeMediaEvent(name: .play)

Expand Down Expand Up @@ -415,6 +418,73 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
mediaSession?.logAdBreakEnd()
self.waitForExpectations(timeout: defaultTimeout, handler: nil)
}

func testAdBreakExclusionDisabledDoesNotPauseContentTime() {
let adBreak = MPMediaAdBreak(title: "foo adbreak title", id: "12345")
mediaSession?.excludeAdBreaksFromContentTime = false

// Start content playback and accumulate 0.2s before the ad break
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0)
mediaSession?.logPlay()
Thread.sleep(forTimeInterval: 0.2)

XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.1)

// 0.2s should count toward content time.
mediaSession?.logAdBreakStart(adBreak: adBreak)
Thread.sleep(forTimeInterval: 0.2)

XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.1)

mediaSession?.logAdBreakEnd()
XCTAssertNil(mediaSession?.adBreak)

mediaSession?.logPause()

XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.1)
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)
}

func testAdBreakExclusionEnabledExcludesAdTime() {
let adBreak = MPMediaAdBreak(title: "foo adbreak title", id: "12345")
mediaSession?.excludeAdBreaksFromContentTime = true

// Start content playback and accumulate 0.2s before the ad break
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0)
mediaSession?.logPlay()
Thread.sleep(forTimeInterval: 0.2)

XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
XCTAssertEqual(mediaSession!.storedPlaybackTime, 0)
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.08)

// Start ad break content time should pause & be stored
mediaSession?.logAdBreakStart(adBreak: adBreak)
Thread.sleep(forTimeInterval: 0.2)

// Content tracking paused: playhead cleared, stored captured 0.2s
XCTAssertEqual(mediaSession!.storedPlaybackTime, mediaSession!.mediaContentTimeSpent)
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.08)
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)

// End ad break auto-resume content tracking
mediaSession?.logAdBreakEnd()
Thread.sleep(forTimeInterval: 0.2)

XCTAssertEqual(mediaSession!.storedPlaybackTime, 0.2, accuracy: 0.08)
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.08)
XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
XCTAssertNil(mediaSession?.adBreak)

// Watch a bit more content after the ad
Thread.sleep(forTimeInterval: 0.2)

mediaSession?.logPause()

XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.6, accuracy: 0.08)
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)
}

func testLogSegmentStart() {
let segment = MPMediaSegment(title: "foo segment title", index: 3, duration: 30000)
Expand Down
Loading