iOS media playback controls notification
Asked Answered
H

2

7

I am new to iOS, and developing a cross platform app with Flutter. I am trying to play audio from network URL, which i found it can be done using the AVPlayer. The audio plays when the app is in foreground and in background, but i can display the media playback controls like this: enter image description here.

i used the let mediaController = MPMusicPlayerController.applicationMusicPlayer and then calling self.mediaController.beginGeneratingPlaybackNotifications(), also providing the playing info MPNowPlayingInfoCenter.default().nowPlayingInfo = mediaInfo and setting the targets for the remote command center in self.registerCommands() method.

i did alot of research but no luck in finding the problem, and as i said before am new to ios.

AppDelegate

import UIKit
import Flutter
import AVFoundation
import MediaPlayer

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    static let CHANNEL = "APP_CHANNEL"
    let mPlayer = AudioPlayer()
    let mediaController = MPMusicPlayerController.applicationMusicPlayer

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    self.requestNotificationPermission(application: application)

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let mainChannel = FlutterMethodChannel(name: AppDelegate.CHANNEL,
                                              binaryMessenger: controller.binaryMessenger)
    mainChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in

        switch(call.method) {
        case "getSharedContainerPath":
            let path = Utils.getSharedContainerPath()
            result(path)
            break
        case "saveSelectedCity":
            let city = call.arguments as! String
            Utils.saveCityToUserDefaults(city: city)
            result(true)
            break
        case "playSurah":
            let number = call.arguments as! Int
            self.initAudioPlayer()
            self.mPlayer.toggle(num: number)
            result(true)
            break
        default:
            result(FlutterMethodNotImplemented)
            return
        }
    })


    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

    func initAudioPlayer() {
        self.mediaController.beginGeneratingPlaybackNotifications()
        self.mPlayer.initPlayer(object: self)

        self.registerCommands()

        let nc = NotificationCenter.default
        nc.addObserver(self,
                          selector: #selector(handleInterruption),
                          name: AVAudioSession.interruptionNotification,
                          object: nil)
        nc.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: nil)
    }

    func requestNotificationPermission(application: UIApplication) {
        if #available(iOS 10, *) {
            // iOS 10 support
            //create the notificationCenter

            let center = UNUserNotificationCenter.current()
            center.delegate = self as UNUserNotificationCenterDelegate
            // set the type as sound or badge
            center.requestAuthorization(options: [.sound,.alert,.badge]) { (granted, error) in
                if granted {
                    print("Notification Enable Successfully")
                }else{
                    print("Some Error Occure")
                }
            }
            application.registerForRemoteNotifications()
        } else if #available(iOS 9, *) {
            // iOS 9 support
            UIApplication.shared.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: nil))
            UIApplication.shared.registerForRemoteNotifications()
        } else if #available(iOS 8, *) {
            // iOS 8 support
            UIApplication.shared.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: nil))
            UIApplication.shared.registerForRemoteNotifications()
        } else { // iOS 7 support
            application.registerForRemoteNotifications(matching: [.badge, .sound, .alert])
        }
    }



    func registerCommands() {

        let command = MPRemoteCommandCenter.shared()

        command.playCommand.isEnabled = true;
             command.playCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
                self.mPlayer.play()
                 return .success
             }
        command.pauseCommand.isEnabled = true;
             command.pauseCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
                self.mPlayer.pause()
                 return .success
             }
        command.togglePlayPauseCommand.isEnabled = true;
             command.togglePlayPauseCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
                self.mPlayer.toggle(num: self.mPlayer.index)
                 return .success
             }
        command.nextTrackCommand.isEnabled = true;
             command.nextTrackCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
             self.mPlayer.playNext()
                 return .success
             }
        command.previousTrackCommand.isEnabled = true;
             command.previousTrackCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
             self.mPlayer.playPrev()
                 return .success
             }
        command.stopCommand.isEnabled = true;
             command.stopCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
                self.mPlayer.stop()
                 return .success
             }

    }

//    [notificationCenter addObserver: self
//                           selector: @selector (handle_NowPlayingItemChanged:)
//                               name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification
//                             object: musicPlayer];
//
//    [notificationCenter addObserver: self
//                           selector: @selector (handle_PlaybackStateChanged:)
//                               name: MPMusicPlayerControllerPlaybackStateDidChangeNotification
//                             object: musicPlayer];
//
//    [notificationCenter addObserver: self
//                           selector: @selector (handle_VolumeChanged:)
//                               name: MPMusicPlayerControllerVolumeDidChangeNotification
//                             object: musicPlayer];

    func destroyPlayer() {
        self.mPlayer.stop()

        let nc = NotificationCenter.default
        nc.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        nc.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)

        self.mediaController.endGeneratingPlaybackNotifications()

        let command = MPRemoteCommandCenter.shared()
        command.playCommand.isEnabled = false;
        command.pauseCommand.isEnabled = false;
        command.togglePlayPauseCommand.isEnabled = false;
        command.nextTrackCommand.isEnabled = false;
        command.previousTrackCommand.isEnabled = false;
        command.stopCommand.isEnabled = false;
    }

