Reputation: 266
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
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
Reputation: 266
I have a temporary solution in the mean time waiting for Apple to fix it:
DON'T use UIApplication.didEnterBackgroundNotification & UIApplication.willEnterForegroundNotification
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
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:
//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.
Upvotes: 3