Compare commits

..

41 Commits

Author SHA1 Message Date
Milen Pivchev 0870ec394a Allow looping of queue player 2021-01-11 13:02:22 +01:00
Milen Pivchev d8658fdc6f Change default value 2020-12-15 16:24:10 +01:00
Milen Pivchev 4530f9856a Add volume multiplier to regular AudioPlayer 2020-12-15 16:18:50 +01:00
Milen Pivchev 6a26073cfb Remove comment 2020-12-15 14:24:08 +01:00
Milen Pivchev b4e95aef09 Add volume to queued audio player 2020-12-15 14:23:47 +01:00
Milen Pivchev a37188ead3 Move volume logic 2020-12-15 13:58:44 +01:00
Milen Pivchev 915e5e22cd Make volume not optional 2020-12-15 11:58:00 +01:00
Milen Pivchev e8a825e012 Add volume 2020-12-15 11:42:53 +01:00
Milen Pivchev 5ec22ea2d4 Add looping 2020-11-24 10:37:24 +01:00
Jørgen Henrichsen b042c3ee6c Bump version 0.11.2. 2019-11-28 18:49:18 +01:00
Jørgen Henrichsen ee5db0c0d5 Merge pull request #87 from biesbjerg/always-emit-ready-event
Fixes a problem where the `ready` event would not be fired when passing `playWhenReady: true`.
2019-11-28 18:47:11 +01:00
Kim Biesbjerg a9509d454f always emit ready event 2019-11-01 10:41:37 +01:00
Jørgen Henrichsen e888c7954a Update Readme. Update podspec.
Bump version to 0.11.1.
2019-08-22 22:55:32 +02:00
Jørgen Henrichsen eea9aee4ec Merge pull request #74 from cbess/patch-1
Seek to initial time
2019-08-22 22:52:36 +02:00
Jørgen Henrichsen f2c95fa48c Merge branch 'master' into patch-1 2019-08-22 22:39:46 +02:00
Jørgen Henrichsen 5c8ac4da6b Update README
Remove incorrect recommendation to enable Remote Notifications.
2019-08-22 21:27:36 +02:00
C. Bess 5369d4f4e8 Seek to initial time
Use initial time operation, even for play when ready
2019-08-12 23:13:36 -05:00
Jørgen Henrichsen 09a7548f3a Update README. Update podspec.
Bump verision to 0.11.0.
2019-07-12 18:13:46 +02:00
Jørgen Henrichsen 565b9a04d4 Merge pull request #71 from dcvz/feature/asset-options
Add ability to set asset init options
2019-07-12 18:12:23 +02:00
David Chavez c2ec751ec0 Address review feedback 2019-07-10 13:56:05 +02:00
David Chavez 2c36c6c239 Add ability to set asset init options 2019-07-06 12:04:49 +02:00
Jørgen Henrichsen 1c4cbf676d Merge pull request #70 from jorgenhenrichsen/more-detaile-player-states
More detailed player states
2019-07-03 18:29:43 +02:00
Jørgen Henrichsen ec9492da17 Add tests for the loading state. 2019-07-03 17:48:28 +02:00
Jørgen Henrichsen 5add7699ff Added an example of how to handle network error.
The example app will now handle a network error, and reload when play is called, after a item failed to load because of a network error.
2019-07-03 16:41:14 +02:00
Jørgen Henrichsen bc1d775875 Reset audio file to spotify preview in example app. 2019-07-03 15:06:50 +02:00
Jørgen Henrichsen 4c1a545e87 Update podspec. Remove swift version file.
Clean up the podspec file and set swift version to 5.0. Remove the .swift-version file as it's use is deprecated.
2019-07-03 14:53:24 +02:00
Jørgen Henrichsen f31b52f81b Make the ready state reachable for items with initial time.
Fixes a problem where the .ready state would not be reached for audio items that had an initial time.
2019-07-03 14:25:43 +02:00
Jørgen Henrichsen 355c729078 Update README. Update podspec.
Bump version to 0.10.0.
2019-07-03 13:22:27 +02:00
Jørgen Henrichsen 719d3c852b Add an activityindicator to the example.
Shows how an activityindicator can be used to indicate loading, when player is loading or buffering.
2019-07-03 13:21:41 +02:00
Jørgen Henrichsen 4f33d7e688 Add a new state buffering.
Introduces an additional state to better describe the current state of the player.
Previously the player would be `idle` until the  item was loaded, then changing to `ready`. When the player started to play it would first enter `loading`, then subsequently `playing`.
Now it will instead immediately enter `loading`, then when the item is loaded it will enter `ready`. When the item then is played it will enter `buffering` and then `playing`.
This should make it easier to update UI accordingly to the players current state.
2019-07-03 11:23:10 +02:00
Jørgen Henrichsen 56d0633df0 Update README. Update podspec.
Bump to version 0.9.3.
2019-07-01 16:23:01 +02:00
Jørgen Henrichsen 825e508ecb Merge pull request #67 from jorgenhenrichsen/async-fixes
Async fixes
2019-07-01 16:14:20 +02:00
Jørgen Henrichsen 76d9dc17af Merge branch 'master' into async-fixes 2019-07-01 15:38:45 +02:00
Jørgen Henrichsen 8ceb0216a0 Merge pull request #65 from dcvz/feature/feedback-commands
Add commands for like, dislike and bookmark
2019-07-01 15:38:33 +02:00
David Chavez db41634631 Fix some missing sets 2019-06-25 10:49:10 +02:00
David Chavez 15843b5fe8 Add commands for like, dislike and bookmark 2019-06-24 23:57:17 +02:00
Jørgen Henrichsen 4241b484b2 Removed commented prints. 2019-06-23 18:19:08 +02:00
Jørgen Henrichsen 92bf0b96b6 Remove cancelloading in the load method.
This was already done in the reset(soft:) method.
Removed the nil check before canceling the pendingAsset load, since the coalescing `?` operator was already used anyway.
2019-06-23 18:13:48 +02:00
Jørgen Henrichsen 457dff7c01 Use weak capturing of the AudioPlayer. Remove initial time test.
Weak capturing of the AudioPlayer is done in the listener callback functions in order to not receive INVOPs in the event the audioPlayer is deallocated.
The initial time test is removed because of the unreliable results it produces.
2019-06-23 18:06:43 +02:00
Jørgen Henrichsen c90e9c4976 Unregister observers in their deinit method. 2019-06-23 16:41:47 +02:00
Jørgen Henrichsen 649bb01e89 Weak self and cancel previous load in load(from:playWhenReady).
Use weak self in the loadValuesAsync callback in the event where this is called after the player has be deintialized.
Cancel loading the pendingAsset before loading a new one.
2019-06-23 16:35:50 +02:00
20 changed files with 451 additions and 281 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...")
}
}
}
}
}
+6 -1
View File
@@ -30,7 +30,12 @@ class AVPlayerWrapperTests: XCTestCase {
XCTAssert(wrapper.state == AVPlayerWrapperState.idle)
}
func test_AVPlayerWrapper__state__when_loading_a_source__should_be_ready() {
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 {
+23 -24
View File
@@ -29,6 +29,11 @@ class AudioPlayerTests: XCTestCase {
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
}
func test_AudioPlayer__state__load_source__should_be_loading() {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
}
func test_AudioPlayer__state__load_source__should_be_ready() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
@@ -68,9 +73,9 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__pausing_source__should_be_paused() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing: self.audioPlayer.pause()
case .playing: audioPlayer?.pause()
case .paused: expectation.fulfill()
default: break
}
@@ -82,11 +87,11 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__stopping_source__should_be_idle() {
let expectation = XCTestExpectation()
var hasBeenPlaying: Bool = false
listener.stateUpdate = { state in
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing:
hasBeenPlaying = true
self.audioPlayer.stop()
audioPlayer?.stop()
case .idle:
if hasBeenPlaying {
expectation.fulfill()
@@ -117,22 +122,6 @@ class AudioPlayerTests: XCTestCase {
// wait(for: [expectation], timeout: 20.0)
// }
func test_AudioPlayer__currentTime__when_loading_source_with_intial_time__should_be_equal_to_initial_time() {
let expectation = XCTestExpectation()
let item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
listener.stateUpdate = { state in
switch state {
case .ready:
if self.audioPlayer.currentTime == item.getInitialTime() {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: item, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
@@ -141,10 +130,11 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__rate__playing_source__should_be_1() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
switch state {
case .playing:
if self.audioPlayer.rate == 1.0 {
if audioPlayer.rate == 1.0 {
expectation.fulfill()
}
default: break
@@ -162,10 +152,11 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
switch state {
case .ready:
if self.audioPlayer.currentItem != nil {
if audioPlayer.currentItem != nil {
expectation.fulfill()
}
default: break
@@ -191,12 +182,20 @@ class AudioPlayerEventListener {
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
var seekCompletion: (() -> Void)?
weak var audioPlayer: AudioPlayer?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
}
deinit {
audioPlayer?.event.stateChange.removeListener(self)
audioPlayer?.event.seek.removeListener(self)
audioPlayer?.event.secondElapse.removeListener(self)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
+2 -4
View File
@@ -25,13 +25,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.9.2'
pod 'SwiftAudio', '~> 0.11.2'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.2
github "jorgenhenrichsen/SwiftAudio" ~> 0.11.2
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
@@ -138,8 +138,6 @@ The current info can be cleared with:
```
### Remote Commands
**First** go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
To enable remote commands for the player you need to populate the RemoteCommands array for the player:
```swift
audioPlayer.remoteCommands = [
+2 -19
View File
@@ -8,35 +8,18 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.9.2'
s.version = '0.11.2'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
DESC
s.homepage = 'https://github.com/jorgenhenrichsen/SwiftAudio'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Jørgen Henrichsen' => 'jh.henrichs@gmail.com' }
s.source = { :git => 'https://github.com/jorgenhenrichsen/SwiftAudio.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '10.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudio/Classes/**/*'
# s.resource_bundles = {
# 'SwiftAudio' => ['SwiftAudio/Assets/*.png']
# }
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
end
@@ -168,7 +168,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
func load(from url: URL, playWhenReady: Bool) {
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
reset(soft: true)
_playWhenReady = playWhenReady
@@ -176,10 +178,16 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
recreateAVPlayer()
}
// Set item
self._pendingAsset = AVURLAsset(url: url)
self._pendingAsset = AVURLAsset(url: url, options: options)
if let pendingAsset = _pendingAsset {
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: {
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)
@@ -201,7 +209,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .failed:
// print("load asset failed")
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
@@ -209,7 +216,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .cancelled:
// print("load asset cancelled")
break
default:
@@ -220,10 +226,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady)
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
// MARK: - Util
@@ -233,10 +239,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
if self._pendingAsset != nil {
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
}
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if !soft {
avPlayer.replaceCurrentItem(with: nil)
@@ -269,7 +273,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
self._state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .loading
self._state = .buffering
case .playing:
self._state = .playing
@unknown default:
@@ -279,17 +283,14 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
else if let initialTime = _initialTime {
self.seek(to: initialTime)
}
break
case .failed:
@@ -49,8 +49,8 @@ protocol AVPlayerWrapperProtocol: class {
func seek(to seconds: TimeInterval)
func load(from url: URL, playWhenReady: Bool)
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
}
@@ -14,11 +14,14 @@ import Foundation
*/
public enum AVPlayerWrapperState: String {
/// The current item is set, and the player is ready to start loading (buffering).
/// An asset is being loaded for playback.
case loading
/// The current item is loaded, and the player is ready to start playing.
case ready
/// The current item is loading, getting ready to play.
case loading
/// The current item is playing, but are currently buffering.
case buffering
/// The player is paused.
case paused
+60 -29
View File
@@ -14,21 +14,21 @@ public enum SourceType {
}
public protocol AudioItem {
func getSourceUrl() -> String
func getArtist() -> String?
func getTitle() -> String?
func getAlbumTitle() -> String?
func getSourceType() -> SourceType
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
func getVolume() -> Float
}
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
public protocol TimePitching {
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
}
/// Make your `AudioItem`-subclass conform to this protocol to control enable the ability to start an item at a specific time of playback.
@@ -36,45 +36,52 @@ public protocol InitialTiming {
func getInitialTime() -> TimeInterval
}
/// Make your `AudioItem`-subclass conform to this protocol to set initialization options for the asset. Available keys available at [Apple Developer Documentation](https://developer.apple.com/documentation/avfoundation/avurlasset/initialization_options).
public protocol AssetOptionsProviding {
func getAssetOptions() -> [String: Any]
}
public class DefaultAudioItem: AudioItem {
public var audioVolume: Float
public var audioUrl: String
public var artist: String?
public var title: String?
public var albumTitle: String?
public var sourceType: SourceType
public var artwork: UIImage?
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil, audioVolume: Float = 1.0) {
self.audioUrl = audioUrl
self.artist = artist
self.title = title
self.albumTitle = albumTitle
self.sourceType = sourceType
self.artwork = artwork
self.audioVolume = audioVolume
}
public func getSourceUrl() -> String {
return audioUrl
}
public func getArtist() -> String? {
return artist
}
public func getTitle() -> String? {
return title
}
public func getAlbumTitle() -> String? {
return albumTitle
}
public func getSourceType() -> SourceType {
return sourceType
}
@@ -82,24 +89,27 @@ public class DefaultAudioItem: AudioItem {
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
public func getVolume() -> Float {
return audioVolume
}
}
/// An AudioItem that also conforms to the `TimePitching`-protocol
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
}
@@ -107,21 +117,42 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
/// An AudioItem that also conforms to the `InitialTiming`-protocol
public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
self.initialTime = 0.0
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) {
self.initialTime = initialTime
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getInitialTime() -> TimeInterval {
return initialTime
}
}
/// An AudioItem that also conforms to the `AssetOptionsProviding`-protocol
public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptionsProviding {
public var options: [String: Any]
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioVolume: Float?) {
self.options = [:]
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, options: [String: Any]) {
self.options = options
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getAssetOptions() -> [String: Any] {
return options
}
}
+86 -67
View File
@@ -11,84 +11,84 @@ import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public class AudioPlayer: AVPlayerWrapperDelegate {
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
return _currentItem
}
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
The elapsed playback time of the current item.
*/
public var currentTime: Double {
return wrapper.currentTime
}
/**
The duration of the current AudioItem.
*/
public var duration: Double {
return wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
return wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
public var playerState: AudioPlayerState {
return wrapper.state
}
// MARK: - Setters for AVPlayerWrapper
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { _wrapper.bufferDuration = newValue }
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
@@ -96,7 +96,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.timeEventFrequency }
set { _wrapper.timeEventFrequency = newValue }
}
/**
Indicates whether the player should automatically delay playback in order to minimize stalling
*/
@@ -104,12 +104,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { _wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { _wrapper.isMuted = newValue }
@@ -119,12 +119,25 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
get { return wrapper.rate }
set { _wrapper.rate = newValue }
}
public var volumeMultiplier: Float = 1 {
didSet {
guard let currentVolume = currentItem?.getVolume() else { return }
self.volume = currentVolume * volumeMultiplier
}
}
/**
Set wether the player should loop when a song is finished.
Default is `false`.
*/
public var loop: Bool = false
// MARK: - Init
/**
Create a new AudioPlayer.
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
@@ -132,16 +145,16 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self._wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self._wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
// MARK: - Player Actions
/**
Load an AudioItem into the manager.
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
@@ -158,40 +171,43 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
case .file:
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime())
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions())
self._currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
self.volume = item.getVolume() * volumeMultiplier
}
/**
Toggle playback status.
*/
public func togglePlaying() {
self.wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
self.wrapper.play()
}
/**
Pause playback
*/
public func pause() {
self.wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
@@ -200,7 +216,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
}
/**
Seek to a specific time in the item.
*/
@@ -210,13 +226,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
self.wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
self.enableRemoteCommands(item.getCommands())
@@ -225,12 +241,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.enableRemoteCommands(remoteCommands)
}
}
// MARK: - NowPlayingInfo
/**
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
@@ -239,19 +255,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
loadArtwork(forItem: item)
}
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
@@ -262,19 +278,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
private func updateNowPlayingDuration(_ duration: Double) {
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
}
private func updateNowPlayingRate(_ rate: Float) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
}
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
}
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
@@ -285,13 +301,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
}
}
// MARK: - Private
func reset() {
self._currentItem = nil
}
private func setTimePitchingAlgorithmForCurrentItem() {
if let item = currentItem as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
@@ -300,52 +316,55 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
}
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .ready:
case .ready, .loading:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
setTimePitchingAlgorithmForCurrentItem()
case .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
updateNowPlayingPlaybackValues()
}
default: break
}
self.event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
self.event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.event.fail.emit(data: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
}
func AVWrapperItemDidPlayToEndTime() {
if loop {
seek(to: .zero)
play()
}
self.event.playbackEnd.emit(data: .playedUntilEnd)
}
func AVWrapperDidRecreateAVPlayer() {
self.event.didRecreateAVPlayer.emit(data: ())
}
}
@@ -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.
*/
+37 -37
View File
@@ -9,32 +9,32 @@ import Foundation
class QueueManager<T> {
private var _items: [T] = []
/**
All items held by the queue.
*/
public var items: [T] {
return _items
}
public var nextItems: [T] {
guard _currentIndex + 1 < _items.count else {
return []
}
return Array(_items[_currentIndex + 1..<_items.count])
}
public var previousItems: [T] {
if (_currentIndex == 0) {
return []
}
return Array(_items[0..<_currentIndex])
}
private var _currentIndex: Int = 0
/**
The index of the current item.
Will be populated event though there is no current item (When the queue is empty).
@@ -42,7 +42,7 @@ class QueueManager<T> {
public var currentIndex: Int {
return _currentIndex
}
/**
The current item for the queue.
*/
@@ -52,28 +52,28 @@ class QueueManager<T> {
}
return nil
}
/**
Add a single item to the queue.
- parameter item: The `AudioItem` to be added.
*/
public func addItem(_ item: T) {
_items.append(item)
}
/**
Add an array of items to the queue.
- parameter items: The `AudioItem`s to be added.
*/
public func addItems(_ items: [T]) {
_items.append(contentsOf: items)
}
/**
Add an array of items to the queue at a given index.
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
@@ -81,15 +81,15 @@ class QueueManager<T> {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
}
/**
Get the next item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The next item.
*/
@@ -102,7 +102,7 @@ class QueueManager<T> {
_currentIndex = nextIndex
return _items[nextIndex]
}
/**
Get the previous item in the queue, if there are any.
Will update the current item.
@@ -119,21 +119,21 @@ class QueueManager<T> {
_currentIndex = previousIndex
return _items[previousIndex]
}
/**
Jump to a position in the queue.
Will update the current item.
- parameter index: The index to jump to.
- throws: `APError.QueueError`
- returns: The item at the index.
*/
@discardableResult
func jump(to index: Int) throws -> T {
guard index != currentIndex else {
func jump(to index: Int, allowSameIndex: Bool = false) throws -> T {
guard index != currentIndex || allowSameIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(_items.count))")
}
@@ -141,35 +141,35 @@ class QueueManager<T> {
_currentIndex = index
return _items[index]
}
/**
Move an item in the queue.
- parameter fromIndex: The index of the item to be moved.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
guard fromIndex != _currentIndex else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
}
guard fromIndex >= 0 && fromIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
guard toIndex >= 0 && toIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
let item = try removeItem(at: fromIndex)
try addItems([item], at: toIndex)
}
/**
Remove an item.
- parameter index: The index of the item to remove.
- throws: APError.QueueError
- returns: The removed item.
@@ -179,31 +179,31 @@ class QueueManager<T> {
guard index != _currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be positive and smaller than the count of current items (\(_items.count)).")
}
if index < _currentIndex {
_currentIndex = _currentIndex - 1
}
return _items.remove(at: index)
}
/**
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: T) {
if current == nil {
self.addItem(item)
}
self._items[_currentIndex] = item
}
/**
Remove all previous items in the queue.
If no previous items exist, no action will be taken.
@@ -223,7 +223,7 @@ class QueueManager<T> {
guard nextIndex < _items.count else { return }
_items.removeSubrange(nextIndex..<_items.count)
}
/**
Removes all items for queue
*/
+52 -36
View File
@@ -12,72 +12,80 @@ import MediaPlayer
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer {
let queueManager: QueueManager = QueueManager<AudioItem>()
/**
Set wether the player should automatically play the next song when a song is finished.
Default is `true`.
*/
public var automaticallyPlayNextSong: Bool = true
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
}
/**
Stops the player and clears the queue.
*/
public override func stop() {
super.stop()
}
override func reset() {
queueManager.clearQueue()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
return queueManager.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem] {
return queueManager.nextItems
}
public override var volumeMultiplier: Float {
didSet {
guard let currentVolume = queueManager.current?.getVolume() else { return }
self.volume = currentVolume * volumeMultiplier
}
}
/**
Will replace the current item with a new one and load it into the player.
- parameter item: The AudioItem to replace the current item.
- throws: APError.LoadError
*/
public override func load(item: AudioItem, playWhenReady: Bool) throws {
try super.load(item: item, playWhenReady: playWhenReady)
queueManager.replaceCurrentItem(with: item)
}
/**
Add a single item to the queue.
- parameter item: The item to add.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
@@ -91,10 +99,10 @@ public class QueuedAudioPlayer: AudioPlayer {
queueManager.addItem(item)
}
}
/**
Add items to the queue.
- parameter items: The items to add to the queue.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the first item in the list. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
@@ -108,22 +116,30 @@ public class QueuedAudioPlayer: AudioPlayer {
queueManager.addItems(items)
}
}
public func add(items: [AudioItem], at index: Int) throws {
try queueManager.addItems(items, at: index)
}
/**
Step to the next item in the queue.
Step to the next item in the queue. If the queue has no next items, it starts again from the first item.
- parameter canRepeatSingleItem: if there is only one item in the queue, allow repeating it forever
- throws: `APError`
*/
public func next() throws {
public func next(canRepeatSingleItem: Bool = false) throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
let nextItem: AudioItem?
do {
nextItem = try queueManager.next()
} catch {
nextItem = try queueManager.jump(to: 0, allowSameIndex: canRepeatSingleItem)
}
try self.load(item: nextItem!, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
@@ -132,20 +148,20 @@ public class QueuedAudioPlayer: AudioPlayer {
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
}
/**
Remove an item from the queue.
- parameter index: The index of the item to remove.
- throws: `APError.QueueError`
*/
public func removeItem(at index: Int) throws {
try queueManager.removeItem(at: index)
}
/**
Jump to a certain item in the queue.
- parameter index: The index of the item to jump to.
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
- throws: `APError`
@@ -155,10 +171,10 @@ public class QueuedAudioPlayer: AudioPlayer {
let item = try queueManager.jump(to: index)
try self.load(item: item, playWhenReady: playWhenReady)
}
/**
Move an item in the queue from one position to another.
- parameter fromIndex: The index of the item to move.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
@@ -166,28 +182,28 @@ public class QueuedAudioPlayer: AudioPlayer {
func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
/**
Remove all upcoming items, those returned by `next()`
*/
public func removeUpcomingItems() {
queueManager.removeUpcomingItems()
}
/**
Remove all previous items, those returned by `previous()`
*/
public func removePreviousItems() {
queueManager.removePreviousItems()
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidPlayToEndTime() {
super.AVWrapperItemDidPlayToEndTime()
if automaticallyPlayNextSong {
try? self.next()
try? self.next(canRepeatSingleItem: true)
}
super.AVWrapperItemDidPlayToEndTime()
}
}
@@ -79,6 +79,30 @@ public struct SkipIntervalCommand: RemoteCommandProtocol {
}
public struct FeedbackCommand: RemoteCommandProtocol {
public static let like = FeedbackCommand(id: "Like", commandKeyPath: \MPRemoteCommandCenter.likeCommand, handlerKeyPath: \RemoteCommandController.handleLikeCommand)
public static let dislike = FeedbackCommand(id: "Dislike", commandKeyPath: \MPRemoteCommandCenter.dislikeCommand, handlerKeyPath: \RemoteCommandController.handleDislikeCommand)
public static let bookmark = FeedbackCommand(id: "Bookmark", commandKeyPath: \MPRemoteCommandCenter.bookmarkCommand, handlerKeyPath: \RemoteCommandController.handleBookmarkCommand)
public typealias Command = MPFeedbackCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPFeedbackCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
func set(isActive: Bool, localizedTitle: String, localizedShortTitle: String) -> FeedbackCommand {
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].isActive = isActive
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedTitle = localizedTitle
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedShortTitle = localizedShortTitle
return self
}
}
public enum RemoteCommand {
case play
@@ -99,6 +123,12 @@ public enum RemoteCommand {
case skipBackward(preferredIntervals: [NSNumber])
case like(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
/**
All values in an array for convenience.
Don't use for associated values.
@@ -114,6 +144,9 @@ public enum RemoteCommand {
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
.like(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.dislike(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.bookmark(isActive: false, localizedTitle: "", localizedShortTitle: "")
]
}
@@ -64,6 +64,12 @@ public class RemoteCommandController {
case .changePlaybackPosition: self.enableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
case .skipBackward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
case .like(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.like.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .dislike(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.dislike.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .bookmark(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.bookmark.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
}
}
@@ -78,6 +84,9 @@ public class RemoteCommandController {
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
case .like(_, _, _): self.disableCommand(FeedbackCommand.like)
case .dislike(_, _, _): self.disableCommand(FeedbackCommand.dislike)
case .bookmark(_, _, _): self.disableCommand(FeedbackCommand.bookmark)
}
}
@@ -92,6 +101,9 @@ public class RemoteCommandController {
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
@@ -180,6 +192,18 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.LoadError {
switch error {