I have to play some videos in my swift application. The video is working perfectly but I want to detect when the video is ended. I searched about that then I found that the NotificationCenter is the solution for that. I used this code but my application crash at the end of the video.
This is my code:
func playVideo(url: NSURL){
let player = AVPlayer(url: url as URL)
NotificationCenter.default.addObserver(self, selector: Selector(("playerDidFinishPlaying")), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
player.play()
}
func playerDidFinishPlaying(note: NSNotification) {
print("Video Finished")
}
The error is:
[myApp.myViewController playerDidFinishPlaying]: unrecognized selector sent to instance 0x79669740
Any help please?
Because your selector is wrong, obviously. You are saying:
Selector(("playerDidFinishPlaying"))
But that is not the Objective-C name of your method.
Clearly, you don't know how to make the Objective-C name of your method. And you don't have to! This is exactly what #selector syntax solves. Just use it:
#selector(playerDidFinishPlaying)
And now it will work, because Swift will solve the problem you don't know how to solve.
Related
I am developing an app that listens to song changes of the MPMusicPlayerController.
For that, I am adding the following observer:
NotificationCenter.default
.addObserver(self,
selector: #selector(systemSongDidChange(_:)),
name: .MPMusicPlayerControllerNowPlayingItemDidChange,
object: nil)
The problem is that, when the notification is fired, the nowPlayingItem that can be found at (notification?.object as? MPMusicPlayerController)!.nowPlayingItem is always nil.
Am I doing anything wrong or is there some special trick that must be done to retrieve the actual nowPlayingItem?
Here is a more complete code:
// ...
init() {
let systemPlayer = MPMusicPlayerController.systemMusicPlayer
NotificationCenter.default.addObserver(self,
selector: #selector(systemSongDidChange(_:)),
name: .MPMusicPlayerControllerNowPlayingItemDidChange,
object: systemPlayer)
player.beginGeneratingPlaybackNotifications()
}
private func systemSongDidChange(notification: Notification) {
let currentSong = (notification.object as? MPMusicPlayerController)?.nowPlayingItem
// `currentSong` is always `nil` =/
}
// ...
The player I am using is the Apple's Music Player. I am not playing songs from the cloud.
You need to set the object within your notification, to be able to get the nowPlayingItem.
The code should look something like this:
private let playerController = MPMusicPlayerController.applicationMusicPlayer
NotificationCenter.default.addObserver(
self,
selector: #selector(systemSongDidChange(_:)),
name: .MPMusicPlayerControllerNowPlayingItemDidChange,
object: playerController
)
Then you should be able to access the nowPlayingItem like this in your systemSongDidChange function:
func systemSongDidChange(_ notification: Notification) {
guard let playerController = notification?.object as? MPMusicPlayerController else {
return
}
let item = playerController.nowPlayingItem
}
I have just found out why the nowPlayingItem is always being nil.
It seems that the user must have allowed the app to access the "Media & Apple Music". If this access has not been granted, the app will not have permission to know what is currently playing on the system's player.
This authorization can be requested as follows:
// if not yet given or requested
MPMediaLibrary.requestAuthorization { authorizationStatus in }
or
// if already requested and denied (will take user to the App Settings Page)
UIApplication.shared.openURL(URL(string:UIApplicationOpenSettingsURLString)!)
This is how in my code, playing from url, looks like:
private func play() {
let streamUrl = ...
let playerItem = AVPlayerItem(url: streamURL)
radioPlayer = AVPlayer(playerItem: playerItem)
radioPlayer.volume = 1.0
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
UIApplication.shared.beginReceivingRemoteControlEvents()
} catch {
print("Error deactivating audio session.")
}
radioPlayer.play()
startListeningForStreamFail()
stopStartButton.setImage(#imageLiteral(resourceName: "pause_btn"), for: .normal)
}
Like the code snippet explains above, after calling the .play() function, I'm calling startListeningForStreamFail(), which registers the current viewcontroller to two types of notifications, on main thread.
private func startListeningForStreamFail() {
DispatchQueue.main.async { [weak self] in
NotificationCenter.default.addObserver(self as Any, selector: #selector(self?.playerItemFailedToPlay), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: self?.radioPlayer?.currentItem)
NotificationCenter.default.addObserver(self as Any, selector: #selector(self?.playerItemFailedToPlay), name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: self?.radioPlayer?.currentItem)
}
}
And the selector functions is this:
#objc private func playerItemFailedToPlay(notification: Notification) {
print("playerItemFailedToPlay")
}
Because the stream right now works fine, I'm trying to test failure by addig some plus characters in it's url. But the playerItemFailedToPlay() functions does NOT get called, does NOT print anything.
Should this selector getting called, even if only the url was changed?
Any help would be appreciated. Thanks!
I tried to build a project on Github for an easy check
I followed these steps:
Adding NSAppTransportSecurity to info.plist as in this answer to allow http
Trim the provided url to remove any spaces
let urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
Check if the string provides a valid link
guard let url = URL(string: urlString) else { return complete(.unvalidURL) }
Check if the link is playable
AVAsset(url: url).isPlayable
If any of the previous steps was not successful then it means the url is not valid
I also added an observers for the errors after starting a playable link
NotificationCenter.default.addObserver(self, selector: #selector(itemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(itemNewErrorLogEntry(_:)), name: .AVPlayerItemNewErrorLogEntry, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(itemPlaybackStalled(_:)), name: .AVPlayerItemPlaybackStalled, object: nil)
EDIT:
The part of AVAsset(url: url).isPlayable might only check if the path ends with an appropriate extension as an example mp3, mp4.
I do not have access to check the actual code so I would only refer to the documentation
I have an array of object. Each object has a property url. Using this url I am playing audio on AVPlayer.
func playAudio(url: URL)
{
let player = AVPlayer(url: url )
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true)
{
do
{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
}
catch _ as NSError {
}
playerViewController.player!.play()
}
NotificationCenter.default.addObserver(self, selector: #selector(CourseDetailViewController.moviePlayBackDidFinish(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
}
I want to play the audios of all objects continuously. For which I have set
NotificationCenter.default.addObserver(self, selector: #selector(CourseDetailViewController.moviePlayBackDidFinish(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
In CourseDetailViewController.moviePlayBackDidFinish I am again calling playAudio. I then took player as a global variable so that it is not initialised again. But there is no overload of AVPlayer where we can supply url to already existing AVPlayer. And if do let player = AVPlayer(url: url ) for next audio, it doesn't play because the previous AVPlayer instance is in the foreground.
This is the challenge I am facing in playing continuous audios. I would appreciate if there a better and neat way to do this.
I did the same in AVPlayer, but playing video continuously. I hope you can solve this issue exactly the same way. Add an observer to check if the player reached at the end of item. When ever each item reached at the end, notification will call.
Adding observer:
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: avPlayer.currentItem)
avPlayer.play()
Notification method:
func playerItemDidReachEnd(notification: Notification){
//TODO: Add your code here..
}
Did you look at AVQueuePlayer. This player is used to play a number of items in sequence. SDK Docs at link
I have add Player-swift in my project to play video but when i pop that controller app getting crash with following log
Terminating app due to uncaught exception 'NSRangeException', reason:
'Cannot remove an observer for the key
path "rate" from because it is not
registered as an observer.'
any one have any idea? i have used this player https://github.com/piemonte/Player
Thanks in advance !
Do not forget to unsubsribe from observing some property. For example if you subscribed for observing rate then to remove observing use
player.removeObserver(observer, forKeyPath: #keyPath(AVPlayer.rate))
First: check KVO basics.
Second: in objective-C you would the removing observers code into try-catch block and live happy. The apple's guide tell the same:
Asking to be removed as an observer if not already registered as one
results in an NSRangeException. You either call
removeObserver:forKeyPath:context: exactly once for the corresponding
call to addObserver:forKeyPath:options:context:, or if that is not
feasible in your app, place the removeObserver:forKeyPath:context:
call inside a try/catch block to process the potential exception.
In swift there’s no KVO API call you can make to ask, “Is X observing key path Y of object Z?” There is some ways to workaround it.
Also check the one of the reasons of the crashes when removing the observer. Here is the quote:
"It" refers to the observer. -removeObserver:forKeyPath: raises this
exception if told to remove an object that isn't currently registered
as an observer. So what's happening is that a table view is trying to
unregister as an observer from one of your objects that it
unfortunately didn't previouly register as an observer for.
The usual cause of this is that you have a property that isn't KVO-
compliant. Something accesses your 'foo' property and registers as an
observer of that property, and also as an observer of the object
that's the property's current value; you change the value of 'foo'
without letting anyone know; the observer then later decides to stop
observing, gets your 'foo' property, and removes itself as an observer
of that object. But it's no longer the same object that it registered
as an observer for...
Take IBOutlet of Your UIView for Example
#IBOutlet var videoView:UIView!
var player:AVPlayer!
func buttonPressed()
{
let videoURL = URL(fileURLWithPath: "your File Path")
player = AVPlayer(url: videoURL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect
playerLayer.frame = videoView.bounds
videoView.layer.addSublayer(playerLayer)
player.play()
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(_:));, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
#objc func playerItemDidReachEnd(_ notification: Notification?)
{
let p = notification?.object as? AVPlayerItem
p?.seek(to: kCMTimeZero)
}
override func viewWillDisappear(_ animated: Bool)
{
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
Hi guys I am having a problem with UIPageViewController and Notifications.
I have a page UIPageVewController with a array of pages, so in these pages I have a AVplayer playing in loop as bellow:
func loopVideo(videoPlayer:AVPlayer){
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: nil){
[weak videoPlayer] notification in
videoPlayer?.seek(to: kCMTimeZero)
videoPlayer?.play()
}
}
The problem is when I change page with scroll the notifications from another pages change my current video playing AVPlayer. I put a print inside the notification and I can see calling the other pages notifications. I don't Know what I have to do?
I tried remove the notification in the viewDidDisappear using NotificationCenter.default.removeObserver(self) but didn't work.
Can you help me?
Thanks
NotificationCenter.default.removeObserver(self) won't work here as you never added yourself as a target.
Instead keep a reference to your notification and remove it. I think it should look something like this:
var notificationObserver:NSObjectProtocol?
func loopVideo(videoPlayer:AVPlayer){
self.notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: nil){
[weak videoPlayer] notification in
videoPlayer?.seek(to: kCMTimeZero)
videoPlayer?.play()
}
}
func removeObserver() {
NotificationCenter.default.removeObserver(self.notificationObserver)
}
You can simply do a check when your notification is received.
Check if the notification object as AVPlayerItem is the same as the visible views player playerItem, videoPlayer.currentItem
Or simply check if the AVPlayerItem in the notification is the same as the yourCustomView.playerItem
EDIT:
I see your object is nil, it should be the AVPlayerItem. Check this thread.