Compare commits
30 Commits
0.9.3
...
app/soundscape
| Author | SHA1 | Date | |
|---|---|---|---|
| 0870ec394a | |||
| d8658fdc6f | |||
| 4530f9856a | |||
| 6a26073cfb | |||
| b4e95aef09 | |||
| a37188ead3 | |||
| 915e5e22cd | |||
| e8a825e012 | |||
| 5ec22ea2d4 | |||
| b042c3ee6c | |||
| ee5db0c0d5 | |||
| a9509d454f | |||
| e888c7954a | |||
| eea9aee4ec | |||
| f2c95fa48c | |||
| 5c8ac4da6b | |||
| 5369d4f4e8 | |||
| 09a7548f3a | |||
| 565b9a04d4 | |||
| c2ec751ec0 | |||
| 2c36c6c239 | |||
| 1c4cbf676d | |||
| ec9492da17 | |||
| 5add7699ff | |||
| bc1d775875 | |||
| 4c1a545e87 | |||
| f31b52f81b | |||
| 355c729078 | |||
| 719d3c852b | |||
| 4f33d7e688 |
@@ -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...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.3'
|
||||
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.3
|
||||
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
@@ -8,35 +8,18 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.9.3'
|
||||
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
|
||||
|
||||
@@ -170,7 +170,7 @@ 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
|
||||
|
||||
@@ -178,9 +178,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
self._pendingAsset = AVURLAsset(url: url)
|
||||
self._pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = _pendingAsset {
|
||||
self._state = .loading
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
||||
|
||||
guard let self = self else {
|
||||
@@ -225,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
|
||||
@@ -272,7 +273,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
self._state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
self._state = .loading
|
||||
self._state = .buffering
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
@unknown default:
|
||||
@@ -282,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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: ())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user