Compare commits
68 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 | |||
| 23fdb9b9db | |||
| 24c19aa661 | |||
| 38429c6ca8 | |||
| 72f9c5d147 | |||
| bd93898809 | |||
| 8276f38b1b | |||
| fcd5790e1e | |||
| ead7c0962e | |||
| 7ff34271e8 | |||
| 4f7a5b02a6 | |||
| af803339dc | |||
| a5bf6eb1dd | |||
| 5e0c27b990 | |||
| 6079234942 | |||
| e74b5ffe4d | |||
| 92554a187c | |||
| 473651f357 | |||
| db2f3e9af7 | |||
| a9f831a258 | |||
| cc3840d81e | |||
| 5307090ea3 | |||
| bdaee8b18f | |||
| 84d359bc4f | |||
| 40ea7ad2f9 | |||
| f2f1c1236c | |||
| a75f0d0201 | |||
| 9e4e7f6807 | |||
| dbd3b03989 | |||
| 7e19604df7 |
@@ -1,20 +1,23 @@
|
||||
name: validate
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: macos-latest
|
||||
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 }}
|
||||
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 */; };
|
||||
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
|
||||
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79326C522D0004BAF2F /* SwiftAudioEx */; };
|
||||
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79526C52382004BAF2F /* SwiftAudioEx */; };
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
|
||||
/* 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>"; };
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -103,51 +41,17 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
607FACE21AFB9204008FA782 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */,
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */,
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx 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,40 +92,10 @@
|
||||
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 = (
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */,
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -244,35 +117,12 @@
|
||||
);
|
||||
name = SwiftAudio_Example;
|
||||
packageProductDependencies = (
|
||||
9B77D79326C522D0004BAF2F /* SwiftAudioEx */,
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = SwiftAudio;
|
||||
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 */,
|
||||
9B77D79526C52382004BAF2F /* 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,52 +377,10 @@
|
||||
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;
|
||||
};
|
||||
9B77D79326C522D0004BAF2F /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
9B77D79526C52382004BAF2F /* 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: [AVMetadataItem]) -> Void)?
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
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,211 +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
|
||||
audioPlayer.bufferDuration = 0.001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
switch state {
|
||||
case .ready: expectation.fulfill()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
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,100 +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 interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0)
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventuallyNot(beNil())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: AVAudioSession.InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: AVAudioSession.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,488 +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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,311 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class QueuedAudioPlayerTests: QuickSpec {
|
||||
override func spec() {
|
||||
describe("A QueuedAudioPlayer") {
|
||||
var audioPlayer: QueuedAudioPlayer!
|
||||
beforeEach {
|
||||
audioPlayer = QueuedAudioPlayer()
|
||||
audioPlayer.bufferDuration = 0.0001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
audioPlayer.volume = 0.0
|
||||
}
|
||||
describe("its current item") {
|
||||
it("should be nil") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
|
||||
context("when adding one item") {
|
||||
var item: AudioItem!
|
||||
beforeEach {
|
||||
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("its repeat mode") {
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
context("then setting repeat mode off") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .off
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seek(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seek(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should stop playback normally") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to next item") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.currentIndex).to(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
it("should fail") {
|
||||
expect(try audioPlayer.next()).to(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode track") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .track
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seek(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should restart current item") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to next item but should not play") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then setting repeat mode queue") {
|
||||
beforeEach {
|
||||
audioPlayer.repeatMode = .queue
|
||||
}
|
||||
|
||||
context("allow playback to end") {
|
||||
beforeEach {
|
||||
audioPlayer.seek(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to next item and should play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(0))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
|
||||
context("allow playback to end again") {
|
||||
beforeEach {
|
||||
audioPlayer.seek(to: 0.0682)
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
expect(audioPlayer.nextItems.count).toEventually(equal(1))
|
||||
expect(audioPlayer.currentIndex).toEventually(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to next item and should play") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
expect(audioPlayer.currentIndex).to(equal(1))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
|
||||
context("then calling next() again") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
}
|
||||
|
||||
it("should move to first track and should play") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
expect(audioPlayer.currentIndex).to(equal(0))
|
||||
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Double Symmetry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+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 10.0+
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// AVPlayerWrapperDelegate.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 26/10/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
protocol AVPlayerWrapperDelegate: AnyObject {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Double, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
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()
|
||||
}
|
||||
+15
-4
@@ -9,12 +9,16 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerWrapperProtocol: class {
|
||||
protocol AVPlayerWrapperProtocol: AnyObject {
|
||||
|
||||
var state: AVPlayerWrapperState { get }
|
||||
var state: AVPlayerWrapperState { get set }
|
||||
|
||||
var playWhenReady: Bool { get set }
|
||||
|
||||
var currentItem: AVPlayerItem? { get }
|
||||
|
||||
var playbackActive: Bool { get }
|
||||
|
||||
var currentTime: TimeInterval { get }
|
||||
|
||||
var duration: TimeInterval { get }
|
||||
@@ -23,6 +27,7 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var playbackError: AudioPlayerError.PlaybackError? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
@@ -37,7 +42,6 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -48,9 +52,16 @@ protocol AVPlayerWrapperProtocol: class {
|
||||
func stop()
|
||||
|
||||
func seek(to seconds: TimeInterval)
|
||||
|
||||
|
||||
func seek(by offset: TimeInterval)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func load(from url: String, type: SourceType, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
|
||||
|
||||
func unload()
|
||||
|
||||
func reload(startFromCurrentTime: Bool)
|
||||
}
|
||||
+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
|
||||
}
|
||||
@@ -66,23 +66,23 @@ public class DefaultAudioItem: AudioItem {
|
||||
}
|
||||
|
||||
public func getSourceUrl() -> String {
|
||||
return audioUrl
|
||||
audioUrl
|
||||
}
|
||||
|
||||
public func getArtist() -> String? {
|
||||
return artist
|
||||
artist
|
||||
}
|
||||
|
||||
public func getTitle() -> String? {
|
||||
return title
|
||||
title
|
||||
}
|
||||
|
||||
public func getAlbumTitle() -> String? {
|
||||
return albumTitle
|
||||
albumTitle
|
||||
}
|
||||
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
sourceType
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
@@ -97,17 +97,17 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
|
||||
self.pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
pitchAlgorithmType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
public var initialTime: TimeInterval
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.initialTime = 0.0
|
||||
initialTime = 0.0
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
}
|
||||
|
||||
public func getInitialTime() -> TimeInterval {
|
||||
return initialTime
|
||||
initialTime
|
||||
}
|
||||
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
public var options: [String: Any]
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.options = [:]
|
||||
options = [:]
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
|
||||
}
|
||||
|
||||
public func getAssetOptions() -> [String: Any] {
|
||||
return options
|
||||
options
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+449
@@ -0,0 +1,449 @@
|
||||
//
|
||||
// AudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 15/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
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
|
||||
*/
|
||||
public var remoteCommands: [RemoteCommand] = [] {
|
||||
didSet {
|
||||
if let item = currentItem {
|
||||
self.enableRemoteCommands(forItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
public var playbackError: AudioPlayerError.PlaybackError? {
|
||||
wrapper.playbackError
|
||||
}
|
||||
|
||||
/**
|
||||
The elapsed playback time of the current item.
|
||||
*/
|
||||
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. 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)
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { wrapper.bufferDuration }
|
||||
set {
|
||||
wrapper.bufferDuration = newValue
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = wrapper.bufferDuration == 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling. Setting this to true will also set `bufferDuration` back to `0`.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set {
|
||||
if (newValue) {
|
||||
wrapper.bufferDuration = 0
|
||||
}
|
||||
wrapper.automaticallyWaitsToMinimizeStalling = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to decide how often the player should call the delegate with time progress events.
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
public var volume: Float {
|
||||
get { wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
public var rate: Float {
|
||||
get { wrapper.rate }
|
||||
set {
|
||||
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: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
enableRemoteCommands(forItem: item)
|
||||
|
||||
wrapper.load(
|
||||
from: item.getSourceUrl(),
|
||||
type: item.getSourceType(),
|
||||
playWhenReady: self.playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
wrapper.play()
|
||||
}
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
wrapper.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
Stop playback
|
||||
*/
|
||||
public func stop() {
|
||||
let wasActive = wrapper.playbackActive
|
||||
wrapper.stop()
|
||||
if (wasActive) {
|
||||
event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Reload the current item.
|
||||
*/
|
||||
public func reload(startFromCurrentTime: Bool) {
|
||||
wrapper.reload(startFromCurrentTime: startFromCurrentTime)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
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())
|
||||
}
|
||||
else {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Syncs the current remoteCommands with the iOS command center.
|
||||
Can be used to update item states - e.g. like, dislike and bookmark.
|
||||
*/
|
||||
@available(*, deprecated, message: "Directly set .remoteCommands instead")
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
/**
|
||||
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
|
||||
|
||||
Reloads:
|
||||
- Artist
|
||||
- Title
|
||||
- Album title
|
||||
- Album artwork
|
||||
*/
|
||||
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
|
||||
*/
|
||||
func updateNowPlayingPlaybackValues() {
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.duration(wrapper.duration),
|
||||
NowPlayingInfoProperty.playbackRate(wrapper.playWhenReady ? Double(wrapper.rate) : 0),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime)
|
||||
])
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
let playbackWasActive = wrapper.playbackActive
|
||||
currentItem = nil
|
||||
wrapper.unload()
|
||||
nowPlayingInfoController.clear()
|
||||
if (playbackWasActive) {
|
||||
event.playbackEnd.emit(data: .cleared)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setNowPlayingCurrentTime(seconds: Double) {
|
||||
nowPlayingInfoController.set(
|
||||
keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(seconds)
|
||||
)
|
||||
}
|
||||
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
} else {
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
if let item = currentItem as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
} else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
switch state {
|
||||
case .ready, .loading:
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
default: break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .ready, .loading, .playing, .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
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: Double, didFinish: Bool) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
setNowPlayingCurrentTime(seconds: Double(seconds))
|
||||
}
|
||||
event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
event.receiveCommonMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveChapterMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
event.receiveTimedMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
event.playWhenReadyChange.emit(data: playWhenReady)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
wrapper.state = .ended
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
AVWrapper(failedWithError: AudioPlayerError.PlaybackError.playbackFailed)
|
||||
}
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AudioPlayerError.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum AudioPlayerError: Error {
|
||||
|
||||
public enum PlaybackError: Error {
|
||||
case failedToLoadKeyValue
|
||||
case invalidSourceUrl(String)
|
||||
case notConnectedToInternet
|
||||
case playbackFailed
|
||||
case itemWasUnplayable
|
||||
}
|
||||
|
||||
public enum QueueError: Error {
|
||||
case noCurrentItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
case empty
|
||||
}
|
||||
}
|
||||
+21
-8
@@ -8,11 +8,14 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
public protocol AudioSessionControllerDelegate: class {
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType)
|
||||
public enum InterruptionType: Equatable {
|
||||
case began
|
||||
case ended(shouldResume: Bool)
|
||||
}
|
||||
|
||||
public protocol AudioSessionControllerDelegate: AnyObject {
|
||||
func handleInterruption(type: InterruptionType)
|
||||
}
|
||||
|
||||
/**
|
||||
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
|
||||
@@ -30,7 +33,7 @@ public class AudioSessionController {
|
||||
True if another app is currently playing audio.
|
||||
*/
|
||||
public var isOtherAudioPlaying: Bool {
|
||||
return audioSession.isOtherAudioPlaying
|
||||
audioSession.isOtherAudioPlaying
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +49,7 @@ public class AudioSessionController {
|
||||
Set this to false to disable the behaviour.
|
||||
*/
|
||||
public var isObservingForInterruptions: Bool {
|
||||
get {
|
||||
return _isObservingForInterruptions
|
||||
}
|
||||
get { _isObservingForInterruptions }
|
||||
set {
|
||||
if newValue == _isObservingForInterruptions {
|
||||
return
|
||||
@@ -112,7 +113,19 @@ public class AudioSessionController {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.handleInterruption(type: type)
|
||||
switch type {
|
||||
case .began:
|
||||
delegate?.handleInterruption(type: .began)
|
||||
case .ended:
|
||||
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: false))
|
||||
return
|
||||
}
|
||||
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
|
||||
delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
|
||||
@unknown default: return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,15 +10,23 @@ import MediaPlayer
|
||||
|
||||
extension AudioPlayer {
|
||||
|
||||
public typealias StateChangeEventData = (AudioPlayerState)
|
||||
public typealias PlaybackEndEventData = (PlaybackEndedReason)
|
||||
public typealias SecondElapseEventData = (TimeInterval)
|
||||
public typealias FailEventData = (Error?)
|
||||
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = (Double)
|
||||
public typealias MetadataEventData = ([AVMetadataItem])
|
||||
public typealias PlayWhenReadyChangeData = Bool
|
||||
public typealias StateChangeEventData = AudioPlayerState
|
||||
public typealias PlaybackEndEventData = PlaybackEndedReason
|
||||
public typealias SecondElapseEventData = TimeInterval
|
||||
public typealias FailEventData = Error?
|
||||
public typealias SeekEventData = (seconds: Double, didFinish: Bool)
|
||||
public typealias UpdateDurationEventData = Double
|
||||
public typealias MetadataCommonEventData = [AVMetadataItem]
|
||||
public typealias MetadataTimedEventData = [AVTimedMetadataGroup]
|
||||
public typealias DidRecreateAVPlayerEventData = ()
|
||||
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
|
||||
public typealias CurrentItemEventData = (
|
||||
item: AudioItem?,
|
||||
index: Int?,
|
||||
lastItem: AudioItem?,
|
||||
lastIndex: Int?,
|
||||
lastPosition: Double?
|
||||
)
|
||||
|
||||
public struct EventHolder {
|
||||
|
||||
@@ -27,6 +35,12 @@ extension AudioPlayer {
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the `AudioPlayer#playWhenReady` has changed
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let playWhenReadyChange: AudioPlayer.Event<PlayWhenReadyChangeData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the playback of the player, for some reason, has stopped.
|
||||
@@ -60,10 +74,22 @@ extension AudioPlayer {
|
||||
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives metadata.
|
||||
Emitted when the player receives common metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveMetadata: AudioPlayer.Event<MetadataEventData> = AudioPlayer.Event()
|
||||
public let receiveCommonMetadata: AudioPlayer.Event<MetadataCommonEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives timed metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveTimedMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the player receives chapter metadata.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
*/
|
||||
public let receiveChapterMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
|
||||
@@ -73,11 +99,11 @@ extension AudioPlayer {
|
||||
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
|
||||
|
||||
/**
|
||||
Emitted when a new track starts and the queue index changes.
|
||||
Emitted when the current track has changed.
|
||||
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
|
||||
- Note: It is only fired for instances of a QueuedAudioPlayer.
|
||||
*/
|
||||
public let queueIndex: AudioPlayer.Event<QueueIndexEventData> = AudioPlayer.Event()
|
||||
public let currentItem: AudioPlayer.Event<CurrentItemEventData> = AudioPlayer.Event()
|
||||
}
|
||||
|
||||
public typealias EventClosure<EventData> = (EventData) -> Void
|
||||
@@ -90,7 +116,7 @@ extension AudioPlayer {
|
||||
|
||||
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
|
||||
self.listener = listener
|
||||
self.invoke = { [weak listener] (data: EventData) in
|
||||
invoke = { [weak listener] (data: EventData) in
|
||||
guard let _ = listener else {
|
||||
return false
|
||||
}
|
||||
@@ -102,44 +128,28 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
public class Event<EventData> {
|
||||
|
||||
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
|
||||
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
|
||||
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
private let queue: DispatchQueue = DispatchQueue(label: "com.swiftAudioEx.eventQueue")
|
||||
var invokers: [Invoker<EventData>] = []
|
||||
|
||||
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers.append(Invoker(listener: listener, closure: closure))
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
public func removeListener(_ listener: AnyObject) {
|
||||
actionQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
if let listenerToCheck = invoker.listener {
|
||||
return listenerToCheck !== listener
|
||||
}
|
||||
return true
|
||||
return invoker.listener !== listener
|
||||
})
|
||||
self.invokersSemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
func emit(data: EventData) {
|
||||
eventQueue.async {
|
||||
self.invokersSemaphore.wait()
|
||||
self.invokers = self.invokers.filter({ (invoker) -> Bool in
|
||||
return invoker.invoke(data)
|
||||
})
|
||||
self.invokersSemaphore.signal()
|
||||
queue.async {
|
||||
self.invokers = self.invokers.filter { $0.invoke(data) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// MediaInfoController.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 15/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
private var infoQueue: DispatchQueueType = DispatchQueue(
|
||||
label: "NowPlayingInfoController.infoQueue",
|
||||
attributes: .concurrent
|
||||
)
|
||||
|
||||
private(set) var infoCenter: NowPlayingInfoCenter
|
||||
private(set) var info: [String: Any] = [:]
|
||||
|
||||
public required init() {
|
||||
infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
infoQueue = dispatchQueue
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public func set(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()
|
||||
}
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.info[keyValue.getKey()] = keyValue.getValue()
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private func update() {
|
||||
infoCenter.nowPlayingInfo = info
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
infoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.info = [:]
|
||||
self.infoCenter.nowPlayingInfo = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -19,6 +19,8 @@ public protocol NowPlayingInfoControllerProtocol {
|
||||
|
||||
func set(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue])
|
||||
|
||||
func clear()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// AVPlayerItemNotificationObserver.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 12/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
|
||||
func itemDidPlayToEndTime()
|
||||
func itemFailedToPlayToEndTime()
|
||||
func itemPlaybackStalled()
|
||||
}
|
||||
|
||||
/**
|
||||
Observes notifications posted by an AVPlayerItem.
|
||||
|
||||
Currently only listening for the AVPlayerItemDidPlayToEndTime notification.
|
||||
*/
|
||||
class AVPlayerItemNotificationObserver {
|
||||
|
||||
private let notificationCenter: NotificationCenter = NotificationCenter.default
|
||||
|
||||
private(set) weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemNotificationObserverDelegate?
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
|
||||
deinit {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
/**
|
||||
Will start observing notifications from an item.
|
||||
|
||||
- parameter item: The item to observe.
|
||||
- important: Cannot observe more than one item at a time.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
stopObservingCurrentItem()
|
||||
observingItem = item
|
||||
isObserving = true
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop receiving notifications for the current item.
|
||||
*/
|
||||
func stopObservingCurrentItem() {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
delegate?.itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
@objc private func itemFailedToPlayToEndTime() {
|
||||
delegate?.itemFailedToPlayToEndTime()
|
||||
}
|
||||
|
||||
@objc private func itemPlaybackStalled() {
|
||||
delegate?.itemPlaybackStalled()
|
||||
}
|
||||
}
|
||||
+55
-18
@@ -8,17 +8,21 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerItemObserverDelegate: class {
|
||||
protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the observed item updates the duration.
|
||||
Called when the duration of the observed item is updated.
|
||||
*/
|
||||
func item(didUpdateDuration duration: Double)
|
||||
|
||||
|
||||
/**
|
||||
Called when the playback of the observed item is or is no longer likely to keep up.
|
||||
*/
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool)
|
||||
/**
|
||||
Called when the observed item receives metadata
|
||||
*/
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem])
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
|
||||
|
||||
}
|
||||
|
||||
@@ -28,12 +32,12 @@ protocol AVPlayerItemObserverDelegate: class {
|
||||
class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
private var currentMetadataOutput: AVPlayerItemMetadataOutput?
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
|
||||
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
@@ -41,6 +45,10 @@ class AVPlayerItemObserver: NSObject {
|
||||
private(set) weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
@@ -51,23 +59,36 @@ class AVPlayerItemObserver: NSObject {
|
||||
- parameter item: The player item to observe.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
self.stopObservingCurrentItem()
|
||||
stopObservingCurrentItem()
|
||||
|
||||
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.timedMetadata, 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.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
|
||||
self.isObserving = false
|
||||
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?) {
|
||||
@@ -79,21 +100,37 @@ class AVPlayerItemObserver: NSObject {
|
||||
switch observedKeyPath {
|
||||
case AVPlayerItemKeyPath.duration:
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
case AVPlayerItemKeyPath.timedMetadata:
|
||||
if let metadata = change?[.newKey] as? [AVMetadataItem] {
|
||||
self.delegate?.item(didReceiveMetadata: metadata)
|
||||
|
||||
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
|
||||
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
default: break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
if output == currentMetadataOutput {
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AVPlayerItem {
|
||||
func removeAllMetadataOutputs() {
|
||||
for output in self.outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
|
||||
self.remove(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-30
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerObserverDelegate: class {
|
||||
protocol AVPlayerObserverDelegate: AnyObject {
|
||||
|
||||
/**
|
||||
Called when the AVPlayer.status changes.
|
||||
@@ -20,90 +20,96 @@ protocol AVPlayerObserverDelegate: class {
|
||||
Called when the AVPlayer.timeControlStatus changes.
|
||||
*/
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Observing an AVPlayers status changes.
|
||||
*/
|
||||
class AVPlayerObserver: NSObject {
|
||||
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
|
||||
|
||||
private struct AVPlayerKeyPath {
|
||||
static let status = #keyPath(AVPlayer.status)
|
||||
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
|
||||
}
|
||||
|
||||
|
||||
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
|
||||
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
|
||||
private(set) var isObserving: Bool = false
|
||||
|
||||
|
||||
weak var delegate: AVPlayerObserverDelegate?
|
||||
weak var player: AVPlayer? {
|
||||
willSet {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
self.stopObserving()
|
||||
stopObserving()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Start receiving events from this observer.
|
||||
*/
|
||||
func startObserving() {
|
||||
if (isObserving) { return };
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
self.stopObserving()
|
||||
self.isObserving = true
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
|
||||
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
|
||||
isObserving = true
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.status,
|
||||
options: statusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
player.addObserver(
|
||||
self,
|
||||
forKeyPath: AVPlayerKeyPath.timeControlStatus,
|
||||
options: timeControlStatusChangeOptions,
|
||||
context: &AVPlayerObserver.context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func stopObserving() {
|
||||
guard let player = player, isObserving else {
|
||||
return
|
||||
}
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
|
||||
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
|
||||
self.isObserving = false
|
||||
isObserving = false
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch observedKeyPath {
|
||||
|
||||
case AVPlayerKeyPath.status:
|
||||
self.handleStatusChange(change)
|
||||
|
||||
handleStatusChange(change)
|
||||
|
||||
case AVPlayerKeyPath.timeControlStatus:
|
||||
self.handleTimeControlStatusChange(change)
|
||||
|
||||
handleTimeControlStatusChange(change)
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.Status
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
status = .unknown
|
||||
}
|
||||
delegate?.player(statusDidChange: status)
|
||||
}
|
||||
|
||||
|
||||
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
|
||||
let status: AVPlayer.TimeControlStatus
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
@@ -111,5 +117,4 @@ class AVPlayerObserver: NSObject {
|
||||
delegate?.player(didChangeTimeControlStatus: status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+14
-8
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerTimeObserverDelegate: class {
|
||||
protocol AVPlayerTimeObserverDelegate: AnyObject {
|
||||
func audioDidStart()
|
||||
func timeEvent(time: CMTime)
|
||||
}
|
||||
@@ -61,19 +61,25 @@ class AVPlayerTimeObserver {
|
||||
return
|
||||
}
|
||||
unregisterForBoundaryTimeEvents()
|
||||
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})
|
||||
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
|
||||
self?.delegate?.audioDidStart()
|
||||
})
|
||||
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(
|
||||
forTimes: [AVPlayerTimeObserver.startBoundaryTime].map({
|
||||
NSValue(time: $0)
|
||||
}),
|
||||
queue: nil,
|
||||
using: { [weak self] in
|
||||
self?.delegate?.audioDidStart()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Unregister from the boundary events of the player.
|
||||
*/
|
||||
func unregisterForBoundaryTimeEvents() {
|
||||
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
|
||||
return
|
||||
}
|
||||
guard
|
||||
let player = player,
|
||||
let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken
|
||||
else { return }
|
||||
player.removeTimeObserver(boundaryTimeStartObserverToken)
|
||||
self.boundaryTimeStartObserverToken = nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
+36
-63
@@ -27,7 +27,7 @@ public class RemoteCommandController {
|
||||
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
|
||||
*/
|
||||
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
|
||||
self.center = remoteCommandCenter
|
||||
center = remoteCommandCenter
|
||||
}
|
||||
|
||||
internal func enable(commands: [RemoteCommand]) {
|
||||
@@ -35,20 +35,13 @@ public class RemoteCommandController {
|
||||
!commands.contains(where: { $0.description == command.description })
|
||||
}
|
||||
|
||||
self.enabledCommands = commands
|
||||
commands.forEach { (command) in
|
||||
self.enable(command: command)
|
||||
}
|
||||
|
||||
commandsToDisable.forEach { (command) in
|
||||
self.disable(command: command)
|
||||
}
|
||||
enabledCommands = commands
|
||||
commands.forEach { self.enable(command: $0) }
|
||||
disable(commands: commandsToDisable)
|
||||
}
|
||||
|
||||
internal func disable(commands: [RemoteCommand]) {
|
||||
commands.forEach { (command) in
|
||||
self.disable(command: command)
|
||||
}
|
||||
commands.forEach { self.disable(command: $0) }
|
||||
}
|
||||
|
||||
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
@@ -102,21 +95,21 @@ public class RemoteCommandController {
|
||||
|
||||
// MARK: - Handlers
|
||||
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
|
||||
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
|
||||
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
|
||||
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
|
||||
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
|
||||
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
|
||||
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
|
||||
public lazy var handlePlayCommand: RemoteCommandHandler = handlePlayCommandDefault
|
||||
public lazy var handlePauseCommand: RemoteCommandHandler = handlePauseCommandDefault
|
||||
public lazy var handleStopCommand: RemoteCommandHandler = handleStopCommandDefault
|
||||
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = handleTogglePlayPauseCommandDefault
|
||||
public lazy var handleSkipForwardCommand: RemoteCommandHandler = handleSkipForwardCommandDefault
|
||||
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = handleSkipBackwardDefault
|
||||
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = handleChangePlaybackPositionCommandDefault
|
||||
public lazy var handleNextTrackCommand: RemoteCommandHandler = handleNextTrackCommandDefault
|
||||
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = handlePreviousTrackCommandDefault
|
||||
public lazy var handleLikeCommand: RemoteCommandHandler = handleLikeCommandDefault
|
||||
public lazy var handleDislikeCommand: RemoteCommandHandler = handleDislikeCommandDefault
|
||||
public lazy var handleBookmarkCommand: RemoteCommandHandler = handleBookmarkCommandDefault
|
||||
|
||||
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -124,7 +117,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -132,7 +125,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.stop()
|
||||
return .success
|
||||
}
|
||||
@@ -140,7 +133,7 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
if let audioPlayer = audioPlayer {
|
||||
audioPlayer.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -150,7 +143,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -160,7 +153,7 @@ public class RemoteCommandController {
|
||||
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -169,7 +162,7 @@ public class RemoteCommandController {
|
||||
|
||||
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
let audioPlayer = audioPlayer {
|
||||
audioPlayer.seek(to: event.positionTime)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
@@ -177,57 +170,37 @@ public class RemoteCommandController {
|
||||
}
|
||||
|
||||
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
if let player = audioPlayer as? QueuedAudioPlayer {
|
||||
player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
if let error = error as? APError.LoadError {
|
||||
switch error {
|
||||
case .invalidSourceUrl(_):
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
}
|
||||
else if let error = error as? APError.QueueError {
|
||||
switch error {
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
|
||||
return MPRemoteCommandHandlerStatus.noSuchContent
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
return error is AudioPlayerError.QueueError
|
||||
? MPRemoteCommandHandlerStatus.noSuchContent
|
||||
: MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '0.14.5'
|
||||
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,361 +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
|
||||
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
let playerItemObserver: AVPlayerItemObserver
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate var _playWhenReady: Bool = true
|
||||
fileprivate var _initialTime: TimeInterval?
|
||||
|
||||
/// True when the track was paused for the purpose of switching tracks
|
||||
fileprivate var _pausedForLoad: Bool = false
|
||||
|
||||
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
if oldValue != _state {
|
||||
self.delegate?.AVWrapper(didChangeState: _state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.avPlayer = AVPlayer()
|
||||
self.playerObserver = AVPlayerObserver()
|
||||
self.playerObserver.player = avPlayer
|
||||
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
||||
self.playerTimeObserver.player = avPlayer
|
||||
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
||||
self.playerItemObserver = AVPlayerItemObserver()
|
||||
|
||||
self.playerObserver.delegate = self
|
||||
self.playerTimeObserver.delegate = self
|
||||
self.playerItemNotificationObserver.delegate = self
|
||||
self.playerItemObserver.delegate = self
|
||||
|
||||
// disabled since we're not making use of video playback
|
||||
self.avPlayer.allowsExternalPlayback = false;
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperProtocol
|
||||
|
||||
var state: AVPlayerWrapperState {
|
||||
return _state
|
||||
}
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
||||
return avPlayer.reasonForWaitingToPlay
|
||||
}
|
||||
|
||||
var currentItem: AVPlayerItem? {
|
||||
return avPlayer.currentItem
|
||||
}
|
||||
|
||||
var _pendingAsset: AVAsset? = nil
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
|
||||
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
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?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
|
||||
var timeEventFrequency: TimeEventFrequency = .everySecond {
|
||||
didSet {
|
||||
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
get { return avPlayer.rate }
|
||||
set { avPlayer.rate = newValue }
|
||||
}
|
||||
|
||||
var volume: Float {
|
||||
get { return avPlayer.volume }
|
||||
set { avPlayer.volume = newValue }
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get { return avPlayer.isMuted }
|
||||
set { avPlayer.isMuted = 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) {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
||||
if let _ = self._initialTime {
|
||||
self._initialTime = nil
|
||||
if self._playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
|
||||
if currentItem?.status == .failed {
|
||||
recreateAVPlayer()
|
||||
}
|
||||
|
||||
self._pendingAsset = AVURLAsset(url: url, options: options)
|
||||
|
||||
if let pendingAsset = _pendingAsset {
|
||||
self._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 {
|
||||
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
|
||||
switch status {
|
||||
case .loaded:
|
||||
if isPendingAsset {
|
||||
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
self.playerItemObserver.startObserving(item: currentItem)
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
if isPendingAsset {
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
self._pendingAsset = nil
|
||||
}
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
|
||||
_initialTime = initialTime
|
||||
|
||||
_pausedForLoad = true
|
||||
self.pause()
|
||||
|
||||
self.load(from: url, playWhenReady: playWhenReady, options: options)
|
||||
}
|
||||
|
||||
// MARK: - Util
|
||||
|
||||
private func reset(soft: Bool) {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
self._pendingAsset?.cancelLoading()
|
||||
self._pendingAsset = nil
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
_state = .idle
|
||||
}
|
||||
else if _pausedForLoad == true {}
|
||||
else {
|
||||
self._state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
self._state = .buffering
|
||||
case .playing:
|
||||
self._state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
self._pausedForLoad = false
|
||||
if _playWhenReady && (_initialTime ?? 0) == 0 {
|
||||
self.play()
|
||||
}
|
||||
else if let initialTime = _initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
|
||||
break
|
||||
|
||||
case .unknown:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerTimeObserverDelegate
|
||||
|
||||
func audioDidStart() {
|
||||
self._state = .playing
|
||||
}
|
||||
|
||||
func timeEvent(time: CMTime) {
|
||||
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
delegate?.AVWrapperItemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// AVPlayerWrapperDelegate.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 26/10/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
protocol AVPlayerWrapperDelegate: class {
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState)
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
//
|
||||
// AudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 15/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public typealias AudioPlayerState = AVPlayerWrapperState
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
var wrapper: AVPlayerWrapperProtocol {
|
||||
return _wrapper
|
||||
}
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
public let event = EventHolder()
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
return _currentItem
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
public var remoteCommands: [RemoteCommand] = [] {
|
||||
didSet {
|
||||
if let item = currentItem {
|
||||
self.enableRemoteCommands(forItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
/**
|
||||
The elapsed playback time of the current item.
|
||||
*/
|
||||
public var currentTime: Double {
|
||||
return wrapper.currentTime
|
||||
}
|
||||
|
||||
/**
|
||||
The duration of the current AudioItem.
|
||||
*/
|
||||
public var duration: Double {
|
||||
return wrapper.duration
|
||||
}
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
return wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
public var playerState: AudioPlayerState {
|
||||
return wrapper.state
|
||||
}
|
||||
|
||||
// MARK: - Setters for AVPlayerWrapper
|
||||
|
||||
/**
|
||||
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
|
||||
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
|
||||
|
||||
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { return wrapper.bufferDuration }
|
||||
set { _wrapper.bufferDuration = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to decide how often the player should call the delegate with time progress events.
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Indicates whether the player should automatically delay playback in order to minimize stalling
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { _wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { return wrapper.isMuted }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
private var _rate: Float = 1.0
|
||||
public var rate: Float {
|
||||
get { return _rate }
|
||||
set {
|
||||
_rate = newValue
|
||||
|
||||
// Only set the rate on the wrapper if it is already playing.
|
||||
if _wrapper.rate > 0 {
|
||||
_wrapper.rate = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/**
|
||||
Create a new AudioPlayer.
|
||||
|
||||
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
|
||||
*/
|
||||
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self._wrapper = AVPlayerWrapper()
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self._wrapper.delegate = self
|
||||
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`.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
let url: URL
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
if let itemUrl = URL(string: item.getSourceUrl()) {
|
||||
url = itemUrl
|
||||
}
|
||||
else {
|
||||
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
|
||||
}
|
||||
case .file:
|
||||
url = URL(fileURLWithPath: item.getSourceUrl())
|
||||
}
|
||||
|
||||
wrapper.load(from: url,
|
||||
playWhenReady: playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions())
|
||||
|
||||
self._currentItem = item
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
self.loadNowPlayingMetaValues()
|
||||
}
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
|
||||
/**
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
self.wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
self.wrapper.play()
|
||||
}
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
self.wrapper.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
*/
|
||||
public func stop() {
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
self.event.playbackEnd.emit(data: .playerStopped)
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
if automaticallyUpdateNowPlayingInfo {
|
||||
self.updateNowPlayingCurrentTime(seconds)
|
||||
}
|
||||
self.wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
// MARK: - Remote Command Center
|
||||
|
||||
func enableRemoteCommands(_ commands: [RemoteCommand]) {
|
||||
self.remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
}
|
||||
else {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Syncs the current remoteCommands with the iOS command center.
|
||||
Can be used to update item states - e.g. like, dislike and bookmark.
|
||||
*/
|
||||
@available(*, deprecated, message: "Directly set .remoteCommands instead")
|
||||
public func syncRemoteCommandsWithCommandCenter() {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
/**
|
||||
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
|
||||
|
||||
Reloads:
|
||||
- Artist
|
||||
- Title
|
||||
- Album title
|
||||
- Album artwork
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
private func updateNowPlayingDuration(_ duration: Double) {
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
|
||||
}
|
||||
|
||||
private func updateNowPlayingRate(_ rate: Float) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
|
||||
}
|
||||
|
||||
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
|
||||
}
|
||||
|
||||
private func loadArtwork(forItem item: AudioItem) {
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
return image
|
||||
})
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func reset() {
|
||||
self._currentItem = nil
|
||||
}
|
||||
|
||||
private func setTimePitchingAlgorithmForCurrentItem() {
|
||||
if let item = currentItem as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
switch state {
|
||||
case .ready, .loading:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
setTimePitchingAlgorithmForCurrentItem()
|
||||
case .playing:
|
||||
// When a track starts playing, reset the rate to the stored rate
|
||||
self.rate = _rate;
|
||||
fallthrough
|
||||
case .paused:
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
self.event.stateChange.emit(data: state)
|
||||
}
|
||||
|
||||
func AVWrapper(secondsElapsed seconds: Double) {
|
||||
self.event.secondElapse.emit(data: seconds)
|
||||
}
|
||||
|
||||
func AVWrapper(failedWithError error: Error?) {
|
||||
self.event.fail.emit(data: error)
|
||||
}
|
||||
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
|
||||
if !didFinish && automaticallyUpdateNowPlayingInfo {
|
||||
updateNowPlayingCurrentTime(currentTime)
|
||||
}
|
||||
self.event.seek.emit(data: (seconds, didFinish))
|
||||
}
|
||||
|
||||
func AVWrapper(didUpdateDuration duration: Double) {
|
||||
self.event.updateDuration.emit(data: duration)
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.event.receiveMetadata.emit(data: metadata)
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// MediaInfoController.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 15/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
|
||||
private let concurrentInfoQueue: DispatchQueueType
|
||||
|
||||
private var _infoCenter: NowPlayingInfoCenter
|
||||
private var _info: [String: Any] = [:]
|
||||
|
||||
var infoCenter: NowPlayingInfoCenter {
|
||||
return _infoCenter
|
||||
}
|
||||
|
||||
var info: [String: Any] {
|
||||
return _info
|
||||
}
|
||||
|
||||
public required init() {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = MPNowPlayingInfoCenter.default()
|
||||
}
|
||||
|
||||
/// Used for testing purposes.
|
||||
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = dispatchQueue
|
||||
self._infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public required init(infoCenter: NowPlayingInfoCenter) {
|
||||
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
|
||||
self._infoCenter = infoCenter
|
||||
}
|
||||
|
||||
public func set(keyValues: [NowPlayingInfoKeyValue]) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
keyValues.forEach { (keyValue) in
|
||||
self._info[keyValue.getKey()] = keyValue.getValue()
|
||||
}
|
||||
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
}
|
||||
}
|
||||
|
||||
public func set(keyValue: NowPlayingInfoKeyValue) {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self._info[keyValue.getKey()] = keyValue.getValue()
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
}
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self._info = [:]
|
||||
self._infoCenter.nowPlayingInfo = self._info
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// AVPlayerItemNotificationObserver.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 12/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
protocol AVPlayerItemNotificationObserverDelegate: class {
|
||||
func itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
/**
|
||||
Observes notifications posted by an AVPlayerItem.
|
||||
|
||||
Currently only listening for the AVPlayerItemDidPlayToEndTime notification.
|
||||
*/
|
||||
class AVPlayerItemNotificationObserver {
|
||||
|
||||
private let notificationCenter: NotificationCenter = NotificationCenter.default
|
||||
|
||||
private(set) weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemNotificationObserverDelegate?
|
||||
|
||||
private(set) var isObserving: Bool = false
|
||||
|
||||
deinit {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
/**
|
||||
Will start observing notifications from an item.
|
||||
|
||||
- parameter item: The item to observe.
|
||||
- important: Cannot observe more than one item at a time.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
stopObservingCurrentItem()
|
||||
observingItem = item
|
||||
isObserving = true
|
||||
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop receiving notifications for the current item.
|
||||
*/
|
||||
func stopObservingCurrentItem() {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
|
||||
self.observingItem = nil
|
||||
self.isObserving = false
|
||||
}
|
||||
|
||||
@objc private func itemDidPlayToEndTime() {
|
||||
delegate?.itemDidPlayToEndTime()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,251 +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
|
||||
|
||||
private var _items: [T] = [] {
|
||||
didSet {
|
||||
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
|
||||
delegate?.onReceivedFirstItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
public var items: [T] {
|
||||
return _items
|
||||
}
|
||||
|
||||
public var nextItems: [T] {
|
||||
guard _currentIndex + 1 < _items.count else {
|
||||
return []
|
||||
}
|
||||
return Array(_items[_currentIndex + 1..<_items.count])
|
||||
}
|
||||
|
||||
public var previousItems: [T] {
|
||||
if (_currentIndex == 0) {
|
||||
return []
|
||||
}
|
||||
return Array(_items[0..<_currentIndex])
|
||||
}
|
||||
|
||||
private var _currentIndex: Int = 0 {
|
||||
didSet {
|
||||
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return _currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
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]) {
|
||||
_items.append(contentsOf: items)
|
||||
}
|
||||
|
||||
/**
|
||||
Add an array of items to the queue at a given index.
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
- parameter at: The index to insert the items at.
|
||||
*/
|
||||
public func addItems(_ items: [T], at index: Int) throws {
|
||||
guard index >= 0 && _items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
|
||||
}
|
||||
|
||||
_items.insert(contentsOf: items, at: index)
|
||||
if (_currentIndex >= index) { _currentIndex = _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 = _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 {
|
||||
self.addItem(item)
|
||||
}
|
||||
|
||||
self._items[_currentIndex] = item
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all previous items in the queue.
|
||||
If no previous items exist, no action will be taken.
|
||||
*/
|
||||
public func removePreviousItems() {
|
||||
guard currentIndex > 0 else { return }
|
||||
_items.removeSubrange(0..<_currentIndex)
|
||||
_currentIndex = 0
|
||||
}
|
||||
|
||||
/**
|
||||
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,234 +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? {
|
||||
return queueManager.current
|
||||
}
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return queueManager.currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
Stops the player and clears the queue.
|
||||
*/
|
||||
public override func stop() {
|
||||
super.stop()
|
||||
self.event.queueIndex.emit(data: (currentIndex, nil))
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
queueManager.clearQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
All items currently in the queue.
|
||||
*/
|
||||
public var items: [AudioItem] {
|
||||
return queueManager.items
|
||||
}
|
||||
|
||||
/**
|
||||
The previous items held by the queue.
|
||||
*/
|
||||
public var previousItems: [AudioItem] {
|
||||
return queueManager.previousItems
|
||||
}
|
||||
|
||||
/**
|
||||
The upcoming items in the queue.
|
||||
*/
|
||||
public var nextItems: [AudioItem] {
|
||||
return 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 self.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 self.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 {
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
|
||||
} catch APError.QueueError.noNextItem {
|
||||
if repeatMode == .queue {
|
||||
event.playbackEnd.emit(data: .skippedToNext)
|
||||
try jumpToItem(atIndex: 0, playWhenReady: true)
|
||||
} else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Step to the previous item in the queue.
|
||||
*/
|
||||
public func previous() throws {
|
||||
let previousItem = try queueManager.previous()
|
||||
event.playbackEnd.emit(data: .skippedToPrevious)
|
||||
try self.load(item: previousItem, playWhenReady: repeatMode != .track)
|
||||
}
|
||||
|
||||
/**
|
||||
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 {
|
||||
let item = try queueManager.jump(to: index)
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
try self.load(item: item, playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
/**
|
||||
Move an item in the queue from one position to another.
|
||||
|
||||
- parameter fromIndex: The index of the item to move.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
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 self.load(item: nextItem, playWhenReady: repeatMode != .track)
|
||||
} catch { /* playback finished */ }
|
||||
case .track:
|
||||
seek(to: 0)
|
||||
play()
|
||||
case .queue:
|
||||
do {
|
||||
let nextItem = try queueManager.next()
|
||||
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
|
||||
} 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 }
|
||||
self.event.queueIndex.emit(data: (oldIndex, newIndex))
|
||||
}
|
||||
|
||||
func onReceivedFirstItem() {
|
||||
self.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)
|
||||
}
|
||||
}
|
||||
+143
-83
@@ -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
|
||||
@@ -145,8 +154,32 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
|
||||
func testAVPlayerWrapperSeekingShouldSeekWhileNotYetLoaded() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
func 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()
|
||||
@@ -154,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 {
|
||||
@@ -171,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: [AVMetadataItem]) {
|
||||
|
||||
private let lockQueue = DispatchQueue(
|
||||
label: "AVPlayerWrapperDelegateHolder.lockQueue",
|
||||
target: .global()
|
||||
)
|
||||
|
||||
func AVWrapperItemPlaybackStalled() {
|
||||
}
|
||||
|
||||
func AVWrapperItemFailedToPlayToEndTime() {
|
||||
}
|
||||
|
||||
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
}
|
||||
|
||||
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
}
|
||||
|
||||
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