//    override func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
//        self.destroyPlayer()
//    }

    override func applicationWillTerminate(_ application: UIApplication) {
        self.destroyPlayer()
    }

    @objc func playerDidFinishPlaying(note: NSNotification) {
        self.mPlayer.playNext()
    }

    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {

        // Only handle observations for the playerItemContext
        guard context == &mPlayer.playerItemContext else {
            super.observeValue(forKeyPath: keyPath,
                               of: object,
                               change: change,
                               context: context)
            return
        }

        if keyPath == #keyPath(AVPlayerItem.status) {
            let status: AVPlayerItem.Status
            if let statusNumber = change?[.newKey] as? NSNumber {
                status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
            } else {
                status = .unknown
            }

            // Switch over status value
            switch status {
            case .readyToPlay:
                self.mPlayer.updateMediaInfo()
                break
                // Player item is ready to play.
            case .failed: break
                // Player item failed. See error.
            case .unknown: break
                // Player item is not yet ready.
            @unknown default:
                super.observeValue(forKeyPath: keyPath,
                of: object,
                change: change,
                context: context)
            }
        } else if keyPath == #keyPath(AVPlayer.timeControlStatus) {
            if object is AVPlayer {
                if (object as? AVPlayer) != nil {
                    self.mPlayer.updateMediaInfo()
                }
            }
        } else {
            super.observeValue(forKeyPath: keyPath,
            of: object,
            change: change,
            context: context)
        }
    }

    @objc func handleInterruption(notification: Notification) {
        guard let userInfo = notification.userInfo,
            let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
            let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                return
        }

        // Switch over the interruption type.
        switch type {
            case .began:
                // An interruption began. Update the UI as needed.
                self.mPlayer.pause()
                break
            case .ended:
               // An interruption ended. Resume playback, if appropriate.
                guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption ended. Playback should resume.
                    self.mPlayer.play()
                } else {
                    // Interruption ended. Playback should not resume.
                }
            default: ()
        }
    }
}

Audio Player class

//
//  AudioPlayer.swift
//  Runner

import Foundation
import AVFoundation
import MediaPlayer

class AudioPlayer {
    private var player: AVPlayer?
    var index: Int = 0
    private var object: NSObject!
    // Key-value observing context
    var playerItemContext = 0
    private var mediaInfo = [String : Any]()


    func initPlayer(object: NSObject) {
        self.object = object
        do {
            if #available(iOS 10.0, *) {
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [.mixWithOthers, .allowAirPlay])
                try AVAudioSession.sharedInstance().setActive(false)
            } else {
                // Fallback on earlier versions
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .mixWithOthers)
            }
        } catch {
            print(error)
        }
    }

    func startPlayer() {
        do {
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print(error)
        }

        self.mediaInfo[MPMediaItemPropertyTitle] = ""
        self.mediaInfo[MPMediaItemPropertyArtist] = ""
        updateMediaInfo()

        let url = getUrl()
        let playerItem = AVPlayerItem(url: url!)
        playerItem.addObserver(self.object, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: &playerItemContext)

        if self.player == nil {
            self.player = AVPlayer(playerItem: playerItem)
        } else {
            self.player?.replaceCurrentItem(with: playerItem)
        }
        self.player?.addObserver(self.object, forKeyPath: #keyPath(AVPlayer.timeControlStatus), options: [.new, .old], context: &playerItemContext)
        if let p = self.player {
            p.play()
        }
        getMetadata(for: url!, completionHandler: { (metadata) in
            self.mediaInfo[MPMediaItemPropertyTitle] = metadata?["title"]
            self.mediaInfo[MPMediaItemPropertyArtist] = metadata!["artist"]
            self.mediaInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds
            self.updateMediaInfo()
        })
    }


    func toggle(num: Int) {
        if self.index == num {
            if let p = self.player {
                if(p.isPlaying) {
                    p.pause()
                }
                else {
                    p.play()
                }
                self.updateMediaInfo()
            }
        } else {
            self.index = num
            startPlayer()
        }
    }

    func pause() {
        if let p = self.player {
            if(p.isPlaying) {
                p.pause()
                self.updateMediaInfo()
            }
        }
    }

    func play() {
        if let p = self.player {
            if(!p.isPlaying ) {
                p.play()
                self.updateMediaInfo()
            }
        }
    }

    func playNext() {
        if self.index + 1 <= 114 {
            self.index += 1
        } else {
            self.index = 1
        }
        self.startPlayer()
    }

    func playPrev() {
        if self.index - 1 >= 1 {
            self.index -= 1
        } else {
            self.index = 114
        }
        self.startPlayer()
    }

    func stop() {
        if let p = self.player {
            p.pause()
            self.player?.replaceCurrentItem(with: nil)
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
    }

    func getUrl() -> URL? {
        return URL(string: String(format: Utils.QURAN_AUDIO, self.index))
    }

    func updateMediaInfo() {
        mediaInfo[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate
        mediaInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime().seconds
        if #available(iOS 10.0, *) {
            mediaInfo[MPNowPlayingInfoPropertyMediaType] = NSNumber(value: MPNowPlayingInfoMediaType.audio.rawValue)
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = mediaInfo
    }

    func getMetadata(for url: URL, completionHandler: @escaping (_ metadata: [String : String]?) -> ()) {

      var request = URLRequest(url: url)
      request.httpMethod = "HEAD"

      let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        guard error == nil,
          let res1 = response as? HTTPURLResponse,
          let contentLength = res1.allHeaderFields["Content-Length"] as? String else {
            completionHandler(nil)
            return
        }
        do {
            var req = URLRequest(url: url)
            req.setValue("bytes=\(UInt64(contentLength)! - 128)-", forHTTPHeaderField: "Range")
            let data = try NSURLConnection.sendSynchronousRequest(req, returning: nil)

            let titleBytes = data.subdata(in: Range<Int>(NSRange(location: 3, length: 29))!)
                .filter { (data) -> Bool in
                    data != 0
                }
            let artistBytes = data.subdata(in: Range<Int>(NSRange(location: 33, length: 29))!)
                .filter { (data) -> Bool in
                    data != 0
                }

            let title = String(data: titleBytes, encoding: String.Encoding.utf8)
            let artist = String(data: artistBytes, encoding: String.Encoding.utf8)

            completionHandler(["title": title!, "artist": artist!])

        } catch {
            completionHandler(nil)
        }
      }

      task.resume()
    }

}

extension AVPlayer {
    var isPlaying: Bool {
        if #available(iOS 10.0, *) {
            return timeControlStatus.rawValue == TimeControlStatus.playing.rawValue
        }
        return rate != 0 && error == nil
    }
}

