Compare commits

..

3 Commits

Author SHA1 Message Date
Jørgen Henrichsen e6ca8c9b1e Update README.
Add part about using Combine for events.
2019-06-21 13:48:11 +02:00
Jørgen Henrichsen 83318d3ff0 Make all error enums public. Add EventError. 2019-06-21 13:28:51 +02:00
Jørgen Henrichsen 3c5f858a94 Add support for Combine in events.
Add an `EventPublisher` and a publisher property in the `Event`-class.
Can be used to subscribe to events with a `Subscriber`.
2019-06-21 13:22:09 +02:00
22 changed files with 329 additions and 456 deletions
+1
View File
@@ -0,0 +1 @@
4.2
+4 -18
View File
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -62,7 +63,7 @@
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
<rect key="frame" x="67.5" y="84" width="240" height="240"/>
<rect key="frame" x="67" y="84" width="240" height="240"/>
<constraints>
<constraint firstAttribute="width" constant="240" id="5Sj-BZ-sg4"/>
<constraint firstAttribute="height" constant="240" id="Hij-Yw-6Lg"/>
@@ -104,37 +105,24 @@
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="1ML-yD-9Rf">
<rect key="frame" x="177.5" y="587" width="20" height="20"/>
</activityIndicatorView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ErrorText" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iCe-6A-2My">
<rect key="frame" x="158.5" y="588.5" width="58.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="0eh-sL-186"/>
<constraint firstItem="iCe-6A-2My" firstAttribute="centerY" secondItem="1ML-yD-9Rf" secondAttribute="centerY" id="4Fp-kE-AAg"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="54L-0h-0ba"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="9Uh-K9-988"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="BhV-UD-qhh"/>
<constraint firstItem="iCe-6A-2My" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="Dhm-Bn-wZH"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="GhI-f1-DkR"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="HoH-i0-yof"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="Nw7-WM-LFd"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="O0h-NL-iXW"/>
<constraint firstItem="1ML-yD-9Rf" firstAttribute="top" secondItem="EOo-zV-6l2" secondAttribute="bottom" constant="20" id="Uop-aD-I5b"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="top" secondItem="FCd-3e-22D" secondAttribute="bottom" constant="30" id="W4w-6K-AW8"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="top" secondItem="T7Y-1Q-7UU" secondAttribute="bottom" constant="25" id="XgV-XL-QCL"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="YUE-uf-Rp1"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="ZkD-u2-Zbr"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="top" secondItem="dfk-yr-rwm" secondAttribute="bottom" constant="4" id="baR-zV-tgo"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="eNt-u9-qot"/>
<constraint firstItem="1ML-yD-9Rf" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="fdl-RK-Hq8"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="16" id="hEd-b2-Ggo"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="top" secondItem="l9B-hM-Ajc" secondAttribute="bottom" constant="30" id="ikz-ZP-jNM"/>
<constraint firstAttribute="trailingMargin" secondItem="RX3-VR-CL6" secondAttribute="trailing" constant="16" id="kSP-Mq-R5P"/>
@@ -147,9 +135,7 @@
<connections>
<outlet property="artistLabel" destination="T7Y-1Q-7UU" id="b5S-lt-PqG"/>
<outlet property="elapsedTimeLabel" destination="3CL-8o-zYW" id="7Wg-7X-Vrd"/>
<outlet property="errorLabel" destination="iCe-6A-2My" id="T4b-0b-wdM"/>
<outlet property="imageView" destination="FCd-3e-22D" id="gKL-za-haV"/>
<outlet property="loadIndicator" destination="1ML-yD-9Rf" id="Xes-Ag-vhg"/>
<outlet property="playButton" destination="EOo-zV-6l2" id="2d1-ad-s1k"/>
<outlet property="remainingTimeLabel" destination="RVb-HZ-QCX" id="8hp-CK-XjF"/>
<outlet property="slider" destination="RWN-If-dGG" id="Yxw-Gf-bR3"/>
+5 -44
View File
@@ -21,12 +21,9 @@ class ViewController: UIViewController {
@IBOutlet weak var elapsedTimeLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var artistLabel: UILabel!
@IBOutlet weak var loadIndicator: UIActivityIndicatorView!
@IBOutlet weak var errorLabel: UILabel!
private var isScrubbing: Bool = false
private let controller = AudioController.shared
private var lastLoadFailed: Bool = false
var isScrubbing: Bool = false
let controller = AudioController.shared
override func viewDidLoad() {
super.viewDidLoad()
@@ -35,23 +32,13 @@ class ViewController: UIViewController {
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
controller.player.event.fail.addListener(self, handlePlayerFailure)
updateMetaData()
handleAudioPlayerStateChange(data: controller.player.playerState)
}
@IBAction func togglePlay(_ sender: Any) {
if !controller.audioSessionController.audioSessionIsActive {
if (!controller.audioSessionController.audioSessionIsActive) {
try? controller.audioSessionController.activateSession()
}
if lastLoadFailed, let item = controller.player.currentItem {
lastLoadFailed = false
errorLabel.isHidden = true
try? controller.player.load(item: item, playWhenReady: true)
}
else {
controller.player.togglePlaying()
}
controller.player.togglePlaying()
}
@IBAction func previous(_ sender: Any) {
@@ -97,31 +84,16 @@ class ViewController: UIViewController {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
}
func setErrorMessage(_ message: String) {
self.loadIndicator.stopAnimating()
errorLabel.isHidden = false
errorLabel.text = message
}
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print(data)
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
case .loading:
self.loadIndicator.startAnimating()
self.updateMetaData()
self.updateTimeValues()
case .buffering:
self.loadIndicator.startAnimating()
case .ready:
self.loadIndicator.stopAnimating()
self.updateMetaData()
self.updateTimeValues()
case .playing, .paused, .idle:
self.loadIndicator.stopAnimating()
case .loading, .playing, .paused, .idle:
self.updateTimeValues()
}
}
@@ -149,15 +121,4 @@ class ViewController: UIViewController {
try? controller.audioSessionController.set(category: .playback)
}
func handlePlayerFailure(data: AudioPlayer.FailEventData) {
if let error = data as NSError? {
if error.code == -1009 {
lastLoadFailed = true
DispatchQueue.main.async {
self.setErrorMessage("Network disconnected. Please try again...")
}
}
}
}
}
+1 -6
View File
@@ -30,12 +30,7 @@ class AVPlayerWrapperTests: XCTestCase {
XCTAssert(wrapper.state == AVPlayerWrapperState.idle)
}
func test_AVPlayerWrapper__state__when_loading_a_source__should_be_loading() {
wrapper.load(from: Source.url, playWhenReady: false)
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.loading)
}
func test_AVPlayerWrapper__state__when_loading_a_source__should_eventually_be_ready() {
func test_AVPlayerWrapper__state__when_loading_a_source__should_be_ready() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .ready {
+24 -23
View File
@@ -29,11 +29,6 @@ class AudioPlayerTests: XCTestCase {
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
}
func test_AudioPlayer__state__load_source__should_be_loading() {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
}
func test_AudioPlayer__state__load_source__should_be_ready() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
@@ -73,9 +68,9 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__pausing_source__should_be_paused() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
listener.stateUpdate = { state in
switch state {
case .playing: audioPlayer?.pause()
case .playing: self.audioPlayer.pause()
case .paused: expectation.fulfill()
default: break
}
@@ -87,11 +82,11 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__stopping_source__should_be_idle() {
let expectation = XCTestExpectation()
var hasBeenPlaying: Bool = false
listener.stateUpdate = { [weak audioPlayer] state in
listener.stateUpdate = { state in
switch state {
case .playing:
hasBeenPlaying = true
audioPlayer?.stop()
self.audioPlayer.stop()
case .idle:
if hasBeenPlaying {
expectation.fulfill()
@@ -122,6 +117,22 @@ class AudioPlayerTests: XCTestCase {
// wait(for: [expectation], timeout: 20.0)
// }
func test_AudioPlayer__currentTime__when_loading_source_with_intial_time__should_be_equal_to_initial_time() {
let expectation = XCTestExpectation()
let item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
listener.stateUpdate = { state in
switch state {
case .ready:
if self.audioPlayer.currentTime == item.getInitialTime() {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: item, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
@@ -130,11 +141,10 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__rate__playing_source__should_be_1() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
listener.stateUpdate = { state in
switch state {
case .playing:
if audioPlayer.rate == 1.0 {
if self.audioPlayer.rate == 1.0 {
expectation.fulfill()
}
default: break
@@ -152,11 +162,10 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
listener.stateUpdate = { state in
switch state {
case .ready:
if audioPlayer.currentItem != nil {
if self.audioPlayer.currentItem != nil {
expectation.fulfill()
}
default: break
@@ -182,20 +191,12 @@ class AudioPlayerEventListener {
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
var seekCompletion: (() -> Void)?
weak var audioPlayer: AudioPlayer?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
}
deinit {
audioPlayer?.event.stateChange.removeListener(self)
audioPlayer?.event.seek.removeListener(self)
audioPlayer?.event.secondElapse.removeListener(self)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
+16 -2
View File
@@ -25,13 +25,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.11.2'
pod 'SwiftAudio', '~> 0.9.2'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.11.2
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.2
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
@@ -63,6 +63,18 @@ class MyCustomViewController: UIViewController {
}
```
If you want to use the [Combine](https://developer.apple.com/documentation/combine) framework for events, each event in the `AudioPlayer` has an `EventPublisher` that can be subscribed to:
```swift
let audioPlayer = AudioPlayer()
audioPlayer.event.stateChange.publisher.subscribe(subscriber: someSubscriber)
/// Using a Sink
audioPlayer.event.stateChange.publisher.sink { state in
/// Handle state change here.
}
```
**Important**: This requires iOS version equal to or later than 13.0. If an application needs to support older iOS versions it is recommended to use the regular `Event.addListener(listener:)` method.
#### QueuedAudioPlayer
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
```swift
@@ -138,6 +150,8 @@ The current info can be cleared with:
```
### Remote Commands
**First** go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
To enable remote commands for the player you need to populate the RemoteCommands array for the player:
```swift
audioPlayer.remoteCommands = [
+19 -2
View File
@@ -8,18 +8,35 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.11.2'
s.version = '0.9.2'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-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'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Jørgen Henrichsen' => 'jh.henrichs@gmail.com' }
s.source = { :git => 'https://github.com/jorgenhenrichsen/SwiftAudio.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '10.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudio/Classes/**/*'
# s.resource_bundles = {
# 'SwiftAudio' => ['SwiftAudio/Assets/*.png']
# }
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
end
+5 -3
View File
@@ -10,18 +10,20 @@ import Foundation
public struct APError {
enum LoadError: Error {
public enum LoadError: Error {
case invalidSourceUrl(String)
}
enum PlaybackError: Error {
public enum PlaybackError: Error {
case noLoadedItem
}
enum QueueError: Error {
public enum QueueError: Error {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
}
public enum EventError: Error {}
}
@@ -168,9 +168,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady
@@ -178,16 +176,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
recreateAVPlayer()
}
self._pendingAsset = AVURLAsset(url: url, options: options)
// Set item
self._pendingAsset = AVURLAsset(url: url)
if let pendingAsset = _pendingAsset {
self._state = .loading
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
guard let self = self else {
return
}
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: {
var error: NSError? = nil
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
@@ -209,6 +201,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .failed:
// print("load asset failed")
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
@@ -216,6 +209,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .cancelled:
// print("load asset cancelled")
break
default:
@@ -226,10 +220,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady, options: options)
self.load(from: url, playWhenReady: playWhenReady)
}
// MARK: - Util
@@ -239,8 +233,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if self._pendingAsset != nil {
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
}
if !soft {
avPlayer.replaceCurrentItem(with: nil)
@@ -273,7 +269,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
self._state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .buffering
self._state = .loading
case .playing:
self._state = .playing
@unknown default:
@@ -283,14 +279,17 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
else if let initialTime = _initialTime {
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
self.play()
}
break
case .failed:
@@ -49,8 +49,8 @@ protocol AVPlayerWrapperProtocol: class {
func seek(to seconds: TimeInterval)
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
}
@@ -14,14 +14,11 @@ import Foundation
*/
public enum AVPlayerWrapperState: String {
/// An asset is being loaded for playback.
case loading
/// The current item is loaded, and the player is ready to start playing.
/// The current item is set, and the player is ready to start loading (buffering).
case ready
/// The current item is playing, but are currently buffering.
case buffering
/// The current item is loading, getting ready to play.
case loading
/// The player is paused.
case paused
+29 -60
View File
@@ -14,21 +14,21 @@ public enum SourceType {
}
public protocol AudioItem {
func getSourceUrl() -> String
func getArtist() -> String?
func getTitle() -> String?
func getAlbumTitle() -> String?
func getSourceType() -> SourceType
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
func getVolume() -> Float
}
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
public protocol TimePitching {
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
}
/// Make your `AudioItem`-subclass conform to this protocol to control enable the ability to start an item at a specific time of playback.
@@ -36,52 +36,45 @@ public protocol InitialTiming {
func getInitialTime() -> TimeInterval
}
/// Make your `AudioItem`-subclass conform to this protocol to set initialization options for the asset. Available keys available at [Apple Developer Documentation](https://developer.apple.com/documentation/avfoundation/avurlasset/initialization_options).
public protocol AssetOptionsProviding {
func getAssetOptions() -> [String: Any]
}
public class DefaultAudioItem: AudioItem {
public var audioVolume: Float
public var audioUrl: String
public var artist: String?
public var title: String?
public var albumTitle: String?
public var sourceType: SourceType
public var artwork: UIImage?
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil, audioVolume: Float = 1.0) {
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
self.audioUrl = audioUrl
self.artist = artist
self.title = title
self.albumTitle = albumTitle
self.sourceType = sourceType
self.artwork = artwork
self.audioVolume = audioVolume
}
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
}
@@ -89,27 +82,24 @@ public class DefaultAudioItem: AudioItem {
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
public func getVolume() -> Float {
return audioVolume
}
}
/// An AudioItem that also conforms to the `TimePitching`-protocol
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
}
@@ -117,42 +107,21 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
/// An AudioItem that also conforms to the `InitialTiming`-protocol
public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.initialTime = 0.0
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) {
self.initialTime = initialTime
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getInitialTime() -> TimeInterval {
return initialTime
}
}
/// An AudioItem that also conforms to the `AssetOptionsProviding`-protocol
public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptionsProviding {
public var options: [String: Any]
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
self.options = [:]
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, options: [String: Any]) {
self.options = options
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getAssetOptions() -> [String: Any] {
return options
}
}
+67 -86
View File
@@ -11,84 +11,84 @@ import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public class AudioPlayer: AVPlayerWrapperDelegate {
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
return _currentItem
}
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
The elapsed playback time of the current item.
*/
public var currentTime: Double {
return wrapper.currentTime
}
/**
The duration of the current AudioItem.
*/
public var duration: Double {
return wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
return wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
public var playerState: AudioPlayerState {
return wrapper.state
}
// MARK: - Setters for AVPlayerWrapper
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { _wrapper.bufferDuration = newValue }
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
@@ -96,7 +96,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.timeEventFrequency }
set { _wrapper.timeEventFrequency = newValue }
}
/**
Indicates whether the player should automatically delay playback in order to minimize stalling
*/
@@ -104,12 +104,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { _wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { _wrapper.isMuted = newValue }
@@ -119,25 +119,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.rate }
set { _wrapper.rate = newValue }
}
public var volumeMultiplier: Float = 1 {
didSet {
guard let currentVolume = currentItem?.getVolume() else { return }
self.volume = currentVolume * volumeMultiplier
}
}
/**
Set wether the player should loop when a song is finished.
Default is `false`.
*/
public var loop: Bool = false
// MARK: - Init
/**
Create a new AudioPlayer.
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
@@ -145,16 +132,16 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self._wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self._wrapper.delegate = self
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`.
*/
@@ -171,43 +158,40 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
case .file:
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions())
initialTime: (item as? InitialTiming)?.getInitialTime())
self._currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
self.volume = item.getVolume() * volumeMultiplier
}
/**
Toggle playback status.
*/
public func togglePlaying() {
self.wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
self.wrapper.play()
}
/**
Pause playback
*/
public func pause() {
self.wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
@@ -216,7 +200,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
}
/**
Seek to a specific time in the item.
*/
@@ -226,13 +210,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
self.wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
self.enableRemoteCommands(item.getCommands())
@@ -241,12 +225,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.enableRemoteCommands(remoteCommands)
}
}
// MARK: - NowPlayingInfo
/**
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
@@ -255,19 +239,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
loadArtwork(forItem: item)
}
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
@@ -278,19 +262,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
private func updateNowPlayingDuration(_ duration: Double) {
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
}
private func updateNowPlayingRate(_ rate: Float) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
}
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
}
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
@@ -301,13 +285,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
}
}
// MARK: - Private
func reset() {
self._currentItem = nil
}
private func setTimePitchingAlgorithmForCurrentItem() {
if let item = currentItem as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
@@ -316,55 +300,52 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
}
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .ready, .loading:
case .ready:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
setTimePitchingAlgorithmForCurrentItem()
case .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
default: break
}
self.event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
self.event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.event.fail.emit(data: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
}
func AVWrapperItemDidPlayToEndTime() {
if loop {
seek(to: .zero)
play()
}
self.event.playbackEnd.emit(data: .playedUntilEnd)
}
func AVWrapperDidRecreateAVPlayer() {
self.event.didRecreateAVPlayer.emit(data: ())
}
}
+31 -2
View File
@@ -6,6 +6,7 @@
//
import Foundation
import Combine
extension AudioPlayer {
@@ -69,7 +70,7 @@ extension AudioPlayer {
class Invoker<EventData> {
// Signals false if the listener object is nil
/// Signals false if the listener object is nil. If `false` is signaled, the invoker should not be retained.
let invoke: (EventData) -> Bool
weak var listener: AnyObject?
@@ -114,7 +115,7 @@ extension AudioPlayer {
self.invokersSemaphore.signal()
}
}
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
@@ -125,6 +126,34 @@ extension AudioPlayer {
}
}
/**
The publisher for this event. Use this for subscription through the Combine framework.
*/
@available(iOS 13.0, *)
public lazy var publisher: EventPublisher<EventData> = EventPublisher(event: self)
}
}
@available(iOS 13.0, *)
extension AudioPlayer {
public class EventPublisher<EventData>: Publisher {
public typealias Output = EventData
public typealias Failure = APError.EventError
private weak var event: Event<EventData>?
init(event: Event<EventData>) {
self.event = event
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
event?.addListener(self) { (data) in
let _ = subscriber.receive(data)
}
}
}
}
@@ -22,15 +22,9 @@ class AVPlayerItemNotificationObserver {
private let notificationCenter: NotificationCenter = NotificationCenter.default
private(set) weak var observingItem: AVPlayerItem?
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemNotificationObserverDelegate?
private(set) var isObserving: Bool = false
deinit {
stopObservingCurrentItem()
}
/**
Will start observing notifications from an item.
@@ -40,7 +34,6 @@ class AVPlayerItemNotificationObserver {
func startObserving(item: AVPlayerItem) {
stopObservingCurrentItem()
observingItem = item
isObserving = true
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
}
@@ -48,12 +41,10 @@ class AVPlayerItemNotificationObserver {
Stop receiving notifications for the current item.
*/
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
if let observingItem = observingItem {
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
}
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
self.observingItem = nil
self.isObserving = false
observingItem = nil
}
@objc private func itemDidPlayToEndTime() {
@@ -30,34 +30,37 @@ class AVPlayerItemObserver: NSObject {
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
}
private(set) var isObserving: Bool = false
var isObserving: Bool = false
private(set) weak var observingItem: AVPlayerItem?
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
deinit {
stopObservingCurrentItem()
if self.isObserving {
stopObservingCurrentItem()
}
}
/**
Start observing an item. Will remove self as observer from old item, if any.
Start observing an item. Will remove self as observer from old item.
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
self.stopObservingCurrentItem()
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
main.async {
if self.isObserving {
self.stopObservingCurrentItem()
}
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
}
}
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
}
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
@@ -36,9 +36,10 @@ class AVPlayerObserver: NSObject {
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
private(set) var isObserving: Bool = false
var isObserving: Bool = false
weak var delegate: AVPlayerObserverDelegate?
weak var player: AVPlayer? {
@@ -65,12 +66,13 @@ class AVPlayerObserver: NSObject {
}
func stopObserving() {
guard let player = player, isObserving else {
guard let player = player, self.isObserving else {
return
}
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
self.isObserving = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@@ -105,6 +107,7 @@ class AVPlayerObserver: NSObject {
}
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.TimeControlStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.TimeControlStatus(rawValue: statusNumber.intValue)!
@@ -10,8 +10,10 @@ import Foundation
import AVFoundation
protocol AVPlayerTimeObserverDelegate: class {
func audioDidStart()
func timeEvent(time: CMTime)
}
/**
@@ -48,11 +50,6 @@ class AVPlayerTimeObserver {
self.periodicObserverTimeInterval = periodicObserverTimeInterval
}
deinit {
unregisterForPeriodicEvents()
unregisterForBoundaryTimeEvents()
}
/**
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
*/
+37 -37
View File
@@ -9,32 +9,32 @@ import Foundation
class QueueManager<T> {
private var _items: [T] = []
/**
All items held by the queue.
*/
public var items: [T] {
return _items
}
public var nextItems: [T] {
guard _currentIndex + 1 < _items.count else {
return []
}
return Array(_items[_currentIndex + 1..<_items.count])
}
public var previousItems: [T] {
if (_currentIndex == 0) {
return []
}
return Array(_items[0..<_currentIndex])
}
private var _currentIndex: Int = 0
/**
The index of the current item.
Will be populated event though there is no current item (When the queue is empty).
@@ -42,7 +42,7 @@ class QueueManager<T> {
public var currentIndex: Int {
return _currentIndex
}
/**
The current item for the queue.
*/
@@ -52,28 +52,28 @@ class QueueManager<T> {
}
return nil
}
/**
Add a single item to the queue.
- parameter item: The `AudioItem` to be added.
*/
public func addItem(_ item: T) {
_items.append(item)
}
/**
Add an array of items to the queue.
- parameter items: The `AudioItem`s to be added.
*/
public func addItems(_ items: [T]) {
_items.append(contentsOf: items)
}
/**
Add an array of items to the queue at a given index.
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
@@ -81,15 +81,15 @@ class QueueManager<T> {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
}
/**
Get the next item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The next item.
*/
@@ -102,7 +102,7 @@ class QueueManager<T> {
_currentIndex = nextIndex
return _items[nextIndex]
}
/**
Get the previous item in the queue, if there are any.
Will update the current item.
@@ -119,21 +119,21 @@ class QueueManager<T> {
_currentIndex = previousIndex
return _items[previousIndex]
}
/**
Jump to a position in the queue.
Will update the current item.
- parameter index: The index to jump to.
- throws: `APError.QueueError`
- returns: The item at the index.
*/
@discardableResult
func jump(to index: Int, allowSameIndex: Bool = false) throws -> T {
guard index != currentIndex || allowSameIndex else {
func jump(to index: Int) throws -> T {
guard index != currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(_items.count))")
}
@@ -141,35 +141,35 @@ class QueueManager<T> {
_currentIndex = index
return _items[index]
}
/**
Move an item in the queue.
- parameter fromIndex: The index of the item to be moved.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
guard fromIndex != _currentIndex else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
}
guard fromIndex >= 0 && fromIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
guard toIndex >= 0 && toIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
let item = try removeItem(at: fromIndex)
try addItems([item], at: toIndex)
}
/**
Remove an item.
- parameter index: The index of the item to remove.
- throws: APError.QueueError
- returns: The removed item.
@@ -179,31 +179,31 @@ class QueueManager<T> {
guard index != _currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be positive and smaller than the count of current items (\(_items.count)).")
}
if index < _currentIndex {
_currentIndex = _currentIndex - 1
}
return _items.remove(at: index)
}
/**
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: T) {
if current == nil {
self.addItem(item)
}
self._items[_currentIndex] = item
}
/**
Remove all previous items in the queue.
If no previous items exist, no action will be taken.
@@ -223,7 +223,7 @@ class QueueManager<T> {
guard nextIndex < _items.count else { return }
_items.removeSubrange(nextIndex..<_items.count)
}
/**
Removes all items for queue
*/
+37 -53
View File
@@ -12,80 +12,72 @@ import MediaPlayer
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer {
let queueManager: QueueManager = QueueManager<AudioItem>()
/**
Set wether the player should automatically play the next song when a song is finished.
Default is `true`.
*/
public var automaticallyPlayNextSong: Bool = true
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
}
/**
Stops the player and clears the queue.
*/
public override func stop() {
super.stop()
}
override func reset() {
queueManager.clearQueue()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
return queueManager.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem] {
return queueManager.nextItems
}
public override var volumeMultiplier: Float {
didSet {
guard let currentVolume = queueManager.current?.getVolume() else { return }
self.volume = currentVolume * volumeMultiplier
}
}
/**
Will replace the current item with a new one and load it into the player.
- parameter item: The AudioItem to replace the current item.
- throws: APError.LoadError
*/
public override func load(item: AudioItem, playWhenReady: Bool) throws {
try super.load(item: item, playWhenReady: playWhenReady)
queueManager.replaceCurrentItem(with: item)
}
/**
Add a single item to the queue.
- parameter item: The item to add.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
@@ -99,10 +91,10 @@ public class QueuedAudioPlayer: AudioPlayer {
queueManager.addItem(item)
}
}
/**
Add items to the queue.
- parameter items: The items to add to the queue.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the first item in the list. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
@@ -116,30 +108,22 @@ public class QueuedAudioPlayer: AudioPlayer {
queueManager.addItems(items)
}
}
public func add(items: [AudioItem], at index: Int) throws {
try queueManager.addItems(items, at: index)
}
/**
Step to the next item in the queue. If the queue has no next items, it starts again from the first item.
- parameter canRepeatSingleItem: if there is only one item in the queue, allow repeating it forever
Step to the next item in the queue.
- throws: `APError`
*/
public func next(canRepeatSingleItem: Bool = false) throws {
public func next() throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem: AudioItem?
do {
nextItem = try queueManager.next()
} catch {
nextItem = try queueManager.jump(to: 0, allowSameIndex: canRepeatSingleItem)
}
try self.load(item: nextItem!, playWhenReady: true)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
@@ -148,20 +132,20 @@ public class QueuedAudioPlayer: AudioPlayer {
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
}
/**
Remove an item from the queue.
- parameter index: The index of the item to remove.
- throws: `APError.QueueError`
*/
public func removeItem(at index: Int) throws {
try queueManager.removeItem(at: index)
}
/**
Jump to a certain item in the queue.
- parameter index: The index of the item to jump to.
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
- throws: `APError`
@@ -171,10 +155,10 @@ public class QueuedAudioPlayer: AudioPlayer {
let item = try queueManager.jump(to: index)
try self.load(item: item, playWhenReady: playWhenReady)
}
/**
Move an item in the queue from one position to another.
- parameter fromIndex: The index of the item to move.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
@@ -182,28 +166,28 @@ public class QueuedAudioPlayer: AudioPlayer {
func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
/**
Remove all upcoming items, those returned by `next()`
*/
public func removeUpcomingItems() {
queueManager.removeUpcomingItems()
}
/**
Remove all previous items, those returned by `previous()`
*/
public func removePreviousItems() {
queueManager.removePreviousItems()
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidPlayToEndTime() {
if automaticallyPlayNextSong {
try? self.next(canRepeatSingleItem: true)
}
super.AVWrapperItemDidPlayToEndTime()
if automaticallyPlayNextSong {
try? self.next()
}
}
}
@@ -79,30 +79,6 @@ public struct SkipIntervalCommand: RemoteCommandProtocol {
}
public struct FeedbackCommand: RemoteCommandProtocol {
public static let like = FeedbackCommand(id: "Like", commandKeyPath: \MPRemoteCommandCenter.likeCommand, handlerKeyPath: \RemoteCommandController.handleLikeCommand)
public static let dislike = FeedbackCommand(id: "Dislike", commandKeyPath: \MPRemoteCommandCenter.dislikeCommand, handlerKeyPath: \RemoteCommandController.handleDislikeCommand)
public static let bookmark = FeedbackCommand(id: "Bookmark", commandKeyPath: \MPRemoteCommandCenter.bookmarkCommand, handlerKeyPath: \RemoteCommandController.handleBookmarkCommand)
public typealias Command = MPFeedbackCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPFeedbackCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
func set(isActive: Bool, localizedTitle: String, localizedShortTitle: String) -> FeedbackCommand {
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].isActive = isActive
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedTitle = localizedTitle
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedShortTitle = localizedShortTitle
return self
}
}
public enum RemoteCommand {
case play
@@ -123,12 +99,6 @@ public enum RemoteCommand {
case skipBackward(preferredIntervals: [NSNumber])
case like(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
/**
All values in an array for convenience.
Don't use for associated values.
@@ -144,9 +114,6 @@ public enum RemoteCommand {
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
.like(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.dislike(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.bookmark(isActive: false, localizedTitle: "", localizedShortTitle: "")
]
}
@@ -64,12 +64,6 @@ public class RemoteCommandController {
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))
case .like(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.like.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .dislike(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.dislike.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .bookmark(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.bookmark.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
}
}
@@ -84,9 +78,6 @@ public class RemoteCommandController {
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
case .like(_, _, _): self.disableCommand(FeedbackCommand.like)
case .dislike(_, _, _): self.disableCommand(FeedbackCommand.dislike)
case .bookmark(_, _, _): self.disableCommand(FeedbackCommand.bookmark)
}
}
@@ -101,9 +92,6 @@ public class RemoteCommandController {
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
@@ -192,18 +180,6 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.LoadError {
switch error {