Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b506bebab | |||
| ea82b81ed9 | |||
| 03c988e8b1 | |||
| 2424550401 | |||
| 5d8b3f2be5 | |||
| e1999c935e | |||
| fd8290c537 | |||
| 7fb762db9c | |||
| 81ce63752d | |||
| c03c83096e | |||
| 0c87d2479e | |||
| 98f3646e84 | |||
| 9ebbd99230 | |||
| 77fb2b88d3 | |||
| 5ff8c9dffc | |||
| 077d4b1d53 | |||
| 05322d9887 | |||
| 645b7bc8e7 | |||
| e64e658b3b | |||
| bf8e54e6a6 | |||
| ed9fe280db | |||
| 1148a6c28b | |||
| 9b6dcff4e2 | |||
| bfe5851dc4 | |||
| 7ffa9b0113 | |||
| ebec7afccd | |||
| 0fa786a91c | |||
| 8fb5c66820 | |||
| 42693b6dfb | |||
| 348dcc17f7 | |||
| b10aea494f | |||
| cbbbd57397 | |||
| a270b3b232 | |||
| 3cac61fe8f | |||
| 7870d3bba6 | |||
| 4c891bcdc6 | |||
| 9e114360ec | |||
| f2c9a272d9 | |||
| e41bb22a48 |
@@ -9,19 +9,15 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: macos-latest
|
||||
runs-on: blaze/macos-14
|
||||
strategy:
|
||||
matrix:
|
||||
destination:
|
||||
[
|
||||
'platform=iOS Simulator,name=iPhone 12 Pro',
|
||||
]
|
||||
destination: ["platform=iOS Simulator,name=iPhone 15 Pro"]
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Tests
|
||||
run: |-
|
||||
cd Example
|
||||
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
|
||||
xcodebuild test -scheme SwiftAudioEx -destination "${destination}" -enableCodeCoverage YES
|
||||
env:
|
||||
destination: ${{ matrix.destination }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -12,78 +12,20 @@
|
||||
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; };
|
||||
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; };
|
||||
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; };
|
||||
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */; };
|
||||
0708ED702116E89900EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
|
||||
0708ED722116E91D00EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
|
||||
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */; };
|
||||
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
|
||||
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
|
||||
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
|
||||
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
|
||||
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
|
||||
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
|
||||
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
|
||||
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */; };
|
||||
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */; };
|
||||
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */; };
|
||||
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */; };
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575820668B020002C6A1 /* QueueManagerTests.swift */; };
|
||||
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.swift */; };
|
||||
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */; };
|
||||
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */; };
|
||||
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */; };
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 607FACC81AFB9204008FA782 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 607FACCF1AFB9204008FA782;
|
||||
remoteInfo = SwiftAudio;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
070713062067EB4F00F789B3 /* Double + Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Extensions.swift"; sourceTree = "<group>"; };
|
||||
070713082067EFFB00F789B3 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
|
||||
0707130A2067F2E000F789B3 /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
|
||||
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QueueTableViewCell.xib; sourceTree = "<group>"; };
|
||||
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionControllerTests.swift; sourceTree = "<group>"; };
|
||||
0708ED6F2116E89900EB29BD /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
|
||||
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerTests.swift; sourceTree = "<group>"; };
|
||||
0708ED78211732F500EB29BD /* TestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestSound.m4a; sourceTree = "<group>"; };
|
||||
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ShortTestSound.m4a; sourceTree = "<group>"; };
|
||||
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
|
||||
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerTests.swift; sourceTree = "<group>"; };
|
||||
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.swift; sourceTree = "<group>"; };
|
||||
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
|
||||
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerEventTests.swift; sourceTree = "<group>"; };
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
|
||||
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
|
||||
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
|
||||
07756B68218A4E870023935E /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = "<group>"; };
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserverTests.swift; sourceTree = "<group>"; };
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedAudioPlayerTests.swift; sourceTree = "<group>"; };
|
||||
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoTests.swift; sourceTree = "<group>"; };
|
||||
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -91,11 +33,7 @@
|
||||
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -107,47 +45,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
607FACE21AFB9204008FA782 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */,
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0708ED712116E91300EB29BD /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */,
|
||||
0708ED6F2116E89900EB29BD /* Source.swift */,
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
|
||||
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
|
||||
0708ED78211732F500EB29BD /* TestSound.m4a */,
|
||||
);
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
07756B67218A4E640023935E /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
07756B68218A4E870023935E /* AudioSession.swift */,
|
||||
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
|
||||
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACC71AFB9204008FA782 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD21AFB9204008FA782 /* Example for SwiftAudio */,
|
||||
607FACE81AFB9204008FA782 /* Tests */,
|
||||
607FACD11AFB9204008FA782 /* Products */,
|
||||
9B05AA2F2660276400C7A389 /* Frameworks */,
|
||||
);
|
||||
@@ -157,7 +61,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */,
|
||||
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -189,36 +92,6 @@
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACE81AFB9204008FA782 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
07756B67218A4E640023935E /* Mocks */,
|
||||
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */,
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */,
|
||||
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
|
||||
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
|
||||
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
|
||||
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
|
||||
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
|
||||
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */,
|
||||
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */,
|
||||
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */,
|
||||
0708ED712116E91300EB29BD /* Source */,
|
||||
607FACE91AFB9204008FA782 /* Supporting Files */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACE91AFB9204008FA782 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACEA1AFB9204008FA782 /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9B05AA2F2660276400C7A389 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -250,29 +123,6 @@
|
||||
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
607FACE41AFB9204008FA782 /* SwiftAudio_Tests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Tests" */;
|
||||
buildPhases = (
|
||||
607FACE11AFB9204008FA782 /* Sources */,
|
||||
607FACE21AFB9204008FA782 /* Frameworks */,
|
||||
607FACE31AFB9204008FA782 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
607FACE71AFB9204008FA782 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SwiftAudio_Tests;
|
||||
packageProductDependencies = (
|
||||
9B05AA302660276400C7A389 /* Quick */,
|
||||
9B05AA322660276400C7A389 /* Nimble */,
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = Tests;
|
||||
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -285,7 +135,6 @@
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
@@ -293,12 +142,6 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 1020;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */;
|
||||
@@ -311,15 +154,12 @@
|
||||
);
|
||||
mainGroup = 607FACC71AFB9204008FA782;
|
||||
packageReferences = (
|
||||
9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */,
|
||||
9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */,
|
||||
);
|
||||
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
607FACCF1AFB9204008FA782 /* SwiftAudio_Example */,
|
||||
607FACE41AFB9204008FA782 /* SwiftAudio_Tests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -331,26 +171,11 @@
|
||||
files = (
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
|
||||
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
|
||||
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */,
|
||||
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */,
|
||||
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
607FACE31AFB9204008FA782 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */,
|
||||
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */,
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */,
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -361,47 +186,14 @@
|
||||
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */,
|
||||
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */,
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
0708ED722116E91D00EB29BD /* Source.swift in Sources */,
|
||||
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */,
|
||||
070713092067EFFB00F789B3 /* AudioController.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
607FACE11AFB9204008FA782 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
|
||||
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */,
|
||||
0708ED702116E89900EB29BD /* Source.swift in Sources */,
|
||||
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */,
|
||||
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
|
||||
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
|
||||
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
|
||||
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */,
|
||||
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
|
||||
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
|
||||
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */,
|
||||
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */,
|
||||
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */,
|
||||
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
|
||||
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
|
||||
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
|
||||
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
607FACE71AFB9204008FA782 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 607FACCF1AFB9204008FA782 /* SwiftAudio_Example */;
|
||||
targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
@@ -530,15 +322,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,69 +341,21 @@
|
||||
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;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
607FACF31AFB9204008FA782 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
607FACF41AFB9204008FA782 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = HPNZWPB9JK;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -633,51 +377,9 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Tests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
607FACF31AFB9204008FA782 /* Debug */,
|
||||
607FACF41AFB9204008FA782 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Quick/Quick";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Quick/Nimble";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 9.2.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
9B05AA302660276400C7A389 /* Quick */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */;
|
||||
productName = Quick;
|
||||
};
|
||||
9B05AA322660276400C7A389 /* Nimble */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
|
||||
productName = Nimble;
|
||||
};
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "CwlCatchException",
|
||||
"repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CwlPreconditionTesting",
|
||||
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "02b7a39a99c4da27abe03cab2053a9034379639f",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nimble",
|
||||
"repositoryURL": "https://github.com/Quick/Nimble",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790",
|
||||
"version": "9.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Quick",
|
||||
"repositoryURL": "https://github.com/Quick/Quick",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6",
|
||||
"version": "4.0.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -17,10 +17,13 @@ class AudioController {
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Longing.mp3", artist: "David Chavez", title: "Longing", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Soul%20Searching.mp3", artist: "David Chavez", title: "Soul Searching (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Rhythm%20City%20(Demo).mp3", artist: "David Chavez", title: "Rhythm City (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://rntp.dev/example/hls/whip/playlist.m3u8", title: "Whip", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
]
|
||||
|
||||
init() {
|
||||
@@ -36,7 +39,10 @@ class AudioController {
|
||||
.changePlaybackPosition
|
||||
]
|
||||
try? audioSessionController.set(category: .playback)
|
||||
try? player.add(items: sources, playWhenReady: false)
|
||||
player.repeatMode = .queue
|
||||
DispatchQueue.main.async {
|
||||
self.player.add(items: self.sources)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,41 +26,37 @@ class ViewController: UIViewController {
|
||||
|
||||
private var isScrubbing: Bool = false
|
||||
private let controller = AudioController.shared
|
||||
private var lastLoadFailed: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
|
||||
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
|
||||
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
|
||||
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
|
||||
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
|
||||
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
|
||||
controller.player.event.fail.addListener(self, handlePlayerFailure)
|
||||
updateMetaData()
|
||||
handleAudioPlayerStateChange(data: controller.player.playerState)
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func togglePlay(_ sender: Any) {
|
||||
if !controller.audioSessionController.audioSessionIsActive {
|
||||
try? controller.audioSessionController.activateSession()
|
||||
}
|
||||
if lastLoadFailed, let item = controller.player.currentItem {
|
||||
lastLoadFailed = false
|
||||
errorLabel.isHidden = true
|
||||
try? controller.player.load(item: item, playWhenReady: true)
|
||||
}
|
||||
else {
|
||||
controller.player.togglePlaying()
|
||||
}
|
||||
controller.player.playWhenReady = playButton.currentTitle == "Play"
|
||||
}
|
||||
|
||||
@IBAction func previous(_ sender: Any) {
|
||||
try? controller.player.previous()
|
||||
controller.player.previous()
|
||||
}
|
||||
|
||||
@IBAction func next(_ sender: Any) {
|
||||
try? controller.player.next()
|
||||
controller.player.next()
|
||||
}
|
||||
|
||||
@IBAction func startScrubbing(_ sender: UISlider) {
|
||||
@@ -77,31 +73,56 @@ class ViewController: UIViewController {
|
||||
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
|
||||
}
|
||||
|
||||
func updateTimeValues() {
|
||||
// MARK: - Render
|
||||
|
||||
func renderTimeValues() {
|
||||
self.slider.maximumValue = Float(self.controller.player.duration)
|
||||
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
|
||||
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
|
||||
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
|
||||
}
|
||||
|
||||
func updateMetaData() {
|
||||
if let item = controller.player.currentItem {
|
||||
titleLabel.text = item.getTitle()
|
||||
artistLabel.text = item.getArtist()
|
||||
|
||||
func render() {
|
||||
let player = self.controller.player
|
||||
|
||||
// Render play button
|
||||
self.playButton.setTitle(
|
||||
!player.playWhenReady || player.playerState == .failed
|
||||
? "Play"
|
||||
: "Pause",
|
||||
for: .normal
|
||||
)
|
||||
|
||||
// Render metadata
|
||||
if let item = player.currentItem {
|
||||
self.titleLabel.text = item.getTitle()
|
||||
self.artistLabel.text = item.getArtist()
|
||||
item.getArtwork({ (image) in
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
|
||||
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
|
||||
}
|
||||
|
||||
func setErrorMessage(_ message: String) {
|
||||
self.loadIndicator.stopAnimating()
|
||||
errorLabel.isHidden = false
|
||||
errorLabel.text = message
|
||||
|
||||
// Render time values
|
||||
self.renderTimeValues()
|
||||
|
||||
// Render error label
|
||||
if (player.playerState == .failed) {
|
||||
self.errorLabel.isHidden = false
|
||||
self.errorLabel.text = "Playback failed."
|
||||
} else {
|
||||
self.errorLabel.text = ""
|
||||
self.errorLabel.isHidden = true
|
||||
}
|
||||
|
||||
// Render load indicator:
|
||||
if (
|
||||
(player.playerState == .loading || player.playerState == .buffering)
|
||||
&& self.controller.player.playWhenReady // Avoid showing indicator before user has pressed play
|
||||
) {
|
||||
self.loadIndicator.startAnimating()
|
||||
} else {
|
||||
self.loadIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
@@ -109,25 +130,17 @@ class ViewController: UIViewController {
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print("state=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.setPlayButtonState(forAudioPlayerState: data)
|
||||
switch data {
|
||||
case .loading:
|
||||
self.loadIndicator.startAnimating()
|
||||
self.updateMetaData()
|
||||
self.updateTimeValues()
|
||||
case .buffering:
|
||||
self.loadIndicator.startAnimating()
|
||||
case .ready:
|
||||
self.loadIndicator.stopAnimating()
|
||||
self.updateMetaData()
|
||||
self.updateTimeValues()
|
||||
case .playing, .paused, .idle:
|
||||
self.loadIndicator.stopAnimating()
|
||||
self.updateTimeValues()
|
||||
}
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
|
||||
print("playWhenReady=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
|
||||
print("playEndReason=\(data)")
|
||||
}
|
||||
@@ -135,7 +148,7 @@ class ViewController: UIViewController {
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,23 +159,11 @@ class ViewController: UIViewController {
|
||||
|
||||
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
|
||||
DispatchQueue.main.async {
|
||||
self.updateTimeValues()
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
|
||||
func handlePlayerFailure(data: AudioPlayer.FailEventData) {
|
||||
if let error = data as NSError? {
|
||||
if error.code == -1009 {
|
||||
lastLoadFailed = true
|
||||
DispatchQueue.main.async {
|
||||
self.setErrorMessage("Network disconnected. Please try again...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerItemNotificationObserverTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("A notification observer") {
|
||||
|
||||
var item: AVPlayerItem!
|
||||
var observer: AVPlayerItemNotificationObserver!
|
||||
|
||||
beforeEach {
|
||||
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer = AVPlayerItemNotificationObserver()
|
||||
}
|
||||
|
||||
context("when started observing") {
|
||||
beforeEach {
|
||||
observer.startObserving(item: item)
|
||||
}
|
||||
|
||||
it("should have an observed item") {
|
||||
expect(observer.observingItem).toNot(beNil())
|
||||
}
|
||||
|
||||
context("when ended observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
it("should have no observed item") {
|
||||
expect(observer.observingItem).to(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerItemObserverTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AVPlayerItemObserver") {
|
||||
var observer: AVPlayerItemObserver!
|
||||
beforeEach {
|
||||
observer = AVPlayerItemObserver()
|
||||
}
|
||||
describe("observed item") {
|
||||
context("when observing") {
|
||||
var item: AVPlayerItem!
|
||||
beforeEach {
|
||||
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer.startObserving(item: item)
|
||||
}
|
||||
|
||||
it("should exist") {
|
||||
expect(observer.observingItem).toEventuallyNot(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("observing status") {
|
||||
it("should not be observing") {
|
||||
expect(observer.isObserving).toEventuallyNot(beTrue())
|
||||
}
|
||||
context("when observing") {
|
||||
var item: AVPlayerItem!
|
||||
beforeEach {
|
||||
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer.startObserving(item: item)
|
||||
}
|
||||
it("should be observing") {
|
||||
expect(observer.isObserving).toEventually(beTrue())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
|
||||
var receivedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var updateDuration: ((_ duration: Double) -> Void)?
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
updateDuration?(duration)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
|
||||
|
||||
var status: AVPlayer.Status?
|
||||
var timeControlStatus: AVPlayer.TimeControlStatus?
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("A player observer") {
|
||||
|
||||
var player: AVPlayer!
|
||||
var observer: AVPlayerObserver!
|
||||
|
||||
beforeEach {
|
||||
player = AVPlayer()
|
||||
player.volume = 0.0
|
||||
observer = AVPlayerObserver()
|
||||
observer.player = player
|
||||
observer.delegate = self
|
||||
}
|
||||
|
||||
it("should not be observing") {
|
||||
expect(observer.isObserving).to(beFalse())
|
||||
}
|
||||
|
||||
context("when observing has started") {
|
||||
beforeEach {
|
||||
observer.startObserving()
|
||||
}
|
||||
|
||||
it("should be observing") {
|
||||
expect(observer.isObserving).toEventually(beTrue())
|
||||
}
|
||||
|
||||
context("when player has started") {
|
||||
beforeEach {
|
||||
player.replaceCurrentItem(with: AVPlayerItem(url: URL(fileURLWithPath: Source.path)))
|
||||
player.play()
|
||||
}
|
||||
|
||||
it("it should update the delegate") {
|
||||
expect(self.status).toEventuallyNot(beNil())
|
||||
expect(self.timeControlStatus).toEventuallyNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("when observing again") {
|
||||
beforeEach {
|
||||
observer.startObserving()
|
||||
}
|
||||
|
||||
it("should be observing") {
|
||||
expect(observer.isObserving).toEventually(beTrue())
|
||||
}
|
||||
}
|
||||
|
||||
context("when stopping observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.stopObserving()
|
||||
}
|
||||
|
||||
it("should not be observing") {
|
||||
expect(observer.isObserving).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
self.timeControlStatus = status
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerTimeObserverTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("AVPlayerTimeObserver") {
|
||||
|
||||
var player: AVPlayer!
|
||||
var observer: AVPlayerTimeObserver!
|
||||
|
||||
beforeEach {
|
||||
player = AVPlayer()
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
player.volume = 0
|
||||
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
|
||||
observer.player = player
|
||||
}
|
||||
|
||||
context("has started boundary time observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.registerForBoundaryTimeEvents()
|
||||
}
|
||||
|
||||
it("should have a boundary token") {
|
||||
expect(observer.boundaryTimeStartObserverToken).toNot(beNil())
|
||||
}
|
||||
|
||||
context("has ended boundary time observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.unregisterForBoundaryTimeEvents()
|
||||
}
|
||||
|
||||
it("should have no boundary token") {
|
||||
expect(observer.boundaryTimeStartObserverToken).to(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("has started periodic time observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.registerForPeriodicTimeEvents()
|
||||
}
|
||||
|
||||
it("should have a periodic token") {
|
||||
expect(observer.periodicTimeObserverToken).toNot(beNil())
|
||||
}
|
||||
|
||||
context("ended periodic time observing") {
|
||||
|
||||
beforeEach {
|
||||
observer.unregisterForPeriodicEvents()
|
||||
}
|
||||
|
||||
it("should have no periodic token") {
|
||||
expect(observer.periodicTimeObserverToken).to(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerEventTests: QuickSpec {
|
||||
|
||||
class EventListener {
|
||||
var handleEvent: ((Void)) -> Void = { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An event") {
|
||||
var event: AudioPlayer.Event<(Void)>!
|
||||
beforeEach {
|
||||
event = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
describe("its invokers") {
|
||||
|
||||
context("when adding a listener") {
|
||||
var listener: EventListener!
|
||||
beforeEach {
|
||||
listener = EventListener()
|
||||
event.addListener(listener, listener!.handleEvent)
|
||||
}
|
||||
|
||||
it("should have one element") {
|
||||
expect(event.invokers.count).toEventuallyNot(equal(0))
|
||||
}
|
||||
|
||||
context("then that listener is deinitialized and an an event is emitted") {
|
||||
beforeEach {
|
||||
listener = nil
|
||||
event.emit(data: ())
|
||||
}
|
||||
|
||||
it("should remove the invoker") {
|
||||
expect(event.invokers.count).toEventually(equal(0))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding multiple listeners") {
|
||||
var listeners: [EventListener]!
|
||||
|
||||
beforeEach {
|
||||
listeners = [0..<15].map {_ in
|
||||
let listener = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
return listener
|
||||
}
|
||||
}
|
||||
|
||||
it("should have several listeners") {
|
||||
expect(event.invokers.count).toEventually(equal(listeners.count))
|
||||
}
|
||||
|
||||
context("then removing one") {
|
||||
beforeEach {
|
||||
event.removeListener(listeners[listeners.count / 2])
|
||||
}
|
||||
|
||||
it("should have one less invoker") {
|
||||
expect(event.invokers.count).toEventually(equal(listeners.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)?
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
|
||||
}
|
||||
|
||||
deinit {
|
||||
audioPlayer?.event.stateChange.removeListener(self)
|
||||
audioPlayer?.event.seek.removeListener(self)
|
||||
audioPlayer?.event.secondElapse.removeListener(self)
|
||||
}
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
seekCompletion?()
|
||||
}
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
self.secondsElapse?(data)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioSessionControllerTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AudioSessionController") {
|
||||
let audioSessionController: AudioSessionController = AudioSessionController(audioSession: NonFailingAudioSession())
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
|
||||
context("when session is activated") {
|
||||
beforeEach {
|
||||
try? audioSessionController.activateSession()
|
||||
}
|
||||
|
||||
it("should be active") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beTrue())
|
||||
}
|
||||
|
||||
context("when deactivating session") {
|
||||
beforeEach {
|
||||
try? audioSessionController.deactivateSession()
|
||||
}
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its isObservingForInterruptions") {
|
||||
it("should be true") {
|
||||
expect(audioSessionController.isObservingForInterruptions).to(beTrue())
|
||||
}
|
||||
|
||||
context("when isObservingForInterruptions is set to false") {
|
||||
beforeEach {
|
||||
audioSessionController.isObservingForInterruptions = false
|
||||
}
|
||||
|
||||
it("should be false") {
|
||||
expect(audioSessionController.isObservingForInterruptions).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its delegate") {
|
||||
context("when a ended interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0),
|
||||
AVAudioSessionInterruptionOptionKey: 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.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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("An AudioSessionController with a failing AudioSession") {
|
||||
var audioSessionController: AudioSessionController!
|
||||
beforeEach {
|
||||
audioSessionController = AudioSessionController(audioSession: FailingAudioSession())
|
||||
}
|
||||
|
||||
context("when activated") {
|
||||
beforeEach {
|
||||
try? audioSessionController.activateSession()
|
||||
}
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
var interruptionType: InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,72 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoControllerTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
describe("An NowPlayingInfoController") {
|
||||
|
||||
var nowPlayingController: NowPlayingInfoController!
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: NowPlayingInfoCenter_Mock())
|
||||
}
|
||||
|
||||
describe("its info dictionary") {
|
||||
|
||||
context("when setting a value") {
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.info.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(nowPlayingController.info.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its info center") {
|
||||
|
||||
context("when setting a value") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
|
||||
class NowPlayingInfoTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AudioPlayer") {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var nowPlayingController: NowPlayingInfoController_Mock!
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController = NowPlayingInfoController_Mock()
|
||||
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
|
||||
audioPlayer.automaticallyUpdateNowPlayingInfo = true
|
||||
audioPlayer.volume = 0
|
||||
}
|
||||
|
||||
describe("its NowPlayingInfoController") {
|
||||
|
||||
context("when loading an AudioItem") {
|
||||
|
||||
var item: AudioItem!
|
||||
|
||||
beforeEach {
|
||||
item = Source.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should eventually be updated with meta data") {
|
||||
expect(nowPlayingController.getTitle()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getTitle()).toEventually(equal(item.getTitle()!))
|
||||
|
||||
expect(nowPlayingController.getArtist()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getArtist()).toEventually(equal(item.getArtist()!))
|
||||
|
||||
expect(nowPlayingController.getAlbumTitle()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getAlbumTitle()).toEventually(equal(item.getAlbumTitle()!))
|
||||
|
||||
expect(nowPlayingController.getArtwork()).toEventuallyNot(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("when playing an AudioItem") {
|
||||
|
||||
var item: AudioItem!
|
||||
|
||||
beforeEach {
|
||||
item = LongSource.getAudioItem()
|
||||
try? audioPlayer.load(item: item, playWhenReady: true)
|
||||
}
|
||||
|
||||
it("should eventually be updated with playback values") {
|
||||
expect(nowPlayingController.getRate()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getDuration()).toEventuallyNot(beNil())
|
||||
expect(nowPlayingController.getCurrentTime()).toEventuallyNot(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueManagerTests: QuickSpec {
|
||||
|
||||
let dummyItem = 0
|
||||
|
||||
let dummyItems: [Int] = [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("A QueueManager") {
|
||||
|
||||
var manager: QueueManager<Int>!
|
||||
|
||||
beforeEach {
|
||||
manager = QueueManager()
|
||||
}
|
||||
|
||||
describe("its current item") {
|
||||
|
||||
it("should be nil") {
|
||||
expect(manager.current).to(beNil())
|
||||
}
|
||||
|
||||
context("when one item is added") {
|
||||
beforeEach {
|
||||
manager.addItem(self.dummyItem)
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(manager.current).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should be the added item") {
|
||||
expect(manager.current).to(equal(self.dummyItem))
|
||||
}
|
||||
|
||||
context("then replaced") {
|
||||
beforeEach {
|
||||
manager.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should be the new item") {
|
||||
expect(manager.current).to(equal(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when replaced") {
|
||||
beforeEach {
|
||||
manager.replaceCurrentItem(with: 1)
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(manager.current).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("when mulitple items are added") {
|
||||
beforeEach {
|
||||
manager.addItems(self.dummyItems)
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(manager.current).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
describe("when adding at index") {
|
||||
context("adding item at index 0 when queue is empty") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([3], at: 0)
|
||||
expect(manager.current).to(equal(3))
|
||||
}
|
||||
}
|
||||
|
||||
context("adding item at index") {
|
||||
beforeEach {
|
||||
manager.addItems([3, 1])
|
||||
}
|
||||
|
||||
context("current [element count]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: manager.items.count)
|
||||
expect(manager.items.last).to(equal(5))
|
||||
}
|
||||
}
|
||||
|
||||
context("before the [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: 0)
|
||||
expect(manager.current).to(equal(3))
|
||||
expect(manager.currentIndex).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("after the [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.addItems([5], at: 1)
|
||||
expect(manager.current).to(equal(3))
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("at [current index]") {
|
||||
it("should add element successfully") {
|
||||
try manager.next()
|
||||
try manager.addItems([5], at: 1)
|
||||
expect(manager.current).to(equal(1))
|
||||
expect(manager.currentIndex).to(equal(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding one item") {
|
||||
|
||||
beforeEach {
|
||||
manager.addItem(self.dummyItem)
|
||||
}
|
||||
|
||||
it("should have an item in the queue") {
|
||||
expect(manager.items).notTo(beEmpty())
|
||||
}
|
||||
|
||||
context("then replacing the item") {
|
||||
beforeEach {
|
||||
manager.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should have replaced the current item") {
|
||||
expect(manager.current).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next") {
|
||||
|
||||
var nextItem: Int?
|
||||
|
||||
beforeEach {
|
||||
nextItem = try? manager.next()
|
||||
}
|
||||
|
||||
it("should not return") {
|
||||
expect(nextItem).to(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("then calling previous") {
|
||||
var previousItem: Int?
|
||||
|
||||
beforeEach {
|
||||
previousItem = try? manager.previous()
|
||||
}
|
||||
|
||||
it("should not return") {
|
||||
expect(previousItem).to(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("when adding multiple items") {
|
||||
|
||||
beforeEach {
|
||||
manager.addItems(self.dummyItems)
|
||||
}
|
||||
|
||||
it("should have items in the queue") {
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
}
|
||||
|
||||
it("should have the first item as a current item") {
|
||||
expect(manager.current).toNot(beNil())
|
||||
expect(manager.current).to(equal(self.dummyItems.first))
|
||||
}
|
||||
|
||||
it("should have next items") {
|
||||
expect(manager.nextItems).toNot(beNil())
|
||||
expect(manager.nextItems.count).to(equal(self.dummyItems.count - 1))
|
||||
}
|
||||
|
||||
context("then calling next") {
|
||||
var nextItem: Int?
|
||||
beforeEach {
|
||||
nextItem = try? manager.next()
|
||||
}
|
||||
|
||||
it("should return the next item") {
|
||||
expect(nextItem).toNot(beNil())
|
||||
expect(nextItem).to(equal(self.dummyItems[1]))
|
||||
}
|
||||
|
||||
it("should have next current item") {
|
||||
expect(manager.current).to(equal(self.dummyItems[1]))
|
||||
}
|
||||
|
||||
it("should have previous items") {
|
||||
expect(manager.previousItems).toNot(beNil())
|
||||
}
|
||||
|
||||
context("then calling previous") {
|
||||
var previousItem: Int?
|
||||
beforeEach {
|
||||
previousItem = try? manager.previous()
|
||||
}
|
||||
it("should return the first item") {
|
||||
expect(previousItem).toNot(beNil())
|
||||
expect(previousItem).to(equal(self.dummyItems.first))
|
||||
}
|
||||
it("should have the previous current item") {
|
||||
expect(manager.current).to(equal(self.dummyItems.first))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing previous items") {
|
||||
beforeEach {
|
||||
manager.removePreviousItems()
|
||||
}
|
||||
it("should have no previous items") {
|
||||
expect(manager.previousItems.count).to(equal(0))
|
||||
}
|
||||
it("should have current index zero") {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("adding more items") {
|
||||
var initialItemCount: Int!
|
||||
let newItems: [Int] = [10, 11, 12, 13]
|
||||
beforeEach {
|
||||
initialItemCount = manager.items.count
|
||||
try? manager.addItems(newItems, at: manager.items.endIndex - 1)
|
||||
}
|
||||
|
||||
it("should have more items") {
|
||||
expect(manager.items.count).to(equal(initialItemCount + newItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("adding more items at a smaller index than currentIndex") {
|
||||
var initialCurrentIndex: Int!
|
||||
let newItems: [Int] = [10, 11, 12, 13]
|
||||
beforeEach {
|
||||
initialCurrentIndex = manager.currentIndex
|
||||
try? manager.addItems(newItems, at: initialCurrentIndex)
|
||||
}
|
||||
|
||||
it("currentIndex should increase by number of new items") {
|
||||
expect(manager.currentIndex).to(equal(initialCurrentIndex + newItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Removal
|
||||
|
||||
context("then removing a item with index less than currentIndex") {
|
||||
beforeEach {
|
||||
var removed: Int?
|
||||
var initialCurrentIndex: Int!
|
||||
beforeEach {
|
||||
let _ = try? manager.jump(to: 3)
|
||||
initialCurrentIndex = manager.currentIndex
|
||||
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
|
||||
}
|
||||
|
||||
it("should remove an item") {
|
||||
expect(removed).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should decrement the currentIndex") {
|
||||
expect(manager.currentIndex).to(equal(initialCurrentIndex - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the second item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.removeItem(at: 1)
|
||||
}
|
||||
|
||||
it("should have one less item") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the last item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.removeItem(at: self.dummyItems.count - 1)
|
||||
}
|
||||
|
||||
it("should have one less item") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the current item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.removeItem(at: manager.currentIndex)
|
||||
}
|
||||
it("should not remove any items") {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing with too large index") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.removeItem(at: self.dummyItems.count)
|
||||
}
|
||||
|
||||
it("should not remove any items") {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing with too small index") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.removeItem(at: -1)
|
||||
}
|
||||
|
||||
it("should not remove any items") {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing upcoming items") {
|
||||
beforeEach {
|
||||
manager.removeUpcomingItems()
|
||||
}
|
||||
|
||||
it("should have no next items") {
|
||||
expect(manager.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Jumping
|
||||
|
||||
context("then jumping to the current item") {
|
||||
var error: Error?
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
do {
|
||||
item = try manager.jump(to: manager.currentIndex)
|
||||
}
|
||||
catch let err {
|
||||
error = err
|
||||
}
|
||||
}
|
||||
|
||||
it("should not return an item") {
|
||||
expect(item).to(beNil())
|
||||
}
|
||||
|
||||
it("should throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to the second item") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
try? jumped = manager.jump(to: 1)
|
||||
}
|
||||
|
||||
it("should return the current item") {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(manager.current))
|
||||
}
|
||||
|
||||
it("should move the current index") {
|
||||
expect(manager.currentIndex).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to last item") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
try? jumped = manager.jump(to: manager.items.count - 1)
|
||||
}
|
||||
it("should return the current item") {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(manager.current))
|
||||
}
|
||||
|
||||
it("should move the current index") {
|
||||
expect(manager.currentIndex).to(equal(manager.items.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to a negative index") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? manager.jump(to: -1)
|
||||
}
|
||||
|
||||
it("should not return") {
|
||||
expect(jumped).to(beNil())
|
||||
}
|
||||
|
||||
it("should not move the current index") {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping with too large index") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? manager.jump(to: manager.items.count)
|
||||
}
|
||||
it("should not return") {
|
||||
expect(jumped).to(beNil())
|
||||
}
|
||||
|
||||
it("should not move the current index") {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Moving
|
||||
|
||||
context("moving from current index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try manager.moveItem(fromIndex: manager.currentIndex, toIndex: manager.currentIndex + 1)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("moving from a negative index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try manager.moveItem(fromIndex: -1, toIndex: manager.currentIndex + 1)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("moving from a too large index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try manager.moveItem(fromIndex: manager.items.count, toIndex: manager.currentIndex + 1)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("moving to a negative index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: -1)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("moving to a too large index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: manager.items.count)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should throw an error") {
|
||||
expect(error).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("then moving 2nd to 4th") {
|
||||
let afterMoving: [Int] = [0, 2, 3, 1, 4, 5, 6]
|
||||
beforeEach {
|
||||
try? manager.moveItem(fromIndex: 1, toIndex: 3)
|
||||
}
|
||||
|
||||
it("should move the item") {
|
||||
expect(manager.items).to(equal(afterMoving))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Clear
|
||||
|
||||
context("when queue is cleared") {
|
||||
beforeEach {
|
||||
manager.clearQueue()
|
||||
}
|
||||
|
||||
it("should have currentIndex 0") {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
}
|
||||
|
||||
it("should have no items") {
|
||||
expect(manager.items.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
extension QueuedAudioPlayer {
|
||||
class SeekEventListener {
|
||||
var eventResult: (Int, Bool) = (-1, false)
|
||||
func handleEvent(seconds: Int, didFinish: Bool) { eventResult = (seconds, didFinish) }
|
||||
}
|
||||
|
||||
func seekWithExpectation(to time: Double) {
|
||||
let eventListener = SeekEventListener()
|
||||
event.seek.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
seek(to: time)
|
||||
expect(eventListener.eventResult).toEventually(equal((0, true)))
|
||||
}
|
||||
}
|
||||
|
||||
class QueuedAudioPlayerTests: QuickSpec {
|
||||
override func spec() {
|
||||
describe("A QueuedAudioPlayer") {
|
||||
var audioPlayer: QueuedAudioPlayer!
|
||||
beforeEach {
|
||||
audioPlayer = QueuedAudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
}
|
||||
describe("its current item") {
|
||||
it("should be nil") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
|
||||
context("when adding one item") {
|
||||
var item: AudioItem!
|
||||
beforeEach {
|
||||
item = ShortSource.getAudioItem()
|
||||
try? audioPlayer.add(item: item, playWhenReady: false)
|
||||
}
|
||||
it("should not be nil") {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
}
|
||||
|
||||
context("then loading a new item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have replaced the item") {
|
||||
expect(audioPlayer.currentItem?.getSourceUrl()).toNot(equal(item.getSourceUrl()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding multiple items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
|
||||
}
|
||||
it("should not be nil") {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its next items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
}
|
||||
it("should contain 1 item") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
it("should contain 0 items") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
}
|
||||
it("should contain 1 item") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing one item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.removeItem(at: 1)
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to the last item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.jumpToItem(atIndex: 1)
|
||||
}
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing upcoming items") {
|
||||
beforeEach {
|
||||
audioPlayer.removeUpcomingItems()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its previous items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
it("should contain one item") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing all previous items") {
|
||||
beforeEach {
|
||||
audioPlayer.removePreviousItems()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
describe("onNext") {
|
||||
context("player was playing") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should go to next item and play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
context("player was paused") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
audioPlayer.pause()
|
||||
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should go to next item and not play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("onPrevious") {
|
||||
context("player was playing") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
}
|
||||
|
||||
it("should go to previous item and play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.previousItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
context("player was paused") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
try? audioPlayer.next()
|
||||
audioPlayer.pause()
|
||||
|
||||
}
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
}
|
||||
|
||||
it("should go to previous item and not play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.previousItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestEventListener {
|
||||
var eventResult: (Int?, Int?) = (-1, -1)
|
||||
func handleEvent(previousIndex: Int?, nextIndex: Int?) { eventResult = (previousIndex, nextIndex) }
|
||||
}
|
||||
|
||||
describe("its repeat mode") {
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
|
||||
}
|
||||
|
||||
context("then setting repeat mode off") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
context("allow playback to end normally") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should stop playback normally") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, nil)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
it("should fail") {
|
||||
try? audioPlayer.next()
|
||||
expect(try audioPlayer.next()).to(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode track") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode queue") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should move to next item and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.currentIndex).to(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 1)))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
expect(audioPlayer.currentIndex).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((1, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding 1 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(item: ShortSource.getAudioItem(), playWhenReady: true)
|
||||
}
|
||||
|
||||
context("then setting repeat mode off") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
context("allow playback to end normally") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should stop playback normally") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, nil)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should fail") {
|
||||
try? audioPlayer.next()
|
||||
expect(try audioPlayer.next()).to(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode track") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode queue") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seekWithExpectation(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
it("should restart current item") {
|
||||
let eventListener = TestEventListener()
|
||||
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
|
||||
|
||||
// workaround: seek not to beggining, for 0 expecations to correctly fail if necessary.
|
||||
audioPlayer.seekWithExpectation(to: 0.05)
|
||||
try? audioPlayer.next()
|
||||
expect(audioPlayer.currentTime).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
expect(eventListener.eventResult).toEventually(equal((0, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// Sources.swift
|
||||
// SwiftAudio_Tests
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 05/08/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudioEx
|
||||
import UIKit
|
||||
|
||||
struct Source {
|
||||
static let path: String = Bundle.main.path(forResource: "TestSound", ofType: "m4a")!
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
struct ShortSource {
|
||||
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct LongSource {
|
||||
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
|
||||
static let url: URL = URL(fileURLWithPath: LongSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -13,7 +13,13 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftAudioEx",
|
||||
dependencies: [],
|
||||
path: "SwiftAudioEx/Classes")
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "SwiftAudioExTests",
|
||||
dependencies: ["SwiftAudioEx"],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,61 +1,85 @@
|
||||

|
||||
|
||||
# SwiftAudio
|
||||
# SwiftAudioEx
|
||||
|
||||
[](https://codecov.io/gh/DoubleSymmetry/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudioEx)
|
||||
[](http://cocoapods.org/pods/SwiftAudioEx)
|
||||
|
||||
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
|
||||
SwiftAudioEx is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
|
||||
|
||||
<div align="left" valign="middle">
|
||||
<a href="https://runblaze.dev">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://www.runblaze.dev/logo_dark.png">
|
||||
<img align="right" src="https://www.runblaze.dev/logo_light.png" height="102px"/>
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<br style="display: none;"/>
|
||||
|
||||
_[Blaze](https://runblaze.dev) sponsors SwiftAudioEx by providing super fast Apple Silicon based macOS Github Action Runners. Use the discount code `RNTP50` at checkout to get 50% off your first year._
|
||||
|
||||
</div>
|
||||
|
||||
## Example
|
||||
|
||||
To see the audio player in action, run the example project!
|
||||
To run the example project, clone the repo, and run `pod install` from the Example directory first.
|
||||
To run the example project, clone the repo, then open
|
||||
`Example/SwiftAudio.xcodeproj` in Xcode. Choose "Example for SwiftAudio" in the
|
||||
XCode project navigator and Build/Run it in a simulator (or on an actual
|
||||
device).
|
||||
|
||||
## Requirements
|
||||
|
||||
iOS 11.0+
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
[Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode.
|
||||
|
||||
SwiftAudio supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudio repo's URL](https://github.com/DoubleSymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudio` to search.
|
||||
SwiftAudioEx supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudioEx repo's URL](https://github.com/doublesymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudioEx` to search.
|
||||
|
||||
After select the package, you can choose the dependency type (tagged version, branch or commit). Then Xcode will setup all the stuff for you.
|
||||
|
||||
If you're a framework author and use SwiftAudio as a dependency, update your `Package.swift` file:
|
||||
If you're a framework author and use SwiftAudioEx as a dependency, update your `Package.swift` file:
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
// 0.12.0 ..< 1.0.0
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/DoubleSymmetry/SwiftAudio.git", from: "0.12.0")
|
||||
.package(url: "https://github.com/doublesymmetry/SwiftAudio.git", from: "1.0.0")
|
||||
],
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### CocoaPods
|
||||
SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
|
||||
SwiftAudioEx 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', '~> 1.0.0'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
|
||||
SwiftAudioEx supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
|
||||
```ruby
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.11.2
|
||||
github "doublesymmetry/SwiftAudioEx" ~> 1.0.0
|
||||
```
|
||||
|
||||
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
|
||||
|
||||
## Usage
|
||||
|
||||
### AudioPlayer
|
||||
|
||||
To get started playing some audio:
|
||||
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
@@ -64,16 +88,17 @@ player.load(item: audioItem, playWhenReady: true) // Load the item and start pla
|
||||
|
||||
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
|
||||
To subscribe to an event:
|
||||
|
||||
```swift
|
||||
class MyCustomViewController: UIViewController {
|
||||
|
||||
let audioPlayer = AudioPlayer()
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
|
||||
}
|
||||
|
||||
|
||||
func handleAudioPlayerStateChange(state: AudioPlayerState) {
|
||||
// Handle the event
|
||||
}
|
||||
@@ -81,7 +106,9 @@ class MyCustomViewController: UIViewController {
|
||||
```
|
||||
|
||||
#### QueuedAudioPlayer
|
||||
|
||||
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
|
||||
|
||||
```swift
|
||||
let player = QueuedAudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
@@ -91,7 +118,9 @@ player.add(item: audioItem, playWhenReady: true) // Since this is the first item
|
||||
When a track is done playing, the player will load the next track and update the queue.
|
||||
|
||||
##### Navigating the queue
|
||||
|
||||
All `AudioItem`s are stored in either `previousItems` or `nextItems`, which refers to items that come prior to the `currentItem` and after, respectively. The queue is navigated with:
|
||||
|
||||
```swift
|
||||
player.next() // Increments the queue, and loads the next item.
|
||||
player.previous() // Decrements the queue, and loads the previous item.
|
||||
@@ -99,13 +128,16 @@ player.jumpToItem(atIndex:) // Jumps to a certain item and loads that item.
|
||||
```
|
||||
|
||||
##### Manipulating the queue
|
||||
|
||||
```swift
|
||||
player.removeItem(at:) // Remove a specific item from the queue.
|
||||
player.removeUpcomingItems() // Remove all items in nextItems.
|
||||
```
|
||||
|
||||
### Configuring the AudioPlayer
|
||||
|
||||
Current options for configuring the `AudioPlayer`:
|
||||
|
||||
- `bufferDuration`: The amount of seconds to be buffered by the player.
|
||||
- `timeEventFrequency`: How often the player should call the delegate with time progress events.
|
||||
- `automaticallyWaitsToMinimizeStalling`: Indicates whether the player should automatically delay playback in order to minimize stalling.
|
||||
@@ -115,10 +147,13 @@ Current options for configuring the `AudioPlayer`:
|
||||
- `audioTimePitchAlgorithm`: This value decides the `AVAudioTimePitchAlgorithm` used for each `AudioItem`. Implement `TimePitching` in your `AudioItem`-subclass to override individually for each `AudioItem`.
|
||||
|
||||
Options particular to `QueuedAudioPlayer`:
|
||||
|
||||
- `repeatMode`: The repeat mode: off, track, queue
|
||||
|
||||
### Audio Session
|
||||
|
||||
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionController`:
|
||||
|
||||
```swift
|
||||
try? AudioSessionController.shared.set(category: .playback)
|
||||
//...
|
||||
@@ -131,34 +166,43 @@ try? AudioSessionController.shared.activateSession()
|
||||
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'.
|
||||
|
||||
#### Interruptions
|
||||
|
||||
If you are using the `AudioSessionController` for setting up the audio session, you can use it to handle interruptions too.
|
||||
Implement `AudioSessionControllerDelegate` and you will be notified by `handleInterruption(type: AVAudioSessionInterruptionType)`.
|
||||
If you are storing progress for playback time on items when the app quits, it can be a good idea to do it on interruptions as well.
|
||||
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
|
||||
|
||||
### Now Playing Info
|
||||
|
||||
The `AudioPlayer` can automatically update `nowPlayingInfo` for you. This requires `automaticallyUpdateNowPlayingInfo` to be true (default), and that the `AudioItem` that is passed in return values for the getters. The `AudioPlayer` will update: artist, title, album, artwork, elapsed time, duration and rate.
|
||||
|
||||
Additional properties for items can be set by accessing the setter of the `nowPlayingInforController`:
|
||||
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.load(item: someItem)
|
||||
player.nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.isLiveStream(true))
|
||||
```
|
||||
|
||||
The set(keyValue:) and set(keyValues:) accept both `MediaItemProperty` and `NowPlayingInfoProperty`.
|
||||
|
||||
The info can be forced to reload/update from the `AudioPlayer`.
|
||||
|
||||
```swift
|
||||
audioPlayer.loadNowPlayingMetaValues()
|
||||
audioPlayer.updateNowPlayingPlaybackValues()
|
||||
```
|
||||
|
||||
The current info can be cleared with:
|
||||
|
||||
```swift
|
||||
audioPlayer.nowPlayingInfoController.clear()
|
||||
```
|
||||
|
||||
### Remote Commands
|
||||
|
||||
To enable remote commands for the player you need to populate the RemoteCommands array for the player:
|
||||
|
||||
```swift
|
||||
audioPlayer.remoteCommands = [
|
||||
.play,
|
||||
@@ -167,25 +211,30 @@ audioPlayer.remoteCommands = [
|
||||
.skipBackward(intervals: [30]),
|
||||
]
|
||||
```
|
||||
|
||||
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable` in a custom `AudioItem`-subclass. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
|
||||
|
||||
#### Custom handlers for remote commands
|
||||
|
||||
To supply custom handlers for your remote commands, just override the handlers contained in the player's `RemoteCommandController`:
|
||||
|
||||
```swift
|
||||
let player = QueuedAudioPlayer()
|
||||
player.remoteCommandController.handlePlayCommand = { (event) in
|
||||
// Handle remote command here.
|
||||
}
|
||||
```
|
||||
|
||||
All available overrides can be found by looking at `RemoteCommandController`.
|
||||
|
||||
### Start playback from a certain point in time
|
||||
|
||||
Make your `AudioItem`-subclass conform to `InitialTiming` to be able to start playback from a certain time.
|
||||
|
||||
## Author
|
||||
|
||||
Jørgen Henrichsen
|
||||
Originally: Jørgen Henrichsen now extended by David Chavez and other contributors.
|
||||
|
||||
## License
|
||||
|
||||
SwiftAudio is available under the MIT license. See the LICENSE file for more info.
|
||||
SwiftAudioEx is available under the MIT license. See the LICENSE file for more info.
|
||||
|
||||
+519
@@ -0,0 +1,519 @@
|
||||
//
|
||||
// AVPlayerWrapper.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 06/03/2018.
|
||||
// Copyright © 2018 Jørgen Henrichsen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
public enum PlaybackEndedReason: String {
|
||||
case playedUntilEnd
|
||||
case playerStopped
|
||||
case skippedToNext
|
||||
case skippedToPrevious
|
||||
case jumpedToIndex
|
||||
case cleared
|
||||
case failed
|
||||
}
|
||||
|
||||
class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
Whether AVPlayer should start playing automatically when the item is ready.
|
||||
*/
|
||||
public var playWhenReady: Bool = false {
|
||||
didSet {
|
||||
if (playWhenReady == true && (state == .failed || state == .stopped)) {
|
||||
reload(startFromCurrentTime: state == .failed)
|
||||
}
|
||||
|
||||
applyAVPlayerRate()
|
||||
|
||||
if oldValue != playWhenReady {
|
||||
delegate?.AVWrapper(didChangePlayWhenReady: playWhenReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
avPlayer.currentItem
|
||||
}
|
||||
|
||||
var playbackActive: Bool {
|
||||
switch state {
|
||||
case .idle, .stopped, .ended, .failed:
|
||||
return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var currentTime: TimeInterval {
|
||||
let seconds = avPlayer.currentTime().seconds
|
||||
return seconds.isNaN ? 0 : seconds
|
||||
}
|
||||
|
||||
var duration: TimeInterval {
|
||||
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
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 volume: Float {
|
||||
get { avPlayer.volume }
|
||||
set { avPlayer.volume = newValue }
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get { avPlayer.isMuted }
|
||||
set { avPlayer.isMuted = newValue }
|
||||
}
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
func play() {
|
||||
playWhenReady = true
|
||||
}
|
||||
|
||||
func pause() {
|
||||
playWhenReady = false
|
||||
}
|
||||
|
||||
func togglePlaying() {
|
||||
switch avPlayer.timeControlStatus {
|
||||
case .playing, .waitingToPlayAtSpecifiedRate:
|
||||
pause()
|
||||
case .paused:
|
||||
play()
|
||||
@unknown default:
|
||||
fatalError("Unknown AVPlayer.timeControlStatus")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
state = .stopped
|
||||
clearCurrentItem()
|
||||
playWhenReady = false
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
// if the player is loading then we need to defer seeking until it's ready.
|
||||
if (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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playbackFailed(error: AudioPlayerError.PlaybackError) {
|
||||
state = .failed
|
||||
self.playbackError = error
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
}
|
||||
|
||||
func load() {
|
||||
if (state == .failed) {
|
||||
recreateAVPlayer()
|
||||
} else {
|
||||
clearCurrentItem()
|
||||
}
|
||||
if let url = url {
|
||||
let pendingAsset = AVURLAsset(url: url, options: urlOptions)
|
||||
asset = pendingAsset
|
||||
state = .loading
|
||||
|
||||
// Load metadata keys asynchronously and separate from playable, to allow that to execute as quickly as it can
|
||||
let metdataKeys = ["commonMetadata", "availableChapterLocales", "availableMetadataFormats"]
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: metdataKeys, completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if (pendingAsset != self.asset) { return; }
|
||||
|
||||
let commonData = pendingAsset.commonMetadata
|
||||
if (!commonData.isEmpty) {
|
||||
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
|
||||
}
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveChapterMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
|
||||
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
|
||||
self.delegate?.AVWrapper(didReceiveTimedMetadata: [group])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Load playable portion of the track and commence when ready
|
||||
let playableKeys = ["playable"]
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: playableKeys, completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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, options: [String: Any]? = nil) {
|
||||
self.playWhenReady = playWhenReady
|
||||
self.url = url
|
||||
self.urlOptions = options
|
||||
self.load()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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 {
|
||||
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
let state = self.state
|
||||
if self.asset == nil && state != .stopped {
|
||||
self.state = .idle
|
||||
} else if (state != .failed && state != .stopped) {
|
||||
// Playback may have become paused externally for example due to a bluetooth device disconnecting:
|
||||
if (self.playWhenReady) {
|
||||
// Only if we are not on the boundaries of the track, otherwise itemDidPlayToEndTime will handle it instead.
|
||||
if (self.currentTime > 0 && self.currentTime < self.duration) {
|
||||
self.playWhenReady = false;
|
||||
}
|
||||
} else {
|
||||
self.state = .paused
|
||||
}
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
if self.asset != nil {
|
||||
self.state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
self.state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
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 {
|
||||
|
||||
// MARK: - AVPlayerTimeObserverDelegate
|
||||
|
||||
func audioDidStart() {
|
||||
state = .playing
|
||||
}
|
||||
|
||||
func timeEvent(time: CMTime) {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
if (playbackLikelyToKeepUp && state != .playing) {
|
||||
state = .ready
|
||||
}
|
||||
}
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveTimedMetadata: metadata)
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -14,10 +14,14 @@ protocol AVPlayerWrapperDelegate: AnyObject {
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem])
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool)
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperItemFailedToPlayToEndTime()
|
||||
func AVWrapperItemPlaybackStalled()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
+16
-6
@@ -10,13 +10,15 @@ import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var state: AVPlayerWrapperState { get }
|
||||
|
||||
var playWhenReady: Bool { get }
|
||||
|
||||
var state: AVPlayerWrapperState { get set }
|
||||
|
||||
var playWhenReady: Bool { get set }
|
||||
|
||||
var currentItem: AVPlayerItem? { get }
|
||||
|
||||
var playbackActive: Bool { get }
|
||||
|
||||
var currentTime: TimeInterval { get }
|
||||
|
||||
var duration: TimeInterval { get }
|
||||
@@ -25,6 +27,7 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var playbackError: AudioPlayerError.PlaybackError? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
@@ -39,7 +42,6 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -50,8 +52,16 @@ protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
func stop()
|
||||
|
||||
func seek(to seconds: TimeInterval)
|
||||
|
||||
|
||||
func seek(by offset: TimeInterval)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func load(from url: String, type: SourceType, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func unload()
|
||||
|
||||
func reload(startFromCurrentTime: Bool)
|
||||
}
|
||||
+8
@@ -26,10 +26,18 @@ public enum AVPlayerWrapperState: String {
|
||||
/// The player is paused.
|
||||
case paused
|
||||
|
||||
/// The player is stopped.
|
||||
case stopped
|
||||
|
||||
/// The player is playing.
|
||||
case playing
|
||||
|
||||
/// No item loaded, the player is stopped.
|
||||
case idle
|
||||
|
||||
/// Failed
|
||||
case failed
|
||||
|
||||
/// Playback has reached the end.
|
||||
case ended
|
||||
}
|
||||
@@ -11,27 +11,26 @@ import MediaPlayer
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
|
||||
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public let event = EventHolder()
|
||||
|
||||
|
||||
private(set) var currentItem: AudioItem?
|
||||
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
|
||||
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
@@ -42,12 +41,39 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Handles the `playWhenReady` setting while executing a given action.
|
||||
|
||||
This method takes an optional `Bool` value and a closure representing an action to execute.
|
||||
If the `Bool` value is not `nil`, `self.playWhenReady` is set accordingly either before or
|
||||
after executing the action.
|
||||
|
||||
- Parameters:
|
||||
- playWhenReady: Optional `Bool` to set `self.playWhenReady`.
|
||||
- If `true`, `self.playWhenReady` will be set after executing the action.
|
||||
- If `false`, `self.playWhenReady` will be set before executing the action.
|
||||
- If `nil`, `self.playWhenReady` will not be changed.
|
||||
- action: A closure representing the action to execute. This closure can throw an error.
|
||||
|
||||
- Throws: This function will propagate any errors thrown by the `action` closure.
|
||||
*/
|
||||
internal func handlePlayWhenReady(_ playWhenReady: Bool?, action: () throws -> Void) rethrows {
|
||||
if playWhenReady == false {
|
||||
self.playWhenReady = false
|
||||
}
|
||||
|
||||
try action()
|
||||
|
||||
if playWhenReady == true {
|
||||
self.playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
internal var willPlayWhenReady: Bool {
|
||||
wrapper.playWhenReady
|
||||
public var playbackError: AudioPlayerError.PlaybackError? {
|
||||
wrapper.playbackError
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,40 +82,66 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
public var currentTime: Double {
|
||||
wrapper.currentTime
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The duration of the current AudioItem.
|
||||
*/
|
||||
public var duration: Double {
|
||||
wrapper.duration
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
public var playerState: AudioPlayerState {
|
||||
wrapper.state
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
|
||||
/**
|
||||
Whether the player should start playing automatically when the item is ready.
|
||||
*/
|
||||
public var playWhenReady: Bool {
|
||||
get { wrapper.playWhenReady }
|
||||
set {
|
||||
wrapper.playWhenReady = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
|
||||
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering. Setting `bufferDuration` to larger than zero automatically disables `automaticallyWaitsToMinimizeStalling`. Setting it back to zero automatically enables `automaticallyWaitsToMinimizeStalling`.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
|
||||
|
||||
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
set {
|
||||
wrapper.bufferDuration = newValue
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = wrapper.bufferDuration == 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling. Setting this to true will also set `bufferDuration` back to `0`.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set {
|
||||
if (newValue) {
|
||||
wrapper.bufferDuration = 0
|
||||
}
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,135 +151,137 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
get { wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var volume: Float {
|
||||
get { wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0
|
||||
public var rate: Float {
|
||||
get { _rate }
|
||||
get { wrapper.rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
|
||||
// Only set the rate on the wrapper if it is already playing.
|
||||
if wrapper.rate > 0 {
|
||||
wrapper.rate = newValue
|
||||
wrapper.rate = newValue
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
|
||||
/**
|
||||
Create a new AudioPlayer.
|
||||
|
||||
|
||||
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
||||
*/
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
|
||||
wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Player Actions
|
||||
|
||||
|
||||
/**
|
||||
Load an AudioItem into the manager.
|
||||
|
||||
|
||||
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
let url: URL
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
if let itemUrl = URL(string: item.getSourceUrl()) {
|
||||
url = itemUrl
|
||||
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
currentItem = item
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
// Reset playback values without updating, because that will happen in
|
||||
// the loadNowPlayingMetaValues call straight after:
|
||||
nowPlayingInfoController.setWithoutUpdate(keyValues: [
|
||||
MediaItemProperty.duration(nil),
|
||||
NowPlayingInfoProperty.playbackRate(nil),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
|
||||
])
|
||||
loadNowPlayingMetaValues()
|
||||
}
|
||||
else {
|
||||
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
|
||||
}
|
||||
case .file:
|
||||
url = URL(fileURLWithPath: item.getSourceUrl())
|
||||
|
||||
enableRemoteCommands(forItem: item)
|
||||
|
||||
wrapper.load(
|
||||
from: item.getSourceUrl(),
|
||||
type: item.getSourceType(),
|
||||
playWhenReady: self.playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions()
|
||||
)
|
||||
}
|
||||
|
||||
wrapper.load(from: url,
|
||||
playWhenReady: playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions())
|
||||
|
||||
currentItem = item
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
loadNowPlayingMetaValues()
|
||||
}
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
wrapper.play()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
wrapper.pause()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
Stop playback
|
||||
*/
|
||||
public func stop() {
|
||||
reset()
|
||||
let wasActive = wrapper.playbackActive
|
||||
wrapper.stop()
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
if (wasActive) {
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Reload the current item.
|
||||
*/
|
||||
public func reload(startFromCurrentTime: Bool) {
|
||||
wrapper.reload(startFromCurrentTime: startFromCurrentTime)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek by relative a time offset in the item.
|
||||
*/
|
||||
public func seek(by offset: TimeInterval) {
|
||||
wrapper.seek(by: offset)
|
||||
}
|
||||
|
||||
// MARK: - Remote Command Center
|
||||
|
||||
|
||||
func enableRemoteCommands(_ commands: [RemoteCommand]) {
|
||||
remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
@@ -245,12 +299,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
|
||||
/**
|
||||
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
|
||||
|
||||
|
||||
Reloads:
|
||||
- Artist
|
||||
- Title
|
||||
@@ -259,42 +313,49 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public func loadNowPlayingMetaValues() {
|
||||
guard let item = currentItem else { return }
|
||||
|
||||
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.artist(item.getArtist()),
|
||||
MediaItemProperty.title(item.getTitle()),
|
||||
MediaItemProperty.albumTitle(item.getAlbumTitle()),
|
||||
])
|
||||
|
||||
loadArtwork(forItem: item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Resyncs the playbackvalues of the currently playing `AudioItem`.
|
||||
|
||||
|
||||
Will resync:
|
||||
- Current time
|
||||
- Duration
|
||||
- Playback rate
|
||||
*/
|
||||
public func updateNowPlayingPlaybackValues() {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
updateNowPlayingDuration(duration)
|
||||
updateNowPlayingRate(rate)
|
||||
func updateNowPlayingPlaybackValues() {
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.duration(wrapper.duration),
|
||||
NowPlayingInfoProperty.playbackRate(wrapper.playWhenReady ? Double(wrapper.rate) : 0),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime)
|
||||
])
|
||||
}
|
||||
|
||||
private func updateNowPlayingDuration(_ duration: Double) {
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
|
||||
|
||||
public func clear() {
|
||||
let playbackWasActive = wrapper.playbackActive
|
||||
currentItem = nil
|
||||
wrapper.unload()
|
||||
nowPlayingInfoController.clear()
|
||||
if (playbackWasActive) {
|
||||
event.playbackEnd.emit(data: .cleared)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNowPlayingRate(_ rate: Float) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setNowPlayingCurrentTime(seconds: Double) {
|
||||
nowPlayingInfoController.set(
|
||||
keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(seconds)
|
||||
)
|
||||
}
|
||||
|
||||
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
|
||||
}
|
||||
|
||||
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
@@ -305,36 +366,26 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func reset() {
|
||||
currentItem = nil
|
||||
}
|
||||
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
if let item = currentItem as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
switch state {
|
||||
case .ready, .loading:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
case .playing:
|
||||
// When a track starts playing, reset the rate to the stored rate
|
||||
rate = _rate;
|
||||
fallthrough
|
||||
case .paused:
|
||||
default: break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .ready, .loading, .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
@@ -342,32 +393,54 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
event.fail.emit(data: error)
|
||||
event.playbackEnd.emit(data: .failed)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
setNowPlayingCurrentTime(seconds: Double(seconds))
|
||||
}
|
||||
event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveMetadata.emit(data: metadata)
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
event.receiveCommonMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveChapterMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveTimedMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
event.playWhenReadyChange.emit(data: playWhenReady)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
wrapper.state = .ended
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
AVWrapper(failedWithError: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
}
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AudioPlayerError.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum AudioPlayerError: Error {
|
||||
|
||||
public enum PlaybackError: Error {
|
||||
case failedToLoadKeyValue
|
||||
case invalidSourceUrl(String)
|
||||
case notConnectedToInternet
|
||||
case playbackFailed
|
||||
case itemWasUnplayable
|
||||
}
|
||||
|
||||
public enum QueueError: Error {
|
||||
case noCurrentItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
case empty
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,23 @@ import MediaPlayer
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
public typealias PlayWhenReadyChangeData = Bool
|
||||
public typealias StateChangeEventData = AudioPlayerState
|
||||
public typealias PlaybackEndEventData = PlaybackEndedReason
|
||||
public typealias SecondElapseEventData = TimeInterval
|
||||
public typealias FailEventData = Error?
|
||||
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
|
||||
public typealias SeekEventData = (seconds: Double, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = Double
|
||||
public typealias MetadataEventData = [AVTimedMetadataGroup]
|
||||
public typealias MetadataCommonEventData = [AVMetadataItem]
|
||||
public typealias MetadataTimedEventData = [AVTimedMetadataGroup]
|
||||
public typealias DidRecreateAVPlayerEventData = ()
|
||||
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
|
||||
public typealias CurrentItemEventData = (
|
||||
item: AudioItem?,
|
||||
index: Int?,
|
||||
lastItem: AudioItem?,
|
||||
lastIndex: Int?,
|
||||
lastPosition: Double?
|
||||
)
|
||||
|
||||
public struct EventHolder {
|
||||
|
||||
@@ -27,6 +35,12 @@ extension AudioPlayer {
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the `AudioPlayer#playWhenReady` has changed
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let playWhenReadyChange: AudioPlayer.Event<PlayWhenReadyChangeData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the playback of the player, for some reason, has stopped.
|
||||
@@ -60,10 +74,22 @@ extension AudioPlayer {
|
||||
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives metadata.
|
||||
Emitted when the player receives common metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveMetadata: AudioPlayer.Event<MetadataEventData> = AudioPlayer.Event()
|
||||
public let receiveCommonMetadata: AudioPlayer.Event<MetadataCommonEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives timed metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveTimedMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives chapter metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveChapterMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
|
||||
@@ -73,11 +99,11 @@ extension AudioPlayer {
|
||||
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when a new track starts and the queue index changes.
|
||||
Emitted when the current track has changed.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
- Note: It is only fired for instances of a QueuedAudioPlayer.
|
||||
*/
|
||||
public let queueIndex: AudioPlayer.Event<QueueIndexEventData> = AudioPlayer.Event()
|
||||
public let currentItem: AudioPlayer.Event<CurrentItemEventData> = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
public typealias EventClosure<EventData> = (EventData) -> Void
|
||||
@@ -102,42 +128,28 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
public class Event<EventData> {
|
||||
|
||||
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
|
||||
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
|
||||
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
private let queue: DispatchQueue = DispatchQueue(label: "com.swiftAudioEx.eventQueue")
|
||||
var invokers: [Invoker<EventData>] = []
|
||||
|
||||
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers.append(Invoker(listener: listener, closure: closure))
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
public func removeListener(_ listener: AnyObject) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
if let listenerToCheck = invoker.listener {
|
||||
return listenerToCheck !== listener
|
||||
}
|
||||
return true
|
||||
return invoker.listener !== listener
|
||||
})
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
func emit(data: EventData) {
|
||||
eventQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter { $0.invoke(data) }
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+26
-16
@@ -9,54 +9,64 @@ import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
private let concurrentInfoQueue: DispatchQueueType
|
||||
private var infoQueue: DispatchQueueType = DispatchQueue(
|
||||
label: "NowPlayingInfoController.infoQueue",
|
||||
attributes: .concurrent
|
||||
)
|
||||
|
||||
private(set) var infoCenter: NowPlayingInfoCenter
|
||||
private(set) var info: [String: Any] = [:]
|
||||
|
||||
public required init() {
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
concurrentInfoQueue = dispatchQueue
|
||||
infoQueue = dispatchQueue
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter) {
|
||||
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
public required init(infoCenter: NowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public func set(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
keyValues.forEach { (keyValue) in
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
keyValues.forEach {
|
||||
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
public func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
keyValues.forEach {
|
||||
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func set(keyValue: NowPlayingInfoKeyValue) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private func update() {
|
||||
infoCenter.nowPlayingInfo = info
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.info = [:]
|
||||
self.infoCenter.nowPlayingInfo = self.info
|
||||
self.infoCenter.nowPlayingInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -19,6 +19,8 @@ public protocol NowPlayingInfoControllerProtocol {
|
||||
|
||||
func set(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func clear()
|
||||
|
||||
}
|
||||
+43
-3
@@ -10,6 +10,8 @@ import AVFoundation
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
|
||||
func itemDidPlayToEndTime()
|
||||
func itemFailedToPlayToEndTime()
|
||||
func itemPlaybackStalled()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +42,24 @@ class AVPlayerItemNotificationObserver {
|
||||
stopObservingCurrentItem()
|
||||
observingItem = item
|
||||
isObserving = true
|
||||
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemDidPlayToEndTime),
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: item
|
||||
)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemFailedToPlayToEndTime),
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: item
|
||||
)
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(itemPlaybackStalled),
|
||||
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
|
||||
object: item
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +69,21 @@ class AVPlayerItemNotificationObserver {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: observingItem
|
||||
)
|
||||
notificationCenter.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
|
||||
object: observingItem
|
||||
)
|
||||
self.observingItem = nil
|
||||
isObserving = false
|
||||
}
|
||||
@@ -58,5 +91,12 @@ class AVPlayerItemNotificationObserver {
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
delegate?.itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
|
||||
@objc private func itemFailedToPlayToEndTime() {
|
||||
delegate?.itemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
@objc private func itemPlaybackStalled() {
|
||||
delegate?.itemPlaybackStalled()
|
||||
}
|
||||
}
|
||||
+43
-14
@@ -11,14 +11,18 @@ import AVFoundation
|
||||
protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the observed item updates the duration.
|
||||
Called when the duration of the observed item is updated.
|
||||
*/
|
||||
func item(didUpdateDuration duration: Double)
|
||||
|
||||
|
||||
/**
|
||||
Called when the playback of the observed item is or is no longer likely to keep up.
|
||||
*/
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool)
|
||||
/**
|
||||
Called when the observed item receives metadata
|
||||
*/
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup])
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
|
||||
}
|
||||
|
||||
@@ -28,12 +32,12 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
private let metadataOutput: AVPlayerItemMetadataOutput
|
||||
private var currentMetadataOutput: AVPlayerItemMetadataOutput?
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
@@ -42,10 +46,7 @@ class AVPlayerItemObserver: NSObject {
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
override init() {
|
||||
metadataOutput = AVPlayerItemMetadataOutput()
|
||||
super.init()
|
||||
|
||||
metadataOutput.setDelegate(self, queue: main)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -59,22 +60,35 @@ class AVPlayerItemObserver: NSObject {
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
stopObservingCurrentItem()
|
||||
isObserving = true
|
||||
observingItem = item
|
||||
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
|
||||
// Create and add a new metadata output to the item.
|
||||
let metadataOutput = AVPlayerItemMetadataOutput()
|
||||
metadataOutput.setDelegate(self, queue: .main)
|
||||
item.add(metadataOutput)
|
||||
self.currentMetadataOutput = metadataOutput
|
||||
}
|
||||
|
||||
func stopObservingCurrentItem() {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
|
||||
observingItem.remove(metadataOutput)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
|
||||
|
||||
// Remove all metadata outputs from the item.
|
||||
observingItem.removeAllMetadataOutputs()
|
||||
|
||||
isObserving = false
|
||||
self.observingItem = nil
|
||||
self.currentMetadataOutput = nil
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
@@ -88,12 +102,17 @@ class AVPlayerItemObserver: NSObject {
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
|
||||
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
|
||||
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
default: break
|
||||
|
||||
}
|
||||
@@ -102,6 +121,16 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
delegate?.item(didReceiveMetadata: groups)
|
||||
if output == currentMetadataOutput {
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AVPlayerItem {
|
||||
func removeAllMetadataOutputs() {
|
||||
for output in self.outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
|
||||
self.remove(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -54,13 +54,23 @@ class AVPlayerObserver: NSObject {
|
||||
Start receiving events from this observer.
|
||||
*/
|
||||
func startObserving() {
|
||||
if (isObserving) { return };
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
stopObserving()
|
||||
isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.status,
|
||||
options: statusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.timeControlStatus,
|
||||
options: timeControlStatusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
}
|
||||
|
||||
func stopObserving() {
|
||||
+13
-7
@@ -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
|
||||
}
|
||||
Executable
+357
@@ -0,0 +1,357 @@
|
||||
//
|
||||
// QueueManager.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol QueueManagerDelegate: AnyObject {
|
||||
func onReceivedFirstItem()
|
||||
func onCurrentItemChanged()
|
||||
func onSkippedToSameCurrentItem()
|
||||
}
|
||||
|
||||
class QueueManager<T> {
|
||||
|
||||
fileprivate let recursiveLock = NSRecursiveLock()
|
||||
|
||||
fileprivate func synchronizeThrows<T>(action: () throws -> T) throws -> T {
|
||||
recursiveLock.lock()
|
||||
let result = try action()
|
||||
recursiveLock.unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func synchronize <T>(action: () -> T) -> T {
|
||||
recursiveLock.lock()
|
||||
let result = action()
|
||||
recursiveLock.unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
weak var delegate: QueueManagerDelegate? = nil
|
||||
|
||||
var _currentIndex: Int = -1
|
||||
/**
|
||||
The index of the current item. `-1` when there is no current item
|
||||
*/
|
||||
private(set) var currentIndex: Int {
|
||||
get {
|
||||
return synchronize {
|
||||
return _currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
return synchronize {
|
||||
self._currentIndex = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
private(set) var items: [T] = [] {
|
||||
didSet {
|
||||
return synchronize {
|
||||
if oldValue.count == 0 && items.count > 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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? {
|
||||
return synchronize {
|
||||
return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : 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 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 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
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(wrap: Bool = false) -> T? {
|
||||
synchronize {
|
||||
return skip(direction: SkipDirection.next, wrap: wrap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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(wrap: Bool = false) -> T? {
|
||||
return synchronize {
|
||||
return skip(direction: SkipDirection.previous, wrap: wrap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Jump to a position in the queue.
|
||||
Will update the current item.
|
||||
|
||||
- parameter index: The index to jump to.
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
- returns: The item at the index.
|
||||
*/
|
||||
@discardableResult
|
||||
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!
|
||||
}
|
||||
if (skippedToSameCurrentItem) {
|
||||
delegate?.onSkippedToSameCurrentItem()
|
||||
}
|
||||
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. 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`
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: AudioPlayerError.QueueError
|
||||
- returns: The removed item.
|
||||
*/
|
||||
public func removeItem(at index: Int) throws -> T {
|
||||
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;
|
||||
}
|
||||
if (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all previous items in the queue.
|
||||
If no previous items exist, no action will be taken.
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
synchronize {
|
||||
if (items.count == 0) { return };
|
||||
guard currentIndex > 0 else { return }
|
||||
items.removeSubrange(0..<currentIndex)
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove upcoming items.
|
||||
If no upcoming items exist, no action will be taken.
|
||||
*/
|
||||
public func removeUpcomingItems() {
|
||||
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() {
|
||||
var currentItemChanged = false
|
||||
synchronize {
|
||||
let itemWasNil = currentIndex == -1;
|
||||
currentIndex = -1
|
||||
items.removeAll()
|
||||
currentItemChanged = !itemWasNil
|
||||
}
|
||||
if (currentItemChanged) {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+236
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// QueuedAudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
/**
|
||||
An audio player that can keep track of a queue of AudioItems.
|
||||
*/
|
||||
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
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)
|
||||
queue.delegate = self
|
||||
}
|
||||
|
||||
/// The repeat mode for the queue player.
|
||||
public var repeatMode: RepeatMode = .off
|
||||
|
||||
public override var currentItem: AudioItem? {
|
||||
queue.current
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
queue.currentIndex
|
||||
}
|
||||
|
||||
override public func clear() {
|
||||
queue.clearQueue()
|
||||
super.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
All items currently in the queue.
|
||||
*/
|
||||
public var items: [AudioItem] {
|
||||
queue.items
|
||||
}
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
queue.previousItems
|
||||
}
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
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.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public override func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.replaceCurrentItem(with: item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
- parameter item: The item to add.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Add items to the queue.
|
||||
|
||||
- parameter items: The items to add to the queue.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(items: [AudioItem], playWhenReady: Bool? = nil) {
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.add(items)
|
||||
}
|
||||
}
|
||||
|
||||
public func add(items: [AudioItem], at index: Int) throws {
|
||||
try queue.add(items, at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
Step to the next item in the queue.
|
||||
*/
|
||||
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() {
|
||||
let lastIndex = currentIndex
|
||||
let playbackWasActive = wrapper.playbackActive;
|
||||
_ = queue.previous(wrap: repeatMode == .queue)
|
||||
if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) {
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item from the queue.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
public func removeItem(at index: Int) throws {
|
||||
try queue.removeItem(at: index)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Jump to a certain item in the queue.
|
||||
|
||||
- parameter index: The index of the item to jump to.
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
- throws: `AudioPlayerError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool? = nil) throws {
|
||||
try handlePlayWhenReady(playWhenReady) {
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
} else {
|
||||
_ = try queue.jump(to: index)
|
||||
}
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Move an item in the queue from one position to another.
|
||||
|
||||
- parameter fromIndex: The index of the item to move.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `AudioPlayerError.QueueError`
|
||||
*/
|
||||
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() {
|
||||
queue.removeUpcomingItems()
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all previous items, those returned by `previous()`
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
queue.removePreviousItems()
|
||||
}
|
||||
|
||||
func replay() {
|
||||
seek(to: 0);
|
||||
play()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
override func AVWrapperItemDidPlayToEndTime() {
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
if (repeatMode == .track) {
|
||||
self.pause()
|
||||
|
||||
// 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 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() {
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
}
|
||||
+7
-27
@@ -171,26 +171,16 @@ public class RemoteCommandController {
|
||||
|
||||
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
@@ -208,19 +198,9 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
if let error = error as? APError.LoadError {
|
||||
switch error {
|
||||
case .invalidSourceUrl(_):
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
}
|
||||
else if let error = error as? APError.QueueError {
|
||||
switch error {
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
|
||||
return MPRemoteCommandHandlerStatus.noSuchContent
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
return error is AudioPlayerError.QueueError
|
||||
? MPRemoteCommandHandlerStatus.noSuchContent
|
||||
: MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '0.15.3'
|
||||
s.version = '1.1.0'
|
||||
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.
|
||||
@@ -22,5 +22,5 @@ DESC
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.swift_version = '5.0'
|
||||
s.source_files = 'SwiftAudioEx/Classes/**/*'
|
||||
s.source_files = 'Sources/SwiftAudioEx/**/*'
|
||||
end
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
//
|
||||
// AVPlayerWrapper.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 06/03/2018.
|
||||
// Copyright © 2018 Jørgen Henrichsen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
public enum PlaybackEndedReason: String {
|
||||
case playedUntilEnd
|
||||
case playerStopped
|
||||
case skippedToNext
|
||||
case skippedToPrevious
|
||||
case jumpedToIndex
|
||||
}
|
||||
|
||||
class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
struct Constants {
|
||||
static let assetPlayableKey = "playable"
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
fileprivate var avPlayer = AVPlayer()
|
||||
private let playerObserver = AVPlayerObserver()
|
||||
internal let playerTimeObserver: AVPlayerTimeObserver
|
||||
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
private let playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
fileprivate var initialTime: TimeInterval?
|
||||
fileprivate var pendingAsset: AVAsset? = nil
|
||||
|
||||
/// True when the track was paused for the purpose of switching tracks
|
||||
fileprivate var pausedForLoad: Bool = false
|
||||
|
||||
public init() {
|
||||
playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
playerTimeObserver.player = avPlayer
|
||||
|
||||
playerObserver.player = avPlayer
|
||||
playerObserver.delegate = self
|
||||
playerTimeObserver.delegate = self
|
||||
playerItemNotificationObserver.delegate = self
|
||||
playerItemObserver.delegate = self
|
||||
|
||||
// disabled since we're not making use of video playback
|
||||
avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
fileprivate(set) var state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
if oldValue != state {
|
||||
delegate?.AVWrapper(didChangeState: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate(set) var lastPlayerTimeControlStatus: AVPlayer.TimeControlStatus = AVPlayer.TimeControlStatus.paused {
|
||||
didSet {
|
||||
if oldValue != lastPlayerTimeControlStatus {
|
||||
switch lastPlayerTimeControlStatus {
|
||||
case .paused:
|
||||
if pendingAsset == nil {
|
||||
state = .idle
|
||||
}
|
||||
else if currentItem != nil && pausedForLoad != true {
|
||||
state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
if pendingAsset != nil {
|
||||
state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate(set) var playWhenReady: Bool = true
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
avPlayer.currentItem
|
||||
}
|
||||
|
||||
var currentTime: TimeInterval {
|
||||
let seconds = avPlayer.currentTime().seconds
|
||||
return seconds.isNaN ? 0 : seconds
|
||||
}
|
||||
|
||||
var duration: TimeInterval {
|
||||
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
get { avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
|
||||
var timeEventFrequency: TimeEventFrequency = .everySecond {
|
||||
didSet {
|
||||
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
var volume: Float {
|
||||
get { avPlayer.volume }
|
||||
set { avPlayer.volume = newValue }
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get { avPlayer.isMuted }
|
||||
set { avPlayer.isMuted = newValue }
|
||||
}
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
func play() {
|
||||
playWhenReady = true
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
playWhenReady = false
|
||||
avPlayer.pause()
|
||||
}
|
||||
|
||||
func togglePlaying() {
|
||||
switch avPlayer.timeControlStatus {
|
||||
case .playing, .waitingToPlayAtSpecifiedRate:
|
||||
pause()
|
||||
case .paused:
|
||||
play()
|
||||
@unknown default:
|
||||
fatalError("Unknown AVPlayer.timeControlStatus")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
pause()
|
||||
reset(soft: false)
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
// if the player is loading then we need to defer seeking until it's ready.
|
||||
if (state == AVPlayerWrapperState.loading) {
|
||||
initialTime = seconds
|
||||
} else {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self.initialTime {
|
||||
self.initialTime = nil
|
||||
if self.playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
reset(soft: true)
|
||||
self.playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = pendingAsset {
|
||||
state = .loading
|
||||
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
var error: NSError? = nil
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if (pendingAsset != self.pendingAsset) { return; }
|
||||
switch status {
|
||||
case .loaded:
|
||||
let item = AVPlayerItem(
|
||||
asset: pendingAsset,
|
||||
automaticallyLoadedAssetKeys: [Constants.assetPlayableKey]
|
||||
)
|
||||
item.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: item)
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: item)
|
||||
self.playerItemObserver.startObserving(item: item)
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
|
||||
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: [group])
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
self.reset(soft: false)
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
|
||||
self.initialTime = initialTime
|
||||
|
||||
pausedForLoad = true
|
||||
pause()
|
||||
|
||||
self.load(from: url, playWhenReady: playWhenReady, options: options)
|
||||
}
|
||||
|
||||
// MARK: - Util
|
||||
|
||||
private func reset(soft: Bool) {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
pendingAsset?.cancelLoading()
|
||||
pendingAsset = nil
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Will recreate the AVPlayer instance. Used when the current one fails.
|
||||
private func recreateAVPlayer() {
|
||||
let player = AVPlayer()
|
||||
playerObserver.player = player
|
||||
playerTimeObserver.player = player
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
avPlayer = player
|
||||
delegate?.AVWrapperDidRecreateAVPlayer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
lastPlayerTimeControlStatus = status;
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
state = .ready
|
||||
pausedForLoad = false
|
||||
if playWhenReady && (initialTime ?? 0) == 0 {
|
||||
play()
|
||||
}
|
||||
else if let initialTime = initialTime {
|
||||
seek(to: initialTime)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
break
|
||||
|
||||
case .unknown:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerTimeObserverDelegate
|
||||
|
||||
func audioDidStart() {
|
||||
state = .playing
|
||||
}
|
||||
|
||||
func timeEvent(time: CMTime) {
|
||||
delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
delegate?.AVWrapperItemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
//
|
||||
// QueueManager.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol QueueManagerDelegate: AnyObject {
|
||||
func onReceivedFirstItem()
|
||||
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int)
|
||||
}
|
||||
|
||||
class QueueManager<T> {
|
||||
|
||||
weak var delegate: QueueManagerDelegate? = nil
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
private(set) var items: [T] = [] {
|
||||
didSet {
|
||||
if oldValue.count == 0 && items.count > 0 && currentIndex == 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
private(set) var currentIndex: Int = 0 {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The current item for the queue.
|
||||
*/
|
||||
public var current: T? {
|
||||
if items.count > currentIndex {
|
||||
return items[currentIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
- parameter item: The `AudioItem` to be added.
|
||||
*/
|
||||
public func addItem(_ item: T) {
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
/**
|
||||
Add an array of items to the queue.
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
*/
|
||||
public func addItems(_ items: [T]) {
|
||||
self.items.append(contentsOf: items)
|
||||
}
|
||||
|
||||
/**
|
||||
Add an array of items to the queue at a given index.
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
- parameter at: The index to insert the items at.
|
||||
*/
|
||||
public func addItems(_ items: [T], at index: Int) throws {
|
||||
guard index >= 0 && self.items.count >= index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index to insert at has to be non-negative and equal to or smaller than the number of items: (\(items.count))")
|
||||
}
|
||||
|
||||
self.items.insert(contentsOf: items, at: index)
|
||||
|
||||
if (currentIndex >= index && self.items.count != 1) { currentIndex += items.count }
|
||||
}
|
||||
|
||||
/**
|
||||
Get the next item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The next item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func next() throws -> T {
|
||||
let nextIndex = currentIndex + 1
|
||||
guard items.count > nextIndex else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
currentIndex = nextIndex
|
||||
return items[nextIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
Get the previous item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The previous item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func previous() throws -> T {
|
||||
let previousIndex = currentIndex - 1
|
||||
guard previousIndex >= 0 else {
|
||||
throw APError.QueueError.noPreviousItem
|
||||
}
|
||||
currentIndex = previousIndex
|
||||
return items[previousIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
Jump to a position in the queue.
|
||||
Will update the current item.
|
||||
|
||||
- parameter index: The index to jump to.
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The item at the index.
|
||||
*/
|
||||
@discardableResult
|
||||
func jump(to index: Int) throws -> T {
|
||||
guard index != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
|
||||
}
|
||||
|
||||
guard index >= 0 && items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(items.count))")
|
||||
}
|
||||
|
||||
currentIndex = index
|
||||
return items[index]
|
||||
}
|
||||
|
||||
/**
|
||||
Move an item in the queue.
|
||||
|
||||
- parameter fromIndex: The index of the item to be moved.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
guard fromIndex != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
|
||||
}
|
||||
|
||||
guard fromIndex >= 0 && fromIndex < items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
guard toIndex >= 0 && toIndex < items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
let item = try removeItem(at: fromIndex)
|
||||
try addItems([item], at: toIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: APError.QueueError
|
||||
- returns: The removed item.
|
||||
*/
|
||||
@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!")
|
||||
}
|
||||
|
||||
guard index >= 0 && items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
if index < currentIndex {
|
||||
currentIndex -= 1
|
||||
}
|
||||
|
||||
return items.remove(at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
|
||||
|
||||
- parameter item: The item to set as the new current item.
|
||||
*/
|
||||
public func replaceCurrentItem(with item: T) {
|
||||
if current == nil {
|
||||
addItem(item)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
Remove upcoming items.
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes all items for queue
|
||||
*/
|
||||
public func clearQueue() {
|
||||
currentIndex = 0
|
||||
items.removeAll()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
//
|
||||
// QueuedAudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
/**
|
||||
An audio player that can keep track of a queue of AudioItems.
|
||||
*/
|
||||
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
|
||||
let queueManager: QueueManager = QueueManager<AudioItem>()
|
||||
|
||||
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
|
||||
queueManager.delegate = self
|
||||
}
|
||||
|
||||
/// The repeat mode for the queue player.
|
||||
public var repeatMode: RepeatMode = .off
|
||||
|
||||
public override var currentItem: AudioItem? {
|
||||
queueManager.current
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
queueManager.currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
Stops the player and clears the queue.
|
||||
*/
|
||||
public override func stop() {
|
||||
super.stop()
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
queueManager.clearQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
All items currently in the queue.
|
||||
*/
|
||||
public var items: [AudioItem] {
|
||||
queueManager.items
|
||||
}
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
queueManager.previousItems
|
||||
}
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
queueManager.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
|
||||
*/
|
||||
public override func load(item: AudioItem, playWhenReady: Bool) throws {
|
||||
try super.load(item: item, playWhenReady: playWhenReady)
|
||||
queueManager.replaceCurrentItem(with: item)
|
||||
}
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
- parameter item: The item to add.
|
||||
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItem(item)
|
||||
try load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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`
|
||||
*/
|
||||
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
|
||||
if currentItem == nil {
|
||||
queueManager.addItems(items)
|
||||
try load(item: currentItem!, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
public func add(items: [AudioItem], at index: Int) throws {
|
||||
try queueManager.addItems(items, at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
Step to the next item in the queue.
|
||||
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func next() throws {
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try load(item: nextItem, playWhenReady: shouldPlayWhenReady)
|
||||
} catch APError.QueueError.noNextItem {
|
||||
if repeatMode == .queue {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try jumpToItem(atIndex: 0, playWhenReady: shouldPlayWhenReady)
|
||||
} else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Step to the previous item in the queue.
|
||||
*/
|
||||
public func previous() throws {
|
||||
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
|
||||
|
||||
let previousItem = try queueManager.previous()
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
try load(item: previousItem, playWhenReady: shouldPlayWhenReady)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item from the queue.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
public func removeItem(at index: Int) throws {
|
||||
try queueManager.removeItem(at: index)
|
||||
}
|
||||
|
||||
/**
|
||||
Jump to a certain item in the queue.
|
||||
|
||||
- parameter index: The index of the item to jump to.
|
||||
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
|
||||
- throws: `APError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
playWhenReady ? play() : pause()
|
||||
onCurrentIndexChanged(oldIndex: index, newIndex: index)
|
||||
} else {
|
||||
let item = try queueManager.jump(to: index)
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
try load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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`
|
||||
*/
|
||||
public func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all upcoming items, those returned by `next()`
|
||||
*/
|
||||
public func removeUpcomingItems() {
|
||||
queueManager.removeUpcomingItems()
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all previous items, those returned by `previous()`
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
queueManager.removePreviousItems()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
override func AVWrapperItemDidPlayToEndTime() {
|
||||
super.AVWrapperItemDidPlayToEndTime()
|
||||
|
||||
switch repeatMode {
|
||||
case .off:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
case .track:
|
||||
try? jumpToItem(atIndex: currentIndex, playWhenReady: true)
|
||||
case .queue:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try load(item: nextItem, playWhenReady: true)
|
||||
} catch {
|
||||
try? jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - QueueManagerDelegate
|
||||
|
||||
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
|
||||
// if _currentItem is nil, then this was triggered by a reset. ignore.
|
||||
if currentItem == nil { return }
|
||||
event.queueIndex.emit(data: (oldIndex, newIndex))
|
||||
}
|
||||
|
||||
func onReceivedFirstItem() {
|
||||
event.queueIndex.emit(data: (nil, 0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerItemNotificationObserverTests: XCTestCase {
|
||||
|
||||
var item: AVPlayerItem!
|
||||
var observer: AVPlayerItemNotificationObserver!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer = AVPlayerItemNotificationObserver()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
item = nil
|
||||
observer = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testObserverHasObservedItemWhenStartedObserving() {
|
||||
observer.startObserving(item: item)
|
||||
XCTAssertNotNil(observer.observingItem)
|
||||
}
|
||||
|
||||
func testObserverHasNoObservedItemWhenEndedObserving() {
|
||||
observer.startObserving(item: item)
|
||||
observer.stopObservingCurrentItem()
|
||||
XCTAssertNil(observer.observingItem)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerItemObserverTests: XCTestCase {
|
||||
var observer: AVPlayerItemObserver!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
observer = AVPlayerItemObserver()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
observer = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testObservingItem() {
|
||||
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer.startObserving(item: item)
|
||||
XCTAssertNotNil(observer.observingItem)
|
||||
}
|
||||
|
||||
func testIsObserving() {
|
||||
XCTAssertFalse(observer.isObserving)
|
||||
|
||||
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer.startObserving(item: item)
|
||||
XCTAssertTrue(observer.isObserving)
|
||||
}
|
||||
|
||||
func testObservingInQuickSucccession() {
|
||||
for _ in 0...1000 {
|
||||
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
|
||||
observer.startObserving(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerObserverTests: XCTestCase, AVPlayerObserverDelegate {
|
||||
|
||||
var status: AVPlayer.Status?
|
||||
var timeControlStatus: AVPlayer.TimeControlStatus?
|
||||
|
||||
var player: AVPlayer!
|
||||
var observer: AVPlayerObserver!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
player = AVPlayer()
|
||||
player.volume = 0.0
|
||||
observer = AVPlayerObserver()
|
||||
observer.player = player
|
||||
observer.delegate = self
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
player = nil
|
||||
observer = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testObserverIsNotObserving() {
|
||||
XCTAssertFalse(observer.isObserving)
|
||||
}
|
||||
|
||||
func testObserverIsObservingWhenObservingStarted() {
|
||||
observer.startObserving()
|
||||
XCTAssertTrue(observer.isObserving)
|
||||
}
|
||||
|
||||
func testObserverUpdatesDelegateWhenPlayerStarted() {
|
||||
observer.startObserving()
|
||||
player.replaceCurrentItem(with: AVPlayerItem(url: URL(fileURLWithPath: Source.path)))
|
||||
player.play()
|
||||
|
||||
XCTAssertNotNil(self.status)
|
||||
XCTAssertNotNil(self.timeControlStatus)
|
||||
}
|
||||
|
||||
func testObserverIsObservingWhenObservingAgain() {
|
||||
observer.startObserving()
|
||||
observer.startObserving()
|
||||
XCTAssertTrue(observer.isObserving)
|
||||
}
|
||||
|
||||
func testObserverIsNotObservingWhenObservingStopped() {
|
||||
observer.startObserving()
|
||||
observer.stopObserving()
|
||||
XCTAssertFalse(observer.isObserving)
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
self.timeControlStatus = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AVPlayerTimeObserverTests: XCTestCase {
|
||||
|
||||
var player: AVPlayer!
|
||||
var observer: AVPlayerTimeObserver!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
player = AVPlayer()
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
player.volume = 0
|
||||
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
|
||||
observer.player = player
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
player = nil
|
||||
observer = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testObserverHasBoundaryTokenWhenStartedBoundaryTimeObserving() {
|
||||
observer.registerForBoundaryTimeEvents()
|
||||
XCTAssertNotNil(observer.boundaryTimeStartObserverToken)
|
||||
}
|
||||
|
||||
func testObserverHasNoBoundaryTokenWhenEndedBoundaryTimeObserving() {
|
||||
observer.registerForBoundaryTimeEvents()
|
||||
observer.unregisterForBoundaryTimeEvents()
|
||||
XCTAssertNil(observer.boundaryTimeStartObserverToken)
|
||||
}
|
||||
|
||||
func testObserverHasPeriodicTokenWhenStartedPeriodicTimeObserving() {
|
||||
observer.registerForPeriodicTimeEvents()
|
||||
XCTAssertNotNil(observer.periodicTimeObserverToken)
|
||||
}
|
||||
|
||||
func testObserverHasNoPeriodicTokenWhenEndedPeriodicTimeObserving() {
|
||||
observer.registerForPeriodicTimeEvents()
|
||||
observer.unregisterForPeriodicEvents()
|
||||
XCTAssertNil(observer.periodicTimeObserverToken)
|
||||
}
|
||||
}
|
||||
+133
-84
@@ -1,14 +1,12 @@
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class AVPlayerWrapperTests: XCTestCase {
|
||||
|
||||
|
||||
var wrapper: AVPlayerWrapper!
|
||||
var holder: AVPlayerWrapperDelegateHolder!
|
||||
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
wrapper = AVPlayerWrapper()
|
||||
@@ -17,25 +15,25 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
holder = AVPlayerWrapperDelegateHolder()
|
||||
wrapper.delegate = holder
|
||||
}
|
||||
|
||||
|
||||
override func tearDown() {
|
||||
wrapper = nil
|
||||
holder = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - State tests
|
||||
|
||||
func test_AVPlayerWrapper__state__should_be_idle() {
|
||||
XCTAssert(wrapper.state == AVPlayerWrapperState.idle)
|
||||
|
||||
func testAVPlayerWrapperStateShouldBeIdle() {
|
||||
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.idle)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_loading_a_source__should_be_loading() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenLoadingSourceShouldBeLoading() {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.loading)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_loading_a_source__should_eventually_be_ready() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenLoadingSourceShouldEventuallyBeReady() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if state == .ready {
|
||||
@@ -45,8 +43,8 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_playing_a_source__should_be_playing() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenPlayingSourceShouldBePlaying() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if state == .playing {
|
||||
@@ -56,65 +54,76 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_pausing_a_source__should_be_paused() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenPausingSourceShouldBePaused() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.pause()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
case .playing:
|
||||
self.wrapper.pause()
|
||||
case .paused:
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_toggling_from_play__should_be_paused() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenTogglingFromPlayShouldBePaused() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.togglePlaying()
|
||||
case .paused: expectation.fulfill()
|
||||
default: break
|
||||
case .playing:
|
||||
self.wrapper.togglePlaying()
|
||||
case .paused:
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_stopping__should_be_stopped() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenStoppingShouldBeStopped() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: self.wrapper.stop()
|
||||
case .idle: expectation.fulfill()
|
||||
default: break
|
||||
case .playing:
|
||||
self.wrapper.stop()
|
||||
case .stopped:
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__loading_with_intial_time__should_be_playing() {
|
||||
|
||||
func testAVPlayerWrapperStateLoadingWithInitialTimeShouldBePlaying() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
switch state {
|
||||
case .playing: expectation.fulfill()
|
||||
default: break
|
||||
case .playing:
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Duration tests
|
||||
|
||||
func test_AVPlayerWrapper__duration__should_be_0() {
|
||||
XCTAssert(wrapper.duration == 0.0)
|
||||
|
||||
func testAVPlayerWrapperDurationShouldBeZero() {
|
||||
XCTAssertEqual(wrapper.duration, 0.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__duration__loading_a_source__should_not_be_0() {
|
||||
|
||||
func testAVPlayerWrapperDurationLoadingSourceShouldNotBeZero() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { _ in
|
||||
if self.wrapper.duration > 0 {
|
||||
@@ -124,16 +133,16 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Current time tests
|
||||
|
||||
func test_AVPlayerWrapper__currentTime__should_be_0() {
|
||||
XCTAssert(wrapper.currentTime == 0)
|
||||
|
||||
func testAVPlayerWrapperCurrentTimeShouldBeZero() {
|
||||
XCTAssertEqual(wrapper.currentTime, 0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Seeking
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek() {
|
||||
|
||||
func testAVPlayerWrapperSeekingShouldSeek() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
@@ -146,7 +155,7 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
|
||||
func testAVPlayerWrapperSeekingShouldSeekWhileNotYetLoaded() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
@@ -156,8 +165,21 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
|
||||
func testAVPlayerWrapperSeekByShouldSeek() {
|
||||
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 testAVPlayerWrapperLoadingSourceWithInitialTimeShouldSeek() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
@@ -165,14 +187,14 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Rate tests
|
||||
|
||||
func test_AVPlayerWrapper__rate__should_be_0() {
|
||||
XCTAssert(wrapper.rate == 0.0)
|
||||
|
||||
func testAVPlayerWrapperRateShouldBe1() {
|
||||
XCTAssertEqual(wrapper.rate, 1)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__rate__playing_a_source__should_be_1() {
|
||||
|
||||
func testAVPlayerWrapperRatePlayingSourceShouldBe1() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if self.wrapper.rate == 1.0 {
|
||||
@@ -182,63 +204,90 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__timeObserver__when_updated__should_update_the_observers_periodicObserverTimeInterval() {
|
||||
wrapper.timeEventFrequency = .everySecond
|
||||
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everySecond.getTime())
|
||||
wrapper.timeEventFrequency = .everyHalfSecond
|
||||
XCTAssert(wrapper.playerTimeObserver.periodicObserverTimeInterval == TimeEventFrequency.everyHalfSecond.getTime())
|
||||
}
|
||||
|
||||
func testAVPlayerWrapperTimeObserverWhenUpdatedShouldUpdateTheObserversPeriodicObserverTimeInterval() {
|
||||
wrapper.timeEventFrequency = .everySecond
|
||||
XCTAssertEqual(wrapper.playerTimeObserver.periodicObserverTimeInterval, TimeEventFrequency.everySecond.getTime())
|
||||
wrapper.timeEventFrequency = .everyHalfSecond
|
||||
XCTAssertEqual(wrapper.playerTimeObserver.periodicObserverTimeInterval, TimeEventFrequency.everyHalfSecond.getTime())
|
||||
}
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "AVPlayerWrapperDelegateHolder.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
didSeekTo?(seconds)
|
||||
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
|
||||
didSeekTo?(seconds)
|
||||
}
|
||||
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
if let state = self.state {
|
||||
self.stateUpdate?(state)
|
||||
}
|
||||
didUpdateDuration?(duration)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import XCTest
|
||||
import MediaPlayer
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerEventTests: XCTestCase {
|
||||
|
||||
class EventListener {
|
||||
var handleEvent: ((Void)) -> Void = { _ in }
|
||||
}
|
||||
|
||||
var event: AudioPlayer.Event<(Void)>!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
event = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
event = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testEventAddListener() {
|
||||
let listener = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
waitTrue(self.event.invokers.count > 0, timeout: 5)
|
||||
}
|
||||
|
||||
func testEventRemoveListener() {
|
||||
var listener: EventListener! = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
listener = nil
|
||||
event.emit(data: ())
|
||||
|
||||
waitEqual(self.event.invokers.count, 0, timeout: 5)
|
||||
}
|
||||
|
||||
func testEventAddMultipleListeners() {
|
||||
var listeners = [EventListener]()
|
||||
|
||||
listeners = (0..<15).map { _ in
|
||||
let listener = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
return listener
|
||||
}
|
||||
|
||||
waitEqual(self.event.invokers.count, listeners.count, timeout: 5)
|
||||
}
|
||||
|
||||
func testEventRemoveOneListener() {
|
||||
var listeners = [EventListener]()
|
||||
|
||||
listeners = (0..<15).map { _ in
|
||||
let listener = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
return listener
|
||||
}
|
||||
|
||||
let listenerToRemove = listeners[listeners.count / 2]
|
||||
event.removeListener(listenerToRemove)
|
||||
|
||||
waitEqual(self.event.invokers.count, listeners.count - 1, timeout: 5)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
import XCTest
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
var playerStateEventListener: PlayerStateEventListener!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
playerStateEventListener = PlayerStateEventListener()
|
||||
audioPlayer.event.stateChange.addListener(playerStateEventListener, playerStateEventListener.handleEvent)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Load
|
||||
|
||||
func testLoadAudioItemNeverMutatesPlayWhenReadyToFalse() {
|
||||
audioPlayer.playWhenReady = true
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
XCTAssertTrue(audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testLoadAudioItemNeverMutatesPlayWhenReadyToTrue() {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
XCTAssertFalse(audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testLoadAudioItemMutatesPlayWhenReadyToFalse() {
|
||||
audioPlayer.playWhenReady = true
|
||||
XCTAssertTrue(audioPlayer.playWhenReady)
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertFalse(audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testLoadAudioItemMutatesPlayWhenReadyToTrue() {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
XCTAssertTrue(audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testLoadAudioItemSeeksWhenInitialTimeIsSet() {
|
||||
let expectation = XCTestExpectation(description: "Seek completion")
|
||||
|
||||
var seekCompleted = false
|
||||
listener.onSeekCompletion = {
|
||||
seekCompleted = true
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
audioPlayer.playWhenReady = false
|
||||
XCTAssertFalse(audioPlayer.playWhenReady)
|
||||
audioPlayer.load(item: FiveSecondSourceWithInitialTimeOfFourSeconds.getAudioItem())
|
||||
|
||||
wait(for: [expectation], timeout: 5)
|
||||
|
||||
XCTAssertTrue(seekCompleted)
|
||||
XCTAssertTrue(audioPlayer.currentTime >= 4)
|
||||
}
|
||||
|
||||
// MARK: - Duration
|
||||
|
||||
func testSetDurationAfterLoading() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
|
||||
}
|
||||
|
||||
func testOnUpdateDurationReceivedAfterLoading() {
|
||||
let expectation = XCTestExpectation(description: "Update duration received")
|
||||
|
||||
var receivedUpdateDuration = false
|
||||
listener.onUpdateDuration = { duration in
|
||||
receivedUpdateDuration = true
|
||||
XCTAssertEqual(duration, 5, accuracy: 0.1)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
|
||||
wait(for: [expectation], timeout: 5) // Adjust the timeout as needed
|
||||
|
||||
XCTAssertTrue(receivedUpdateDuration)
|
||||
}
|
||||
|
||||
func testResetDurationAfterLoadingAgain() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
|
||||
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
|
||||
}
|
||||
|
||||
func testResetDurationAfterReset() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
|
||||
audioPlayer.clear()
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
}
|
||||
|
||||
// MARK: - Failure
|
||||
|
||||
func testFailEventOnLoadWithNonMalformedURL() {
|
||||
let expectation = XCTestExpectation(description: "Fail event received on load with non-malformed URL")
|
||||
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: "", // malformed url
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
|
||||
wait(for: [expectation], timeout: 5) // Adjust the timeout as needed
|
||||
|
||||
XCTAssertNotNil(audioPlayer.playbackError)
|
||||
XCTAssertEqual(audioPlayer.playerState, .failed)
|
||||
XCTAssertTrue(didReceiveFail)
|
||||
}
|
||||
|
||||
func testFailEventOnLoadWithNonExistingResource() {
|
||||
let expectation = XCTestExpectation(description: "Fail event received on load with non-existing resource")
|
||||
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
wait(for: [expectation], timeout: 10) // Adjust the timeout as needed
|
||||
|
||||
XCTAssertNotNil(audioPlayer.playbackError)
|
||||
XCTAssertEqual(audioPlayer.playerState, .failed)
|
||||
XCTAssertTrue(didReceiveFail)
|
||||
}
|
||||
|
||||
func testRetryLoadingAfterFailure() {
|
||||
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)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
|
||||
|
||||
audioPlayer.play()
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
|
||||
}
|
||||
|
||||
func testRetryLoadingAfterFailureWithPlayWhenReady() {
|
||||
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)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
|
||||
|
||||
audioPlayer.playWhenReady = true
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
|
||||
}
|
||||
|
||||
func testRetryLoadingAfterFailureWithReload() {
|
||||
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)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
|
||||
|
||||
audioPlayer.reload(startFromCurrentTime: true)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
|
||||
}
|
||||
|
||||
func testLoadResourceSucceedsAfterPreviousFailure() {
|
||||
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)
|
||||
waitTrue(didReceiveFail, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .failed, timeout: 5)
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .failed], timeout: 5)
|
||||
|
||||
self.audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitTrue(self.audioPlayer.playbackError == nil, timeout: 5)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .playing], timeout: 5)
|
||||
}
|
||||
|
||||
func testLoadResourceSucceedsAfterPreviousFailureWithPlayWhenReady() {
|
||||
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)
|
||||
waitTrue(didReceiveFail, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .failed, timeout: 5)
|
||||
|
||||
self.audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitTrue(self.audioPlayer.playbackError == nil, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - States
|
||||
|
||||
func testInitialStateIsIdle() {
|
||||
XCTAssertEqual(audioPlayer.playerState, .idle)
|
||||
}
|
||||
|
||||
func testLoadingStateAfterLoadSource() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, .loading)
|
||||
}
|
||||
|
||||
func testReadyStateAfterLoadSource() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterLoadSourceWithPlayWhenReady() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEvents() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.play()
|
||||
expectedEvents.append(.playing)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
}
|
||||
|
||||
func testUpdatePlayWhenReadyAfterExternalPause() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
|
||||
|
||||
// Simulate AVPlayer becoming paused due to external reason:
|
||||
audioPlayer.wrapper.rate = 0
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
XCTAssertFalse(self.audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEventsAtEndCallStop() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.stop()
|
||||
expectedEvents.append(.stopped)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEventsAfterLoadingAfterReset() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expectedEvents.append(contentsOf: [.loading, .playing])
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterPlay() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
|
||||
|
||||
audioPlayer.play()
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testPausedStateAfterPause() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
|
||||
audioPlayer.pause()
|
||||
waitEqual(self.audioPlayer.playerState, .paused, timeout: 5)
|
||||
}
|
||||
|
||||
func testPausedStateAfterSettingPlayWhenReadyToFalse() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
|
||||
audioPlayer.playWhenReady = false
|
||||
waitEqual(self.audioPlayer.playerState, .paused, timeout: 5)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterSettingPlayWhenReadyToTrue() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
|
||||
|
||||
audioPlayer.playWhenReady = true
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testStoppedStateAfterStop() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - State (Current Time)
|
||||
|
||||
func testInitialCurrentTime() {
|
||||
XCTAssertEqual(audioPlayer.currentTime, 0.0)
|
||||
}
|
||||
|
||||
func testSecondsElapseEventEmittedWhenPlaying() {
|
||||
var onSecondsElapseTime = 0.0
|
||||
|
||||
audioPlayer.timeEventFrequency = .everyQuarterSecond
|
||||
listener.onSecondsElapse = { time in
|
||||
onSecondsElapseTime = time
|
||||
}
|
||||
|
||||
audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
waitTrue(onSecondsElapseTime > 0, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Buffer
|
||||
|
||||
func testAutomaticallyWaitsToMinimizeStalling() {
|
||||
XCTAssertTrue(audioPlayer.automaticallyWaitsToMinimizeStalling)
|
||||
}
|
||||
|
||||
func testBufferDurationZero() {
|
||||
XCTAssertEqual(audioPlayer.bufferDuration, 0)
|
||||
}
|
||||
|
||||
func testBufferDurationDisablesAutomaticallyWaitsToMinimizeStalling() {
|
||||
audioPlayer.bufferDuration = 1
|
||||
XCTAssertEqual(audioPlayer.bufferDuration, 1)
|
||||
XCTAssertFalse(audioPlayer.automaticallyWaitsToMinimizeStalling)
|
||||
}
|
||||
|
||||
func testEnablingAutomaticallyWaitsToMinimizeStallingSetsBufferDurationToZero() {
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
||||
XCTAssertEqual(audioPlayer.bufferDuration, 0)
|
||||
}
|
||||
|
||||
// MARK: - Seek
|
||||
|
||||
func testSeekingBeforeLoadingComplete() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
XCTAssertTrue(audioPlayer.playerState == .loading)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: 5)
|
||||
}
|
||||
|
||||
func testSeekingAfterLoadingComplete() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: 5)
|
||||
}
|
||||
|
||||
func testSeekingWhenPaused() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitEqual(self.audioPlayer.currentTime, 4.75, timeout: 5)
|
||||
}
|
||||
|
||||
func testSeekingWhenStopped() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.play()
|
||||
waitForSeek(audioPlayer, to: 2)
|
||||
audioPlayer.stop()
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Rate
|
||||
|
||||
func testRateInitially() {
|
||||
XCTAssertEqual(audioPlayer.rate, 1)
|
||||
}
|
||||
|
||||
func testSpeedUpPlayback() {
|
||||
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
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
|
||||
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start)
|
||||
XCTAssertLessThan(duration, 1, "Duration should be less than 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
func testSlowDownPlayback() {
|
||||
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)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
|
||||
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start)
|
||||
XCTAssertLessThanOrEqual(duration, 1, "Duration should be less than or equal to 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Current Item
|
||||
|
||||
func testCurrentItemInitially() {
|
||||
XCTAssertNil(audioPlayer.currentItem, "Current item should be nil initially")
|
||||
}
|
||||
|
||||
func testCurrentItemAfterLoading() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), Source.getAudioItem().getSourceUrl(), "Current item should not be nil after loading")
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
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.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 handleStateChange(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
onSeekCompletion?()
|
||||
}
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioSessionControllerTests: XCTestCase {
|
||||
var audioSessionController: AudioSessionController!
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioSessionController = AudioSessionController(audioSession: NonFailingAudioSession())
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioSessionController = nil
|
||||
delegate = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testAudioSessionIsInactive() {
|
||||
XCTAssertFalse(audioSessionController.audioSessionIsActive)
|
||||
}
|
||||
|
||||
func testActivateSession() {
|
||||
do {
|
||||
try audioSessionController.activateSession()
|
||||
XCTAssertTrue(audioSessionController.audioSessionIsActive)
|
||||
} catch {
|
||||
XCTFail("Failed to activate session: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testDeactivateSession() {
|
||||
do {
|
||||
try audioSessionController.activateSession()
|
||||
try audioSessionController.deactivateSession()
|
||||
XCTAssertFalse(audioSessionController.audioSessionIsActive)
|
||||
} catch {
|
||||
XCTFail("Failed to deactivate session: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testIsObservingForInterruptions() {
|
||||
XCTAssertTrue(audioSessionController.isObservingForInterruptions)
|
||||
}
|
||||
|
||||
func testIsObservingForInterruptionsFalse() {
|
||||
audioSessionController.isObservingForInterruptions = false
|
||||
XCTAssertFalse(audioSessionController.isObservingForInterruptions)
|
||||
}
|
||||
|
||||
func testInterruptionEnded() {
|
||||
let notification = Notification(
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0),
|
||||
AVAudioSessionInterruptionOptionKey: UInt(1),
|
||||
]
|
||||
)
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
XCTAssertEqual(delegate.interruptionType, .ended(shouldResume: true))
|
||||
}
|
||||
|
||||
func testInterruptionBegan() {
|
||||
let notification = Notification(
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil,
|
||||
userInfo: [AVAudioSessionInterruptionTypeKey: UInt(1)]
|
||||
)
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
XCTAssertEqual(delegate.interruptionType, .began)
|
||||
}
|
||||
|
||||
func testAudioSessionIsInactiveWithFailingAudioSession() {
|
||||
audioSessionController = AudioSessionController(audioSession: FailingAudioSession())
|
||||
try? audioSessionController.activateSession()
|
||||
XCTAssertFalse(audioSessionController.audioSessionIsActive)
|
||||
}
|
||||
}
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
var interruptionType: InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -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
|
||||
@@ -0,0 +1,48 @@
|
||||
import XCTest
|
||||
import MediaPlayer
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoControllerTests: XCTestCase {
|
||||
|
||||
var nowPlayingController: NowPlayingInfoController!
|
||||
var infoCenter: NowPlayingInfoCenter_Mock!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
infoCenter = NowPlayingInfoCenter_Mock()
|
||||
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: infoCenter)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
infoCenter = nil
|
||||
nowPlayingController = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testInfoDictionaryNotEmpty() {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
XCTAssertGreaterThan(nowPlayingController.info.count, 0)
|
||||
}
|
||||
|
||||
func testInfoDictionaryEmptyAfterClear() {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
nowPlayingController.clear()
|
||||
XCTAssertEqual(nowPlayingController.info.count, 0)
|
||||
}
|
||||
|
||||
func testInfoCenterNotNil() {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
XCTAssertNotNil(nowPlayingController.infoCenter.nowPlayingInfo)
|
||||
}
|
||||
|
||||
func testInfoCenterNotEmpty() {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
XCTAssertGreaterThan(nowPlayingController.infoCenter.nowPlayingInfo?.count ?? 0, 0)
|
||||
}
|
||||
|
||||
func testInfoCenterEmptyAfterClear() {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
nowPlayingController.clear()
|
||||
XCTAssertNil(nowPlayingController.infoCenter.nowPlayingInfo)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import XCTest
|
||||
import MediaPlayer
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoTests: XCTestCase {
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var nowPlayingController: NowPlayingInfoController_Mock!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
nowPlayingController = NowPlayingInfoController_Mock()
|
||||
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
|
||||
audioPlayer.automaticallyUpdateNowPlayingInfo = true
|
||||
audioPlayer.volume = 0
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
nowPlayingController = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testNowPlayingInfoControllerMetadataUpdate() {
|
||||
let item = Source.getAudioItem()
|
||||
audioPlayer.load(item: item, playWhenReady: false)
|
||||
|
||||
XCTAssertEqual(nowPlayingController.getTitle(), item.getTitle())
|
||||
XCTAssertEqual(nowPlayingController.getArtist(), item.getArtist())
|
||||
XCTAssertEqual(nowPlayingController.getAlbumTitle(), item.getAlbumTitle())
|
||||
XCTAssertNotNil(nowPlayingController.getArtwork())
|
||||
}
|
||||
|
||||
func testNowPlayingInfoControllerPlaybackValuesUpdate() {
|
||||
let item = LongSource.getAudioItem()
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
|
||||
XCTAssertNotNil(nowPlayingController.getRate())
|
||||
XCTAssertNotNil(nowPlayingController.getDuration())
|
||||
XCTAssertNotNil(nowPlayingController.getCurrentTime())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import XCTest
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class QueueManagerTests: XCTestCase {
|
||||
|
||||
let dummyItem = 0
|
||||
let items: [Int] = [0, 1, 2]
|
||||
var queue: QueueManager<Int>!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
queue = QueueManager()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
queue = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Current Item
|
||||
|
||||
func testCurrentItemOnStart() {
|
||||
XCTAssertNil(queue.current)
|
||||
}
|
||||
|
||||
func testOneItemAdded() {
|
||||
queue.add(dummyItem)
|
||||
XCTAssertNil(queue.current)
|
||||
}
|
||||
|
||||
func testOneItemAddedAndJumping() {
|
||||
queue.add(dummyItem)
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
|
||||
func testOneItemAddedAndJumpingAndReplacing() {
|
||||
queue.add(self.dummyItem)
|
||||
try! queue.jump(to: 0)
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
XCTAssertEqual(queue.current, 1)
|
||||
}
|
||||
|
||||
func testReplacingCurrentItemWithEmptyQueue() {
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
XCTAssertNotNil(queue.current)
|
||||
}
|
||||
|
||||
func testAddingItemsAndJumpingToLast() {
|
||||
queue.add(self.items)
|
||||
try! queue.jump(to: queue.items.count - 1)
|
||||
XCTAssertNotNil(queue.current)
|
||||
}
|
||||
|
||||
// MARK: - Adding At Index
|
||||
|
||||
func testAddingItemAtIndexZeroWhenQueueIsEmpty() {
|
||||
try! queue.add([3], at: 0)
|
||||
XCTAssertEqual(queue.items.first, 3)
|
||||
XCTAssertNil(queue.current)
|
||||
XCTAssertEqual(queue.currentIndex, -1)
|
||||
}
|
||||
|
||||
func testAddingItemAtIndexAndJumpingToFirstItem() {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
XCTAssertEqual(queue.items.last, 2)
|
||||
}
|
||||
|
||||
func testAddingItemAtCurrentElementCount() {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
try! queue.add([3, 4, 5], at: queue.items.count)
|
||||
XCTAssertEqual(queue.items.last, 5)
|
||||
}
|
||||
|
||||
func testAddingItemBeforeTheFirstItem() {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
try! queue.add([-1], at: 0)
|
||||
XCTAssertEqual(queue.items.first, -1)
|
||||
}
|
||||
|
||||
func testAddingItemAfterTheLastItem() {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
try! queue.add([6], at: queue.items.count)
|
||||
XCTAssertEqual(queue.items.last, 6)
|
||||
}
|
||||
|
||||
func testAddingItemAtCurrentIndex() {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
|
||||
queue.next()
|
||||
try! queue.add([5], at: queue.currentIndex)
|
||||
|
||||
XCTAssertEqual(queue.current, 2)
|
||||
XCTAssertEqual(queue.currentIndex, 2)
|
||||
}
|
||||
|
||||
// MARK: - Add Item (No Jump)
|
||||
|
||||
func testAddOneItemWithoutJumping() {
|
||||
queue.add(0)
|
||||
XCTAssertEqual(queue.items.count, 1)
|
||||
}
|
||||
|
||||
func testReplaceItem() {
|
||||
queue.add(0)
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
XCTAssertEqual(queue.items.count, 2)
|
||||
XCTAssertEqual(queue.current, 1)
|
||||
XCTAssertEqual(queue.currentIndex, 1)
|
||||
}
|
||||
|
||||
func testCallingNextAfterReplacement() {
|
||||
queue.add(0)
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
let item = queue.next()
|
||||
XCTAssertEqual(item, 1)
|
||||
}
|
||||
|
||||
func testCallingPreviousAfterReplacement() {
|
||||
queue.add(0)
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
let item = queue.previous()
|
||||
XCTAssertEqual(item, 0)
|
||||
}
|
||||
|
||||
func testCallingNext() {
|
||||
queue.add(0)
|
||||
queue.next()
|
||||
let item = queue.next()
|
||||
XCTAssertNil(item)
|
||||
}
|
||||
|
||||
func testCallingPrevious() {
|
||||
queue.add(0)
|
||||
queue.previous()
|
||||
let item = queue.previous()
|
||||
XCTAssertNil(item)
|
||||
}
|
||||
|
||||
func testJumpToZeroAndCallNextWithWrap() {
|
||||
queue.add(0)
|
||||
try! queue.jump(to: 0)
|
||||
let nextIndex = queue.next(wrap: true)
|
||||
XCTAssertEqual(nextIndex, 0)
|
||||
}
|
||||
|
||||
func testJumpToZeroAndCallPreviousWithWrap() {
|
||||
queue.add(0)
|
||||
try! queue.jump(to: 0)
|
||||
let previousIndex = queue.previous(wrap: true)
|
||||
XCTAssertEqual(previousIndex, 0)
|
||||
}
|
||||
|
||||
// MARK: - Adding Multiple Items
|
||||
|
||||
func testAddMultipleItems() {
|
||||
queue.add(items)
|
||||
XCTAssertEqual(queue.items.count, items.count)
|
||||
XCTAssertNil(queue.current)
|
||||
XCTAssertEqual(queue.nextItems.count, 0)
|
||||
}
|
||||
|
||||
func testQueueNavigation() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
let nextItem = queue.next()
|
||||
|
||||
XCTAssertEqual(nextItem, items[1])
|
||||
XCTAssertEqual(queue.current, items[1])
|
||||
XCTAssertNotNil(queue.previousItems)
|
||||
|
||||
// Previous
|
||||
XCTAssertEqual(queue.previous(), 0)
|
||||
XCTAssertEqual(queue.current, items.first)
|
||||
|
||||
// Previous at start of queue
|
||||
XCTAssertEqual(queue.previous(), 0)
|
||||
|
||||
// Previous at start of queue with wrap
|
||||
let index3 = queue.previous(wrap: true)
|
||||
XCTAssertEqual(index3, items.count - 1)
|
||||
XCTAssertEqual(queue.currentIndex, items.count - 1)
|
||||
XCTAssertEqual(queue.current, items.last)
|
||||
|
||||
// Next at end of queue
|
||||
let index4 = queue.next()
|
||||
XCTAssertEqual(index4, items.count - 1)
|
||||
|
||||
// Next at end of queue with wrap
|
||||
let index5 = queue.next(wrap: true)
|
||||
XCTAssertEqual(index5, 0)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
XCTAssertEqual(queue.current, items.first)
|
||||
}
|
||||
|
||||
func testRemovePreviousItemsAfterNext() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
queue.next()
|
||||
queue.removePreviousItems()
|
||||
|
||||
XCTAssertEqual(queue.previousItems.count, 0)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
}
|
||||
|
||||
func testAddMoreItems() {
|
||||
queue.add(items)
|
||||
let initialItemCount = queue.items.count
|
||||
try? queue.add([10, 11, 12, 13], at: queue.items.endIndex - 1)
|
||||
XCTAssertEqual(queue.items.count, initialItemCount + 4)
|
||||
}
|
||||
|
||||
func testAddMoreItemsAtSmallerIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
let initialCurrentIndex = queue.currentIndex
|
||||
try? queue.add([10, 11, 12, 13], at: initialCurrentIndex)
|
||||
XCTAssertEqual(queue.currentIndex, initialCurrentIndex + 4)
|
||||
}
|
||||
|
||||
// MARK: - Remove
|
||||
|
||||
func testRemoveItemAtIndexLessThanCurrent() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 1)
|
||||
|
||||
let initialCurrentIndex = queue.currentIndex
|
||||
let removed = try? queue.removeItem(at: initialCurrentIndex - 1)
|
||||
|
||||
XCTAssertEqual(removed, 0)
|
||||
XCTAssertEqual(initialCurrentIndex, 1)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
}
|
||||
|
||||
func testRemoveSecondItem() {
|
||||
queue.add(items)
|
||||
let removed = try? queue.removeItem(at: 1)
|
||||
XCTAssertNotNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count - 1)
|
||||
}
|
||||
|
||||
func testRemoveLastItem() {
|
||||
queue.add(items)
|
||||
let removed = try? queue.removeItem(at: items.count - 1)
|
||||
XCTAssertNotNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count - 1)
|
||||
}
|
||||
|
||||
func testRemoveCurrentItemWhenFirstItem() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
let removed = try? queue.removeItem(at: queue.currentIndex)
|
||||
|
||||
XCTAssertNotNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count - 1)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
XCTAssertEqual(queue.current, 1)
|
||||
}
|
||||
|
||||
func testRemoveCurrentItemWhenLastItem() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: items.count - 1)
|
||||
let removed = try? queue.removeItem(at: queue.currentIndex)
|
||||
|
||||
XCTAssertNotNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count - 1)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
}
|
||||
|
||||
func testRemoveWithTooLargeIndex() {
|
||||
queue.add(items)
|
||||
let removed = try? queue.removeItem(at: items.count)
|
||||
XCTAssertNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count)
|
||||
}
|
||||
|
||||
func testRemoveWithTooSmallIndex() {
|
||||
queue.add(items)
|
||||
let removed = try? queue.removeItem(at: -1)
|
||||
XCTAssertNil(removed)
|
||||
XCTAssertEqual(queue.items.count, items.count)
|
||||
}
|
||||
|
||||
func testRemoveUpcomingItems() {
|
||||
queue.add(items)
|
||||
queue.removeUpcomingItems()
|
||||
XCTAssertEqual(queue.nextItems.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - Jump
|
||||
|
||||
// Test case 22: jumping to the current item
|
||||
func testJumpToCurrentItem() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
|
||||
let item = try! queue.jump(to: queue.currentIndex)
|
||||
XCTAssertNotNil(item)
|
||||
}
|
||||
|
||||
func testJumpToSecondItem() {
|
||||
queue.add(items)
|
||||
let _ = try? queue.jump(to: 1)
|
||||
let jumped = try? queue.jump(to: 1)
|
||||
XCTAssertNotNil(jumped)
|
||||
XCTAssertEqual(jumped, queue.current)
|
||||
XCTAssertEqual(queue.currentIndex, 1)
|
||||
}
|
||||
|
||||
func testJumpToLastItem() {
|
||||
queue.add(items)
|
||||
let jumped = try? queue.jump(to: items.count - 1)
|
||||
XCTAssertNotNil(jumped)
|
||||
XCTAssertEqual(jumped, queue.current)
|
||||
XCTAssertEqual(queue.currentIndex, items.count - 1)
|
||||
}
|
||||
|
||||
func testJumpToNegativeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
let jumped = try? queue.jump(to: -1)
|
||||
XCTAssertNil(jumped)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
}
|
||||
|
||||
func testJumpWithTooLargeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
let jumped = try? queue.jump(to: items.count)
|
||||
XCTAssertNil(jumped)
|
||||
XCTAssertEqual(queue.currentIndex, 0)
|
||||
}
|
||||
|
||||
// MARK: - Moving
|
||||
|
||||
func testMoveItemUpOne() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
try! queue.moveItem(fromIndex: queue.currentIndex, toIndex: queue.currentIndex + 1)
|
||||
XCTAssertEqual(queue.currentIndex, 1)
|
||||
}
|
||||
|
||||
func testMoveFromNegativeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
|
||||
XCTAssertThrowsError(try queue.moveItem(fromIndex: -1, toIndex: queue.currentIndex + 1))
|
||||
}
|
||||
|
||||
func testMoveFromTooLargeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
|
||||
XCTAssertThrowsError(try queue.moveItem(fromIndex: queue.items.count, toIndex: queue.currentIndex + 1))
|
||||
}
|
||||
|
||||
func testMoveToNegativeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.jump(to: 0)
|
||||
|
||||
XCTAssertThrowsError(try queue.moveItem(fromIndex: queue.currentIndex + 1, toIndex: -1))
|
||||
}
|
||||
|
||||
func testMoveToTooLargeIndex() {
|
||||
queue.add(items)
|
||||
try! queue.moveItem(fromIndex: 0, toIndex: queue.items.count)
|
||||
XCTAssertEqual(queue.items.last, 0)
|
||||
XCTAssertEqual(queue.items.first, 1)
|
||||
}
|
||||
|
||||
func testMoveSecondToThird() {
|
||||
queue.add(items)
|
||||
try? queue.moveItem(fromIndex: 1, toIndex: 3)
|
||||
XCTAssertEqual(queue.items, [0, 2, 1])
|
||||
}
|
||||
|
||||
// MARK: - Clear
|
||||
|
||||
func testClearQueue() {
|
||||
queue.add(items)
|
||||
queue.clearQueue()
|
||||
XCTAssertEqual(queue.currentIndex, -1)
|
||||
XCTAssertEqual(queue.items.count, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
import XCTest
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class QueuedAudioPlayerTests: XCTestCase {
|
||||
|
||||
var audioPlayer: QueuedAudioPlayer!
|
||||
var currentItemEventListener: QueuedAudioPlayer.CurrentItemEventListener!
|
||||
var playbackEndEventListener: QueuedAudioPlayer.PlaybackEndEventListener!
|
||||
var playerStateEventListener: QueuedAudioPlayer.PlayerStateEventListener!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = QueuedAudioPlayer()
|
||||
|
||||
currentItemEventListener = QueuedAudioPlayer.CurrentItemEventListener()
|
||||
audioPlayer.event.currentItem.addListener(
|
||||
currentItemEventListener,
|
||||
currentItemEventListener.handleEvent
|
||||
)
|
||||
|
||||
playbackEndEventListener = QueuedAudioPlayer.PlaybackEndEventListener()
|
||||
audioPlayer.event.playbackEnd.addListener(
|
||||
playbackEndEventListener,
|
||||
playbackEndEventListener.handleEvent
|
||||
)
|
||||
|
||||
playerStateEventListener = QueuedAudioPlayer.PlayerStateEventListener()
|
||||
audioPlayer.event.stateChange.addListener(
|
||||
playerStateEventListener,
|
||||
playerStateEventListener.handleEvent
|
||||
)
|
||||
|
||||
audioPlayer.volume = 0.0
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer.event.currentItem.removeListener(currentItemEventListener)
|
||||
currentItemEventListener = nil
|
||||
|
||||
audioPlayer.event.playbackEnd.removeListener(playbackEndEventListener)
|
||||
playbackEndEventListener = nil
|
||||
|
||||
audioPlayer.event.stateChange.removeListener(playerStateEventListener)
|
||||
playerStateEventListener = nil
|
||||
|
||||
audioPlayer = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Current Item
|
||||
|
||||
func testCurrentItemOnCreate() {
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
func testAddingOneItem() {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertNotNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
func testLoadItemAfterAdding() {
|
||||
testAddingOneItem()
|
||||
let item = Source.getAudioItem()
|
||||
audioPlayer.load(item: item)
|
||||
|
||||
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), item.getSourceUrl())
|
||||
}
|
||||
|
||||
func testRemovingItemAfterAdding() {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem())
|
||||
audioPlayer.repeatMode = RepeatMode.track
|
||||
audioPlayer.play()
|
||||
audioPlayer.seek(to: 4)
|
||||
try? audioPlayer.removeItem(at: audioPlayer.currentIndex)
|
||||
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.idle)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .idle], timeout: 5)
|
||||
}
|
||||
|
||||
func testLoadAfterRemoval() {
|
||||
testRemovingItemAfterAdding()
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
XCTAssertNotEqual(audioPlayer.currentItem?.getSourceUrl(), FiveSecondSource.getAudioItem().getSourceUrl())
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .idle, .loading, .playing], timeout: 5)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.playing)
|
||||
}
|
||||
|
||||
func testAddingMultipleItems() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
|
||||
XCTAssertNotNil(audioPlayer.currentItem)
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 0)
|
||||
}
|
||||
|
||||
func testRemoveItemAfterAddingMultiple() {
|
||||
testAddingMultipleItems()
|
||||
|
||||
try? audioPlayer.removeItem(at: 0)
|
||||
XCTAssertEqual(audioPlayer.items.count, 1)
|
||||
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), ShortSource.getAudioItem().getSourceUrl())
|
||||
}
|
||||
|
||||
// MARK: - Next Items
|
||||
|
||||
func testNextItemsEmptyOnCreate() {
|
||||
XCTAssertTrue(audioPlayer.nextItems.isEmpty)
|
||||
}
|
||||
|
||||
func testNextItemsAfterAddingTwoItems() {
|
||||
audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
XCTAssertEqual(audioPlayer.nextItems.count, 1)
|
||||
}
|
||||
|
||||
func testNextItemsWhileNavigationAfterAddingTwoItems() {
|
||||
testNextItemsAfterAddingTwoItems()
|
||||
|
||||
// Test next
|
||||
audioPlayer.next()
|
||||
XCTAssertEqual(audioPlayer.nextItems.count, 0)
|
||||
|
||||
// Test previous
|
||||
audioPlayer.previous()
|
||||
XCTAssertEqual(audioPlayer.nextItems.count, 1)
|
||||
}
|
||||
|
||||
func testRemovingOneItem() {
|
||||
let queue = [Source.getAudioItem(), Source.getAudioItem()]
|
||||
audioPlayer.add(items: queue)
|
||||
try? audioPlayer.removeItem(at: queue.count - 1)
|
||||
XCTAssertEqual(audioPlayer.nextItems.count, queue.count - 2)
|
||||
}
|
||||
|
||||
func testJumpingToLastItem() {
|
||||
let queue = [Source.getAudioItem(), Source.getAudioItem()]
|
||||
audioPlayer.add(items: queue)
|
||||
try? audioPlayer.jumpToItem(atIndex: queue.count - 1)
|
||||
XCTAssertTrue(audioPlayer.nextItems.isEmpty)
|
||||
}
|
||||
|
||||
func testRemovingUpcomingItems() {
|
||||
audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
audioPlayer.removeUpcomingItems()
|
||||
XCTAssertTrue(audioPlayer.nextItems.isEmpty)
|
||||
}
|
||||
|
||||
func testStopping() {
|
||||
audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
audioPlayer.stop()
|
||||
XCTAssertEqual(audioPlayer.nextItems.count, 1)
|
||||
}
|
||||
|
||||
// MARK: - Previous Items
|
||||
|
||||
func testPreviousItemsEmptyOnCreate() {
|
||||
XCTAssertTrue(audioPlayer.previousItems.isEmpty)
|
||||
}
|
||||
|
||||
func testPreviousItemsAfterAddingTwoItems() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
XCTAssertTrue(audioPlayer.previousItems.isEmpty)
|
||||
}
|
||||
|
||||
func testPreviousItemsWhileNavigationAfterAddingTwoItems() {
|
||||
testPreviousItemsAfterAddingTwoItems()
|
||||
|
||||
// Test next
|
||||
audioPlayer.next()
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .paused, .loading, .paused], timeout: 5)
|
||||
XCTAssertEqual(audioPlayer.previousItems.count, 1)
|
||||
waitEqual(self.playbackEndEventListener.lastReason, .skippedToNext, timeout: 5)
|
||||
|
||||
// Test stop
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
|
||||
|
||||
// Test stop again
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
|
||||
|
||||
// Test previous
|
||||
audioPlayer.previous()
|
||||
waitEqual(self.audioPlayer.playerState, .loading, timeout: 5)
|
||||
// should not have emitted playbackEnd .skippedToPrevious because playback was already stopped previously
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
|
||||
|
||||
}
|
||||
|
||||
func testRemoveAllPreviousItems() {
|
||||
testPreviousItemsAfterAddingTwoItems()
|
||||
audioPlayer.removePreviousItems()
|
||||
XCTAssertEqual(audioPlayer.previousItems.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - Pause
|
||||
|
||||
func testPauseWithPlayWhenReadyTrue() {
|
||||
audioPlayer.playWhenReady = true
|
||||
XCTAssertTrue(audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testPauseOnEmptyQueue() {
|
||||
audioPlayer.playWhenReady = true
|
||||
audioPlayer.pause()
|
||||
XCTAssertFalse(audioPlayer.playWhenReady)
|
||||
|
||||
// It should not have mutated player state to .paused because playback was already idle
|
||||
XCTAssertEqual(playerStateEventListener.states, [])
|
||||
}
|
||||
|
||||
func testPauseWithItemAndPausingDirectly() {
|
||||
audioPlayer.playWhenReady = true
|
||||
|
||||
// Adding an item and pausing directly
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.pause()
|
||||
|
||||
// It should have gone into .paused state from .loading and then into .ready because playback can be started
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Stop
|
||||
|
||||
func testStopOnEmptyQueue() {
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.playerStateEventListener.states, [.stopped], timeout: 5)
|
||||
|
||||
// It should not have emitted a playbackEnd event
|
||||
XCTAssertNil(playbackEndEventListener.lastReason)
|
||||
}
|
||||
|
||||
func testStopWithTwoItems() {
|
||||
audioPlayer.add(items: [
|
||||
FiveSecondSource.getAudioItem(),
|
||||
FiveSecondSource.getAudioItem()
|
||||
])
|
||||
audioPlayer.stop()
|
||||
|
||||
// It should have emitted a playbackEnd .playerStopped event
|
||||
waitEqual(self.playbackEndEventListener.lastReason, .playerStopped, timeout: 5)
|
||||
|
||||
// It should have mutated player state from .loading to .stopped
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .stopped], timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Load
|
||||
|
||||
func testLoadItemOnEmptyQueue() {
|
||||
// Calling load(item) on an empty queue should set currentItem
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertNotNil(audioPlayer.currentItem)
|
||||
|
||||
// It should have started loading, but not playing yet
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: 5)
|
||||
}
|
||||
|
||||
func testLoadItemAfterPlaying() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertNotNil(audioPlayer.currentItem)
|
||||
|
||||
// It should have started playing
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .playing], timeout: 5)
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
|
||||
XCTAssertEqual(audioPlayer.items.count, 1)
|
||||
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), Source.getAudioItem().getSourceUrl())
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering.prefix(4), [.loading, .playing, .loading, .playing], timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Next
|
||||
|
||||
func testNextOnEmptyQueue() {
|
||||
audioPlayer.next()
|
||||
// should not have emitted a playbackEnd event
|
||||
XCTAssertNil(playbackEndEventListener.lastReason)
|
||||
}
|
||||
|
||||
func testNextWhenPaused() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenPausedWithoutPlaying() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.pause()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenPlaying() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
// should go to previous item and play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Previous
|
||||
|
||||
func testPreviousOnEmptyQueue() {
|
||||
audioPlayer.previous()
|
||||
// should not have emitted a playbackEnd event
|
||||
XCTAssertNil(playbackEndEventListener.lastReason)
|
||||
}
|
||||
|
||||
func testPreviousWhenPlaying() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()], playWhenReady: true)
|
||||
audioPlayer.next()
|
||||
audioPlayer.previous()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
// should go to previous item and play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testPreviousWhenPaused() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.next()
|
||||
audioPlayer.pause()
|
||||
audioPlayer.previous()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Move
|
||||
|
||||
func testMoveItemsRepeatModeOff() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
|
||||
// Move the first (currently playing track) above the second and seek to near the end of the track
|
||||
try? audioPlayer.moveItem(fromIndex: 0, toIndex: 1)
|
||||
audioPlayer.repeatMode = RepeatMode.off
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ended, timeout: 5)
|
||||
}
|
||||
|
||||
func testMoveItemsRepeatModeQueue() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
|
||||
// Move the first (currently playing track) above the second and seek to near the end of the track
|
||||
try? audioPlayer.moveItem(fromIndex: 0, toIndex: 1)
|
||||
audioPlayer.repeatMode = RepeatMode.queue
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testMoveItemsRepeatModeTrack() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
|
||||
// Move the first (currently playing track) above the second and seek to near the end of the track
|
||||
try? audioPlayer.moveItem(fromIndex: 0, toIndex: 1)
|
||||
audioPlayer.repeatMode = RepeatMode.track
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitTrue(self.audioPlayer.currentTime < 4.6, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 1)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Repeat Mode (Off - Two Items)
|
||||
|
||||
func setupRepeatModeOffTests() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
func testTrackEndWhenRepeatModeOff() {
|
||||
setupRepeatModeOffTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
|
||||
// Allow final track to end
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.index, 1, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeOff() {
|
||||
setupRepeatModeOffTests()
|
||||
audioPlayer.play()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
|
||||
// Calling next on the final track
|
||||
audioPlayer.next()
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Repeat Mode (Track - Two Items)
|
||||
|
||||
func setupRepeatModeTrackTests() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
func testRestartTrackWhenRepeatModeTrack() {
|
||||
setupRepeatModeTrackTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeTrack() {
|
||||
setupRepeatModeTrackTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Repeat Mode (Queue - Two Items)
|
||||
|
||||
func setupRepeatModeQueueTests() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
func testSeekToEndWhenRepeatModeQueue() {
|
||||
setupRepeatModeQueueTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
|
||||
// Allow the final track to end
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeQueue() {
|
||||
setupRepeatModeQueueTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextTwiceWhenRepeatModeQueue() {
|
||||
setupRepeatModeQueueTests()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 0)
|
||||
XCTAssertNil(currentItemEventListener.lastIndex)
|
||||
|
||||
audioPlayer.next()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 1)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
|
||||
|
||||
audioPlayer.next()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 0)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Repeat Mode (Off - One Item)
|
||||
|
||||
func setupRepeatModeOffOneItemTests() {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
func testTrackEndWhenRepeatModeOffOneItem() {
|
||||
setupRepeatModeOffOneItemTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeOffOneItem() {
|
||||
setupRepeatModeOffOneItemTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
// TODO: Test this more thoroughly?
|
||||
}
|
||||
|
||||
// MARK: - Repeat Mode (Track - One Item)
|
||||
|
||||
func setupRepeatModeTrackOneItemTests() {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
func testRestartTrackWhenRepeatModeTrackOneItem() {
|
||||
setupRepeatModeTrackOneItemTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, nil, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeTrackOneItem() {
|
||||
setupRepeatModeTrackOneItemTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Repeat Mode (Queue - One Item)
|
||||
|
||||
func setupRepeatModeQueueOneItemTests() {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
func testSeekToEndWhenRepeatModeQueueOneItem() {
|
||||
setupRepeatModeQueueOneItemTests()
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.5, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime < 1, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeQueueOneItem() {
|
||||
setupRepeatModeQueueOneItemTests()
|
||||
waitForSeek(audioPlayer, to: 2)
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
waitTrue(self.audioPlayer.currentTime < 1.9, timeout: 5)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
|
||||
}
|
||||
}
|
||||
|
||||
extension QueuedAudioPlayer {
|
||||
|
||||
class SeekEventListener {
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "SeekEventListener.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
var _eventResult: (Double, Bool) = (-1, false)
|
||||
var eventResult: (Double, Bool) {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
_eventResult
|
||||
}
|
||||
}
|
||||
}
|
||||
func handleEvent(seconds: Double, didFinish: Bool) {
|
||||
lockQueue.sync {
|
||||
_eventResult = (seconds, didFinish)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CurrentItemEventListener {
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "CurrentItemEventListener.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
var _item: AudioItem? = nil
|
||||
var _index: Int? = nil
|
||||
var _lastItem: AudioItem? = nil
|
||||
var _lastIndex: Int? = nil
|
||||
var _lastPosition: Double? = nil
|
||||
|
||||
var item: AudioItem? {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _item
|
||||
}
|
||||
}
|
||||
}
|
||||
var index: Int? {
|
||||
return lockQueue.sync {
|
||||
return _index
|
||||
}
|
||||
}
|
||||
var lastItem: AudioItem? {
|
||||
return lockQueue.sync {
|
||||
return _lastItem
|
||||
}
|
||||
}
|
||||
var lastIndex: Int? {
|
||||
return lockQueue.sync {
|
||||
return _lastIndex
|
||||
}
|
||||
}
|
||||
var lastPosition: Double? {
|
||||
return lockQueue.sync {
|
||||
return _lastPosition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handleEvent(
|
||||
item: AudioItem?,
|
||||
index: Int?,
|
||||
lastItem: AudioItem?,
|
||||
lastIndex: Int?,
|
||||
lastPosition: Double?
|
||||
) {
|
||||
lockQueue.sync {
|
||||
_item = item
|
||||
_index = index
|
||||
_lastItem = lastItem
|
||||
_lastIndex = lastIndex
|
||||
_lastPosition = lastPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlaybackEndEventListener {
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "PlaybackEndEventListener.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
var _lastReason: PlaybackEndedReason? = nil
|
||||
var lastReason: PlaybackEndedReason? {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _lastReason
|
||||
}
|
||||
}
|
||||
}
|
||||
var _reasons: [PlaybackEndedReason] = []
|
||||
var reasons: [PlaybackEndedReason] {
|
||||
get {
|
||||
return lockQueue.sync {
|
||||
return _reasons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(reason: PlaybackEndedReason) {
|
||||
lockQueue.sync {
|
||||
_lastReason = reason
|
||||
_reasons.append(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
extension XCTestCase {
|
||||
func waitForSeek(_ audioPlayer: AudioPlayer, to time: Double) {
|
||||
let seekEventListener = QueuedAudioPlayer.SeekEventListener()
|
||||
audioPlayer.event.seek.addListener(seekEventListener, seekEventListener.handleEvent)
|
||||
audioPlayer.seek(to: time)
|
||||
|
||||
waitEqual(seekEventListener.eventResult.0, time, accuracy: 0.1, timeout: 5)
|
||||
waitEqual(seekEventListener.eventResult.1, true, timeout: 5)
|
||||
}
|
||||
|
||||
func waitTrue(_ expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval) {
|
||||
let expectation = XCTestExpectation(description: "Value should eventually equal expected value")
|
||||
|
||||
DispatchQueue.global().async {
|
||||
while !expression() {
|
||||
usleep(100_000) // Sleep for 100 milliseconds
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
|
||||
func waitEqual<T: Equatable>(_ expression1: @autoclosure @escaping () -> T, _ expression2: @autoclosure @escaping () -> T, timeout: TimeInterval) {
|
||||
let expectation = XCTestExpectation(description: "Value should eventually equal expected value")
|
||||
|
||||
DispatchQueue.global().async {
|
||||
while expression1() != expression2() {
|
||||
usleep(100_000) // Sleep for 100 milliseconds
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
|
||||
func waitEqual<T: Equatable>(_ expression1: @autoclosure @escaping () -> T, _ expression2: @autoclosure @escaping () -> T, accuracy: T, timeout: TimeInterval) where T: FloatingPoint {
|
||||
let expectation = XCTestExpectation(description: "Value should eventually equal expected value with accuracy")
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let startTime = Date()
|
||||
while abs(expression1() - expression2()) > accuracy {
|
||||
if Date().timeIntervalSince(startTime) >= timeout {
|
||||
break
|
||||
}
|
||||
usleep(100_000) // Sleep for 100 milliseconds
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
return wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
|
||||
func waitEqual<T1: Equatable, T2: Equatable>(_ expression1: @autoclosure @escaping () -> (T1, T2), _ expression2: @autoclosure @escaping () -> (T1, T2), timeout: TimeInterval) {
|
||||
let expectation = XCTestExpectation(description: "Values should eventually be equal")
|
||||
|
||||
DispatchQueue.global().async {
|
||||
while expression1() != expression2() {
|
||||
usleep(100_000) // Sleep for 100 milliseconds
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
import SwiftAudioEx
|
||||
import UIKit
|
||||
|
||||
struct Source {
|
||||
static let path: String = Bundle.module.path(forResource: "TestSound", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
|
||||
}
|
||||
}
|
||||
|
||||
struct ShortSource {
|
||||
static let path: String = Bundle.module.path(forResource: "ShortTestSound", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: self.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct LongSource {
|
||||
static let path: String = Bundle.module.path(forResource: "WAV-MP3", ofType: "wav")!
|
||||
static let url: URL = URL(fileURLWithPath: LongSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: self.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
struct FiveSecondSource {
|
||||
static let path: String = Bundle.module.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.module.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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user