Compare commits

...

42 Commits

Author SHA1 Message Date
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
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
26 changed files with 595 additions and 506 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"/>
+45 -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()
@@ -32,13 +35,23 @@ 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()
}
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) {
@@ -84,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()
}
}
@@ -121,4 +149,15 @@ 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...")
}
}
}
}
}
+176 -227
View File
@@ -1,234 +1,182 @@
import Quick
import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
class AVPlayerWrapperTests: QuickSpec {
override func spec() {
describe("An AVPlayerWrapper") {
var wrapper: AVPlayerWrapper!
beforeEach {
wrapper = AVPlayerWrapper()
wrapper.automaticallyWaitsToMinimizeStalling = false
wrapper.volume = 0.0
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())
}
}
@@ -251,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) {
@@ -265,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 -2
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.9.0'
pod 'SwiftAudio', '~> 0.10.0'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.0
github "jorgenhenrichsen/SwiftAudio" ~> 0.10.0
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
+2 -19
View File
@@ -8,35 +8,18 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.9.0'
s.version = '0.10.0'
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
@@ -31,7 +31,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
@@ -77,6 +77,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
return avPlayer.currentItem
}
var _pendingAsset: AVAsset? = nil
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
@@ -86,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
@@ -104,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
@@ -114,7 +116,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var rate: Float {
get { return avPlayer.rate }
set { avPlayer.rate = newValue }
@@ -165,26 +167,63 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady
if currentItem?.status == .failed {
recreateAVPlayer()
}
// 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)
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
}
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?) {
@@ -200,6 +239,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
@@ -231,7 +273,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
self._state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .loading
self._state = .buffering
case .playing:
self._state = .playing
@unknown default:
@@ -243,17 +285,18 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
switch status {
case .readyToPlay:
self._state = .ready
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
if _playWhenReady {
self.play()
}
else {
self._state = .ready
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
}
break
case .failed:
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
break
@@ -9,7 +9,7 @@ import Foundation
import AVFoundation
protocol AVPlayerWrapperProtocol {
protocol AVPlayerWrapperProtocol: class {
var state: AVPlayerWrapperState { get }
@@ -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
+12 -10
View File
@@ -163,13 +163,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime())
if let item = item as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
self._currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
@@ -299,18 +292,27 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
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
}
@@ -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,10 +36,9 @@ class AVPlayerObserver: NSObject {
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
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?
weak var player: AVPlayer? {
@@ -66,13 +65,12 @@ class AVPlayerObserver: NSObject {
}
func stopObserving() {
guard let player = player, self.isObserving else {
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?) {
@@ -107,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)
}
/**
@@ -50,6 +48,11 @@ class AVPlayerTimeObserver {
self.periodicObserverTimeInterval = periodicObserverTimeInterval
}
deinit {
unregisterForPeriodicEvents()
unregisterForBoundaryTimeEvents()
}
/**
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
*/
@@ -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 {