avoid Headset plugout stops AVAudioPlayer in iOS - iphone

In my iPhone app I am using AVAudioPlayer to play the songs...But when I plug out or plugin the headset during song playing, It automatically stops the AVAudioPlayer... I need to run audio player even though these changes occur.. any ideas will be appreciated.Thanks in advance.

First, you have to tell AVAudioSession the audio behaviour of your app. Apple name this the audio session category, an can be set by
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:&setCategoryErr];
For example, AVAudioSessionCategoryPlayback :
When using this category, your app audio continues with the Silent switch set to silent or when the screen locks. (The switch is called the Ring/Silent switch on iPhone.)
This category normally prevents audio from other apps from mixing with your app's audio. To allow mixing for this category, use the kAudioSessionProperty_OverrideCategoryMixWithOthers property.
Then, once the audio session set, the app will respond to some audio notifications, like AVAudioSessionInterruptionNotification or AVAudioSessionRouteChangeNotification
To answer, the original question, AVAudioSessionRouteChangeNotification is called when the audio route has been changed (ex: headset plug-out/plug-in, but also bluetooth device turning off, ...). With a bit of code, we can find the route change reason. And, in our case, start the player again il the headset has been unplugged.
- (void)viewDidLoad {
[super viewDidLoad];
NSError *setCategoryErr;
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:&setCategoryErr];
// Detects when the audio route changes (ex: jack unplugged)
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(audioHardwareRouteChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
// Don't forget to remove notification in dealloc method!!
}
- (void)audioHardwareRouteChanged:(NSNotification *)notification {
NSInteger routeChangeReason = [notification.userInfo[AVAudioSessionRouteChangeReasonKey] integerValue];
if (routeChangeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
// if we're here, the player has been stopped, so play again!
[self.player play];
}
}
To conclude, also think about a user, in a boring meeting, who accidentaly plug-out his headset. He would not have this kind of behaviour, whose would make the device suddently scream in the room!

Swift 3
Setup your player - play audio (even on silent mode) and silence other music / podcasts:
let audioSession = AVAudioSession.sharedInstance()
_ = try? audioSession.setCategory(AVAudioSessionCategoryPlayback, with: .duckOthers)
_ = try? audioSession.setActive(true)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(audioRouteChanged), name: .AVAudioSessionRouteChange, object: nil)
Route change observer (fix for unplugging headphones during playback):
func audioRouteChanged(note: Notification) {
if let userInfo = note.userInfo {
if let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int {
if reason == AVAudioSessionRouteChangeReason.oldDeviceUnavailable.rawValue {
// headphones plugged out
player.play()
}
}
}
}
Swift 2
let audioSession = AVAudioSession.sharedInstance()
_ = try? audioSession.setCategory(AVAudioSessionCategoryPlayback, withOptions: .DuckOthers)
_ = try? audioSession.setActive(true)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(audioRouteChanged), name: AVAudioSessionRouteChangeNotification, object: nil)
Route change observer:
func audioRouteChanged(note: NSNotification) {
if let userInfo = note.userInfo {
if let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int {
if reason == AVAudioSessionRouteChangeReason.OldDeviceUnavailable.rawValue {
// headphones plugged out -> continue playback
player.play()
}
}
}
}

