Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23fdb9b9db | |||
| 24c19aa661 | |||
| 38429c6ca8 | |||
| 72f9c5d147 | |||
| bd93898809 | |||
| 8276f38b1b | |||
| fcd5790e1e | |||
| ead7c0962e | |||
| 7ff34271e8 | |||
| 4f7a5b02a6 | |||
| af803339dc | |||
| a5bf6eb1dd | |||
| 5e0c27b990 | |||
| 6079234942 | |||
| e74b5ffe4d | |||
| 92554a187c | |||
| 473651f357 | |||
| db2f3e9af7 | |||
| a9f831a258 | |||
| cc3840d81e | |||
| 5307090ea3 | |||
| bdaee8b18f | |||
| 84d359bc4f | |||
| 40ea7ad2f9 | |||
| f2f1c1236c | |||
| a75f0d0201 | |||
| 9e4e7f6807 | |||
| dbd3b03989 | |||
| 7e19604df7 | |||
| 481130dc58 | |||
| 300b34afa3 | |||
| da3af0e9db | |||
| d9eb313c1b | |||
| cca7f68da4 | |||
| 7ed74b80ec | |||
| 2773e4bfec | |||
| 77dc8f4ff1 | |||
| accdf2c00c | |||
| 542d3a5764 | |||
| 4131e54f3e | |||
| 03c4a7310f | |||
| 9d2d2594a1 | |||
| 4e790876cb | |||
| b19d01bdfc | |||
| 3c8ecb353c | |||
| cafd513468 | |||
| 7b8a4f318d | |||
| acab6473b2 | |||
| 57b6fb08f3 |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: DoubleSymmetry
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,5 +1,12 @@
|
||||
name: validate
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: macos-latest
|
||||
@@ -17,4 +24,4 @@ jobs:
|
||||
cd Example
|
||||
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
|
||||
env:
|
||||
destination: ${{ matrix.destination }}
|
||||
destination: ${{ matrix.destination }}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
|
||||
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA39266028E200C7A389 /* SwiftAudio */; };
|
||||
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA3B26602C0E00C7A389 /* SwiftAudio */; };
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */; };
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
|
||||
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftAudio_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -111,7 +111,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */,
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */,
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
|
||||
);
|
||||
@@ -222,7 +222,7 @@
|
||||
9B05AA2F2660276400C7A389 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */,
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -244,7 +244,7 @@
|
||||
);
|
||||
name = SwiftAudio_Example;
|
||||
packageProductDependencies = (
|
||||
9B05AA39266028E200C7A389 /* SwiftAudio */,
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = SwiftAudio;
|
||||
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
|
||||
@@ -267,7 +267,7 @@
|
||||
packageProductDependencies = (
|
||||
9B05AA302660276400C7A389 /* Quick */,
|
||||
9B05AA322660276400C7A389 /* Nimble */,
|
||||
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = Tests;
|
||||
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
|
||||
@@ -532,7 +532,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -551,7 +551,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -577,6 +577,7 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -598,6 +599,7 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -672,13 +674,13 @@
|
||||
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
|
||||
productName = Nimble;
|
||||
};
|
||||
9B05AA39266028E200C7A389 /* SwiftAudio */ = {
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudio;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
9B05AA3B26602C0E00C7A389 /* SwiftAudio */ = {
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudio;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class AudioController {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueViewController: UIViewController {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
@@ -31,6 +31,7 @@ class ViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
|
||||
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
|
||||
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
|
||||
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
|
||||
@@ -106,7 +107,7 @@ class ViewController: UIViewController {
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print(data)
|
||||
print("state=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.setPlayButtonState(forAudioPlayerState: data)
|
||||
switch data {
|
||||
@@ -126,6 +127,10 @@ class ViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
|
||||
print("playEndReason=\(data)")
|
||||
}
|
||||
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerItemNotificationObserverTests: QuickSpec {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerItemObserverTests: QuickSpec {
|
||||
|
||||
@@ -47,9 +47,9 @@ class AVPlayerItemObserverTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
|
||||
var receivedMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
var receivedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerTimeObserverTests: QuickSpec {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerWrapperTests: XCTestCase {
|
||||
@@ -145,6 +145,17 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
let expectation = XCTestExpectation()
|
||||
@@ -182,7 +193,7 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerEventTests: QuickSpec {
|
||||
|
||||
|
||||
@@ -3,37 +3,35 @@ import Nimble
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
audioPlayer.bufferDuration = 0.001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -45,7 +43,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -57,7 +55,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__play_source__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -70,7 +68,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__pausing_source__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -83,7 +81,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__stopping_source__should_be_idle() {
|
||||
let expectation = XCTestExpectation()
|
||||
var hasBeenPlaying: Bool = false
|
||||
@@ -102,13 +100,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Current time
|
||||
|
||||
|
||||
func test_AudioPlayer__currentTime__should_be_0() {
|
||||
XCTAssert(audioPlayer.currentTime == 0.0)
|
||||
}
|
||||
|
||||
|
||||
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
|
||||
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
|
||||
// let expectation = XCTestExpectation()
|
||||
@@ -121,13 +119,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
// wait(for: [expectation], timeout: 20.0)
|
||||
// }
|
||||
|
||||
|
||||
// MARK: - Rate
|
||||
|
||||
func test_AudioPlayer__rate__should_be_0() {
|
||||
XCTAssert(audioPlayer.rate == 0.0)
|
||||
|
||||
func test_AudioPlayer__rate__should_be_1() {
|
||||
XCTAssert(audioPlayer.rate == 1.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__rate__playing_source__should_be_1() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -143,13 +141,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Current item
|
||||
|
||||
|
||||
func test_AudioPlayer__currentItem__should_be_nil() {
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -165,11 +163,11 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class AudioPlayerEventListener {
|
||||
|
||||
|
||||
var state: AudioPlayerState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
@@ -177,35 +175,35 @@ class AudioPlayerEventListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
audioPlayer?.event.stateChange.removeListener(self)
|
||||
audioPlayer?.event.seek.removeListener(self)
|
||||
audioPlayer?.event.secondElapse.removeListener(self)
|
||||
}
|
||||
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
seekCompletion?()
|
||||
}
|
||||
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
self.secondsElapse?(data)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioSessionControllerTests: QuickSpec {
|
||||
|
||||
@@ -52,11 +52,12 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
describe("its delegate") {
|
||||
context("when a interruption arrives") {
|
||||
context("when a ended interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0)
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0),
|
||||
AVAudioSessionInterruptionOptionKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
@@ -64,7 +65,23 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventuallyNot(beNil())
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.ended(shouldResume: true)))
|
||||
}
|
||||
|
||||
}
|
||||
context("when a begin interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.began))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,10 +108,9 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
var interruptionType: InterruptionType? = nil
|
||||
|
||||
var interruptionType: AVAudioSession.InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType) {
|
||||
func handleInterruption(type: InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class NonFailingAudioSession: AudioSession {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
final class MockDispatchQueue: DispatchQueueType {
|
||||
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoControllerTests: QuickSpec {
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
|
||||
class NowPlayingInfoTests: QuickSpec {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueManagerTests: QuickSpec {
|
||||
@@ -70,6 +70,53 @@ class QueueManagerTests: QuickSpec {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
describe("when adding at index") {
|
||||
context("adding item at index 0 when queue is empty") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([3], at: 0)
|
||||
expect(manager.current).to(equal(3))
|
||||
}
|
||||
}
|
||||
|
||||
context("adding item at index") {
|
||||
beforeEach {
|
||||
manager.addItems([3, 1])
|
||||
}
|
||||
|
||||
context("current [element count]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: manager.items.count)
|
||||
expect(manager.items.last).to(equal(5))
|
||||
}
|
||||
}
|
||||
|
||||
context("before the [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: 0)
|
||||
expect(manager.current).to(equal(3))
|
||||
expect(manager.currentIndex).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("after the [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: 1)
|
||||
expect(manager.current).to(equal(3))
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("at [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.next()
|
||||
try manager.addItems([5], at: 1)
|
||||
expect(manager.current).to(equal(1))
|
||||
expect(manager.currentIndex).to(equal(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding one item") {
|
||||
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
extension QueuedAudioPlayer {
|
||||
class SeekEventListener {
|
||||
var eventResult: (Int, Bool) = (-1, false)
|
||||
func handleEvent(seconds: Int, didFinish: Bool) { eventResult = (seconds, didFinish) }
|
||||
}
|
||||
|
||||
func seekWithExpectation(to time: Double) {
|
||||
let eventListener = SeekEventListener()
|
||||
event.seek.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
seek(to: time)
|
||||
expect(eventListener.eventResult).toEventually(equal((0, true)))
|
||||
}
|
||||
}
|
||||
|
||||
class QueuedAudioPlayerTests: QuickSpec {
|
||||
override func spec() {
|
||||
@@ -9,15 +24,13 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
var audioPlayer: QueuedAudioPlayer!
|
||||
beforeEach {
|
||||
audioPlayer = QueuedAudioPlayer()
|
||||
audioPlayer.bufferDuration = 0.0001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
audioPlayer.volume = 0.0
|
||||
}
|
||||
describe("its current item") {
|
||||
it("should be nil") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
|
||||
|
||||
context("when adding one item") {
|
||||
var item: AudioItem!
|
||||
beforeEach {
|
||||
@@ -27,18 +40,18 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should not be nil") {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
}
|
||||
|
||||
|
||||
context("then loading a new item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
|
||||
it("should have replaced the item") {
|
||||
expect(audioPlayer.currentItem?.getSourceUrl()).toNot(equal(item.getSourceUrl()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("when adding multiple items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
|
||||
@@ -48,12 +61,12 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("its next items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
@@ -61,7 +74,7 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should contain 1 item") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
}
|
||||
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
@@ -69,7 +82,7 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should contain 0 items") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
@@ -79,17 +92,17 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing one item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.removeItem(at: 1)
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then jumping to the last item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.jumpToItem(atIndex: 1)
|
||||
@@ -98,43 +111,43 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing upcoming items") {
|
||||
beforeEach {
|
||||
audioPlayer.removeUpcomingItems()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("its previous items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
@@ -143,27 +156,388 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.previousItems.count).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing all previous items") {
|
||||
beforeEach {
|
||||
audioPlayer.removePreviousItems()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
describe("onNext") {
|
||||
context("player was playing") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should go to next item and play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
context("player was paused") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
audioPlayer.pause()
|
||||
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should go to next item and not play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("onPrevious") {
|
||||
context("player was playing") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
}
|
||||
|
||||
it("should go to previous item and play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.previousItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
context("player was paused") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
try? audioPlayer.next()
|
||||
audioPlayer.pause()
|
||||
|
||||
}
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
}
|
||||
|
||||
it("should go to previous item and not play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.previousItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestEventListener {
|
||||
var eventResult: (Int?, Int?) = (-1, -1)
|
||||
func handleEvent(previousIndex: Int?, nextIndex: Int?) { eventResult = (previousIndex, nextIndex) }
|
||||
}
|
||||
|
||||
describe("its repeat mode") {
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
|
||||
}
|
||||
|
||||
context("then setting repeat mode off") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
context("allow playback to end normally") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should stop playback normally") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, nil)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
it("should fail") {
|
||||
try? audioPlayer.next()
|
||||
expect(try audioPlayer.next()).to(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode track") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode queue") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.currentIndex).to(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
expect(audioPlayer.currentIndex).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding 1 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(item: ShortSource.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
context("then setting repeat mode off") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
context("allow playback to end normally") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should stop playback normally") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, nil)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should fail") {
|
||||
try? audioPlayer.next()
|
||||
expect(try audioPlayer.next()).to(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode track") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode queue") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
// workaround: seek not to beggining, for 0 expecations to correctly fail if necessary.
|
||||
audioPlayer.seekWithExpectation(to: 0.05)
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
import UIKit
|
||||
|
||||
struct Source {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Double Symmetry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+7
-7
@@ -2,18 +2,18 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftAudio",
|
||||
platforms: [.iOS(.v10)],
|
||||
name: "SwiftAudioEx",
|
||||
platforms: [.iOS(.v11)],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftAudio",
|
||||
targets: ["SwiftAudio"]),
|
||||
name: "SwiftAudioEx",
|
||||
targets: ["SwiftAudioEx"]),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftAudio",
|
||||
name: "SwiftAudioEx",
|
||||
dependencies: [],
|
||||
path: "SwiftAudio/Classes")
|
||||
path: "SwiftAudioEx/Classes")
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ To see the audio player in action, run the example project!
|
||||
To run the example project, clone the repo, and run `pod install` from the Example directory first.
|
||||
|
||||
## Requirements
|
||||
iOS 10.0+
|
||||
iOS 11.0+
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '0.13.1'
|
||||
s.version = '0.15.3'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
s.description = <<-DESC
|
||||
SwiftAudioEx is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
|
||||
@@ -20,7 +20,7 @@ DESC
|
||||
'Jørgen Henrichsen' => 'jh.henrichs@gmail.com', }
|
||||
s.source = { :git => 'https://github.com/DoubleSymmetry/SwiftAudioEx.git', :tag => s.version.to_s }
|
||||
|
||||
s.ios.deployment_target = '10.0'
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.swift_version = '5.0'
|
||||
s.source_files = 'SwiftAudioEx/Classes/**/*'
|
||||
end
|
||||
|
||||
@@ -22,6 +22,7 @@ public struct APError {
|
||||
case noPreviousItem
|
||||
case noNextItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
case noNextWhenRepeatModeTrack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,65 +26,75 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
let playerItemObserver: AVPlayerItemObserver
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate var _playWhenReady: Bool = true
|
||||
fileprivate var _initialTime: TimeInterval?
|
||||
|
||||
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
if oldValue != _state {
|
||||
self.delegate?.AVWrapper(didChangeState: _state)
|
||||
}
|
||||
}
|
||||
}
|
||||
fileprivate var avPlayer = AVPlayer()
|
||||
private let playerObserver = AVPlayerObserver()
|
||||
internal let playerTimeObserver: AVPlayerTimeObserver
|
||||
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
private let playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
fileprivate var initialTime: TimeInterval?
|
||||
fileprivate var pendingAsset: AVAsset? = nil
|
||||
|
||||
/// True when the track was paused for the purpose of switching tracks
|
||||
fileprivate var pausedForLoad: Bool = false
|
||||
|
||||
public init() {
|
||||
self.avPlayer = AVPlayer()
|
||||
self.playerObserver = AVPlayerObserver()
|
||||
self.playerObserver.player = avPlayer
|
||||
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
self.playerTimeObserver.player = avPlayer
|
||||
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
self.playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
self.playerObserver.delegate = self
|
||||
self.playerTimeObserver.delegate = self
|
||||
self.playerItemNotificationObserver.delegate = self
|
||||
self.playerItemObserver.delegate = self
|
||||
playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
playerTimeObserver.player = avPlayer
|
||||
|
||||
playerObserver.player = avPlayer
|
||||
playerObserver.delegate = self
|
||||
playerTimeObserver.delegate = self
|
||||
playerItemNotificationObserver.delegate = self
|
||||
playerItemObserver.delegate = self
|
||||
|
||||
// disabled since we're not making use of video playback
|
||||
self.avPlayer.allowsExternalPlayback = false;
|
||||
avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
var state: AVPlayerWrapperState {
|
||||
return _state
|
||||
|
||||
fileprivate(set) var state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
if oldValue != state {
|
||||
delegate?.AVWrapper(didChangeState: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
return avPlayer.reasonForWaitingToPlay
|
||||
|
||||
fileprivate(set) var lastPlayerTimeControlStatus: AVPlayer.TimeControlStatus = AVPlayer.TimeControlStatus.paused {
|
||||
didSet {
|
||||
if oldValue != lastPlayerTimeControlStatus {
|
||||
switch lastPlayerTimeControlStatus {
|
||||
case .paused:
|
||||
if pendingAsset == nil {
|
||||
state = .idle
|
||||
}
|
||||
else if currentItem != nil && pausedForLoad != true {
|
||||
state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
if pendingAsset != nil {
|
||||
state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate(set) var playWhenReady: Bool = true
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
return avPlayer.currentItem
|
||||
}
|
||||
|
||||
var _pendingAsset: AVAsset? = nil
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
avPlayer.currentItem
|
||||
}
|
||||
|
||||
var currentTime: TimeInterval {
|
||||
@@ -99,49 +109,58 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
get { avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
|
||||
|
||||
var timeEventFrequency: TimeEventFrequency = .everySecond {
|
||||
didSet {
|
||||
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
get { return avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
}
|
||||
|
||||
var volume: Float {
|
||||
get { return avPlayer.volume }
|
||||
get { avPlayer.volume }
|
||||
set { avPlayer.volume = newValue }
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get { return avPlayer.isMuted }
|
||||
get { avPlayer.isMuted }
|
||||
set { avPlayer.isMuted = newValue }
|
||||
}
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
func play() {
|
||||
_playWhenReady = true
|
||||
playWhenReady = true
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
_playWhenReady = false
|
||||
playWhenReady = false
|
||||
avPlayer.pause()
|
||||
}
|
||||
|
||||
@@ -162,65 +181,75 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self._initialTime {
|
||||
self._initialTime = nil
|
||||
if self._playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
// if the player is loading then we need to defer seeking until it's ready.
|
||||
if (state == AVPlayerWrapperState.loading) {
|
||||
initialTime = seconds
|
||||
} else {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self.initialTime {
|
||||
self.initialTime = nil
|
||||
if self.playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
self.playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
self._pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = _pendingAsset {
|
||||
self._state = .loading
|
||||
if let pendingAsset = pendingAsset {
|
||||
state = .loading
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
||||
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
|
||||
var error: NSError? = nil
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
|
||||
if (pendingAsset != self.pendingAsset) { return; }
|
||||
switch status {
|
||||
case .loaded:
|
||||
if isPendingAsset {
|
||||
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
self.playerItemObserver.startObserving(item: currentItem)
|
||||
let item = AVPlayerItem(
|
||||
asset: pendingAsset,
|
||||
automaticallyLoadedAssetKeys: [Constants.assetPlayableKey]
|
||||
)
|
||||
item.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: item)
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: item)
|
||||
self.playerItemObserver.startObserving(item: item)
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
|
||||
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
|
||||
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: [group])
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
if isPendingAsset {
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
self._pendingAsset = nil
|
||||
}
|
||||
self.reset(soft: false)
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
@@ -235,8 +264,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
|
||||
_initialTime = initialTime
|
||||
self.pause()
|
||||
self.initialTime = initialTime
|
||||
|
||||
pausedForLoad = true
|
||||
pause()
|
||||
|
||||
self.load(from: url, playWhenReady: playWhenReady, options: options)
|
||||
}
|
||||
|
||||
@@ -246,9 +278,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
self._pendingAsset?.cancelLoading()
|
||||
self._pendingAsset = nil
|
||||
|
||||
pendingAsset?.cancelLoading()
|
||||
pendingAsset = nil
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
@@ -272,37 +304,24 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
_state = .idle
|
||||
}
|
||||
else {
|
||||
self._state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
self._state = .buffering
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
lastPlayerTimeControlStatus = status;
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
if _playWhenReady && (_initialTime ?? 0) == 0 {
|
||||
self.play()
|
||||
state = .ready
|
||||
pausedForLoad = false
|
||||
if playWhenReady && (initialTime ?? 0) == 0 {
|
||||
play()
|
||||
}
|
||||
else if let initialTime = _initialTime {
|
||||
self.seek(to: initialTime)
|
||||
else if let initialTime = initialTime {
|
||||
seek(to: initialTime)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
break
|
||||
|
||||
case .unknown:
|
||||
@@ -311,7 +330,6 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
@@ -319,11 +337,11 @@ extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
// MARK: - AVPlayerTimeObserverDelegate
|
||||
|
||||
func audioDidStart() {
|
||||
self._state = .playing
|
||||
state = .playing
|
||||
}
|
||||
|
||||
func timeEvent(time: CMTime) {
|
||||
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -343,11 +361,11 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
protocol AVPlayerWrapperDelegate: class {
|
||||
protocol AVPlayerWrapperDelegate: AnyObject {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol: class {
|
||||
|
||||
protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var state: AVPlayerWrapperState { get }
|
||||
|
||||
var playWhenReady: Bool { get }
|
||||
|
||||
var currentItem: AVPlayerItem? { get }
|
||||
|
||||
@@ -52,5 +54,4 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
}
|
||||
|
||||
@@ -66,23 +66,23 @@ public class DefaultAudioItem: AudioItem {
|
||||
}
|
||||
|
||||
public func getSourceUrl() -> String {
|
||||
return audioUrl
|
||||
audioUrl
|
||||
}
|
||||
|
||||
public func getArtist() -> String? {
|
||||
return artist
|
||||
artist
|
||||
}
|
||||
|
||||
public func getTitle() -> String? {
|
||||
return title
|
||||
title
|
||||
}
|
||||
|
||||
public func getAlbumTitle() -> String? {
|
||||
return albumTitle
|
||||
albumTitle
|
||||
}
|
||||
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
sourceType
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
@@ -97,17 +97,17 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain
|
||||
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
|
||||
pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
pitchAlgorithmType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
public var initialTime: TimeInterval
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.initialTime = 0.0
|
||||
initialTime = 0.0
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
}
|
||||
|
||||
public func getInitialTime() -> TimeInterval {
|
||||
return initialTime
|
||||
initialTime
|
||||
}
|
||||
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
public var options: [String: Any]
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.options = [:]
|
||||
options = [:]
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
}
|
||||
|
||||
public func getAssetOptions() -> [String: Any] {
|
||||
return options
|
||||
options
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,22 +11,15 @@ 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
|
||||
}
|
||||
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public let event = EventHolder()
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
return _currentItem
|
||||
}
|
||||
private(set) var currentItem: AudioItem?
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
@@ -37,42 +30,52 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
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
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
public var remoteCommands: [RemoteCommand] = []
|
||||
public var remoteCommands: [RemoteCommand] = [] {
|
||||
didSet {
|
||||
if let item = currentItem {
|
||||
self.enableRemoteCommands(forItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
internal var willPlayWhenReady: Bool {
|
||||
wrapper.playWhenReady
|
||||
}
|
||||
|
||||
/**
|
||||
The elapsed playback time of the current item.
|
||||
*/
|
||||
public var currentTime: Double {
|
||||
return wrapper.currentTime
|
||||
wrapper.currentTime
|
||||
}
|
||||
|
||||
/**
|
||||
The duration of the current AudioItem.
|
||||
*/
|
||||
public var duration: Double {
|
||||
return wrapper.duration
|
||||
wrapper.duration
|
||||
}
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
return wrapper.bufferedPosition
|
||||
wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
public var playerState: AudioPlayerState {
|
||||
return wrapper.state
|
||||
wrapper.state
|
||||
}
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
@@ -85,45 +88,45 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
- 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 }
|
||||
get { wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to decide how often the player should call the delegate with time progress events.
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
get { wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
get { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { _wrapper.volume = newValue }
|
||||
get { wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { return wrapper.isMuted }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
get { wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0
|
||||
public var rate: Float {
|
||||
get { return _rate }
|
||||
get { _rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
|
||||
// Only set the rate on the wrapper if it is already playing.
|
||||
if _wrapper.rate > 0 {
|
||||
_wrapper.rate = newValue
|
||||
if wrapper.rate > 0 {
|
||||
wrapper.rate = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,11 +140,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self._wrapper = AVPlayerWrapper()
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self._wrapper.delegate = self
|
||||
wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
@@ -172,10 +174,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions())
|
||||
|
||||
self._currentItem = item
|
||||
currentItem = item
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
self.loadNowPlayingMetaValues()
|
||||
loadNowPlayingMetaValues()
|
||||
}
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
@@ -184,30 +186,30 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
self.wrapper.togglePlaying()
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
self.wrapper.play()
|
||||
wrapper.play()
|
||||
}
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
self.wrapper.pause()
|
||||
wrapper.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
*/
|
||||
public func stop() {
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
self.event.playbackEnd.emit(data: .playerStopped)
|
||||
reset()
|
||||
wrapper.stop()
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,15 +217,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
self.updateNowPlayingCurrentTime(seconds)
|
||||
updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
self.wrapper.seek(to: seconds)
|
||||
wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
// MARK: - Remote Command Center
|
||||
|
||||
func enableRemoteCommands(_ commands: [RemoteCommand]) {
|
||||
self.remoteCommandController.enable(commands: commands)
|
||||
remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
@@ -234,6 +236,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Syncs the current remoteCommands with the iOS command center.
|
||||
Can be used to update item states - e.g. like, dislike and bookmark.
|
||||
*/
|
||||
@available(*, deprecated, message: "Directly set .remoteCommands instead")
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
@@ -267,8 +278,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
- Playback rate
|
||||
*/
|
||||
public func updateNowPlayingPlaybackValues() {
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingRate(rate)
|
||||
}
|
||||
|
||||
@@ -287,10 +298,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
return image
|
||||
})
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
} else {
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +309,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
// MARK: - Private
|
||||
|
||||
func reset() {
|
||||
self._currentItem = nil
|
||||
currentItem = nil
|
||||
}
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
@@ -321,7 +332,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
case .playing:
|
||||
// When a track starts playing, reset the rate to the stored rate
|
||||
self.rate = _rate;
|
||||
rate = _rate;
|
||||
fallthrough
|
||||
case .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
@@ -329,38 +340,37 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
default: break
|
||||
}
|
||||
self.event.stateChange.emit(data: state)
|
||||
event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
self.event.secondElapse.emit(data: seconds)
|
||||
event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
self.event.fail.emit(data: error)
|
||||
event.fail.emit(data: error)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
}
|
||||
self.event.seek.emit(data: (seconds, didFinish))
|
||||
event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
self.event.updateDuration.emit(data: duration)
|
||||
event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.event.receiveMetadata.emit(data: metadata)
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ protocol AudioSession {
|
||||
|
||||
var availableCategories: [AVAudioSession.Category] { get }
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
|
||||
|
||||
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
public protocol AudioSessionControllerDelegate: class {
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType)
|
||||
public enum InterruptionType: Equatable {
|
||||
case began
|
||||
case ended(shouldResume: Bool)
|
||||
}
|
||||
|
||||
public protocol AudioSessionControllerDelegate: AnyObject {
|
||||
func handleInterruption(type: InterruptionType)
|
||||
}
|
||||
|
||||
/**
|
||||
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
|
||||
@@ -30,7 +33,7 @@ public class AudioSessionController {
|
||||
True if another app is currently playing audio.
|
||||
*/
|
||||
public var isOtherAudioPlaying: Bool {
|
||||
return audioSession.isOtherAudioPlaying
|
||||
audioSession.isOtherAudioPlaying
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +49,7 @@ public class AudioSessionController {
|
||||
Set this to false to disable the behaviour.
|
||||
*/
|
||||
public var isObservingForInterruptions: Bool {
|
||||
get {
|
||||
return _isObservingForInterruptions
|
||||
}
|
||||
get { _isObservingForInterruptions }
|
||||
set {
|
||||
if newValue == _isObservingForInterruptions {
|
||||
return
|
||||
@@ -112,7 +113,19 @@ public class AudioSessionController {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.handleInterruption(type: type)
|
||||
switch type {
|
||||
case .began:
|
||||
delegate?.handleInterruption(type: .began)
|
||||
case .ended:
|
||||
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: false))
|
||||
return
|
||||
}
|
||||
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
|
||||
@unknown default: return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import MediaPlayer
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
public typealias StateChangeEventData = (AudioPlayerState)
|
||||
public typealias PlaybackEndEventData = (PlaybackEndedReason)
|
||||
public typealias SecondElapseEventData = (TimeInterval)
|
||||
public typealias FailEventData = (Error?)
|
||||
public typealias StateChangeEventData = AudioPlayerState
|
||||
public typealias PlaybackEndEventData = PlaybackEndedReason
|
||||
public typealias SecondElapseEventData = TimeInterval
|
||||
public typealias FailEventData = Error?
|
||||
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = (Double)
|
||||
public typealias MetadataEventData = ([AVMetadataItem])
|
||||
public typealias UpdateDurationEventData = Double
|
||||
public typealias MetadataEventData = [AVTimedMetadataGroup]
|
||||
public typealias DidRecreateAVPlayerEventData = ()
|
||||
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
|
||||
|
||||
@@ -90,7 +90,7 @@ extension AudioPlayer {
|
||||
|
||||
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
|
||||
self.listener = listener
|
||||
self.invoke = { [weak listener] (data: EventData) in
|
||||
invoke = { [weak listener] (data: EventData) in
|
||||
guard let _ = listener else {
|
||||
return false
|
||||
}
|
||||
@@ -133,9 +133,7 @@ extension AudioPlayer {
|
||||
func emit(data: EventData) {
|
||||
eventQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
return invoker.invoke(data)
|
||||
})
|
||||
self.invokers = self.invokers.filter { $0.invoke(data) }
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,31 +11,23 @@ import MediaPlayer
|
||||
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
private let concurrentInfoQueue: DispatchQueueType
|
||||
|
||||
private var _infoCenter: NowPlayingInfoCenter
|
||||
private var _info: [String: Any] = [:]
|
||||
|
||||
var infoCenter: NowPlayingInfoCenter {
|
||||
return _infoCenter
|
||||
}
|
||||
|
||||
var info: [String: Any] {
|
||||
return _info
|
||||
}
|
||||
private(set) var infoCenter: NowPlayingInfoCenter
|
||||
private(set) var info: [String: Any] = [:]
|
||||
|
||||
public required init() {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = MPNowPlayingInfoCenter.default()
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = dispatchQueue
|
||||
self._infoCenter = infoCenter
|
||||
concurrentInfoQueue = dispatchQueue
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = infoCenter
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public func set(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
@@ -43,10 +35,10 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
guard let self = self else { return }
|
||||
|
||||
keyValues.forEach { (keyValue) in
|
||||
self._info[keyValue.getKey()] = keyValue.getValue()
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +46,8 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self._info[keyValue.getKey()] = keyValue.getValue()
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +55,8 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self._info = [:]
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
self.info = [:]
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
|
||||
The URL pointing to the now playing item's underlying asset.
|
||||
This constant is used by the system UI when video thumbnails or audio waveform visualizations are applicable.
|
||||
*/
|
||||
@available(iOS 10.3, *)
|
||||
case assetUrl(URL?)
|
||||
|
||||
/**
|
||||
@@ -116,7 +115,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
|
||||
The service provider associated with the now-playing item.
|
||||
Value is a unique NSString that identifies the service provider for the now-playing item. If the now-playing item belongs to a channel or subscription service, this key can be used to coordinate various types of now-playing content from the service provider.
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
case serviceIdentifier(String?)
|
||||
|
||||
|
||||
@@ -130,11 +128,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
|
||||
return MPNowPlayingInfoPropertyAvailableLanguageOptions
|
||||
|
||||
case .assetUrl(_):
|
||||
if #available(iOS 10.3, *) {
|
||||
return MPNowPlayingInfoPropertyAssetURL
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
return MPNowPlayingInfoPropertyAssetURL
|
||||
case .chapterCount(_):
|
||||
return MPNowPlayingInfoPropertyChapterCount
|
||||
|
||||
@@ -175,11 +169,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
|
||||
return MPNowPlayingInfoPropertyPlaybackRate
|
||||
|
||||
case .serviceIdentifier(_):
|
||||
if #available(iOS 11.0, *) {
|
||||
return MPNowPlayingInfoPropertyServiceIdentifier
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
return MPNowPlayingInfoPropertyServiceIdentifier
|
||||
|
||||
}
|
||||
}
|
||||
@@ -194,10 +184,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
|
||||
return options
|
||||
|
||||
case .assetUrl(let url):
|
||||
if #available(iOS 10.3, *) {
|
||||
return url
|
||||
}
|
||||
return false
|
||||
return url
|
||||
|
||||
case .chapterCount(let count):
|
||||
return count != nil ? NSNumber(value: count!) : nil
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: class {
|
||||
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
|
||||
func itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
@@ -51,9 +50,9 @@ class AVPlayerItemNotificationObserver {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
self.observingItem = nil
|
||||
self.isObserving = false
|
||||
isObserving = false
|
||||
}
|
||||
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerItemObserverDelegate: class {
|
||||
protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the observed item updates the duration.
|
||||
@@ -18,7 +18,7 @@ protocol AVPlayerItemObserverDelegate: class {
|
||||
/**
|
||||
Called when the observed item receives metadata
|
||||
*/
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem])
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
private let metadataOutput: AVPlayerItemMetadataOutput
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
|
||||
}
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
@@ -41,6 +41,13 @@ class AVPlayerItemObserver: NSObject {
|
||||
private(set) weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
override init() {
|
||||
metadataOutput = AVPlayerItemMetadataOutput()
|
||||
super.init()
|
||||
|
||||
metadataOutput.setDelegate(self, queue: main)
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
@@ -51,12 +58,12 @@ class AVPlayerItemObserver: NSObject {
|
||||
- parameter item: The player item to observe.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
self.stopObservingCurrentItem()
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
stopObservingCurrentItem()
|
||||
isObserving = true
|
||||
observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.add(metadataOutput)
|
||||
}
|
||||
|
||||
func stopObservingCurrentItem() {
|
||||
@@ -65,8 +72,8 @@ class AVPlayerItemObserver: NSObject {
|
||||
}
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
|
||||
self.isObserving = false
|
||||
observingItem.remove(metadataOutput)
|
||||
isObserving = false
|
||||
self.observingItem = nil
|
||||
}
|
||||
|
||||
@@ -79,21 +86,22 @@ class AVPlayerItemObserver: NSObject {
|
||||
switch observedKeyPath {
|
||||
case AVPlayerItemKeyPath.duration:
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.timedMetadata:
|
||||
if let metadata = change?[.newKey] as? [AVMetadataItem] {
|
||||
self.delegate?.item(didReceiveMetadata: metadata)
|
||||
}
|
||||
default: break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
delegate?.item(didReceiveMetadata: groups)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerObserverDelegate: class {
|
||||
protocol AVPlayerObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the AVPlayer.status changes.
|
||||
@@ -20,37 +20,36 @@ protocol AVPlayerObserverDelegate: class {
|
||||
Called when the AVPlayer.timeControlStatus changes.
|
||||
*/
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Observing an AVPlayers status changes.
|
||||
*/
|
||||
class AVPlayerObserver: NSObject {
|
||||
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
|
||||
|
||||
private struct AVPlayerKeyPath {
|
||||
static let status = #keyPath(AVPlayer.status)
|
||||
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
|
||||
}
|
||||
|
||||
|
||||
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
|
||||
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
|
||||
private(set) var isObserving: Bool = false
|
||||
|
||||
|
||||
weak var delegate: AVPlayerObserverDelegate?
|
||||
weak var player: AVPlayer? {
|
||||
willSet {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start receiving events from this observer.
|
||||
*/
|
||||
@@ -58,52 +57,49 @@ class AVPlayerObserver: NSObject {
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
self.stopObserving()
|
||||
self.isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
stopObserving()
|
||||
isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
}
|
||||
|
||||
|
||||
func stopObserving() {
|
||||
guard let player = player, isObserving else {
|
||||
return
|
||||
}
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
self.isObserving = false
|
||||
isObserving = false
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch observedKeyPath {
|
||||
|
||||
case AVPlayerKeyPath.status:
|
||||
self.handleStatusChange(change)
|
||||
|
||||
handleStatusChange(change)
|
||||
|
||||
case AVPlayerKeyPath.timeControlStatus:
|
||||
self.handleTimeControlStatusChange(change)
|
||||
|
||||
handleTimeControlStatusChange(change)
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.Status
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
status = .unknown
|
||||
}
|
||||
delegate?.player(statusDidChange: status)
|
||||
}
|
||||
|
||||
|
||||
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.TimeControlStatus
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
@@ -111,5 +107,4 @@ class AVPlayerObserver: NSObject {
|
||||
delegate?.player(didChangeTimeControlStatus: status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerTimeObserverDelegate: class {
|
||||
protocol AVPlayerTimeObserverDelegate: AnyObject {
|
||||
func audioDidStart()
|
||||
func timeEvent(time: CMTime)
|
||||
}
|
||||
|
||||
@@ -16,55 +16,47 @@ class QueueManager<T> {
|
||||
|
||||
weak var delegate: QueueManagerDelegate? = nil
|
||||
|
||||
private var _items: [T] = [] {
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
private(set) var items: [T] = [] {
|
||||
didSet {
|
||||
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
|
||||
if oldValue.count == 0 && items.count > 0 && currentIndex == 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
public var items: [T] {
|
||||
return _items
|
||||
}
|
||||
|
||||
public var nextItems: [T] {
|
||||
guard _currentIndex + 1 < _items.count else {
|
||||
guard currentIndex + 1 < items.count else {
|
||||
return []
|
||||
}
|
||||
return Array(_items[_currentIndex + 1..<_items.count])
|
||||
return Array(items[currentIndex + 1..<items.count])
|
||||
}
|
||||
|
||||
public var previousItems: [T] {
|
||||
if (_currentIndex == 0) {
|
||||
if (currentIndex == 0) {
|
||||
return []
|
||||
}
|
||||
return Array(_items[0..<_currentIndex])
|
||||
return Array(items[0..<currentIndex])
|
||||
}
|
||||
|
||||
private var _currentIndex: Int = 0 {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return _currentIndex
|
||||
private(set) var currentIndex: Int = 0 {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The current item for the queue.
|
||||
*/
|
||||
public var current: T? {
|
||||
if _items.count > _currentIndex {
|
||||
return _items[_currentIndex]
|
||||
if items.count > currentIndex {
|
||||
return items[currentIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -75,7 +67,7 @@ class QueueManager<T> {
|
||||
- parameter item: The `AudioItem` to be added.
|
||||
*/
|
||||
public func addItem(_ item: T) {
|
||||
_items.append(item)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +76,7 @@ class QueueManager<T> {
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
*/
|
||||
public func addItems(_ items: [T]) {
|
||||
_items.append(contentsOf: items)
|
||||
self.items.append(contentsOf: items)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,12 +86,13 @@ class QueueManager<T> {
|
||||
- parameter at: The index to insert the items at.
|
||||
*/
|
||||
public func addItems(_ items: [T], at index: Int) throws {
|
||||
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))")
|
||||
guard index >= 0 && self.items.count >= index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index to insert at has to be non-negative and equal to or smaller than the number of items: (\(items.count))")
|
||||
}
|
||||
|
||||
_items.insert(contentsOf: items, at: index)
|
||||
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
|
||||
|
||||
self.items.insert(contentsOf: items, at: index)
|
||||
|
||||
if (currentIndex >= index && self.items.count != 1) { currentIndex += items.count }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,12 +104,12 @@ class QueueManager<T> {
|
||||
*/
|
||||
@discardableResult
|
||||
public func next() throws -> T {
|
||||
let nextIndex = _currentIndex + 1
|
||||
guard _items.count > nextIndex else {
|
||||
let nextIndex = currentIndex + 1
|
||||
guard items.count > nextIndex else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
_currentIndex = nextIndex
|
||||
return _items[nextIndex]
|
||||
currentIndex = nextIndex
|
||||
return items[nextIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,12 +121,12 @@ class QueueManager<T> {
|
||||
*/
|
||||
@discardableResult
|
||||
public func previous() throws -> T {
|
||||
let previousIndex = _currentIndex - 1
|
||||
let previousIndex = currentIndex - 1
|
||||
guard previousIndex >= 0 else {
|
||||
throw APError.QueueError.noPreviousItem
|
||||
}
|
||||
_currentIndex = previousIndex
|
||||
return _items[previousIndex]
|
||||
currentIndex = previousIndex
|
||||
return items[previousIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,12 +143,12 @@ class QueueManager<T> {
|
||||
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))")
|
||||
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))")
|
||||
}
|
||||
|
||||
_currentIndex = index
|
||||
return _items[index]
|
||||
currentIndex = index
|
||||
return items[index]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,17 +159,16 @@ class QueueManager<T> {
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
|
||||
guard fromIndex != _currentIndex else {
|
||||
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 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)).")
|
||||
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)
|
||||
@@ -192,19 +184,19 @@ class QueueManager<T> {
|
||||
*/
|
||||
@discardableResult
|
||||
public func removeItem(at index: Int) throws -> T {
|
||||
guard index != _currentIndex else {
|
||||
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)).")
|
||||
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
|
||||
if index < currentIndex {
|
||||
currentIndex -= 1
|
||||
}
|
||||
|
||||
return _items.remove(at: index)
|
||||
return items.remove(at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,10 +206,10 @@ class QueueManager<T> {
|
||||
*/
|
||||
public func replaceCurrentItem(with item: T) {
|
||||
if current == nil {
|
||||
self.addItem(item)
|
||||
addItem(item)
|
||||
}
|
||||
|
||||
self._items[_currentIndex] = item
|
||||
|
||||
items[currentIndex] = item
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,8 +218,8 @@ class QueueManager<T> {
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
guard currentIndex > 0 else { return }
|
||||
_items.removeSubrange(0..<_currentIndex)
|
||||
_currentIndex = 0
|
||||
items.removeSubrange(0..<currentIndex)
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,17 +227,17 @@ class QueueManager<T> {
|
||||
If no upcoming items exist, no action will be taken.
|
||||
*/
|
||||
public func removeUpcomingItems() {
|
||||
let nextIndex = _currentIndex + 1
|
||||
guard nextIndex < _items.count else { return }
|
||||
_items.removeSubrange(nextIndex..<_items.count)
|
||||
let nextIndex = currentIndex + 1
|
||||
guard nextIndex < items.count else { return }
|
||||
items.removeSubrange(nextIndex..<items.count)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes all items for queue
|
||||
*/
|
||||
public func clearQueue() {
|
||||
_currentIndex = 0
|
||||
_items.removeAll()
|
||||
currentIndex = 0
|
||||
items.removeAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
|
||||
let queueManager: QueueManager = QueueManager<AudioItem>()
|
||||
|
||||
public init() {
|
||||
super.init()
|
||||
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
|
||||
queueManager.delegate = self
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
public var repeatMode: RepeatMode = .off
|
||||
|
||||
public override var currentItem: AudioItem? {
|
||||
return queueManager.current
|
||||
queueManager.current
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return queueManager.currentIndex
|
||||
queueManager.currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
*/
|
||||
public override func stop() {
|
||||
super.stop()
|
||||
self.event.queueIndex.emit(data: (currentIndex, nil))
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
@@ -51,21 +51,21 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
All items currently in the queue.
|
||||
*/
|
||||
public var items: [AudioItem] {
|
||||
return queueManager.items
|
||||
queueManager.items
|
||||
}
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
return queueManager.previousItems
|
||||
queueManager.previousItems
|
||||
}
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
return queueManager.nextItems
|
||||
queueManager.nextItems
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItem(item)
|
||||
try self.load(item: item, playWhenReady: playWhenReady)
|
||||
try load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItem(item)
|
||||
@@ -106,7 +106,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItems(items)
|
||||
try self.load(item: currentItem!, playWhenReady: playWhenReady)
|
||||
try load(item: currentItem!, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItems(items)
|
||||
@@ -123,18 +123,33 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func next() throws {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
let nextItem = try queueManager.next()
|
||||
try self.load(item: nextItem, playWhenReady: true)
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try load(item: nextItem, playWhenReady: shouldPlayWhenReady)
|
||||
} catch APError.QueueError.noNextItem {
|
||||
if repeatMode == .queue {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try jumpToItem(atIndex: 0, playWhenReady: shouldPlayWhenReady)
|
||||
} else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Step to the previous item in the queue.
|
||||
*/
|
||||
public func previous() throws {
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
let previousItem = try queueManager.previous()
|
||||
try self.load(item: previousItem, playWhenReady: true)
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
try load(item: previousItem, playWhenReady: shouldPlayWhenReady)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,9 +170,15 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
let item = try queueManager.jump(to: index)
|
||||
try self.load(item: item, playWhenReady: playWhenReady)
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
playWhenReady ? play() : pause()
|
||||
onCurrentIndexChanged(oldIndex: index, newIndex: index)
|
||||
} else {
|
||||
let item = try queueManager.jump(to: index)
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
try load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +188,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
public func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
|
||||
}
|
||||
|
||||
@@ -191,18 +212,22 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
super.AVWrapperItemDidPlayToEndTime()
|
||||
|
||||
switch repeatMode {
|
||||
case .off: try? self.next()
|
||||
case .off:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
case .track:
|
||||
seek(to: 0)
|
||||
play()
|
||||
try? jumpToItem(atIndex: currentIndex, playWhenReady: true)
|
||||
case .queue:
|
||||
do {
|
||||
try self.next()
|
||||
} catch APError.QueueError.noNextItem {
|
||||
do {
|
||||
try jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
} catch { /* TODO: handle possible errors from load */ }
|
||||
} catch { /* TODO: handle possible errors from load */ }
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
try? jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +235,11 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
|
||||
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
|
||||
// if _currentItem is nil, then this was triggered by a reset. ignore.
|
||||
if _currentItem == nil { return }
|
||||
self.event.queueIndex.emit(data: (oldIndex, newIndex))
|
||||
if currentItem == nil { return }
|
||||
event.queueIndex.emit(data: (oldIndex, newIndex))
|
||||
}
|
||||
|
||||
func onReceivedFirstItem() {
|
||||
self.event.queueIndex.emit(data: (nil, 0))
|
||||
event.queueIndex.emit(data: (nil, 0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public struct FeedbackCommand: RemoteCommandProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCommand {
|
||||
public enum RemoteCommand: CustomStringConvertible {
|
||||
|
||||
case play
|
||||
|
||||
@@ -128,6 +128,23 @@ public enum RemoteCommand {
|
||||
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
|
||||
|
||||
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .play: return "play"
|
||||
case .pause: return "pause"
|
||||
case .stop: return "stop"
|
||||
case .togglePlayPause: return "togglePlayPause"
|
||||
case .next: return "nextTrack"
|
||||
case .previous: return "previousTrack"
|
||||
case .changePlaybackPosition: return "changePlaybackPosition"
|
||||
case .skipForward(_): return "skipForward"
|
||||
case .skipBackward(_): return "skipBackward"
|
||||
case .like(_, _, _): return "like"
|
||||
case .dislike(_, _, _): return "dislike"
|
||||
case .bookmark(_, _, _): return "bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All values in an array for convenience.
|
||||
|
||||
@@ -19,31 +19,34 @@ public class RemoteCommandController {
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
var commandTargetPointers: [String: Any] = [:]
|
||||
|
||||
private var enabledCommands: [RemoteCommand] = []
|
||||
|
||||
/**
|
||||
Create a new RemoteCommandController.
|
||||
|
||||
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
|
||||
*/
|
||||
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
|
||||
self.center = remoteCommandCenter
|
||||
center = remoteCommandCenter
|
||||
}
|
||||
|
||||
internal func enable(commands: [RemoteCommand]) {
|
||||
self.disable(commands: RemoteCommand.all())
|
||||
commands.forEach { (command) in
|
||||
self.enable(command: command)
|
||||
let commandsToDisable = enabledCommands.filter { command in
|
||||
!commands.contains(where: { $0.description == command.description })
|
||||
}
|
||||
|
||||
enabledCommands = commands
|
||||
commands.forEach { self.enable(command: $0) }
|
||||
disable(commands: commandsToDisable)
|
||||
}
|
||||
|
||||
internal func disable(commands: [RemoteCommand]) {
|
||||
commands.forEach { (command) in
|
||||
self.disable(command: command)
|
||||
}
|
||||
commands.forEach { self.disable(command: $0) }
|
||||
}
|
||||
|
||||
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
center[keyPath: command.commandKeyPath].isEnabled = true
|
||||
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
|
||||
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
|
||||
}
|
||||
|
||||
@@ -92,21 +95,21 @@ public class RemoteCommandController {
|
||||
|
||||
// MARK: - Handlers
|
||||
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
|
||||
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
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = handleSkipBackwardDefault
|
||||
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = handleChangePlaybackPositionCommandDefault
|
||||
public lazy var handleNextTrackCommand: RemoteCommandHandler = handleNextTrackCommandDefault
|
||||
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = handlePreviousTrackCommandDefault
|
||||
public lazy var handleLikeCommand: RemoteCommandHandler = handleLikeCommandDefault
|
||||
public lazy var handleDislikeCommand: RemoteCommandHandler = handleDislikeCommandDefault
|
||||
public lazy var handleBookmarkCommand: RemoteCommandHandler = handleBookmarkCommandDefault
|
||||
|
||||
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -114,7 +117,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -122,7 +125,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.stop()
|
||||
return .success
|
||||
}
|
||||
@@ -130,7 +133,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -140,7 +143,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -150,7 +153,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -159,7 +162,7 @@ public class RemoteCommandController {
|
||||
|
||||
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: event.positionTime)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -167,41 +170,41 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
@@ -213,7 +216,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
else if let error = error as? APError.QueueError {
|
||||
switch error {
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
|
||||
return MPRemoteCommandHandlerStatus.noSuchContent
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user