Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c4cbf676d | |||
| ec9492da17 | |||
| 5add7699ff | |||
| bc1d775875 | |||
| 4c1a545e87 | |||
| f31b52f81b | |||
| 355c729078 | |||
| 719d3c852b | |||
| 4f33d7e688 | |||
| 56d0633df0 | |||
| 825e508ecb | |||
| 76d9dc17af | |||
| 8ceb0216a0 | |||
| db41634631 | |||
| 15843b5fe8 | |||
| 4241b484b2 | |||
| 92bf0b96b6 | |||
| 457dff7c01 | |||
| c90e9c4976 | |||
| 649bb01e89 | |||
| 965f7dbbe1 | |||
| 536414bd44 | |||
| b7d5db0a55 | |||
| d0907b16c8 | |||
| 60eb4b4676 | |||
| 4ec9e9c2a2 | |||
| b2efdf4d65 | |||
| d9badfec9b | |||
| 4d0f29adf6 | |||
| f817bc5840 | |||
| 5755b70e1b | |||
| 83ac2af5d6 | |||
| a2f001a968 | |||
| 9390064581 | |||
| 7a45ee9e47 | |||
| 17a4b18333 | |||
| ad01101d3c | |||
| aab8a2302b | |||
| 4a512c6aa0 |
@@ -1 +0,0 @@
|
||||
4.2
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# SwiftAudio
|
||||
|
||||
[](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.1'
|
||||
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.1
|
||||
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).
|
||||
|
||||
|
||||
@@ -8,35 +8,18 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.9.1'
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||