AVPlayer seek right after it starts to play - ios

I need to jump to particular time in audiofile right after it starts to play. So I use seekToTime method with completion handler
avPlayer.play()
...
avPlayer?.seekToTime(jumpTime, completionHandler: { isComplete in
if isComplete {
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds((self.avPlayer!.currentItem?.currentTime())!)
}
})
The problem is that it needs time to start playing file from the internet. And for some reason the version of seekToTime with completion handler crashes the app, because it's invoked before avPlayer started to play. The version without completion handler works fine.
Error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'AVPlayerItem cannot service a seek request with a completion handler until its status is AVPlayerItemStatusReadyToPlay.'
Is there a way to use a callback with avPlayer.play()?

Yes, there is a way.
You need to observe changes in player:
avPlayer?.addPeriodicTimeObserverForInterval(CMTime(value: 1, timescale: 3), queue: dispatch_get_main_queue()) { [weak self] time in
self?.handleCallback(time)
}
And then you handle the state in the callback:
func handleCallback(time: CMTime) {
if avPlayer?.status == .ReadyToPlay {
// place your seek logic here
}
}

Please look at the Apple's answer for it:
https://developer.apple.com/library/content/qa/qa1820/_index.html
class MyClass {
var isSeekInProgress = false
let player = <#A valid player object #>
var chaseTime = kCMTimeZero
// your player.currentItem.status
var playerCurrentItemStatus:AVPlayerItemStatus = .Unknown
...
func stopPlayingAndSeekSmoothlyToTime(newChaseTime:CMTime)
{
player.pause()
if CMTimeCompare(newChaseTime, chaseTime) != 0
{
chaseTime = newChaseTime;
if !isSeekInProgress
{
trySeekToChaseTime()
}
}
}
func trySeekToChaseTime()
{
if playerCurrentItemStatus == .Unknown
{
// wait until item becomes ready (KVO player.currentItem.status)
}
else if playerCurrentItemStatus == .ReadyToPlay
{
actuallySeekToTime()
}
}
func actuallySeekToTime()
{
isSeekInProgress = true
let seekTimeInProgress = chaseTime
player.seekToTime(seekTimeInProgress, toleranceBefore: kCMTimeZero,
toleranceAfter: kCMTimeZero, completionHandler:
{ (isFinished:Bool) -> Void in
if CMTimeCompare(seekTimeInProgress, chaseTime) == 0
{
isSeekInProgress = false
}
else
{
trySeekToChaseTime()
}
})
}
}

Related

Why does Swift AVAudioPlayer not change state after MPRemoteCommandCenter play command?

