MPRemoteCommandCenter play/pause Flashing when touched - swift

i'm working on music player app that written in Swift, audio streams with AVPlayer and everything is fine
but when i tried to add MPRemoteCommandCenter to my app there was a lot of bug that i dont know even why this happend
link to video that describes my problem
AVPlayer Implemented like:
func setupPlayer() {
let item = AVPlayerItem(url: musicURL)
self.player = AVPlayer.init(playerItem: item)
self.player.play()
self.player.volume = 1
self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { (time) in
if self.player.currentItem?.status == .readyToPlay {
self.reloadNowPlayingInfo()
let currentTime = self.player.currentTime().seconds
self.playingTime.text = currentTime.getTimeString()
self.playerSlider.value = currentTime/duration
}
})
}
func reloadNowPlayingInfo() {
var info = [String : Any]()
info[MPMediaItemPropertyTitle] = self.titleText
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork.init("some image")
info[MPMediaItemPropertyPlaybackDuration] = seconds
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentSecs
info[MPMediaItemPropertyArtist] = "Artist name"
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
and for command center,
MPRemoteCommandCenter Implemented like:
func setupCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action: #selector(self.playCommand(_:)))
commandCenter.pauseCommand.addTarget(self, action: #selector(self.pauseCommand(_:)))
}
#objc func playCenter(_ action: MPRemoteCommandEvent) {
self.state = .play
self.playBtn.setBackgroundImage("some image"), for: .normal)
self.player.play()
self.fetchTracks()
}
#objc func pauseCenter(_ action: MPRemoteCommandEvent) {
self.state = .pause
self.playBtn.setBackgroundImage("some image"), for: .normal)
self.player.pause()
self.fetchTracks()
}

In addition to the code you've presented you might also have called the following somewhere in your app delegate:
UIApplication.shared.beginReceivingRemoteControlEvents()
Doing this in addition to using MPRemoteCommandCenter.shared() seems to cause a race condition.
According to Apple's documentation:
In iOS 7.1 and later, use the shared MPRemoteCommandCenter object to register for remote control events. You do not need to call this method when using the shared command center object.
This method starts the delivery of remote control events using the responder chain.
Remove that method from your app delegate and you should be fine.

If you add 2 observer to your code to receive player notifications. you may see lag or jump in player lock screen.
avoid adding observer and once a target.
commandCenter.playCommand.addTarget(self, action: #selector(self.playCommand(_:)))
only once. and remove it when u dont wanted

Related

How to set NowPlaying properties with a AVQueuePlayer in Swift?

I have an AVQueuePlayer that gets songs from a Firebase Storage via their URL and plays them in sequence.
static func playQueue() {
for song in songs {
guard let url = song.url else { return }
lofiSongs.append(AVPlayerItem(url: url))
}
if queuePlayer == nil {
queuePlayer = AVQueuePlayer(items: lofiSongs)
} else {
queuePlayer?.removeAllItems()
lofiSongs.forEach { queuePlayer?.insert($0, after: nil) }
}
queuePlayer?.seek(to: .zero) // In case we added items back in
queuePlayer?.play()
}
And this works great.
I can also make the lock screen controls appear and use the play pause button like this:
private static func setRemoteControlActions() {
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [self] event in
queuePlayer?.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [self] event in
if queuePlayer?.rate == 1.0 {
queuePlayer?.pause()
return .success
}
return .commandFailed
}
}
The problem comes with setting the metadata of the player (name, image, etc).
I know it can be done once by setting MPMediaItemPropertyTitle and MPMediaItemArtwork, but how would I change it when the next track loads?
I'm not sure if my approach works for AVQueueplayer, but for playing live streams with AVPlayer you can "listen" to metadata receiving.
extension ViewController: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
//look for metadata in groups
}
}
I added the AVPlayerItemMetadataOutputPushDelegate via an extension to my ViewController.
I also found this post.
I hope this gives you a lead to a solution. As said I'm not sure how this works with AVQueuePlayer.

Swift callkit sometimes can't activate loudspeaker after received call (only incoming call)

