Tung Dang
Tung Dang

Reputation: 266

iOS 14 Play audio from video in background

iOS 14 play audio from video in background: Audio is paused when app enterbackground (even I turn on background mode in Capability).

If avPlayer is playing in foreground, user push device lock-button, and it happens.

Upvotes: 1

Views: 3736

Answers (3)

user523234
user523234

Reputation: 14834

I finally found the solution. Not sure if this is a bug of iOS 14 or just by design. Per Apple’s document (Special Considerations for Video Media), you need to disable video track when going to background and reenable it back when going to foreground. But I don’t think they had really enforced this recommendation until iOS14. Here is my complete sample.

import UIKit
import AVKit

class ViewController: UIViewController
{
    var aPlayer: AVPlayer! = AVPlayer()
    var playButton: UIButton!
    var videoView: UIView!
    var videoFrame: CGRect!
    var active: Bool! = false
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        videoFrame = CGRect(x: 100, y: 100, width: 640, height: 480)
        self.view.backgroundColor = .blue
        
        // These notifications were send from AppDelegate accordingly.
        NotificationCenter.default.addObserver(self, selector: #selector(fromApplicationWillResignActive), name: NSNotification.Name(rawValue: "ApplicationWillResignActive"), object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(fromApplicaitonWillEnterForeground), name: NSNotification.Name(rawValue: "ApplicationWillEnterForeground"), object: nil)
        setupUI()
    }
    
    func setupUI()
    {
        playButton = UIButton(type: .custom)
        playButton.frame = CGRect(x: 50, y: 50, width: 100, height: 40)
        playButton.setTitle("Play", for: .normal)
        playButton.showsTouchWhenHighlighted = true
        playButton.addTarget(self, action: #selector(playButtonPressed), for: .touchUpInside)
        self.view.addSubview(playButton)
    }
    
    @objc func playButtonPressed(button: UIButton)
    {
        self.active = true
        
        videoView = UIView(frame: videoFrame)
        videoView.backgroundColor = .purple
        videoView.frame = videoFrame
        self.view.addSubview(videoView)
        
        let videoURL: URL = (Bundle.main.resourceURL?.appendingPathComponent("myfavor1.mp4"))!
        let item = AVPlayerItem(url: videoURL)
        self.aPlayer.replaceCurrentItem(with: item)
        
        let aLayerPlayer = AVPlayerLayer(player: self.aPlayer)
        aLayerPlayer.frame = self.videoView.bounds
        self.videoView.layer.addSublayer(aLayerPlayer)
        self.aPlayer.play()
    }
    
    @objc func fromApplicationWillResignActive(aNotification: NSNotification)
    {
        if self.active
        {
            self.videoView.removeFromSuperview()
            self.videoView = nil
            
            // For iOS 14
            let tracks = self.aPlayer.currentItem!.tracks
            for playerItemTrack in tracks
            {
                if playerItemTrack.assetTrack!.hasMediaCharacteristic(AVMediaCharacteristic.visual)
                {
                    // Disable the track.
                    playerItemTrack.isEnabled = false
                }
            }
        }
    }
    
    @objc func fromApplicaitonWillEnterForeground(aNotification: NSNotification)
    {
        if self.active
        {
            self.videoView = UIView(frame: self.videoFrame)
            self.videoView.backgroundColor = .purple
            let aLayerPlayer = AVPlayerLayer(player: self.aPlayer)
            aLayerPlayer.frame = self.videoView.bounds
            self.videoView.layer.addSublayer(aLayerPlayer)
            self.view.addSubview(self.videoView)
            
//            For iOS 14
            let tracks = self.aPlayer.currentItem!.tracks
            for playerItemTrack in tracks
            {
                if playerItemTrack.assetTrack!.hasMediaCharacteristic(AVMediaCharacteristic.visual)
                {
                    // Disable the track.
                    playerItemTrack.isEnabled = true
                }
            }
        }
    }
}

Upvotes: 1

Tung Dang
Tung Dang

Reputation: 266

I have a temporary solution in the mean time waiting for Apple to fix it:

  1. DON'T use UIApplication.didEnterBackgroundNotification & UIApplication.willEnterForegroundNotification

  2. USE UIApplicationWillResignActiveNotification & UIApplication.didBecomeActiveNotification

Now, it works smoothly in case 'change app' (slide bottom up, bring app to background).

But, it doesn't work when app goes directly by pushing the 'lock button' -> player will pause the music, users have to click playButton on lockScreen to continue (this make user annoyed)

-> *** I fixed the annoyed thing by setting autoPlay (1 time when app goes to background):

var didEnterBackground: Bool = false, canSkipPausing: Bool = true

//in the timerDuration block (timerDuration is used to count playing seconds):

        if self.didEnterBackground && self.canSkipPausing == true {
            //fix player auto-paused when enter background
            self.canSkipPausing = false
            if self.playerPlayer?.rate == 0.0 {
                self.playerPlayer?.play()
            }
        }

// handle 'didEnterBackground' & 'canSkipPausing':

@objc
func applicationDidEnterBackground(_ notification: Notification) {
    didEnterBackground = true
    // canSkipPausing = false // remember don't set this here, set it before entering background
    playerLayer?.player = nil
}

@objc
func applicationWillEnterForeground(_ notification: Notification) {
    didEnterBackground = false
    canSkipPausing = true
    playerLayer?.player = playerPlayer
}

//By this trick, when push device lock-button, the music still have a little bit paused and autoPlay again in 0.5 second, but at least user don't have to do anything.

Upvotes: 1

BinaryBang
BinaryBang

Reputation: 83

If you are using avplayer to play video,and also want to play audio when app is at background.

Try this code, it works on my machine:

  • Xcode Version:12.0
  • iOS Version: iOS 14
//first, observe the relative notification
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationWillResignActive:)
                                                 name:UIApplicationWillResignActiveNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationWillEnterForeground:)
                                                 name:UIApplicationWillEnterForegroundNotification object:nil];


    //Add an observer to AVPlayerItem,this is the key point!!!
    [playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];

-(void)applicationWillResignActive:(NSNotification *)notification{
    //1 make sure the category of AVAudioSession is AVAudioSessionCategoryPlayback
    AVAudioSession *session=[AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback  error:nil];
    [session setActive:YES error:nil];
    
    //2 This is important!!!
    //set the avplayerlayer.player = nil.
    mavPlayerLayer.player = nil;
}


//when app becomes active,we should add the player to the avplayerlayer.
-(void)applicationWillEnterForeground:(NSNotification *)notification{
    // restore the avPlayerLayer.player
    mavPlayerLayer.player = mavPlayer;
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
        NSLog(@"playbackLikelyToKeepUp");
        [mavPlayer play];
    }
}


of course,don't forget to add the relative properties at your info.plist file to support background play.

info.plist

Upvotes: 3

Related Questions