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: .
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
}
}
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.
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.
Open your phone's Settings app. Notifications. Under "Lock screen," tap Notifications on lock screen or On lock screen. Choose Don't show notifications.
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.
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With