teodorpatras / Jukebox

Player for streaming local and remote audio files. Written in Swift.
MIT License
551 stars 122 forks source link

I Added a new protocol that gets title data from streaming m3u. for radio apps #8

Closed alexrmacleod closed 8 years ago

alexrmacleod commented 8 years ago

@teodorpatras Loving jukebox - found a hacky solution to getting title metadata from a http://soundradio.hk/sound-radio.m3u stream.

I use item.addObserver inside the registerForPlayToEndNotification() to listen for "timedMetadata". Then at the bottom of the observeValueForKeyPath() I call updateInfoCenter() and add the new title to the currentItem, (had to change the properties from private to public)

`

private func registerForPlayToEndNotification(withItem item: AVPlayerItem) {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerItemDidPlayToEnd:", name: AVPlayerItemDidPlayToEndTimeNotification, object: item)
    item.addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.New, context: nil)
}

private func unregisterForPlayToEndNotification(withItem item : AVPlayerItem) {
    NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object: item)
    item.removeObserver(self, forKeyPath: "timedMetadata", context: nil)
}

override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    print("keyPath: \(keyPath)")

    if keyPath == "timedMetadata" {

        let playerItem = object

        for metadata in playerItem!.timedMetadata {

            print("metadata: \(metadata)")

            let info = String(format: "%@", metadata.stringValue)
            let title = "\(info) ~ "
            currentItem?.title = title
            currentItem?.artwork = UIImage(named: "album-art")
            if !info.isEmpty {
                updateInfoCenter()
            }
            print("Now Playing: \(info)")
        }
        print("object: \(object)")
        print("change: \(change)")
        print("context: \(context)")
    }
}

`

Then at the bottom of the updateInfoCenter() (look below) I call a new protocol method self.delegate!.jukeboxMetadataDidUpdate(item) so I can update UILabels and UITextViews in any viewcontroller with the new title!

`

private func updateInfoCenter() {
    guard let item = self.currentItem else {return}

    let title = (item.title ?? item.localTitle) ?? item.URL.lastPathComponent!
    let currentTime = item.currentTime ?? 0
    let duration = item.duration ?? 0
    let trackNumber = self.playIndex
    let trackCount = self.queuedItems.count

    var nowPlayingInfo : [String : AnyObject] = [
        MPMediaItemPropertyPlaybackDuration : duration,
        MPMediaItemPropertyTitle : title,
        MPNowPlayingInfoPropertyElapsedPlaybackTime : currentTime,
        MPNowPlayingInfoPropertyPlaybackQueueCount :trackCount,
        MPNowPlayingInfoPropertyPlaybackQueueIndex : trackNumber,
        MPMediaItemPropertyMediaType : MPMediaType.AnyAudio.rawValue
    ]

    if let artist = item.artist {
        nowPlayingInfo[MPMediaItemPropertyArtist] = artist
    }

    if let album = item.album {
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
    }

    if let img = self.currentItem?.artwork {
        nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: img)
    }

    MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = nowPlayingInfo

    self.delegate!.jukeboxMetadataDidUpdate(item)
}

`

Just letting you know... maybe theres a way you can integrate this into jukeBox in a less hacky fashion. Since you know jukeBox very well.

teodorpatras commented 8 years ago

Cool, thanks for your contribution! I'll have a look later and see how I can integrate that into the main project.

alexrmacleod commented 8 years ago

@teodorpatras if you do decide to add it, I would really appreciate it if you let me know here... so I can update my app to your new logic. btw http://soundradio.hk/holding-page/ some songs on the constant stream don't seem to have any titlemeta data. would be great if you could accommodate for this with a notitle setting.

teodorpatras commented 8 years ago

new version released, thanks a lot for your input

alexrmacleod commented 8 years ago

@teodorpatras Awesome! how would I use this new feature? how to get to the time metadata?

teodorpatras commented 8 years ago

implement func jukeboxDidUpdateMetadata(jukebox : Jukebox, forItem: JukeboxItem) and then you can access item.meta

alexrmacleod commented 8 years ago

💯 thx!

alexrmacleod commented 8 years ago

