AVPlayer Item not starting during UI text on Travis - ios

If I've an AVPlayerItem its status is never reaching .readyToPlay on the Travis VM while running a UI test. Everything works fine locally.
I've set up simple repro:
https://travis-ci.org/gsabran/TestAVItemStatus
https://github.com/gsabran/TestAVItemStatus
This makes my tests fail on Travis because some events are only getting triggered once the video item is ready to play.
Here is my application (a single view controller). Basically it just loads a local video and changes the UI when the video starts playing.
override func viewDidLoad() {
super.viewDidLoad()
item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "video", ofType: "mp4")!))
player = AVPlayer(playerItem: item)
item.addObserver(
self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.initial, .old, .new],
context: nil)
if player.currentItem?.status == .readyToPlay {
videoDidLoad()
}
player.play()
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard let item = object as? AVPlayerItem else { return }
if item.status == .readyToPlay {
DispatchQueue.main.async {
self.videoDidLoad()
}
}
}
func videoDidLoad() {
// Not triggered on Travis VM, while working fine on local simulator and device
label.text = "video ready"
}
Here is the test, simply checking for a label after a while:
class TestAVItemStatusUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
func testExample() {
sleep(5)
if !XCUIApplication().staticTexts["video ready"].exists {
XCTFail()
}
}
}
It's not working on Travis.

Related

AVPlayer.status doesn't run when wrapped in a DispatchWorkItem with a Delay

Because I'm playing videos in cells I have an AVPlayer that plays videos in certain circumstances immediately and others it runs a few seconds later. When it runs immediately the .status works fine. But when I wrap it in a DispatchWorkItem with a .asyncAfter delay that same exact .status is never called. I also tried to use a perform(_:, with:, afterDelay:) and a Timer but this didn't work either.
var player: AVPlayer?
var playerItem: AVPlayerItem?
var observer: NSKeyValueObservation? = nil
var workItem: DispatchWorkItem? = nil
var startImmediately = false
var timer: Timer?
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
}
func someCircumstance() {
if startImmediately {
setNSKeyValueObserver() // this works fine and .status is called
} else {
createWorkItem()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.33, execute: executeWorkItem) // this delay runs but .status is never called
// perform(#selector(executeWorkItem), with: nil, afterDelay: 0.33) // same issue
// timer = Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(executeWorkItem), userInfo: nil, repeats: false) // same issue
// RunLoop.current.add(timer!, forMode: .common)
}
}
func createWorkItem() {
workItem = DispatchWorkItem {
DispatchQueue.main.async { [weak self] in
self?.setNSKeyValueObserver()
}
}
}
#objc func executeWorkItem() {
guard let workItem = workItem else { return }
workItem.perform()
}
func setNSKeyValueObserver() {
// without a long explanation this sometimes has to start with a delay because of scrolling reason I also might have to cancel it
observer = player?.observe(\.status, options: [.new, .old]) { [weak self] (player, change) in
switch (player.status) {
case .readyToPlay:
print("Media Ready to Play")
case .failed, .unknown:
print("Media Failed to Play")
#unknown default:
print("Unknown Error")
}
}
}
I also tried to use the older KVO API .status observer instead but the same issue occurred when using a delay
private var keepUpContext = 0
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
player?.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp", options: [.old, .new], context: &keepUpContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &keepUpContext {
if let player = player, let item = player.currentItem, item.isPlaybackLikelyToKeepUp {
print("Media Ready to Play")
}
}
}
The problem seems to be the delay.
Update
I just tried the following code and and this doesn't work either:
if startImmediately {
setNSKeyValueObserver()
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
self?.setNSKeyValueObserver()
}
}
It looks like you're adding your observer after the player has loaded the content. It likely loads between the viewDidLoad and the delay. If you add .initial to the list of options when adding the observer, you'll be sure to get the state notification even if the player is already ready.
Can you try to change the forKeyPath parameter value to #keyPath(AVPlayerItem.status)

Swift AVAudioPlayer's playing status always be true?