I know this is old post but i did some research about this. #Martin answer was correct and i am using NSNotificationCenter but i am using Swift3 so these are things you can get from notification.userInfo
case AVAudioSessionInterruptionNotificationKey
/* value is an NSNumber representing an AVAudioSessionInterruptionType */
case AVAudioSessionInterruptionOptionsKey */
/* Only present for end interruption events. Value is of type AVAudioSessionInterruptionOptions.*/
case AVAudioSessionRouteChangeReasonKey */
/* value is an NSNumber representing an AVAudioSessionRouteChangeReason */
case unknown
case newDeviceAvailable
case oldDeviceUnavailable
case categoryChange
case override
case wakeFromSleep
case noSuitableRouteForCategory
case routeConfigurationChange
case AVAudioSessionRouteChangePreviousRouteKey * */
/* value is AVAudioSessionRouteDescription * */
case input
case output
case AVAudioSessionSilenceSecondaryAudioHintTypeKey */
/* value is an NSNumber representing an AVAudioSessionSilenceSecondaryAudioHintType */
Here is method in swift3
func audioSessionRouteChange(notification: NSNotification) {
if let userInfo = notification.userInfo {
print("Notification: AVAudioSessionInterruptionTypeKey = \(userInfo[AVAudioSessionInterruptionTypeKey])")
print("Notification: AVAudioSessionInterruptionOptionKey = \(userInfo[AVAudioSessionInterruptionOptionKey])")
print("Notification: AVAudioSessionSilenceSecondaryAudioHintTypeKey = \(userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey])")
if let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int {
print("Notification: AVAudioSessionRouteChangeReasonOldDeviceUnavailable")
if reason == AVAudioSessionRouteChangeReason.oldDeviceUnavailable.hashValue {
print("Notification: Headphones out")
}
if reason == AVAudioSessionRouteChangeReason.newDeviceAvailable.hashValue {
print("Notification: Headphones in")
}
}
if let description = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
// here you can check previous input and output
// po description.outputs[0].portType == AVAudioSessionPortBuiltInSpeaker
print("Notification: AVAudioSessionRouteChangePreviousRouteKey Inputs: \(description.inputs)")
print("Notification: AVAudioSessionRouteChangePreviousRouteKey Outputs: \(description.outputs)")
}
}
}

#Martin is almost right except when we get AVAudioSessionRouteChangeNotification notification, the audio may still play, you must check player's rate property. If it is zero, play it, otherwise you should observe rate, when it change to zero, play it. Check the link
Another note is that AVAudioSessionRouteChangeNotification is posted on a background thread (not main thread), you should dispatch it to the main thread if needed.

This is the best tutorial dealing this issue: (working well also on iOS7)
http://www.techotopia.com/index.php/Detecting_when_an_iPhone_Headphone_or_Docking_Connector_is_Unplugged_(iOS_4)

thanks to #budidino Swift 5 and above
let audioSession = AVAudioSession.sharedInstance()
_ = try? audioSession.setCategory(AVAudioSession.Category.playback, options: .duckOthers)
_ = try? audioSession.setActive(true)
NotificationCenter.default.addObserver(self, selector: #selector(audioRouteChanged), name: AVAudioSession.routeChangeNotification, object: nil)
#objc func audioRouteChanged(note: Notification) {
if let userInfo = note.userInfo {
if let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int {
if reason == AVAudioSession.RouteChangeReason.oldDeviceUnavailable.rawValue {
// headphones plugged out
self.avPlayer?.play()
}
}
}
}

I found the answer.
Just we have to Import the followings
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVAudioPlayer.h>
and write this code
//Play the Event in Background
NSError *setCategoryErr = nil;
NSError *activationErr = nil;
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error: &setCategoryErr];
[[AVAudioSession sharedInstance] setActive: YES error: &activationErr];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
UIBackgroundTaskIdentifier newTaskId = UIBackgroundTaskInvalid;
newTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
Now its continuously playing even I plugged in&out the Ear phone.

Related

Playing one sound instance at a time