The environment for this is iOS 13.6 and Swift 5. I have a very simple app that successfully plays an MP3 file in the foreground or background. I added MPRemoteCommandCenter play and pause command handlers to it. I play the sound file in the foreground and then pause it.
When I tap the play button from the lock screen, my code calls audioPlayer.play(), which returns true. I hear the sound start playing again, but the currentTime of the player does not advance. After that, the play and pause buttons on the lock screen do nothing. When I foreground the app again, the play button plays from where it was before I went to the lock screen.
Here is my AudioPlayer class:
import AVFoundation
import MediaPlayer
class AudioPlayer: RemoteAudioCommandDelegate {
var audioPlayer = AVAudioPlayer()
let remoteCommandHandler = RemoteAudioCommandHandler()
var timer:Timer!
func play(title: String) {
let path = Bundle.main.path(forResource: title, ofType: "mp3")!
let url = URL(fileURLWithPath: path)
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try AVAudioSession.sharedInstance().setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: url)
remoteCommandHandler.delegate = self
remoteCommandHandler.enableDisableRemoteCommands(true)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateNowPlayingInfo), userInfo: nil, repeats: true)
} catch let error as NSError {
print("error = \(error)")
}
}
func play() {
print ("audioPlayer.play() returned \(audioPlayer.play())")
}
func pause() {
audioPlayer.pause()
}
func stop() {
audioPlayer.stop()
}
func currentTime() -> TimeInterval {
return audioPlayer.currentTime
}
func setCurrentTime(_ time:TimeInterval) {
audioPlayer.currentTime = time
}
#objc func updateNowPlayingInfo() {
// Hard-code the nowPlayingInfo since this is a simple test app
var nowPlayingDict =
[MPMediaItemPropertyTitle: "Tin Man",
MPMediaItemPropertyAlbumTitle: "The Complete Greatest Hits",
MPMediaItemPropertyAlbumTrackNumber: NSNumber(value: UInt(10) as UInt),
MPMediaItemPropertyArtist: "America",
MPMediaItemPropertyPlaybackDuration: 208,
MPNowPlayingInfoPropertyPlaybackRate: NSNumber(value: 1.0 as Float)] as [String : Any]
nowPlayingDict[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: audioPlayer.currentTime as Double)
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingDict
}
}
Here is my RemoteCommandHandler class:
import Foundation
import MediaPlayer
protocol RemoteAudioCommandDelegate: class {
func play()
func pause()
}
class RemoteAudioCommandHandler: NSObject {
weak var delegate: RemoteAudioCommandDelegate?
var remoteCommandCenter = MPRemoteCommandCenter.shared()
var playTarget: Any? = nil
var pauseTarget: Any? = nil
func enableDisableRemoteCommands(_ enabled: Bool) {
print("Called with enabled = \(enabled)")
remoteCommandCenter.playCommand.isEnabled = enabled
remoteCommandCenter.pauseCommand.isEnabled = enabled
if enabled {
addRemoteCommandHandlers()
} else {
removeRemoteCommandHandlers()
}
}
fileprivate func addRemoteCommandHandlers() {
print( "Entered")
if playTarget == nil {
print( "Adding playTarget")
playTarget = remoteCommandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
print("addRemoteCommandHandlers calling delegate play")
self.delegate?.play()
return .success
}
}
if pauseTarget == nil {
print( "Adding pauseTarget")
pauseTarget = remoteCommandCenter.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
print("addRemoteCommandHandlers calling delegate pause")
self.delegate?.pause()
return .success
}
}
}
fileprivate func removeRemoteCommandHandlers() {
print( "Entered")
if playTarget != nil {
print( "Removing playTarget")
remoteCommandCenter.playCommand.removeTarget(playTarget)
playTarget = nil
}
if pauseTarget != nil {
print( "Removing pauseTarget")
remoteCommandCenter.pauseCommand.removeTarget(pauseTarget)
pauseTarget = nil
}
}
}
I will gladly supply further required info, because I'm baffled at why this relatively straightforward code (in my mind) code doesn't work.
Assistance is much appreciated!
After some more debugging, I found that the AVAudioPlayer started to play the sound from the lock screen, but stopped again shortly after.
I mitigated the problem by adding a Timer. The timer checks if the last command by the user was play, but the sound is not playing. I also change the status when the user selects pause or the song stops playing at its natural end.
I am still at a loss for an actual fix for this problem.

AVAudioPlayer 'forgets' that its playing when triggered by MPRemoteCommand

