Skip to content
This repository has been archived by the owner on Jan 16, 2023. It is now read-only.

Commit

Permalink
Merge pull request #374 from clappr/feature/new_quality_metrics
Browse files Browse the repository at this point in the history
New quality metrics
  • Loading branch information
paulogamatw authored Mar 19, 2020
2 parents 376f7c8 + 979daa4 commit 4d5c701
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 31 deletions.
31 changes: 23 additions & 8 deletions Sources/Clappr/Classes/Plugin/Playback/AVFoundationPlayback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ open class AVFoundationPlayback: Playback {
private var observers = [NSKeyValueObservation]()

private var lastLogEvent: AVPlayerItemAccessLogEvent? { player?.currentItem?.accessLog()?.events.last }

private var numberOfDroppedVideoFrames: Int? { lastLogEvent?.numberOfDroppedVideoFrames }

open var bitrate: Double? { lastLogEvent?.indicatedBitrate }
open var bandwidth: Double? { lastLogEvent?.observedBitrate }
open var averageBitrate: Double? { lastLogEvent?.averageVideoBitrate }
open var droppedFrames: Int? { lastLogEvent?.numberOfDroppedVideoFrames }
open var droppedFrames: Int = 0
open var decodedFrames: Int? { -1 }
open var domainHost: String? { asset?.url.host }

Expand Down Expand Up @@ -296,19 +297,19 @@ open class AVFoundationPlayback: Playback {
NotificationCenter.default.addObserver(
self,
selector: #selector(playbackDidEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem)

NotificationCenter.default.addObserver(
self,
selector: #selector(onAccessLogEntry),
name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
name: .AVPlayerItemNewAccessLogEntry,
object: nil)

NotificationCenter.default.addObserver(
self,
selector: #selector(onFailedToPlayToEndTime),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
name: .AVPlayerItemFailedToPlayToEndTime,
object: nil)
}

Expand All @@ -319,7 +320,13 @@ open class AVFoundationPlayback: Playback {
}

@objc func onAccessLogEntry(notification: NSNotification?) {
updateDroppedFrames()
updateBitrate()
}

private func updateBitrate() {
guard lastBitrate != bitrate else { return }

lastBitrate = bitrate
if let lastBitrate = lastBitrate, !lastBitrate.isNaN {
trigger(.didUpdateBitrate, userInfo: ["bitrate": lastBitrate])
Expand All @@ -328,8 +335,10 @@ open class AVFoundationPlayback: Playback {
}
}

private var hasEnoughBufferToPlay: Bool {
return player?.currentItem?.isPlaybackLikelyToKeepUp == true && state == .stalling
private func updateDroppedFrames() {
guard let numberOfDroppedVideoFrames = numberOfDroppedVideoFrames, numberOfDroppedVideoFrames > 0 else { return }

droppedFrames += numberOfDroppedVideoFrames
}

private func handlePlaybackLikelyToKeepUp(_ player: AVPlayer) {
Expand All @@ -339,7 +348,11 @@ open class AVFoundationPlayback: Playback {
play()
}
}


private var hasEnoughBufferToPlay: Bool {
return player?.currentItem?.isPlaybackLikelyToKeepUp == true && state == .stalling
}

private func handlePlaybackBufferEmpty(_ player: AVPlayer) {
guard state != .paused else { return }
updateState(.stalling)
Expand Down Expand Up @@ -442,6 +455,7 @@ open class AVFoundationPlayback: Playback {
guard didFinishedItem(from: notification) else { return }
trigger(.didComplete)
updateState(.idle)
droppedFrames = 0
}

open override func pause() {
Expand All @@ -467,6 +481,7 @@ open class AVFoundationPlayback: Playback {
playerLayer = nil
player?.replaceCurrentItem(with: nil)
player = nil
droppedFrames = 0
}

@objc var isReadyToPlay: Bool {
Expand Down
17 changes: 13 additions & 4 deletions Tests/Clappr_Tests/Classes/Mocks/PlayerItemMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import AVFoundation

class PlayerItemMock: AVPlayerItem {
private var itemAccessLog: AVPlayerItemAccessLog

init(accessLogEvent: AccessLogEventMock) {
private var isFinished: Bool
private var durationMocked: CMTime
private var currentTimeMocked: CMTime

override var duration: CMTime { durationMocked }

init(accessLogEvent: AccessLogEventMock, isFinished: Bool = false) {
self.itemAccessLog = PlayerItemAccessLogMock(accessLogEvent: accessLogEvent)

self.isFinished = isFinished
self.durationMocked = CMTimeMakeWithSeconds(100, preferredTimescale: Int32(NSEC_PER_SEC))
self.currentTimeMocked = CMTimeMakeWithSeconds(0, preferredTimescale: Int32(NSEC_PER_SEC))

super.init(asset: AVAsset(url: URL(string: "http://clappr.sample/master.m3u8")!), automaticallyLoadedAssetKeys: nil)
}


override func currentTime() -> CMTime { isFinished ? duration : currentTimeMocked }
override func accessLog() -> AVPlayerItemAccessLog? { itemAccessLog }
}
11 changes: 8 additions & 3 deletions Tests/Clappr_Tests/Classes/Mocks/PlayerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import AVFoundation

class PlayerMock: AVPlayer {
private var accessLogEvent: AccessLogEventMock

init(accessLogEvent: AccessLogEventMock) {
private var isFinished: Bool
private var playerItemMock: PlayerItemMock

init(accessLogEvent: AccessLogEventMock, isFinished: Bool = false) {
self.accessLogEvent = accessLogEvent
self.isFinished = isFinished
self.playerItemMock = PlayerItemMock(accessLogEvent: accessLogEvent, isFinished: isFinished)

super.init()
}

override var currentItem: AVPlayerItem? { PlayerItemMock(accessLogEvent: accessLogEvent) }
override var currentItem: AVPlayerItem? { playerItemMock }
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,123 @@ class AVFoundationPlaybackQualityMetricsTests: QuickSpec {
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

expect(avfoundationPlayback.bandwidth).toEventually(equal(13.0))
}
}

context("with new numberOfDroppedVideoFrames value") {
it("changes the droppedFrames") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(2)
let player = PlayerMock(accessLogEvent: accessLog)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

expect(avfoundationPlayback.droppedFrames).toEventually(equal(2))
describe("droppedFrames") {
context("with new numberOfDroppedVideoFrames value") {
it("changes the droppedFrames") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(2)
let player = PlayerMock(accessLogEvent: accessLog)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

expect(avfoundationPlayback.droppedFrames).toEventually(equal(2))
}

context("when occurs another AVPlayerItemNewAccessLogEntry event") {
it("changes the droppedFrames to an accumulated value") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(2)
let player = PlayerMock(accessLogEvent: accessLog)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

accessLog.setDroppedFrames(3)

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

expect(avfoundationPlayback.droppedFrames).toEventually(equal(5))
}

context("when value is negative") {
it("doesn't change the droppedFrames accumulated value") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(31)
let player = PlayerMock(accessLogEvent: accessLog)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

accessLog.setDroppedFrames(-1)

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

expect(avfoundationPlayback.droppedFrames).toEventually(equal(31))
}
}
}
}

context("when call stop") {
it("clears droppedFrames value") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(24)
let player = PlayerMock(accessLogEvent: accessLog)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()
NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: player.currentItem
)

avfoundationPlayback.stop()

expect(avfoundationPlayback.droppedFrames).toEventually(equal(0))
}
}

context("when playback did end") {
it("clears droppedFrames value") {
let avfoundationPlayback = AVFoundationPlayback(options: [:])
let accessLog = AccessLogEventMock()
accessLog.setDroppedFrames(76)
let player = PlayerMock(accessLogEvent: accessLog, isFinished: true)
avfoundationPlayback.player = player
avfoundationPlayback.addObservers()

NotificationCenter.default.post(
name: .AVPlayerItemNewAccessLogEntry,
object: nil
)

NotificationCenter.default.post(
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem
)

expect(avfoundationPlayback.droppedFrames).toEventually(equal(0))
}
}
}
}
Expand Down

0 comments on commit 4d5c701

Please sign in to comment.