Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42767039eb | |||
| 4d3c1db059 | |||
| b6510ca858 | |||
| 4ed73418a3 | |||
| 5b1594da9b | |||
| 0890671ec5 | |||
| 4bd0251fac | |||
| 1fe9b0da35 | |||
| 546dd29838 | |||
| 790d7b655b | |||
| ba12380126 | |||
| 4543c58f38 | |||
| a574f94c6b | |||
| 1e64a9aa8b | |||
| e969fd5550 | |||
| 7b506bebab | |||
| ea82b81ed9 | |||
| 03c988e8b1 | |||
| 2424550401 | |||
| 5d8b3f2be5 | |||
| e1999c935e | |||
| fd8290c537 | |||
| 7fb762db9c | |||
| 81ce63752d | |||
| c03c83096e | |||
| 0c87d2479e | |||
| 98f3646e84 | |||
| 9ebbd99230 |
@@ -1,2 +1,16 @@
|
||||
ignore:
|
||||
- "Example/.*"
|
||||
- "Tests/.*"
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# https://docs.codecov.com/docs/commit-status#informational
|
||||
informational: true
|
||||
target: 78%
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
target: 78%
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
name: validate
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
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',
|
||||
]
|
||||
target: [macos]
|
||||
include:
|
||||
- target: macos
|
||||
destination: '-destination "platform=macOS,name=Any Mac"'
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Tests
|
||||
run: |-
|
||||
cd Example
|
||||
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
|
||||
env:
|
||||
destination: ${{ matrix.destination }}
|
||||
run: xcodebuild test -scheme SwiftAudioEx ${{ matrix.destination }} -enableCodeCoverage YES
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.target == 'macos'
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -3,230 +3,92 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713062067EB4F00F789B3 /* Double + Extensions.swift */; };
|
||||
070713092067EFFB00F789B3 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713082067EFFB00F789B3 /* AudioController.swift */; };
|
||||
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; };
|
||||
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; };
|
||||
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; };
|
||||
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */; };
|
||||
0708ED702116E89900EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
|
||||
0708ED722116E91D00EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
|
||||
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */; };
|
||||
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
|
||||
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
|
||||
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
|
||||
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
|
||||
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
|
||||
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
|
||||
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
|
||||
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */; };
|
||||
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */; };
|
||||
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */; };
|
||||
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */; };
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
|
||||
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575820668B020002C6A1 /* QueueManagerTests.swift */; };
|
||||
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.swift */; };
|
||||
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */; };
|
||||
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */; };
|
||||
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */; };
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */; };
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
|
||||
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
|
||||
F048FE7728D215A9001AA2AB /* five_seconds.m4a in Resources */ = {isa = PBXBuildFile; fileRef = F048FE7628D215A9001AA2AB /* five_seconds.m4a */; };
|
||||
F048FE7828D215A9001AA2AB /* five_seconds.m4a in Resources */ = {isa = PBXBuildFile; fileRef = F048FE7628D215A9001AA2AB /* five_seconds.m4a */; };
|
||||
9B88195D2BC8657A00E20DCE /* SwiftAudioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B88195C2BC8657A00E20DCE /* SwiftAudioApp.swift */; };
|
||||
9B8819612BC8657B00E20DCE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9B8819602BC8657B00E20DCE /* Assets.xcassets */; };
|
||||
9B8819652BC8657B00E20DCE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9B8819642BC8657B00E20DCE /* Preview Assets.xcassets */; };
|
||||
9B8819712BC866A300E20DCE /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B88196C2BC866A300E20DCE /* AudioController.swift */; };
|
||||
9B8819742BC866A300E20DCE /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B88196F2BC866A300E20DCE /* Extensions.swift */; };
|
||||
9B8819752BC866A300E20DCE /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8819702BC866A300E20DCE /* PlayerView.swift */; };
|
||||
9B8819782BC866E800E20DCE /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B8819772BC866E800E20DCE /* SwiftAudioEx */; };
|
||||
9B88197A2BC9883200E20DCE /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8819792BC9883200E20DCE /* PlayerViewModel.swift */; };
|
||||
9B88197C2BC98F5000E20DCE /* QueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B88197B2BC98F5000E20DCE /* QueueView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 607FACC81AFB9204008FA782 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 607FACCF1AFB9204008FA782;
|
||||
remoteInfo = SwiftAudio;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
070713062067EB4F00F789B3 /* Double + Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Extensions.swift"; sourceTree = "<group>"; };
|
||||
070713082067EFFB00F789B3 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
|
||||
0707130A2067F2E000F789B3 /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
|
||||
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QueueTableViewCell.xib; sourceTree = "<group>"; };
|
||||
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionControllerTests.swift; sourceTree = "<group>"; };
|
||||
0708ED6F2116E89900EB29BD /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
|
||||
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerTests.swift; sourceTree = "<group>"; };
|
||||
0708ED78211732F500EB29BD /* TestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestSound.m4a; sourceTree = "<group>"; };
|
||||
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ShortTestSound.m4a; sourceTree = "<group>"; };
|
||||
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
|
||||
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
|
||||
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerTests.swift; sourceTree = "<group>"; };
|
||||
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.swift; sourceTree = "<group>"; };
|
||||
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
|
||||
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerEventTests.swift; sourceTree = "<group>"; };
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
|
||||
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
|
||||
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
|
||||
07756B68218A4E870023935E /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = "<group>"; };
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserverTests.swift; sourceTree = "<group>"; };
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedAudioPlayerTests.swift; sourceTree = "<group>"; };
|
||||
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoTests.swift; sourceTree = "<group>"; };
|
||||
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftAudio_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = five_seconds.m4a; sourceTree = "<group>"; };
|
||||
9B8819592BC8657A00E20DCE /* SwiftAudio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9B88195C2BC8657A00E20DCE /* SwiftAudioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftAudioApp.swift; sourceTree = "<group>"; };
|
||||
9B8819602BC8657B00E20DCE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
9B8819622BC8657B00E20DCE /* SwiftAudio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftAudio.entitlements; sourceTree = "<group>"; };
|
||||
9B8819642BC8657B00E20DCE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
9B88196B2BC865E100E20DCE /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
|
||||
9B88196C2BC866A300E20DCE /* AudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
|
||||
9B88196F2BC866A300E20DCE /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
9B8819702BC866A300E20DCE /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
|
||||
9B8819792BC9883200E20DCE /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
9B88197B2BC98F5000E20DCE /* QueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
607FACCD1AFB9204008FA782 /* Frameworks */ = {
|
||||
9B8819562BC8657A00E20DCE /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
607FACE21AFB9204008FA782 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
9B05AA312660276400C7A389 /* Quick in Frameworks */,
|
||||
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
|
||||
9B8819782BC866E800E20DCE /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0708ED712116E91300EB29BD /* Source */ = {
|
||||
9B8819502BC8657A00E20DCE = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F048FE7628D215A9001AA2AB /* five_seconds.m4a */,
|
||||
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 */,
|
||||
9B88196B2BC865E100E20DCE /* SwiftAudioEx */,
|
||||
9B88195B2BC8657A00E20DCE /* SwiftAudio */,
|
||||
9B88195A2BC8657A00E20DCE /* Products */,
|
||||
9B8819762BC866E800E20DCE /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACD11AFB9204008FA782 /* Products */ = {
|
||||
9B88195A2BC8657A00E20DCE /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */,
|
||||
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */,
|
||||
9B8819592BC8657A00E20DCE /* SwiftAudio.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACD21AFB9204008FA782 /* Example for SwiftAudio */ = {
|
||||
9B88195B2BC8657A00E20DCE /* SwiftAudio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
|
||||
070713082067EFFB00F789B3 /* AudioController.swift */,
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */,
|
||||
0707130A2067F2E000F789B3 /* QueueViewController.swift */,
|
||||
070713062067EB4F00F789B3 /* Double + Extensions.swift */,
|
||||
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */,
|
||||
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */,
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */,
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */,
|
||||
9B88196C2BC866A300E20DCE /* AudioController.swift */,
|
||||
9B88196F2BC866A300E20DCE /* Extensions.swift */,
|
||||
9B8819792BC9883200E20DCE /* PlayerViewModel.swift */,
|
||||
9B8819702BC866A300E20DCE /* PlayerView.swift */,
|
||||
9B88195C2BC8657A00E20DCE /* SwiftAudioApp.swift */,
|
||||
9B88197B2BC98F5000E20DCE /* QueueView.swift */,
|
||||
9B8819602BC8657B00E20DCE /* Assets.xcassets */,
|
||||
9B8819622BC8657B00E20DCE /* SwiftAudio.entitlements */,
|
||||
9B8819632BC8657B00E20DCE /* Preview Content */,
|
||||
);
|
||||
name = "Example for SwiftAudio";
|
||||
path = SwiftAudio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */ = {
|
||||
9B8819632BC8657B00E20DCE /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD41AFB9204008FA782 /* Info.plist */,
|
||||
9B8819642BC8657B00E20DCE /* Preview Assets.xcassets */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACE81AFB9204008FA782 /* Tests */ = {
|
||||
9B8819762BC866E800E20DCE /* Frameworks */ = {
|
||||
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 = (
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -234,212 +96,106 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
607FACCF1AFB9204008FA782 /* SwiftAudio_Example */ = {
|
||||
9B8819582BC8657A00E20DCE /* SwiftAudio */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Example" */;
|
||||
buildConfigurationList = 9B8819682BC8657B00E20DCE /* Build configuration list for PBXNativeTarget "SwiftAudio" */;
|
||||
buildPhases = (
|
||||
607FACCC1AFB9204008FA782 /* Sources */,
|
||||
607FACCD1AFB9204008FA782 /* Frameworks */,
|
||||
607FACCE1AFB9204008FA782 /* Resources */,
|
||||
9B8819552BC8657A00E20DCE /* Sources */,
|
||||
9B8819562BC8657A00E20DCE /* Frameworks */,
|
||||
9B8819572BC8657A00E20DCE /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SwiftAudio_Example;
|
||||
name = SwiftAudio;
|
||||
packageProductDependencies = (
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
|
||||
9B8819772BC866E800E20DCE /* SwiftAudioEx */,
|
||||
);
|
||||
productName = SwiftAudio;
|
||||
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
|
||||
productReference = 9B8819592BC8657A00E20DCE /* SwiftAudio.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
607FACE41AFB9204008FA782 /* SwiftAudio_Tests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Tests" */;
|
||||
buildPhases = (
|
||||
607FACE11AFB9204008FA782 /* Sources */,
|
||||
607FACE21AFB9204008FA782 /* Frameworks */,
|
||||
607FACE31AFB9204008FA782 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
607FACE71AFB9204008FA782 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SwiftAudio_Tests;
|
||||
packageProductDependencies = (
|
||||
9B05AA302660276400C7A389 /* Quick */,
|
||||
9B05AA322660276400C7A389 /* Nimble */,
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = Tests;
|
||||
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
607FACC81AFB9204008FA782 /* Project object */ = {
|
||||
9B8819512BC8657A00E20DCE /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1530;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 1020;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
9B8819582BC8657A00E20DCE = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
buildConfigurationList = 9B8819542BC8657A00E20DCE /* Build configuration list for PBXProject "SwiftAudio" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 607FACC71AFB9204008FA782;
|
||||
packageReferences = (
|
||||
9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */,
|
||||
9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */,
|
||||
);
|
||||
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
|
||||
mainGroup = 9B8819502BC8657A00E20DCE;
|
||||
productRefGroup = 9B88195A2BC8657A00E20DCE /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
607FACCF1AFB9204008FA782 /* SwiftAudio_Example */,
|
||||
607FACE41AFB9204008FA782 /* SwiftAudio_Tests */,
|
||||
9B8819582BC8657A00E20DCE /* SwiftAudio */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
607FACCE1AFB9204008FA782 /* Resources */ = {
|
||||
9B8819572BC8657A00E20DCE /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
|
||||
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
|
||||
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */,
|
||||
F048FE7728D215A9001AA2AB /* five_seconds.m4a in Resources */,
|
||||
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */,
|
||||
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
|
||||
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
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 */,
|
||||
F048FE7828D215A9001AA2AB /* five_seconds.m4a in Resources */,
|
||||
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */,
|
||||
9B8819652BC8657B00E20DCE /* Preview Assets.xcassets in Resources */,
|
||||
9B8819612BC8657B00E20DCE /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
607FACCC1AFB9204008FA782 /* Sources */ = {
|
||||
9B8819552BC8657A00E20DCE /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
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 */,
|
||||
9B8819742BC866A300E20DCE /* Extensions.swift in Sources */,
|
||||
9B8819752BC866A300E20DCE /* PlayerView.swift in Sources */,
|
||||
9B8819712BC866A300E20DCE /* AudioController.swift in Sources */,
|
||||
9B88197A2BC9883200E20DCE /* PlayerViewModel.swift in Sources */,
|
||||
9B88197C2BC98F5000E20DCE /* QueueView.swift in Sources */,
|
||||
9B88195D2BC8657A00E20DCE /* SwiftAudioApp.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;
|
||||
children = (
|
||||
607FACDA1AFB9204008FA782 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
607FACDF1AFB9204008FA782 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.xib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
607FACED1AFB9204008FA782 /* Debug */ = {
|
||||
9B8819662BC8657B00E20DCE /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
@@ -448,17 +204,19 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
@@ -466,35 +224,39 @@
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
607FACEE1AFB9204008FA782 /* Release */ = {
|
||||
9B8819672BC8657B00E20DCE /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
@@ -503,17 +265,19 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
@@ -521,172 +285,118 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
MTL_FAST_MATH = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
607FACF01AFB9204008FA782 /* Debug */ = {
|
||||
9B8819692BC8657B00E20DCE /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = SwiftAudio/SwiftAudio.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"SwiftAudio/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.doublesymmetry.SwiftAudio;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
607FACF11AFB9204008FA782 /* Release */ = {
|
||||
9B88196A2BC8657B00E20DCE /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = SwiftAudio/SwiftAudio.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"SwiftAudio/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
INFOPLIST_FILE = SwiftAudio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.doublesymmetry.SwiftAudio;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
607FACF31AFB9204008FA782 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 7U2TUNKNQX;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.--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 = 7U2TUNKNQX;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.--PRODUCT-NAME-rfc1034identifier-";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */ = {
|
||||
9B8819542BC8657A00E20DCE /* Build configuration list for PBXProject "SwiftAudio" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
607FACED1AFB9204008FA782 /* Debug */,
|
||||
607FACEE1AFB9204008FA782 /* Release */,
|
||||
9B8819662BC8657B00E20DCE /* Debug */,
|
||||
9B8819672BC8657B00E20DCE /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Example" */ = {
|
||||
9B8819682BC8657B00E20DCE /* Build configuration list for PBXNativeTarget "SwiftAudio" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
607FACF01AFB9204008FA782 /* Debug */,
|
||||
607FACF11AFB9204008FA782 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Tests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
607FACF31AFB9204008FA782 /* Debug */,
|
||||
607FACF41AFB9204008FA782 /* Release */,
|
||||
9B8819692BC8657B00E20DCE /* Debug */,
|
||||
9B88196A2BC8657B00E20DCE /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Quick/Quick";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Quick/Nimble";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 9.2.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
9B05AA302660276400C7A389 /* Quick */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */;
|
||||
productName = Quick;
|
||||
};
|
||||
9B05AA322660276400C7A389 /* Nimble */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
|
||||
productName = Nimble;
|
||||
};
|
||||
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
|
||||
9B8819772BC866E800E20DCE /* SwiftAudioEx */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SwiftAudioEx;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
|
||||
rootObject = 9B8819512BC8657A00E20DCE /* Project object */;
|
||||
}
|
||||
|
||||
-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
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Example.app"
|
||||
BlueprintName = "SwiftAudio_Example"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACE41AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Tests.xctest"
|
||||
BlueprintName = "SwiftAudio_Tests"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Example.app"
|
||||
BlueprintName = "SwiftAudio_Example"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACE41AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Tests.xctest"
|
||||
BlueprintName = "SwiftAudio_Tests"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Example.app"
|
||||
BlueprintName = "SwiftAudio_Example"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "SwiftAudio_Example.app"
|
||||
BlueprintName = "SwiftAudio_Example"
|
||||
ReferencedContainer = "container:SwiftAudio.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 03/11/2018.
|
||||
// Copyright (c) 2018 Jørgen Henrichsen. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
|
||||
application.beginReceivingRemoteControlEvents()
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "22AMillion.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "cover.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -9,20 +9,18 @@
|
||||
import Foundation
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class AudioController {
|
||||
|
||||
static let shared = AudioController()
|
||||
let player: QueuedAudioPlayer
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
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/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")),
|
||||
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://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")),
|
||||
DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
]
|
||||
|
||||
@@ -38,7 +36,7 @@ class AudioController {
|
||||
.previous,
|
||||
.changePlaybackPosition
|
||||
]
|
||||
try? audioSessionController.set(category: .playback)
|
||||
|
||||
player.repeatMode = .queue
|
||||
DispatchQueue.main.async {
|
||||
self.player.add(items: self.sources)
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 CocoaPods. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||
<rect key="frame" x="20" y="439" width="441" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SwiftAudio" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
|
||||
<rect key="frame" x="20" y="140" width="441" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
|
||||
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
|
||||
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
|
||||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="548" y="455"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,208 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="ufC-wZ-h7g">
|
||||
<objects>
|
||||
<viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="RX3-VR-CL6">
|
||||
<rect key="frame" x="32" y="533" width="311" height="34"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Q1-U9-TUC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="103.5" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<state key="normal" title="Prev"/>
|
||||
<connections>
|
||||
<action selector="previous:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fFb-iW-sFr"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
|
||||
<rect key="frame" x="103.5" y="0.0" width="104" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<state key="normal" title="Play"/>
|
||||
<connections>
|
||||
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nhf-qB-91A">
|
||||
<rect key="frame" x="207.5" y="0.0" width="103.5" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<state key="normal" title="Next"/>
|
||||
<connections>
|
||||
<action selector="next:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Tha-3J-gVM"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="34" id="T4q-HG-vqM"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="l9B-hM-Ajc">
|
||||
<rect key="frame" x="302" y="20" width="57" height="34"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<state key="normal" title="Queue"/>
|
||||
<connections>
|
||||
<segue destination="vDz-qW-uY8" kind="presentation" identifier="QueueSegue" id="eke-1c-Fsm"/>
|
||||
</connections>
|
||||
</button>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
|
||||
<rect key="frame" x="67.5" y="84" width="240" height="240"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="240" id="5Sj-BZ-sg4"/>
|
||||
<constraint firstAttribute="height" constant="240" id="Hij-Yw-6Lg"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3CL-8o-zYW">
|
||||
<rect key="frame" x="16" y="462" width="39" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RVb-HZ-QCX">
|
||||
<rect key="frame" x="320" y="462" width="39" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
|
||||
<rect key="frame" x="14" y="424" width="347" height="31"/>
|
||||
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="maximumTrackTintColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="thumbTintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="HeH-aB-VXZ"/>
|
||||
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="NfP-3T-dnw"/>
|
||||
<action selector="scrubbingValueChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="MLD-nW-rXm"/>
|
||||
<action selector="startScrubbing:" destination="vXZ-lx-hvc" eventType="touchDown" id="lD9-dR-QTO"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dfk-yr-rwm">
|
||||
<rect key="frame" x="16" y="354" width="343" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T7Y-1Q-7UU">
|
||||
<rect key="frame" x="16" y="379.5" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="1ML-yD-9Rf">
|
||||
<rect key="frame" x="177.5" y="587" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ErrorText" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iCe-6A-2My">
|
||||
<rect key="frame" x="158.5" y="588.5" width="58.5" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="0eh-sL-186"/>
|
||||
<constraint firstItem="iCe-6A-2My" firstAttribute="centerY" secondItem="1ML-yD-9Rf" secondAttribute="centerY" id="4Fp-kE-AAg"/>
|
||||
<constraint firstItem="l9B-hM-Ajc" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="54L-0h-0ba"/>
|
||||
<constraint firstItem="l9B-hM-Ajc" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="9Uh-K9-988"/>
|
||||
<constraint firstItem="RVb-HZ-QCX" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="BhV-UD-qhh"/>
|
||||
<constraint firstItem="iCe-6A-2My" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="Dhm-Bn-wZH"/>
|
||||
<constraint firstItem="FCd-3e-22D" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="GhI-f1-DkR"/>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="HoH-i0-yof"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="Nw7-WM-LFd"/>
|
||||
<constraint firstItem="RX3-VR-CL6" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="O0h-NL-iXW"/>
|
||||
<constraint firstItem="1ML-yD-9Rf" firstAttribute="top" secondItem="EOo-zV-6l2" secondAttribute="bottom" constant="20" id="Uop-aD-I5b"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="top" secondItem="FCd-3e-22D" secondAttribute="bottom" constant="30" id="W4w-6K-AW8"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="top" secondItem="T7Y-1Q-7UU" secondAttribute="bottom" constant="25" id="XgV-XL-QCL"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="YUE-uf-Rp1"/>
|
||||
<constraint firstItem="RVb-HZ-QCX" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="ZkD-u2-Zbr"/>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="top" secondItem="dfk-yr-rwm" secondAttribute="bottom" constant="4" id="baR-zV-tgo"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="eNt-u9-qot"/>
|
||||
<constraint firstItem="1ML-yD-9Rf" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="fdl-RK-Hq8"/>
|
||||
<constraint firstItem="RX3-VR-CL6" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="16" id="hEd-b2-Ggo"/>
|
||||
<constraint firstItem="FCd-3e-22D" firstAttribute="top" secondItem="l9B-hM-Ajc" secondAttribute="bottom" constant="30" id="ikz-ZP-jNM"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="RX3-VR-CL6" secondAttribute="trailing" constant="16" id="kSP-Mq-R5P"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="m6u-7a-ffF"/>
|
||||
<constraint firstItem="3CL-8o-zYW" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="sGK-bn-zxD"/>
|
||||
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="RX3-VR-CL6" secondAttribute="bottom" constant="100" id="vd2-dd-hVu"/>
|
||||
<constraint firstItem="3CL-8o-zYW" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="wOy-Rx-rvK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="artistLabel" destination="T7Y-1Q-7UU" id="b5S-lt-PqG"/>
|
||||
<outlet property="elapsedTimeLabel" destination="3CL-8o-zYW" id="7Wg-7X-Vrd"/>
|
||||
<outlet property="errorLabel" destination="iCe-6A-2My" id="T4b-0b-wdM"/>
|
||||
<outlet property="imageView" destination="FCd-3e-22D" id="gKL-za-haV"/>
|
||||
<outlet property="loadIndicator" destination="1ML-yD-9Rf" id="Xes-Ag-vhg"/>
|
||||
<outlet property="playButton" destination="EOo-zV-6l2" id="2d1-ad-s1k"/>
|
||||
<outlet property="remainingTimeLabel" destination="RVb-HZ-QCX" id="8hp-CK-XjF"/>
|
||||
<outlet property="slider" destination="RWN-If-dGG" id="Yxw-Gf-bR3"/>
|
||||
<outlet property="titleLabel" destination="dfk-yr-rwm" id="Hk3-m5-IOi"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="117.59999999999999" y="118.29085457271366"/>
|
||||
</scene>
|
||||
<!--Queue View Controller-->
|
||||
<scene sceneID="5Fm-oE-9Zc">
|
||||
<objects>
|
||||
<viewController id="vDz-qW-uY8" customClass="QueueViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="kv3-s6-lb0"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="Fhe-7w-8BG"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="y7Y-Gm-oyZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dzA-9p-ejh">
|
||||
<rect key="frame" x="310" y="20" width="49" height="34"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeButton:" destination="vDz-qW-uY8" eventType="touchUpInside" id="0TB-bG-he7"/>
|
||||
</connections>
|
||||
</button>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HPi-Pd-J9K">
|
||||
<rect key="frame" x="0.0" y="74" width="375" height="593"/>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="tintColor" red="1" green="0.1857388616" blue="0.57339501380000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="HPi-Pd-J9K" secondAttribute="trailing" id="CdI-lT-19N"/>
|
||||
<constraint firstItem="Fhe-7w-8BG" firstAttribute="top" secondItem="HPi-Pd-J9K" secondAttribute="bottom" id="Gb9-C1-ajx"/>
|
||||
<constraint firstItem="HPi-Pd-J9K" firstAttribute="leading" secondItem="y7Y-Gm-oyZ" secondAttribute="leading" id="aN2-LD-yxR"/>
|
||||
<constraint firstItem="HPi-Pd-J9K" firstAttribute="top" secondItem="dzA-9p-ejh" secondAttribute="bottom" constant="20" id="aSx-t1-T3e"/>
|
||||
<constraint firstItem="dzA-9p-ejh" firstAttribute="top" secondItem="kv3-s6-lb0" secondAttribute="bottom" id="nAL-i2-VQS"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dzA-9p-ejh" secondAttribute="trailing" constant="16" id="qrg-S3-JJ2"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="tableView" destination="HPi-Pd-J9K" id="P8P-at-xLc"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zk4-9r-5Oh" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="917.60000000000002" y="117.39130434782609"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,15 +1,13 @@
|
||||
//
|
||||
// Double + Extensions.swift
|
||||
// SwiftAudio_Example
|
||||
// Extensions.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
// Created by Brandon Sneed on 3/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
|
||||
private var formatter: DateComponentsFormatter {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
@@ -21,5 +19,4 @@ extension Double {
|
||||
func secondsToString() -> String {
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,48 +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>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// PlayerView.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Brandon Sneed on 3/30/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftAudioEx
|
||||
|
||||
struct PlayerView: View {
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
@State private var showingQueue = false
|
||||
|
||||
let controller = AudioController.shared
|
||||
|
||||
init(viewModel: PlayerView.ViewModel = ViewModel()) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
Button(action: { showingQueue.toggle() }, label: {
|
||||
Text("Queue")
|
||||
.fontWeight(.bold)
|
||||
})
|
||||
}
|
||||
|
||||
if let image = viewModel.artwork {
|
||||
#if os(macOS)
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 240, height: 240)
|
||||
.padding(.top, 30)
|
||||
#elseif os(iOS)
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 240, height: 240)
|
||||
.padding(.top, 30)
|
||||
#endif
|
||||
} else {
|
||||
AsyncImage(url: nil)
|
||||
.frame(width: 240, height: 240)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(viewModel.title)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: 18))
|
||||
Text(viewModel.artist)
|
||||
.fontWeight(.thin)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
|
||||
if viewModel.maxTime > 0 {
|
||||
VStack {
|
||||
Slider(value: $viewModel.position, in: 0...viewModel.maxTime) { editing in
|
||||
viewModel.isScrubbing = editing
|
||||
print("scrubbing = \(viewModel.isScrubbing)")
|
||||
if viewModel.isScrubbing == false {
|
||||
controller.player.seek(to: viewModel.position)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text(viewModel.elapsedTime)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(viewModel.remainingTime)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
}
|
||||
.padding(.top, 25)
|
||||
} else {
|
||||
Text("Live Stream")
|
||||
.padding(.top, 35)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button(action: controller.player.previous, label: {
|
||||
Text("Prev")
|
||||
.font(.system(size: 14))
|
||||
})
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Button(action: {
|
||||
if viewModel.playing {
|
||||
controller.player.pause()
|
||||
} else {
|
||||
controller.player.play()
|
||||
}
|
||||
}, label: {
|
||||
Text(!viewModel.playWhenReady || viewModel.playbackState == .failed ? "Play" : "Pause")
|
||||
.font(.system(size: 18))
|
||||
.fontWeight(.semibold)
|
||||
})
|
||||
|
||||
.frame(maxWidth: .infinity)
|
||||
Button(action: controller.player.next, label: {
|
||||
Text("Next")
|
||||
.font(.system(size: 14))
|
||||
})
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.top, 80)
|
||||
|
||||
VStack {
|
||||
if viewModel.playbackState == .failed {
|
||||
Text("Playback failed.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.red)
|
||||
.padding(.top, 20)
|
||||
} else if (viewModel.playbackState == .loading || viewModel.playbackState == .buffering) && viewModel.playWhenReady {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.controlSize(.small)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.sheet(isPresented: $showingQueue) {
|
||||
QueueView()
|
||||
#if os(macOS)
|
||||
.frame(width: 300, height: 400)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Standard") {
|
||||
let viewModel = PlayerView.ViewModel()
|
||||
viewModel.title = "Longing"
|
||||
viewModel.artist = "David Chavez"
|
||||
|
||||
return PlayerView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
#Preview("Error") {
|
||||
let viewModel = PlayerView.ViewModel()
|
||||
viewModel.title = "Longing"
|
||||
viewModel.artist = "David Chavez"
|
||||
viewModel.playbackState = .failed
|
||||
|
||||
return PlayerView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
#Preview("Buffering") {
|
||||
let viewModel = PlayerView.ViewModel()
|
||||
viewModel.title = "Longing"
|
||||
viewModel.artist = "David Chavez"
|
||||
viewModel.playbackState = .buffering
|
||||
viewModel.playWhenReady = true
|
||||
|
||||
return PlayerView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
#Preview("Live Stream") {
|
||||
let viewModel = PlayerView.ViewModel()
|
||||
viewModel.title = "Longing"
|
||||
viewModel.artist = "David Chavez"
|
||||
viewModel.maxTime = 0
|
||||
|
||||
return PlayerView(viewModel: viewModel)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// PlayerViewModel.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by David Chavez on 4/12/24.
|
||||
//
|
||||
|
||||
import SwiftAudioEx
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
public typealias NativeImage = NSImage
|
||||
#elseif os(iOS)
|
||||
import UIKit
|
||||
public typealias NativeImage = UIImage
|
||||
#endif
|
||||
|
||||
extension PlayerView {
|
||||
final class ViewModel: ObservableObject {
|
||||
// MARK: - Observables
|
||||
|
||||
@Published var playing: Bool = false
|
||||
@Published var position: Double = 0
|
||||
@Published var artwork: NativeImage? = nil
|
||||
@Published var title: String = ""
|
||||
@Published var artist: String = ""
|
||||
@Published var maxTime: TimeInterval = 100
|
||||
@Published var isScrubbing: Bool = false
|
||||
@Published var elapsedTime: String = "00:00"
|
||||
@Published var remainingTime: String = "00:00"
|
||||
|
||||
@Published var playWhenReady: Bool = false
|
||||
@Published var playbackState: AudioPlayerState = .idle
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let controller = AudioController.shared
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Updates
|
||||
|
||||
private func render() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
playing = (controller.player.playerState == .playing)
|
||||
playbackState = controller.player.playerState
|
||||
playWhenReady = controller.player.playWhenReady
|
||||
position = controller.player.currentTime
|
||||
maxTime = controller.player.duration
|
||||
artist = controller.player.currentItem?.getArtist() ?? ""
|
||||
title = controller.player.currentItem?.getTitle() ?? ""
|
||||
elapsedTime = controller.player.currentTime.secondsToString()
|
||||
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
if let item = controller.player.currentItem as? DefaultAudioItem {
|
||||
artwork = item.artwork
|
||||
} else {
|
||||
artwork = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func renderTimes() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
position = controller.player.currentTime
|
||||
maxTime = controller.player.duration
|
||||
elapsedTime = controller.player.currentTime.secondsToString()
|
||||
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
print(elapsedTime)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print("state=\(data)")
|
||||
render()
|
||||
}
|
||||
|
||||
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
|
||||
print("playWhenReady=\(data)")
|
||||
render()
|
||||
}
|
||||
|
||||
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
|
||||
print("playEndReason=\(data)")
|
||||
}
|
||||
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
renderTimes()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
|
||||
// .. don't need this
|
||||
}
|
||||
|
||||
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
|
||||
if !isScrubbing {
|
||||
renderTimes()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
// .. don't need this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// QueueTableViewCell.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class QueueTableViewCell: UITableViewCell {
|
||||
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var artistLabel: UILabel!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="79.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R0I-g7-ETn">
|
||||
<rect key="frame" x="16" y="16" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jRU-3B-2pA">
|
||||
<rect key="frame" x="16" y="43.5" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="8gl-XI-iAW"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="A7F-XO-H0i"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="top" secondItem="R0I-g7-ETn" secondAttribute="bottom" constant="8" id="Jdu-e3-Oeq"/>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="VNU-d7-G4N"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="jRU-3B-2pA" secondAttribute="bottom" constant="6" id="nBr-J4-PUM"/>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="5" id="tE6-pp-JML"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="z3F-hI-GcC"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="artistLabel" destination="jRU-3B-2pA" id="IVV-n5-wmt"/>
|
||||
<outlet property="titleLabel" destination="R0I-g7-ETn" id="ICg-6a-6vz"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="34.5" y="54"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// QueueView.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by David Chavez on 4/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftAudioEx
|
||||
|
||||
struct QueueView: View {
|
||||
let controller = AudioController.shared
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
List {
|
||||
if controller.player.currentItem != nil {
|
||||
Section(header: Text("Playing Now")) {
|
||||
QueueItemView(
|
||||
title: controller.player.currentItem?.getTitle() ?? "",
|
||||
artist: controller.player.currentItem?.getArtist() ?? ""
|
||||
)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Up Next")) {
|
||||
ForEach(controller.player.nextItems as! [DefaultAudioItem]) { item in
|
||||
QueueItemView(
|
||||
title: item.getTitle() ?? "",
|
||||
artist: item.getArtist() ?? ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Queue")
|
||||
.toolbar {
|
||||
Button("Close") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QueueItemView: View {
|
||||
let title: String
|
||||
let artist: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.fontWeight(.semibold)
|
||||
Text(artist)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
QueueView()
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
//
|
||||
// QueueViewController.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueViewController: UIViewController {
|
||||
|
||||
let controller = AudioController.shared
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
let cellReuseId: String = "QueueCell"
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(UINib.init(nibName: "QueueTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: cellReuseId)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
}
|
||||
|
||||
@IBAction func closeButton(_ sender: UIButton) {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0:
|
||||
return 1
|
||||
case 1:
|
||||
return controller.player.nextItems.count
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) as! QueueTableViewCell
|
||||
|
||||
let item: AudioItem?
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
item = controller.player.currentItem
|
||||
case 1:
|
||||
item = controller.player.nextItems[indexPath.row]
|
||||
default:
|
||||
item = nil
|
||||
}
|
||||
|
||||
if let item = item {
|
||||
cell.titleLabel.text = item.getTitle()
|
||||
cell.artistLabel.text = item.getArtist()
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0: return "Playing Now"
|
||||
case 1: return "Up Next"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// SwiftAudioApp.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Brandon Sneed on 3/30/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SwiftAudioApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
PlayerView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 03/11/2018.
|
||||
// Copyright (c) 2018 Jørgen Henrichsen. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudioEx
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var playButton: UIButton!
|
||||
@IBOutlet weak var slider: UISlider!
|
||||
@IBOutlet weak var imageView: UIImageView!
|
||||
@IBOutlet weak var remainingTimeLabel: UILabel!
|
||||
@IBOutlet weak var elapsedTimeLabel: UILabel!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var artistLabel: UILabel!
|
||||
@IBOutlet weak var loadIndicator: UIActivityIndicatorView!
|
||||
@IBOutlet weak var errorLabel: UILabel!
|
||||
|
||||
private var isScrubbing: Bool = false
|
||||
private let controller = AudioController.shared
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
controller.player.playWhenReady = playButton.currentTitle == "Play"
|
||||
}
|
||||
|
||||
@IBAction func previous(_ sender: Any) {
|
||||
controller.player.previous()
|
||||
}
|
||||
|
||||
@IBAction func next(_ sender: Any) {
|
||||
controller.player.next()
|
||||
}
|
||||
|
||||
@IBAction func startScrubbing(_ sender: UISlider) {
|
||||
isScrubbing = true
|
||||
}
|
||||
|
||||
@IBAction func scrubbing(_ sender: UISlider) {
|
||||
controller.player.seek(to: Double(slider.value))
|
||||
}
|
||||
|
||||
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
|
||||
let value = Double(slider.value)
|
||||
elapsedTimeLabel.text = value.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
|
||||
}
|
||||
|
||||
// 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 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
|
||||
})
|
||||
}
|
||||
|
||||
// Render time values
|
||||
self.renderTimeValues()
|
||||
|
||||
// Render error label
|
||||
if (player.playerState == .failed) {
|
||||
self.errorLabel.isHidden = false
|
||||
self.errorLabel.text = "Playback failed."
|
||||
} else {
|
||||
self.errorLabel.text = ""
|
||||
self.errorLabel.isHidden = true
|
||||
}
|
||||
|
||||
// Render load indicator:
|
||||
if (
|
||||
(player.playerState == .loading || player.playerState == .buffering)
|
||||
&& self.controller.player.playWhenReady // Avoid showing indicator before user has pressed play
|
||||
) {
|
||||
self.loadIndicator.startAnimating()
|
||||
} else {
|
||||
self.loadIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AudioPlayer Event Handlers
|
||||
|
||||
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
|
||||
print("state=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
|
||||
print("playWhenReady=\(data)")
|
||||
DispatchQueue.main.async {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
|
||||
print("playEndReason=\(data)")
|
||||
}
|
||||
|
||||
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
|
||||
if !isScrubbing {
|
||||
DispatchQueue.main.async {
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
|
||||
isScrubbing = false
|
||||
}
|
||||
|
||||
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
|
||||
DispatchQueue.main.async {
|
||||
self.renderTimeValues()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
}
|
||||
@@ -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,80 +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 {
|
||||
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
|
||||
|
||||
}
|
||||
|
||||
var receivedCommonMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
|
||||
|
||||
func item(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
|
||||
receivedCommonMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var receivedTimedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedTimedMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var receivedChapterMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
|
||||
|
||||
func item(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
|
||||
receivedChapterMetadata?(metadata)
|
||||
}
|
||||
|
||||
|
||||
var updateDuration: ((_ duration: Double) -> Void)?
|
||||
|
||||
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,590 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import Foundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: QuickSpec {
|
||||
override func spec() {
|
||||
beforeSuite {
|
||||
Nimble.AsyncDefaults.timeout = .seconds(10)
|
||||
Nimble.AsyncDefaults.pollInterval = .milliseconds(100)
|
||||
}
|
||||
describe("AudioPlayer") {
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
var playerStateEventListener: QueuedAudioPlayer.PlayerStateEventListener!
|
||||
beforeEach {
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
playerStateEventListener = QueuedAudioPlayer.PlayerStateEventListener()
|
||||
audioPlayer.event.stateChange.addListener(
|
||||
playerStateEventListener,
|
||||
playerStateEventListener.handleEvent
|
||||
)
|
||||
}
|
||||
|
||||
afterEach {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
}
|
||||
|
||||
// MARK: - Load
|
||||
context("when loading audio item") {
|
||||
it("should never mutate playWhenReady to false") {
|
||||
audioPlayer.playWhenReady = true
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
}
|
||||
|
||||
it("should never mutate playWhenReady to true") {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
|
||||
it("should mutate playWhenReady when loading with playWhenReady equals true") {
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
|
||||
it("should mutate playWhenReady when loading with playWhenReady equals false") {
|
||||
audioPlayer.playWhenReady = false
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playWhenReady).to(beTrue())
|
||||
}
|
||||
|
||||
it("should seek when audio item sets initial time") {
|
||||
var seekCompleted = false
|
||||
listener.onSeekCompletion = {
|
||||
seekCompleted = true
|
||||
}
|
||||
audioPlayer.playWhenReady = false
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
audioPlayer.load(item: FiveSecondSourceWithInitialTimeOfFourSeconds.getAudioItem())
|
||||
expect(seekCompleted).toEventually(beTrue())
|
||||
expect(audioPlayer?.currentTime ?? 0).to(beGreaterThanOrEqualTo(4))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Duration
|
||||
context("when dealing with duration") {
|
||||
it("should set duration eventually after loading") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
|
||||
it("audioPlayer.event.updateDuration should receive duration after loading") {
|
||||
var receivedUpdateDuration = false
|
||||
listener.onUpdateDuration = { duration in
|
||||
receivedUpdateDuration = true
|
||||
expect(duration).to(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(receivedUpdateDuration).toEventually(beTrue())
|
||||
}
|
||||
|
||||
it("should reset duration after loading again") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
}
|
||||
|
||||
it("should reset duration after reset") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
expect(audioPlayer.duration).toEventually(beCloseTo(5, within: 0.1))
|
||||
audioPlayer.clear()
|
||||
expect(audioPlayer.duration).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Failure
|
||||
context("when handling failure") {
|
||||
it("should emit fail event on load with non-malformed URL") {
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
}
|
||||
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: "", // malformed url
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).toNot(beNil())
|
||||
expect(audioPlayer.playerState).to(equal(.failed))
|
||||
expect(didReceiveFail).to(beTrue())
|
||||
}
|
||||
|
||||
it("should emit fail event on load with non-existing resource") {
|
||||
var didReceiveFail = false
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true
|
||||
}
|
||||
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3"
|
||||
let item = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream)
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).toEventuallyNot(beNil())
|
||||
expect(audioPlayer.playerState).to(equal(.failed))
|
||||
expect(didReceiveFail).to(beTrue())
|
||||
}
|
||||
|
||||
context("calling play after failure") {
|
||||
it("should retry loading") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("setting playWhenReady after failure") {
|
||||
it("should retry loading") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([ .loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("calling reload after failure") {
|
||||
it("should retry loading but fail again with same broken source") {
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(
|
||||
audioUrl: nonExistingUrl,
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
albumTitle: "AlbumTitle",
|
||||
sourceType: .stream
|
||||
);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed]))
|
||||
|
||||
audioPlayer.reload(startFromCurrentTime: true)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal([.loading, .failed, .loading, .failed]))
|
||||
}
|
||||
}
|
||||
|
||||
context("load resource") {
|
||||
it("should succeed after previous failure") {
|
||||
var didReceiveFail = false;
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true;
|
||||
}
|
||||
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let failItem = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream);
|
||||
audioPlayer.load(item: failItem, playWhenReady: false)
|
||||
expect(didReceiveFail).toEventually(beTrue())
|
||||
expect(audioPlayer.playerState).toEventually(equal(.failed))
|
||||
expect(playerStateEventListener.states).toEventually(equal([.loading, .failed]))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).to(beNil())
|
||||
expect(playerStateEventListener.statesWithoutBuffering)
|
||||
.toEventually(equal([.loading, .failed, .loading, .playing]))
|
||||
}
|
||||
|
||||
it("with playWhenReady=false it should succeed after previous failure") {
|
||||
var didReceiveFail = false;
|
||||
listener.onReceiveFail = { error in
|
||||
didReceiveFail = true;
|
||||
}
|
||||
let nonExistingUrl = "https://\(String.random(length: 100)).com/\(String.random(length: 100)).mp3";
|
||||
let item = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream);
|
||||
audioPlayer.load(item: item, playWhenReady: true)
|
||||
expect(didReceiveFail).toEventually(beTrue())
|
||||
expect(audioPlayer.playerState).toEventually(equal(.failed))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playbackError).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - States
|
||||
context("states") {
|
||||
it("should initially be idle") {
|
||||
expect(audioPlayer.playerState).to(equal(.idle))
|
||||
}
|
||||
|
||||
it("should be loading after load source") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).to(equal(.loading))
|
||||
}
|
||||
|
||||
it("should become ready after load source") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
}
|
||||
|
||||
it("should be playing after load source with playWhenReady") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
it("should emit events in reliable order") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
it("should update playWhenReady after external pause") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing];
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(0.0))
|
||||
|
||||
// Simulate avplayer becoming paused due to external reason:
|
||||
audioPlayer.wrapper.rate = 0
|
||||
|
||||
expectedEvents.append(.paused);
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
expect(audioPlayer.playWhenReady).to(beFalse())
|
||||
}
|
||||
|
||||
it("should emit events in reliable order at end call stop") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.stop()
|
||||
expectedEvents.append(.stopped)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
|
||||
it("should emit events in reliable order also after loading after reset") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents : [AVPlayerWrapperState] = [.loading, .playing]
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expectedEvents.append(contentsOf: [.loading, .playing])
|
||||
expect(playerStateEventListener.statesWithoutBuffering).toEventually(equal(expectedEvents))
|
||||
}
|
||||
|
||||
it("should be playing after calling play()") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
audioPlayer.play()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
|
||||
it("should be paused after calling pause()") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.pause()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.paused))
|
||||
}
|
||||
|
||||
it("should be paused after setting playWhenReady to false") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.playWhenReady = false
|
||||
expect(audioPlayer.playerState).toEventually(equal(.paused))
|
||||
}
|
||||
|
||||
it("should be playing after setting playWhenReady to true") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ready))
|
||||
audioPlayer.playWhenReady = true
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
}
|
||||
|
||||
it("should be stopped after stop") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.playing))
|
||||
audioPlayer.stop()
|
||||
expect(audioPlayer.playerState).toEventually(equal(.stopped))
|
||||
}
|
||||
}
|
||||
// MARK: - States
|
||||
context("current time") {
|
||||
it("should be 0 initially") {
|
||||
expect(audioPlayer.currentTime).to(equal(0.0))
|
||||
}
|
||||
|
||||
it("audioPlayer.event.secondElapse should be emitted when playing") {
|
||||
var onSecondsElapseTime = 0.0
|
||||
audioPlayer.timeEventFrequency = .everyQuarterSecond
|
||||
listener.onSecondsElapse = { time in
|
||||
onSecondsElapseTime = time
|
||||
}
|
||||
audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
expect(onSecondsElapseTime).toEventually(beGreaterThan(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Buffer
|
||||
context("buffer") {
|
||||
it("automaticallyWaitsToMinimizeStalling should be true") {
|
||||
expect(audioPlayer.automaticallyWaitsToMinimizeStalling).to(beTrue())
|
||||
}
|
||||
it("bufferDuration should be zero") {
|
||||
expect(audioPlayer.bufferDuration).to(equal(0))
|
||||
}
|
||||
it("setting bufferDuration disables automaticallyWaitsToMinimizeStalling") {
|
||||
audioPlayer.bufferDuration = 1;
|
||||
expect(audioPlayer.bufferDuration).to(equal(1))
|
||||
expect(audioPlayer.automaticallyWaitsToMinimizeStalling).to(beFalse())
|
||||
}
|
||||
it("enabling automaticallyWaitsToMinimizeStalling sets bufferDuration to zero") {
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
||||
expect(audioPlayer.bufferDuration).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Seek
|
||||
context("Seek") {
|
||||
it("Seeking should work before loading is complete") {
|
||||
let player = audioPlayer
|
||||
player!.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
player!.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(4.75))
|
||||
}
|
||||
it("Seeking should work after loading is complete") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(beGreaterThan(4.75))
|
||||
}
|
||||
it("Seeking should work when paused") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toEventually(equal(4.75))
|
||||
}
|
||||
it("Seeking can not change currentTime when stopped") {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.stop()
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.currentTime).toNotEventually(equal(4.75))
|
||||
expect(audioPlayer.currentTime).to(equal(0))
|
||||
}
|
||||
}
|
||||
// MARK: - Rate
|
||||
context("Rate") {
|
||||
it("should be 1 initially") {
|
||||
expect(audioPlayer.rate).to(equal(1))
|
||||
}
|
||||
it("should speed up playback when setting to more than 1") {
|
||||
var start: Date? = nil;
|
||||
var end: Date? = nil;
|
||||
|
||||
listener.onPlaybackEnd = { reason in
|
||||
if (reason == .playedUntilEnd) {
|
||||
end = Date()
|
||||
}
|
||||
}
|
||||
|
||||
listener.onStateChange = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
if (start == nil) {
|
||||
start = Date()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
audioPlayer.rate = 10
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ended))
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start);
|
||||
expect(duration).to(beLessThan(1))
|
||||
}
|
||||
}
|
||||
|
||||
it("should slow down playback when setting to less than 1") {
|
||||
var start: Date? = nil;
|
||||
var end: Date? = nil;
|
||||
|
||||
listener.onPlaybackEnd = { reason in
|
||||
if (reason == .playedUntilEnd) {
|
||||
end = Date()
|
||||
}
|
||||
}
|
||||
|
||||
audioPlayer.rate = 0.5
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
listener.onStateChange = { state in
|
||||
switch state {
|
||||
case .playing:
|
||||
if (start == nil) {
|
||||
start = Date()
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
audioPlayer.seek(to: 4.75)
|
||||
expect(audioPlayer.playerState).toEventually(equal(.ended))
|
||||
if let start = start, let end = end {
|
||||
let duration = end.timeIntervalSince(start);
|
||||
expect(duration).to(beLessThanOrEqualTo(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - Current Item
|
||||
context("Current Item") {
|
||||
it("should be nil initially") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
it("should not be nil after loading") {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
expect(audioPlayer.currentItem?.getSourceUrl()).to(equal(Source.getAudioItem().getSourceUrl()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioSessionControllerTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("An AudioSessionController") {
|
||||
let audioSessionController: AudioSessionController = AudioSessionController(audioSession: NonFailingAudioSession())
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
|
||||
context("when session is activated") {
|
||||
beforeEach {
|
||||
try? audioSessionController.activateSession()
|
||||
}
|
||||
|
||||
it("should be active") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beTrue())
|
||||
}
|
||||
|
||||
context("when deactivating session") {
|
||||
beforeEach {
|
||||
try? audioSessionController.deactivateSession()
|
||||
}
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its isObservingForInterruptions") {
|
||||
it("should be true") {
|
||||
expect(audioSessionController.isObservingForInterruptions).to(beTrue())
|
||||
}
|
||||
|
||||
context("when isObservingForInterruptions is set to false") {
|
||||
beforeEach {
|
||||
audioSessionController.isObservingForInterruptions = false
|
||||
}
|
||||
|
||||
it("should be false") {
|
||||
expect(audioSessionController.isObservingForInterruptions).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its delegate") {
|
||||
context("when a ended interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(0),
|
||||
AVAudioSessionInterruptionOptionKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.ended(shouldResume: true)))
|
||||
}
|
||||
|
||||
}
|
||||
context("when a begin interruption arrives") {
|
||||
var delegate: AudioSessionControllerDelegateImplementation!
|
||||
beforeEach {
|
||||
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
|
||||
AVAudioSessionInterruptionTypeKey: UInt(1),
|
||||
])
|
||||
delegate = AudioSessionControllerDelegateImplementation()
|
||||
audioSessionController.delegate = delegate
|
||||
audioSessionController.handleInterruption(notification: notification)
|
||||
}
|
||||
|
||||
it("should eventually be updated with the interruption type") {
|
||||
expect(delegate.interruptionType).toEventually(equal(InterruptionType.began))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("An AudioSessionController with a failing AudioSession") {
|
||||
var audioSessionController: AudioSessionController!
|
||||
beforeEach {
|
||||
audioSessionController = AudioSessionController(audioSession: FailingAudioSession())
|
||||
}
|
||||
|
||||
context("when activated") {
|
||||
beforeEach {
|
||||
try? audioSessionController.activateSession()
|
||||
}
|
||||
|
||||
it("should be inactive") {
|
||||
expect(audioSessionController.audioSessionIsActive).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
var interruptionType: InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,72 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class NowPlayingInfoControllerTests: QuickSpec {
|
||||
|
||||
override func spec() {
|
||||
describe("An NowPlayingInfoController") {
|
||||
|
||||
var nowPlayingController: NowPlayingInfoController!
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: NowPlayingInfoCenter_Mock())
|
||||
}
|
||||
|
||||
describe("its info dictionary") {
|
||||
|
||||
context("when setting a value") {
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.info.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(nowPlayingController.info.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("its info center") {
|
||||
|
||||
context("when setting a value") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should not be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
|
||||
}
|
||||
|
||||
context("then calling clear()") {
|
||||
|
||||
beforeEach {
|
||||
nowPlayingController.clear()
|
||||
}
|
||||
|
||||
it("should be empty") {
|
||||
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
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()
|
||||
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,673 +0,0 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueManagerTests: QuickSpec {
|
||||
|
||||
let dummyItem = 0
|
||||
|
||||
let items: [Int] = [0, 1, 2]
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("A QueueManager") {
|
||||
|
||||
var queue: QueueManager<Int>!
|
||||
|
||||
beforeEach {
|
||||
queue = QueueManager()
|
||||
}
|
||||
|
||||
describe("its current item") {
|
||||
|
||||
it("should be nil starting out") {
|
||||
expect(queue.current).to(beNil())
|
||||
}
|
||||
|
||||
context("when one item is added") {
|
||||
beforeEach {
|
||||
queue.add(self.dummyItem)
|
||||
}
|
||||
|
||||
it("should be nil, because it wasn't jumped to") {
|
||||
expect(queue.current).to(beNil())
|
||||
}
|
||||
|
||||
context("after being jumped to") {
|
||||
beforeEach {
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
|
||||
it("should be the added item") {
|
||||
expect(queue.current).to(equal(self.dummyItem))
|
||||
}
|
||||
|
||||
context("then replaced") {
|
||||
beforeEach {
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should be the new item") {
|
||||
expect(queue.current).to(equal(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when replacing the current item when the queue is still empty") {
|
||||
beforeEach {
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
}
|
||||
|
||||
it("the current item should be the replaced item") {
|
||||
expect(queue.current).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("when multiple items are added and the last is jumped to") {
|
||||
beforeEach {
|
||||
queue.add(self.items)
|
||||
try! queue.jump(to: queue.items.count - 1)
|
||||
}
|
||||
|
||||
it("should not be nil") {
|
||||
expect(queue.current).toNot(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
describe("when adding at index") {
|
||||
context("adding item at index 0 when queue is empty") {
|
||||
beforeEach {
|
||||
try! queue.add([3], at: 0)
|
||||
}
|
||||
it("should add element successfully") {
|
||||
expect(queue.items.first).to(equal(3))
|
||||
}
|
||||
it("should not set currentItem") {
|
||||
expect(queue.current).to(beNil())
|
||||
}
|
||||
it("should not set currentIndex") {
|
||||
expect(queue.currentIndex).to(equal(-1))
|
||||
}
|
||||
}
|
||||
|
||||
context("adding item at index and jumping to the first item") {
|
||||
beforeEach {
|
||||
queue.add([1, 2])
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
|
||||
context("adding item at current [element count]") {
|
||||
it("should add element successfully") {
|
||||
try queue.add([3, 4, 5], at: queue.items.count)
|
||||
expect(queue.items.last).to(equal(5))
|
||||
}
|
||||
|
||||
context("before the first item") {
|
||||
it("should add element successfully") {
|
||||
try queue.add([-1], at: 0)
|
||||
expect(queue.items.first).to(equal(-1))
|
||||
}
|
||||
}
|
||||
|
||||
context("after the last item") {
|
||||
it("should add element successfully") {
|
||||
try queue.add([6], at: queue.items.count)
|
||||
expect(queue.items.last).to(equal(6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("calling next, causing currentIndex to become 1, then adding at index 1") {
|
||||
beforeEach {
|
||||
queue.next()
|
||||
try! queue.add([5], at: queue.currentIndex)
|
||||
}
|
||||
it("should cause the current item to be shifted to index 2") {
|
||||
expect(queue.current).to(equal(2))
|
||||
expect(queue.currentIndex).to(equal(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("when adding one item but not jumping to it yet") {
|
||||
|
||||
beforeEach {
|
||||
queue.add(0)
|
||||
}
|
||||
|
||||
it("should have an item in the queue") {
|
||||
expect(queue.items.count).to(equal(1))
|
||||
}
|
||||
|
||||
context("then replacing the item") {
|
||||
beforeEach {
|
||||
queue.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should have added an item and jumped to it") {
|
||||
expect(queue.items.count).to(equal(2))
|
||||
expect(queue.current).to(equal(1))
|
||||
expect(queue.currentIndex).to(equal(1))
|
||||
}
|
||||
|
||||
context("then calling next") {
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
item = queue.next()
|
||||
}
|
||||
|
||||
it("should noop") {
|
||||
expect(item).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling previous") {
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
item = queue.previous()
|
||||
}
|
||||
|
||||
it("should go back to the first") {
|
||||
expect(item).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next") {
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
item = queue.next()
|
||||
}
|
||||
|
||||
it("should noop") {
|
||||
expect(item).to(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling previous") {
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
item = queue.previous()
|
||||
}
|
||||
|
||||
it("should noop") {
|
||||
expect(item).to(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to 0 and calling next(wrap: true)") {
|
||||
|
||||
var nextIndex: Int?
|
||||
|
||||
beforeEach {
|
||||
try! queue.jump(to: 0)
|
||||
nextIndex = queue.next(wrap: true)
|
||||
}
|
||||
|
||||
it("should wrap to itself") {
|
||||
expect(nextIndex).to(equal(0))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("then jumping to 0 and then calling previous(wrap: true") {
|
||||
var previousIndex: Int?
|
||||
|
||||
beforeEach {
|
||||
try! queue.jump(to: 0)
|
||||
previousIndex = queue.previous(wrap: true)
|
||||
}
|
||||
|
||||
it("should wrap to itself") {
|
||||
expect(previousIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("when adding multiple items") {
|
||||
|
||||
beforeEach {
|
||||
queue.add(self.items)
|
||||
}
|
||||
|
||||
it("should have items in the queue") {
|
||||
expect(queue.items.count).to(equal(self.items.count))
|
||||
}
|
||||
|
||||
it("the current item should be nil") {
|
||||
expect(queue.current).to(beNil())
|
||||
}
|
||||
|
||||
it("should not have next items") {
|
||||
expect(queue.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
context("when jumping to first item") {
|
||||
beforeEach {
|
||||
try! queue.jump(to: 0)
|
||||
}
|
||||
|
||||
context("then calling next") {
|
||||
var nextItem: Int?
|
||||
beforeEach {
|
||||
nextItem = queue.next()
|
||||
}
|
||||
|
||||
it("should return the next item") {
|
||||
expect(nextItem).toNot(beNil())
|
||||
expect(nextItem).to(equal(self.items[1]))
|
||||
}
|
||||
|
||||
it("should have next current item") {
|
||||
expect(queue.current).to(equal(self.items[1]))
|
||||
}
|
||||
|
||||
it("should have previous items") {
|
||||
expect(queue.previousItems).toNot(beNil())
|
||||
}
|
||||
|
||||
context("then calling previous") {
|
||||
var index: Int?
|
||||
beforeEach {
|
||||
index = queue.previous()
|
||||
}
|
||||
it("should return the first item") {
|
||||
expect(index).to(equal(0))
|
||||
}
|
||||
it("should have the previous current item") {
|
||||
expect(queue.current).to(equal(self.items.first))
|
||||
}
|
||||
context("then calling previous at the start of the queue") {
|
||||
var index: Int?
|
||||
beforeEach {
|
||||
index = queue.previous()
|
||||
}
|
||||
it("should noop and return the first item") {
|
||||
expect(index).to(equal(0))
|
||||
}
|
||||
}
|
||||
context("then calling previous(wrap: true)") {
|
||||
var index: Int?
|
||||
beforeEach {
|
||||
index = queue.previous(wrap: true)
|
||||
}
|
||||
it("should return the last item") {
|
||||
expect(index).to(equal(queue.items.count - 1))
|
||||
expect(queue.currentIndex).to(equal(queue.items.count - 1))
|
||||
expect(queue.current).to(equal(self.items.last))
|
||||
}
|
||||
|
||||
context("then calling next again at the end of the queue") {
|
||||
var index: Int?
|
||||
beforeEach {
|
||||
index = queue.next()
|
||||
}
|
||||
it("should noop and return the last item") {
|
||||
expect(index).to(equal(self.items.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then calling next(wrap: true)") {
|
||||
var index: Int?
|
||||
beforeEach {
|
||||
index = queue.next(wrap: true)
|
||||
}
|
||||
it("should return the first item") {
|
||||
expect(index).to(equal(0))
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
expect(queue.current).to(equal(self.items.first))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing previous items") {
|
||||
beforeEach {
|
||||
queue.removePreviousItems()
|
||||
}
|
||||
it("should have no previous items") {
|
||||
expect(queue.previousItems.count).to(equal(0))
|
||||
}
|
||||
it("should have current index zero") {
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("adding more items") {
|
||||
var initialItemCount: Int!
|
||||
let newItems: [Int] = [10, 11, 12, 13]
|
||||
beforeEach {
|
||||
initialItemCount = queue.items.count
|
||||
try? queue.add(newItems, at: queue.items.endIndex - 1)
|
||||
}
|
||||
|
||||
it("should have more items") {
|
||||
expect(queue.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 = queue.currentIndex
|
||||
try? queue.add(newItems, at: initialCurrentIndex)
|
||||
}
|
||||
|
||||
it("currentIndex should increase by number of new items") {
|
||||
expect(queue.currentIndex).to(equal(initialCurrentIndex + newItems.count))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Removal
|
||||
|
||||
context("then removing a item with index less than currentIndex") {
|
||||
var removed: Int?
|
||||
var initialCurrentIndex: Int!
|
||||
beforeEach {
|
||||
let _ = try? queue.jump(to: 1)
|
||||
initialCurrentIndex = queue.currentIndex
|
||||
removed = try? queue.removeItem(at: initialCurrentIndex - 1)
|
||||
}
|
||||
|
||||
it("should remove the first item") {
|
||||
expect(removed).to(equal(0))
|
||||
}
|
||||
|
||||
it("should have set the initial current index to 1") {
|
||||
expect(initialCurrentIndex).to(equal(1))
|
||||
}
|
||||
|
||||
it("should decremented the currentIndex from 1 to 0") {
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the second item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? queue.removeItem(at: 1)
|
||||
}
|
||||
|
||||
it("should have one less item") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the last item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? queue.removeItem(at: self.items.count - 1)
|
||||
}
|
||||
|
||||
it("should have one less item") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the current item when it is the first item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? queue.removeItem(at: queue.currentIndex)
|
||||
}
|
||||
it("should remove the current item, and make the next item current") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count - 1))
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
expect(queue.current).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing the current item when it is the last item") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
try! queue.jump(to: queue.items.count - 1);
|
||||
removed = try? queue.removeItem(at: queue.currentIndex)
|
||||
}
|
||||
it("should remove the current item") {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count - 1))
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing with too large index") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? queue.removeItem(at: self.items.count)
|
||||
}
|
||||
|
||||
it("should not remove any items") {
|
||||
expect(removed).to(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing with too small index") {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? queue.removeItem(at: -1)
|
||||
}
|
||||
|
||||
it("should not remove any items") {
|
||||
expect(removed).to(beNil())
|
||||
expect(queue.items.count).to(equal(self.items.count))
|
||||
}
|
||||
}
|
||||
|
||||
context("then removing upcoming items") {
|
||||
beforeEach {
|
||||
queue.removeUpcomingItems()
|
||||
}
|
||||
|
||||
it("should have no next items") {
|
||||
expect(queue.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Jumping
|
||||
|
||||
context("then jumping to the current item") {
|
||||
var error: Error?
|
||||
var item: Int?
|
||||
beforeEach {
|
||||
do {
|
||||
item = try queue.jump(to: queue.currentIndex)
|
||||
}
|
||||
catch let err {
|
||||
error = err
|
||||
}
|
||||
}
|
||||
|
||||
it("should return an item") {
|
||||
expect(item).toNot(beNil())
|
||||
}
|
||||
|
||||
it("should not throw an error") {
|
||||
expect(error).to(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to the second item") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
try? jumped = queue.jump(to: 1)
|
||||
}
|
||||
|
||||
it("should return the current item") {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(queue.current))
|
||||
}
|
||||
|
||||
it("should move the current index") {
|
||||
expect(queue.currentIndex).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to last item") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
try? jumped = queue.jump(to: queue.items.count - 1)
|
||||
}
|
||||
it("should return the current item") {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(queue.current))
|
||||
}
|
||||
|
||||
it("should move the current index") {
|
||||
expect(queue.currentIndex).to(equal(queue.items.count - 1))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping to a negative index") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? queue.jump(to: -1)
|
||||
}
|
||||
|
||||
it("should not return") {
|
||||
expect(jumped).to(beNil())
|
||||
}
|
||||
|
||||
it("should not move the current index") {
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
context("then jumping with too large index") {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? queue.jump(to: queue.items.count)
|
||||
}
|
||||
it("should not return") {
|
||||
expect(jumped).to(beNil())
|
||||
}
|
||||
|
||||
it("should not move the current index") {
|
||||
expect(queue.currentIndex).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Moving
|
||||
|
||||
context("moving the current item up one") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try queue.moveItem(fromIndex: queue.currentIndex, toIndex: queue.currentIndex + 1)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should not throw an error") {
|
||||
expect(error).to(beNil())
|
||||
}
|
||||
it("should change currentIndex") {
|
||||
expect(queue.currentIndex).to(equal(1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("moving from a negative index") {
|
||||
var error: Error?
|
||||
beforeEach {
|
||||
do {
|
||||
try queue.moveItem(fromIndex: -1, toIndex: queue.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 queue.moveItem(fromIndex: queue.items.count, toIndex: queue.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 queue.moveItem(fromIndex: queue.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 queue.moveItem(fromIndex: 0, toIndex: queue.items.count)
|
||||
}
|
||||
catch let err { error = err }
|
||||
}
|
||||
|
||||
it("should not throw an error") {
|
||||
expect(error).to(beNil())
|
||||
}
|
||||
|
||||
it("should have moved the first item to the end of the queue") {
|
||||
expect(queue.items.last).to(equal(0))
|
||||
}
|
||||
it("the first item in the queue should be what was previously the second item") {
|
||||
expect(queue.items.first).to(equal(1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
context("then moving 2nd to 3rd") {
|
||||
let afterMoving: [Int] = [0, 2, 1]
|
||||
beforeEach {
|
||||
try? queue.moveItem(fromIndex: 1, toIndex: 3)
|
||||
}
|
||||
|
||||
it("should move the item") {
|
||||
expect(queue.items).to(equal(afterMoving))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Clear
|
||||
|
||||
context("when queue is cleared") {
|
||||
beforeEach {
|
||||
queue.clearQueue()
|
||||
}
|
||||
|
||||
it("should have currentIndex -1") {
|
||||
expect(queue.currentIndex).to(equal(-1))
|
||||
}
|
||||
|
||||
it("should have no items") {
|
||||
expect(queue.items.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+9
-3
@@ -3,7 +3,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftAudioEx",
|
||||
platforms: [.iOS(.v11)],
|
||||
platforms: [.iOS(.v11), .macOS(.v11)],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftAudioEx",
|
||||
@@ -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,12 +1,26 @@
|
||||

|
||||
|
||||
# SwiftAudio
|
||||
# SwiftAudioEx
|
||||
|
||||
[](https://codecov.io/gh/DoubleSymmetry/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](https://codecov.io/gh/doublesymmetry/SwiftAudioEx)
|
||||
[](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
|
||||
|
||||
@@ -17,48 +31,56 @@ XCode project navigator and Build/Run it in a simulator (or on an actual
|
||||
device).
|
||||
|
||||
## Requirements
|
||||
|
||||
iOS 11.0+
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
[Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode.
|
||||
|
||||
SwiftAudio supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudio repo's URL](https://github.com/DoubleSymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudio` to search.
|
||||
SwiftAudioEx supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudioEx repo's URL](https://github.com/doublesymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudioEx` to search.
|
||||
|
||||
After select the package, you can choose the dependency type (tagged version, branch or commit). Then Xcode will setup all the stuff for you.
|
||||
|
||||
If you're a framework author and use SwiftAudio as a dependency, update your `Package.swift` file:
|
||||
If you're a framework author and use SwiftAudioEx as a dependency, update your `Package.swift` file:
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
// 0.12.0 ..< 1.0.0
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/DoubleSymmetry/SwiftAudio.git", from: "0.12.0")
|
||||
.package(url: "https://github.com/doublesymmetry/SwiftAudio.git", from: "1.0.0")
|
||||
],
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### CocoaPods
|
||||
SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
|
||||
SwiftAudioEx is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftAudioEx', '~> 0.15.3'
|
||||
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)
|
||||
@@ -67,6 +89,7 @@ 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 {
|
||||
|
||||
@@ -84,7 +107,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)
|
||||
@@ -94,7 +119,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.
|
||||
@@ -102,13 +129,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.
|
||||
@@ -118,10 +148,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)
|
||||
//...
|
||||
@@ -134,34 +167,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,
|
||||
@@ -170,25 +212,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.
|
||||
|
||||
+3
-1
@@ -245,7 +245,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
if (pendingAsset != self.asset) { return; }
|
||||
|
||||
let commonData = pendingAsset.commonMetadata
|
||||
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
|
||||
if (!commonData.isEmpty) {
|
||||
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
|
||||
}
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
@@ -7,7 +7,14 @@
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
public typealias AudioItemImage = UIImage
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
public typealias AudioItemImage = NSImage
|
||||
#endif
|
||||
|
||||
public enum SourceType {
|
||||
case stream
|
||||
@@ -15,19 +22,17 @@ public enum SourceType {
|
||||
}
|
||||
|
||||
public protocol AudioItem {
|
||||
|
||||
func getSourceUrl() -> String
|
||||
func getArtist() -> String?
|
||||
func getTitle() -> String?
|
||||
func getAlbumTitle() -> String?
|
||||
func getSourceType() -> SourceType
|
||||
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
|
||||
func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
|
||||
public protocol TimePitching {
|
||||
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
|
||||
}
|
||||
@@ -42,8 +47,8 @@ public protocol AssetOptionsProviding {
|
||||
func getAssetOptions() -> [String: Any]
|
||||
}
|
||||
|
||||
public class DefaultAudioItem: AudioItem {
|
||||
|
||||
public class DefaultAudioItem: AudioItem, Identifiable {
|
||||
|
||||
public var audioUrl: String
|
||||
|
||||
public var artist: String?
|
||||
@@ -54,9 +59,9 @@ public class DefaultAudioItem: AudioItem {
|
||||
|
||||
public var sourceType: SourceType
|
||||
|
||||
public var artwork: UIImage?
|
||||
public var artwork: AudioItemImage?
|
||||
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: AudioItemImage? = nil) {
|
||||
self.audioUrl = audioUrl
|
||||
self.artist = artist
|
||||
self.title = title
|
||||
@@ -85,7 +90,7 @@ public class DefaultAudioItem: AudioItem {
|
||||
sourceType
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
public func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) {
|
||||
handler(artwork)
|
||||
}
|
||||
|
||||
@@ -96,12 +101,12 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
|
||||
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) {
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
|
||||
pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
@@ -116,12 +121,12 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
|
||||
|
||||
public var initialTime: TimeInterval
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
|
||||
initialTime = 0.0
|
||||
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?, initialTime: TimeInterval) {
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, initialTime: TimeInterval) {
|
||||
self.initialTime = initialTime
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
@@ -137,12 +142,12 @@ 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?) {
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
|
||||
options = [:]
|
||||
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?, options: [String: Any]) {
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, options: [String: Any]) {
|
||||
self.options = options
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
@@ -42,6 +42,33 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handles the `playWhenReady` setting while executing a given action.
|
||||
|
||||
This method takes an optional `Bool` value and a closure representing an action to execute.
|
||||
If the `Bool` value is not `nil`, `self.playWhenReady` is set accordingly either before or
|
||||
after executing the action.
|
||||
|
||||
- Parameters:
|
||||
- playWhenReady: Optional `Bool` to set `self.playWhenReady`.
|
||||
- If `true`, `self.playWhenReady` will be set after executing the action.
|
||||
- If `false`, `self.playWhenReady` will be set before executing the action.
|
||||
- If `nil`, `self.playWhenReady` will not be changed.
|
||||
- action: A closure representing the action to execute. This closure can throw an error.
|
||||
|
||||
- Throws: This function will propagate any errors thrown by the `action` closure.
|
||||
*/
|
||||
internal func handlePlayWhenReady(_ playWhenReady: Bool?, action: () throws -> Void) rethrows {
|
||||
if playWhenReady == false {
|
||||
self.playWhenReady = false
|
||||
}
|
||||
|
||||
try action()
|
||||
|
||||
if playWhenReady == true, playbackError == nil {
|
||||
self.playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
@@ -137,7 +164,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
public var rate: Float {
|
||||
get { wrapper.rate }
|
||||
set { wrapper.rate = newValue }
|
||||
set {
|
||||
wrapper.rate = newValue
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
updateNowPlayingPlaybackValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
@@ -165,32 +197,30 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
currentItem = item
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
currentItem = item
|
||||
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
// Reset playback values without updating, because that will happen in
|
||||
// the loadNowPlayingMetaValues call straight after:
|
||||
nowPlayingInfoController.setWithoutUpdate(keyValues: [
|
||||
MediaItemProperty.duration(nil),
|
||||
NowPlayingInfoProperty.playbackRate(nil),
|
||||
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
|
||||
])
|
||||
loadNowPlayingMetaValues()
|
||||
}
|
||||
|
||||
enableRemoteCommands(forItem: item)
|
||||
|
||||
wrapper.load(
|
||||
from: item.getSourceUrl(),
|
||||
type: item.getSourceType(),
|
||||
playWhenReady: self.playWhenReady,
|
||||
initialTime: (item as? InitialTiming)?.getInitialTime(),
|
||||
options:(item as? AssetOptionsProviding)?.getAssetOptions()
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
+2
-1
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
#if os(iOS)
|
||||
protocol AudioSession {
|
||||
|
||||
var isOtherAudioPlaying: Bool { get }
|
||||
@@ -30,3 +30,4 @@ protocol AudioSession {
|
||||
}
|
||||
|
||||
extension AVAudioSession: AudioSession {}
|
||||
#endif
|
||||
+4
@@ -13,6 +13,8 @@ public enum InterruptionType: Equatable {
|
||||
case ended(shouldResume: Bool)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
public protocol AudioSessionControllerDelegate: AnyObject {
|
||||
func handleInterruption(type: InterruptionType)
|
||||
}
|
||||
@@ -129,3 +131,5 @@ public class AudioSessionController {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
+30
-13
@@ -14,7 +14,7 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
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.
|
||||
*/
|
||||
@@ -32,8 +32,7 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
|
||||
class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
private let metadataOutput: AVPlayerItemMetadataOutput
|
||||
private var currentMetadataOutput: AVPlayerItemMetadataOutput?
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
@@ -47,10 +46,7 @@ class AVPlayerItemObserver: NSObject {
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
override init() {
|
||||
metadataOutput = AVPlayerItemMetadataOutput()
|
||||
super.init()
|
||||
|
||||
metadataOutput.setDelegate(self, queue: main)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -64,24 +60,35 @@ class AVPlayerItemObserver: NSObject {
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
stopObservingCurrentItem()
|
||||
isObserving = true
|
||||
observingItem = item
|
||||
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
|
||||
// Create and add a new metadata output to the item.
|
||||
let metadataOutput = AVPlayerItemMetadataOutput()
|
||||
metadataOutput.setDelegate(self, queue: .main)
|
||||
item.add(metadataOutput)
|
||||
self.currentMetadataOutput = metadataOutput
|
||||
}
|
||||
|
||||
func stopObservingCurrentItem() {
|
||||
guard let observingItem = observingItem, isObserving else {
|
||||
return
|
||||
}
|
||||
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
|
||||
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
|
||||
observingItem.remove(metadataOutput)
|
||||
|
||||
// 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?) {
|
||||
@@ -95,17 +102,17 @@ class AVPlayerItemObserver: NSObject {
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
|
||||
|
||||
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
|
||||
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
|
||||
}
|
||||
|
||||
|
||||
default: break
|
||||
|
||||
}
|
||||
@@ -114,6 +121,16 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
|
||||
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
if output == currentMetadataOutput {
|
||||
delegate?.item(didReceiveTimedMetadata: groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AVPlayerItem {
|
||||
func removeAllMetadataOutputs() {
|
||||
for output in self.outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
|
||||
self.remove(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ protocol QueueManagerDelegate: AnyObject {
|
||||
func onSkippedToSameCurrentItem()
|
||||
}
|
||||
|
||||
class QueueManager<T> {
|
||||
class QueueManager<Element> {
|
||||
|
||||
fileprivate let recursiveLock = NSRecursiveLock()
|
||||
|
||||
@@ -54,7 +54,7 @@ class QueueManager<T> {
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
private(set) var items: [T] = [] {
|
||||
private(set) var items: [Element] = [] {
|
||||
didSet {
|
||||
return synchronize {
|
||||
if oldValue.count == 0 && items.count > 0 {
|
||||
@@ -64,7 +64,7 @@ class QueueManager<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public var nextItems: [T] {
|
||||
public var nextItems: [Element] {
|
||||
return synchronize {
|
||||
return currentIndex == -1 || currentIndex == items.count - 1
|
||||
? []
|
||||
@@ -72,7 +72,7 @@ class QueueManager<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public var previousItems: [T] {
|
||||
public var previousItems: [Element] {
|
||||
return synchronize {
|
||||
return currentIndex <= 0
|
||||
? []
|
||||
@@ -83,7 +83,7 @@ class QueueManager<T> {
|
||||
/**
|
||||
The current item for the queue.
|
||||
*/
|
||||
public var current: T? {
|
||||
public var current: Element? {
|
||||
return synchronize {
|
||||
return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class QueueManager<T> {
|
||||
|
||||
- parameter item: The `AudioItem` to be added.
|
||||
*/
|
||||
public func add(_ item: T) {
|
||||
public func add(_ item: Element) {
|
||||
synchronize {
|
||||
items.append(item)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class QueueManager<T> {
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
*/
|
||||
public func add(_ items: [T]) {
|
||||
public func add(_ items: [Element]) {
|
||||
synchronize {
|
||||
if (items.count == 0) { return }
|
||||
self.items.append(contentsOf: items)
|
||||
@@ -138,14 +138,14 @@ class QueueManager<T> {
|
||||
- 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 {
|
||||
public func add(_ items: [Element], 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) {
|
||||
if (self.items.count > 0 && currentIndex >= index) {
|
||||
currentIndex += items.count
|
||||
}
|
||||
self.items.insert(contentsOf: items, at: index)
|
||||
@@ -157,7 +157,7 @@ class QueueManager<T> {
|
||||
case previous = -1
|
||||
}
|
||||
|
||||
private func skip(direction: SkipDirection, wrap: Bool) -> T? {
|
||||
private func skip(direction: SkipDirection, wrap: Bool) -> Element? {
|
||||
let count = items.count
|
||||
if (current == nil || count == 0) {
|
||||
return nil
|
||||
@@ -174,9 +174,7 @@ class QueueManager<T> {
|
||||
let oldIndex = currentIndex
|
||||
currentIndex = max(0, min(items.count - 1, index))
|
||||
if (oldIndex != currentIndex) {
|
||||
defer {
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
delegate?.onCurrentItemChanged()
|
||||
}
|
||||
}
|
||||
return current
|
||||
@@ -188,7 +186,7 @@ class QueueManager<T> {
|
||||
- returns: The next (or current) item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func next(wrap: Bool = false) -> T? {
|
||||
public func next(wrap: Bool = false) -> Element? {
|
||||
synchronize {
|
||||
return skip(direction: SkipDirection.next, wrap: wrap);
|
||||
}
|
||||
@@ -201,7 +199,7 @@ class QueueManager<T> {
|
||||
- returns: The previous item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func previous(wrap: Bool = false) -> T? {
|
||||
public func previous(wrap: Bool = false) -> Element? {
|
||||
return synchronize {
|
||||
return skip(direction: SkipDirection.previous, wrap: wrap);
|
||||
}
|
||||
@@ -216,7 +214,7 @@ class QueueManager<T> {
|
||||
- returns: The item at the index.
|
||||
*/
|
||||
@discardableResult
|
||||
public func jump(to index: Int) throws -> T {
|
||||
public func jump(to index: Int) throws -> Element {
|
||||
var skippedToSameCurrentItem = false
|
||||
var currentItemChanged = false
|
||||
let result = try synchronizeThrows {
|
||||
@@ -268,7 +266,8 @@ class QueueManager<T> {
|
||||
- throws: AudioPlayerError.QueueError
|
||||
- returns: The removed item.
|
||||
*/
|
||||
public func removeItem(at index: Int) throws -> T {
|
||||
@discardableResult
|
||||
public func removeItem(at index: Int) throws -> Element {
|
||||
var currentItemChanged = false
|
||||
let result = try synchronizeThrows {
|
||||
try throwIfQueueEmpty()
|
||||
@@ -294,7 +293,7 @@ class QueueManager<T> {
|
||||
|
||||
- parameter item: The item to set as the new current item.
|
||||
*/
|
||||
public func replaceCurrentItem(with item: T) {
|
||||
public func replaceCurrentItem(with item: Element) {
|
||||
var currentItemChanged = false
|
||||
synchronize {
|
||||
if currentIndex == -1 {
|
||||
+15
-17
@@ -68,10 +68,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public override func load(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.replaceCurrentItem(with: item)
|
||||
}
|
||||
queue.replaceCurrentItem(with: item)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,10 +80,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(item: AudioItem, playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.add(item)
|
||||
}
|
||||
queue.add(item)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,10 +92,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
|
||||
*/
|
||||
public func add(items: [AudioItem], playWhenReady: Bool? = nil) {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
handlePlayWhenReady(playWhenReady) {
|
||||
queue.add(items)
|
||||
}
|
||||
queue.add(items)
|
||||
}
|
||||
|
||||
public func add(items: [AudioItem], at index: Int) throws {
|
||||
@@ -147,15 +144,14 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
- throws: `AudioPlayerError`
|
||||
*/
|
||||
public func jumpToItem(atIndex index: Int, playWhenReady: Bool? = nil) throws {
|
||||
if let playWhenReady = playWhenReady {
|
||||
self.playWhenReady = playWhenReady
|
||||
try handlePlayWhenReady(playWhenReady) {
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
} else {
|
||||
_ = try queue.jump(to: index)
|
||||
}
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
}
|
||||
if (index == currentIndex) {
|
||||
seek(to: 0)
|
||||
} else {
|
||||
_ = try queue.jump(to: index)
|
||||
}
|
||||
event.playbackEnd.emit(data: .jumpedToIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,6 +189,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
|
||||
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) {
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '1.0.0-rc.9'
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+97
-100
@@ -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 {
|
||||
@@ -43,10 +41,10 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__state__when_playing_a_source__should_be_playing() {
|
||||
|
||||
func testAVPlayerWrapperStateWhenPlayingSourceShouldBePlaying() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
if state == .playing {
|
||||
@@ -54,67 +52,78 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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 .stopped: 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)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
@@ -122,18 +131,18 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
@@ -143,10 +152,10 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
|
||||
func testAVPlayerWrapperSeekingShouldSeekWhileNotYetLoaded() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
@@ -154,10 +163,10 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__seek_by__should_seek() {
|
||||
func testAVPlayerWrapperSeekByShouldSeek() {
|
||||
let seekTime: TimeInterval = 5.0
|
||||
let expectation = XCTestExpectation()
|
||||
holder.stateUpdate = { state in
|
||||
@@ -167,25 +176,25 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
|
||||
|
||||
func testAVPlayerWrapperLoadingSourceWithInitialTimeShouldSeek() {
|
||||
let expectation = XCTestExpectation()
|
||||
holder.didSeekTo = { seconds in
|
||||
expectation.fulfill()
|
||||
}
|
||||
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Rate tests
|
||||
|
||||
func test_AVPlayerWrapper__rate__should_be_1() {
|
||||
XCTAssert(wrapper.rate == 1)
|
||||
|
||||
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 {
|
||||
@@ -193,16 +202,15 @@ 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())
|
||||
wait(for: [expectation], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -212,37 +220,29 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
)
|
||||
|
||||
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? {
|
||||
get {
|
||||
@@ -269,28 +269,25 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
var didUpdateDuration: ((_ duration: Double) -> 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: Double, didFinish: Bool) {
|
||||
didSeekTo?(seconds)
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
func testEventRemoveListener() {
|
||||
var listener: EventListener! = EventListener()
|
||||
event.addListener(listener, listener.handleEvent)
|
||||
listener = nil
|
||||
event.emit(data: ())
|
||||
|
||||
waitEqual(self.event.invokers.count, 0, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
}
|
||||
}
|
||||
@@ -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: defaultTimeout)
|
||||
|
||||
XCTAssertTrue(seekCompleted)
|
||||
XCTAssertTrue(audioPlayer.currentTime >= 4)
|
||||
}
|
||||
|
||||
// MARK: - Duration
|
||||
|
||||
func testSetDurationAfterLoading() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout) // 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: defaultTimeout)
|
||||
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testResetDurationAfterReset() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertEqual(audioPlayer.duration, 0)
|
||||
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: defaultTimeout)
|
||||
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: defaultTimeout) // 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: defaultTimeout)
|
||||
|
||||
audioPlayer.play()
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
|
||||
audioPlayer.playWhenReady = true
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
|
||||
audioPlayer.reload(startFromCurrentTime: true)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .failed, timeout: defaultTimeout)
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .failed], timeout: defaultTimeout)
|
||||
|
||||
self.audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitTrue(self.audioPlayer.playbackError == nil, timeout: defaultTimeout)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .idle, .loading, .playing], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .failed, timeout: defaultTimeout)
|
||||
|
||||
self.audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitTrue(self.audioPlayer.playbackError == nil, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterLoadSourceWithPlayWhenReady() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEvents() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.play()
|
||||
expectedEvents.append(.playing)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testUpdatePlayWhenReadyAfterExternalPause() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: defaultTimeout)
|
||||
|
||||
// Simulate AVPlayer becoming paused due to external reason:
|
||||
audioPlayer.wrapper.rate = 0
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
XCTAssertFalse(self.audioPlayer.playWhenReady)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEventsAtEndCallStop() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.pause()
|
||||
expectedEvents.append(.paused)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
expectedEvents.append(.playing)
|
||||
audioPlayer.play()
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.stop()
|
||||
expectedEvents.append(.stopped)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testReliableOrderOfEventsAfterLoadingAfterReset() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
var expectedEvents: [AVPlayerWrapperState] = [.loading, .playing]
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.clear()
|
||||
expectedEvents.append(.idle)
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem())
|
||||
expectedEvents.append(contentsOf: [.loading, .playing])
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterPlay() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
waitEqual(self.audioPlayer.playerState, .ready, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.play()
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPausedStateAfterPause() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.pause()
|
||||
waitEqual(self.audioPlayer.playerState, .paused, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPausedStateAfterSettingPlayWhenReadyToFalse() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.playWhenReady = false
|
||||
waitEqual(self.audioPlayer.playerState, .paused, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPlayingStateAfterSettingPlayWhenReadyToTrue() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
waitEqual(self.audioPlayer.playerState, .ready, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.playWhenReady = true
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testStoppedStateAfterStop() {
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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 == .buffering)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testSeekingAfterLoadingComplete() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testSeekingWhenPaused() {
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: 4.75)
|
||||
waitEqual(self.audioPlayer.currentTime, 4.75, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
|
||||
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: defaultTimeout)
|
||||
|
||||
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,96 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
+4
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
@@ -64,3 +66,5 @@ class FailingAudioSession: AudioSession {
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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,745 @@
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
func testLoadAfterRemoval() {
|
||||
testRemovingItemAfterAdding()
|
||||
|
||||
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
XCTAssertNotEqual(audioPlayer.currentItem?.getSourceUrl(), FiveSecondSource.getAudioItem().getSourceUrl())
|
||||
waitTrue(self.playerStateEventListener.statesWithoutBuffering.contains(.playing), timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
// Covers: https://github.com/doublesymmetry/SwiftAudioEx/pull/81
|
||||
func testAddingItemWhenOnlyOneTrackInQueue() throws {
|
||||
audioPlayer.add(item: FiveSecondSource.getAudioItem())
|
||||
audioPlayer.play()
|
||||
try audioPlayer.add(items: [ShortSource.getAudioItem()], at: 0)
|
||||
XCTAssertEqual(audioPlayer.items.count, 2)
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 1)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
XCTAssertEqual(audioPlayer.previousItems.count, 1)
|
||||
waitEqual(self.playbackEndEventListener.lastReason, .skippedToNext, timeout: defaultTimeout)
|
||||
|
||||
// Test stop
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
|
||||
|
||||
// Test stop again
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
|
||||
|
||||
// Test previous
|
||||
audioPlayer.previous()
|
||||
XCTAssertEqual(self.audioPlayer.playerState, .loading)
|
||||
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
|
||||
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Stop
|
||||
|
||||
func testStopOnEmptyQueue() {
|
||||
audioPlayer.stop()
|
||||
waitEqual(self.playerStateEventListener.states, [.stopped], timeout: defaultTimeout)
|
||||
|
||||
// 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: defaultTimeout)
|
||||
|
||||
// It should have mutated player state from .loading to .stopped
|
||||
waitEqual(self.playerStateEventListener.states, [.loading, .stopped], timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
}
|
||||
|
||||
func testLoadItemAfterPlaying() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.load(item: FiveSecondSource.getAudioItem())
|
||||
XCTAssertNotNil(audioPlayer.currentItem)
|
||||
|
||||
// It should have started playing
|
||||
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .playing], timeout: defaultTimeout)
|
||||
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: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenPausedWithoutPlaying() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.pause()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenPlaying() {
|
||||
audioPlayer.play()
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
// should go to previous item and play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
// should go to previous item and play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testPreviousWhenPaused() {
|
||||
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
|
||||
audioPlayer.next()
|
||||
audioPlayer.pause()
|
||||
audioPlayer.previous()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
// should go to previous item and not play
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime > 0, timeout: defaultTimeout)
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 1)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
|
||||
// Allow final track to end
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.index, 1, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeOff() {
|
||||
setupRepeatModeOffTests()
|
||||
audioPlayer.play()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
|
||||
// Calling next on the final track
|
||||
audioPlayer.next()
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeTrack() {
|
||||
setupRepeatModeTrackTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
|
||||
// Allow the final track to end
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitForSeek(audioPlayer, to: 4.6)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeQueue() {
|
||||
setupRepeatModeQueueTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextTwiceWhenRepeatModeQueue() {
|
||||
setupRepeatModeQueueTests()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 0)
|
||||
XCTAssertNil(currentItemEventListener.lastIndex)
|
||||
|
||||
audioPlayer.next()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 1)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
|
||||
|
||||
audioPlayer.next()
|
||||
XCTAssertEqual(audioPlayer.currentIndex, 0)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeOffOneItem() {
|
||||
setupRepeatModeOffOneItemTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
// 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: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitEqual(self.currentItemEventListener.lastIndex, nil, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeTrackOneItem() {
|
||||
setupRepeatModeTrackOneItemTests()
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.currentTime, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
// 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: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime > 4.5, timeout: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime < 1, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testNextWhenRepeatModeQueueOneItem() {
|
||||
setupRepeatModeQueueOneItemTests()
|
||||
waitForSeek(audioPlayer, to: 2)
|
||||
audioPlayer.next()
|
||||
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
waitTrue(self.audioPlayer.currentTime < 1.9, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
|
||||
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
extension XCTestCase {
|
||||
var defaultTimeout: TimeInterval {
|
||||
if ProcessInfo.processInfo.environment["CI"] != nil {
|
||||
return 20
|
||||
} else {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
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: defaultTimeout)
|
||||
waitEqual(seekEventListener.eventResult.1, true, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func waitEqual<T: Equatable>(_ expression1: @autoclosure @escaping () -> T, _ expression2: @autoclosure @escaping () -> T, timeout: TimeInterval) {
|
||||
let expectation = XCTestExpectation(description: "Value should eventually equal expected value")
|
||||
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if expression1() == expression2() {
|
||||
expectation.fulfill()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
RunLoop.current.add(timer, forMode: .default)
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if abs(expression1() - expression2()) < accuracy {
|
||||
expectation.fulfill()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
RunLoop.current.add(timer, forMode: .default)
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if expression1() == expression2() {
|
||||
expectation.fulfill()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
RunLoop.current.add(timer, forMode: .default)
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
|
||||
|
||||
func waitTrue(_ expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval) {
|
||||
let expectation = XCTestExpectation(description: "Expression should eventually be true")
|
||||
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if expression() {
|
||||
expectation.fulfill()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
RunLoop.current.add(timer, forMode: .default)
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,17 @@
|
||||
//
|
||||
// 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 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())
|
||||
return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: AudioItemImage())
|
||||
}
|
||||
}
|
||||
|
||||
struct ShortSource {
|
||||
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
|
||||
static let path: String = Bundle.module.path(forResource: "ShortTestSound", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
@@ -29,7 +20,7 @@ struct ShortSource {
|
||||
}
|
||||
|
||||
struct LongSource {
|
||||
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
|
||||
static let path: String = Bundle.module.path(forResource: "WAV-MP3", ofType: "wav")!
|
||||
static let url: URL = URL(fileURLWithPath: LongSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
@@ -38,7 +29,7 @@ struct LongSource {
|
||||
}
|
||||
|
||||
struct FiveSecondSource {
|
||||
static let path: String = Bundle.main.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let path: String = Bundle.module.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: FiveSecondSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
@@ -47,7 +38,7 @@ struct FiveSecondSource {
|
||||
}
|
||||
|
||||
struct FiveSecondSourceWithInitialTimeOfFourSeconds {
|
||||
static let path: String = Bundle.main.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let path: String = Bundle.module.path(forResource: "five_seconds", ofType: "m4a")!
|
||||
static let url: URL = URL(fileURLWithPath: FiveSecondSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
Reference in New Issue
Block a user