Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS media playback controls notification

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
    }
}

like image 925
medyas Avatar asked Jan 23 '20 20:01

medyas


People also ask

How do I get rid of the unknown part notification on my iPhone?

Answer: A: You will never be able to get rid of that. The only way to get rid of it would be to have the display replaced by Apple or an authorized service provider with a genuine Apple display. It is unlikely in the extreme that they will agree to do so now that your phone has been tampered with.

Why does my iPhone keep showing music on lock screen?

At your home screen, click the gear icon (settings) to visit device settings. Then what you have to do is open the notifications tab. You have to find and click on Music and slide the notification toggle to disable it. Restart your iPhone after doing so and the music app lock screen problem should be fixed easily.

How do I make the notification settings go away?

Open your phone's Settings app. Notifications. Under "Lock screen," tap Notifications on lock screen or On lock screen. Choose Don't show notifications.

What does notification grouping mean?

Android 7+ Default behavior. Automatically groups your notifications after the device has 4 or more visible notifications for your app, even if you don't set a group key. Automatically generates the text for the summary notification for the grouped notifications.


2 Answers

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.

like image 87
matt Avatar answered Oct 09 '22 06:10

matt


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)

like image 36
Robin Schmidt Avatar answered Oct 09 '22 07:10

Robin Schmidt