I have an application that is constantly receiving integer data from a bluetooth sensor and I made it so that if the integer is less than 50, then it should play the MP3.
The problem is that the sensor is very rapidly checking and sending the integers, which is resulting in too many audio instances, basically the the mp3 file is being played too many times at the same time. How can I have it so that it finishes the audio before starting again?
This is the main code:
var player: AVAudioPlayer?
if let unwrappedString = Reading {
let optionalInt = Int(unwrappedString)
if let upwrappedInt = optionalInt {
if(upwrappedInt < 50){
DispatchQueue.global(qos: .background).async {
self.playSound()
}
}
}
}
Sound function:
func playSound() {
guard let url = Bundle.main.url(forResource: "beep1", withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
If the audio player is already playing (isPlaying), don't start playing!
https://developer.apple.com/reference/avfoundation/avaudioplayer/1390139-isplaying
I believe AVAudioPlayer has a delegate method to check if the audio has finished playing:
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
// ----------------------------------------------
// set your custom boolean flag 'isPlayingAudio'
// to false so you can play another audio again
// ----------------------------------------------
}
...
-(void)monitorBluetoothNumber
{
if(bluetoothNumber < 50 && !self.isPlayingAudio)
{
[self playMusic];
self.isPlayingAudio = YES;
}
}
You'll need to setup your audio player and set its delegate obviously.
The code is Objective C but you can easily adapt to Swift.

Headphones plugin/out detection in Swift

im working on an iphone app for iOS 8.1 that works with core audio to generate frequencies and adjust intensities. In the view controller that i generate the frequencies i need to control if the headphones are plugged out in some moment, i'm already controlling if headphones are connected before proceed to my frequencies generator view with the following function:
- (BOOL)isHeadsetPluggedIn {
AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription* desc in [route outputs]) {
if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones])
return YES;
}
return NO;
}
this function is in C because im working with core-audio to generate the frequencies, but in the view controllers im working with swift so a need a way to implement a listener to detect the headphones plug-out event and return to the user to the previous view, i don't know if i can use my function isHeadsetPluggedin() with an event listener or i should make a new one.
In my MenuViewController i control if the headphones are plugged in using the following function:
func isHeadsetPluggedIn() -> Bool {
return freqController.isHeadsetPluggedIn();
}
In Swift 4
func activateHeadPhonesStatus(){
NotificationCenter.default.addObserver(self, selector: #selector(audioRouteChangeListener(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
}
#objc func audioRouteChangeListener(_ notification:Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
headphonesConnected = true
print("headphone plugged in")
break
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
headphonesConnected = false
print("headphone pulled out")
break
}
}
default: ()
}
}
You can track the route changes by observing AVAudioSessionRouteChangeNotification notification.
//Observe for route changing notification
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
-(void)handleRouteChange:(NSNotification *)notif
{
NSDictionary *dict = notif.userInfo;
AVAudioSessionRouteDescription *routeDesc = dict[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *prevPort = [routeDesc.outputs objectAtIndex:0];
if ([prevPort.portType isEqualToString:AVAudioSessionPortHeadphones]) {
//Head phone removed
}
}
This article worked for me. There is also a GitHub repo with solution. If you don't want to read, here is my code:
Put this in your INIT method:
self.session = AVAudioSession.sharedInstance()
let currentRoute = self.session.currentRoute
if currentRoute.outputs.count != 0 {
for description in currentRoute.outputs {
if description.portType == AVAudioSessionPortHeadphones {
print("headphone plugged in")
} else {
print("headphone pulled out")
}
}
} else {
print("requires connection to device")
}
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(YOUR_VIEW_CONTROLLER_OR_VIEW.audioRouteChangeListener(_:)),
name: AVAudioSessionRouteChangeNotification,
object: nil)
And put this anywhere in your class:
dynamic private func audioRouteChangeListener(notification:NSNotification) {
let audioRouteChangeReason = notification.userInfo![AVAudioSessionRouteChangeReasonKey] as! UInt
switch audioRouteChangeReason {
case AVAudioSessionRouteChangeReason.NewDeviceAvailable.rawValue:
print("headphone plugged in")
case AVAudioSessionRouteChangeReason.OldDeviceUnavailable.rawValue:
print("headphone pulled out")
default:
break
}
}
Take care!

How to get notification for audio streaming status from AVPlayer?

