Compare commits
70 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 | |||
| 23fdb9b9db | |||
| 24c19aa661 | |||
| 38429c6ca8 | |||
| 72f9c5d147 | |||
| bd93898809 | |||
| 8276f38b1b | |||
| fcd5790e1e | |||
| ead7c0962e | |||
| 7ff34271e8 | |||
| 4f7a5b02a6 | |||
| af803339dc | |||
| a5bf6eb1dd | |||
| 5e0c27b990 | |||
| 6079234942 | |||
| e74b5ffe4d | |||
| 92554a187c | |||
| 473651f357 | |||
| db2f3e9af7 | |||
| a9f831a258 | |||
| cc3840d81e | |||
| 5307090ea3 | |||
| bdaee8b18f | |||
| 84d359bc4f | |||
| 40ea7ad2f9 | |||
| f2f1c1236c | |||
| a75f0d0201 | |||
| 9e4e7f6807 | |||
| dbd3b03989 | |||
| 7e19604df7 | |||
| 481130dc58 | |||
| 300b34afa3 | |||
| da3af0e9db | |||
| d9eb313c1b | |||
| cca7f68da4 | |||
| 7ed74b80ec | |||
| 2773e4bfec | |||
| 77dc8f4ff1 | |||
| accdf2c00c | |||
| 542d3a5764 | |||
| 4131e54f3e | |||
| 03c4a7310f | |||
| 9d2d2594a1 | |||
| 4e790876cb | |||
| b19d01bdfc |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: DoubleSymmetry
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,5 +1,12 @@
|
||||
name: validate
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: macos-latest
|
||||
@@ -17,4 +24,4 @@ jobs:
|
||||
cd Example
|
||||
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
|
||||
env:
|
||||
destination: ${{ matrix.destination }}
|
||||
destination: ${{ matrix.destination }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -44,9 +44,11 @@
|
||||
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
|
||||
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 */; };
|
||||
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79326C522D0004BAF2F /* SwiftAudioEx */; };
|
||||
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79526C52382004BAF2F /* SwiftAudioEx */; };
|
||||
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 */
|
||||
@@ -94,8 +96,9 @@
|
||||
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftAudio_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = five_seconds.m4a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -103,7 +106,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */,
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -111,7 +114,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */,
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */,
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
|
||||
);
|
||||
@@ -123,6 +126,7 @@
|
||||
0708ED712116E91300EB29BD /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */,
|
||||
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */,
|
||||
0708ED6F2116E89900EB29BD /* Source.swift */,
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
|
||||
@@ -222,7 +226,7 @@
|
||||
9B05AA2F2660276400C7A389 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */,
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -244,7 +248,7 @@
|
||||
);
|
||||
name = SwiftAudio_Example;
|
||||
packageProductDependencies = (
|
||||
9B77D79326C522D0004BAF2F /* SwiftAudioEx */,
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = SwiftAudio;
|
||||
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
|
||||
@@ -267,7 +271,7 @@
|
||||
packageProductDependencies = (
|
||||
9B05AA302660276400C7A389 /* Quick */,
|
||||
9B05AA322660276400C7A389 /* Nimble */,
|
||||
9B77D79526C52382004BAF2F /* SwiftAudioEx */,
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = Tests;
|
||||
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
|
||||
@@ -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,12 +581,13 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
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";
|
||||
@@ -592,18 +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 = 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";
|
||||
@@ -672,11 +678,11 @@
|
||||
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
|
||||
productName = Nimble;
|
||||
};
|
||||
9B77D79326C522D0004BAF2F /* SwiftAudioEx */ = {
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
9B77D79526C52382004BAF2F /* SwiftAudioEx */ = {
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
|
||||
@@ -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,40 +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) {
|
||||
@@ -76,61 +73,82 @@ 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
|
||||
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print(data)
|
||||
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)")
|
||||
}
|
||||
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,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: [AVMetadataItem]) -> Void)?
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
receivedMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,30 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__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()
|
||||
@@ -157,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() {
|
||||
@@ -182,7 +206,32 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
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]) {
|
||||
|
||||
}
|
||||
|
||||
@@ -194,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) {
|
||||
@@ -219,7 +282,7 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
|
||||
didSeekTo?(seconds)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,211 +1,590 @@
|
||||
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
|
||||
audioPlayer.bufferDuration = 0.001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
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 stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
|
||||
|
||||
var state: AudioPlayerState?
|
||||
|
||||
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 {
|
||||
audioPlayer?.event.stateChange.removeListener(self)
|
||||
audioPlayer?.event.seek.removeListener(self)
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,11 +52,12 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
describe("its delegate") {
|
||||
context("when a interruption arrives") {
|
||||
context("when a ended interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0)
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0),
|
||||
AVAudioSessionInterruptionOptionKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
@@ -64,7 +65,23 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventuallyNot(beNil())
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.ended(shouldResume: true)))
|
||||
}
|
||||
|
||||
}
|
||||
context("when a begin interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.began))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,10 +108,9 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
var interruptionType: InterruptionType? = nil
|
||||
|
||||
var interruptionType: AVAudioSession.InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType) {
|
||||
func handleInterruption(type: InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -0,0 +1,42 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Double Symmetry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftAudio",
|
||||
name: "SwiftAudioEx",
|
||||
platforms: [.iOS(.v11)],
|
||||
products: [
|
||||
.library(
|
||||
|
||||
@@ -11,10 +11,13 @@ 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 10.0+
|
||||
iOS 11.0+
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -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.14.0'
|
||||
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,75 +16,94 @@ 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
|
||||
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
let playerItemObserver: AVPlayerItemObserver
|
||||
fileprivate var avPlayer = AVPlayer()
|
||||
private let playerObserver = AVPlayerObserver()
|
||||
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
|
||||
)
|
||||
|
||||
public init() {
|
||||
playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
|
||||
playerObserver.delegate = self
|
||||
playerTimeObserver.delegate = self
|
||||
playerItemNotificationObserver.delegate = self
|
||||
playerItemObserver.delegate = self
|
||||
|
||||
setupAVPlayer();
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
fileprivate(set) var playbackError: AudioPlayerError.PlaybackError? = nil
|
||||
|
||||
var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle
|
||||
var state: AVPlayerWrapperState {
|
||||
get {
|
||||
var state: AVPlayerWrapperState!
|
||||
stateQueue.sync {
|
||||
state = _state
|
||||
}
|
||||
|
||||
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 var _playWhenReady: Bool = true
|
||||
fileprivate var _initialTime: TimeInterval?
|
||||
|
||||
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
public var playWhenReady: Bool = false {
|
||||
didSet {
|
||||
if oldValue != _state {
|
||||
self.delegate?.AVWrapper(didChangeState: _state)
|
||||
if (playWhenReady == true && (state == .failed || state == .stopped)) {
|
||||
reload(startFromCurrentTime: state == .failed)
|
||||
}
|
||||
|
||||
applyAVPlayerRate()
|
||||
|
||||
if oldValue != playWhenReady {
|
||||
delegate?.AVWrapper(didChangePlayWhenReady: playWhenReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.avPlayer = AVPlayer()
|
||||
self.playerObserver = AVPlayerObserver()
|
||||
self.playerObserver.player = avPlayer
|
||||
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
self.playerTimeObserver.player = avPlayer
|
||||
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
self.playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
self.playerObserver.delegate = self
|
||||
self.playerTimeObserver.delegate = self
|
||||
self.playerItemNotificationObserver.delegate = self
|
||||
self.playerItemObserver.delegate = self
|
||||
|
||||
// disabled since we're not making use of video playback
|
||||
self.avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
var state: AVPlayerWrapperState {
|
||||
return _state
|
||||
}
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
return avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
return avPlayer.currentItem
|
||||
avPlayer.currentItem
|
||||
}
|
||||
|
||||
var _pendingAsset: AVAsset? = nil
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
|
||||
var playbackActive: Bool {
|
||||
switch state {
|
||||
case .idle, .stopped, .ended, .failed:
|
||||
return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var currentTime: TimeInterval {
|
||||
@@ -99,50 +118,61 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0;
|
||||
var rate: Float {
|
||||
get { _rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
applyAVPlayerRate()
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
|
||||
|
||||
var timeEventFrequency: TimeEventFrequency = .everySecond {
|
||||
didSet {
|
||||
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
get { return avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
}
|
||||
|
||||
var volume: Float {
|
||||
get { return avPlayer.volume }
|
||||
get { avPlayer.volume }
|
||||
set { avPlayer.volume = newValue }
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get { return avPlayer.isMuted }
|
||||
get { avPlayer.isMuted }
|
||||
set { avPlayer.isMuted = newValue }
|
||||
}
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
func play() {
|
||||
_playWhenReady = true
|
||||
avPlayer.play()
|
||||
playWhenReady = true
|
||||
}
|
||||
|
||||
func pause() {
|
||||
_playWhenReady = false
|
||||
avPlayer.pause()
|
||||
playWhenReady = false
|
||||
}
|
||||
|
||||
func togglePlaying() {
|
||||
@@ -157,114 +187,240 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
pause()
|
||||
reset(soft: false)
|
||||
state = .stopped
|
||||
clearCurrentItem()
|
||||
playWhenReady = false
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self._initialTime {
|
||||
self._initialTime = nil
|
||||
if self._playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
// if the player is loading then we need to defer seeking until it's ready.
|
||||
if (avPlayer.currentItem == nil) {
|
||||
timeToSeekToAfterLoading = seconds
|
||||
} else {
|
||||
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 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
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
|
||||
private func playbackFailed(error: AudioPlayerError.PlaybackError) {
|
||||
state = .failed
|
||||
self.playbackError = error
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
}
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
func load() {
|
||||
if (state == .failed) {
|
||||
recreateAVPlayer()
|
||||
} else {
|
||||
clearCurrentItem()
|
||||
}
|
||||
|
||||
self._pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = _pendingAsset {
|
||||
self._state = .loading
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
||||
if let url = url {
|
||||
let pendingAsset = AVURLAsset(url: url, options: urlOptions)
|
||||
asset = pendingAsset
|
||||
state = .loading
|
||||
|
||||
// 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; }
|
||||
|
||||
guard let self = self else {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
var error: NSError? = nil
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
})
|
||||
|
||||
// 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 }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
|
||||
switch status {
|
||||
case .loaded:
|
||||
if isPendingAsset {
|
||||
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
self.playerItemObserver.startObserving(item: currentItem)
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
|
||||
}
|
||||
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:
|
||||
if isPendingAsset {
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
self._pendingAsset = nil
|
||||
}
|
||||
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) {
|
||||
_initialTime = initialTime
|
||||
self.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()
|
||||
|
||||
self._pendingAsset?.cancelLoading()
|
||||
self._pendingAsset = nil
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -274,44 +430,40 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
_state = .idle
|
||||
}
|
||||
else {
|
||||
self._state = .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:
|
||||
self._state = .buffering
|
||||
if self.asset != nil {
|
||||
self.state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
self.state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
if _playWhenReady && (_initialTime ?? 0) == 0 {
|
||||
self.play()
|
||||
}
|
||||
else if let initialTime = _initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
self.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
@@ -319,18 +471,26 @@ extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
// MARK: - AVPlayerTimeObserverDelegate
|
||||
|
||||
func audioDidStart() {
|
||||
self._state = .playing
|
||||
state = .playing
|
||||
}
|
||||
|
||||
func timeEvent(time: CMTime) {
|
||||
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemFailedToPlayToEndTime() {
|
||||
playbackFailed(error: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
delegate?.AVWrapperItemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
func itemPlaybackStalled() {
|
||||
delegate?.AVWrapperItemPlaybackStalled()
|
||||
}
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
delegate?.AVWrapperItemDidPlayToEndTime()
|
||||
@@ -339,15 +499,19 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
if (playbackLikelyToKeepUp && state != .playing) {
|
||||
state = .ready
|
||||
}
|
||||
}
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveTimedMetadata: metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,19 @@ import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
protocol AVPlayerWrapperDelegate: class {
|
||||
protocol AVPlayerWrapperDelegate: AnyObject {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol: class {
|
||||
protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var state: AVPlayerWrapperState { 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 }
|
||||
@@ -23,6 +27,7 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var playbackError: AudioPlayerError.PlaybackError? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
@@ -37,7 +42,6 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -48,9 +52,16 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -66,23 +66,23 @@ public class DefaultAudioItem: AudioItem {
|
||||
}
|
||||
|
||||
public func getSourceUrl() -> String {
|
||||
return audioUrl
|
||||
audioUrl
|
||||
}
|
||||
|
||||
public func getArtist() -> String? {
|
||||
return artist
|
||||
artist
|
||||
}
|
||||
|
||||
public func getTitle() -> String? {
|
||||
return title
|
||||
title
|
||||
}
|
||||
|
||||
public func getAlbumTitle() -> String? {
|
||||
return albumTitle
|
||||
albumTitle
|
||||
}
|
||||
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
sourceType
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
@@ -97,17 +97,17 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
|
||||
self.pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
pitchAlgorithmType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
public var initialTime: TimeInterval
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.initialTime = 0.0
|
||||
initialTime = 0.0
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
}
|
||||
|
||||
public func getInitialTime() -> TimeInterval {
|
||||
return initialTime
|
||||
initialTime
|
||||
}
|
||||
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
public var options: [String: Any]
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.options = [:]
|
||||
options = [:]
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
}
|
||||
|
||||
public func getAssetOptions() -> [String: Any] {
|
||||
return options
|
||||
options
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,221 +11,247 @@ import MediaPlayer
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
var wrapper: AVPlayerWrapperProtocol {
|
||||
return _wrapper
|
||||
}
|
||||
|
||||
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public let event = EventHolder()
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
return _currentItem
|
||||
}
|
||||
|
||||
|
||||
private(set) var currentItem: AudioItem?
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
public var remoteCommands: [RemoteCommand] = []
|
||||
|
||||
|
||||
public var remoteCommands: [RemoteCommand] = [] {
|
||||
didSet {
|
||||
if let item = currentItem {
|
||||
self.enableRemoteCommands(forItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
public var playbackError: AudioPlayerError.PlaybackError? {
|
||||
wrapper.playbackError
|
||||
}
|
||||
|
||||
/**
|
||||
The elapsed playback time of the current item.
|
||||
*/
|
||||
public var currentTime: Double {
|
||||
return wrapper.currentTime
|
||||
wrapper.currentTime
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The duration of the current AudioItem.
|
||||
*/
|
||||
public var duration: Double {
|
||||
return wrapper.duration
|
||||
wrapper.duration
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
return wrapper.bufferedPosition
|
||||
wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
public var playerState: AudioPlayerState {
|
||||
return wrapper.state
|
||||
wrapper.state
|
||||
}
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
|
||||
/**
|
||||
Whether the player should start playing automatically when the item is ready.
|
||||
*/
|
||||
public var playWhenReady: Bool {
|
||||
get { wrapper.playWhenReady }
|
||||
set {
|
||||
wrapper.playWhenReady = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
|
||||
/**
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
|
||||
|
||||
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 { return wrapper.bufferDuration }
|
||||
set { _wrapper.bufferDuration = newValue }
|
||||
get { wrapper.bufferDuration }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to decide how often the player should call the delegate with time progress events.
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
get { wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { _wrapper.volume = newValue }
|
||||
get { wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { return wrapper.isMuted }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
get { wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0
|
||||
public var rate: Float {
|
||||
get { return _rate }
|
||||
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._wrapper = AVPlayerWrapper()
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self._wrapper.delegate = self
|
||||
|
||||
wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Player Actions
|
||||
|
||||
|
||||
/**
|
||||
Load an AudioItem into the manager.
|
||||
|
||||
|
||||
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
- 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())
|
||||
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
currentItem = item
|
||||
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
}
|
||||
|
||||
wrapper.load(from: url,
|
||||
playWhenReady: playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions())
|
||||
|
||||
self._currentItem = item
|
||||
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
self.loadNowPlayingMetaValues()
|
||||
// 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() {
|
||||
self.wrapper.togglePlaying()
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
self.wrapper.play()
|
||||
wrapper.play()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
self.wrapper.pause()
|
||||
wrapper.pause()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
Stop playback
|
||||
*/
|
||||
public func stop() {
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
self.event.playbackEnd.emit(data: .playerStopped)
|
||||
let wasActive = wrapper.playbackActive
|
||||
wrapper.stop()
|
||||
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 {
|
||||
self.updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
self.wrapper.seek(to: 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]) {
|
||||
self.remoteCommandController.enable(commands: commands)
|
||||
remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
@@ -234,12 +260,21 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Syncs the current remoteCommands with the iOS command center.
|
||||
Can be used to update item states - e.g. like, dislike and bookmark.
|
||||
*/
|
||||
@available(*, deprecated, message: "Directly set .remoteCommands instead")
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
|
||||
/**
|
||||
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
|
||||
@@ -248,119 +283,137 @@ 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() {
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
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 {
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
return image
|
||||
})
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
} else {
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func reset() {
|
||||
self._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
|
||||
self.rate = _rate;
|
||||
fallthrough
|
||||
case .paused:
|
||||
default: break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .ready, .loading, .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
self.event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
self.event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
self.event.fail.emit(data: error)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
}
|
||||
self.event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
self.event.updateDuration.emit(data: duration)
|
||||
event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.event.receiveMetadata.emit(data: metadata)
|
||||
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: 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(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() {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
wrapper.state = .ended
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
AVWrapper(failedWithError: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
}
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,14 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
public protocol AudioSessionControllerDelegate: class {
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType)
|
||||
public enum InterruptionType: Equatable {
|
||||
case began
|
||||
case ended(shouldResume: Bool)
|
||||
}
|
||||
|
||||
public protocol AudioSessionControllerDelegate: AnyObject {
|
||||
func handleInterruption(type: InterruptionType)
|
||||
}
|
||||
|
||||
/**
|
||||
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
|
||||
@@ -30,7 +33,7 @@ public class AudioSessionController {
|
||||
True if another app is currently playing audio.
|
||||
*/
|
||||
public var isOtherAudioPlaying: Bool {
|
||||
return audioSession.isOtherAudioPlaying
|
||||
audioSession.isOtherAudioPlaying
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +49,7 @@ public class AudioSessionController {
|
||||
Set this to false to disable the behaviour.
|
||||
*/
|
||||
public var isObservingForInterruptions: Bool {
|
||||
get {
|
||||
return _isObservingForInterruptions
|
||||
}
|
||||
get { _isObservingForInterruptions }
|
||||
set {
|
||||
if newValue == _isObservingForInterruptions {
|
||||
return
|
||||
@@ -112,7 +113,19 @@ public class AudioSessionController {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.handleInterruption(type: type)
|
||||
switch type {
|
||||
case .began:
|
||||
delegate?.handleInterruption(type: .began)
|
||||
case .ended:
|
||||
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: false))
|
||||
return
|
||||
}
|
||||
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
|
||||
@unknown default: return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,15 +10,23 @@ import MediaPlayer
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
public typealias StateChangeEventData = (AudioPlayerState)
|
||||
public typealias PlaybackEndEventData = (PlaybackEndedReason)
|
||||
public typealias SecondElapseEventData = (TimeInterval)
|
||||
public typealias FailEventData = (Error?)
|
||||
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = (Double)
|
||||
public typealias MetadataEventData = ([AVMetadataItem])
|
||||
public typealias PlayWhenReadyChangeData = Bool
|
||||
public typealias StateChangeEventData = AudioPlayerState
|
||||
public typealias PlaybackEndEventData = PlaybackEndedReason
|
||||
public typealias SecondElapseEventData = TimeInterval
|
||||
public typealias FailEventData = Error?
|
||||
public typealias SeekEventData = (seconds: Double, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = Double
|
||||
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
|
||||
@@ -90,7 +116,7 @@ extension AudioPlayer {
|
||||
|
||||
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
|
||||
self.listener = listener
|
||||
self.invoke = { [weak listener] (data: EventData) in
|
||||
invoke = { [weak listener] (data: EventData) in
|
||||
guard let _ = listener else {
|
||||
return false
|
||||
}
|
||||
@@ -102,44 +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()
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
return invoker.invoke(data)
|
||||
})
|
||||
self.invokersSemaphore.signal()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter { $0.invoke(data) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,62 +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 var _infoCenter: NowPlayingInfoCenter
|
||||
private var _info: [String: Any] = [:]
|
||||
|
||||
var infoCenter: NowPlayingInfoCenter {
|
||||
return _infoCenter
|
||||
}
|
||||
|
||||
var info: [String: Any] {
|
||||
return _info
|
||||
}
|
||||
private(set) var infoCenter: NowPlayingInfoCenter
|
||||
private(set) var info: [String: Any] = [:]
|
||||
|
||||
public required init() {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = MPNowPlayingInfoCenter.default()
|
||||
infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = dispatchQueue
|
||||
self._infoCenter = infoCenter
|
||||
infoQueue = dispatchQueue
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = infoCenter
|
||||
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.info[keyValue.getKey()] = keyValue.getValue()
|
||||
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.info = [:]
|
||||
self.infoCenter.nowPlayingInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ public protocol NowPlayingInfoControllerProtocol {
|
||||
|
||||
func set(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func clear()
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: class {
|
||||
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
|
||||
func itemDidPlayToEndTime()
|
||||
func itemFailedToPlayToEndTime()
|
||||
func itemPlaybackStalled()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,13 +69,34 @@ class AVPlayerItemNotificationObserver {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
|
||||
object: observingItem
|
||||
)
|
||||
self.observingItem = nil
|
||||
self.isObserving = false
|
||||
isObserving = false
|
||||
}
|
||||
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
delegate?.itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
|
||||
@objc private func itemFailedToPlayToEndTime() {
|
||||
delegate?.itemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
@objc private func itemPlaybackStalled() {
|
||||
delegate?.itemPlaybackStalled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,21 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerItemObserverDelegate: class {
|
||||
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: [AVMetadataItem])
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
|
||||
}
|
||||
|
||||
@@ -29,11 +33,12 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
private let metadataOutput: AVPlayerItemMetadataOutput
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
|
||||
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
@@ -41,6 +46,13 @@ class AVPlayerItemObserver: NSObject {
|
||||
private(set) weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
override init() {
|
||||
metadataOutput = AVPlayerItemMetadataOutput()
|
||||
super.init()
|
||||
|
||||
metadataOutput.setDelegate(self, queue: main)
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
@@ -51,12 +63,13 @@ class AVPlayerItemObserver: NSObject {
|
||||
- parameter item: The player item to observe.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
self.stopObservingCurrentItem()
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
stopObservingCurrentItem()
|
||||
isObserving = true
|
||||
observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.add(metadataOutput)
|
||||
}
|
||||
|
||||
func stopObservingCurrentItem() {
|
||||
@@ -65,8 +78,9 @@ class AVPlayerItemObserver: NSObject {
|
||||
}
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
|
||||
self.isObserving = false
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
|
||||
observingItem.remove(metadataOutput)
|
||||
isObserving = false
|
||||
self.observingItem = nil
|
||||
}
|
||||
|
||||
@@ -79,21 +93,27 @@ class AVPlayerItemObserver: NSObject {
|
||||
switch observedKeyPath {
|
||||
case AVPlayerItemKeyPath.duration:
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.timedMetadata:
|
||||
if let metadata = change?[.newKey] as? [AVMetadataItem] {
|
||||
self.delegate?.item(didReceiveMetadata: metadata)
|
||||
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
|
||||
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
default: break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerObserverDelegate: class {
|
||||
protocol AVPlayerObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the AVPlayer.status changes.
|
||||
@@ -20,90 +20,96 @@ protocol AVPlayerObserverDelegate: class {
|
||||
Called when the AVPlayer.timeControlStatus changes.
|
||||
*/
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Observing an AVPlayers status changes.
|
||||
*/
|
||||
class AVPlayerObserver: NSObject {
|
||||
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
|
||||
|
||||
private struct AVPlayerKeyPath {
|
||||
static let status = #keyPath(AVPlayer.status)
|
||||
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
|
||||
}
|
||||
|
||||
|
||||
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
|
||||
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
|
||||
private(set) var isObserving: Bool = false
|
||||
|
||||
|
||||
weak var delegate: AVPlayerObserverDelegate?
|
||||
weak var player: AVPlayer? {
|
||||
willSet {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start receiving events from this observer.
|
||||
*/
|
||||
func startObserving() {
|
||||
if (isObserving) { return };
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
self.stopObserving()
|
||||
self.isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
isObserving = true
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.status,
|
||||
options: statusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.timeControlStatus,
|
||||
options: timeControlStatusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func stopObserving() {
|
||||
guard let player = player, isObserving else {
|
||||
return
|
||||
}
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
self.isObserving = false
|
||||
isObserving = false
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch observedKeyPath {
|
||||
|
||||
case AVPlayerKeyPath.status:
|
||||
self.handleStatusChange(change)
|
||||
|
||||
handleStatusChange(change)
|
||||
|
||||
case AVPlayerKeyPath.timeControlStatus:
|
||||
self.handleTimeControlStatusChange(change)
|
||||
|
||||
handleTimeControlStatusChange(change)
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.Status
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
status = .unknown
|
||||
}
|
||||
delegate?.player(statusDidChange: status)
|
||||
}
|
||||
|
||||
|
||||
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.TimeControlStatus
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
@@ -111,5 +117,4 @@ class AVPlayerObserver: NSObject {
|
||||
delegate?.player(didChangeTimeControlStatus: status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerTimeObserverDelegate: class {
|
||||
protocol AVPlayerTimeObserverDelegate: AnyObject {
|
||||
func audioDidStart()
|
||||
func timeEvent(time: CMTime)
|
||||
}
|
||||
@@ -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,225 +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
|
||||
|
||||
private var _items: [T] = [] {
|
||||
didSet {
|
||||
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
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.
|
||||
*/
|
||||
public var items: [T] {
|
||||
return _items
|
||||
}
|
||||
|
||||
public var nextItems: [T] {
|
||||
guard _currentIndex + 1 < _items.count else {
|
||||
return []
|
||||
}
|
||||
return Array(_items[_currentIndex + 1..<_items.count])
|
||||
}
|
||||
|
||||
public var previousItems: [T] {
|
||||
if (_currentIndex == 0) {
|
||||
return []
|
||||
}
|
||||
return Array(_items[0..<_currentIndex])
|
||||
}
|
||||
|
||||
private var _currentIndex: Int = 0 {
|
||||
private(set) var items: [T] = [] {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
|
||||
return synchronize {
|
||||
if oldValue.count == 0 && items.count > 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return _currentIndex
|
||||
|
||||
public var nextItems: [T] {
|
||||
return synchronize {
|
||||
return currentIndex == -1 || currentIndex == items.count - 1
|
||||
? []
|
||||
: Array(items[currentIndex + 1..<items.count])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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]) {
|
||||
_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 && _items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
|
||||
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)
|
||||
}
|
||||
|
||||
_items.insert(contentsOf: items, at: index)
|
||||
if (_currentIndex >= index) { _currentIndex = _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 = _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 {
|
||||
self.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()
|
||||
}
|
||||
|
||||
self._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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,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,218 +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? {
|
||||
return queueManager.current
|
||||
queue.current
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return queueManager.currentIndex
|
||||
queue.currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
Stops the player and clears the queue.
|
||||
*/
|
||||
public override func stop() {
|
||||
super.stop()
|
||||
self.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] {
|
||||
return queueManager.items
|
||||
queue.items
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
return queueManager.previousItems
|
||||
queue.previousItems
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
return 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 self.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 self.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 {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
|
||||
} catch APError.QueueError.noNextItem {
|
||||
if repeatMode == .queue {
|
||||
try jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
} else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
} catch {
|
||||
throw error
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Step to the previous item in the queue.
|
||||
*/
|
||||
public func previous() throws {
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
let previousItem = try queueManager.previous()
|
||||
try self.load(item: previousItem, playWhenReady: repeatMode != .track)
|
||||
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: `APError.QueueError`
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
public func removeItem(at index: Int) throws {
|
||||
try queueManager.removeItem(at: index)
|
||||
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: Wether the item should start playing when ready. Default is `true`.
|
||||
- throws: `APError`
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
- throws: `AudioPlayerError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
|
||||
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)
|
||||
let item = try queueManager.jump(to: index)
|
||||
try self.load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Move an item in the queue from one position to another.
|
||||
|
||||
|
||||
- parameter fromIndex: The index of the item to move.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
|
||||
public func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
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: try? self.next()
|
||||
case .track:
|
||||
seek(to: 0)
|
||||
play()
|
||||
case .queue:
|
||||
do {
|
||||
try self.next()
|
||||
} 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 }
|
||||
self.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() {
|
||||
self.event.queueIndex.emit(data: (nil, 0))
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public struct FeedbackCommand: RemoteCommandProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCommand {
|
||||
public enum RemoteCommand: CustomStringConvertible {
|
||||
|
||||
case play
|
||||
|
||||
@@ -128,6 +128,23 @@ public enum RemoteCommand {
|
||||
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
|
||||
|
||||
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .play: return "play"
|
||||
case .pause: return "pause"
|
||||
case .stop: return "stop"
|
||||
case .togglePlayPause: return "togglePlayPause"
|
||||
case .next: return "nextTrack"
|
||||
case .previous: return "previousTrack"
|
||||
case .changePlaybackPosition: return "changePlaybackPosition"
|
||||
case .skipForward(_): return "skipForward"
|
||||
case .skipBackward(_): return "skipBackward"
|
||||
case .like(_, _, _): return "like"
|
||||
case .dislike(_, _, _): return "dislike"
|
||||
case .bookmark(_, _, _): return "bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All values in an array for convenience.
|
||||
|
||||
@@ -19,31 +19,34 @@ public class RemoteCommandController {
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
var commandTargetPointers: [String: Any] = [:]
|
||||
|
||||
private var enabledCommands: [RemoteCommand] = []
|
||||
|
||||
/**
|
||||
Create a new RemoteCommandController.
|
||||
|
||||
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
|
||||
*/
|
||||
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
|
||||
self.center = remoteCommandCenter
|
||||
center = remoteCommandCenter
|
||||
}
|
||||
|
||||
internal func enable(commands: [RemoteCommand]) {
|
||||
self.disable(commands: RemoteCommand.all())
|
||||
commands.forEach { (command) in
|
||||
self.enable(command: command)
|
||||
let commandsToDisable = enabledCommands.filter { command in
|
||||
!commands.contains(where: { $0.description == command.description })
|
||||
}
|
||||
|
||||
enabledCommands = commands
|
||||
commands.forEach { self.enable(command: $0) }
|
||||
disable(commands: commandsToDisable)
|
||||
}
|
||||
|
||||
internal func disable(commands: [RemoteCommand]) {
|
||||
commands.forEach { (command) in
|
||||
self.disable(command: command)
|
||||
}
|
||||
commands.forEach { self.disable(command: $0) }
|
||||
}
|
||||
|
||||
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
center[keyPath: command.commandKeyPath].isEnabled = true
|
||||
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
|
||||
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
|
||||
}
|
||||
|
||||
@@ -92,21 +95,21 @@ public class RemoteCommandController {
|
||||
|
||||
// MARK: - Handlers
|
||||
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
|
||||
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
|
||||
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
|
||||
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
|
||||
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
|
||||
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
|
||||
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = handleSkipBackwardDefault
|
||||
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = handleChangePlaybackPositionCommandDefault
|
||||
public lazy var handleNextTrackCommand: RemoteCommandHandler = handleNextTrackCommandDefault
|
||||
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = handlePreviousTrackCommandDefault
|
||||
public lazy var handleLikeCommand: RemoteCommandHandler = handleLikeCommandDefault
|
||||
public lazy var handleDislikeCommand: RemoteCommandHandler = handleDislikeCommandDefault
|
||||
public lazy var handleBookmarkCommand: RemoteCommandHandler = handleBookmarkCommandDefault
|
||||
|
||||
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -114,7 +117,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -122,7 +125,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.stop()
|
||||
return .success
|
||||
}
|
||||
@@ -130,7 +133,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -140,7 +143,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -150,7 +153,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -159,7 +162,7 @@ public class RemoteCommandController {
|
||||
|
||||
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: event.positionTime)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -167,57 +170,37 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
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