after follow #Marco comment, i updated code like below, but still not working, the loudspeaker sometimes can not enabled
Before report new call/ user accepted call I called the 2 methods below:
configureAudioSessionToDefaultSpeaker()
func configureAudioSessionToDefaultSpeaker() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSession.Category.playAndRecord, mode: .default)
try session.setActive(true)
try session.setMode(AVAudioSession.Mode.voiceChat)
try session.setPreferredSampleRate(44100.0)
try session.setPreferredIOBufferDuration(0.005)
} catch {
print("Failed to configure `AVAudioSession`: \(error)")
}
}
I updated more code:
func startCallWithPhoneNumber(call : CallInfoModel) {
configureAudioSessionToDefaultSpeaker()
currentCall = call
if let unwrappedCurrentCall = currentCall {
let handle = CXHandle.init(type: .generic, value: unwrappedCurrentCall.CallerDisplay ?? UNKNOWN)
let startCallAction = CXStartCallAction.init(call: unwrappedCurrentCall.uuid, handle: handle)
let transaction = CXTransaction.init()
transaction.addAction(startCallAction)
requestTransaction(transaction: transaction)
self.provider?.reportOutgoingCall(with: startCallAction.callUUID, startedConnectingAt: nil)
}
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
configureAudioSessionToDefaultSpeaker()
delegate?.callDidAnswer()
action.fulfill()
currentCall?.isAccepted = true
let sb = UIStoryboard(name: "main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "SingleCallVC") as! SingleCallVC
vc.modalPresentationStyle = .fullScreen
vc.callObj = currentCall
vc.isIncoming = true
let appDelegate = AppDelegate.shared
appDelegate.window?.rootViewController?.present(vc, animated: true, completion: nil)
}
My call almost work normally but sometime loudspeaker can not be enabled. I read many documents but nothing worked for me. Could someone give me some advice? Thanks.
You're configuring the AudioSession two times. The RTCAudioSession it's a proxy of AVAudioSession. You should do only one configuration to avoid unexpected results. RTCAudioSession should expose all the methods of the AVAudioSession, so you should be able to make all the configurations you want inside configureRtcAudioSession() and eliminate configureAudioSessionToDefaultSpeaker() or viceversa. I'm not sure if it will solve your issue but at least it should help to avoid unexpected behaviors.
I've had success with enabling the speaker using the method below.
let audioQueue = DispatchQueue(label: "audio")
func setSpeaker(_ isEnabled: Bool) {
audioQueue.async {
defer {
AVAudioSession.sharedInstance().unlockForConfiguration()
}
AVAudioSession.sharedInstance().lockForConfiguration()
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(isEnabled ? .speaker : .none)
} catch {
debugPrint(error.localizedDescription)
}
}
}
// Enables the audio speaker.
setSpeaker(true)
// Disables the audio speaker.
setSpeaker(false)

How to stop AVAudioPlayer from colliding with other sound from another viewcontroller?

