Skip to content

Commit a12375e

Browse files
feat: update media content time spent calculations with ad breaks (#22)
* Created new inits for feature flag * add logic to exclude ad break from mediaContentTimeSpent * extract to helper to clean logic * follow implementation of file * add core SDK and switch deployment target to 12.0 * add check to initializer test * added tests for new feature * removed extra private var logic * update readme with new optional init * Update README.md Co-authored-by: James Newman <[email protected]> * removed self * addressed @denischilik comments * remove comment and fix internal init * fix default value bug in init * give more cushion for CI --------- Co-authored-by: James Newman <[email protected]>
1 parent 50a6ca5 commit a12375e

File tree

4 files changed

+133
-13
lines changed

4 files changed

+133
-13
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,23 @@ mParticle.start(with: options)
6161
// Later in your code, when a user begins to engage with your content
6262
let mediaSession = MPMediaSession.init(
6363
coreSDK: mParticle, // mParticle SDK Instance
64-
mediaContentId: '1234567', // Custom media ID
65-
title: 'Funny internet cat video', // Custom media Title
64+
mediaContentId: "1234567", // Custom media ID
65+
title: "Funny internet cat video", // Custom media Title
6666
duration: 120000, // Duration in milliseconds
6767
contentType: .video, // Content Type (Video or Audio)
68-
streamType: .onDemand) // Stream Type (OnDemand, Live, etc.)
68+
streamType: .onDemand // Stream Type (OnDemand, Live, etc.)
69+
)
6970

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

7182
mediaSession.logMediaSessionStart()
7283
mediaSession.logPlay()

mParticle-Apple-Media-SDK-Shared/MPMediaSDK.swift

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ let PlayerOvp = "player_ovp"
239239
@objc public var mediaSessionAttributes: [String:Any]
240240
@objc public var adContent: MPMediaAdContent?
241241
@objc public var adBreak: MPMediaAdBreak?
242+
@objc public var excludeAdBreaksFromContentTime: Bool
242243
@objc public var segment: MPMediaSegment?
243244
@objc public var mediaEventListener: ((MPMediaEvent)->Void)?
244245

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

