Marcus Kirsch
Marcus Kirsch

Reputation: 111

Getting any AudioPlayer to stream or play music

Bit at a loss here. I am on Xcode 7 and Swift 2 and I am trying to find and example of streaming audio that actually works.

Essentially I want to be able to stream, but I also tried this: Swift Radio Streaming AVPlayer

Which uses AVAudioPlayer. The github code works, but copying the code into my project with the same audio file crashes the app : Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SwiftBasicMVC_Git.AudioStreamProxy copyWithZone:]: unrecognized selector sent to instance 0x14fdea100'

I also tried some ACPlayer examples and AVQueuePlayer, without success of playing any music.

Upvotes: 0

Views: 1227

Answers (2)

Kemal Can Kaynak
Kemal Can Kaynak

Reputation: 1660

I use singleton for my basic radio application because need to reach and handle it on every page. It's so simple to create, let's start with declare singleton and variables. I will explain everything step by step;

import AVKit
import AVFoundation

private var radioContext: UInt8 = 1

final class RadioModel: NSObject {
    
    dynamic var radioItem:AVPlayerItem? = nil
    dynamic var radioPlayer:AVPlayer? = nil
    static let sharedModel = RadioModel()
    private final let dataModel = BasicRadioApp.DataManager
    
    private override init() {
        super.init()
        
        do {
            _ = try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
            _ = try AVAudioSession.sharedInstance().setActive(true)
            UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
        } catch {
            print("Error")
        }
    }

If you want to use AirPlay mode for podcast or radio, you have to control your sound object with KVO (Key Value Observer) and it's not supporting for Swift. So, our AVPlayerItem and AVPlayer objects are defined by dynamic which means that according to documentation;

Apply this modifier to any member of a class that can be represented by Objective-C. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched using the Objective-C runtime. Access to that member is never inlined or devirtualized by the compiler.

Because declarations marked with the dynamic modifier are dispatched using the Objective-C runtime, they’re implicitly marked with the objc attribute.

I'm getting data from server in my other Singleton object BasicRadioApp.

I need to control my audio session interacts with others (etc. Apple Watch) so need to set your AVAudioSessionCategory. Also i want to remote my audio on lockscreen and Glance in Apple Watch music app.

In AVAudioSession class, this function declared like that;

/* set session category */
    public func setCategory(category: String) throws

As you see, they throw exception so you need to set them in error handling like do/try concept for >Swift 2.0.

Let's try to play sound;

final func playRadio() {
        
         do {
                try removeObserverFromRadioItem()
            } catch {
                print("Can not remove observer from AVPlayerItem object!")
            }
            
            radioItem = AVPlayerItem(URL: NSURL(string: dataModel.radioStreamUrl)!)
            radioPlayer = AVPlayer(playerItem: radioItem!)
            radioItem!.addObserver(self, forKeyPath: "status", options: [.New, .Initial, .Old], context: &radioContext)
        
        if MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo == nil {
            
            let albumArt = MPMediaItemArtwork(image: UIImage(named: "AlbumArt")!)
            let songInfo: Dictionary = [MPMediaItemPropertyTitle: "Now listening Hard Rock Bla Bla FM",
                MPMediaItemPropertyArtist: "My Radyo",
                MPMediaItemPropertyAlbumTitle: "105.5 FM",
                MPMediaItemPropertyArtwork: albumArt]
            
            MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = songInfo
        }
    }

Before declaring my AVPlayerItem, i need to remove KVO on it if it exist. You'll see what removeObserverFromRadioItem() is doing next. I need to register my AVPlayerItem to KVO object and it'll handle to play or not. As i said, i need to remote my audio on lockscreen so i'm using MPNowPlayingInfoCenter for that.

final func stopRadio() {
        MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = nil
        radioPlayer?.pause()
    }

final func removeObserverFromRadioItem() throws {
        radioItem?.removeObserver(self, forKeyPath: "status", context: &radioContext)
    }

The most important part is here;

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if object as? AVPlayerItem == radioPlayer?.currentItem && keyPath! == "status" {
            if self.radioPlayer?.currentItem!.status == .Failed {
                print("STREAM FAILED")
            } else if self.radioPlayer?.currentItem!.status == .ReadyToPlay {
                self.radioPlayer!.play()
                radioDelegate?.RadioDidStartPlay()
            }
            return
        }
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }

How do you start your radio from any class?

class RadioController: UIViewController {
    
    let sharedRadioPlayer = RadioModel.sharedModel
    ..

override func viewDidLoad() {
        super.viewDidLoad()
        
        // check if radio is NOT playing
        if sharedRadioPlayer.radioPlayer?.rate < 1 {
            sharedRadioPlayer.playRadio()
        }
    }

Last step is remote my app from lockscreen. Stop, play etc. For remote that, i'm calling my method in RadioModel from AppDelegate. In AppDelegate;

override func remoteControlReceivedWithEvent(event: UIEvent?) {
        RadioModel.sharedModel.handleRemoteControlEvent(event!)
    }

and in RadioModel;

func handleRemoteControlEvent(responseEvent: UIEvent) {
        if responseEvent.type == UIEventType.RemoteControl {
            switch(responseEvent.subtype) {
            case UIEventSubtype.RemoteControlPlay:
                playRadio()
                break
            case UIEventSubtype.RemoteControlPause:
                stopRadio()
                break
            case UIEventSubtype.RemoteControlTogglePlayPause:
                if self.radioPlayer!.rate > 0 {
                    stopRadio()
                } else {
                    playRadio()
                }
                break
            default:
                print("Remote Error")
            }
        }
    }

Don't forget that in your app TARGETS > Capabilities > switch Background Modes on and select Audio, AirPlay and Picture in Picture. Then check your info.plist and search "Requires background modes". If it's not exist, create new one as Array and set item that "App plays audio or streams audio/video using AirPlay". That's it now you can play airPlay radio/stream.

Upvotes: 2

Marcus Kirsch
Marcus Kirsch

Reputation: 111

DONT forget to add : App Transport Security Settings As of XCode 7.x AND iOS 9.x

Needed in the pList settings : Add Arbitrary as YES

Upvotes: 1

Related Questions