@teodorpatras How would I check if jukebox stopped playing when I turn on airport mode in control center and killing the internet connection? I was previously doing this (in the jukebox object) by running if change!["new"]!.isKindOfClass(NSNull) (which gets run when there is no internet connection) in the observeValueForKeyPath and then setting the jukebox to self.state = .Failed. then in my Viewcontroller using the jukeboxStateDidChange delegate method I would update my view items accordingly. example below from my project.

Am I doing this wrong? currently in the new 0.1.3 jukebox update I don't know how to tell my view controller the music has stopped playing when I kill the internet connection via control center with airport mode turned on! any ideas?

`

private func registerForPlayToEndNotification(withItem item: AVPlayerItem) {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerItemDidPlayToEnd:", name: AVPlayerItemDidPlayToEndTimeNotification, object: item)
    item.addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.New, context: nil)
}

private func unregisterForPlayToEndNotification(withItem item : AVPlayerItem) {
    NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object: item)
    item.removeObserver(self, forKeyPath: "timedMetadata", context: nil)
}

override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {

    print("keyPath: \(keyPath)")

    if keyPath == "timedMetadata" {

        let playerItem = object as? AVPlayerItem

        if playerItem?.timedMetadata != nil {

            print("playerItem: \(playerItem)")
            print("playerItem?.timedMetadata: \(playerItem?.timedMetadata)")
            for metadata in playerItem!.timedMetadata! as [AVMetadataItem] {

                print("metadata: \(metadata)")

                let info = String(format: "%@", metadata.stringValue!)
                //                let title = "\(info) ~ "

                let infoArray = info.characters.split{$0 == " "}.map(String.init)
                let parsedArray = removeDuplicates(infoArray)
                print("parsedArray: \(parsedArray)")
                var title:String = ""
                //                for i in 0...parsedArray.count {
                //                    title.append(parsedArray[i])
                //                }
                for parsedInfo in parsedArray {
                    print("parsedInfo:!!!! \(parsedInfo)")
                    //                    title + parsedInfo
                    title.addString("\(parsedInfo) ")
                }

                print("title:!!!! \(title)")
                // or simply:
                // let fullNameArr = fullName.characters.split{" "}.map(String.init)
                //                print("infoArray: \(infoArray)")
                //                var i = 0
                //                for info in infoArray {
                //                    i += 1
                //                    if info.containsString("-") {
                //
                //                        print("##########: \(info)")
                //                    }
                //                }
                //                infoArray[0] // First
                //                infoArray[1] // Last

                currentItem?.title = title
                currentItem?.artwork = UIImage(named: "album-art")

                if !info.isEmpty {
                    updateInfoCenter()
                }

                print("Now Playing: \(info)")
            }
        }

        //Checking if internet connection has canceled
        if change!["new"]!.isKindOfClass(NSNull) {
            print("isKindOfClass")
            queuedItems.removeAll()
            self.state = .Failed
        }

        print("object: \(object)")
        print("change: \(change)")
        print("context: \(context)")
    }
}

`

teodorpatras commented 8 years ago

This is a sensible issue because if an item fails loading this might be due to several reasons, and there's no point in removing all items from the playlist and setting the status to .Failed. Right now the status remains on .Loading if it cannot fetch the data and if you turn the airplane mode off and loop through the songs list, it should automatically reload them.

alexrmacleod commented 8 years ago

That's strange because before I came up with the above solution I was watching jukeboxStateDidChange hoping for a state change when I turn on airport mode. However when I turn Airport mode on the jukebox state did NOT change. let alone change to .Loading. Can you please confirm that the jukebox status does change to jukebox.state == .Loading when airport mode is switched on?

My situation is a follows. I have an animation image loop that needs to be stopped when the internet connection is stopped (when the music stops streaming). How could I achieve this best with your framework?

Here is how I start the jukebox item

`

         jukebox = Jukebox(
            delegate: self,
            items: [
                JukeboxItem(
                    URL: NSURL(string: "http://soundradio.hk/sound-radio.m3u")!),
            ])
        print("jukebox.currentItem?.title: \(jukebox.currentItem?.title)")
        print("jukebox.currentItem?.playerItem: \(jukebox.currentItem?.playerItem)")

        //Play music when app opens
        jukebox.play()

`

alexrmacleod commented 8 years ago

@teodorpatras ???

teodorpatras commented 8 years ago

atm, when the loading fails, the status does not update, can you open an issue for that? and I'll take care of it when I get the time, right now I don't have so much time to spare