Here's my code:
#objc func playSmusic1() {
guard let url = Bundle.main.url(forResource: "Snote6", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
while (true) {
player.play()
player.enableRate = true;
player.rate = playrate
if !player.isPlaying {
break
}
}
} catch let error {
print(error.localizedDescription)
}
}
I found player.isPlaying properties always be true, so sometimes a tone will be play for 2 or 3 times. How to fix this bug? Thanks a lot!
First: Don't use while(true) for checking because it blocks the main thread!
You should add KVO observer to check this property asynchronously e.g.:
class YourController : UIViewController {
var player: AVAudioPlayer?
//...
deinit {
player?.removeObserver(self, forKeyPath: #keyPath(AVAudioPlayer.isPlaying))
}
func play() {
//...
player?.addObserver(self,
forKeyPath: #keyPath(AVAudioPlayer.isPlaying),
options: .new,
context: nil)
player?.play()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVAudioPlayer.isPlaying) {
if let isPlaying = player?.isPlaying {
print(isPlaying)
}
}
else {
self.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

AVPlayer doesn't play video until a video from UIImagePickerController is played

I'm having a problem which I haven't seen a post like it on here. I have an AVPlayerViewController which plays a video-based off the path from my Firebase Database (not Storage). The video plays perfectly as I want it to, but only once I watch a video that is clicked on in the UIImagePickerController elsewhere in the app.
So for example, the AVPlayer will show a black background (this occurs with all of the AVPlayer's in the app), except for when I watch a video from the UIImagePickerController which has nothing to do with any of the other views. I have no clue where to start with this. I appreciate all your help and suggestions!
Here is the example code of my AVPlayerViewController:
import UIKit
import AVFoundation
import AVKit
class VideoView: UIViewController {
private var videoURL: URL
var player: AVPlayer?
var playerController : AVPlayerViewController?
init(videoURL: URL) {
self.videoURL = videoURL
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.gray
player = AVPlayer(url: videoURL)
playerController = AVPlayerViewController()
guard player != nil && playerController != nil else {
return
}
playerController!.showsPlaybackControls = false
playerController!.player = player!
self.addChild(playerController!)
self.view.addSubview(playerController!.view)
playerController!.view.frame = view.frame
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player!.currentItem)
let cancelButton = UIButton(frame: CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0))
cancelButton.setImage(#imageLiteral(resourceName: "cancel"), for: UIControl.State())
cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside)
view.addSubview(cancelButton)
// Allow background audio to continue to play
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
} catch let error as NSError {
print(error)
}
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch let error as NSError {
print(error)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.play()
}
#objc func cancel() {
dismiss(animated: true, completion: nil)
}
#objc fileprivate func playerItemDidReachEnd(_ notification: Notification) {
if self.player != nil {
self.player!.seek(to: CMTime.zero)
self.player!.play()
}
}
}
Did you check the size of child controller view?
playerController!.view.frame = view.frame
Put it into
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerController!.view.frame = view.frame
}
or use constraints.
The problem is that when your view appears it might not be ready for playback. What you should do to properly handle this is to KVO on the player to make sure it is ready to play.
I have answered this before here for a similar question:
Video playback issues only on iOS 13 with AVPlayerViewController and AVPlayer when using HLS video
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}
}
}
You can find documentation about it from Apple here: https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/responding_to_playback_state_changes
Another example of usage is here:
playerItem?.observe(\AVPlayerItem.status, options: [.new, .initial]) { [weak self] item, _ in
guard let self = self else { return }
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}

How to set AVQuePlayer to the begining after it finishes playing all in Que

I have an AVQuePlayer that is downloading videos a user posts to firebase and then playing them in order via an AVQuePlayer. Everything works fine until the video que plays all vidoes, in which case, if you try and replay the que, a SIGABRT 1 error occurs. I believe this is due to the fact that it is looking for videos to play but none are present. I have already tried to reset the playerItem array after the que finishes but it has come with no luck. Here is what I have so far.
Loads data from Firebase:
func loadStory() {
DataService.ds.REF_POSTS.observe(.value, with: { (snapshot) in
self.posts = []
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
if let dict = snap.value as? Dictionary<String, Any> {
let key = snap.key
let post = HotPosts(postID: key, postData: dict)
self.posts.append(post)
let media = post.mediaURL
let ref = Storage.storage().reference(forURL: media)
ref.downloadURL(completion: { (url, error) in
if error != nil {
print(error!)
} else {
self.videos.append(url!)
}
})
}
}
}
})
}
Downloads the urls:
func playStory() {
for vid in videos{
asset = AVAsset(url: vid)
let assetKeys = ["playable", "hasProtectedContent"]
let item = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: assetKeys)
item.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: &myContext)
self.playerItem.append(item)
}
player = AVQueuePlayer(items: playerItem)
print(playerItem.count)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
}
Handles buffering and plays video when ready:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("playing1")
guard context == &myContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
print("playing2")
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
print("playing3")
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
switch status {
case .readyToPlay:
print("We ARE IN THE CLEAR BOIIIIII")
player.play()
break
case .failed:
print("ITem Fialed")
break
case .unknown:
print("Could Not play the video")
break
}
}
}
Try subscribing to this notification and see if it works correctly for your situation:
NotificationCenter.default.addObserver(self, selector: #selector(endOfAudio), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
I also found this in a piece of Apple code, which illustrates the use of some KVO on avQueuePlayer ( https://developer.apple.com/library/content/samplecode/avloopplayer/Listings/Projects_VideoLooper_VideoLooper_QueuePlayerLooper_swift.html )
/*
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.
*/
// 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)
}
}
}
The problem with AVQueuePlayer is that it removes the AVPlayerItems that have finished playing. An option might be to create a copy of your AVQueuePlayer when your QueuePlayer is completely done playing, then you can just play your copy and make a new copy to play again once that one also finishes.
You could also maintain all your AVPlayerItems in an array and load up a new AVQueuePlayer whenever it's needed.
There is a project on github that might also be helpful, which extends AVQueuePlayer and maintains previous playerItems that have been played on your Queue Player. It can be found through: https://github.com/dgiovann/AVQueuePlayerPrevious

Record audio from an audio stream

I'm building an app that plays audio streams. Below is the code I have.
class ViewController: UIViewController {
private var player: AVPlayer!
private var kvoContext: UInt8 = 1
override func viewDidLoad() {
super.viewDidLoad()
// Streaming
let streamURL = NSURL(string: "http://adsl.ranfm.lk/")!
player = AVPlayer(URL: streamURL)
}
#IBAction func didTapPlayButton(sender: UIButton) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerDidReachEnd(_:)), name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
player.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &kvoContext)
player.play()
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &kvoContext {
print("Change at \(keyPath) for \(object)")
}
if player.status == .Failed {
print("AVPlayer Failed")
}
if player.status == .ReadyToPlay {
print("Ready to play")
player.play()
}
if player.status == .Unknown {
print("AVPlayer Unknown")
}
}
func playerDidReachEnd(notification: NSNotification) {
print("Reached the end of file")
}
}
This plays the stream successfully. Now I want to add the ability to record the audio from this stream.
Based on what I could find, the AVAudioRecorder class is only for recording audio through the microphone. Is there a way to record audio from a stream?

Resources