I am using AVPlayer to stream some live HTTP audio, not AVAudioPlayer which does not support live HTTP audio streaming, the question is, how do I get the status of current playback? For example:
Tap Play Button -> [Loading] -> [Playing]
Tap Pause Button -> [Paused]
I need to show a spinner when loading, show a pause button when playing and show a play button when paused, I know I can observe the 'status' and 'rate' properties of AVPlayer:
rate:
the current rate of playback. 0.0 means “stopped”, 1.0 means “play at the natural rate of the current item”.
status:
Indicates whether the player can be used for playback.
AVPlayerStatusUnknown,
AVPlayerStatusReadyToPlay,
AVPlayerStatusFailed
so there is no way to indicate the audio is "LOADING", and after the status changes to AVPlayerStatusReadyToPlay, it still takes some time to have the audio playing(maybe because it is a live audio).
But anyway, how do I get the correct status of current playback? I know there is an AudioStream from Matt, but it does not support HTTP Live audio.
Thanks very much!
I used
[self.mPlayerItem addObserver:self
forKeyPath:kStatusKey
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:AVPlayerDemoPlaybackViewControllerStatusObservationContext];
to monitor the status key ("status"). Then I created the player
[self setPlayer:[AVPlayer playerWithPlayerItem:self.mPlayerItem]];
And in the observeValueForKeyPath
if (context == AVPlayerDemoPlaybackViewControllerStatusObservationContext)
{
AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
switch (status)
{
/* Indicates that the status of the player is not yet known because
it has not tried to load new media resources for playback */
case AVPlayerStatusUnknown:
{
[lblvalidation setText:#"Loading..."];
NSLog(#"AVPlayerStatusUnknown");
}
break;
case AVPlayerStatusReadyToPlay:
{
/* Once the AVPlayerItem becomes ready to play, i.e.
[playerItem status] == AVPlayerItemStatusReadyToPlay,
its duration can be fetched from the item. */
NSLog(#"AVPlayerStatusReadyToPlay");
[self.player play];
[lblvalidation setText:#"Playing..."];
}
break;
case AVPlayerStatusFailed:
{
[lblvalidation setText:#"Error..."];
NSLog(#"AVPlayerStatusFailed");
}
break;
}
}
this works for me... I hope it help for you.
Updated for Swift 2:
private var AVPlayerDemoPlaybackViewControllerStatusObservationContext = 0
Add observer:
player.currentItem!.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &AVPlayerDemoPlaybackViewControllerStatusObservationContext)
Observer
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &AVPlayerDemoPlaybackViewControllerStatusObservationContext {
if let change = change as? [String: Int]
{
let status = change[NSKeyValueChangeNewKey]!
switch status {
case AVPlayerStatus.Unknown.rawValue:
print("The status of the player is not yet known because it has not tried to load new media resources for playback")
case AVPlayerStatus.ReadyToPlay.rawValue:
self.playButtonPressed(playButton)
print("The player is Ready to Play")
case AVPlayerStatus.Failed.rawValue:
print("The player failed to load the video")
default:
print("Other status")
}
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}

Knowing when AVPlayer object is ready to play

I'm trying to play an MP3 file that is passed to an UIView from a previous UIView (stored in a NSURL *fileURL variable).
I'm initializing an AVPlayer with:
player = [AVPlayer playerWithURL:fileURL];
NSLog(#"Player created:%d",player.status);
The NSLog prints Player created:0, which i figured means it is not ready to play yet.
When i click the play UIButton, the code i run is:
-(IBAction)playButtonClicked
{
NSLog(#"Clicked Play. MP3:%#",[fileURL absoluteString]);
if(([player status] == AVPlayerStatusReadyToPlay) && !isPlaying)
// if(!isPlaying)
{
[player play];
NSLog(#"Playing:%# with %d",[fileURL absoluteString], player.status);
isPlaying = YES;
}
else if(isPlaying)
{
[player pause];
NSLog(#"Pausing:%#",[fileURL absoluteString]);
isPlaying = NO;
}
else {
NSLog(#"Error in player??");
}
}
When i run this, I always get Error in player?? in the console.
If i however replace the if condition that checks if AVPlayer is ready to play, with a simple if(!isPlaying)..., then the music plays the SECOND TIME I click on the play UIButton.
The console log is:
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 0**
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Pausing:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
2011-03-23 11:06:43.674 Podcasts[2050:207] Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 1**
I see that the SECOND TIME, the player.status seems to hold 1, which I'm guessing is AVPlayerReadyToPlay.
What can I do to have the playing to work properly the first time i click the play UIButton?
(ie, how can i make sure the AVPlayer is not just created, but also ready to play?)
You are playing a remote file. It may take some time for the AVPlayer to buffer enough data and be ready to play the file (see AV Foundation Programming Guide)
But you don't seem to wait for the player to be ready before tapping the play button. What I would to is disable this button and enable it only when the player is ready.
Using KVO, it's possible to be notified for changes of the player status:
playButton.enabled = NO;
player = [AVPlayer playerWithURL:fileURL];
[player addObserver:self forKeyPath:#"status" options:0 context:nil];
This method will be called when the status changes:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (object == player && [keyPath isEqualToString:#"status"]) {
if (player.status == AVPlayerStatusReadyToPlay) {
playButton.enabled = YES;
} else if (player.status == AVPlayerStatusFailed) {
// something went wrong. player.error should contain some information
}
}
}
Swift Solution
var observer: NSKeyValueObservation?
func prepareToPlay() {
let url = <#Asset URL#>
// Create asset to be played
let asset = AVAsset(url: url)
let assetKeys = [
"playable",
"hasProtectedContent"
]
// Create a new AVPlayerItem with the asset and an
// array of asset keys to be automatically loaded
let playerItem = AVPlayerItem(asset: asset,
automaticallyLoadedAssetKeys: assetKeys)
// Register as an observer of the player item's status property
self.observer = playerItem.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
if playerItem.status == .readyToPlay {
//Do your work here
}
})
// Associate the player item with the player
player = AVPlayer(playerItem: playerItem)
}
Also you can invalidate the observer this way
self.observer.invalidate()
Important: You must keep the observer variable retained otherwise it will deallocate and the changeHandler will no longer get called. So don't define the observer as a function variable but define it as a instance variable like the given example.
This key value observer syntax is new to Swift 4.
For more information, see here https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift
I had a lot of trouble trying to figure out the status of an AVPlayer. The status property didn't always seem to be terribly helpful, and this led to endless frustration when I was trying to handle audio session interruptions. Sometimes the AVPlayer told me it was ready to play (with AVPlayerStatusReadyToPlay) when it didn't actually seem to be. I used Jilouc's KVO method, but it didn't work in all cases.
To supplement, when the status property wasn't being useful, I queried the amount of the stream that the AVPlayer had loaded by looking at the loadedTimeRanges property of the AVPlayer's currentItem (which is an AVPlayerItem).
It's all a little confusing, but here's what it looks like:
NSValue *val = [[[audioPlayer currentItem] loadedTimeRanges] objectAtIndex:0];
CMTimeRange timeRange;
[val getValue:&timeRange];
CMTime duration = timeRange.duration;
float timeLoaded = (float) duration.value / (float) duration.timescale;
if (0 == timeLoaded) {
// AVPlayer not actually ready to play
} else {
// AVPlayer is ready to play
}
private var playbackLikelyToKeepUpContext = 0
For register observer
avPlayer.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
options: .new, context: &playbackLikelyToKeepUpContext)
Listen the observer
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &playbackLikelyToKeepUpContext {
if avPlayer.currentItem!.isPlaybackLikelyToKeepUp {
// loadingIndicatorView.stopAnimating() or something else
} else {
// loadingIndicatorView.startAnimating() or something else
}
}
}
For remove observer
deinit {
avPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp")
}
The key point in the code is instance property isPlaybackLikelyToKeepUp.
After researching a lot and try many ways I've noticed that normally the status observer is not the better for know really when AVPlayer object is ready to play, because the object can be ready for play but this not that mean it will be play immediately.
The better idea for know this is with loadedTimeRanges.
For Register observer
[playerClip addObserver:self forKeyPath:#"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
Listen the observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == playerClip && [keyPath isEqualToString:#"currentItem.loadedTimeRanges"]) {
NSArray *timeRanges = (NSArray*)[change objectForKey:NSKeyValueChangeNewKey];
if (timeRanges && [timeRanges count]) {
CMTimeRange timerange=[[timeRanges objectAtIndex:0]CMTimeRangeValue];
float currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timerange.start, timerange.duration));
CMTime duration = playerClip.currentItem.asset.duration;
float seconds = CMTimeGetSeconds(duration);
//I think that 2 seconds is enough to know if you're ready or not
if (currentBufferDuration > 2 || currentBufferDuration == seconds) {
// Ready to play. Your logic here
}
} else {
[[[UIAlertView alloc] initWithTitle:#"Alert!" message:#"Error trying to play the clip. Please try again" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
}
}
}
For remove observer (dealloc, viewWillDissapear or before register observer) its a good places for called
- (void)removeObserverForTimesRanges
{
#try {
[playerClip removeObserver:self forKeyPath:#"currentItem.loadedTimeRanges"];
} #catch(id anException){
NSLog(#"excepcion remove observer == %#. Remove previously or never added observer.",anException);
//do nothing, obviously it wasn't attached because an exception was thrown
}
}
Based on Tim Camber answer, here is the Swift function I use :
private func isPlayerReady(_ player:AVPlayer?) -> Bool {
guard let player = player else { return false }
let ready = player.status == .readyToPlay
let timeRange = player.currentItem?.loadedTimeRanges.first as? CMTimeRange
guard let duration = timeRange?.duration else { return false } // Fail when loadedTimeRanges is empty
let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
let loaded = timeLoaded > 0
return ready && loaded
}
Or, as an extension
extension AVPlayer {
var ready:Bool {
let timeRange = currentItem?.loadedTimeRanges.first as? CMTimeRange
guard let duration = timeRange?.duration else { return false }
let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
let loaded = timeLoaded > 0
return status == .readyToPlay && loaded
}
}
I had issues with not getting any callbacks.
Turns out it depends on how you create the stream. In my case I used a playerItem to initialize, and thus I had to add the observer to the item instead.
For example:
- (void) setup
{
...
self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
...
// add callback
[self.player.currentItem addObserver:self forKeyPath:#"status" options:0 context:nil];
}
// the callback method
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSLog(#"[VideoView] player status: %i", self.player.status);
if (object == self.player.currentItem && [keyPath isEqualToString:#"status"])
{
if (self.player.currentItem.status == AVPlayerStatusReadyToPlay)
{
//do stuff
}
}
}
// cleanup or it will crash
-(void)dealloc
{
[self.player.currentItem removeObserver:self forKeyPath:#"status"];
}
Swift 4:
var player:AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReadyToPlay(notification:)),
name: .AVPlayerItemNewAccessLogEntry,
object: player?.currentItem)
}
#objc func playerItemDidReadyToPlay(notification: Notification) {
if let _ = notification.object as? AVPlayerItem {
// player is ready to play now!!
}
}
Check the status of the player's currentItem:
if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
#JoshBernfeld's answer didn't work for me. Not sure why. He observed playerItem.observe(\.status. I had to observe player?.observe(\.currentItem?.status. Seems like they're the same thing, the playerItem status property.
var playerStatusObserver: NSKeyValueObservation?
player?.automaticallyWaitsToMinimizeStalling = false // starts faster
playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { (player, change) in
switch (player.status) {
case .readyToPlay:
// here is where it's ready to play so play player
DispatchQueue.main.async { [weak self] in
self?.player?.play()
}
case .failed, .unknown:
print("Media Failed to Play")
#unknown default:
break
}
}
when you are finished using the player set playerStatusObserver = nil

How do I get my AVPlayer to play while app is in background?

I've done my homework... been reading here, the docs, googling, stackoverflowing... but still no luck in making my sound stick when the user makes the app go into the background.
What I have done so far:
Added the UIBackgroundModes, audio to the plist-file.
First this code:
radioAudio = [[AVAudioSession alloc] init];
[radioAudio setCategory:AVAudioSessionCategoryPlayback error:nil];
[radioAudio setActive:YES error:nil];
Then this:
NSString *radioURL = #"http://xxx.xxx.xxx/radio.m3u";
radioPlayer = [[AVPlayer playerWithURL:[NSURL URLWithString:radioURL]] retain];
But as soon as the user hits the home-button, my sound dies.
I also found this, but not added yet cuase some stuff I've read says it is not needed;
newTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
if (newTaskId != UIBackgroundTaskInvalid && bgTaskId != UIBackgroundTaskInvalid)
[[UIApplication sharedApplication] endBackgroundTask: bgTaskId];
bgTaskId = newTaskId;
Right now I have no idea where I should go to make my AVPlayer let the radio sing while the user does other stuff on the phone.
I am testing this on an iPhone 4 with 4.2. Building it for 4.0.
Anyone have any suggestions what I should do?
UPDATE IOS 11.2 with Swift 4:
Now if you are using AVPlayer to play music files you should also configure MPNowPlayingInfoCenter.default() to show now playing info on the lock screen.
Below code will show now playing controls on the screen but it won't be able to respond any commands.
If you also want to controls to work you should check apple's sample project here: https://developer.apple.com/library/content/samplecode/MPRemoteCommandSample/Introduction/Intro.html#//apple_ref/doc/uid/TP40017322
Apple sample code covers all but i find it confusing.
If you want to play sound and show controls on the lock screen these steps will do just fine.
IMPORTANT NOTE: If you are NOT using AVPlayer to play sound. If you are using some third party libraries to generate sound or playback a sound file you should read comments inside the code. Also if you are using ios simulator 11.2 you won't be able to see any controls on lock screen. You should use a device to see it work.
1- Select project -> capabilites -> set background modes ON -> tick Audio, AirPlay and Picture in Picture
2- AppDelegate.swift file should look like this :
import UIKit
import AVFoundation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch.
do
{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
//!! IMPORTANT !!
/*
If you're using 3rd party libraries to play sound or generate sound you should
set sample rate manually here.
Otherwise you wont be able to hear any sound when you lock screen
*/
//try AVAudioSession.sharedInstance().setPreferredSampleRate(4096)
}
catch
{
print(error)
}
// This will enable to show nowplaying controls on lock screen
application.beginReceivingRemoteControlEvents()
return true
}
}
3- ViewController.swift should look like this:
import UIKit
import AVFoundation
import MediaPlayer
class ViewController: UIViewController
{
var player : AVPlayer = AVPlayer()
override func viewDidLoad()
{
super.viewDidLoad()
let path = Bundle.main.path(forResource: "music", ofType: "mp3")
let url = URL(fileURLWithPath: path!)
// !! IMPORTANT !!
/*
If you are using 3rd party libraries to play sound
or generate sound you should always setNowPlayingInfo
before you create your player object.
right:
self.setNowPlayingInfo()
let notAVPlayer = SomePlayer()
wrong(You won't be able to see any controls on lock screen.):
let notAVPlayer = SomePlayer()
self.setNowPlayingInfo()
*/
self.setNowPlayingInfo()
self.player = AVPlayer(url: url)
}
func setNowPlayingInfo()
{
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
let title = "title"
let album = "album"
let artworkData = Data()
let image = UIImage(data: artworkData) ?? UIImage()
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (_) -> UIImage in
return image
})
nowPlayingInfo[MPMediaItemPropertyTitle] = title
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}
#IBAction func startPlayingButtonPressed(_ sender: Any)
{
self.player.play()
}
OLD ANSWER IOS 8.2:
Patrick's answer is totally right.
But i'm gonna write what i do for ios 8.2:
I add my app's info.plist
required background modes
like below:
And in my AppDelegate.h i add these imports:
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
Then in my AppDelegate.m i wrote application didFinishLaunchingWithOptionsthis exactly like below:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
return YES;
}
Now App keeps playing music even if screen is locked :)
Had the same problem, but found a solution for this..
Look here: https://devforums.apple.com/message/395049#395049
The content of the above link:
Replace APPNAME with your own app name!
Im on iOS 4.2.1
EDIT: Working with iOS5 + 6 + 7 beta so far
Add UIBackgroundModes in the APPNAME-Info.plist, with the selection App plays audio
Then add the AudioToolBox framework to the folder frameworks.
In the APPNAMEAppDelegate.h add:
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
so it look like this:
...
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
...
In the APPNAMEAppDelegate.m add the following:
// Set AudioSession
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setDelegate:self];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
/* Pick any one of them */
// 1. Overriding the output audio route
//UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
//AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
// 2. Changing the default output audio route
UInt32 doChangeDefaultRoute = 1;
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker, sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);
into the
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
but before the two lines:
[self.window addSubview:viewController.view];
[self.window makeKeyAndVisible];
Build your project and see if theres any error, If not, try debug on Device insted of the Simulator, it CAN bug on simulator.
Hope this helps others with same problem..
I have successfully made audio run in the background by adding the following to my applicationDidFinishLaunching
// Registers this class as the delegate of the audio session.
[[AVAudioSession sharedInstance] setDelegate: self];
// Allow the app sound to continue to play when the screen is locked.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
iOS 9 Swift
All you should need is add the following to your didFinishLaunchingWithOptions
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch {
//TODO
}
You have a good example of a GPS background app that plays sounds even when in background :
http://www.informit.com/articles/article.aspx?p=1646438&seqNum=5
In this example, AudioToolbox is used.
I did a test myself and it works : create a simple project that monitors the GPS post (UIBackgroundModes location) , and every x received position, play a sound using
AudioServicesPlaySystemSound(soundId);
Then if you put audio as part of your UIBackgroundModes and the sounds will be played, even if the application isn't in foreground anymore.
I've made such a test and it works ok !
(I didn't manage to get it working with AVPlayer so I fallbacked to AudioToolbox)
I've the same problem and I found solution in Apple Docs:https://developer.apple.com/library/ios/qa/qa1668/_index.html
Q: How do I ensure my media will continue playing when using AV Foundation while my application is in the background?
A: You must declare your app plays audible content while in the background, and assign an appropriate category to your audio session. See also Special Considerations for Video Media.
Working for Swift 5
Here are the 4 things that I did for my music to play in the background from a video that the avPlayer was playing. I followed the Apple directions link from #AndriySavran's answer and this Apple link plus a few other things.
1- In AppDelegate's didFinishLaunching I put:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try AVAudioSession.sharedInstance().setCategory( .playback, mode: .moviePlayback, options: [.mixWithOthers, .allowAirPlay])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
// whatever other code that you use ...
return true
}
2- Follow this answer from #LeoDabus. In your Signing & Capabilties > Background Modes (if background modes isn't there then select it from Capabilites then) > tick Audio, Airplay, and Picture in Picture
3- In the view controller that has your AVPlayer, add the .didEnterBackgroundNotification and .willEnterForegroundNotification notifications
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification, object: nil)
}
4- for the selector method add the code from the Apple link
#objc func appDidEnterBackground() {
playerLayer?.player = nil
}
#objc func appWillEnterForeground() {
playerLayer?.player = self.player
}