I have two view controllers for this app. The first one includes a button that can be clicked for a video to play. The second one has a list of songs which can be played. My problem is when I play a song from the 2nd controller, the background music collides with the sound of the video. I want the music from the 2nd controller to stop and not to collide with the sound of the video from the 1st controller.
I tried to use this code in the 1st controller but it didn't work:
audioPlayer.stop()
It gives an error saying that it is "EXC_BAD_ACCESS Swift"
Could anyone please help me? Thx :)
As per my understanding you need to manage A single player throughout your app. In my one project I managed to use it in following way. Please check if that helps you
struct Manager
{
//Required Objects - AVFoundation
///AVAudio Session
static var recordingSession: AVAudioSession!
///AVAudio Recorder
static var recorder: AVAudioRecorder?
///AVAudio Player
static var player: AVAudioPlayer?
///Bool to check an audio is being played or not
static var recordingalreadyPlayedStatus = Bool()
}
Select File From MainSource and Play it
//MARK: Button Handler
/**
Function is used to play selected Row music Audio File
- parameter sender : UIButton Reference
*/
#objc func playButtonHandler(sender: UIButton)
{
let buttonPosition : CGPoint = sender.convert(CGPoint.zero, to: self.recordingListTableView)
let indexPath : IndexPath = self.recordingListTableView.indexPathForRow(at: buttonPosition)!
///Check is Recorder is Recording Sound ?
if (Manager.recorder != nil) {
return
}
///Check Audio Already Playing ?
/// Bool Used to check is anything getting Played
if (Manager.recordingalreadyPlayedStatus) {
///If yes
///Stop From Playing
if self.currentIndexPath == indexPath {
///Time to stop playing music
var cell = self.recordingListTableView.dequeueReusableCell(withIdentifier: "Cell") as! recordingListCell
cell = self.recordingListTableView.cellForRow(at: self.currentIndexPath) as! recordingListCell
cell.playButton.setImage(#imageLiteral(resourceName: "start"), for: .normal)
Manager.player?.stop()
Manager.recordingalreadyPlayedStatus = false
self.currentIndexPath = indexPath
print("Stopped Playing Current Sound")
}
else {
///Time to stop playing music
var cell = self.recordingListTableView.dequeueReusableCell(withIdentifier: "Cell") as! recordingListCell
cell = self.recordingListTableView.cellForRow(at: self.currentIndexPath) as! recordingListCell
cell.playButton.setImage(#imageLiteral(resourceName: "start"), for: .normal)
Manager.player?.stop()
Manager.recordingalreadyPlayedStatus = false
///Play music at new index
self.playMusic(indexPath: indexPath)
}
}
else {
/// NoThing was Played lets play Directly
self.playMusic(indexPath: indexPath)
}
print("Starting to Play Sound")
}
Play Audio
//MARK: Play Music
/**
This Function get the URL of Audio and Plays it in View
*/
func playMusic(indexPath: IndexPath)
{
//get cell Index Status Using CGPoint
var cell = self.recordingListTableView.dequeueReusableCell(withIdentifier: "Cell") as! recordingListCell
cell = self.recordingListTableView.cellForRow(at: indexPath) as! recordingListCell
//Set Current value so we can change status if any other audio being played
self.currentIndexPath = indexPath
coreClassObject.initWithData(data: fileNameArray, index: indexPath.row)
//Play music now
let isMusicPlayed = RecordingManager.shared.playRecording(fileName: coreClassObject.audioFileName)
//Change Button status
if (isMusicPlayed)
{
//yes being played
//time to change button Text
cell.playButton.setImage(#imageLiteral(resourceName: "stop"), for: .normal)
}
else
{
//music is not played
cell.playButton.setImage(#imageLiteral(resourceName: "start"), for: .normal)
}
}
Function used to check is recording in progress if no, play Audio
//MARK: Play Recording at selected Index
/**
This function is used to check is any file being currently Played ?
if no, Play File else return bool that another file is being already played
- parameter fileName : File Name refer to Audio File name which is to be played
- returns : Bool That file is Successfully played or not
*/
func playRecording(fileName:String) -> Bool {
/// Just a bool used here to check is recording being played
/// Used to Update timer in TableView Cell
var recordingPlayed = Bool()
/// Whenever we need to play an audio file we will allocate player
/// Optional will let you compare that player is nil ?
if Manager.recorder == nil {
/// Player is found nil
/// Thus no Audio is being Played here
/// Yes, We can Play Sound
recordingPlayed = true
Manager.recordingalreadyPlayedStatus = true
//Set player with audio File
do {
try Manager.player = AVAudioPlayer.init(contentsOf: returnPathAtSelectedIndex(fileName: fileName))
//Set required delegates and Values
Manager.player?.delegate = self
Manager.player?.volume = 1.0
Manager.player?.prepareToPlay()
Manager.player?.play()
}
catch {
print("Error while playing music: \(error.localizedDescription)")
}
}
else {
/// Player is already playing a file
/// Can not play audio for now
recordingPlayed = false
Manager.recorder = nil
Manager.recordingalreadyPlayedStatus = false
}
return recordingPlayed
}

avplayer with slow-internet connection not working after first frame

my problem is that when having very bad connection,the activtiy indicator start animating till first frame in video is shown,then disappear thinking is the video is playing,but the video stops playing stuck on loaded first frame,until the whole video is loaded then its resume playing,how to show activity indicator while video is stuck on frame and buffering ,then play until next loaded frame ?
notes:
it's working when internet connetion is off ,video is played until the loaded frame and activity indicator is shown,then when turn on video us resumed to play and activity indicator is hidden
it's working when normal internet connection is present
removing and showing indicator using override observevalue for key path
"currentItem.loadedTimeRanges"/"currentItem.playbackBufferEmpty"
i made a uiview class with avplayer in it
import UIKit
import AVKit
import AVFoundation
class videoplaying: UIView {
override static var layerClass: AnyClass {
return AVPlayerLayer.self;
}
var playerlayer: AVPlayerLayer{
return layer as! AVPlayerLayer;
}
var player: AVPlayer?{
get{
return playerlayer.player
}
set {
playerlayer.player = newValue
}
}
var playetitem: AVPlayerItem?
}
i assigned a uivew in uicollectioncell to this class(using storyboard)
avplayer starts playing and adding observes when pressing play in uicollectioncell
#IBAction func play(_ sender: Any) {
activityindicator.isHidden = false
activityindicator.startAnimating()
self.butttoonheight.isHidden = true
self.postimage.isHidden = true
let url2 = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
let avplayer = AVPlayer(url: url2! )
let playeritem = AVPlayerItem(url: url2!)
//videoss is class of type uiview
videoss.playetitem = playeritem
videoss.playerlayer.player = avplayer
videoss.player?.play()
videoss.player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
videoss.player?.addObserver(self, forKeyPath: "rate", options: .new
, context: nil)
videoss.player?.addObserver(self, forKeyPath: "currentItem.playbackBufferEmpty", options: .new, context: nil)
playying.isHidden = false
}
//observing when video is playing
//playpause button to play or pause video while bad network is present video is stuck on first frame and playorpause is not changing while pressed
#IBAction func playorpause(_ sender: Any) {
if videoss.player?.timeControlStatus == AVPlayerTimeControlStatus.paused{
videoss.player?.play()
playying.setImage(UIImage(named: "pas50"), for: .normal)
}
if videoss.player?.timeControlStatus == AVPlayerTimeControlStatus.playing{
videoss.player?.pause()
playying.setImage(UIImage(named: "p24"), for: .normal)
}
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate"{
print(videoss.player?.rate)
if videoss.player?.rate == 0.0 {
print("dawdaopwdaopwdipo")
}
}
if keyPath == "currentItem.loadedTimeRanges"{
print("its is working")
activityindicator.stopAnimating()
activityindicator.isHidden = true
}
if keyPath == "currentItem.playbackBufferEmpty"
{
activityindicator.startAnimating()
activityindicator.isHidden = false
print("pkawdawdawd")
}
}
I solved this problem by adding a timer that runs ever 0.3 seconds,evrry 0.3seconds it checks the current time of the video if current time equals previous time then the video is not playing activity indicator is shown,if not video if playing activity indicators was hidden,also you need to check if the users pause the video,an advantage is you also get the current time also in the same function.
What i tried also was seeing rate of the video ,but over slow internet the rate was always giving that is playing,but it wasn’t,which didn’t help

Add Action To Apple TV Remote Play/Pause Button

I have an AVAudioPlayer in my app and I am adding a function where the user can press the Play/Pause button on the remote to Play/Pause the AVAudioPlayer. I have this code in my App Delegate:
func initializePlayButtonRecognition() {
addPlayButtonRecognizer(#selector(AppDelegate.handlePlayButton(_:)))
}
func addPlayButtonRecognizer(_ selector: Selector) {
let playButtonRecognizer = UITapGestureRecognizer(target: self, action:selector)
playButtonRecognizer.allowedPressTypes = [NSNumber(value: UIPressType.playPause.rawValue as Int)]
self.window?.addGestureRecognizer(playButtonRecognizer)
}
func handlePlayButton(_ sender: AnyObject) {
if audioPlayer.isPlaying {
audioPlayer.pause() {
} else {
audioPlayer.play()
}
}
}
The AVAudioPlayer's name is an audio player. I have that stated in the code, but the App Delegate does not have a reference to the AVAudioPlayer from the PlayerViewController. How do I do this?
Your code looks correct. Just a missing detail that could explain why it does not work as expected... in tvOS, apart from setting allowedPressTypes it is also needed to define allowedTouchTypes as indirect:
playButtonRecognizer.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]