Heathendom answered 23/1, 2020 at 20:41 Comment(4)
can anyone helpHeathendom
It's unclear what the problem is. What do you mean by 'display the media playback controls'?Aspa
i want to get the media playback controls on home screen like: miro.medium.com/max/378/1*wLeFryy1Cqb-Yj_V8Agbiw.png but it won't displayHeathendom
this- https://mcmap.net/q/507275/-where-to-add-current-time-and-duration-time-for-avplayer-and-avaudioplayer-using-mpremotecommandcenterStav
K
5

From a comment:

i don't have a real device, i am using the IPhone 11 pro max simulator

That’s the problem. You cannot test this feature except on a device. The simulator is not a reliable guide for many iOS features / behaviors, and this is one of them. Without a device, you have no evidence of whether your code works as desired.

Kootenay answered 26/1, 2020 at 13:59 Comment(2)
oh thank you, i did not know that, i have been stuck with this for days now thinking i got something wrong in my codeHeathendom
There may be something wrong with your code. What I’m saying is that without a device you have no evidence either way.Kootenay
P
5

If I get you right, the NowPlayingInfo doesn't show your MediaInfo (Title, etc..).

This is because currently iOS ignores NowPlayingInfo from AVAudioSessions with .mixWithOthers option enabled.

I did setup a little test project with your code. With .mixWithOthers option I could reproduce your problem. After removing this option NowPlayingInfoCenter worked as expected.

One more thing: When trying to set AVAudioSession category I always get an error Error Domain=NSOSStatusErrorDomain Code=-50 "(null)". This is because setting the .allowsAirPlay option isn't allowed for category .playback. (https://developer.apple.com/documentation/avfoundation/avaudiosession/categoryoptions/1771736-allowairplay)

Platte answered 26/1, 2020 at 8:21 Comment(2)
thnx for ur repsone, i removed both .mixWithOthers and .allowsAirPlay and still did not get the media playback controls. i also created a new swift project and copied the player code to it but the same thing, no playback controls appear. Am i doing something wrong ? and can you share the test project that you created. thnx - PS: i don't have a real device, i am using the IPhone 11 pro max simulatorHeathendom
This audio control doesn't appear on Simulators, you need o run it on a real device.Stevenage
K
5

From a comment:

i don't have a real device, i am using the IPhone 11 pro max simulator

That’s the problem. You cannot test this feature except on a device. The simulator is not a reliable guide for many iOS features / behaviors, and this is one of them. Without a device, you have no evidence of whether your code works as desired.

Kootenay answered 26/1, 2020 at 13:59 Comment(2)
oh thank you, i did not know that, i have been stuck with this for days now thinking i got something wrong in my codeHeathendom
There may be something wrong with your code. What I’m saying is that without a device you have no evidence either way.Kootenay

© 2022 - 2024 — McMap. All rights reserved.