278278
// MARK: init
279279
/// Creates a media session object. This does not start a session, you can do so by calling `logMediaSessionStart`.
@@ -284,7 +284,7 @@ let PlayerOvp = "player_ovp"
284284
/// :param: duration The playback time of the media content in milliseconds
285285
/// :param: contentType The type of the media content (e.g. video)
286286
/// :param: streamType The stream type for the media (e.g. on-demand)
287-
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType) {
287+
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, excludeAdBreaksFromContentTime: Bool = false) {
288288
if let coreSDK = coreSDK {
289289
self.coreSDK = coreSDK
290290
} else {
@@ -296,6 +296,7 @@ let PlayerOvp = "player_ovp"
296296
self.duration = duration
297297
self.contentType = contentType
298298
self.streamType = streamType
299+
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
299300
self.mediaSessionId = NSUUID().uuidString
300301
self.mediaSessionAttributes = [:]
301302
self.logMPEvents = false
@@ -318,7 +319,7 @@ let PlayerOvp = "player_ovp"
318319
/// :param: logMPEvents Set to true if you would like custom events forwarded to the mParticle SDK
319320
/// :param: logMediaEvents Set to true if you would like media events forwarded to the mParticle SDK
320321
/// :param: completeLimit Int from 1 to 100 denotes percentage of progress needed to be considered "completed"
321-
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int) {
322+
@objc public init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, excludeAdBreaksFromContentTime: Bool = false, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int) {
322323
if let coreSDK = coreSDK {
323324
self.coreSDK = coreSDK
324325
} else {
@@ -330,23 +331,45 @@ let PlayerOvp = "player_ovp"
330331
self.duration = duration
331332
self.contentType = contentType
332333
self.streamType = streamType
334+
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
333335
self.mediaSessionId = NSUUID().uuidString
334336
self.mediaSessionAttributes = [:]
335337
self.logMPEvents = logMPEvents
336338
self.logMediaEvents = logMediaEvents
337339
if ( 100 >= completeLimit && completeLimit > 0) {
338340
self.mediaContentCompleteLimit = completeLimit
339341
}
342+
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
340343

341344
let currentTimestamp = Date()
342345
self.mediaSessionStartTimestamp = currentTimestamp
343346
self.mediaSessionEndTimestamp = currentTimestamp
344347
}
345348

346-
internal convenience init(coreSDK: MParticle?, mediaContentId: String, title: String, duration: NSNumber?, contentType: MPMediaContentType, streamType: MPMediaStreamType, logMPEvents: Bool, logMediaEvents: Bool, completeLimit: Int, testing: Bool) {
347-
self.init(coreSDK: coreSDK, mediaContentId: mediaContentId, title: title, duration: duration, contentType: contentType, streamType: streamType, logMPEvents: logMPEvents, logMediaEvents: logMediaEvents, completeLimit: completeLimit)
348-
349-
self.sessionSummarySent = true
349+
internal convenience init(
350+
coreSDK: MParticle?,
351+
mediaContentId: String,
352+
title: String,
353+
duration: NSNumber?,
354+
contentType: MPMediaContentType,
355+
streamType: MPMediaStreamType,
356+
logMPEvents: Bool,
357+
logMediaEvents: Bool,
358+
completeLimit: Int,
359+
excludeAdBreaksFromContentTime: Bool = false,
360+
testing: Bool) {
361+
self.init(coreSDK: coreSDK,
362+
mediaContentId: mediaContentId,
363+
title: title,
364+
duration: duration,
365+
contentType: contentType,
366+
streamType: streamType,
367+
excludeAdBreaksFromContentTime: excludeAdBreaksFromContentTime,
368+
logMPEvents: logMPEvents,
369+
logMediaEvents: logMediaEvents,
370+
completeLimit: completeLimit)
371+
372+
self.sessionSummarySent = true
350373
}
351374

352375
deinit {
@@ -473,6 +496,8 @@ let PlayerOvp = "player_ovp"
473496
// MARK: ad break
474497
/// Logs that a sequence of one or more ads has begun
475498
@objc public func logAdBreakStart(adBreak: MPMediaAdBreak, options: Options? = nil) {
499+
pauseContentTimeIfAdBreakExclusionEnabled()
500+
476501
self.adBreak = adBreak
477502
let mediaEvent = self.makeMediaEvent(name: .adBreakStart, options: options)
478503
mediaEvent.adBreak = self.adBreak
@@ -481,11 +506,25 @@ let PlayerOvp = "player_ovp"
481506

482507
/// Indicates that the ad break is complete
483508
@objc public func logAdBreakEnd(options: Options? = nil) {
509+
resumeContentTimeIfAdBreakExclusionEnabled()
510+
484511
let mediaEvent = self.makeMediaEvent(name: .adBreakEnd, options: options)
485512
mediaEvent.adBreak = self.adBreak
486513
self.logEvent(mediaEvent: mediaEvent)
487514
self.adBreak = nil
488515
}
516+
517+
// MARK: private helpers (ad break)
518+
private func pauseContentTimeIfAdBreakExclusionEnabled() {
519+
guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp != nil else { return }
520+
storedPlaybackTime += Date().timeIntervalSince(currentPlaybackStartTimestamp!)
521+
currentPlaybackStartTimestamp = nil
522+
}
523+
524+
private func resumeContentTimeIfAdBreakExclusionEnabled() {
525+
guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp == nil else { return }
526+
currentPlaybackStartTimestamp = Date()
527+
}
489528

490529
// MARK: ad content
491530
/// Indicates a given ad creative has started playing

mParticle-Apple-Media-SDK.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@
247247
};
248248
};
249249
buildConfigurationList = DBD815E22319A7F400A9809C /* Build configuration list for PBXProject "mParticle-Apple-Media-SDK" */;
250-
compatibilityVersion = "Xcode 9.3";
250+
compatibilityVersion = "Xcode 12.0";
251251
developmentRegion = en;
252252
hasScannedForEncodings = 0;
253253
knownRegions = (

mParticle-Apple-Media-SDKTests/mParticle_Apple_MediaTests.swift

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
7979
override func setUp() {
8080
// Put setup code here. This method is called before the invocation of each test method in the class.
8181
coreSDK = MParticle.sharedInstance()
82-
mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "12345", title: "foo title", duration: 90000, contentType: .video, streamType: .onDemand, logMPEvents: false, logMediaEvents: true, completeLimit: 90, testing: true)
82+
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)
8383
MPListenerController.sharedInstance().addSdkListener(self)
8484
}
8585

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

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

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

116-
mediaSession = MPMediaSession(coreSDK: coreSDK, mediaContentId: "678", title: "foo title 2", duration: 80000, contentType: .audio, streamType: .liveStream, logMPEvents: true, logMediaEvents: false, completeLimit: 90, testing: true)
117+
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)
117118
mediaSession?.mediaSessionAttributes = ["exampleKey1": "exampleValue1"]
118119

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

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

@@ -415,6 +418,73 @@ class mParticle_Apple_MediaTests: XCTestCase, MPListenerProtocol {
415418
mediaSession?.logAdBreakEnd()
416419
self.waitForExpectations(timeout: defaultTimeout, handler: nil)
417420
}
421+
422+
func testAdBreakExclusionDisabledDoesNotPauseContentTime() {
423+
let adBreak = MPMediaAdBreak(title: "foo adbreak title", id: "12345")
424+
mediaSession?.excludeAdBreaksFromContentTime = false
425+
426+
// Start content playback and accumulate 0.2s before the ad break
427+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0)
428+
mediaSession?.logPlay()
429+
Thread.sleep(forTimeInterval: 0.2)
430+
431+
XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
432+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.1)
433+
434+
// 0.2s should count toward content time.
435+
mediaSession?.logAdBreakStart(adBreak: adBreak)
436+
Thread.sleep(forTimeInterval: 0.2)
437+
438+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.1)
439+
440+
mediaSession?.logAdBreakEnd()
441+
XCTAssertNil(mediaSession?.adBreak)
442+
443+
mediaSession?.logPause()
444+
445+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.1)
446+
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)
447+
}
448+
449+
func testAdBreakExclusionEnabledExcludesAdTime() {
450+
let adBreak = MPMediaAdBreak(title: "foo adbreak title", id: "12345")
451+
mediaSession?.excludeAdBreaksFromContentTime = true
452+
453+
// Start content playback and accumulate 0.2s before the ad break
454+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0)
455+
mediaSession?.logPlay()
456+
Thread.sleep(forTimeInterval: 0.2)
457+
458+
XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
459+
XCTAssertEqual(mediaSession!.storedPlaybackTime, 0)
460+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.08)
461+
462+
// Start ad break content time should pause & be stored
463+
mediaSession?.logAdBreakStart(adBreak: adBreak)
464+
Thread.sleep(forTimeInterval: 0.2)
465+
466+
// Content tracking paused: playhead cleared, stored captured 0.2s
467+
XCTAssertEqual(mediaSession!.storedPlaybackTime, mediaSession!.mediaContentTimeSpent)
468+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.2, accuracy: 0.08)
469+
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)
470+
471+
// End ad break auto-resume content tracking
472+
mediaSession?.logAdBreakEnd()
473+
Thread.sleep(forTimeInterval: 0.2)
474+
475+
XCTAssertEqual(mediaSession!.storedPlaybackTime, 0.2, accuracy: 0.08)
476+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.4, accuracy: 0.08)
477+
XCTAssertNotNil(mediaSession?.currentPlaybackStartTimestamp)
478+
XCTAssertNil(mediaSession?.adBreak)
479+
480+
// Watch a bit more content after the ad
481+
Thread.sleep(forTimeInterval: 0.2)
482+
483+
mediaSession?.logPause()
484+
485+
XCTAssertEqual(mediaSession!.mediaContentTimeSpent, 0.6, accuracy: 0.08)
486+
XCTAssertNil(mediaSession?.currentPlaybackStartTimestamp)
487+
}
418488

419489
func testLogSegmentStart() {
420490
let segment = MPMediaSegment(title: "foo segment title", index: 3, duration: 30000)

0 commit comments

Comments
 (0)