Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77fb2b88d3 | |||
| 5ff8c9dffc | |||
| 077d4b1d53 | |||
| 05322d9887 | |||
| 645b7bc8e7 | |||
| e64e658b3b | |||
| bf8e54e6a6 | |||
| ed9fe280db | |||
| 1148a6c28b | |||
| 9b6dcff4e2 | |||
| bfe5851dc4 | |||
| 7ffa9b0113 | |||
| ebec7afccd | |||
| 0fa786a91c | |||
| 8fb5c66820 | |||
| 42693b6dfb | |||
| 348dcc17f7 | |||
| b10aea494f | |||
| cbbbd57397 | |||
| a270b3b232 | |||
| 3cac61fe8f | |||
| 7870d3bba6 | |||
| 4c891bcdc6 | |||
| 9e114360ec | |||
| f2c9a272d9 | |||
| e41bb22a48 |
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -47,6 +47,8 @@
|
||||
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 */; };
|
||||
F048FE7728D215A9001AA2AB /* five_seconds.m4a in Resources */ = {isa = PBXBuildFile; fileRef = F048FE7628D215A9001AA2AB /* five_seconds.m4a */; };
|
||||
F048FE7828D215A9001AA2AB /* five_seconds.m4a in Resources */ = {isa = PBXBuildFile; fileRef = F048FE7628D215A9001AA2AB /* five_seconds.m4a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -96,6 +98,7 @@
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; 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>"; };
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = five_seconds.m4a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -123,6 +126,7 @@
|
||||
0708ED712116E91300EB29BD /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */,
|
||||
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */,
|
||||
0708ED6F2116E89900EB29BD /* Source.swift */,
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
|
||||
@@ -285,7 +289,6 @@
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
@@ -295,7 +298,6 @@
|
||||
};
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1020;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
@@ -333,6 +335,7 @@
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
|
||||
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
|
||||
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */,
|
||||
F048FE7728D215A9001AA2AB /* five_seconds.m4a in Resources */,
|
||||
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */,
|
||||
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
@@ -347,6 +350,7 @@
|
||||
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */,
|
||||
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */,
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */,
|
||||
F048FE7828D215A9001AA2AB /* five_seconds.m4a in Resources */,
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -530,15 +534,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@@ -549,15 +553,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@@ -567,7 +571,7 @@
|
||||
607FACF31AFB9204008FA782 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
@@ -577,13 +581,13 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.--PRODUCT-NAME-rfc1034identifier-";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
@@ -593,19 +597,19 @@
|
||||
607FACF41AFB9204008FA782 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.--PRODUCT-NAME-rfc1034identifier-";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
|
||||
@@ -17,10 +17,13 @@ class AudioController {
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Longing.mp3", artist: "David Chavez", title: "Longing", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Soul%20Searching.mp3", artist: "David Chavez", title: "Soul Searching (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Rhythm%20City%20(Demo).mp3", artist: "David Chavez", title: "Rhythm City (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/hls/whip/playlist.m3u8", title: "Whip", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
]
|
||||
|
||||
init() {
|
||||
@@ -36,7 +39,10 @@ class AudioController {
|
||||
.changePlaybackPosition
|
||||
]
|
||||
try? audioSessionController.set(category: .playback)
|
||||
try? player.add(items: sources, playWhenReady: false)
|
||||
player.repeatMode = .queue
|
||||
DispatchQueue.main.async {
|
||||
self.player.add(items: self.sources)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,41 +26,37 @@ class ViewController: UIViewController {
|
||||
|
||||
private var isScrubbing: Bool = false
|
||||
private let controller = AudioController.shared
|
||||
private var lastLoadFailed: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
|
||||
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)
|
||||
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
|
||||
controller.player.event.fail.addListener(self, handlePlayerFailure)
|
||||
updateMetaData()
|
||||
handleAudioPlayerStateChange(data: controller.player.playerState)
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func togglePlay(_ sender: Any) {
|
||||
if !controller.audioSessionController.audioSessionIsActive {
|
||||
try? controller.audioSessionController.activateSession()
|
||||
}
|
||||
if lastLoadFailed, let item = controller.player.currentItem {
|
||||
lastLoadFailed = false
|
||||
errorLabel.isHidden = true
|
||||
try? controller.player.load(item: item, playWhenReady: true)
|
||||
}
|
||||
else {
|
||||
controller.player.togglePlaying()
|
||||
}
|
||||
controller.player.playWhenReady = playButton.currentTitle == "Play"
|
||||
}
|
||||
|
||||
@IBAction func previous(_ sender: Any) {
|
||||
try? controller.player.previous()
|
||||
controller.player.previous()
|
||||
}
|
||||
|
||||
@IBAction func next(_ sender: Any) {
|
||||
try? controller.player.next()
|
||||
controller.player.next()
|
||||
}
|
||||
|
||||
@IBAction func startScrubbing(_ sender: UISlider) {
|
||||
@@ -77,31 +73,56 @@ class ViewController: UIViewController {
|
||||
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
|
||||
}
|
||||
|
||||
func updateTimeValues() {
|
||||
// MARK: - Render
|
||||
|
||||
func renderTimeValues() {
|
||||
self.slider.maximumValue = Float(self.controller.player.duration)
|
||||
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
|
||||
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
|
||||
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
|
||||
}
|
||||
|
||||
func updateMetaData() {
|
||||
if let item = controller.player.currentItem {
|
||||
titleLabel.text = item.getTitle()
|
||||
artistLabel.text = item.getArtist()
|
||||
|
||||
func render() {
|
||||
let player = self.controller.player
|
||||
|
||||
// Render play button
|
||||
self.playButton.setTitle(
|
||||
!player.playWhenReady || player.playerState == .failed
|
||||
? "Play"
|
||||
: "Pause",
|
||||
for: .normal
|
||||
)
|
||||
|
||||
// Render metadata
|
||||
if let item = player.currentItem {
|
||||
self.titleLabel.text = item.getTitle()
|
||||
self.artistLabel.text = item.getArtist()
|
||||
item.getArtwork({ (image) in
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
|
||||
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
|
||||
}
|
||||
|
||||
func setErrorMessage(_ message: String) {
|
||||
self.loadIndicator.stopAnimating()
|
||||
errorLabel.isHidden = false
|
||||
errorLabel.text = message
|
||||
|
||||
// Render time values
|
||||
self.renderTimeValues()
|
||||
|
||||
// Render error label
|
||||
if (player.playerState == .failed) {
|
||||
self.errorLabel.isHidden = false
|
||||
self.errorLabel.text = "Playback failed."
|
||||
} else {
|
||||
self.errorLabel.text = ""
|
||||
self.errorLabel.isHidden = true
|
||||
}
|
||||
|
||||
// Render load indicator:
|
||||
if (
|
||||
(player.playerState == .loading || player.playerState == .buffering)
|
||||
&& self.controller.player.playWhenReady // Avoid showing indicator before user has pressed play
|
||||
) {
|
||||
self.loadIndicator.startAnimating()
|
||||
} else {
|
||||
self.loadIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
@@ -109,25 +130,17 @@ class ViewController: UIViewController {
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print("state=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.setPlayButtonState(forAudioPlayerState: data)
|
||||
switch data {
|
||||
case .loading:
|
||||
self.loadIndicator.startAnimating()
|
||||
self.updateMetaData()
|
||||
self.updateTimeValues()
|
||||
case .buffering:
|
||||
self.loadIndicator.startAnimating()
|
||||
case .ready:
|
||||
self.loadIndicator.stopAnimating()
|
||||
self.updateMetaData()
|
||||
self.updateTimeValues()
|
||||
case .playing, .paused, .idle:
|
||||
self.loadIndicator.stopAnimating()
|
||||
self.updateTimeValues()
|
||||
}
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
|
||||
print("playWhenReady=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
|
||||
print("playEndReason=\(data)")
|
||||
}
|
||||
@@ -135,7 +148,7 @@ class ViewController: UIViewController {
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,23 +159,11 @@ class ViewController: UIViewController {
|
||||
|
||||
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
|
||||
func handlePlayerFailure(data: AudioPlayer.FailEventData) {
|
||||
if let error = data as NSError? {
|
||||
if error.code == -1009 {
|
||||
lastLoadFailed = true
|
||||
DispatchQueue.main.async {
|
||||
self.setErrorMessage("Network disconnected. Please try again...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,12 +47,30 @@ class AVPlayerItemObserverTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
|
||||
var receivedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedMetadata?(metadata)
|
||||
}
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
|
||||
}
|
||||
|
||||
var receivedCommonMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
|
||||
|
||||
func item(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
receivedCommonMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var receivedTimedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedTimedMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var receivedChapterMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedChapterMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var updateDuration: ((_ duration: Double) -> Void)?
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.stop()
|
||||
case .idle: expectation.fulfill()
|
||||
case .stopped: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,19 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seek_by__should_seek() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
self.wrapper.seek(by: seekTime)
|
||||
}
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
let expectation = XCTestExpectation()
|
||||
@@ -168,8 +181,8 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
|
||||
// MARK: - Rate tests
|
||||
|
||||
func test_AVPlayerWrapper__rate__should_be_0() {
|
||||
XCTAssert(wrapper.rate == 0.0)
|
||||
func test_AVPlayerWrapper__rate__should_be_1() {
|
||||
XCTAssert(wrapper.rate == 1)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__rate__playing_a_source__should_be_1() {
|
||||
@@ -193,7 +206,32 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "AVPlayerWrapperDelegateHolder.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
|
||||
}
|
||||
|
||||
@@ -205,17 +243,31 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
|
||||
}
|
||||
|
||||
private var _state: AVPlayerWrapperState? = nil
|
||||
var state: AVPlayerWrapperState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
self.stateUpdate?(state)
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _state
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
lockQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if let newValue = newValue {
|
||||
let changed = self._state != newValue;
|
||||
if (changed) {
|
||||
self._state = newValue
|
||||
self.stateUpdate?(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var stateUpdate: ((_ state: AVPlayerWrapperState) -> Void)?
|
||||
var didUpdateDuration: ((_ duration: Double) -> Void)?
|
||||
var didSeekTo: ((_ seconds: Int) -> Void)?
|
||||
var didSeekTo: ((_ seconds: Double) -> Void)?
|
||||
var itemDidComplete: (() -> Void)?
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
@@ -230,7 +282,7 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
|
||||
didSeekTo?(seconds)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,191 +1,547 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
import Foundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
class AudioPlayerTests: QuickSpec {
|
||||
override func spec() {
|
||||
beforeSuite {
|
||||
Nimble.AsyncDefaults.timeout = .seconds(10)
|
||||
Nimble.AsyncDefaults.pollInterval = .milliseconds(100)
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
describe("AudioPlayer") {
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
var playerStateEventListener: QueuedAudioPlayer.PlayerStateEventListener!
|
||||
beforeEach {
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
playerStateEventListener = QueuedAudioPlayer.PlayerStateEventListener()
|
||||
audioPlayer.event.stateChange.addListener(
|
||||
playerStateEventListener,
|
||||
playerStateEventListener.handleEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__play_source__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: self.audioPlayer.play()
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
|
||||
afterEach {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__pausing_source__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
switch state {
|
||||
case .playing: audioPlayer?.pause()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__stopping_source__should_be_idle() {
|
||||
let expectation = XCTestExpectation()
|
||||
var hasBeenPlaying: Bool = false
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
switch state {
|
||||
case .playing:
|
||||
hasBeenPlaying = true
|
||||
audioPlayer?.stop()
|
||||
case .idle:
|
||||
if hasBeenPlaying {
|
||||
expectation.fulfill()
|
||||
|
||||
// MARK: - Load
|
||||
context("when loading audio item") {
|
||||
it("should never mutate playWhenReady to false") {
|
||||
audioPlayer.playWhenReady = true
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Current time
|
||||
|
||||
func test_AudioPlayer__currentTime__should_be_0() {
|
||||
XCTAssert(audioPlayer.currentTime == 0.0)
|
||||
}
|
||||
|
||||
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
|
||||
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
|
||||
// let expectation = XCTestExpectation()
|
||||
// audioPlayer.timeEventFrequency = .everyQuarterSecond
|
||||
// listener.secondsElapse = { _ in
|
||||
// if self.audioPlayer.currentTime > 0.0 {
|
||||
// expectation.fulfill()
|
||||
// }
|
||||
// }
|
||||
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
// wait(for: [expectation], timeout: 20.0)
|
||||
// }
|
||||
|
||||
// MARK: - Rate
|
||||
|
||||
func test_AudioPlayer__rate__should_be_1() {
|
||||
XCTAssert(audioPlayer.rate == 1.0)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__rate__playing_source__should_be_1() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
guard let audioPlayer = audioPlayer else { return }
|
||||
switch state {
|
||||
case .playing:
|
||||
if audioPlayer.rate == 1.0 {
|
||||
expectation.fulfill()
|
||||
|
||||
it("should never mutate playWhenReady to true") {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
// MARK: - Current item
|
||||
|
||||
func test_AudioPlayer__currentItem__should_be_nil() {
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
guard let audioPlayer = audioPlayer else { return }
|
||||
switch state {
|
||||
case .ready:
|
||||
if audioPlayer.currentItem != nil {
|
||||
expectation.fulfill()
|
||||
|
||||
it("should mutate playWhenReady when loading with playWhenReady equals true") {
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
|
||||
it("should mutate playWhenReady when loading with playWhenReady equals false") {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
}
|
||||
|
||||
it("should seek when audio item sets initial time") {
|
||||
var seekCompleted = false
|
||||
listener.onSeekCompletion = {
|
||||
seekCompleted = true
|
||||
}
|
||||
audioPlayer.playWhenReady = false
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
audioPlayer.load(item: FiveSecondSourceWithInitialTimeOfFourSeconds.getAudioItem())
|
||||
expect(seekCompleted).toEventually(beTrue())
|
||||
expect(audioPlayer?.currentTime ?? 0).to(beGreaterThanOrEqualTo(4))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Duration
|
||||
context("when dealing with duration") {
|
||||
it("should set duration eventually after loading") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
|
||||
it("audioPlayer.event.updateDuration should receive duration after loading") {
|
||||
var receivedUpdateDuration = false
|
||||
listener.onUpdateDuration = { duration in
|
||||
receivedUpdateDuration = true
|
||||
expect(duration).to(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(receivedUpdateDuration).toEventually(beTrue())
|
||||
}
|
||||
|
||||
it("should reset duration after loading again") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
|
||||
it("should reset duration after reset") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
audioPlayer.clear()
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Failure
|
||||
context("when handling failure") {
|
||||
it("should emit fail event on load with non-malformed URL") {
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
}
|
||||
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: "", // malformed url
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).toNot(beNil())
|
||||
expect(audioPlayer.playerState).to(equal(.failed))
|
||||
expect(didReceiveFail).to(beTrue())
|
||||
}
|
||||
|
||||
it("should emit fail event on load with non-existing resource") {
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
}
|
||||
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3"
|
||||
let item = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).toEventuallyNot(beNil())
|
||||
expect(audioPlayer.playerState).to(equal(.failed))
|
||||
expect(didReceiveFail).to(beTrue())
|
||||
}
|
||||
|
||||
context("calling play after failure") {
|
||||
it("should retry loading") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("setting playWhenReady after failure") {
|
||||
it("should retry loading") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([ .loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("calling reload after failure") {
|
||||
it("should retry loading but fail again with same broken source") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
|
||||
audioPlayer.reload(startFromCurrentTime: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("load resource") {
|
||||
it("should succeed after previous failure") {
|
||||
var didReceiveFail = false;
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true;
|
||||
}
|
||||
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let failItem = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream);
|
||||
audioPlayer.load(item: failItem, playWhenReady: false)
|
||||
expect(didReceiveFail).toEventually(beTrue())
|
||||
expect(audioPlayer.playerState).toEventually(equal(.failed))
|
||||
expect(playerStateEventListener.states).toEventually(equal([.loading, .failed]))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).to(beNil())
|
||||
expect(playerStateEventListener.statesWithoutBuffering)
|
||||
.toEventually(equal([.loading, .failed, .loading, .playing]))
|
||||
}
|
||||
|
||||
it("with playWhenReady=false it should succeed after previous failure") {
|
||||
var didReceiveFail = false;
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true;
|
||||
}
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(didReceiveFail).toEventually(beTrue())
|
||||
expect(audioPlayer.playerState).toEventually(equal(.failed))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - States
|
||||
context("states") {
|
||||
it("should initially be idle") {
|
||||
expect(audioPlayer.playerState).to(equal(.idle))
|
||||
}
|
||||
|
||||
it("should be loading after load source") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).to(equal(.loading))
|
||||
}
|
||||
|
||||
it("should become ready after load source") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
}
|
||||
|
||||
it("should be playing after load source with playWhenReady") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
it("should emit events in reliable order") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
it("should update playWhenReady after external pause") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing];
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(0.0))
|
||||
|
||||
// Simulate avplayer becoming paused due to external reason:
|
||||
audioPlayer.wrapper.rate = 0
|
||||
|
||||
expectedEvents.append(.paused);
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
|
||||
it("should emit events in reliable order at end call stop") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.stop()
|
||||
expectedEvents.append(.stopped)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
|
||||
it("should emit events in reliable order also after loading after reset") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expectedEvents.append(contentsOf: [.loading, .playing])
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
|
||||
it("should be playing after calling play()") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
audioPlayer.play()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
|
||||
it("should be paused after calling pause()") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.pause()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.paused))
|
||||
}
|
||||
|
||||
it("should be paused after setting playWhenReady to false") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.playWhenReady = false
|
||||
expect(audioPlayer.playerState).toEventually(equal(.paused))
|
||||
}
|
||||
|
||||
it("should be playing after setting playWhenReady to true") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
|
||||
it("should be stopped after stop") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.stop()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.stopped))
|
||||
}
|
||||
}
|
||||
// MARK: - States
|
||||
context("current time") {
|
||||
it("should be 0 initially") {
|
||||
expect(audioPlayer.currentTime).to(equal(0.0))
|
||||
}
|
||||
|
||||
it("audioPlayer.event.secondElapse should be emitted when playing") {
|
||||
var onSecondsElapseTime = 0.0
|
||||
audioPlayer.timeEventFrequency = .everyQuarterSecond
|
||||
listener.onSecondsElapse = { time in
|
||||
onSecondsElapseTime = time
|
||||
}
|
||||
audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
expect(onSecondsElapseTime).toEventually(beGreaterThan(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Buffer
|
||||
context("buffer") {
|
||||
it("automaticallyWaitsToMinimizeStalling should be true") {
|
||||
expect(audioPlayer.automaticallyWaitsToMinimizeStalling).to(beTrue())
|
||||
}
|
||||
it("bufferDuration should be zero") {
|
||||
expect(audioPlayer.bufferDuration).to(equal(0))
|
||||
}
|
||||
it("setting bufferDuration disables automaticallyWaitsToMinimizeStalling") {
|
||||
audioPlayer.bufferDuration = 1;
|
||||
expect(audioPlayer.bufferDuration).to(equal(1))
|
||||
expect(audioPlayer.automaticallyWaitsToMinimizeStalling).to(beFalse())
|
||||
}
|
||||
it("enabling automaticallyWaitsToMinimizeStalling sets bufferDuration to zero") {
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
||||
expect(audioPlayer.bufferDuration).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Seek
|
||||
context("Seek") {
|
||||
it("Seeking should work before loading is complete") {
|
||||
let player = audioPlayer
|
||||
player!.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
player!.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(4.75))
|
||||
}
|
||||
it("Seeking should work after loading is complete") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(4.75))
|
||||
}
|
||||
it("Seeking should work when paused") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(equal(4.75))
|
||||
}
|
||||
it("Seeking can not change currentTime when stopped") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.stop()
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toNotEventually(equal(4.75))
|
||||
expect(audioPlayer.currentTime).to(equal(0))
|
||||
}
|
||||
}
|
||||
// MARK: - Rate
|
||||
context("Rate") {
|
||||
it("should be 1 initially") {
|
||||
expect(audioPlayer.rate).to(equal(1))
|
||||
}
|
||||
it("should speed up playback when setting to more than 1") {
|
||||
var start: Date? = nil;
|
||||
var end: Date? = nil;
|
||||
|
||||
listener.onPlaybackEnd = { reason in
|
||||
if (reason == .playedUntilEnd) {
|
||||
end = Date()
|
||||
}
|
||||
}
|
||||
|
||||
listener.onStateChange = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
if (start == nil) {
|
||||
start = Date()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.rate = 10
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ended))
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start);
|
||||
expect(duration).to(beLessThan(1))
|
||||
}
|
||||
}
|
||||
|
||||
it("should slow down playback when setting to less than 1") {
|
||||
var start: Date? = nil;
|
||||
var end: Date? = nil;
|
||||
|
||||
listener.onPlaybackEnd = { reason in
|
||||
if (reason == .playedUntilEnd) {
|
||||
end = Date()
|
||||
}
|
||||
}
|
||||
|
||||
audioPlayer.rate = 0.5
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
listener.onStateChange = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
if (start == nil) {
|
||||
start = Date()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ended))
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start);
|
||||
expect(duration).to(beLessThanOrEqualTo(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - Current Item
|
||||
context("Current Item") {
|
||||
it("should be nil initially") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
it("should not be nil after loading") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.currentItem?.getSourceUrl()).to(equal(Source.getAudioItem().getSourceUrl()))
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerStateEventListener {
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "PlayerStateEventListener.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
var _states: [AudioPlayerState] = []
|
||||
var states: [AudioPlayerState] {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _states
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
lockQueue.sync {
|
||||
_states = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
private var _statesWithoutBuffering: [AudioPlayerState] = []
|
||||
var statesWithoutBuffering: [AudioPlayerState] {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _statesWithoutBuffering
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
lockQueue.sync {
|
||||
_statesWithoutBuffering = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
func handleEvent(state: AudioPlayerState) {
|
||||
states.append(state)
|
||||
if (state != .ready && state != .buffering && (statesWithoutBuffering.isEmpty || statesWithoutBuffering.last != state)) {
|
||||
statesWithoutBuffering.append(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AudioPlayerEventListener {
|
||||
|
||||
var state: AudioPlayerState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
stateUpdate?(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
var state: AudioPlayerState?
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
var onStateChange: ((_ state: AudioPlayerState) -> Void)?
|
||||
var onSecondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var onSeekCompletion: (() -> Void)?
|
||||
var onReceiveFail: ((_ error: Error?) -> Void)?
|
||||
var onPlaybackEnd: ((_: AudioPlayer.PlaybackEndEventData) -> Void)?
|
||||
var onUpdateDuration: ((_: AudioPlayer.UpdateDurationEventData) -> Void)?
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.updateDuration.addListener(self, handleUpdateDuration)
|
||||
audioPlayer.event.stateChange.addListener(self, handleStateChange)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
|
||||
audioPlayer.event.fail.addListener(self, handleFail)
|
||||
audioPlayer.event.playbackEnd.addListener(self, handlePlaybackEnd)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -194,16 +550,41 @@ class AudioPlayerEventListener {
|
||||
audioPlayer?.event.secondElapse.removeListener(self)
|
||||
}
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
func handleStateChange(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
seekCompletion?()
|
||||
onSeekCompletion?()
|
||||
}
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
self.secondsElapse?(data)
|
||||
self.onSecondsElapse?(data)
|
||||
}
|
||||
|
||||
func handleFail(error: Error?) {
|
||||
self.onReceiveFail?(error)
|
||||
}
|
||||
|
||||
func handlePlaybackEnd(_ data: AudioPlayer.PlaybackEndEventData) {
|
||||
self.onPlaybackEnd?(data)
|
||||
}
|
||||
|
||||
func handleUpdateDuration(_ data: AudioPlayer.UpdateDurationEventData) {
|
||||
self.onUpdateDuration?(data)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
static func random(length: Int = 20) -> String {
|
||||
let base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var randomString: String = ""
|
||||
|
||||
for _ in 0..<length {
|
||||
let randomValue = arc4random_uniform(UInt32(base.count))
|
||||
randomString += "\(base[base.index(base.startIndex, offsetBy: Int(randomValue))])"
|
||||
}
|
||||
return randomString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import MediaPlayer
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
|
||||
|
||||
var info: [String: Any] = [:]
|
||||
|
||||
required public init() {
|
||||
@@ -30,6 +29,12 @@ class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
|
||||
public func set(keyValue: NowPlayingInfoKeyValue) {
|
||||
info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
|
||||
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
keyValues.forEach { (keyValue) in
|
||||
info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
}
|
||||
|
||||
func getTitle() -> String? {
|
||||
return info[MediaItemProperty.title(nil).getKey()] as? String
|
||||
|
||||
@@ -61,7 +61,7 @@ class NowPlayingInfoControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class NowPlayingInfoTests: QuickSpec {
|
||||
|
||||
beforeEach {
|
||||
item = Source.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventually be updated with meta data") {
|
||||
@@ -53,7 +53,7 @@ class NowPlayingInfoTests: QuickSpec {
|
||||
|
||||
beforeEach {
|
||||
item = LongSource.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: true)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be updated with playback values") {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ struct Source {
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: Source.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
|
||||
return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ struct ShortSource {
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
|
||||
return DefaultAudioItem(audioUrl: self.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,32 @@ struct LongSource {
|
||||
static let url: URL = URL(fileURLWithPath: LongSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
|
||||
return DefaultAudioItem(audioUrl: self.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct FiveSecondSource {
|
||||
static let path: String = Bundle.main.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: FiveSecondSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: self.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct FiveSecondSourceWithInitialTimeOfFourSeconds {
|
||||
static let path: String = Bundle.main.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: FiveSecondSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItemInitialTime(
|
||||
audioUrl: self.path,
|
||||
artist: "a",
|
||||
title: "a",
|
||||
albumTitle: "a",
|
||||
sourceType: .file,
|
||||
artwork: nil,
|
||||
initialTime: 4
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -11,7 +11,10 @@ SwiftAudio is an audio player written in Swift, making it simpler to work with a
|
||||
## Example
|
||||
|
||||
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.
|
||||
To run the example project, clone the repo, then open
|
||||
`Example/SwiftAudio.xcodeproj` in Xcode. Choose "Example for SwiftAudio" in the
|
||||
XCode project navigator and Build/Run it in a simulator (or on an actual
|
||||
device).
|
||||
|
||||
## Requirements
|
||||
iOS 11.0+
|
||||
@@ -42,7 +45,7 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftAudio', '~> 0.11.2'
|
||||
pod 'SwiftAudioEx', '~> 0.15.3'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
@@ -68,12 +71,12 @@ To subscribe to an event:
|
||||
class MyCustomViewController: UIViewController {
|
||||
|
||||
let audioPlayer = AudioPlayer()
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
}
|
||||
|
||||
|
||||
func handleAudioPlayerStateChange(state: AudioPlayerState) {
|
||||
// Handle the event
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '0.15.3'
|
||||
s.version = '1.0.0-rc.9'
|
||||
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.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// APError.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct APError {
|
||||
|
||||
enum LoadError: Error {
|
||||
case invalidSourceUrl(String)
|
||||
}
|
||||
|
||||
enum PlaybackError: Error {
|
||||
case noLoadedItem
|
||||
}
|
||||
|
||||
enum QueueError: Error {
|
||||
case noPreviousItem
|
||||
case noNextItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
case noNextWhenRepeatModeTrack
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,14 +16,11 @@ public enum PlaybackEndedReason: String {
|
||||
case skippedToNext
|
||||
case skippedToPrevious
|
||||
case jumpedToIndex
|
||||
case cleared
|
||||
case failed
|
||||
}
|
||||
|
||||
class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
struct Constants {
|
||||
static let assetPlayableKey = "playable"
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
fileprivate var avPlayer = AVPlayer()
|
||||
@@ -31,71 +28,83 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
internal let playerTimeObserver: AVPlayerTimeObserver
|
||||
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
private let playerItemObserver = AVPlayerItemObserver()
|
||||
fileprivate var timeToSeekToAfterLoading: TimeInterval?
|
||||
fileprivate var asset: AVAsset? = nil
|
||||
fileprivate var item: AVPlayerItem? = nil
|
||||
fileprivate var url: URL? = nil
|
||||
fileprivate var urlOptions: [String: Any]? = nil
|
||||
fileprivate let stateQueue = DispatchQueue(
|
||||
label: "AVPlayerWrapper.stateQueue",
|
||||
attributes: .concurrent
|
||||
)
|
||||
|
||||
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() {
|
||||
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
|
||||
avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
setupAVPlayer();
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
fileprivate(set) var state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
if oldValue != state {
|
||||
delegate?.AVWrapper(didChangeState: state)
|
||||
fileprivate(set) var playbackError: AudioPlayerError.PlaybackError? = nil
|
||||
|
||||
var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle
|
||||
var state: AVPlayerWrapperState {
|
||||
get {
|
||||
var state: AVPlayerWrapperState!
|
||||
stateQueue.sync {
|
||||
state = _state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return state
|
||||
}
|
||||
set {
|
||||
stateQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let currentState = self._state
|
||||
if (currentState != newValue) {
|
||||
self._state = newValue
|
||||
self.delegate?.AVWrapper(didChangeState: newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate(set) var lastPlayerTimeControlStatus: AVPlayer.TimeControlStatus = AVPlayer.TimeControlStatus.paused
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
Whether AVPlayer should start playing automatically when the item is ready.
|
||||
*/
|
||||
fileprivate(set) var playWhenReady: Bool = true
|
||||
public var playWhenReady: Bool = false {
|
||||
didSet {
|
||||
if (playWhenReady == true && (state == .failed || state == .stopped)) {
|
||||
reload(startFromCurrentTime: state == .failed)
|
||||
}
|
||||
|
||||
applyAVPlayerRate()
|
||||
|
||||
if oldValue != playWhenReady {
|
||||
delegate?.AVWrapper(didChangePlayWhenReady: playWhenReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
avPlayer.currentItem
|
||||
}
|
||||
|
||||
var playbackActive: Bool {
|
||||
switch state {
|
||||
case .idle, .stopped, .ended, .failed:
|
||||
return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var currentTime: TimeInterval {
|
||||
let seconds = avPlayer.currentTime().seconds
|
||||
@@ -124,9 +133,13 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0;
|
||||
var rate: Float {
|
||||
get { avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
get { _rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
applyAVPlayerRate()
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
@@ -156,12 +169,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
func play() {
|
||||
playWhenReady = true
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
playWhenReady = false
|
||||
avPlayer.pause()
|
||||
}
|
||||
|
||||
func togglePlaying() {
|
||||
@@ -176,127 +187,240 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
pause()
|
||||
reset(soft: false)
|
||||
state = .stopped
|
||||
clearCurrentItem()
|
||||
playWhenReady = false
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
// if the player is loading then we need to defer seeking until it's ready.
|
||||
if (state == AVPlayerWrapperState.loading) {
|
||||
initialTime = seconds
|
||||
if (avPlayer.currentItem == nil) {
|
||||
timeToSeekToAfterLoading = 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)
|
||||
let time = CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)
|
||||
avPlayer.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { (finished) in
|
||||
self.delegate?.AVWrapper(seekTo: Double(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
reset(soft: true)
|
||||
self.playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
func seek(by seconds: TimeInterval) {
|
||||
if let currentItem = avPlayer.currentItem {
|
||||
let time = currentItem.currentTime().seconds + seconds
|
||||
avPlayer.seek(
|
||||
to: CMTimeMakeWithSeconds(time, preferredTimescale: 1000)
|
||||
) { (finished) in
|
||||
self.delegate?.AVWrapper(seekTo: Double(time), didFinish: finished)
|
||||
}
|
||||
} else {
|
||||
if let timeToSeekToAfterLoading = timeToSeekToAfterLoading {
|
||||
self.timeToSeekToAfterLoading = timeToSeekToAfterLoading + seconds
|
||||
} else {
|
||||
timeToSeekToAfterLoading = seconds
|
||||
}
|
||||
}
|
||||
|
||||
pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = pendingAsset {
|
||||
}
|
||||
|
||||
private func playbackFailed(error: AudioPlayerError.PlaybackError) {
|
||||
state = .failed
|
||||
self.playbackError = error
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
}
|
||||
|
||||
func load() {
|
||||
if (state == .failed) {
|
||||
recreateAVPlayer()
|
||||
} else {
|
||||
clearCurrentItem()
|
||||
}
|
||||
if let url = url {
|
||||
let pendingAsset = AVURLAsset(url: url, options: urlOptions)
|
||||
asset = pendingAsset
|
||||
state = .loading
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
||||
|
||||
// Load metadata keys asynchronously and separate from playable, to allow that to execute as quickly as it can
|
||||
let metdataKeys = ["commonMetadata", "availableChapterLocales", "availableMetadataFormats"]
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: metdataKeys, completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if (pendingAsset != self.asset) { return; }
|
||||
|
||||
let commonData = pendingAsset.commonMetadata
|
||||
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveChapterMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
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(didReceiveTimedMetadata: [group])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Load playable portion of the track and commence when ready
|
||||
let playableKeys = ["playable"]
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: playableKeys, completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
var error: NSError? = nil
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if (pendingAsset != self.pendingAsset) { return; }
|
||||
switch status {
|
||||
case .loaded:
|
||||
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 {
|
||||
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])
|
||||
}
|
||||
if (pendingAsset != self.asset) { return; }
|
||||
|
||||
for key in playableKeys {
|
||||
var error: NSError?
|
||||
let keyStatus = pendingAsset.statusOfValue(forKey: key, error: &error)
|
||||
switch keyStatus {
|
||||
case .failed:
|
||||
self.playbackFailed(error: AudioPlayerError.PlaybackError.failedToLoadKeyValue)
|
||||
return
|
||||
case .cancelled, .loading, .unknown:
|
||||
return
|
||||
case .loaded:
|
||||
break
|
||||
default: break
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
self.reset(soft: false)
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!pendingAsset.isPlayable) {
|
||||
self.playbackFailed(error: AudioPlayerError.PlaybackError.itemWasUnplayable)
|
||||
return;
|
||||
}
|
||||
|
||||
let item = AVPlayerItem(
|
||||
asset: pendingAsset,
|
||||
automaticallyLoadedAssetKeys: playableKeys
|
||||
)
|
||||
self.item = item;
|
||||
item.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: item)
|
||||
self.startObservingAVPlayer(item: item)
|
||||
self.applyAVPlayerRate()
|
||||
|
||||
if let initialTime = self.timeToSeekToAfterLoading {
|
||||
self.timeToSeekToAfterLoading = nil
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
|
||||
self.initialTime = initialTime
|
||||
|
||||
pausedForLoad = true
|
||||
pause()
|
||||
|
||||
self.load(from: url, playWhenReady: playWhenReady, options: options)
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
self.playWhenReady = playWhenReady
|
||||
self.url = url
|
||||
self.urlOptions = options
|
||||
self.load()
|
||||
}
|
||||
|
||||
// MARK: - Util
|
||||
|
||||
private func reset(soft: Bool) {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
func load(
|
||||
from url: URL,
|
||||
playWhenReady: Bool,
|
||||
initialTime: TimeInterval? = nil,
|
||||
options: [String : Any]? = nil
|
||||
) {
|
||||
self.load(from: url, playWhenReady: playWhenReady, options: options)
|
||||
if let initialTime = initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
}
|
||||
|
||||
pendingAsset?.cancelLoading()
|
||||
pendingAsset = nil
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
func load(
|
||||
from url: String,
|
||||
type: SourceType = .stream,
|
||||
playWhenReady: Bool = false,
|
||||
initialTime: TimeInterval? = nil,
|
||||
options: [String : Any]? = nil
|
||||
) {
|
||||
if let itemUrl = type == .file
|
||||
? URL(fileURLWithPath: url)
|
||||
: URL(string: url)
|
||||
{
|
||||
self.load(from: itemUrl, playWhenReady: playWhenReady, options: options)
|
||||
if let initialTime = initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
} else {
|
||||
clearCurrentItem()
|
||||
playbackFailed(error: AudioPlayerError.PlaybackError.invalidSourceUrl(url))
|
||||
}
|
||||
}
|
||||
|
||||
func unload() {
|
||||
clearCurrentItem()
|
||||
state = .idle
|
||||
}
|
||||
|
||||
func reload(startFromCurrentTime: Bool) {
|
||||
var time : Double? = nil
|
||||
if (startFromCurrentTime) {
|
||||
if let currentItem = currentItem {
|
||||
if (!currentItem.duration.isIndefinite) {
|
||||
time = currentItem.currentTime().seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
load()
|
||||
if let time = time {
|
||||
seek(to: time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Will recreate the AVPlayer instance. Used when the current one fails.
|
||||
// MARK: - Util
|
||||
|
||||
private func clearCurrentItem() {
|
||||
guard let asset = asset else { return }
|
||||
stopObservingAVPlayerItem()
|
||||
|
||||
asset.cancelLoading()
|
||||
self.asset = nil
|
||||
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
|
||||
private func startObservingAVPlayer(item: AVPlayerItem) {
|
||||
playerItemObserver.startObserving(item: item)
|
||||
playerItemNotificationObserver.startObserving(item: item)
|
||||
}
|
||||
|
||||
private func stopObservingAVPlayerItem() {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
private func recreateAVPlayer() {
|
||||
let player = AVPlayer()
|
||||
playerObserver.player = player
|
||||
playerTimeObserver.player = player
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
avPlayer = player
|
||||
playbackError = nil
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerTimeObserver.unregisterForPeriodicEvents()
|
||||
playerObserver.stopObserving()
|
||||
stopObservingAVPlayerItem()
|
||||
clearCurrentItem()
|
||||
|
||||
avPlayer = AVPlayer();
|
||||
setupAVPlayer()
|
||||
|
||||
delegate?.AVWrapperDidRecreateAVPlayer()
|
||||
}
|
||||
|
||||
private func setupAVPlayer() {
|
||||
// disabled since we're not making use of video playback
|
||||
avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerObserver.player = avPlayer
|
||||
playerObserver.startObserving()
|
||||
|
||||
playerTimeObserver.player = avPlayer
|
||||
playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
|
||||
applyAVPlayerRate()
|
||||
}
|
||||
|
||||
private func applyAVPlayerRate() {
|
||||
avPlayer.rate = playWhenReady ? _rate : 0
|
||||
}
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
@@ -304,30 +428,40 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
lastPlayerTimeControlStatus = status;
|
||||
switch status {
|
||||
case .paused:
|
||||
let state = self.state
|
||||
if self.asset == nil && state != .stopped {
|
||||
self.state = .idle
|
||||
} else if (state != .failed && state != .stopped) {
|
||||
// Playback may have become paused externally for example due to a bluetooth device disconnecting:
|
||||
if (self.playWhenReady) {
|
||||
// Only if we are not on the boundaries of the track, otherwise itemDidPlayToEndTime will handle it instead.
|
||||
if (self.currentTime > 0 && self.currentTime < self.duration) {
|
||||
self.playWhenReady = false;
|
||||
}
|
||||
} else {
|
||||
self.state = .paused
|
||||
}
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
if self.asset != nil {
|
||||
self.state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
self.state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
state = .ready
|
||||
pausedForLoad = false
|
||||
if playWhenReady && (initialTime ?? 0) == 0 {
|
||||
play()
|
||||
}
|
||||
else if let initialTime = initialTime {
|
||||
seek(to: initialTime)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
break
|
||||
|
||||
case .unknown:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
if (status == .failed) {
|
||||
let error = item!.error as NSError?
|
||||
playbackFailed(error: error?.code == URLError.notConnectedToInternet.rawValue
|
||||
? AudioPlayerError.PlaybackError.notConnectedToInternet
|
||||
: AudioPlayerError.PlaybackError.playbackFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,8 +481,16 @@ extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemFailedToPlayToEndTime() {
|
||||
playbackFailed(error: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
delegate?.AVWrapperItemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
func itemPlaybackStalled() {
|
||||
delegate?.AVWrapperItemPlaybackStalled()
|
||||
}
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
delegate?.AVWrapperItemDidPlayToEndTime()
|
||||
@@ -357,15 +499,19 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
if (playbackLikelyToKeepUp && state != .playing) {
|
||||
state = .ready
|
||||
}
|
||||
}
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveTimedMetadata: metadata)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ 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(seekTo seconds: Double, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem])
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool)
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperItemFailedToPlayToEndTime()
|
||||
func AVWrapperItemPlaybackStalled()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var state: AVPlayerWrapperState { get }
|
||||
|
||||
var playWhenReady: Bool { get }
|
||||
|
||||
var state: AVPlayerWrapperState { get set }
|
||||
|
||||
var playWhenReady: Bool { get set }
|
||||
|
||||
var currentItem: AVPlayerItem? { get }
|
||||
|
||||
var playbackActive: Bool { get }
|
||||
|
||||
var currentTime: TimeInterval { get }
|
||||
|
||||
var duration: TimeInterval { get }
|
||||
@@ -25,6 +27,7 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var playbackError: AudioPlayerError.PlaybackError? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
@@ -39,7 +42,6 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -50,8 +52,16 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
func stop()
|
||||
|
||||
func seek(to seconds: TimeInterval)
|
||||
|
||||
|
||||
func seek(by offset: TimeInterval)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func load(from url: String, type: SourceType, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func unload()
|
||||
|
||||
func reload(startFromCurrentTime: Bool)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,18 @@ public enum AVPlayerWrapperState: String {
|
||||
/// The player is paused.
|
||||
case paused
|
||||
|
||||
/// The player is stopped.
|
||||
case stopped
|
||||
|
||||
/// The player is playing.
|
||||
case playing
|
||||
|
||||
/// No item loaded, the player is stopped.
|
||||
case idle
|
||||
|
||||
/// Failed
|
||||
case failed
|
||||
|
||||
/// Playback has reached the end.
|
||||
case ended
|
||||
}
|
||||
|
||||
@@ -11,27 +11,26 @@ import MediaPlayer
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
|
||||
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public let event = EventHolder()
|
||||
|
||||
|
||||
private(set) var currentItem: AudioItem?
|
||||
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
|
||||
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
@@ -42,12 +41,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
internal var willPlayWhenReady: Bool {
|
||||
wrapper.playWhenReady
|
||||
public var playbackError: AudioPlayerError.PlaybackError? {
|
||||
wrapper.playbackError
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,40 +55,66 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
public var currentTime: Double {
|
||||
wrapper.currentTime
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The duration of the current AudioItem.
|
||||
*/
|
||||
public var duration: Double {
|
||||
wrapper.duration
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
public var playerState: AudioPlayerState {
|
||||
wrapper.state
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
|
||||
/**
|
||||
Whether the player should start playing automatically when the item is ready.
|
||||
*/
|
||||
public var playWhenReady: Bool {
|
||||
get { wrapper.playWhenReady }
|
||||
set {
|
||||
wrapper.playWhenReady = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
|
||||
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering. Setting `bufferDuration` to larger than zero automatically disables `automaticallyWaitsToMinimizeStalling`. Setting it back to zero automatically enables `automaticallyWaitsToMinimizeStalling`.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
|
||||
|
||||
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
set {
|
||||
wrapper.bufferDuration = newValue
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = wrapper.bufferDuration == 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling. Setting this to true will also set `bufferDuration` back to `0`.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set {
|
||||
if (newValue) {
|
||||
wrapper.bufferDuration = 0
|
||||
}
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,135 +124,134 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
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 { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var volume: Float {
|
||||
get { wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0
|
||||
public var rate: Float {
|
||||
get { _rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
|
||||
// Only set the rate on the wrapper if it is already playing.
|
||||
if wrapper.rate > 0 {
|
||||
wrapper.rate = newValue
|
||||
}
|
||||
}
|
||||
get { wrapper.rate }
|
||||
set { wrapper.rate = newValue }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
|
||||
/**
|
||||
Create a new AudioPlayer.
|
||||
|
||||
|
||||
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
||||
*/
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
|
||||
wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Player Actions
|
||||
|
||||
|
||||
/**
|
||||
Load an AudioItem into the manager.
|
||||
|
||||
|
||||
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
let url: URL
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
if let itemUrl = URL(string: item.getSourceUrl()) {
|
||||
url = itemUrl
|
||||
}
|
||||
else {
|
||||
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
|
||||
}
|
||||
case .file:
|
||||
url = URL(fileURLWithPath: item.getSourceUrl())
|
||||
}
|
||||
|
||||
wrapper.load(from: url,
|
||||
playWhenReady: playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions())
|
||||
|
||||
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
currentItem = item
|
||||
|
||||
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
// Reset playback values without updating, because that will happen in
|
||||
// the loadNowPlayingMetaValues call straight after:
|
||||
nowPlayingInfoController.setWithoutUpdate(keyValues: [
|
||||
MediaItemProperty.duration(nil),
|
||||
NowPlayingInfoProperty.playbackRate(nil),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
|
||||
])
|
||||
loadNowPlayingMetaValues()
|
||||
}
|
||||
|
||||
enableRemoteCommands(forItem: item)
|
||||
|
||||
wrapper.load(
|
||||
from: item.getSourceUrl(),
|
||||
type: item.getSourceType(),
|
||||
playWhenReady: self.playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
wrapper.play()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
wrapper.pause()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
Stop playback
|
||||
*/
|
||||
public func stop() {
|
||||
reset()
|
||||
let wasActive = wrapper.playbackActive
|
||||
wrapper.stop()
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
if (wasActive) {
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Reload the current item.
|
||||
*/
|
||||
public func reload(startFromCurrentTime: Bool) {
|
||||
wrapper.reload(startFromCurrentTime: startFromCurrentTime)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek by relative a time offset in the item.
|
||||
*/
|
||||
public func seek(by offset: TimeInterval) {
|
||||
wrapper.seek(by: offset)
|
||||
}
|
||||
|
||||
// MARK: - Remote Command Center
|
||||
|
||||
|
||||
func enableRemoteCommands(_ commands: [RemoteCommand]) {
|
||||
remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
@@ -245,12 +269,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
|
||||
/**
|
||||
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
|
||||
|
||||
|
||||
Reloads:
|
||||
- Artist
|
||||
- Title
|
||||
@@ -259,42 +283,49 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public func loadNowPlayingMetaValues() {
|
||||
guard let item = currentItem else { return }
|
||||
|
||||
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.artist(item.getArtist()),
|
||||
MediaItemProperty.title(item.getTitle()),
|
||||
MediaItemProperty.albumTitle(item.getAlbumTitle()),
|
||||
])
|
||||
|
||||
loadArtwork(forItem: item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Resyncs the playbackvalues of the currently playing `AudioItem`.
|
||||
|
||||
|
||||
Will resync:
|
||||
- Current time
|
||||
- Duration
|
||||
- Playback rate
|
||||
*/
|
||||
public func updateNowPlayingPlaybackValues() {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingRate(rate)
|
||||
func updateNowPlayingPlaybackValues() {
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.duration(wrapper.duration),
|
||||
NowPlayingInfoProperty.playbackRate(wrapper.playWhenReady ? Double(wrapper.rate) : 0),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime)
|
||||
])
|
||||
}
|
||||
|
||||
private func updateNowPlayingDuration(_ duration: Double) {
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
|
||||
|
||||
public func clear() {
|
||||
let playbackWasActive = wrapper.playbackActive
|
||||
currentItem = nil
|
||||
wrapper.unload()
|
||||
nowPlayingInfoController.clear()
|
||||
if (playbackWasActive) {
|
||||
event.playbackEnd.emit(data: .cleared)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNowPlayingRate(_ rate: Float) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setNowPlayingCurrentTime(seconds: Double) {
|
||||
nowPlayingInfoController.set(
|
||||
keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(seconds)
|
||||
)
|
||||
}
|
||||
|
||||
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
|
||||
}
|
||||
|
||||
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
@@ -305,36 +336,26 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func reset() {
|
||||
currentItem = nil
|
||||
}
|
||||
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
if let item = currentItem as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
switch state {
|
||||
case .ready, .loading:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
case .playing:
|
||||
// When a track starts playing, reset the rate to the stored rate
|
||||
rate = _rate;
|
||||
fallthrough
|
||||
case .paused:
|
||||
default: break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .ready, .loading, .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
@@ -342,32 +363,54 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
event.fail.emit(data: error)
|
||||
event.playbackEnd.emit(data: .failed)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
setNowPlayingCurrentTime(seconds: Double(seconds))
|
||||
}
|
||||
event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveMetadata.emit(data: metadata)
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
event.receiveCommonMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveChapterMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveTimedMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
event.playWhenReadyChange.emit(data: playWhenReady)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
wrapper.state = .ended
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
AVWrapper(failedWithError: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
}
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AudioPlayerError.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum AudioPlayerError: Error {
|
||||
|
||||
public enum PlaybackError: Error {
|
||||
case failedToLoadKeyValue
|
||||
case invalidSourceUrl(String)
|
||||
case notConnectedToInternet
|
||||
case playbackFailed
|
||||
case itemWasUnplayable
|
||||
}
|
||||
|
||||
public enum QueueError: Error {
|
||||
case noCurrentItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
case empty
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,23 @@ import MediaPlayer
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
public typealias PlayWhenReadyChangeData = Bool
|
||||
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 SeekEventData = (seconds: Double, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = Double
|
||||
public typealias MetadataEventData = [AVTimedMetadataGroup]
|
||||
public typealias MetadataCommonEventData = [AVMetadataItem]
|
||||
public typealias MetadataTimedEventData = [AVTimedMetadataGroup]
|
||||
public typealias DidRecreateAVPlayerEventData = ()
|
||||
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
|
||||
public typealias CurrentItemEventData = (
|
||||
item: AudioItem?,
|
||||
index: Int?,
|
||||
lastItem: AudioItem?,
|
||||
lastIndex: Int?,
|
||||
lastPosition: Double?
|
||||
)
|
||||
|
||||
public struct EventHolder {
|
||||
|
||||
@@ -27,6 +35,12 @@ extension AudioPlayer {
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the `AudioPlayer#playWhenReady` has changed
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let playWhenReadyChange: AudioPlayer.Event<PlayWhenReadyChangeData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the playback of the player, for some reason, has stopped.
|
||||
@@ -60,10 +74,22 @@ extension AudioPlayer {
|
||||
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives metadata.
|
||||
Emitted when the player receives common metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveMetadata: AudioPlayer.Event<MetadataEventData> = AudioPlayer.Event()
|
||||
public let receiveCommonMetadata: AudioPlayer.Event<MetadataCommonEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives timed metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveTimedMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives chapter metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveChapterMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
|
||||
@@ -73,11 +99,11 @@ extension AudioPlayer {
|
||||
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when a new track starts and the queue index changes.
|
||||
Emitted when the current track has changed.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
- Note: It is only fired for instances of a QueuedAudioPlayer.
|
||||
*/
|
||||
public let queueIndex: AudioPlayer.Event<QueueIndexEventData> = AudioPlayer.Event()
|
||||
public let currentItem: AudioPlayer.Event<CurrentItemEventData> = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
public typealias EventClosure<EventData> = (EventData) -> Void
|
||||
@@ -102,42 +128,28 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
public class Event<EventData> {
|
||||
|
||||
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
|
||||
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
|
||||
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
private let queue: DispatchQueue = DispatchQueue(label: "com.swiftAudioEx.eventQueue")
|
||||
var invokers: [Invoker<EventData>] = []
|
||||
|
||||
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers.append(Invoker(listener: listener, closure: closure))
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
public func removeListener(_ listener: AnyObject) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
if let listenerToCheck = invoker.listener {
|
||||
return listenerToCheck !== listener
|
||||
}
|
||||
return true
|
||||
return invoker.listener !== listener
|
||||
})
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
func emit(data: EventData) {
|
||||
eventQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter { $0.invoke(data) }
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,54 +9,64 @@ import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
private let concurrentInfoQueue: DispatchQueueType
|
||||
private var infoQueue: DispatchQueueType = DispatchQueue(
|
||||
label: "NowPlayingInfoController.infoQueue",
|
||||
attributes: .concurrent
|
||||
)
|
||||
|
||||
private(set) var infoCenter: NowPlayingInfoCenter
|
||||
private(set) var info: [String: Any] = [:]
|
||||
|
||||
public required init() {
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
concurrentInfoQueue = dispatchQueue
|
||||
infoQueue = dispatchQueue
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter) {
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
public required init(infoCenter: NowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public func set(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
keyValues.forEach { (keyValue) in
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
keyValues.forEach {
|
||||
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
public func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
keyValues.forEach {
|
||||
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func set(keyValue: NowPlayingInfoKeyValue) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private func update() {
|
||||
infoCenter.nowPlayingInfo = info
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.info = [:]
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
self.infoCenter.nowPlayingInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ public protocol NowPlayingInfoControllerProtocol {
|
||||
|
||||
func set(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func clear()
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import AVFoundation
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
|
||||
func itemDidPlayToEndTime()
|
||||
func itemFailedToPlayToEndTime()
|
||||
func itemPlaybackStalled()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +42,24 @@ class AVPlayerItemNotificationObserver {
|
||||
stopObservingCurrentItem()
|
||||
observingItem = item
|
||||
isObserving = true
|
||||
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemDidPlayToEndTime),
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: item
|
||||
)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemFailedToPlayToEndTime),
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: item
|
||||
)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemPlaybackStalled),
|
||||
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
|
||||
object: item
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +69,21 @@ class AVPlayerItemNotificationObserver {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
|
||||
object: observingItem
|
||||
)
|
||||
self.observingItem = nil
|
||||
isObserving = false
|
||||
}
|
||||
@@ -58,5 +91,12 @@ class AVPlayerItemNotificationObserver {
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
delegate?.itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
|
||||
@objc private func itemFailedToPlayToEndTime() {
|
||||
delegate?.itemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
@objc private func itemPlaybackStalled() {
|
||||
delegate?.itemPlaybackStalled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@ import AVFoundation
|
||||
protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the observed item updates the duration.
|
||||
Called when the duration of the observed item is updated.
|
||||
*/
|
||||
func item(didUpdateDuration duration: Double)
|
||||
|
||||
/**
|
||||
Called when the playback of the observed item is or is no longer likely to keep up.
|
||||
*/
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool)
|
||||
/**
|
||||
Called when the observed item receives metadata
|
||||
*/
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +38,7 @@ class AVPlayerItemObserver: NSObject {
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
@@ -63,6 +68,7 @@ class AVPlayerItemObserver: NSObject {
|
||||
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.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.add(metadataOutput)
|
||||
}
|
||||
|
||||
@@ -72,6 +78,7 @@ 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.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
|
||||
observingItem.remove(metadataOutput)
|
||||
isObserving = false
|
||||
self.observingItem = nil
|
||||
@@ -94,6 +101,11 @@ class AVPlayerItemObserver: NSObject {
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
|
||||
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
default: break
|
||||
|
||||
}
|
||||
@@ -102,6 +114,6 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
delegate?.item(didReceiveMetadata: groups)
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,23 @@ class AVPlayerObserver: NSObject {
|
||||
Start receiving events from this observer.
|
||||
*/
|
||||
func startObserving() {
|
||||
if (isObserving) { return };
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
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)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.status,
|
||||
options: statusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.timeControlStatus,
|
||||
options: timeControlStatusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
}
|
||||
|
||||
func stopObserving() {
|
||||
|
||||
@@ -61,19 +61,25 @@ class AVPlayerTimeObserver {
|
||||
return
|
||||
}
|
||||
unregisterForBoundaryTimeEvents()
|
||||
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})
|
||||
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
|
||||
self?.delegate?.audioDidStart()
|
||||
})
|
||||
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(
|
||||
forTimes: [AVPlayerTimeObserver.startBoundaryTime].map({
|
||||
NSValue(time: $0)
|
||||
}),
|
||||
queue: nil,
|
||||
using: { [weak self] in
|
||||
self?.delegate?.audioDidStart()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Unregister from the boundary events of the player.
|
||||
*/
|
||||
func unregisterForBoundaryTimeEvents() {
|
||||
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
|
||||
return
|
||||
}
|
||||
guard
|
||||
let player = player,
|
||||
let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken
|
||||
else { return }
|
||||
player.removeTimeObserver(boundaryTimeStartObserverToken)
|
||||
self.boundaryTimeStartObserverToken = nil
|
||||
}
|
||||
|
||||
@@ -9,217 +9,320 @@ import Foundation
|
||||
|
||||
protocol QueueManagerDelegate: AnyObject {
|
||||
func onReceivedFirstItem()
|
||||
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int)
|
||||
func onCurrentItemChanged()
|
||||
func onSkippedToSameCurrentItem()
|
||||
}
|
||||
|
||||
class QueueManager<T> {
|
||||
|
||||
fileprivate let recursiveLock = NSRecursiveLock()
|
||||
|
||||
fileprivate func synchronizeThrows<T>(action: () throws -> T) throws -> T {
|
||||
recursiveLock.lock()
|
||||
let result = try action()
|
||||
recursiveLock.unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func synchronize <T>(action: () -> T) -> T {
|
||||
recursiveLock.lock()
|
||||
let result = action()
|
||||
recursiveLock.unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
weak var delegate: QueueManagerDelegate? = nil
|
||||
|
||||
var _currentIndex: Int = -1
|
||||
/**
|
||||
The index of the current item. `-1` when there is no current item
|
||||
*/
|
||||
private(set) var currentIndex: Int {
|
||||
get {
|
||||
return synchronize {
|
||||
return _currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
return synchronize {
|
||||
self._currentIndex = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
private(set) var items: [T] = [] {
|
||||
didSet {
|
||||
if oldValue.count == 0 && items.count > 0 && currentIndex == 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
return synchronize {
|
||||
if oldValue.count == 0 && items.count > 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var nextItems: [T] {
|
||||
guard currentIndex + 1 < items.count else {
|
||||
return []
|
||||
return synchronize {
|
||||
return currentIndex == -1 || currentIndex == items.count - 1
|
||||
? []
|
||||
: Array(items[currentIndex + 1..<items.count])
|
||||
}
|
||||
return Array(items[currentIndex + 1..<items.count])
|
||||
}
|
||||
|
||||
public var previousItems: [T] {
|
||||
if (currentIndex == 0) {
|
||||
return []
|
||||
}
|
||||
return Array(items[0..<currentIndex])
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
private(set) var currentIndex: Int = 0 {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: currentIndex)
|
||||
public var previousItems: [T] {
|
||||
return synchronize {
|
||||
return currentIndex <= 0
|
||||
? []
|
||||
: Array(items[0..<currentIndex])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The current item for the queue.
|
||||
*/
|
||||
public var current: T? {
|
||||
if items.count > currentIndex {
|
||||
return items[currentIndex]
|
||||
return synchronize {
|
||||
return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
private func throwIfQueueEmpty() throws {
|
||||
if items.count == 0 {
|
||||
throw AudioPlayerError.QueueError.empty
|
||||
}
|
||||
}
|
||||
|
||||
private func throwIfIndexInvalid(
|
||||
index: Int,
|
||||
name: String = "index",
|
||||
min: Int? = nil,
|
||||
max: Int? = nil
|
||||
) throws {
|
||||
guard index >= (min ?? 0) && (max ?? items.count) > index else {
|
||||
throw AudioPlayerError.QueueError.invalidIndex(
|
||||
index: index,
|
||||
message: "\(name.prefix(1).uppercased() + name.dropFirst())) has to be positive and smaller than the count of current items (\(items.count))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
|
||||
- parameter item: The `AudioItem` to be added.
|
||||
*/
|
||||
public func addItem(_ item: T) {
|
||||
items.append(item)
|
||||
public func add(_ item: T) {
|
||||
synchronize {
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Add an array of items to the queue.
|
||||
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
*/
|
||||
public func addItems(_ items: [T]) {
|
||||
self.items.append(contentsOf: items)
|
||||
public func add(_ items: [T]) {
|
||||
synchronize {
|
||||
if (items.count == 0) { return }
|
||||
self.items.append(contentsOf: items)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Add an array of items to the queue at a given index.
|
||||
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
- parameter at: The index to insert the items at.
|
||||
*/
|
||||
public func addItems(_ items: [T], at index: Int) throws {
|
||||
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))")
|
||||
public func add(_ items: [T], at index: Int) throws {
|
||||
try synchronizeThrows {
|
||||
if (items.count == 0) { return }
|
||||
guard index >= 0 && self.items.count >= index else {
|
||||
throw AudioPlayerError.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))")
|
||||
}
|
||||
// Correct index when items were inserted in front of it:
|
||||
if (self.items.count > 1 && currentIndex >= index) {
|
||||
currentIndex += items.count
|
||||
}
|
||||
self.items.insert(contentsOf: items, at: index)
|
||||
}
|
||||
|
||||
self.items.insert(contentsOf: items, at: index)
|
||||
|
||||
if (currentIndex >= index && self.items.count != 1) { currentIndex += items.count }
|
||||
}
|
||||
|
||||
|
||||
internal enum SkipDirection : Int {
|
||||
case next = 1
|
||||
case previous = -1
|
||||
}
|
||||
|
||||
private func skip(direction: SkipDirection, wrap: Bool) -> T? {
|
||||
let count = items.count
|
||||
if (current == nil || count == 0) {
|
||||
return nil
|
||||
}
|
||||
if (count == 1) {
|
||||
if (wrap) {
|
||||
delegate?.onSkippedToSameCurrentItem()
|
||||
}
|
||||
} else {
|
||||
var index = currentIndex + direction.rawValue
|
||||
if (wrap) {
|
||||
index = (items.count + index) % items.count;
|
||||
}
|
||||
let oldIndex = currentIndex
|
||||
currentIndex = max(0, min(items.count - 1, index))
|
||||
if (oldIndex != currentIndex) {
|
||||
defer {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
Get the next item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The next item.
|
||||
Makes the next item in the queue active, or the last item when already at the end of the queue. When wrap is true and at the end of the queue, the first track in the queue is made active.
|
||||
- parameter wrap: Whether to wrap to the start of the queue
|
||||
- returns: The next (or current) item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func next() throws -> T {
|
||||
let nextIndex = currentIndex + 1
|
||||
guard items.count > nextIndex else {
|
||||
throw APError.QueueError.noNextItem
|
||||
public func next(wrap: Bool = false) -> T? {
|
||||
synchronize {
|
||||
return skip(direction: SkipDirection.next, wrap: wrap);
|
||||
}
|
||||
currentIndex = nextIndex
|
||||
return items[nextIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
Get the previous item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
/**
|
||||
Makes the previous item in the queue active, or the first item when already at the start of the queue. When wrap is true and at the start of the queue, the last track in the queue is made active.
|
||||
|
||||
- parameter wrap: Whether to wrap to the end of the queue
|
||||
- returns: The previous item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func previous() throws -> T {
|
||||
let previousIndex = currentIndex - 1
|
||||
guard previousIndex >= 0 else {
|
||||
throw APError.QueueError.noPreviousItem
|
||||
public func previous(wrap: Bool = false) -> T? {
|
||||
return synchronize {
|
||||
return skip(direction: SkipDirection.previous, wrap: wrap);
|
||||
}
|
||||
currentIndex = previousIndex
|
||||
return items[previousIndex]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Jump to a position in the queue.
|
||||
Will update the current item.
|
||||
|
||||
|
||||
- parameter index: The index to jump to.
|
||||
- throws: `APError.QueueError`
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
- returns: The item at the index.
|
||||
*/
|
||||
@discardableResult
|
||||
func jump(to index: Int) throws -> T {
|
||||
guard index != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
|
||||
public func jump(to index: Int) throws -> T {
|
||||
var skippedToSameCurrentItem = false
|
||||
var currentItemChanged = false
|
||||
let result = try synchronizeThrows {
|
||||
try throwIfQueueEmpty();
|
||||
try throwIfIndexInvalid(index: index)
|
||||
|
||||
if (index == currentIndex) {
|
||||
skippedToSameCurrentItem = true
|
||||
} else {
|
||||
currentIndex = index
|
||||
currentItemChanged = true
|
||||
}
|
||||
return current!
|
||||
}
|
||||
|
||||
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))")
|
||||
if (skippedToSameCurrentItem) {
|
||||
delegate?.onSkippedToSameCurrentItem()
|
||||
}
|
||||
|
||||
currentIndex = index
|
||||
return items[index]
|
||||
if (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Move an item in the queue.
|
||||
|
||||
|
||||
- parameter fromIndex: The index of the item to be moved.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
- parameter toIndex: The index to move the item to. If the index is larger than the size of the queue, the item is moved to the end of the queue instead.
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
guard fromIndex != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
|
||||
public func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
try synchronizeThrows {
|
||||
try throwIfQueueEmpty();
|
||||
try throwIfIndexInvalid(index: fromIndex, name: "fromIndex")
|
||||
try throwIfIndexInvalid(index: toIndex, name: "toIndex", max: Int.max)
|
||||
|
||||
let item = items.remove(at: fromIndex)
|
||||
self.items.insert(item, at: min(items.count, toIndex));
|
||||
if (fromIndex == currentIndex) {
|
||||
currentIndex = toIndex;
|
||||
}
|
||||
}
|
||||
|
||||
guard fromIndex >= 0 && fromIndex < items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
guard toIndex >= 0 && toIndex < items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
let item = try removeItem(at: fromIndex)
|
||||
try addItems([item], at: toIndex)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Remove an item.
|
||||
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: APError.QueueError
|
||||
- throws: AudioPlayerError.QueueError
|
||||
- returns: The removed item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func removeItem(at index: Int) throws -> T {
|
||||
guard index != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
|
||||
var currentItemChanged = false
|
||||
let result = try synchronizeThrows {
|
||||
try throwIfQueueEmpty()
|
||||
try throwIfIndexInvalid(index: index)
|
||||
let result = items.remove(at: index)
|
||||
if index == currentIndex {
|
||||
currentIndex = items.count > 0 ? currentIndex % items.count : -1
|
||||
currentItemChanged = true
|
||||
} else if index < currentIndex {
|
||||
currentIndex -= 1
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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 (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
|
||||
if index < currentIndex {
|
||||
currentIndex -= 1
|
||||
}
|
||||
|
||||
return items.remove(at: index)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
|
||||
|
||||
Replace the current item with a new one. If there is no current item, it is equivalent to calling `add(item:)`, `jump(to: itemIndex)`.
|
||||
|
||||
- parameter item: The item to set as the new current item.
|
||||
*/
|
||||
public func replaceCurrentItem(with item: T) {
|
||||
if current == nil {
|
||||
addItem(item)
|
||||
var currentItemChanged = false
|
||||
synchronize {
|
||||
if currentIndex == -1 {
|
||||
add(item)
|
||||
if (currentIndex == -1) {
|
||||
currentIndex = items.count - 1
|
||||
}
|
||||
} else {
|
||||
items[currentIndex] = item
|
||||
currentItemChanged = true
|
||||
}
|
||||
}
|
||||
if (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
|
||||
items[currentIndex] = item
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Remove all previous items in the queue.
|
||||
If no previous items exist, no action will be taken.
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
guard currentIndex > 0 else { return }
|
||||
items.removeSubrange(0..<currentIndex)
|
||||
currentIndex = 0
|
||||
synchronize {
|
||||
if (items.count == 0) { return };
|
||||
guard currentIndex > 0 else { return }
|
||||
items.removeSubrange(0..<currentIndex)
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,17 +330,28 @@ 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)
|
||||
synchronize {
|
||||
if (items.count == 0) { return };
|
||||
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()
|
||||
var currentItemChanged = false
|
||||
synchronize {
|
||||
let itemWasNil = currentIndex == -1;
|
||||
currentIndex = -1
|
||||
items.removeAll()
|
||||
currentItemChanged = !itemWasNil
|
||||
}
|
||||
if (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,234 +12,227 @@ import MediaPlayer
|
||||
An audio player that can keep track of a queue of AudioItems.
|
||||
*/
|
||||
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
|
||||
let queueManager: QueueManager = QueueManager<AudioItem>()
|
||||
let queue: QueueManager = QueueManager<AudioItem>()
|
||||
fileprivate var lastIndex: Int = -1
|
||||
fileprivate var lastItem: AudioItem? = nil
|
||||
|
||||
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
|
||||
queueManager.delegate = self
|
||||
queue.delegate = self
|
||||
}
|
||||
|
||||
/// The repeat mode for the queue player.
|
||||
public var repeatMode: RepeatMode = .off
|
||||
|
||||
|
||||
public override var currentItem: AudioItem? {
|
||||
queueManager.current
|
||||
queue.current
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
queueManager.currentIndex
|
||||
queue.currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
Stops the player and clears the queue.
|
||||
*/
|
||||
public override func stop() {
|
||||
super.stop()
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
|
||||
override public func clear() {
|
||||
queue.clearQueue()
|
||||
super.clear()
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
queueManager.clearQueue()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
All items currently in the queue.
|
||||
*/
|
||||
public var items: [AudioItem] {
|
||||
queueManager.items
|
||||
queue.items
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
queueManager.previousItems
|
||||
queue.previousItems
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
queueManager.nextItems
|
||||
queue.nextItems
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Will replace the current item with a new one and load it into the player.
|
||||
|
||||
|
||||
- parameter item: The AudioItem to replace the current item.
|
||||
- throws: APError.LoadError
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public override func load(item: AudioItem, playWhenReady: Bool) throws {
|
||||
try super.load(item: item, playWhenReady: playWhenReady)
|
||||
queueManager.replaceCurrentItem(with: item)
|
||||
public override func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
queue.replaceCurrentItem(with: item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
|
||||
- parameter item: The item to add.
|
||||
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
|
||||
- throws: `APError`
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItem(item)
|
||||
try load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItem(item)
|
||||
public func add(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
queue.add(item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Add items to the queue.
|
||||
|
||||
|
||||
- parameter items: The items to add to the queue.
|
||||
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the first item in the list. If this is `true` it will automatically start playback. Default is `true`.
|
||||
- throws: `APError`
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItems(items)
|
||||
try load(item: currentItem!, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItems(items)
|
||||
public func add(items: [AudioItem], playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
queue.add(items)
|
||||
}
|
||||
|
||||
|
||||
public func add(items: [AudioItem], at index: Int) throws {
|
||||
try queueManager.addItems(items, at: index)
|
||||
try queue.add(items, at: index)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Step to the next item in the queue.
|
||||
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func next() throws {
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
public func next() {
|
||||
let lastIndex = currentIndex
|
||||
let playbackWasActive = wrapper.playbackActive;
|
||||
_ = queue.next(wrap: repeatMode == .queue)
|
||||
if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) {
|
||||
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 {
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
let previousItem = try queueManager.previous()
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
try load(item: previousItem, playWhenReady: shouldPlayWhenReady)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item from the queue.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
public func removeItem(at index: Int) throws {
|
||||
try queueManager.removeItem(at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
Jump to a certain item in the queue.
|
||||
|
||||
- parameter index: The index of the item to jump to.
|
||||
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
|
||||
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)
|
||||
public func previous() {
|
||||
let lastIndex = currentIndex
|
||||
let playbackWasActive = wrapper.playbackActive;
|
||||
_ = queue.previous(wrap: repeatMode == .queue)
|
||||
if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) {
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Remove an item from the queue.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
public func removeItem(at index: Int) throws {
|
||||
try queue.removeItem(at: index)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Jump to a certain item in the queue.
|
||||
|
||||
- parameter index: The index of the item to jump to.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
- throws: `AudioPlayerError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool? = nil) throws {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
} else {
|
||||
_ = try queue.jump(to: index)
|
||||
}
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
Move an item in the queue from one position to another.
|
||||
|
||||
|
||||
- parameter fromIndex: The index of the item to move.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
public func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
|
||||
try queue.moveItem(fromIndex: fromIndex, toIndex: toIndex)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Remove all upcoming items, those returned by `next()`
|
||||
*/
|
||||
public func removeUpcomingItems() {
|
||||
queueManager.removeUpcomingItems()
|
||||
queue.removeUpcomingItems()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Remove all previous items, those returned by `previous()`
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
queueManager.removePreviousItems()
|
||||
queue.removePreviousItems()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
override func AVWrapperItemDidPlayToEndTime() {
|
||||
super.AVWrapperItemDidPlayToEndTime()
|
||||
|
||||
switch repeatMode {
|
||||
case .off:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
case .track:
|
||||
try? jumpToItem(atIndex: currentIndex, playWhenReady: true)
|
||||
case .queue:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
try? jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
}
|
||||
func replay() {
|
||||
seek(to: 0);
|
||||
play()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
override func AVWrapperItemDidPlayToEndTime() {
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
if (repeatMode == .track) {
|
||||
// quick workaround for race condition - schedule a call after 2 frames
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.016 * 2) { [weak self] in self?.replay() }
|
||||
} else if (repeatMode == .queue) {
|
||||
_ = queue.next(wrap: true)
|
||||
} else if (currentIndex != items.count - 1) {
|
||||
_ = queue.next(wrap: false)
|
||||
} else {
|
||||
wrapper.state = .ended
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - QueueManagerDelegate
|
||||
|
||||
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
|
||||
// if _currentItem is nil, then this was triggered by a reset. ignore.
|
||||
if currentItem == nil { return }
|
||||
event.queueIndex.emit(data: (oldIndex, newIndex))
|
||||
func onCurrentItemChanged() {
|
||||
let lastPosition = currentTime;
|
||||
if let currentItem = currentItem {
|
||||
super.load(item: currentItem)
|
||||
} else {
|
||||
super.clear()
|
||||
}
|
||||
event.currentItem.emit(
|
||||
data: (
|
||||
item: currentItem,
|
||||
index: currentIndex == -1 ? nil : currentIndex,
|
||||
lastItem: lastItem,
|
||||
lastIndex: lastIndex == -1 ? nil : lastIndex,
|
||||
lastPosition: lastPosition
|
||||
)
|
||||
)
|
||||
lastItem = currentItem
|
||||
lastIndex = currentIndex
|
||||
}
|
||||
|
||||
func onSkippedToSameCurrentItem() {
|
||||
if (wrapper.playbackActive) {
|
||||
replay()
|
||||
}
|
||||
}
|
||||
|
||||
func onReceivedFirstItem() {
|
||||
event.queueIndex.emit(data: (nil, 0))
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,26 +171,16 @@ public class RemoteCommandController {
|
||||
|
||||
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
@@ -208,19 +198,9 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
if let error = error as? APError.LoadError {
|
||||
switch error {
|
||||
case .invalidSourceUrl(_):
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
}
|
||||
else if let error = error as? APError.QueueError {
|
||||
switch error {
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
|
||||
return MPRemoteCommandHandlerStatus.noSuchContent
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
return error is AudioPlayerError.QueueError
|
||||
? MPRemoteCommandHandlerStatus.noSuchContent
|
||||
: MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user