Projects/VideoLooper/VideoLooper/QueuePlayerLooper.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
An object that uses AVQueuePlayer to loop a video. |
*/ |
import AVFoundation |
class QueuePlayerLooper : NSObject, Looper { |
// MARK: Types |
private struct ObserverContexts { |
static var playerStatus = 0 |
static var playerStatusKey = "status" |
static var currentItem = 0 |
static var currentItemKey = "currentItem" |
static var currentItemStatus = 0 |
static var currentItemStatusKey = "currentItem.status" |
static var urlAssetDurationKey = "duration" |
static var urlAssetPlayableKey = "playable" |
} |
// MARK: Properties |
private var player: AVQueuePlayer? |
private var playerLayer: AVPlayerLayer? |
private var isObserving = false |
private var numberOfTimesPlayed = 0 |
private let numberOfTimesToPlay: Int |
private let videoURL: URL |
// MARK: Looper |
required init(videoURL: URL, loopCount: Int) { |
self.videoURL = videoURL |
self.numberOfTimesToPlay = loopCount |
super.init() |
} |
func start(in parentLayer: CALayer) { |
stop() |
player = AVQueuePlayer() |
playerLayer = AVPlayerLayer(player: player) |
guard let playerLayer = playerLayer else { fatalError("Error creating player layer") } |
playerLayer.frame = parentLayer.bounds |
parentLayer.addSublayer(playerLayer) |
let videoAsset = AVURLAsset(url: videoURL) |
videoAsset.loadValuesAsynchronously(forKeys: [ObserverContexts.urlAssetDurationKey, ObserverContexts.urlAssetPlayableKey]) { |
/* |
The asset invokes its completion handler on an arbitrary queue |
when loading is complete. Because we want to access our AVQueuePlayer |
in our ensuing set-up, we must dispatch our handler to the main |
queue. |
*/ |
DispatchQueue.main.async(execute: { |
var durationError: NSError? |
let durationStatus = videoAsset.statusOfValue(forKey: ObserverContexts.urlAssetDurationKey, error: &durationError) |
guard durationStatus == .loaded else { fatalError("Failed to load duration property with error: \(durationError)") } |
var playableError: NSError? |
let playableStatus = videoAsset.statusOfValue(forKey: ObserverContexts.urlAssetPlayableKey, error: &playableError) |
guard playableStatus == .loaded else { fatalError("Failed to read playable duration property with error: \(playableError)") } |
guard videoAsset.isPlayable else { |
print("Can't loop since asset is not playable") |
return |
} |
guard CMTimeCompare(videoAsset.duration, CMTime(value:1, timescale:100)) >= 0 else { |
print("Can't loop since asset duration too short. Duration is(\(CMTimeGetSeconds(videoAsset.duration)) seconds") |
return |
} |
/* |
Based on the duration of the asset, we decide the number of player |
items to add to demonstrate gapless playback of the same asset. |
*/ |
let numberOfPlayerItems = (Int)(1.0 / CMTimeGetSeconds(videoAsset.duration)) + 2 |
for _ in 1...numberOfPlayerItems { |
let loopItem = AVPlayerItem(asset: videoAsset) |
self.player?.insert(loopItem, after: nil) |
} |
self.startObserving() |
self.numberOfTimesPlayed = 0 |
self.player?.play() |
}) |
} |
} |
func stop() { |
player?.pause() |
stopObserving() |
player?.removeAllItems() |
player = nil |
playerLayer?.removeFromSuperlayer() |
playerLayer = nil |
} |
// MARK: Convenience |
private func startObserving() { |
guard let player = player, !isObserving else { return } |
player.addObserver(self, forKeyPath: ObserverContexts.playerStatusKey, options: .new, context: &ObserverContexts.playerStatus) |
player.addObserver(self, forKeyPath: ObserverContexts.currentItemKey, options: .old, context: &ObserverContexts.currentItem) |
player.addObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, options: .new, context: &ObserverContexts.currentItemStatus) |
isObserving = true |
} |
private func stopObserving() { |
guard let player = player, isObserving else { return } |
player.removeObserver(self, forKeyPath: ObserverContexts.playerStatusKey, context: &ObserverContexts.playerStatus) |
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemKey, context: &ObserverContexts.currentItem) |
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, context: &ObserverContexts.currentItemStatus) |
isObserving = false |
} |
// MARK: KVO |
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
if context == &ObserverContexts.playerStatus { |
guard let newPlayerStatus = change?[.newKey] as? AVPlayerStatus else { return } |
if newPlayerStatus == AVPlayerStatus.failed { |
print("End looping since player has failed with error: \(player?.error)") |
stop() |
} |
} |
else if context == &ObserverContexts.currentItem { |
guard let player = player else { return } |
if player.items().isEmpty { |
print("Play queue emptied out due to bad player item. End looping") |
stop() |
} |
else { |
// If `loopCount` has been set, check if looping needs to stop. |
if numberOfTimesToPlay > 0 { |
numberOfTimesPlayed = numberOfTimesPlayed + 1 |
if numberOfTimesPlayed >= numberOfTimesToPlay { |
print("Looped \(numberOfTimesToPlay) times. Stopping."); |
stop() |
} |
} |
/* |
Append the previous current item to the player's queue. An initial |
change from a nil currentItem yields NSNull here. Check to make |
sure the class is AVPlayerItem before appending it to the end |
of the queue. |
*/ |
if let itemRemoved = change?[.oldKey] as? AVPlayerItem { |
itemRemoved.seek(to: kCMTimeZero) |
stopObserving() |
player.insert(itemRemoved, after: nil) |
startObserving() |
} |
} |
} |
else if context == &ObserverContexts.currentItemStatus { |
guard let newPlayerItemStatus = change?[.newKey] as? AVPlayerItemStatus else { return } |
if newPlayerItemStatus == .failed { |
print("End looping since player item has failed with error: \(player?.currentItem?.error)") |
stop() |
} |
} |
else { |
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) |
} |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13