Compare commits

...

22 Commits

Author SHA1 Message Date
jorgenhenrichsen 31c3c728f6 Update podscpec
Version
2018-03-22 12:14:43 +01:00
jorgenhenrichsen d6375d996d Merge pull request #4 from jorgenhenrichsen/feature/audio-items-supply-commands
Audio items supply commands
2018-03-22 12:14:13 +01:00
jorgenhenrichsen 730629a07a Merge branch 'master' into feature/audio-items-supply-commands 2018-03-22 12:01:38 +01:00
Jørgen Henrichsen 0bb59cde4a Update README
RemoteCommandable
2018-03-22 11:48:40 +01:00
Jørgen Henrichsen 661a7755df AudioItems can select their own remote commands.
ALso fixed a problem where remote commands would not be disabled properly.
2018-03-22 11:40:27 +01:00
jorgenhenrichsen 01702b41f1 Update README
Correct method signature for enabling remote commands.
2018-03-22 10:11:34 +01:00
jorgenhenrichsen 47a091bb9a Update podspec
Bump version. Update description.
2018-03-22 10:00:37 +01:00
jorgenhenrichsen f2419282d0 Merge pull request #3 from jorgenhenrichsen/remote-event-handler
Remote event handler
2018-03-22 09:58:03 +01:00
Jørgen Henrichsen 41ac357193 Update README
Restructured sections, cleaned up. New bit about remote commands.
2018-03-22 01:41:01 +01:00
Jørgen Henrichsen 670c3d423c Removed old code 2018-03-21 00:14:30 +01:00
Jørgen Henrichsen ca8f35166d RemoteEventController that handles remote events 2018-03-21 00:13:17 +01:00
Jørgen Henrichsen e10ea3cd2a Remove current item on stop() 2018-03-20 14:57:51 +01:00
Jørgen Henrichsen 5e8bb37c17 Add method to reload the current info for the now playing center. 2018-03-20 14:19:33 +01:00
Jørgen Henrichsen 74d03d3e13 Stop function 2018-03-20 14:19:24 +01:00
Jørgen Henrichsen 6af21680c3 Made config props public 2018-03-20 13:29:47 +01:00
Jørgen Henrichsen 3342ba2f30 Make public 2018-03-20 11:54:51 +01:00
Jørgen Henrichsen ca292281d2 Add self in closure 2018-03-20 11:22:44 +01:00
Jørgen Henrichsen 02b6755362 Make getArtwork() handler escaping 2018-03-20 11:18:56 +01:00
jorgenhenrichsen 1cbbf7c686 Update README
travis-ci.org shield
2018-03-19 18:06:25 +01:00
jorgenhenrichsen cc329d8d9e Merge pull request #2 from jorgenhenrichsen/fix/use-functions-in-audioitem
Fix/use functions in audioitem
2018-03-19 17:48:24 +01:00
Jørgen Henrichsen 2129df3071 Update usage of AudioItem 2018-03-19 17:44:18 +01:00
Jørgen Henrichsen 12c3f1bcd6 Use functions for data in AudioItem
To avoid collisions with properties in objects that implement it.
2018-03-19 17:41:46 +01:00
11 changed files with 458 additions and 159 deletions
+8
View File
@@ -11,7 +11,9 @@
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */; };
0773265C205ED6C400C4D1CD /* AVPlayerTimeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */; };
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0773265F205ED7BF00C4D1CD /* AudioItem.swift */; };
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775574A2061C1820002C6A1 /* RemoteCommand.swift */; };
07F41B1A205FC0B100E25749 /* AudioSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B19205FC0B100E25749 /* AudioSessionController.swift */; };
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B2120614BDC00E25749 /* RemoteCommandController.swift */; };
0A2CA8B0DD7E300ABFCF1956E6B58248 /* Nimble.h in Headers */ = {isa = PBXBuildFile; fileRef = 2727DBDF3F41A52F002F6B1992165881 /* Nimble.h */; settings = {ATTRIBUTES = (Public, ); }; };
0AB1F4B2325838C42320440B1E6BAB1E /* QuickSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 38A82DE4A0130E4C946A904FEDDD74CA /* QuickSpec.m */; };
12BBD20E3BEA5D34DCF748EF73904EAF /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEA1DF1C7C22E7B20D2DC30FB9D3EF1 /* Configuration.swift */; };
@@ -170,7 +172,9 @@
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserver.swift; sourceTree = "<group>"; };
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserver.swift; sourceTree = "<group>"; };
0773265F205ED7BF00C4D1CD /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioItem.swift; path = SwiftAudio/Classes/AudioItem.swift; sourceTree = "<group>"; };
0775574A2061C1820002C6A1 /* RemoteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommand.swift; path = SwiftAudio/Classes/RemoteCommand.swift; sourceTree = "<group>"; };
07F41B19205FC0B100E25749 /* AudioSessionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioSessionController.swift; path = SwiftAudio/Classes/AudioSessionController.swift; sourceTree = "<group>"; };
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommandController.swift; path = SwiftAudio/Classes/RemoteCommandController.swift; sourceTree = "<group>"; };
0A9D7EA20C39A55A1EF0C23094895A75 /* Quick-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Quick-dummy.m"; sourceTree = "<group>"; };
0AB0094EAAC3C18B000459B228F01637 /* ThrowAssertion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ThrowAssertion.swift; path = Sources/Nimble/Matchers/ThrowAssertion.swift; sourceTree = "<group>"; };
0AC83F8F84F3F843896F4907ACFE3394 /* Pods-SwiftAudio_Tests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Tests-resources.sh"; sourceTree = "<group>"; };
@@ -390,6 +394,8 @@
EEC13E9D5F4FD604F703ACBF80ECA35E /* NowPlayingInfoProperty.swift */,
2B276281A286DB68E700474617B8AE3B /* TimeEventFrequency.swift */,
07F41B19205FC0B100E25749 /* AudioSessionController.swift */,
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */,
0775574A2061C1820002C6A1 /* RemoteCommand.swift */,
07732656205ED6C400C4D1CD /* Observer */,
25961FA3FF989AFD1E08A55F4E464276 /* AVPlayerWrapper */,
853A8C0AF6F0B4902930084E06DEA640 /* Support Files */,
@@ -980,7 +986,9 @@
5E48B05B2B78E0AD05E2DBA8B1862AE5 /* MediaItemProperty.swift in Sources */,
0773265A205ED6C400C4D1CD /* AVPlayerObserver.swift in Sources */,
FCE55A287889D68B475EBFBE8AB26E4B /* NowPlayingInfoController.swift in Sources */,
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */,
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */,
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */,
6E0C5034AB99CC7E6B0BF002D488D85D /* NowPlayingInfoProperty.swift in Sources */,
A45D4CA6BC297DB896B20F391DF60D61 /* SwiftAudio-dummy.m in Sources */,
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */,
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "22AMillion.jpg",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
+12 -5
View File
@@ -21,13 +21,20 @@ class ViewController: UIViewController {
var audioPlayer: AudioPlayer = AudioPlayer()
let audioSessionController: AudioSessionController = AudioSessionController.shared
let localSource = DefaultAudioItem(audioUrl: Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!, artist: "Artist", title: "Title", albumTitle: "Album", sourceType: .file, artwork: #imageLiteral(resourceName: "cover"))
let streamSource = DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/4839b070015ab7d6de9fec1756e1f3096d908fba", artist: "Artist", title: "Title", albumTitle: "Album", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover"))
let streamSource = DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
var artwork: MPMediaItemArtwork!
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer.delegate = self
audioPlayer.remoteCommands = [
.stop,
.togglePlayPause,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
try? audioSessionController.activateSession()
let image = #imageLiteral(resourceName: "cover")
@@ -37,15 +44,15 @@ class ViewController: UIViewController {
}
@IBAction func playA(_ sender: Any) {
audioPlayer.load(item: localSource)
try? audioPlayer.load(item: localSource)
}
@IBAction func playB(_ sender: Any) {
audioPlayer.load(item: streamSource)
try? audioPlayer.load(item: streamSource)
}
@IBAction func togglePlay(_ sender: Any) {
audioPlayer.togglePlaying()
try? audioPlayer.togglePlaying()
}
@IBAction func startScrubbing(_ sender: UISlider) {
@@ -53,7 +60,7 @@ class ViewController: UIViewController {
}
@IBAction func scrubbing(_ sender: UISlider) {
audioPlayer.seek(to: Double(slider.value))
try? audioPlayer.seek(to: Double(slider.value))
}
func update() {
+37 -20
View File
@@ -1,11 +1,11 @@
# SwiftAudio
[![Build Status](https://travis-ci.com/jorgenhenrichsen/SwiftAudio.svg?token=vuPZfsuL1yx6emZGn1Qz&branch=master)](https://travis-ci.com/jorgenhenrichsen/SwiftAudio)
[![Build Status](https://travis-ci.org/jorgenhenrichsen/SwiftAudio.svg?branch=master)](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![License](https://img.shields.io/cocoapods/l/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
SwiftAudio aims to make audio playback easier on iOS. No more boundaryTimeObserver, periodicTimeObserver, KVO and NotificationCenter to get state update from the player. It also updates NowPlayingInfo for you and handles remote events.
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
## Example
@@ -25,16 +25,25 @@ pod 'SwiftAudio'
## Usage
Using the AudioPlayer:
### AudioPlayer
```swift
let player = AudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem)
```
The player will load the track and start playing when ready.
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time etc.
To enable this behaviour the AudioItems supplied to the player must supply these values. You can either use the `DefaultAudioItem` or make your playback objects implement the `AudioItem` protocol.
You must also remember to set a AudioSessionCategory that supports this behaviour, and activate the session:
The player will load the track and start playing when ready. To disable this behaviour use `load(item:playWhenReady:)` and pass in `false`. This is `true` by default. To get notified of events during playback and loading, implement `AudioPlayerDelegate` and the player will notify you with changes.
#### States
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The different states:
+ **idle**: The player is doing nothing, no item is set as current. This is the default state.
+ **ready**: The player has its current item set and is ready to start loading for playback. This is when you can call `play()` if you supplied `playWhenReady=false` when calling `load(item:playWhenReady)`.
+ **loading**: The player is loading the track and will start playback soon.
+ **playing**: The player is playing.
+ **paused**: The player is paused.
### Audio Session
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionCategory`:
```swift
try? AudioSessionController.set(category: .playback)
//...
@@ -42,21 +51,30 @@ try? AudioSessionController.set(category: .playback)
// This is to avoid interrupting other audio without the need to do it.
try? AudioSessionController.activateSession()
```
If you want audio to continue playing when the app is closed or phone locked remember to activate background audio:
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'
The player will also handle remote events received from `MPRemoteCommandCenter`'s shared instance. To enable this, you have to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
If you want audio to continue playing when the app is inactive, remember to activate background audio:
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'.
To get notified of events during playback and loading, implement `AudioPlayerDelegate`
The player will notify you with changes.
### Now Playing Info
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time if the passed in `AudioItem` supports this.
If you need to set additional properties for some items use `AudioPlayer.add(property:)`. Available properties can be found in `NowPlayingInfoProperty`.
### Remote Commands
The player will handle remote commands received from `MPRemoteCommandCenter`'s shared instance, enabled by:
```swift
audioPlayer.remoteCommands = [
.play,
.pause,
.skipForward(intervals: [30]),
.skipBackward(intervals: [30]),
]
```
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable`. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
**Remember** to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
### States
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The delegate will be called when the state is updated.
+ **idle**: The player is doing nothing, no item is set as current. This is the default state.
+ **ready**: The player has its current item set and is ready to start loading for playback. This is when you can call `play()` if you supplied `playWhenReady=false` when calling `load(item:playWhenReady)`.
+ **loading**: The player is loading the track and will start playback soon.
+ **playing**: The player is playing.
+ **paused**: The player is paused.
## Configuration
@@ -68,7 +86,6 @@ Currently some configuration options are supported:
+ `automaticallyUpdateNowPlayingInfo`: If you want to handle updating of the `MPNowPlayingInfoCenter` yourself, set this to `false`. Default is `true`.
## Plans
* More configuration on the RemoteHandlerEvents
* Ability to queue items
## Author
+3 -3
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.1.0'
s.version = '0.2.1'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
@@ -18,8 +18,8 @@ Pod::Spec.new do |s|
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
SwiftAudio aims to make audio playback easier on iOS. No more boundaryTimeObserver, periodicTimeObserver, KVO and NotificationCenter to get state update from the player. It also updates NowPlayingInfo for you and handles remote events.
DESC
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
DESC
s.homepage = 'https://github.com/jorgenhenrichsen/SwiftAudio'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
+28 -14
View File
@@ -7,7 +7,6 @@
import Foundation
public enum SourceType {
case stream
case file
@@ -15,22 +14,17 @@ public enum SourceType {
public protocol AudioItem {
var audioUrl: String { get }
var artist: String? { get }
var title: String? { get }
var albumTitle: String? { get }
var sourceType: SourceType { get }
func getArtwork(_ handler: (UIImage?) -> Void)
func getSourceUrl() -> String
func getArtist() -> String?
func getTitle() -> String?
func getAlbumTitle() -> String?
func getSourceType() -> SourceType
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
}
public struct DefaultAudioItem: AudioItem {
public var audioUrl: String
public var artist: String?
@@ -52,7 +46,27 @@ public struct DefaultAudioItem: AudioItem {
self.artwork = artwork
}
public func getArtwork(_ handler: (UIImage?) -> Void) {
public func getSourceUrl() -> String {
return audioUrl
}
public func getArtist() -> String? {
return artist
}
public func getTitle() -> String? {
return title
}
public func getAlbumTitle() -> String? {
return albumTitle
}
public func getSourceType() -> SourceType {
return sourceType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
}
+80 -115
View File
@@ -28,16 +28,22 @@ public class AudioPlayer {
let wrapper: AVPlayerWrapper
let nowPlayingInfoController: NowPlayingInfoController
let remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()
let remoteCommandController: RemoteCommandController
public weak var delegate: AudioPlayerDelegate?
public var currentItem: AudioItem?
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
@@ -74,7 +80,7 @@ public class AudioPlayer {
Indicates wether the player should automatically delay playback in order to minimize stalling.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
*/
var automaticallyWaitsToMinimizeStalling: Bool {
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
@@ -86,7 +92,7 @@ public class AudioPlayer {
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true`
*/
var bufferDuration: TimeInterval {
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { wrapper.bufferDuration = newValue }
}
@@ -94,7 +100,7 @@ public class AudioPlayer {
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
var timeEventFrquency: TimeEventFrequency {
public var timeEventFrquency: TimeEventFrequency {
get { return wrapper.timeEventFrequency }
set { wrapper.timeEventFrequency = newValue }
}
@@ -103,12 +109,12 @@ public class AudioPlayer {
The player volume, from 0.0 to 1.0
Default is 1.0
*/
var volume: Float {
public var volume: Float {
get { return wrapper.volume }
set { wrapper.volume = newValue }
}
// MARK: - Public Methods
// MARK: - Init
/**
Create a new AudioManager.
@@ -119,69 +125,114 @@ public class AudioPlayer {
public init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = NowPlayingInfoController(infoCenter: infoCenter)
self.remoteCommandController = RemoteCommandController()
self.wrapper.delegate = self
connectToCommandCenter()
self.remoteCommandController.audioPlayer = self
}
// MARK: - Player Actions
/**
Load an AudioItem into the manager.
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
public func load(item: AudioItem, playWhenReady: Bool = true) {
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
switch item.sourceType {
switch item.getSourceType() {
case .stream:
try? self.wrapper.load(fromUrlString: item.audioUrl, playWhenReady: playWhenReady)
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
case .file:
print(item.audioUrl)
try? self.wrapper.load(fromFilePath: item.audioUrl, playWhenReady: playWhenReady)
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
}
self.currentItem = item
set(item: item)
setArtwork(forItem: item)
enableRemoteCommands(forItem: item)
}
/**
Toggle playback status.
*/
public func togglePlaying() {
try? self.wrapper.togglePlaying()
public func togglePlaying() throws {
try self.wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
try? self.wrapper.play()
public func play() throws {
try self.wrapper.play()
}
/**
Pause playback
*/
public func pause() {
try? self.wrapper.pause()
public func pause() throws {
try self.wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
public func stop() {
self.reset()
self.wrapper.stop()
}
/**
Seek to a specific time in the item.
*/
public func seek(to seconds: TimeInterval) {
try? self.wrapper.seek(to: seconds)
public func seek(to seconds: TimeInterval) throws {
try self.wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
/**
Set the remote commands that should be activated and handled.
Calling this will disable all earlier enabled commands, so include all commands you need.
*/
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
print("Enabling remote commands for item")
self.enableRemoteCommands(item.getCommands())
}
else {
self.enableRemoteCommands(remoteCommands)
}
}
// MARK: - NowPlayingInfo
/**
Reloads the NowPlayingInfo from the current AudioItem.
*/
public func reloadNowPlayingInfo() {
guard let item = currentItem else { return }
set(item: item)
setArtwork(forItem: item)
updatePlaybackValues()
}
public func add(property: NowPlayingInfoKeyValue) {
self.nowPlayingInfoController.set(keyValue: property)
}
func set(item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.artist),
MediaItemProperty.title(item.title),
MediaItemProperty.albumTitle(item.albumTitle),
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
}
@@ -189,11 +240,12 @@ public class AudioPlayer {
guard automaticallyUpdateNowPlayingInfo else { return }
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
}
}
}
@@ -205,99 +257,12 @@ public class AudioPlayer {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)))
}
// MARK: - Remote Commands Handlers
// MARK: - Private
func connectToCommandCenter() {
self.remoteCommandCenter.playCommand.addTarget(handler: handlePlayCommand(event:))
self.remoteCommandCenter.pauseCommand.addTarget(handler: handlePauseCommand(event:))
self.remoteCommandCenter.togglePlayPauseCommand.addTarget(handler: handleTogglePlaybackCommand(event:))
self.remoteCommandCenter.stopCommand.addTarget(handler: handleStopCommand(event:))
self.remoteCommandCenter.skipForwardCommand.addTarget(handler: handleSkipForwardCommand(event:))
remoteCommandCenter.skipForwardCommand.preferredIntervals = [15]
self.remoteCommandCenter.skipBackwardCommand.addTarget(handler: handleSkipBackwardCommand(event:))
remoteCommandCenter.skipBackwardCommand.preferredIntervals = [15]
self.remoteCommandCenter.changePlaybackPositionCommand.addTarget(handler: handleChangePlaybackPositionCommand(event:))
private func reset() {
self.currentItem = nil
}
func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.PlaybackError {
switch error {
case .noLoadedItem:
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
func handlePlayCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
do {
try self.wrapper.play()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return getRemoteCommandHandlerStatus(forError: error)
}
}
func handlePauseCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
do {
try self.wrapper.pause()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return getRemoteCommandHandlerStatus(forError: error)
}
}
func handleTogglePlaybackCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
do {
try self.wrapper.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return getRemoteCommandHandlerStatus(forError: error)
}
}
func handleStopCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.wrapper.stop()
return MPRemoteCommandHandlerStatus.success
}
func handleSkipForwardCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand {
if let interval = command.preferredIntervals.first {
self.seek(to: currentTime + interval.doubleValue)
return MPRemoteCommandHandlerStatus.success
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
func handleSkipBackwardCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand {
if let interval = command.preferredIntervals.first {
self.seek(to: currentTime - interval.doubleValue)
return MPRemoteCommandHandlerStatus.success
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
func handleChangePlaybackPositionCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let event = event as? MPChangePlaybackPositionCommandEvent {
do {
try wrapper.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
}
extension AudioPlayer: AVPlayerWrapperDelegate {
@@ -65,7 +65,7 @@ public class AudioSessionController {
/**
True if another app is currently playing audio.
*/
var isOtherAudioPlaying: Bool {
public var isOtherAudioPlaying: Bool {
return audioSession.isOtherAudioPlaying
}
@@ -74,7 +74,7 @@ public class AudioSessionController {
- warning: This will only be correct if the audiosession is activated through this class!
*/
var audioSessionIsActive: Bool = false
public var audioSessionIsActive: Bool = false
private init() {}
+103
View File
@@ -0,0 +1,103 @@
//
// RemoteCommand.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 20/03/2018.
//
import Foundation
import MediaPlayer
public typealias RemoteCommandHandler = (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus
public protocol RemoteCommandProtocol {
associatedtype Command: MPRemoteCommand
var commandKeyPath: KeyPath<MPRemoteCommandCenter, Command> { get }
var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler> { get }
}
public struct BaseRemoteCommand: RemoteCommandProtocol {
public static let play = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
public static let pause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
public static let stop = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
public static let togglePlayPause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
public typealias Command = MPRemoteCommand
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPRemoteCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
}
public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
public static let changePlaybackPosition = ChangePlaybackPositionCommand(commandKeyPath: \MPRemoteCommandCenter.changePlaybackPositionCommand, handlerKeyPath: \RemoteCommandController.handleChangePlaybackPositionCommand)
public typealias Command = MPChangePlaybackPositionCommand
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPChangePlaybackPositionCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
}
public struct SkipIntervalCommand: RemoteCommandProtocol {
public static let skipForward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
public static let skipBackward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipBackwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipBackwardCommand)
public typealias Command = MPSkipIntervalCommand
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPSkipIntervalCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
func set(preferredIntervals: [NSNumber]) -> SkipIntervalCommand {
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].preferredIntervals = preferredIntervals
return self
}
}
public enum RemoteCommand {
case play
case pause
case stop
case togglePlayPause
case changePlaybackPosition
case skipForward(preferredIntervals: [NSNumber])
case skipBackward(preferredIntervals: [NSNumber])
/**
All values in an array for convenience.
Don't use for associated values.
*/
static func all() -> [RemoteCommand] {
return [
.play,
.pause,
.stop,
.togglePlayPause,
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
]
}
}
@@ -0,0 +1,164 @@
//
// File.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 20/03/2018.
//
import Foundation
import MediaPlayer
public protocol RemoteCommandable {
func getCommands() -> [RemoteCommand]
}
public class RemoteCommandController {
private let center = MPRemoteCommandCenter.shared()
weak var audioPlayer: AudioPlayer?
init() {}
/**
Enable a set of RemoteCommands. Calling this will disable all earlier set commands, so include all commands that needs to be active.
- parameter commands: The RemoteCommands that is to be enabled.
*/
public func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
commands.forEach { (command) in
self.enable(command: command)
}
}
private func disable(commands: [RemoteCommand]) {
commands.forEach { (command) in
self.disable(command: command)
}
}
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
private func disableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = false
center[keyPath: command.commandKeyPath].removeTarget(self[keyPath: command.handlerKeyPath])
}
private func enable(command: RemoteCommand) {
switch command {
case .play: self.enableCommand(BaseRemoteCommand.play)
case .pause: self.enableCommand(BaseRemoteCommand.pause)
case .stop: self.enableCommand(BaseRemoteCommand.stop)
case .togglePlayPause: self.enableCommand(BaseRemoteCommand.togglePlayPause)
case .changePlaybackPosition: self.enableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(let preferredIntervals):
self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
case .skipBackward(let preferredIntervals):
self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
}
}
private func disable(command: RemoteCommand) {
switch command {
case .play: self.disableCommand(BaseRemoteCommand.play)
case .pause: self.disableCommand(BaseRemoteCommand.pause)
case .stop: self.disableCommand(BaseRemoteCommand.stop)
case .togglePlayPause: self.disableCommand(BaseRemoteCommand.togglePlayPause)
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
}
}
// MARK: - Handlers
lazy var handlePlayCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.play()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
lazy var handlePauseCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.pause()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
lazy var handleStopCommand: RemoteCommandHandler = { (event) in
self.audioPlayer?.stop()
return .success
}
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
lazy var handleSkipForwardCommand: RemoteCommandHandler = { (event) in
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
try? audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleSkipBackwardCommand: RemoteCommandHandler = { (event) in
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
try? audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { (event) in
if let event = event as? MPChangePlaybackPositionCommandEvent {
do {
try self.audioPlayer?.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.PlaybackError {
switch error {
case .noLoadedItem:
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
}