I'm trying to play an audiofile and control it's playback via the Remote Command Centre available on the lock screen.
If I do the following:
Begin playback
Pause playback
Lock device
Begin playback from lockscreen (MPRemoteCommandCenter)
It is then impossible to pause playback from lockscreen. The button flickers and nothing happens.
How can I fix this?
Further details below:
It appears that when attempting to pause the audio, the AVAudioPlayer returns 'false' for audioPlayer.isPlaying.
This occurs on iOS13.1 on my iPhoneXR, iPhoneSE and iPhone8. I have no other devices to test against
The logs indicate that AVAudioPlayer.isPlaying initially returns true when playback is started, but subsequently returns false. The player's currentTime also appears stuck at around the time playback was started.
My entire view controller is below (~100 lines). This is the minimum necessary to reproduce the problem.
The this example project demonstrating the error is also available on Github here.
import UIKit
import MediaPlayer
class ViewController: UIViewController {
#IBOutlet weak var playPauseButton: UIButton!
#IBAction func playPauseButtonTap(_ sender: Any) {
if self.audioPlayer.isPlaying {
pause()
} else {
play()
}
}
private var audioPlayer: AVAudioPlayer!
private var hasPlayed = false
override func viewDidLoad() {
super.viewDidLoad()
let fileUrl = Bundle.main.url(forResource: "temp/intro", withExtension: ".mp3")
try! self.audioPlayer = AVAudioPlayer(contentsOf: fileUrl!)
let audioSession = AVAudioSession.sharedInstance()
do { // play on speakers if headphones not plugged in
try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
} catch let error as NSError {
print("Override headphones failed, probably because none are available: \(error.localizedDescription)")
}
do {
try audioSession.setCategory(.playback, mode: .spokenAudio)
try audioSession.setActive(true)
} catch let error as NSError {
print("Warning: Setting audio category to .playback|.spokenAudio failed: \(error.localizedDescription)")
}
playPauseButton.setTitle("Play", for: .normal)
}
func play() {
playPauseButton.setTitle("Pause", for: .normal)
self.audioPlayer.play()
if(!hasPlayed){
self.setupRemoteTransportControls()
self.hasPlayed = true
}
}
func pause() {
playPauseButton.setTitle("Play", for: .normal)
self.audioPlayer.pause()
}
// MARK: Remote Transport Protocol
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) not playing: \(!self.audioPlayer.isPlaying)")
guard !self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to play")
let success = self.audioPlayer.play()
print("play() invoked with success \(success)")
print("now playing \(self.audioPlayer.isPlaying)")
return success ? .success : .commandFailed
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) playing: \(self.audioPlayer.isPlaying)")
guard self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to pause")
self.pause()
print("pause() invoked")
return .success
}
private func setupRemoteTransportControls() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget(self, action: #selector(self.handlePlay))
commandCenter.pauseCommand.addTarget(self, action: #selector(self.handlePause))
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Major title"
nowPlayingInfo[MPMediaItemPropertyTitle] = "Minor Title"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.audioPlayer.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
This logs the following (with my // comments added):
.......................
1.438140589569161 // audio was paused here
0x0000000283361cc0 not playing: true // player correctly says its not playing
attempting to play // so it'll start to play
play() invoked with success true // play() successfully invoked
now playing true // and the player correctly reports it's playing
.......................
1.4954875283446711 // The player thinks it's being playing for about half a second
0x0000000283361cc0 playing: false // and has now paused??? WTF?
.......................
1.4954875283446711 // but there's definitely sound coming from the speakers. It has **NOT** paused.
0x0000000283361cc0 playing: false // yet it thinks it's paused?
// note that the memory addresses are the same. This seems to be the same player. ='(
I'm at my wits' end. Help me StackOverflow—You're my only hope.
Edits: I've also tried
Always returning .success
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
guard !self.audioPlayer.isPlaying else { return .success }
self.audioPlayer.play()
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(self.audioPlayer.isPlaying)
guard self.audioPlayer.isPlaying else { return .success }
self.pause()
return .success
}
Ignoring the audioPlayer state and just doing as the remote command centre says
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.play()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.pause()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
Both of these result in the bug persisting the first time pause is tapped on the lock screen. Subsequent taps reset the audio to the original paused position and then work normally.
Update
Replacing AVAudioPlayer with AVPlayer appears to make the problem go away entirely! I'm reasonably sure this is a bug in AVAudioPlayer now.
The necessary steps to switch to AVPlayer are in this public diff
I've used one of my developer-question tickets and submitted this question to Apple. I'll post an answer when I hear back from them.
Update 2
Apple Dev Support confirmed that as of 5 Dec 2019 there's no known workaround for this issue. I've submitted an issue to feedbackassistant.apple.com and will update this answer when something changes.
This is indeed a bug in AVAudioPlayer.
Another workaround if you dont want to switch to AVPlayer is to simply check if playing before pausing and if not, call play just before pause. It's not pretty but it works:
if (!self.player.isPlaying) [self.player play];
[self.player pause];

How to handle network interruption in AVPlayer

I have using reachability to determine network change but once I determine I am trying to reload item but it is not working
// Check if the playback could keep up after a network interruption
private func checkNetworkInterruption() {
guard
let item = playerItem,
!item.isPlaybackLikelyToKeepUp,
reachability?.connection != .unavailable else { return }
self.player?.pause()
// Wait 1 sec to recheck and make sure the reload is needed
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
if !item.isPlaybackLikelyToKeepUp {
self.reloadItem()
}
self.isPlaying ? self.playSong() : self.player?.pause()
}
}
private func reloadItem() {
player?.replaceCurrentItem(with: nil)
player?.replaceCurrentItem(with: playerItem)
}
AVPlayer can handle the network interrupts by itself, when network fails, the video is paused and keep in buffering status when network is stable, the video is resumed.
but if you need to handle the network interruption, you can try:
private func checkNetworkInterruption() {
guard
let item = playerItem,
!item.isPlaybackLikelyToKeepUp,
reachability?.connection != .unavailable else { return }
self.player?.pause()
// Wait 1 sec to recheck and make sure the reload is needed
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
if !item.isPlaybackLikelyToKeepUp {
//you can create a new AVItem here
player?.replaceCurrentItem(with: item)
}
self.isPlaying ? self.playSong() : self.player?.pause()
}
}

AVPlayer state listener in swift

