Compare commits

..

71 Commits

Author SHA1 Message Date
Milen Pivchev 0870ec394a Allow looping of queue player 2021-01-11 13:02:22 +01:00
Milen Pivchev d8658fdc6f Change default value 2020-12-15 16:24:10 +01:00
Milen Pivchev 4530f9856a Add volume multiplier to regular AudioPlayer 2020-12-15 16:18:50 +01:00
Milen Pivchev 6a26073cfb Remove comment 2020-12-15 14:24:08 +01:00
Milen Pivchev b4e95aef09 Add volume to queued audio player 2020-12-15 14:23:47 +01:00
Milen Pivchev a37188ead3 Move volume logic 2020-12-15 13:58:44 +01:00
Milen Pivchev 915e5e22cd Make volume not optional 2020-12-15 11:58:00 +01:00
Milen Pivchev e8a825e012 Add volume 2020-12-15 11:42:53 +01:00
Milen Pivchev 5ec22ea2d4 Add looping 2020-11-24 10:37:24 +01:00
Jørgen Henrichsen b042c3ee6c Bump version 0.11.2. 2019-11-28 18:49:18 +01:00
Jørgen Henrichsen ee5db0c0d5 Merge pull request #87 from biesbjerg/always-emit-ready-event
Fixes a problem where the `ready` event would not be fired when passing `playWhenReady: true`.
2019-11-28 18:47:11 +01:00
Kim Biesbjerg a9509d454f always emit ready event 2019-11-01 10:41:37 +01:00
Jørgen Henrichsen e888c7954a Update Readme. Update podspec.
Bump version to 0.11.1.
2019-08-22 22:55:32 +02:00
Jørgen Henrichsen eea9aee4ec Merge pull request #74 from cbess/patch-1
Seek to initial time
2019-08-22 22:52:36 +02:00
Jørgen Henrichsen f2c95fa48c Merge branch 'master' into patch-1 2019-08-22 22:39:46 +02:00
Jørgen Henrichsen 5c8ac4da6b Update README
Remove incorrect recommendation to enable Remote Notifications.
2019-08-22 21:27:36 +02:00
C. Bess 5369d4f4e8 Seek to initial time
Use initial time operation, even for play when ready
2019-08-12 23:13:36 -05:00
Jørgen Henrichsen 09a7548f3a Update README. Update podspec.
Bump verision to 0.11.0.
2019-07-12 18:13:46 +02:00
Jørgen Henrichsen 565b9a04d4 Merge pull request #71 from dcvz/feature/asset-options
Add ability to set asset init options
2019-07-12 18:12:23 +02:00
David Chavez c2ec751ec0 Address review feedback 2019-07-10 13:56:05 +02:00
David Chavez 2c36c6c239 Add ability to set asset init options 2019-07-06 12:04:49 +02:00
Jørgen Henrichsen 1c4cbf676d Merge pull request #70 from jorgenhenrichsen/more-detaile-player-states
More detailed player states
2019-07-03 18:29:43 +02:00
Jørgen Henrichsen ec9492da17 Add tests for the loading state. 2019-07-03 17:48:28 +02:00
Jørgen Henrichsen 5add7699ff Added an example of how to handle network error.
The example app will now handle a network error, and reload when play is called, after a item failed to load because of a network error.
2019-07-03 16:41:14 +02:00
Jørgen Henrichsen bc1d775875 Reset audio file to spotify preview in example app. 2019-07-03 15:06:50 +02:00
Jørgen Henrichsen 4c1a545e87 Update podspec. Remove swift version file.
Clean up the podspec file and set swift version to 5.0. Remove the .swift-version file as it's use is deprecated.
2019-07-03 14:53:24 +02:00
Jørgen Henrichsen f31b52f81b Make the ready state reachable for items with initial time.
Fixes a problem where the .ready state would not be reached for audio items that had an initial time.
2019-07-03 14:25:43 +02:00
Jørgen Henrichsen 355c729078 Update README. Update podspec.
Bump version to 0.10.0.
2019-07-03 13:22:27 +02:00
Jørgen Henrichsen 719d3c852b Add an activityindicator to the example.
Shows how an activityindicator can be used to indicate loading, when player is loading or buffering.
2019-07-03 13:21:41 +02:00
Jørgen Henrichsen 4f33d7e688 Add a new state buffering.
Introduces an additional state to better describe the current state of the player.
Previously the player would be `idle` until the  item was loaded, then changing to `ready`. When the player started to play it would first enter `loading`, then subsequently `playing`.
Now it will instead immediately enter `loading`, then when the item is loaded it will enter `ready`. When the item then is played it will enter `buffering` and then `playing`.
This should make it easier to update UI accordingly to the players current state.
2019-07-03 11:23:10 +02:00
Jørgen Henrichsen 56d0633df0 Update README. Update podspec.
Bump to version 0.9.3.
2019-07-01 16:23:01 +02:00
Jørgen Henrichsen 825e508ecb Merge pull request #67 from jorgenhenrichsen/async-fixes
Async fixes
2019-07-01 16:14:20 +02:00
Jørgen Henrichsen 76d9dc17af Merge branch 'master' into async-fixes 2019-07-01 15:38:45 +02:00
Jørgen Henrichsen 8ceb0216a0 Merge pull request #65 from dcvz/feature/feedback-commands
Add commands for like, dislike and bookmark
2019-07-01 15:38:33 +02:00
David Chavez db41634631 Fix some missing sets 2019-06-25 10:49:10 +02:00
David Chavez 15843b5fe8 Add commands for like, dislike and bookmark 2019-06-24 23:57:17 +02:00
Jørgen Henrichsen 4241b484b2 Removed commented prints. 2019-06-23 18:19:08 +02:00
Jørgen Henrichsen 92bf0b96b6 Remove cancelloading in the load method.
This was already done in the reset(soft:) method.
Removed the nil check before canceling the pendingAsset load, since the coalescing `?` operator was already used anyway.
2019-06-23 18:13:48 +02:00
Jørgen Henrichsen 457dff7c01 Use weak capturing of the AudioPlayer. Remove initial time test.
Weak capturing of the AudioPlayer is done in the listener callback functions in order to not receive INVOPs in the event the audioPlayer is deallocated.
The initial time test is removed because of the unreliable results it produces.
2019-06-23 18:06:43 +02:00
Jørgen Henrichsen c90e9c4976 Unregister observers in their deinit method. 2019-06-23 16:41:47 +02:00
Jørgen Henrichsen 649bb01e89 Weak self and cancel previous load in load(from:playWhenReady).
Use weak self in the loadValuesAsync callback in the event where this is called after the player has be deintialized.
Cancel loading the pendingAsset before loading a new one.
2019-06-23 16:35:50 +02:00
Jørgen Henrichsen 965f7dbbe1 Update README and podspec.
Bump version to 0.9.2.
2019-06-19 12:42:31 +02:00
Jørgen Henrichsen 536414bd44 Merge pull request #51 from minhtc/async-load-asset
load asset async to prevent freeze UI when streaming audio from internet
2019-06-19 12:39:00 +02:00
HackerMeo b7d5db0a55 Merge pull request #1 from jorgenhenrichsen/minhtc-async-load-asset
Fix test problems and a few other things
2019-06-19 16:07:40 +07:00
Jørgen Henrichsen d0907b16c8 Merge branch 'async-load-asset' into minhtc-async-load-asset 2019-06-18 21:34:24 +02:00
Jørgen Henrichsen 60eb4b4676 Removed the currentTime test.
Removes the test that always fails in the CI env at bitrise.
2019-06-18 11:30:47 +02:00
Jørgen Henrichsen 4ec9e9c2a2 Update currentTime tests.
Use a shorter `timeEventFrequenct` in order to recieve the callback quicker, so the test can pass quicker.
2019-06-18 11:16:04 +02:00
Jørgen Henrichsen b2efdf4d65 Rewrote the current time test.
Checks each time a second elapses, instead of just at state == `playing`.
2019-06-13 22:33:41 +02:00
Jørgen Henrichsen d9badfec9b Do not set automaticallyWaitsToMinimizeStalling to false when an item is loaded. 2019-06-13 21:42:47 +02:00
Jørgen Henrichsen 4d0f29adf6 Increased timeout for expectations. 2019-06-13 21:42:47 +02:00
Jørgen Henrichsen f817bc5840 Wrote AudioPlayerTests using the XCTest framework. 2019-06-13 21:42:47 +02:00
Jørgen Henrichsen 5755b70e1b Rewrote AVPlayerWrapperTests using the XCTest framework. 2019-06-13 21:42:47 +02:00
Jørgen Henrichsen 83ac2af5d6 Set timepitchingalgo on audio ready. Update tests to support async loading. 2019-06-13 21:10:57 +02:00
minhtcx a2f001a968 load asset async to prevent freeze UI when streaming audio from internet 2019-06-13 21:10:56 +02:00
Jørgen Henrichsen 9390064581 Merge pull request #62 from anharismail/master
Logo SwiftAudio
2019-06-13 12:15:45 +02:00
Anhar Ismail 7a45ee9e47 Update README.md 2019-06-13 16:57:58 +07:00
Anhar Ismail 17a4b18333 Add files via upload 2019-06-13 16:56:34 +07:00
Anhar Ismail ad01101d3c Create logomark.png 2019-06-13 16:55:40 +07:00
Jørgen Henrichsen d1bbc94bdd Update Readme. Update podspec.
Bump version to 0.9.1.
2019-06-11 13:48:16 +02:00
Jørgen Henrichsen fb5a8dde6c Merge pull request #61 from minhtc/fix-exclusive-access
Fix crash on modification requires exclusive access
2019-06-11 13:47:03 +02:00
minhtcx 9b71040f43 fix crash on modification requires exclusive access 2019-06-10 12:02:46 +07:00
Jørgen Henrichsen 5e50ea48d6 Merge pull request #60 from jorgenhenrichsen/recover-from-AVPlayer-failed
Recover from AVPlayer failed
2019-05-11 19:41:12 +02:00
Jørgen Henrichsen 92a804e9e4 Update Readme. Update podspec.
Bump version to v0.9.0.
2019-05-11 19:30:59 +02:00
Jørgen Henrichsen af4635d047 Remove AVPlayer parameter from init in both AudioPlayer and AVPlayerWrapper. 2019-05-11 19:29:36 +02:00
Jørgen Henrichsen addebef7f9 Add more detailed description on the fail event. 2019-05-11 18:14:54 +02:00
Jørgen Henrichsen 46022ef0d6 Add event when AVPlayer is recreated.
The AudioSession category will need to be set again when this event is emitted.
2019-05-11 17:56:43 +02:00
Jørgen Henrichsen 386dc5202c Recreate the AVPlayer when loading an item, if the previous item failed. 2019-05-11 17:46:55 +02:00
Jørgen Henrichsen 17166093c2 Correctly set isObserving when stopping the observation. 2019-05-11 17:46:27 +02:00
Jørgen Henrichsen c06b8ce64c Made the player observer not retain the AVPlayer.
They will also stop observing the current AVPlayer if a new one is going to be set.
2019-05-11 17:34:24 +02:00
Minh Tran aab8a2302b Merge branch 'master' into async-load-asset 2019-04-19 12:55:36 +07:00
minhtcx 4a512c6aa0 load asset async to prevent freeze UI when streaming audio from internet 2019-04-17 17:01:55 +07:00
33 changed files with 933 additions and 715 deletions
-1
View File
@@ -1 +0,0 @@
4.2
+18 -4
View File
@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<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">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -63,7 +62,7 @@
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
<rect key="frame" x="67" y="84" width="240" height="240"/>
<rect key="frame" x="67.5" 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"/>
@@ -105,24 +104,37 @@
<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"/>
@@ -135,7 +147,9 @@
<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"/>
+50 -6
View File
@@ -21,9 +21,12 @@ 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!
var isScrubbing: Bool = false
let controller = AudioController.shared
private var isScrubbing: Bool = false
private let controller = AudioController.shared
private var lastLoadFailed: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
@@ -31,13 +34,24 @@ class ViewController: UIViewController {
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
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()
}
controller.player.togglePlaying()
if lastLoadFailed, let item = controller.player.currentItem {
lastLoadFailed = false
errorLabel.isHidden = true
try? controller.player.load(item: item, playWhenReady: true)
}
else {
controller.player.togglePlaying()
}
}
@IBAction func previous(_ sender: Any) {
@@ -83,16 +97,31 @@ 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 .ready:
case .loading:
self.loadIndicator.startAnimating()
self.updateMetaData()
self.updateTimeValues()
case .loading, .playing, .paused, .idle:
case .buffering:
self.loadIndicator.startAnimating()
case .ready:
self.loadIndicator.stopAnimating()
self.updateMetaData()
self.updateTimeValues()
case .playing, .paused, .idle:
self.loadIndicator.stopAnimating()
self.updateTimeValues()
}
}
@@ -116,4 +145,19 @@ class ViewController: UIViewController {
}
}
func handleAVPlayerRecreated() {
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...")
}
}
}
}
}
+17 -1
View File
@@ -20,10 +20,15 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
beforeEach {
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver(player: player)
observer = AVPlayerObserver()
observer.player = player
observer.delegate = self
}
it("should not be observing", closure: {
expect(observer.isObserving).to(beFalse())
})
context("when observing has started", {
beforeEach {
observer.startObserving()
@@ -54,6 +59,17 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
expect(observer.isObserving).toEventually(beTrue())
})
})
context("when stopping observing", closure: {
beforeEach {
observer.stopObserving()
}
it("should not be observing", closure: {
expect(observer.isObserving).to(beFalse())
})
})
})
}
@@ -17,7 +17,8 @@ class AVPlayerTimeObserverTests: QuickSpec {
player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0
observer = AVPlayerTimeObserver(player: player, periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
observer.player = player
}
context("has started boundary time observing", {
+180 -228
View File
@@ -1,240 +1,191 @@
import Quick
import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
class AVPlayerWrapperTests: QuickSpec {
override func spec() {
describe("An AVPlayerWrapper") {
var wrapper: AVPlayerWrapper!
beforeEach {
let player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0.0
wrapper = AVPlayerWrapper(avPlayer: player)
wrapper.bufferDuration = 0.0001
class AVPlayerWrapperTests: XCTestCase {
var wrapper: AVPlayerWrapper!
var holder: AVPlayerWrapperDelegateHolder!
override func setUp() {
super.setUp()
wrapper = AVPlayerWrapper()
wrapper.volume = 0.0
wrapper.automaticallyWaitsToMinimizeStalling = false
holder = AVPlayerWrapperDelegateHolder()
wrapper.delegate = holder
}
override func tearDown() {
wrapper = nil
holder = nil
super.tearDown()
}
// MARK: - State tests
func test_AVPlayerWrapper__state__should_be_idle() {
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() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .ready {
expectation.fulfill()
}
describe("its state", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
context("when loading a source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
}
it("should eventually be ready", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
})
})
context("when playing with no source", {
beforeEach {
wrapper.play()
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
context("when playing a source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
})
})
context("when pausing the source", {
let holder = AVPlayerWrapperDelegateHolder()
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
wrapper.pause()
}
}
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when toggling the source from play", {
let holder = AVPlayerWrapperDelegateHolder()
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
wrapper.togglePlaying()
}
}
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when stopping the source", {
var holder: AVPlayerWrapperDelegateHolder!
var receivedIdleUpdate: Bool = false
beforeEach {
holder = AVPlayerWrapperDelegateHolder()
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
wrapper.stop()
}
if state == .idle {
receivedIdleUpdate = true
}
}
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be 'idle'", closure: {
expect(receivedIdleUpdate).toEventually(beTrue())
})
})
context("when seeking before loading", {
beforeEach {
wrapper.seek(to: 10)
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
context("when loading source with initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: initialTime)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
})
})
})
describe("its duration", {
it("should be 0", closure: {
expect(wrapper.duration).to(equal(0))
})
context("when loading source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: LongSource.path), playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
})
})
})
describe("its current time", {
it("should be 0", closure: {
expect(wrapper.currentTime).to(equal(0))
})
context("when seeking to a time", {
let holder = AVPlayerWrapperDelegateHolder()
let seekTime: TimeInterval = 0.5
beforeEach {
wrapper.delegate = holder
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(wrapper.currentTime).toEventually(equal(seekTime))
})
})
context("when playing from initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: initialTime)
}
it("should eventuallt be equal to the initial time", closure: {
expect(wrapper.currentTime).toEventually(equal(initialTime))
})
})
})
describe("its rate", {
it("should be 0", closure: {
expect(wrapper.rate).to(equal(0.0))
})
context("when playing a source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be 1.0", closure: {
expect(wrapper.rate).toEventually(equal(1.0))
})
})
})
describe("its automaticallyWaitsToMinimizeStalling option", {
it("should be false", closure: {
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beFalse())
})
context("when setting it to true", {
beforeEach {
wrapper.automaticallyWaitsToMinimizeStalling = true
}
it("should be true", closure: {
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beTrue())
})
})
})
describe("its timeEventFrequency", {
context("when updated", {
beforeEach {
wrapper.timeEventFrequency = .everyHalfSecond
}
it("should update the playerTimeObservers periodicObserverTimeInterval", closure: {
expect(wrapper.playerTimeObserver.periodicObserverTimeInterval).to(equal(TimeEventFrequency.everyHalfSecond.getTime()))
})
})
})
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_playing_a_source__should_be_playing() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .playing {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_pausing_a_source__should_be_paused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.pause()
case .paused: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_toggling_from_play__should_be_paused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.togglePlaying()
case .paused: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_stopping__should_be_stopped() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.stop()
case .idle: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__loading_with_intial_time__should_be_playing() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: expectation.fulfill()
default: break
}
}
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Duration tests
func test_AVPlayerWrapper__duration__should_be_0() {
XCTAssert(wrapper.duration == 0.0)
}
func test_AVPlayerWrapper__duration__loading_a_source__should_not_be_0() {
let expectation = XCTestExpectation()
holder.stateUpdate = { _ in
if self.wrapper.duration > 0 {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current time tests
func test_AVPlayerWrapper__currentTime__should_be_0() {
XCTAssert(wrapper.currentTime == 0)
}
// MARK: - Seeking
func test_AVPlayerWrapper__seeking__should_seek() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
self.wrapper.seek(to: seekTime)
}
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
let expectation = XCTestExpectation()
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Rate tests
func test_AVPlayerWrapper__rate__should_be_0() {
XCTAssert(wrapper.rate == 0.0)
}
func test_AVPlayerWrapper__rate__playing_a_source__should_be_1() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if self.wrapper.rate == 1.0 {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__timeObserver__when_updated__should_update_the_observers_periodicObserverTimeInterval() {
wrapper.timeEventFrequency = .everySecond
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everySecond.getTime())
wrapper.timeEventFrequency = .everyHalfSecond
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everyHalfSecond.getTime())
}
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapperDidRecreateAVPlayer() {
}
func AVWrapperItemDidPlayToEndTime() {
}
@@ -248,6 +199,8 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
}
var stateUpdate: ((_ state: AVPlayerWrapperState) -> Void)?
var didUpdateDuration: ((_ duration: Double) -> Void)?
var didSeekTo: ((_ seconds: Int) -> Void)?
var itemDidComplete: (() -> Void)?
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
@@ -262,16 +215,15 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
}
var seekCompletion: (() -> Void)?
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
seekCompletion?()
didSeekTo?(seconds)
}
func AVWrapper(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
didUpdateDuration?(duration)
}
}
+171 -179
View File
@@ -1,191 +1,169 @@
import Quick
import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
class AudioPlayerTests: QuickSpec {
class AudioPlayerTests: XCTestCase {
override func spec() {
describe("An AudioPlayer") {
var audioPlayer: AudioPlayer!
beforeEach {
audioPlayer = AudioPlayer()
audioPlayer.bufferDuration = 0.0001
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.volume = 0.0
var audioPlayer: AudioPlayer!
var listener: AudioPlayerEventListener!
override func setUp() {
super.setUp()
audioPlayer = AudioPlayer()
audioPlayer.volume = 0.0
audioPlayer.bufferDuration = 0.001
audioPlayer.automaticallyWaitsToMinimizeStalling = false
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
}
override func tearDown() {
audioPlayer = nil
listener = nil
super.tearDown()
}
func test_AudioPlayer__state__should_be_idle() {
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
switch state {
case .ready: expectation.fulfill()
default: break
}
describe("its state", {
it("should be idle", closure: {
expect(audioPlayer.playerState).to(equal(AudioPlayerState.idle))
})
context("when audio item is loaded", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("it should eventually be ready", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
})
})
context("when an item is loaded (playWhenReady=true)", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("it should eventually be playing", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
})
})
context("when playing an item", {
var listener: AudioPlayerEventListener!
beforeEach {
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { state in
if state == .ready {
audioPlayer.play()
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be playing", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
})
})
context("when pausing an item", {
var listener: AudioPlayerEventListener!
beforeEach {
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
audioPlayer.pause()
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
})
})
context("when stopping an item", {
var listener: AudioPlayerEventListener!
beforeEach {
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
audioPlayer.stop()
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be idle", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.idle))
})
})
})
describe("its current time", {
it("should be 0", closure: {
expect(audioPlayer.currentTime).to(equal(0))
})
context("when seeking to a time", {
let seekTime: TimeInterval = 1.0
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
audioPlayer.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(seekTime))
})
})
context("when playing an item with an initial time", {
var item: DefaultAudioItemInitialTime!
beforeEach {
item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventaully be equal to the initial time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(item.getInitialTime()))
})
})
})
describe("its rate", {
it("should be 0", closure: {
expect(audioPlayer.rate).to(equal(0))
})
context("when playing an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be 1.0", closure: {
expect(audioPlayer.rate).toEventually(equal(1.0))
})
})
})
describe("its currentItem", {
it("should be nil", closure: {
expect(audioPlayer.currentItem).to(beNil())
})
context("when loading an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should not be nil", closure: {
expect(audioPlayer.currentItem).toNot(beNil())
})
})
context("when setting the timePitchAlgorithm", {
beforeEach {
audioPlayer.audioTimePitchAlgorithm = .timeDomain
}
context("then loading an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
})
})
context("then loading a timepitching item", {
beforeEach {
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
})
})
})
})
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
switch state {
case .playing: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__play_source__should_be_playing() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
switch state {
case .ready: self.audioPlayer.play()
case .playing: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__pausing_source__should_be_paused() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing: audioPlayer?.pause()
case .paused: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__stopping_source__should_be_idle() {
let expectation = XCTestExpectation()
var hasBeenPlaying: Bool = false
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing:
hasBeenPlaying = true
audioPlayer?.stop()
case .idle:
if hasBeenPlaying {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current time
func test_AudioPlayer__currentTime__should_be_0() {
XCTAssert(audioPlayer.currentTime == 0.0)
}
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
// let expectation = XCTestExpectation()
// audioPlayer.timeEventFrequency = .everyQuarterSecond
// listener.secondsElapse = { _ in
// if self.audioPlayer.currentTime > 0.0 {
// expectation.fulfill()
// }
// }
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
// wait(for: [expectation], timeout: 20.0)
// }
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
XCTAssert(audioPlayer.rate == 0.0)
}
func test_AudioPlayer__rate__playing_source__should_be_1() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
switch state {
case .playing:
if audioPlayer.rate == 1.0 {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current item
func test_AudioPlayer__currentItem__should_be_nil() {
XCTAssertNil(audioPlayer.currentItem)
}
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 }
switch state {
case .ready:
if audioPlayer.currentItem != nil {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
}
@@ -201,11 +179,21 @@ class AudioPlayerEventListener {
}
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
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) {
@@ -216,4 +204,8 @@ class AudioPlayerEventListener {
seekCompletion?()
}
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
self.secondsElapse?(data)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+4 -4
View File
@@ -1,3 +1,5 @@
![logo](Images/original-horizontal.png)
# SwiftAudio
[![Build Status](https://app.bitrise.io/app/3d3ac2ba8d817235/status.svg?token=PHIPu3oMde5GdQEOZ1Ilww&branch=master)](https://app.bitrise.io/app/3d3ac2ba8d817235)
@@ -23,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.8.0'
pod 'SwiftAudio', '~> 0.11.2'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.8.0
github "jorgenhenrichsen/SwiftAudio" ~> 0.11.2
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
@@ -136,8 +138,6 @@ 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 = [
+2 -19
View File
@@ -8,35 +8,18 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.8.0'
s.version = '0.11.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
@@ -26,12 +26,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
// MARK: - Properties
let avPlayer: AVPlayer
var avPlayer: AVPlayer
let playerObserver: AVPlayerObserver
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
@@ -46,10 +46,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
public init(avPlayer: AVPlayer = AVPlayer()) {
self.avPlayer = avPlayer
self.playerObserver = AVPlayerObserver(player: avPlayer)
self.playerTimeObserver = AVPlayerTimeObserver(player: avPlayer, periodicObserverTimeInterval: timeEventFrequency.getTime())
public init() {
self.avPlayer = AVPlayer()
self.playerObserver = AVPlayerObserver()
self.playerObserver.player = avPlayer
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
self.playerTimeObserver.player = avPlayer
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
self.playerItemObserver = AVPlayerItemObserver()
@@ -75,6 +77,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
return avPlayer.currentItem
}
var _pendingAsset: AVAsset? = nil
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
@@ -84,7 +88,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
}
var duration: TimeInterval {
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
return seconds
@@ -102,7 +106,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
var bufferedPosition: TimeInterval {
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
@@ -112,7 +116,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var rate: Float {
get { return avPlayer.rate }
set { avPlayer.rate = newValue }
@@ -163,28 +167,69 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
func load(from url: URL, playWhenReady: Bool) {
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
reset(soft: true)
_playWhenReady = playWhenReady
// Set item
let currentAsset = AVURLAsset(url: url)
let currentItem = AVPlayerItem(asset: currentAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
currentItem.preferredForwardBufferDuration = bufferDuration
avPlayer.replaceCurrentItem(with: currentItem)
// Register for events
playerTimeObserver.registerForBoundaryTimeEvents()
playerObserver.startObserving()
playerItemNotificationObserver.startObserving(item: currentItem)
playerItemObserver.startObserving(item: currentItem)
if currentItem?.status == .failed {
recreateAVPlayer()
}
self._pendingAsset = AVURLAsset(url: url, options: options)
if let pendingAsset = _pendingAsset {
self._state = .loading
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
guard let self = self else {
return
}
var error: NSError? = nil
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
DispatchQueue.main.async {
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
switch status {
case .loaded:
if isPendingAsset {
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
currentItem.preferredForwardBufferDuration = self.bufferDuration
self.avPlayer.replaceCurrentItem(with: currentItem)
// Register for events
self.playerTimeObserver.registerForBoundaryTimeEvents()
self.playerObserver.startObserving()
self.playerItemNotificationObserver.startObserving(item: currentItem)
self.playerItemObserver.startObserving(item: currentItem)
}
break
case .failed:
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
}
break
case .cancelled:
break
default:
break
}
}
})
}
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady)
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
// MARK: - Util
@@ -194,11 +239,24 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
}
/// Will recreate the AVPlayer instance. Used when the current one fails.
private func recreateAVPlayer() {
let player = AVPlayer()
playerObserver.player = player
playerTimeObserver.player = player
playerTimeObserver.registerForPeriodicTimeEvents()
avPlayer = player
delegate?.AVWrapperDidRecreateAVPlayer()
}
}
extension AVPlayerWrapper: AVPlayerObserverDelegate {
@@ -215,7 +273,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
self._state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .loading
self._state = .buffering
case .playing:
self._state = .playing
@unknown default:
@@ -225,19 +283,16 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
else if let initialTime = _initialTime {
self.seek(to: initialTime)
}
break
case .failed:
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
break
@@ -16,5 +16,6 @@ protocol AVPlayerWrapperDelegate: class {
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()
func AVWrapperDidRecreateAVPlayer()
}
@@ -9,7 +9,7 @@ import Foundation
import AVFoundation
protocol AVPlayerWrapperProtocol {
protocol AVPlayerWrapperProtocol: class {
var state: AVPlayerWrapperState { get }
@@ -49,8 +49,8 @@ protocol AVPlayerWrapperProtocol {
func seek(to seconds: TimeInterval)
func load(from url: URL, playWhenReady: Bool)
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
}
@@ -14,11 +14,14 @@ import Foundation
*/
public enum AVPlayerWrapperState: String {
/// The current item is set, and the player is ready to start loading (buffering).
/// An asset is being loaded for playback.
case loading
/// The current item is loaded, and the player is ready to start playing.
case ready
/// The current item is loading, getting ready to play.
case loading
/// The current item is playing, but are currently buffering.
case buffering
/// The player is paused.
case paused
+60 -29
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,45 +36,52 @@ 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) {
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil, audioVolume: Float = 1.0) {
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
}
@@ -82,24 +89,27 @@ 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?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
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
}
@@ -107,21 +117,42 @@ 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?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
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
}
}
+100 -74
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,30 +119,42 @@ 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(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
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`.
*/
@@ -159,47 +171,43 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
case .file:
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime())
if let item = item as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions())
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.
*/
@@ -208,7 +216,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
}
/**
Seek to a specific time in the item.
*/
@@ -218,13 +226,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())
@@ -233,12 +241,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
@@ -247,19 +255,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
@@ -270,19 +278,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 {
@@ -293,52 +301,70 @@ 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()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
}
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .ready:
case .ready, .loading:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
setTimePitchingAlgorithmForCurrentItem()
case .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
updateNowPlayingPlaybackValues()
}
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: ())
}
}
+10 -1
View File
@@ -15,6 +15,7 @@ extension AudioPlayer {
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias DidRecreateAVPlayerEventData = ()
public struct EventHolder {
@@ -37,7 +38,8 @@ extension AudioPlayer {
public let secondElapse: AudioPlayer.Event<SecondElapseEventData> = AudioPlayer.Event()
/**
Emitted when the player encounters an error.
Emitted when the player encounters an error. This will ultimately result in the AVPlayer instance to be recreated.
If this event is emitted, it means you will need to load a new item in some way. Calling play() will not resume playback.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let fail: AudioPlayer.Event<FailEventData> = AudioPlayer.Event()
@@ -54,6 +56,13 @@ extension AudioPlayer {
*/
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
/**
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: It can be necessary to set the AVAudioSession's category again when this event is emitted.
*/
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
}
public typealias EventClosure<EventData> = (EventData) -> Void
@@ -22,9 +22,15 @@ class AVPlayerItemNotificationObserver {
private let notificationCenter: NotificationCenter = NotificationCenter.default
weak var observingItem: AVPlayerItem?
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemNotificationObserverDelegate?
private(set) var isObserving: Bool = false
deinit {
stopObservingCurrentItem()
}
/**
Will start observing notifications from an item.
@@ -34,6 +40,7 @@ class AVPlayerItemNotificationObserver {
func startObserving(item: AVPlayerItem) {
stopObservingCurrentItem()
observingItem = item
isObserving = true
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
}
@@ -41,10 +48,12 @@ class AVPlayerItemNotificationObserver {
Stop receiving notifications for the current item.
*/
func stopObservingCurrentItem() {
if let observingItem = observingItem {
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
guard let observingItem = observingItem, isObserving else {
return
}
observingItem = nil
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
self.observingItem = nil
self.isObserving = false
}
@objc private func itemDidPlayToEndTime() {
@@ -30,37 +30,34 @@ class AVPlayerItemObserver: NSObject {
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
}
var isObserving: Bool = false
private(set) var isObserving: Bool = false
weak var observingItem: AVPlayerItem?
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
deinit {
if self.isObserving {
stopObservingCurrentItem()
}
stopObservingCurrentItem()
}
/**
Start observing an item. Will remove self as observer from old item.
Start observing an item. Will remove self as observer from old item, if any.
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
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)
}
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() {
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
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)
self.isObserving = false
self.observingItem = nil
}
@@ -36,39 +36,41 @@ class AVPlayerObserver: NSObject {
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
let player: AVPlayer
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
var isObserving: Bool = false
private(set) var isObserving: Bool = false
weak var delegate: AVPlayerObserverDelegate?
init(player: AVPlayer) {
self.player = player
weak var player: AVPlayer? {
willSet {
self.stopObserving()
}
}
deinit {
if self.isObserving {
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
}
self.stopObserving()
}
/**
Start receiving events from this observer.
- Important: If this observer is already receiving events, it will first be removed. Never remove this observer manually.
*/
func startObserving() {
main.async {
if self.isObserving {
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
}
self.isObserving = true
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
guard let player = player else {
return
}
self.stopObserving()
self.isObserving = true
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
}
func stopObserving() {
guard let player = player, 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?) {
@@ -103,7 +105,6 @@ 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,10 +10,8 @@ import Foundation
import AVFoundation
protocol AVPlayerTimeObserverDelegate: class {
func audioDidStart()
func timeEvent(time: CMTime)
}
/**
@@ -27,7 +25,12 @@ class AVPlayerTimeObserver {
var boundaryTimeStartObserverToken: Any?
var periodicTimeObserverToken: Any?
private let player: AVPlayer
weak var player: AVPlayer? {
willSet {
unregisterForBoundaryTimeEvents()
unregisterForPeriodicEvents()
}
}
/// The frequence to receive periodic time events.
/// Setting this to a new value will trigger a re-registering to the periodic events of the player.
@@ -41,18 +44,24 @@ class AVPlayerTimeObserver {
weak var delegate: AVPlayerTimeObserverDelegate?
init(player: AVPlayer, periodicObserverTimeInterval: CMTime) {
self.player = player
init(periodicObserverTimeInterval: CMTime) {
self.periodicObserverTimeInterval = periodicObserverTimeInterval
}
deinit {
unregisterForPeriodicEvents()
unregisterForBoundaryTimeEvents()
}
/**
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
*/
func registerForBoundaryTimeEvents() {
guard let player = player else {
return
}
unregisterForBoundaryTimeEvents()
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
self?.delegate?.audioDidStart()
})
@@ -62,10 +71,11 @@ class AVPlayerTimeObserver {
Unregister from the boundary events of the player.
*/
func unregisterForBoundaryTimeEvents() {
if let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken {
player.removeTimeObserver(boundaryTimeStartObserverToken)
self.boundaryTimeStartObserverToken = nil
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
return
}
player.removeTimeObserver(boundaryTimeStartObserverToken)
self.boundaryTimeStartObserverToken = nil
}
/**
@@ -73,6 +83,9 @@ class AVPlayerTimeObserver {
Will trigger unregisterForPeriodicEvents() first to avoid multiple subscriptions.
*/
func registerForPeriodicTimeEvents() {
guard let player = player else {
return
}
unregisterForPeriodicEvents()
periodicTimeObserverToken = player.addPeriodicTimeObserver(forInterval: periodicObserverTimeInterval, queue: nil, using: { (time) in
self.delegate?.timeEvent(time: time)
@@ -83,12 +96,11 @@ class AVPlayerTimeObserver {
Unregister for periodic events.
*/
func unregisterForPeriodicEvents() {
if let periodicTimeObserverToken = periodicTimeObserverToken {
self.player.removeTimeObserver(periodicTimeObserverToken)
self.periodicTimeObserverToken = nil
guard let player = player, let periodicTimeObserverToken = periodicTimeObserverToken else {
return
}
player.removeTimeObserver(periodicTimeObserverToken)
self.periodicTimeObserverToken = nil
}
}
+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) throws -> T {
guard index != currentIndex else {
func jump(to index: Int, allowSameIndex: Bool = false) throws -> T {
guard index != currentIndex || allowSameIndex 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
*/
+52 -36
View File
@@ -12,72 +12,80 @@ 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`
@@ -91,10 +99,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`
@@ -108,22 +116,30 @@ 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.
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
- throws: `APError`
*/
public func next() throws {
public func next(canRepeatSingleItem: Bool = false) throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
let nextItem: AudioItem?
do {
nextItem = try queueManager.next()
} catch {
nextItem = try queueManager.jump(to: 0, allowSameIndex: canRepeatSingleItem)
}
try self.load(item: nextItem!, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
@@ -132,20 +148,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`
@@ -155,10 +171,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`
@@ -166,28 +182,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() {
super.AVWrapperItemDidPlayToEndTime()
if automaticallyPlayNextSong {
try? self.next()
try? self.next(canRepeatSingleItem: true)
}
super.AVWrapperItemDidPlayToEndTime()
}
}
@@ -79,6 +79,30 @@ 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
@@ -99,6 +123,12 @@ 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.
@@ -114,6 +144,9 @@ public enum RemoteCommand {
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
.like(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.dislike(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.bookmark(isActive: false, localizedTitle: "", localizedShortTitle: "")
]
}
@@ -64,6 +64,12 @@ 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))
}
}
@@ -78,6 +84,9 @@ 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)
}
}
@@ -92,6 +101,9 @@ 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 {
@@ -180,6 +192,18 @@ 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 {