Compare commits

...

66 Commits

Author SHA1 Message Date
dcvz 77fb2b88d3 Bump version to 1.0.0-rc.9 2023-09-22 10:21:49 +02:00
dcvz 5ff8c9dffc Use async loading of metadata asset keys 2023-09-22 10:13:14 +02:00
dcvz 077d4b1d53 Bump version to 1.0.0-rc.8 2023-09-18 17:33:59 +02:00
dcvz 05322d9887 Fix rate updates in notification 2023-09-18 17:22:08 +02:00
dcvz 645b7bc8e7 Bump version to 1.0.0-rc.6 2023-09-07 11:45:34 +02:00
Hai Phung N.T e64e658b3b Timer is not showing properly from the notification center (#60) 2023-09-04 20:43:16 +02:00
Kirill Zyusko bf8e54e6a6 fix: repeat mode (pt. 2) (#62) 2023-09-04 20:41:36 +02:00
dcvz ed9fe280db Bump version to 1.0.0-rc.6 2023-08-15 11:20:42 +02:00
David Chavez 1148a6c28b chore(metadata): Handle more metadata types (#58) 2023-08-15 11:15:03 +02:00
David Chavez 9b6dcff4e2 Bump version to 1.0.0-rc.5 2023-07-25 18:12:26 +02:00
David Chavez bfe5851dc4 fix(replay): replay at bottom of stack 2023-07-25 18:10:27 +02:00
David Chavez 7ffa9b0113 chore(tests): fix comparsion approximations 2023-07-25 12:16:13 +02:00
Jonathan Puckey ebec7afccd Fix/race conditions (#55) 2023-07-18 12:23:04 +02:00
David Chavez 0fa786a91c Bump version to 1.0.0-rc.4 2023-06-20 14:04:05 +02:00
Jakub Bogacki 8fb5c66820 Fix QueuedAudioPlayer.currentItem.getter crash (#48)
Co-authored-by: David Chavez <david@dcvz.io>
2023-06-20 14:02:56 +02:00
David Chavez 42693b6dfb Bump version to 1.0.0-rc.3 2023-03-27 10:02:33 +02:00
Jonathan Puckey 348dcc17f7 Fix player state not becoming paused after loading (#46)
- fix player state not becoming .paused after .loading
- change tests to reflect the correct result
- set async defaults timeout to 10 seconds & polling to 100ms
- refactor AudioPlayerTests to use PlayerStateEventListener, quick & nimble
2023-03-27 09:57:51 +02:00
Jonathan Puckey b10aea494f Fix/queue manager (#45)
- avoid calling delegate?.onSkippedToSameCurrentItem() when the current index becomes one less due to a track before it being removed (the root cause of what fix: #42 - update current item correctly upon removal #43 fixes)
- avoid calling onCurrentItemChanged unnecessarily in skip
- move onSkippedToSameCurrentItem() call into skip & jump
- fix a few tests
2023-03-27 09:56:44 +02:00
David Chavez cbbbd57397 Revert "Fix removing items from queue other than the current track (#41)"
This reverts commit a270b3b232.
2023-03-27 09:56:09 +02:00
Christian Duvholt a270b3b232 Fix removing items from queue other than the current track (#41)
Co-authored-by: Christian Duvholt <christian.duvholt@iterate.no>
2023-03-27 09:51:50 +02:00
David Chavez 3cac61fe8f Bump version to 1.0.0-rc.2 2023-02-21 15:54:26 +01:00
Jonathan Puckey 7870d3bba6 Fix seeking within a large file over http & updating of playWhenReady to external pause (#40) 2023-02-20 19:07:38 +01:00
Alexey Strelkov 4c891bcdc6 Fix installation instructions via cocoapods (#32) 2023-02-10 10:33:42 +01:00
Philip Su 9e114360ec fix: update README to not refer to pod install for Example (#35)
The sample project doesn't have a Podfile, and it also seems to build/run just fine without `pod install` after cloning the repo. So I've updated the README to reflect that.
2023-02-10 10:32:28 +01:00
David Chavez f2c9a272d9 Bump version to 1.0.0-rc.1 2023-01-24 15:57:13 +01:00
Jonathan Puckey e41bb22a48 Feature: Queue improvements (#28) 2023-01-24 15:50:48 +01:00
David Chavez 23fdb9b9db Release 0.15.3 2022-09-14 15:02:30 +02:00
Jonathan Puckey 24c19aa661 Remove unnecessary buffer settings from tests. (#26)
As expected, the tests run successfully without these set too. See https://github.com/doublesymmetry/react-native-track-player/pull/1695
2022-09-06 09:15:36 +02:00
Jonathan Puckey 38429c6ca8 Reset AVPlayerWrapper on failure to load pending asset (#25)
See https://github.com/doublesymmetry/react-native-track-player/issues/1538
2022-09-06 09:13:38 +02:00
Jonathan Puckey 72f9c5d147 Fix order of AVPlayerWrapperState.state (#21) 2022-09-06 09:02:45 +02:00
David Chavez bd93898809 fix(tests): run workflows on PRs (#27) 2022-09-06 08:58:59 +02:00
David Chavez 8276f38b1b Release 0.15.2 2022-05-07 21:14:56 +02:00
David Akpan fcd5790e1e fix/ios-hls-live-duration (#18) 2022-05-07 08:59:17 +02:00
Jacob Spizziri ead7c0962e fix(audioplayer): fix loadArtwork method to unset artwork value if no image is given (#17)
https://github.com/doublesymmetry/react-native-track-player/issues/1511
2022-04-30 00:51:14 +02:00
David Chavez 7ff34271e8 Release 0.15.1 2022-04-22 23:11:59 +02:00
David Chavez 4f7a5b02a6 Fix: Bug - repeat mode and queue index event (#16) 2022-04-22 23:11:01 +02:00
David Chavez af803339dc More syntax updates and simplification 2022-04-03 13:16:43 +02:00
David Chavez a5bf6eb1dd Use timeDomain as default audioTimePitchAlgorithm 2022-04-03 12:35:30 +02:00
David Chavez 5e0c27b990 More syntax improvements 2022-04-03 12:24:18 +02:00
David Chavez 6079234942 More syntax updates 2022-04-03 12:13:39 +02:00
David Chavez e74b5ffe4d Syntax improvements 2022-04-03 11:49:23 +02:00
David Chavez 92554a187c Release 0.15.0 2022-04-01 23:54:08 +02:00
David Chavez 473651f357 Support mp3 embedded chapters 2022-04-01 23:47:46 +02:00
David Chavez db2f3e9af7 Remove obsolete code 2022-04-01 23:22:26 +02:00
David Chavez a9f831a258 Fix bug in addItems at index and add tests 2022-04-01 21:18:52 +02:00
David Chavez cc3840d81e Fix next/previous with repeat modes 2022-04-01 20:47:54 +02:00
David Chavez 5307090ea3 Replace deprecated “timedMetadata" KVO 2022-04-01 17:47:57 +02:00
David Chavez bdaee8b18f Extract more information from interruptions 2022-04-01 00:14:47 +02:00
David Chavez 84d359bc4f Update README.md 2022-02-24 09:14:36 +01:00
David Chavez 40ea7ad2f9 Release 0.14.7 2022-02-24 08:49:31 +01:00
David Chavez f2f1c1236c Add tests for new seek improvements 2022-02-24 08:48:54 +01:00
Terkel a75f0d0201 fix: make moveItem public and accessible from outside the class (#9) 2022-02-23 21:40:39 +01:00
Jacob Spizziri 9e4e7f6807 fix(seek): fix an issue causing seek to fail if called immediatly after load (#11) 2022-02-23 21:27:38 +01:00
David Chavez dbd3b03989 Release 0.14.6 2021-11-06 14:38:13 +01:00
David Chavez 7e19604df7 Create LICENSE (#5)
* Create LICENSE

* Update LICENSE
2021-11-06 14:29:06 +01:00
David Chavez 481130dc58 Release 0.14.5 2021-10-25 14:08:31 +02:00
David Chavez 300b34afa3 Do not emit paused state when changing tracks 2021-10-25 14:08:01 +02:00
David Chavez da3af0e9db Release 0.14.4 2021-09-28 10:58:23 +02:00
David Chavez d9eb313c1b Deprecate syncRemoteCommandsWithCommandCenter 2021-09-28 10:57:36 +02:00
David Chavez cca7f68da4 Increase deployment target for Test Target 2021-09-28 10:12:22 +02:00
David Chavez 7ed74b80ec Release 0.14.2 2021-09-28 10:04:01 +02:00
David Chavez 2773e4bfec Trigger skip and jump events only when actually taking action 2021-09-28 09:57:24 +02:00
David Chavez 77dc8f4ff1 Fix flickering elapsed time on a lock screen after pause 2021-09-28 09:41:04 +02:00
David Chavez accdf2c00c Rename exposed SPM package name 2021-09-28 09:31:31 +02:00
David Chavez 542d3a5764 Remove syncRemoteCommandsWithCommandCenter
Removed in favor of a didSet on remoteCommands property
2021-09-28 09:28:14 +02:00
David Chavez 4131e54f3e Create FUNDING.yml 2021-09-28 09:19:46 +02:00
39 changed files with 3506 additions and 1628 deletions
+12
View File
@@ -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']
+9 -2
View File
@@ -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 }}
+29 -23
View File
@@ -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;
};
+11 -5
View File
@@ -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)
}
}
}
+66 -60
View File
@@ -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...")
}
}
}
}
}
+22 -4
View File
@@ -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)?
+73 -10
View File
@@ -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)
}
+556 -177
View File
@@ -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())
}
}
}
+2 -2
View File
@@ -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
+29 -3
View File
@@ -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.
+42
View File
@@ -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
View File
@@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "SwiftAudio",
name: "SwiftAudioEx",
platforms: [.iOS(.v11)],
products: [
.library(
+8 -5
View File
@@ -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
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.14.2'
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.
-28
View File
@@ -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
}
+12 -13
View File
@@ -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
}
}
+223 -178
View File
@@ -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())
@@ -239,15 +265,16 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
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
@@ -256,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
}
}
}
+45 -35
View File
@@ -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
}
+245 -139
View File
@@ -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()
}
}
}
+127 -118
View File
@@ -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)
}
}
@@ -27,7 +27,7 @@ public class 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]) {
@@ -35,20 +35,13 @@ public class RemoteCommandController {
!commands.contains(where: { $0.description == command.description })
}
self.enabledCommands = commands
commands.forEach { (command) in
self.enable(command: command)
}
commandsToDisable.forEach { (command) in
self.disable(command: command)
}
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) {
@@ -102,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
}
@@ -124,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
}
@@ -132,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
}
@@ -140,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
}
@@ -150,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
}
@@ -160,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
}
@@ -169,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
}
@@ -177,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
}
}