I want to know how to get the state of my player (AVPlayer) (buffering, playing, stopped, error) and update the ui according to those states (including the player on the lock screen). How exactly should I do it?
I have a label that may contain:
"Buffering...", "Playing", "Stopped" or "Error".
Basically, I have the following:
MediaPlayer:
import Foundation
import AVFoundation
class MediaPlayer {
static let sharedInstance = MediaPlayer()
fileprivate var player = AVPlayer(url: URL(string: "my_hls_stream_url_here")!)
fileprivate var isPlaying = false
func play() {
player.play()
isPlaying = true
}
func pause() {
player.pause()
isPlaying = false
}
func toggle() {
if isPlaying == true {
pause()
} else {
play()
}
}
func currentlyPlaying() -> Bool {
return isPlaying
}
}
PlayerViewController:
class PlayerViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBAction func playStopButtonAction(_ sender: UIButton) {
MediaPlayer.sharedInstance.toggle()
}
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Disconnected"
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
print("Audio session ok\n")
} catch {
print("Error: Audio session.\n")
}
// Show only play/pause button on the lock screen
if #available(iOS 9.1, *) {
let center = MPRemoteCommandCenter.shared()
[center.previousTrackCommand, center.nextTrackCommand, center.seekForwardCommand, center.seekBackwardCommand, center.skipForwardCommand, center.skipBackwardCommand, center.ratingCommand, center.changePlaybackRateCommand, center.likeCommand, center.dislikeCommand, center.bookmarkCommand, center.changePlaybackPositionCommand].forEach {
$0.isEnabled = false
}
center.togglePlayPauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
MediaPlayer.sharedInstance.toggle()
return MPRemoteCommandHandlerStatus.success
}
center.playCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
MediaPlayer.sharedInstance.play()
return MPRemoteCommandHandlerStatus.success
}
center.pauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
MediaPlayer.sharedInstance.pause()
return MPRemoteCommandHandlerStatus.success
}
} else {
// Fallback on earlier versions
print("Error (MPRemoteCommandCenter)")
}
}
override func remoteControlReceived(with event: UIEvent?) {
guard let event = event else {
print("No event\n")
return
}
guard event.type == UIEventType.remoteControl else {
print("Another event received\n")
return
}
switch event.subtype {
case UIEventSubtype.remoteControlPlay:
print("'Play' event received\n")
case UIEventSubtype.remoteControlPause:
print("'Pause' event received\n")
case UIEventSubtype.remoteControlTogglePlayPause:
print("'Toggle' event received\n")
default:
print("\(event.subtype)\n")
}
}
}
I think you could use the timeControlStatus property of AVPlayer. According to the doc it can be paused, waitingToPlayAtSpecifiedRate which is basically what you call buffering or playing.
If you really need the error state, you could observe the error property or whether the status property is set to failed.
A simple KVO observer on these properties would do the trick.
A place to start could be through using the AVPlayer's "status" property. It is an enumeration that contains the following values (this is taken directly from the documentation):
'unknown': Indicates that the status of the player is not yet known because it has not tried to load new media resources for playback.
'readyToPlay': Indicates that the player is ready to play AVPlayerItem instances.
'failed': Indicates that the player can no longer play AVPlayerItem instances because of an error.
As to how you could tell if the content is actually playing, you could just use boolean checks as it seems you have partially implemented. For pausing and stopping, you could just keep the file loaded for pause, and delete the file for stop that way you could differentiate the two.
For buffering, if the enum is not unknown or readyToPlay then that theoretically should mean that there is a file being attached but is not quite ready to play (i.e. buffering).

How to use thread to play next song by MPRemoteCommandCenter?

I want to use MPRemoteCommandCenter to control my music player app. And now it can play and pause music, but can not play next/previous song, only a poor chance can make it.
When user tap next song button in MPRemoteCommandCenter(e.g in the lock screen), it will call startExtendBGJob() function, then I ask for a thread to do the change song job(I think the bug is here, because I'm don't totally understand the background job's anatomy).
func startExtendBGJob(taskBlock: #escaping () -> Void) {
registerBackgroundTask()
DispatchQueue.global(qos: .userInitiated).async {
DLog("APP into BG")
DispatchQueue.global(qos: .default).async {
taskBlock()
}
while self.isPlaying == false || self.tmpPlayer == nil { // waiting for new avplayer been created.
Thread.sleep(forTimeInterval: 1)
}
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 10) {
self.endBackgroundTask()
}
}
}
func registerBackgroundTask() {
bgIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.endBackgroundTask()
})
assert(bgIdentifier != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(bgIdentifier)
bgIdentifier = UIBackgroundTaskInvalid
isExtendingBGJob = false
DLog("App exit BG!")
}
In startNextPlay() function just finding the next song's url, and prepareToPlay() is for creating a new AVPlayer to play next song.
self.tmpPlayer = AVPlayer(url: streamURL)
I'm not english native spearker, thank you so much to read here if you understand what I'm talking about :]. Any help is welcome.
Sorry, It's my fault. It doesn't need any backgournd job.
Just change something like following:
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
DLog("fail to set category: \(error)")
}
Make sure it's AVAudioSessionCategoryPlayback

Resources