Compare commits

..

103 Commits

Author SHA1 Message Date
David Chavez 42767039eb feat(example): Universal App (#86) 2024-04-12 18:56:05 +02:00
dcvz 4d3c1db059 chore(ci): Remove ios matrix for now 2024-04-11 19:27:29 +02:00
dcvz b6510ca858 chore(ci): update workflow step versions 2024-04-11 19:26:16 +02:00
dcvz 4ed73418a3 chore(ci): Disable iOS for now [skip ci] 2024-04-11 19:24:46 +02:00
dcvz 5b1594da9b chore(codecov): Lower target limit for now [skip ci] 2024-04-11 19:19:43 +02:00
dcvz 0890671ec5 chore(codecov): Ignore Tests folder 2024-04-11 19:07:13 +02:00
dcvz 4bd0251fac chore(ci): update workflow 2024-04-11 18:47:40 +02:00
dcvz 1fe9b0da35 chore(codecov): Update the config 2024-04-11 18:47:06 +02:00
David Chavez 546dd29838 Update README.md 2024-04-11 18:40:40 +02:00
dcvz 790d7b655b chore(ci): increase timeout limit for CI 2024-04-11 18:31:39 +02:00
dcvz ba12380126 chore(ci): Explicitly set codecov token 2024-04-11 18:24:38 +02:00
David Chavez 4543c58f38 chore(tests): Fix tests (#84) 2024-04-11 18:15:04 +02:00
shantanu-daisy a574f94c6b fix queue manager (#81)
fix queue manager to handle adding only one item before the current item
2024-04-11 13:50:32 +02:00
David Chavez 1e64a9aa8b chore(ci): Add a macOS workflow (#83) 2024-04-11 13:50:11 +02:00
Brandon Sneed e969fd5550 Added macOS support & example (#79) 2024-04-11 13:25:51 +02:00
dcvz 7b506bebab Bump to v1.1.0 2024-03-25 14:30:43 +01:00
Jonathan Puckey ea82b81ed9 Improve handling of playWhenReady parameters (#69)
This fixes an issue where calling one of the player methods with an optional playWhenReady parameter with playWhenReady= true, it would first start loading the current track before the track-changing action was called and then it would be called again because the track changed.

Instead, when playWhenReady is false, playback is paused before changing the track. When playWhenReady is true, playback is started after changing the track – which causes only the new track to start loading.
2024-03-25 14:28:44 +01:00
Kirill Zyusko 03c988e8b1 fix: broken progress bar after repeat (#75) 2024-03-25 14:27:22 +01:00
Fonos-development 2424550401 Fix crash on attaching metadata output (#74)
* fix: delay attach metadata more to avoid duplicate attachment

* fix: ensuring each AVPlayerItem has its own metadataOutput

* refactor: safely check current metadata output before broadcasting and removal

---------

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

* Update LICENSE
2021-11-06 14:29:06 +01:00
David Chavez 481130dc58 Release 0.14.5 2021-10-25 14:08:31 +02:00
David Chavez 300b34afa3 Do not emit paused state when changing tracks 2021-10-25 14:08:01 +02:00
David Chavez da3af0e9db Release 0.14.4 2021-09-28 10:58:23 +02:00
David Chavez d9eb313c1b Deprecate syncRemoteCommandsWithCommandCenter 2021-09-28 10:57:36 +02:00
David Chavez cca7f68da4 Increase deployment target for Test Target 2021-09-28 10:12:22 +02:00
David Chavez 7ed74b80ec Release 0.14.2 2021-09-28 10:04:01 +02:00
David Chavez 2773e4bfec Trigger skip and jump events only when actually taking action 2021-09-28 09:57:24 +02:00
David Chavez 77dc8f4ff1 Fix flickering elapsed time on a lock screen after pause 2021-09-28 09:41:04 +02:00
David Chavez accdf2c00c Rename exposed SPM package name 2021-09-28 09:31:31 +02:00
David Chavez 542d3a5764 Remove syncRemoteCommandsWithCommandCenter
Removed in favor of a didSet on remoteCommands property
2021-09-28 09:28:14 +02:00
David Chavez 4131e54f3e Create FUNDING.yml 2021-09-28 09:19:46 +02:00
David Chavez 03c4a7310f Release 0.14.2 2021-09-25 00:17:58 +02:00
David Chavez 9d2d2594a1 Replace commands based on diff to avoid iOS 15 issues 2021-09-25 00:17:31 +02:00
David Chavez 4e790876cb Release 0.14.1 2021-09-23 10:51:50 +02:00
David Chavez b19d01bdfc Allow manual resyncing of command center commands 2021-09-23 10:51:21 +02:00
David Chavez 3c8ecb353c Release 0.14.0 2021-09-16 17:50:55 +02:00
David Chavez cafd513468 Raise minimum deployment target to iOS11
Due to breaking change in Swift 5.5 & Xcode 13
2021-09-16 17:50:43 +02:00
David Chavez 7b8a4f318d Add tests for repeat mode 2021-08-19 16:27:41 +02:00
David Chavez acab6473b2 Release 0.13.2 2021-08-12 11:40:50 +02:00
David Chavez 57b6fb08f3 Fix tests 2021-08-12 11:40:21 +02:00
109 changed files with 5564 additions and 4760 deletions
+14
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: DoubleSymmetry
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+21 -12
View File
@@ -1,20 +1,29 @@
name: validate
on: [push]
on:
push:
branches:
- main
pull_request:
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 }}
+171 -455
View File
@@ -3,226 +3,92 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
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 */; };
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA39266028E200C7A389 /* SwiftAudio */; };
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA3B26602C0E00C7A389 /* SwiftAudio */; };
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
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>"; };
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; 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 = (
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
607FACE21AFB9204008FA782 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3C26602C0E00C7A389 /* SwiftAudio 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 = (
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 = (
9B05AA38266028D600C7A389 /* SwiftAudio */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -230,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 = (
9B05AA39266028E200C7A389 /* SwiftAudio */,
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 */,
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
);
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;
DevelopmentTeam = HPNZWPB9JK;
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
};
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = HPNZWPB9JK;
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 */,
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */,
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
607FACE31AFB9204008FA782 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */,
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */,
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */,
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */,
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;
@@ -444,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;
@@ -462,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;
@@ -499,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;
@@ -517,170 +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;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
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;
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;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
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;
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 = HPNZWPB9JK;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
);
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
};
name = Debug;
};
607FACF41AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = HPNZWPB9JK;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
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 */ = {
9B8819772BC866E800E20DCE /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
package = 9B05AA292660273200C7A389 /* XCRemoteSwiftPackageReference "Quick" */;
productName = Quick;
};
9B05AA322660276400C7A389 /* Nimble */ = {
isa = XCSwiftPackageProductDependency;
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
9B05AA39266028E200C7A389 /* SwiftAudio */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
};
9B05AA3B26602C0E00C7A389 /* SwiftAudio */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
rootObject = 9B8819512BC8657A00E20DCE /* Project object */;
}
@@ -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>
-48
View File
@@ -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

@@ -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
}
}
@@ -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

+13 -9
View File
@@ -7,20 +7,21 @@
//
import Foundation
import SwiftAudio
import SwiftAudioEx
class AudioController {
static let shared = AudioController()
let player: QueuedAudioPlayer
let audioSessionController = AudioSessionController.shared
let sources: [AudioItem] = [
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Longing.mp3", artist: "David Chavez", title: "Longing", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Soul%20Searching.mp3", artist: "David Chavez", title: "Soul Searching (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "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: "cover")),
DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
]
init() {
@@ -35,8 +36,11 @@ class AudioController {
.previous,
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
try? player.add(items: sources, playWhenReady: false)
player.repeatMode = .queue
DispatchQueue.main.async {
self.player.add(items: self.sources)
}
}
}
@@ -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"
}
}
-48
View File
@@ -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>
+172
View File
@@ -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)
}
+120
View File
@@ -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)
}
}
-55
View File
@@ -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>
+65
View File
@@ -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 SwiftAudio
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>
+17
View File
@@ -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()
}
}
}
-163
View File
@@ -1,163 +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 SwiftAudio
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
private var lastLoadFailed: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
controller.player.event.fail.addListener(self, handlePlayerFailure)
updateMetaData()
handleAudioPlayerStateChange(data: controller.player.playerState)
}
@IBAction func togglePlay(_ sender: Any) {
if !controller.audioSessionController.audioSessionIsActive {
try? controller.audioSessionController.activateSession()
}
if lastLoadFailed, let item = controller.player.currentItem {
lastLoadFailed = false
errorLabel.isHidden = true
try? controller.player.load(item: item, playWhenReady: true)
}
else {
controller.player.togglePlaying()
}
}
@IBAction func previous(_ sender: Any) {
try? controller.player.previous()
}
@IBAction func next(_ sender: Any) {
try? 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()
}
func updateTimeValues() {
self.slider.maximumValue = Float(self.controller.player.duration)
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
}
func updateMetaData() {
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
}
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
}
func setErrorMessage(_ message: String) {
self.loadIndicator.stopAnimating()
errorLabel.isHidden = false
errorLabel.text = message
}
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print(data)
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
case .loading:
self.loadIndicator.startAnimating()
self.updateMetaData()
self.updateTimeValues()
case .buffering:
self.loadIndicator.startAnimating()
case .ready:
self.loadIndicator.stopAnimating()
self.updateMetaData()
self.updateTimeValues()
case .playing, .paused, .idle:
self.loadIndicator.stopAnimating()
self.updateTimeValues()
}
}
}
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
DispatchQueue.main.async {
self.updateTimeValues()
}
}
}
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
isScrubbing = false
}
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
DispatchQueue.main.async {
self.updateTimeValues()
}
}
func handleAVPlayerRecreated() {
try? controller.audioSessionController.set(category: .playback)
}
func handlePlayerFailure(data: AudioPlayer.FailEventData) {
if let error = data as NSError? {
if error.code == -1009 {
lastLoadFailed = true
DispatchQueue.main.async {
self.setErrorMessage("Network disconnected. Please try again...")
}
}
}
}
}
@@ -1,48 +0,0 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
class AVPlayerItemNotificationObserverTests: QuickSpec {
override func spec() {
describe("A notification observer") {
var item: AVPlayerItem!
var observer: AVPlayerItemNotificationObserver!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer = AVPlayerItemNotificationObserver()
}
context("when started observing") {
beforeEach {
observer.startObserving(item: item)
}
it("should have an observed item") {
expect(observer.observingItem).toNot(beNil())
}
context("when ended observing") {
beforeEach {
observer.stopObservingCurrentItem()
}
it("should have no observed item") {
expect(observer.observingItem).to(beNil())
}
}
}
}
}
}
@@ -1,62 +0,0 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
class AVPlayerItemObserverTests: QuickSpec {
override func spec() {
describe("An AVPlayerItemObserver") {
var observer: AVPlayerItemObserver!
beforeEach {
observer = AVPlayerItemObserver()
}
describe("observed item") {
context("when observing") {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer.startObserving(item: item)
}
it("should exist") {
expect(observer.observingItem).toEventuallyNot(beNil())
}
}
}
describe("observing status") {
it("should not be observing") {
expect(observer.isObserving).toEventuallyNot(beTrue())
}
context("when observing") {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer.startObserving(item: item)
}
it("should be observing") {
expect(observer.isObserving).toEventually(beTrue())
}
}
}
}
}
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var receivedMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
receivedMetadata?(metadata)
}
var updateDuration: ((_ duration: Double) -> Void)?
func item(didUpdateDuration duration: Double) {
updateDuration?(duration)
}
}
-86
View File
@@ -1,86 +0,0 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
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 SwiftAudio
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())
}
}
}
}
}
}
-233
View File
@@ -1,233 +0,0 @@
import AVFoundation
import XCTest
@testable import SwiftAudio
class AVPlayerWrapperTests: XCTestCase {
var wrapper: AVPlayerWrapper!
var holder: AVPlayerWrapperDelegateHolder!
override func setUp() {
super.setUp()
wrapper = AVPlayerWrapper()
wrapper.volume = 0.0
wrapper.automaticallyWaitsToMinimizeStalling = false
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 test_AVPlayerWrapper__state__when_loading_a_source__should_be_loading() {
wrapper.load(from: Source.url, playWhenReady: false)
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.loading)
}
func test_AVPlayerWrapper__state__when_loading_a_source__should_eventually_be_ready() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .ready {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_playing_a_source__should_be_playing() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .playing {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_pausing_a_source__should_be_paused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.pause()
case .paused: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_toggling_from_play__should_be_paused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.togglePlaying()
case .paused: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__when_stopping__should_be_stopped() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: self.wrapper.stop()
case .idle: expectation.fulfill()
default: break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__state__loading_with_intial_time__should_be_playing() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing: expectation.fulfill()
default: break
}
}
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Duration tests
func test_AVPlayerWrapper__duration__should_be_0() {
XCTAssert(wrapper.duration == 0.0)
}
func test_AVPlayerWrapper__duration__loading_a_source__should_not_be_0() {
let expectation = XCTestExpectation()
holder.stateUpdate = { _ in
if self.wrapper.duration > 0 {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current time tests
func test_AVPlayerWrapper__currentTime__should_be_0() {
XCTAssert(wrapper.currentTime == 0)
}
// MARK: - Seeking
func test_AVPlayerWrapper__seeking__should_seek() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
self.wrapper.seek(to: seekTime)
}
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
let expectation = XCTestExpectation()
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Rate tests
func test_AVPlayerWrapper__rate__should_be_0() {
XCTAssert(wrapper.rate == 0.0)
}
func test_AVPlayerWrapper__rate__playing_a_source__should_be_1() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if self.wrapper.rate == 1.0 {
expectation.fulfill()
}
}
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())
}
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
}
func AVWrapperDidRecreateAVPlayer() {
}
func AVWrapperItemDidPlayToEndTime() {
}
var state: AVPlayerWrapperState? {
didSet {
if let state = state {
self.stateUpdate?(state)
}
}
}
var stateUpdate: ((_ state: AVPlayerWrapperState) -> Void)?
var didUpdateDuration: ((_ duration: Double) -> Void)?
var didSeekTo: ((_ seconds: Int) -> Void)?
var itemDidComplete: (() -> Void)?
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
self.state = state
}
func AVWrapper(secondsElapsed seconds: Double) {
}
func AVWrapper(failedWithError error: Error?) {
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
didSeekTo?(seconds)
}
func AVWrapper(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
didUpdateDuration?(duration)
}
}
-81
View File
@@ -1,81 +0,0 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
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))
}
}
}
}
}
}
}
-211
View File
@@ -1,211 +0,0 @@
import Quick
import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
class AudioPlayerTests: XCTestCase {
var audioPlayer: AudioPlayer!
var listener: AudioPlayerEventListener!
override func setUp() {
super.setUp()
audioPlayer = AudioPlayer()
audioPlayer.volume = 0.0
audioPlayer.bufferDuration = 0.001
audioPlayer.automaticallyWaitsToMinimizeStalling = false
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
}
override func tearDown() {
audioPlayer = nil
listener = nil
super.tearDown()
}
func test_AudioPlayer__state__should_be_idle() {
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
}
func test_AudioPlayer__state__load_source__should_be_loading() {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
}
func test_AudioPlayer__state__load_source__should_be_ready() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
switch state {
case .ready: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
switch state {
case .playing: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__play_source__should_be_playing() {
let expectation = XCTestExpectation()
listener.stateUpdate = { state in
switch state {
case .ready: self.audioPlayer.play()
case .playing: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__pausing_source__should_be_paused() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing: audioPlayer?.pause()
case .paused: expectation.fulfill()
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
func test_AudioPlayer__state__stopping_source__should_be_idle() {
let expectation = XCTestExpectation()
var hasBeenPlaying: Bool = false
listener.stateUpdate = { [weak audioPlayer] state in
switch state {
case .playing:
hasBeenPlaying = true
audioPlayer?.stop()
case .idle:
if hasBeenPlaying {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current time
func test_AudioPlayer__currentTime__should_be_0() {
XCTAssert(audioPlayer.currentTime == 0.0)
}
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
// let expectation = XCTestExpectation()
// audioPlayer.timeEventFrequency = .everyQuarterSecond
// listener.secondsElapse = { _ in
// if self.audioPlayer.currentTime > 0.0 {
// expectation.fulfill()
// }
// }
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
// wait(for: [expectation], timeout: 20.0)
// }
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
XCTAssert(audioPlayer.rate == 0.0)
}
func test_AudioPlayer__rate__playing_source__should_be_1() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
switch state {
case .playing:
if audioPlayer.rate == 1.0 {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Current item
func test_AudioPlayer__currentItem__should_be_nil() {
XCTAssertNil(audioPlayer.currentItem)
}
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
switch state {
case .ready:
if audioPlayer.currentItem != nil {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
}
class AudioPlayerEventListener {
var state: AudioPlayerState? {
didSet {
if let state = state {
stateUpdate?(state)
}
}
}
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
var seekCompletion: (() -> Void)?
weak var audioPlayer: AudioPlayer?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
}
deinit {
audioPlayer?.event.stateChange.removeListener(self)
audioPlayer?.event.seek.removeListener(self)
audioPlayer?.event.secondElapse.removeListener(self)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
func handleSeek(data: AudioPlayer.SeekEventData) {
seekCompletion?()
}
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
self.secondsElapse?(data)
}
}
@@ -1,100 +0,0 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
class AudioSessionControllerTests: QuickSpec {
override func spec() {
describe("An AudioSessionController") {
let audioSessionController: AudioSessionController = AudioSessionController(audioSession: NonFailingAudioSession())
it("should be inactive") {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
}
context("when session is activated") {
beforeEach {
try? audioSessionController.activateSession()
}
it("should be active") {
expect(audioSessionController.audioSessionIsActive).to(beTrue())
}
context("when deactivating session") {
beforeEach {
try? audioSessionController.deactivateSession()
}
it("should be inactive") {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
}
}
}
describe("its isObservingForInterruptions") {
it("should be true") {
expect(audioSessionController.isObservingForInterruptions).to(beTrue())
}
context("when isObservingForInterruptions is set to false") {
beforeEach {
audioSessionController.isObservingForInterruptions = false
}
it("should be false") {
expect(audioSessionController.isObservingForInterruptions).to(beFalse())
}
}
}
describe("its delegate") {
context("when a interruption arrives") {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(0)
])
delegate = AudioSessionControllerDelegateImplementation()
audioSessionController.delegate = delegate
audioSessionController.handleInterruption(notification: notification)
}
it("should eventually be updated with the interruption type") {
expect(delegate.interruptionType).toEventuallyNot(beNil())
}
}
}
}
describe("An AudioSessionController with a failing AudioSession") {
var audioSessionController: AudioSessionController!
beforeEach {
audioSessionController = AudioSessionController(audioSession: FailingAudioSession())
}
context("when activated") {
beforeEach {
try? audioSessionController.activateSession()
}
it("should be inactive") {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
}
}
}
}
}
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
var interruptionType: AVAudioSession.InterruptionType? = nil
func handleInterruption(type: AVAudioSession.InterruptionType) {
self.interruptionType = type
}
}
-24
View File
@@ -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 SwiftAudio
class NowPlayingInfoControllerTests: QuickSpec {
override func spec() {
describe("An NowPlayingInfoController") {
var nowPlayingController: NowPlayingInfoController!
beforeEach {
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: NowPlayingInfoCenter_Mock())
}
describe("its info dictionary") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be empty") {
expect(nowPlayingController.info.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty") {
expect(nowPlayingController.info.count).to(equal(0))
}
}
}
}
describe("its info center") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be nil") {
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
}
it("should not be empty") {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty") {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
}
}
}
}
}
}
}
-73
View File
@@ -1,73 +0,0 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
class NowPlayingInfoTests: QuickSpec {
override func spec() {
describe("An AudioPlayer") {
var audioPlayer: AudioPlayer!
var nowPlayingController: NowPlayingInfoController_Mock!
beforeEach {
nowPlayingController = NowPlayingInfoController_Mock()
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
audioPlayer.automaticallyUpdateNowPlayingInfo = true
audioPlayer.volume = 0
}
describe("its NowPlayingInfoController") {
context("when loading an AudioItem") {
var item: AudioItem!
beforeEach {
item = Source.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventually be updated with meta data") {
expect(nowPlayingController.getTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getTitle()).toEventually(equal(item.getTitle()!))
expect(nowPlayingController.getArtist()).toEventuallyNot(beNil())
expect(nowPlayingController.getArtist()).toEventually(equal(item.getArtist()!))
expect(nowPlayingController.getAlbumTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getAlbumTitle()).toEventually(equal(item.getAlbumTitle()!))
expect(nowPlayingController.getArtwork()).toEventuallyNot(beNil())
}
}
context("when playing an AudioItem") {
var item: AudioItem!
beforeEach {
item = LongSource.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: true)
}
it("should eventually be updated with playback values") {
expect(nowPlayingController.getRate()).toEventuallyNot(beNil())
expect(nowPlayingController.getDuration()).toEventuallyNot(beNil())
expect(nowPlayingController.getCurrentTime()).toEventuallyNot(beNil())
}
}
}
}
}
}
-488
View File
@@ -1,488 +0,0 @@
import Quick
import Nimble
@testable import SwiftAudio
class QueueManagerTests: QuickSpec {
let dummyItem = 0
let dummyItems: [Int] = [0, 1, 2, 3, 4, 5, 6]
override func spec() {
describe("A QueueManager") {
var manager: QueueManager<Int>!
beforeEach {
manager = QueueManager()
}
describe("its current item") {
it("should be nil") {
expect(manager.current).to(beNil())
}
context("when one item is added") {
beforeEach {
manager.addItem(self.dummyItem)
}
it("should not be nil") {
expect(manager.current).toNot(beNil())
}
it("should be the added item") {
expect(manager.current).to(equal(self.dummyItem))
}
context("then replaced") {
beforeEach {
manager.replaceCurrentItem(with: 1)
}
it("should be the new item") {
expect(manager.current).to(equal(1))
}
}
}
context("when replaced") {
beforeEach {
manager.replaceCurrentItem(with: 1)
}
it("should not be nil") {
expect(manager.current).toNot(beNil())
}
}
context("when mulitple items are added") {
beforeEach {
manager.addItems(self.dummyItems)
}
it("should not be nil") {
expect(manager.current).toNot(beNil())
}
}
}
context("when adding one item") {
beforeEach {
manager.addItem(self.dummyItem)
}
it("should have an item in the queue") {
expect(manager.items).notTo(beEmpty())
}
context("then replacing the item") {
beforeEach {
manager.replaceCurrentItem(with: 1)
}
it("should have replaced the current item") {
expect(manager.current).to(equal(1))
}
}
context("then calling next") {
var nextItem: Int?
beforeEach {
nextItem = try? manager.next()
}
it("should not return") {
expect(nextItem).to(beNil())
}
}
context("then calling previous") {
var previousItem: Int?
beforeEach {
previousItem = try? manager.previous()
}
it("should not return") {
expect(previousItem).to(beNil())
}
}
}
context("when adding multiple items") {
beforeEach {
manager.addItems(self.dummyItems)
}
it("should have items in the queue") {
expect(manager.items.count).to(equal(self.dummyItems.count))
}
it("should have the first item as a current item") {
expect(manager.current).toNot(beNil())
expect(manager.current).to(equal(self.dummyItems.first))
}
it("should have next items") {
expect(manager.nextItems).toNot(beNil())
expect(manager.nextItems.count).to(equal(self.dummyItems.count - 1))
}
context("then calling next") {
var nextItem: Int?
beforeEach {
nextItem = try? manager.next()
}
it("should return the next item") {
expect(nextItem).toNot(beNil())
expect(nextItem).to(equal(self.dummyItems[1]))
}
it("should have next current item") {
expect(manager.current).to(equal(self.dummyItems[1]))
}
it("should have previous items") {
expect(manager.previousItems).toNot(beNil())
}
context("then calling previous") {
var previousItem: Int?
beforeEach {
previousItem = try? manager.previous()
}
it("should return the first item") {
expect(previousItem).toNot(beNil())
expect(previousItem).to(equal(self.dummyItems.first))
}
it("should have the previous current item") {
expect(manager.current).to(equal(self.dummyItems.first))
}
}
context("then removing previous items") {
beforeEach {
manager.removePreviousItems()
}
it("should have no previous items") {
expect(manager.previousItems.count).to(equal(0))
}
it("should have current index zero") {
expect(manager.currentIndex).to(equal(0))
}
}
}
context("adding more items") {
var initialItemCount: Int!
let newItems: [Int] = [10, 11, 12, 13]
beforeEach {
initialItemCount = manager.items.count
try? manager.addItems(newItems, at: manager.items.endIndex - 1)
}
it("should have more items") {
expect(manager.items.count).to(equal(initialItemCount + newItems.count))
}
}
context("adding more items at a smaller index than currentIndex") {
var initialCurrentIndex: Int!
let newItems: [Int] = [10, 11, 12, 13]
beforeEach {
initialCurrentIndex = manager.currentIndex
try? manager.addItems(newItems, at: initialCurrentIndex)
}
it("currentIndex should increase by number of new items") {
expect(manager.currentIndex).to(equal(initialCurrentIndex + newItems.count))
}
}
// MARK: - Removal
context("then removing a item with index less than currentIndex") {
beforeEach {
var removed: Int?
var initialCurrentIndex: Int!
beforeEach {
let _ = try? manager.jump(to: 3)
initialCurrentIndex = manager.currentIndex
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
}
it("should remove an item") {
expect(removed).toNot(beNil())
}
it("should decrement the currentIndex") {
expect(manager.currentIndex).to(equal(initialCurrentIndex - 1))
}
}
}
context("then removing the second item") {
var removed: Int?
beforeEach {
removed = try? manager.removeItem(at: 1)
}
it("should have one less item") {
expect(removed).toNot(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
}
}
context("then removing the last item") {
var removed: Int?
beforeEach {
removed = try? manager.removeItem(at: self.dummyItems.count - 1)
}
it("should have one less item") {
expect(removed).toNot(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
}
}
context("then removing the current item") {
var removed: Int?
beforeEach {
removed = try? manager.removeItem(at: manager.currentIndex)
}
it("should not remove any items") {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
}
}
context("then removing with too large index") {
var removed: Int?
beforeEach {
removed = try? manager.removeItem(at: self.dummyItems.count)
}
it("should not remove any items") {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
}
}
context("then removing with too small index") {
var removed: Int?
beforeEach {
removed = try? manager.removeItem(at: -1)
}
it("should not remove any items") {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
}
}
context("then removing upcoming items") {
beforeEach {
manager.removeUpcomingItems()
}
it("should have no next items") {
expect(manager.nextItems.count).to(equal(0))
}
}
// MARK: - Jumping
context("then jumping to the current item") {
var error: Error?
var item: Int?
beforeEach {
do {
item = try manager.jump(to: manager.currentIndex)
}
catch let err {
error = err
}
}
it("should not return an item") {
expect(item).to(beNil())
}
it("should throw an error") {
expect(error).toNot(beNil())
}
}
context("then jumping to the second item") {
var jumped: Int?
beforeEach {
try? jumped = manager.jump(to: 1)
}
it("should return the current item") {
expect(jumped).toNot(beNil())
expect(jumped).to(equal(manager.current))
}
it("should move the current index") {
expect(manager.currentIndex).to(equal(1))
}
}
context("then jumping to last item") {
var jumped: Int?
beforeEach {
try? jumped = manager.jump(to: manager.items.count - 1)
}
it("should return the current item") {
expect(jumped).toNot(beNil())
expect(jumped).to(equal(manager.current))
}
it("should move the current index") {
expect(manager.currentIndex).to(equal(manager.items.count - 1))
}
}
context("then jumping to a negative index") {
var jumped: Int?
beforeEach {
jumped = try? manager.jump(to: -1)
}
it("should not return") {
expect(jumped).to(beNil())
}
it("should not move the current index") {
expect(manager.currentIndex).to(equal(0))
}
}
context("then jumping with too large index") {
var jumped: Int?
beforeEach {
jumped = try? manager.jump(to: manager.items.count)
}
it("should not return") {
expect(jumped).to(beNil())
}
it("should not move the current index") {
expect(manager.currentIndex).to(equal(0))
}
}
// MARK: - Moving
context("moving from current index") {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("throw an error") {
expect(error).toNot(beNil())
}
}
context("moving from a negative index") {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: -1, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("should throw an error") {
expect(error).toNot(beNil())
}
}
context("moving from a too large index") {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.items.count, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("should throw an error") {
expect(error).toNot(beNil())
}
}
context("moving to a negative index") {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: -1)
}
catch let err { error = err }
}
it("should throw an error") {
expect(error).toNot(beNil())
}
}
context("moving to a too large index") {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: manager.items.count)
}
catch let err { error = err }
}
it("should throw an error") {
expect(error).toNot(beNil())
}
}
context("then moving 2nd to 4th") {
let afterMoving: [Int] = [0, 2, 3, 1, 4, 5, 6]
beforeEach {
try? manager.moveItem(fromIndex: 1, toIndex: 3)
}
it("should move the item") {
expect(manager.items).to(equal(afterMoving))
}
}
// MARK: - Clear
context("when queue is cleared") {
beforeEach {
manager.clearQueue()
}
it("should have currentIndex 0") {
expect(manager.currentIndex).to(equal(0))
}
it("should have no items") {
expect(manager.items.count).to(equal(0))
}
}
}
}
}
}
-171
View File
@@ -1,171 +0,0 @@
import Quick
import Nimble
@testable import SwiftAudio
class QueuedAudioPlayerTests: QuickSpec {
override func spec() {
describe("A QueuedAudioPlayer") {
var audioPlayer: QueuedAudioPlayer!
beforeEach {
audioPlayer = QueuedAudioPlayer()
audioPlayer.bufferDuration = 0.0001
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.volume = 0.0
}
describe("its current item") {
it("should be nil") {
expect(audioPlayer.currentItem).to(beNil())
}
context("when adding one item") {
var item: AudioItem!
beforeEach {
item = ShortSource.getAudioItem()
try? audioPlayer.add(item: item, playWhenReady: false)
}
it("should not be nil") {
expect(audioPlayer.currentItem).toNot(beNil())
}
context("then loading a new item") {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should have replaced the item") {
expect(audioPlayer.currentItem?.getSourceUrl()).toNot(equal(item.getSourceUrl()))
}
}
}
context("when adding multiple items") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
}
it("should not be nil") {
expect(audioPlayer.currentItem).toNot(beNil())
}
}
}
describe("its next items") {
it("should be empty") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
context("when adding 2 items") {
beforeEach {
try? audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
}
it("should contain 1 item") {
expect(audioPlayer.nextItems.count).to(equal(1))
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should contain 0 items") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
context("then calling previous()") {
beforeEach {
try? audioPlayer.previous()
}
it("should contain 1 item") {
expect(audioPlayer.nextItems.count).to(equal(1))
}
}
}
context("then removing one item") {
beforeEach {
try? audioPlayer.removeItem(at: 1)
}
it("should be empty") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
}
context("then jumping to the last item") {
beforeEach {
try? audioPlayer.jumpToItem(atIndex: 1)
}
it("should be empty") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
}
context("then removing upcoming items") {
beforeEach {
audioPlayer.removeUpcomingItems()
}
it("should be empty") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
}
context("then stopping") {
beforeEach {
audioPlayer.stop()
}
it("should be empty") {
expect(audioPlayer.nextItems.count).to(equal(0))
}
}
}
}
describe("its previous items") {
it("should be empty") {
expect(audioPlayer.previousItems.count).to(equal(0))
}
context("when adding 2 items") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
}
it("should be empty") {
expect(audioPlayer.previousItems.count).to(equal(0))
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should contain one item") {
expect(audioPlayer.previousItems.count).to(equal(1))
}
}
context("then removing all previous items") {
beforeEach {
audioPlayer.removePreviousItems()
}
it("should be empty") {
expect(audioPlayer.previousItems.count).to(equal(0))
}
}
context("then stopping") {
beforeEach {
audioPlayer.stop()
}
it("should be empty") {
expect(audioPlayer.previousItems.count).to(equal(0))
}
}
}
}
}
}
}
-38
View File
@@ -1,38 +0,0 @@
//
// Sources.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 05/08/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
import SwiftAudio
import UIKit
struct Source {
static let path: String = Bundle.main.path(forResource: "TestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: Source.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: Source.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
}
}
struct ShortSource {
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: ShortSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
}
}
struct LongSource {
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
static let url: URL = URL(fileURLWithPath: LongSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
}
}
+42
View File
@@ -0,0 +1,42 @@
MIT License
Copyright (c) 2021 Double Symmetry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+14 -8
View File
@@ -2,18 +2,24 @@
import PackageDescription
let package = Package(
name: "SwiftAudio",
platforms: [.iOS(.v10)],
name: "SwiftAudioEx",
platforms: [.iOS(.v11), .macOS(.v11)],
products: [
.library(
name: "SwiftAudio",
targets: ["SwiftAudio"]),
name: "SwiftAudioEx",
targets: ["SwiftAudioEx"]),
],
dependencies: [],
targets: [
.target(
name: "SwiftAudio",
dependencies: [],
path: "SwiftAudio/Classes")
name: "SwiftAudioEx",
dependencies: []),
.testTarget(
name: "SwiftAudioExTests",
dependencies: ["SwiftAudioEx"],
resources: [
.process("Resources")
]
),
]
)
)
+68 -18
View File
@@ -1,61 +1,86 @@
![logo](Images/original-horizontal.png)
# SwiftAudio
# SwiftAudioEx
[![codecov](https://codecov.io/gh/DoubleSymmetry/SwiftAudio/branch/master/graph/badge.svg?token=FD5THGSHM5)](https://codecov.io/gh/DoubleSymmetry/SwiftAudio)
[![License](https://img.shields.io/cocoapods/l/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![codecov](https://codecov.io/gh/doublesymmetry/SwiftAudioEx/graph/badge.svg?token=FD5THGSHM5)](https://codecov.io/gh/doublesymmetry/SwiftAudioEx)
[![License](https://img.shields.io/cocoapods/l/SwiftAudioEx.svg?style=flat)](http://cocoapods.org/pods/SwiftAudioEx)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioEx.svg?style=flat)](http://cocoapods.org/pods/SwiftAudioEx)
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
SwiftAudioEx is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
<div align="left" valign="middle">
<a href="https://runblaze.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.runblaze.dev/logo_dark.png">
<img align="right" src="https://www.runblaze.dev/logo_light.png" height="102px"/>
</picture>
</a>
<br style="display: none;"/>
_[Blaze](https://runblaze.dev) sponsors SwiftAudioEx by providing super fast Apple Silicon based macOS Github Action Runners. Use the discount code `RNTP50` at checkout to get 50% off your first year._
</div>
## Example
To see the audio player in action, run the example project!
To run the example project, clone the repo, and run `pod install` from the Example directory first.
To run the example project, clone the repo, then open
`Example/SwiftAudio.xcodeproj` in Xcode. Choose "Example for SwiftAudio" in the
XCode project navigator and Build/Run it in a simulator (or on an actual
device).
## Requirements
iOS 10.0+
iOS 11.0+
## Installation
### Swift Package Manager
[Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode.
SwiftAudio supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudio repo's URL](https://github.com/DoubleSymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudio` to search.
SwiftAudioEx supports SwiftPM from version 0.12.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SwiftAudioEx repo's URL](https://github.com/doublesymmetry/SwiftAudio.git). Or you can login Xcode with your GitHub account and just type `SwiftAudioEx` to search.
After select the package, you can choose the dependency type (tagged version, branch or commit). Then Xcode will setup all the stuff for you.
If you're a framework author and use SwiftAudio as a dependency, update your `Package.swift` file:
If you're a framework author and use SwiftAudioEx as a dependency, update your `Package.swift` file:
```swift
let package = Package(
// 0.12.0 ..< 1.0.0
dependencies: [
.package(url: "https://github.com/DoubleSymmetry/SwiftAudio.git", from: "0.12.0")
.package(url: "https://github.com/doublesymmetry/SwiftAudio.git", from: "1.0.0")
],
// ...
)
```
### CocoaPods
SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
SwiftAudioEx is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.11.2'
pod 'SwiftAudioEx', '~> 1.0.0'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
SwiftAudioEx supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.11.2
github "doublesymmetry/SwiftAudioEx" ~> 1.0.0
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
## Usage
### AudioPlayer
To get started playing some audio:
```swift
let player = AudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
@@ -64,16 +89,17 @@ player.load(item: audioItem, playWhenReady: true) // Load the item and start pla
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
To subscribe to an event:
```swift
class MyCustomViewController: UIViewController {
let audioPlayer = AudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
}
func handleAudioPlayerStateChange(state: AudioPlayerState) {
// Handle the event
}
@@ -81,7 +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)
@@ -91,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.
@@ -99,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.
@@ -115,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)
//...
@@ -131,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,
@@ -167,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.
+519
View File
@@ -0,0 +1,519 @@
//
// AVPlayerWrapper.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 06/03/2018.
// Copyright © 2018 Jørgen Henrichsen. All rights reserved.
//
import Foundation
import AVFoundation
import MediaPlayer
public enum PlaybackEndedReason: String {
case playedUntilEnd
case playerStopped
case skippedToNext
case skippedToPrevious
case jumpedToIndex
case cleared
case failed
}
class AVPlayerWrapper: AVPlayerWrapperProtocol {
// MARK: - Properties
fileprivate var avPlayer = AVPlayer()
private let playerObserver = AVPlayerObserver()
internal let playerTimeObserver: AVPlayerTimeObserver
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
private let playerItemObserver = AVPlayerItemObserver()
fileprivate var timeToSeekToAfterLoading: TimeInterval?
fileprivate var asset: AVAsset? = nil
fileprivate var item: AVPlayerItem? = nil
fileprivate var url: URL? = nil
fileprivate var urlOptions: [String: Any]? = nil
fileprivate let stateQueue = DispatchQueue(
label: "AVPlayerWrapper.stateQueue",
attributes: .concurrent
)
public init() {
playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
playerObserver.delegate = self
playerTimeObserver.delegate = self
playerItemNotificationObserver.delegate = self
playerItemObserver.delegate = self
setupAVPlayer();
}
// MARK: - AVPlayerWrapperProtocol
fileprivate(set) var playbackError: AudioPlayerError.PlaybackError? = nil
var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle
var state: AVPlayerWrapperState {
get {
var state: AVPlayerWrapperState!
stateQueue.sync {
state = _state
}
return state
}
set {
stateQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
let currentState = self._state
if (currentState != newValue) {
self._state = newValue
self.delegate?.AVWrapper(didChangeState: newValue)
}
}
}
}
fileprivate(set) var lastPlayerTimeControlStatus: AVPlayer.TimeControlStatus = AVPlayer.TimeControlStatus.paused
/**
Whether AVPlayer should start playing automatically when the item is ready.
*/
public var playWhenReady: Bool = false {
didSet {
if (playWhenReady == true && (state == .failed || state == .stopped)) {
reload(startFromCurrentTime: state == .failed)
}
applyAVPlayerRate()
if oldValue != playWhenReady {
delegate?.AVWrapper(didChangePlayWhenReady: playWhenReady)
}
}
}
var currentItem: AVPlayerItem? {
avPlayer.currentItem
}
var playbackActive: Bool {
switch state {
case .idle, .stopped, .ended, .failed:
return false
default: return true
}
}
var currentTime: TimeInterval {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
}
var duration: TimeInterval {
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
return 0.0
}
var bufferedPosition: TimeInterval {
currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
}
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
avPlayer.reasonForWaitingToPlay
}
private var _rate: Float = 1.0;
var rate: Float {
get { _rate }
set {
_rate = newValue
applyAVPlayerRate()
}
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
var timeEventFrequency: TimeEventFrequency = .everySecond {
didSet {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var volume: Float {
get { avPlayer.volume }
set { avPlayer.volume = newValue }
}
var isMuted: Bool {
get { avPlayer.isMuted }
set { avPlayer.isMuted = newValue }
}
var automaticallyWaitsToMinimizeStalling: Bool {
get { avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
func play() {
playWhenReady = true
}
func pause() {
playWhenReady = false
}
func togglePlaying() {
switch avPlayer.timeControlStatus {
case .playing, .waitingToPlayAtSpecifiedRate:
pause()
case .paused:
play()
@unknown default:
fatalError("Unknown AVPlayer.timeControlStatus")
}
}
func stop() {
state = .stopped
clearCurrentItem()
playWhenReady = false
}
func seek(to seconds: TimeInterval) {
// if the player is loading then we need to defer seeking until it's ready.
if (avPlayer.currentItem == nil) {
timeToSeekToAfterLoading = seconds
} else {
let time = CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)
avPlayer.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { (finished) in
self.delegate?.AVWrapper(seekTo: Double(seconds), didFinish: finished)
}
}
}
func seek(by seconds: TimeInterval) {
if let currentItem = avPlayer.currentItem {
let time = currentItem.currentTime().seconds + seconds
avPlayer.seek(
to: CMTimeMakeWithSeconds(time, preferredTimescale: 1000)
) { (finished) in
self.delegate?.AVWrapper(seekTo: Double(time), didFinish: finished)
}
} else {
if let timeToSeekToAfterLoading = timeToSeekToAfterLoading {
self.timeToSeekToAfterLoading = timeToSeekToAfterLoading + seconds
} else {
timeToSeekToAfterLoading = seconds
}
}
}
private func playbackFailed(error: AudioPlayerError.PlaybackError) {
state = .failed
self.playbackError = error
self.delegate?.AVWrapper(failedWithError: error)
}
func load() {
if (state == .failed) {
recreateAVPlayer()
} else {
clearCurrentItem()
}
if let url = url {
let pendingAsset = AVURLAsset(url: url, options: urlOptions)
asset = pendingAsset
state = .loading
// Load metadata keys asynchronously and separate from playable, to allow that to execute as quickly as it can
let metdataKeys = ["commonMetadata", "availableChapterLocales", "availableMetadataFormats"]
pendingAsset.loadValuesAsynchronously(forKeys: metdataKeys, completionHandler: { [weak self] in
guard let self = self else { return }
if (pendingAsset != self.asset) { return; }
let commonData = pendingAsset.commonMetadata
if (!commonData.isEmpty) {
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
}
if pendingAsset.availableChapterLocales.count > 0 {
for locale in pendingAsset.availableChapterLocales {
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
self.delegate?.AVWrapper(didReceiveChapterMetadata: chapters)
}
} else {
for format in pendingAsset.availableMetadataFormats {
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
self.delegate?.AVWrapper(didReceiveTimedMetadata: [group])
}
}
})
// Load playable portion of the track and commence when ready
let playableKeys = ["playable"]
pendingAsset.loadValuesAsynchronously(forKeys: playableKeys, completionHandler: { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
if (pendingAsset != self.asset) { return; }
for key in playableKeys {
var error: NSError?
let keyStatus = pendingAsset.statusOfValue(forKey: key, error: &error)
switch keyStatus {
case .failed:
self.playbackFailed(error: AudioPlayerError.PlaybackError.failedToLoadKeyValue)
return
case .cancelled, .loading, .unknown:
return
case .loaded:
break
default: break
}
}
if (!pendingAsset.isPlayable) {
self.playbackFailed(error: AudioPlayerError.PlaybackError.itemWasUnplayable)
return;
}
let item = AVPlayerItem(
asset: pendingAsset,
automaticallyLoadedAssetKeys: playableKeys
)
self.item = item;
item.preferredForwardBufferDuration = self.bufferDuration
self.avPlayer.replaceCurrentItem(with: item)
self.startObservingAVPlayer(item: item)
self.applyAVPlayerRate()
if let initialTime = self.timeToSeekToAfterLoading {
self.timeToSeekToAfterLoading = nil
self.seek(to: initialTime)
}
}
})
}
}
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
self.playWhenReady = playWhenReady
self.url = url
self.urlOptions = options
self.load()
}
func load(
from url: URL,
playWhenReady: Bool,
initialTime: TimeInterval? = nil,
options: [String : Any]? = nil
) {
self.load(from: url, playWhenReady: playWhenReady, options: options)
if let initialTime = initialTime {
self.seek(to: initialTime)
}
}
func load(
from url: String,
type: SourceType = .stream,
playWhenReady: Bool = false,
initialTime: TimeInterval? = nil,
options: [String : Any]? = nil
) {
if let itemUrl = type == .file
? URL(fileURLWithPath: url)
: URL(string: url)
{
self.load(from: itemUrl, playWhenReady: playWhenReady, options: options)
if let initialTime = initialTime {
self.seek(to: initialTime)
}
} else {
clearCurrentItem()
playbackFailed(error: AudioPlayerError.PlaybackError.invalidSourceUrl(url))
}
}
func unload() {
clearCurrentItem()
state = .idle
}
func reload(startFromCurrentTime: Bool) {
var time : Double? = nil
if (startFromCurrentTime) {
if let currentItem = currentItem {
if (!currentItem.duration.isIndefinite) {
time = currentItem.currentTime().seconds
}
}
}
load()
if let time = time {
seek(to: time)
}
}
// MARK: - Util
private func clearCurrentItem() {
guard let asset = asset else { return }
stopObservingAVPlayerItem()
asset.cancelLoading()
self.asset = nil
avPlayer.replaceCurrentItem(with: nil)
}
private func startObservingAVPlayer(item: AVPlayerItem) {
playerItemObserver.startObserving(item: item)
playerItemNotificationObserver.startObserving(item: item)
}
private func stopObservingAVPlayerItem() {
playerItemObserver.stopObservingCurrentItem()
playerItemNotificationObserver.stopObservingCurrentItem()
}
private func recreateAVPlayer() {
playbackError = nil
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerTimeObserver.unregisterForPeriodicEvents()
playerObserver.stopObserving()
stopObservingAVPlayerItem()
clearCurrentItem()
avPlayer = AVPlayer();
setupAVPlayer()
delegate?.AVWrapperDidRecreateAVPlayer()
}
private func setupAVPlayer() {
// disabled since we're not making use of video playback
avPlayer.allowsExternalPlayback = false;
playerObserver.player = avPlayer
playerObserver.startObserving()
playerTimeObserver.player = avPlayer
playerTimeObserver.registerForBoundaryTimeEvents()
playerTimeObserver.registerForPeriodicTimeEvents()
applyAVPlayerRate()
}
private func applyAVPlayerRate() {
avPlayer.rate = playWhenReady ? _rate : 0
}
}
extension AVPlayerWrapper: AVPlayerObserverDelegate {
// MARK: - AVPlayerObserverDelegate
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
switch status {
case .paused:
let state = self.state
if self.asset == nil && state != .stopped {
self.state = .idle
} else if (state != .failed && state != .stopped) {
// Playback may have become paused externally for example due to a bluetooth device disconnecting:
if (self.playWhenReady) {
// Only if we are not on the boundaries of the track, otherwise itemDidPlayToEndTime will handle it instead.
if (self.currentTime > 0 && self.currentTime < self.duration) {
self.playWhenReady = false;
}
} else {
self.state = .paused
}
}
case .waitingToPlayAtSpecifiedRate:
if self.asset != nil {
self.state = .buffering
}
case .playing:
self.state = .playing
@unknown default:
break
}
}
func player(statusDidChange status: AVPlayer.Status) {
if (status == .failed) {
let error = item!.error as NSError?
playbackFailed(error: error?.code == URLError.notConnectedToInternet.rawValue
? AudioPlayerError.PlaybackError.notConnectedToInternet
: AudioPlayerError.PlaybackError.playbackFailed
)
}
}
}
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
// MARK: - AVPlayerTimeObserverDelegate
func audioDidStart() {
state = .playing
}
func timeEvent(time: CMTime) {
delegate?.AVWrapper(secondsElapsed: time.seconds)
}
}
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemFailedToPlayToEndTime() {
playbackFailed(error: AudioPlayerError.PlaybackError.playbackFailed)
delegate?.AVWrapperItemFailedToPlayToEndTime()
}
func itemPlaybackStalled() {
delegate?.AVWrapperItemPlaybackStalled()
}
func itemDidPlayToEndTime() {
delegate?.AVWrapperItemDidPlayToEndTime()
}
}
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
// MARK: - AVPlayerItemObserverDelegate
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) {
if (playbackLikelyToKeepUp && state != .playing) {
state = .ready
}
}
func item(didUpdateDuration duration: Double) {
delegate?.AVWrapper(didUpdateDuration: duration)
}
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
delegate?.AVWrapper(didReceiveTimedMetadata: metadata)
}
}
@@ -0,0 +1,27 @@
//
// AVPlayerWrapperDelegate.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 26/10/2018.
//
import Foundation
import MediaPlayer
protocol AVPlayerWrapperDelegate: AnyObject {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Double, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem])
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup])
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool)
func AVWrapperItemDidPlayToEndTime()
func AVWrapperItemFailedToPlayToEndTime()
func AVWrapperItemPlaybackStalled()
func AVWrapperDidRecreateAVPlayer()
}
@@ -9,12 +9,16 @@ import Foundation
import AVFoundation
protocol AVPlayerWrapperProtocol: class {
protocol AVPlayerWrapperProtocol: AnyObject {
var state: AVPlayerWrapperState { get }
var state: AVPlayerWrapperState { get set }
var playWhenReady: Bool { get set }
var currentItem: AVPlayerItem? { get }
var playbackActive: Bool { get }
var currentTime: TimeInterval { get }
var duration: TimeInterval { get }
@@ -23,6 +27,7 @@ protocol AVPlayerWrapperProtocol: class {
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
var playbackError: AudioPlayerError.PlaybackError? { get }
var rate: Float { get set }
@@ -37,7 +42,6 @@ protocol AVPlayerWrapperProtocol: class {
var isMuted: Bool { get set }
var automaticallyWaitsToMinimizeStalling: Bool { get set }
func play()
@@ -48,9 +52,16 @@ protocol AVPlayerWrapperProtocol: class {
func stop()
func seek(to seconds: TimeInterval)
func seek(by offset: TimeInterval)
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
func load(from url: String, type: SourceType, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
func unload()
func reload(startFromCurrentTime: Bool)
}
@@ -26,10 +26,18 @@ public enum AVPlayerWrapperState: String {
/// The player is paused.
case paused
/// The player is stopped.
case stopped
/// The player is playing.
case playing
/// No item loaded, the player is stopped.
case idle
/// Failed
case failed
/// Playback has reached the end.
case ended
}
@@ -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
@@ -66,26 +71,26 @@ public class DefaultAudioItem: AudioItem {
}
public func getSourceUrl() -> String {
return audioUrl
audioUrl
}
public func getArtist() -> String? {
return artist
artist
}
public func getTitle() -> String? {
return title
title
}
public func getAlbumTitle() -> String? {
return albumTitle
albumTitle
}
public func getSourceType() -> SourceType {
return sourceType
sourceType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
public func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) {
handler(artwork)
}
@@ -96,18 +101,18 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
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) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
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)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
pitchAlgorithmType
}
}
@@ -116,18 +121,18 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.initialTime = 0.0
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)
}
public func getInitialTime() -> TimeInterval {
return initialTime
initialTime
}
}
@@ -137,18 +142,17 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
public var options: [String: Any]
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.options = [:]
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)
}
public func getAssetOptions() -> [String: Any] {
return options
options
}
}
+449
View File
@@ -0,0 +1,449 @@
//
// AudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public class AudioPlayer: AVPlayerWrapperDelegate {
/// The wrapper around the underlying AVPlayer
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
private(set) var currentItem: AudioItem?
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = [] {
didSet {
if let item = currentItem {
self.enableRemoteCommands(forItem: item)
}
}
}
/**
Handles the `playWhenReady` setting while executing a given action.
This method takes an optional `Bool` value and a closure representing an action to execute.
If the `Bool` value is not `nil`, `self.playWhenReady` is set accordingly either before or
after executing the action.
- Parameters:
- playWhenReady: Optional `Bool` to set `self.playWhenReady`.
- If `true`, `self.playWhenReady` will be set after executing the action.
- If `false`, `self.playWhenReady` will be set before executing the action.
- If `nil`, `self.playWhenReady` will not be changed.
- action: A closure representing the action to execute. This closure can throw an error.
- Throws: This function will propagate any errors thrown by the `action` closure.
*/
internal func handlePlayWhenReady(_ playWhenReady: Bool?, action: () throws -> Void) rethrows {
if playWhenReady == false {
self.playWhenReady = false
}
try action()
if playWhenReady == true, playbackError == nil {
self.playWhenReady = true
}
}
// MARK: - Getters from AVPlayerWrapper
public var playbackError: AudioPlayerError.PlaybackError? {
wrapper.playbackError
}
/**
The elapsed playback time of the current item.
*/
public var currentTime: Double {
wrapper.currentTime
}
/**
The duration of the current AudioItem.
*/
public var duration: Double {
wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
public var playerState: AudioPlayerState {
wrapper.state
}
// MARK: - Setters for AVPlayerWrapper
/**
Whether the player should start playing automatically when the item is ready.
*/
public var playWhenReady: Bool {
get { wrapper.playWhenReady }
set {
wrapper.playWhenReady = newValue
}
}
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering. Setting `bufferDuration` to larger than zero automatically disables `automaticallyWaitsToMinimizeStalling`. Setting it back to zero automatically enables `automaticallyWaitsToMinimizeStalling`.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
*/
public var bufferDuration: TimeInterval {
get { wrapper.bufferDuration }
set {
wrapper.bufferDuration = newValue
wrapper.automaticallyWaitsToMinimizeStalling = wrapper.bufferDuration == 0
}
}
/**
Indicates whether the player should automatically delay playback in order to minimize stalling. Setting this to true will also set `bufferDuration` back to `0`.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { wrapper.automaticallyWaitsToMinimizeStalling }
set {
if (newValue) {
wrapper.bufferDuration = 0
}
wrapper.automaticallyWaitsToMinimizeStalling = newValue
}
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
public var timeEventFrequency: TimeEventFrequency {
get { wrapper.timeEventFrequency }
set { wrapper.timeEventFrequency = newValue }
}
public var volume: Float {
get { wrapper.volume }
set { wrapper.volume = newValue }
}
public var isMuted: Bool {
get { wrapper.isMuted }
set { wrapper.isMuted = newValue }
}
public var rate: Float {
get { wrapper.rate }
set {
wrapper.rate = newValue
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
}
}
// MARK: - Init
/**
Create a new AudioPlayer.
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
// MARK: - Player Actions
/**
Load an AudioItem into the manager.
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
handlePlayWhenReady(playWhenReady) {
currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
// Reset playback values without updating, because that will happen in
// the loadNowPlayingMetaValues call straight after:
nowPlayingInfoController.setWithoutUpdate(keyValues: [
MediaItemProperty.duration(nil),
NowPlayingInfoProperty.playbackRate(nil),
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
])
loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
wrapper.load(
from: item.getSourceUrl(),
type: item.getSourceType(),
playWhenReady: self.playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions()
)
}
}
/**
Toggle playback status.
*/
public func togglePlaying() {
wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
wrapper.play()
}
/**
Pause playback
*/
public func pause() {
wrapper.pause()
}
/**
Stop playback
*/
public func stop() {
let wasActive = wrapper.playbackActive
wrapper.stop()
if (wasActive) {
event.playbackEnd.emit(data: .playerStopped)
}
}
/**
Reload the current item.
*/
public func reload(startFromCurrentTime: Bool) {
wrapper.reload(startFromCurrentTime: startFromCurrentTime)
}
/**
Seek to a specific time in the item.
*/
public func seek(to seconds: TimeInterval) {
wrapper.seek(to: seconds)
}
/**
Seek by relative a time offset in the item.
*/
public func seek(by offset: TimeInterval) {
wrapper.seek(by: offset)
}
// MARK: - Remote Command Center
func enableRemoteCommands(_ commands: [RemoteCommand]) {
remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
self.enableRemoteCommands(item.getCommands())
}
else {
self.enableRemoteCommands(remoteCommands)
}
}
/**
Syncs the current remoteCommands with the iOS command center.
Can be used to update item states - e.g. like, dislike and bookmark.
*/
@available(*, deprecated, message: "Directly set .remoteCommands instead")
public func syncRemoteCommandsWithCommandCenter() {
self.enableRemoteCommands(remoteCommands)
}
// MARK: - NowPlayingInfo
/**
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
- Album title
- Album artwork
*/
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
loadArtwork(forItem: item)
}
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
- Playback rate
*/
func updateNowPlayingPlaybackValues() {
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.duration(wrapper.duration),
NowPlayingInfoProperty.playbackRate(wrapper.playWhenReady ? Double(wrapper.rate) : 0),
NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime)
])
}
public func clear() {
let playbackWasActive = wrapper.playbackActive
currentItem = nil
wrapper.unload()
nowPlayingInfoController.clear()
if (playbackWasActive) {
event.playbackEnd.emit(data: .cleared)
}
}
// MARK: - Private
private func setNowPlayingCurrentTime(seconds: Double) {
nowPlayingInfoController.set(
keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(seconds)
)
}
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
} else {
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
}
}
}
private func setTimePitchingAlgorithmForCurrentItem() {
if let item = currentItem as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
} else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
}
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .ready, .loading:
setTimePitchingAlgorithmForCurrentItem()
default: break
}
switch state {
case .ready, .loading, .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
default: break
}
event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
event.fail.emit(data: error)
event.playbackEnd.emit(data: .failed)
}
func AVWrapper(seekTo seconds: Double, didFinish: Bool) {
if automaticallyUpdateNowPlayingInfo {
setNowPlayingCurrentTime(seconds: Double(seconds))
}
event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
event.updateDuration.emit(data: duration)
}
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
event.receiveCommonMetadata.emit(data: metadata)
}
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
event.receiveChapterMetadata.emit(data: metadata)
}
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
event.receiveTimedMetadata.emit(data: metadata)
}
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
event.playWhenReadyChange.emit(data: playWhenReady)
}
func AVWrapperItemDidPlayToEndTime() {
event.playbackEnd.emit(data: .playedUntilEnd)
wrapper.state = .ended
}
func AVWrapperItemFailedToPlayToEndTime() {
AVWrapper(failedWithError: AudioPlayerError.PlaybackError.playbackFailed)
}
func AVWrapperItemPlaybackStalled() {
}
func AVWrapperDidRecreateAVPlayer() {
event.didRecreateAVPlayer.emit(data: ())
}
}
@@ -0,0 +1,26 @@
//
// AudioPlayerError.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 25/03/2018.
//
import Foundation
public enum AudioPlayerError: Error {
public enum PlaybackError: Error {
case failedToLoadKeyValue
case invalidSourceUrl(String)
case notConnectedToInternet
case playbackFailed
case itemWasUnplayable
}
public enum QueueError: Error {
case noCurrentItem
case invalidIndex(index: Int, message: String)
case empty
}
}
@@ -8,7 +8,7 @@
import Foundation
import AVFoundation
#if os(iOS)
protocol AudioSession {
var isOtherAudioPlaying: Bool { get }
@@ -21,10 +21,8 @@ protocol AudioSession {
var availableCategories: [AVAudioSession.Category] { get }
@available(iOS 10.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
@available(iOS 11.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
@@ -32,3 +30,4 @@ protocol AudioSession {
}
extension AVAudioSession: AudioSession {}
#endif
@@ -8,11 +8,16 @@
import Foundation
import AVFoundation
public protocol AudioSessionControllerDelegate: class {
func handleInterruption(type: AVAudioSession.InterruptionType)
public enum InterruptionType: Equatable {
case began
case ended(shouldResume: Bool)
}
#if os(iOS)
public protocol AudioSessionControllerDelegate: AnyObject {
func handleInterruption(type: InterruptionType)
}
/**
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
@@ -30,7 +35,7 @@ public class AudioSessionController {
True if another app is currently playing audio.
*/
public var isOtherAudioPlaying: Bool {
return audioSession.isOtherAudioPlaying
audioSession.isOtherAudioPlaying
}
/**
@@ -46,9 +51,7 @@ public class AudioSessionController {
Set this to false to disable the behaviour.
*/
public var isObservingForInterruptions: Bool {
get {
return _isObservingForInterruptions
}
get { _isObservingForInterruptions }
set {
if newValue == _isObservingForInterruptions {
return
@@ -112,7 +115,21 @@ public class AudioSessionController {
return
}
self.delegate?.handleInterruption(type: type)
switch type {
case .began:
delegate?.handleInterruption(type: .began)
case .ended:
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
delegate?.handleInterruption(type: .ended(shouldResume: false))
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
@unknown default: return
}
}
}
#endif
@@ -10,15 +10,23 @@ import MediaPlayer
extension AudioPlayer {
public typealias StateChangeEventData = (AudioPlayerState)
public typealias PlaybackEndEventData = (PlaybackEndedReason)
public typealias SecondElapseEventData = (TimeInterval)
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias PlayWhenReadyChangeData = Bool
public typealias StateChangeEventData = AudioPlayerState
public typealias PlaybackEndEventData = PlaybackEndedReason
public typealias SecondElapseEventData = TimeInterval
public typealias FailEventData = Error?
public typealias SeekEventData = (seconds: Double, didFinish: Bool)
public typealias UpdateDurationEventData = Double
public typealias MetadataCommonEventData = [AVMetadataItem]
public typealias MetadataTimedEventData = [AVTimedMetadataGroup]
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
public typealias CurrentItemEventData = (
item: AudioItem?,
index: Int?,
lastItem: AudioItem?,
lastIndex: Int?,
lastPosition: Double?
)
public struct EventHolder {
@@ -27,6 +35,12 @@ extension AudioPlayer {
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
/**
Emitted when the `AudioPlayer#playWhenReady` has changed
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let playWhenReadyChange: AudioPlayer.Event<PlayWhenReadyChangeData> = AudioPlayer.Event()
/**
Emitted when the playback of the player, for some reason, has stopped.
@@ -60,10 +74,22 @@ extension AudioPlayer {
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
/**
Emitted when the player receives metadata.
Emitted when the player receives common metadata.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let receiveMetadata: AudioPlayer.Event<MetadataEventData> = AudioPlayer.Event()
public let receiveCommonMetadata: AudioPlayer.Event<MetadataCommonEventData> = AudioPlayer.Event()
/**
Emitted when the player receives timed metadata.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let receiveTimedMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
/**
Emitted when the player receives chapter metadata.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let receiveChapterMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()
/**
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
@@ -73,11 +99,11 @@ extension AudioPlayer {
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
/**
Emitted when a new track starts and the queue index changes.
Emitted when the current track has changed.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: It is only fired for instances of a QueuedAudioPlayer.
*/
public let queueIndex: AudioPlayer.Event<QueueIndexEventData> = AudioPlayer.Event()
public let currentItem: AudioPlayer.Event<CurrentItemEventData> = AudioPlayer.Event()
}
public typealias EventClosure<EventData> = (EventData) -> Void
@@ -90,7 +116,7 @@ extension AudioPlayer {
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
self.listener = listener
self.invoke = { [weak listener] (data: EventData) in
invoke = { [weak listener] (data: EventData) in
guard let _ = listener else {
return false
}
@@ -102,44 +128,28 @@ extension AudioPlayer {
}
public class Event<EventData> {
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
private let queue: DispatchQueue = DispatchQueue(label: "com.swiftAudioEx.eventQueue")
var invokers: [Invoker<EventData>] = []
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
actionQueue.async {
self.invokersSemaphore.wait()
queue.async {
self.invokers.append(Invoker(listener: listener, closure: closure))
self.invokersSemaphore.signal()
}
}
public func removeListener(_ listener: AnyObject) {
actionQueue.async {
self.invokersSemaphore.wait()
queue.async {
self.invokers = self.invokers.filter({ (invoker) -> Bool in
if let listenerToCheck = invoker.listener {
return listenerToCheck !== listener
}
return true
return invoker.listener !== listener
})
self.invokersSemaphore.signal()
}
}
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
return invoker.invoke(data)
})
self.invokersSemaphore.signal()
queue.async {
self.invokers = self.invokers.filter { $0.invoke(data) }
}
}
}
}
@@ -0,0 +1,73 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private var infoQueue: DispatchQueueType = DispatchQueue(
label: "NowPlayingInfoController.infoQueue",
attributes: .concurrent
)
private(set) var infoCenter: NowPlayingInfoCenter
private(set) var info: [String: Any] = [:]
public required init() {
infoCenter = MPNowPlayingInfoCenter.default()
}
/// Used for testing purposes.
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
infoQueue = dispatchQueue
self.infoCenter = infoCenter
}
public required init(infoCenter: NowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
infoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
keyValues.forEach {
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
}
self.update()
}
}
public func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
infoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
keyValues.forEach {
(keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
}
}
}
public func set(keyValue: NowPlayingInfoKeyValue) {
infoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self.info[keyValue.getKey()] = keyValue.getValue()
self.update()
}
}
private func update() {
infoCenter.nowPlayingInfo = info
}
public func clear() {
infoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self.info = [:]
self.infoCenter.nowPlayingInfo = nil
}
}
}
@@ -19,6 +19,8 @@ public protocol NowPlayingInfoControllerProtocol {
func set(keyValues: [NowPlayingInfoKeyValue])
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue])
func clear()
}
@@ -31,7 +31,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The URL pointing to the now playing item's underlying asset.
This constant is used by the system UI when video thumbnails or audio waveform visualizations are applicable.
*/
@available(iOS 10.3, *)
case assetUrl(URL?)
/**
@@ -116,7 +115,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The service provider associated with the now-playing item.
Value is a unique NSString that identifies the service provider for the now-playing item. If the now-playing item belongs to a channel or subscription service, this key can be used to coordinate various types of now-playing content from the service provider.
*/
@available(iOS 11.0, *)
case serviceIdentifier(String?)
@@ -130,11 +128,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyAvailableLanguageOptions
case .assetUrl(_):
if #available(iOS 10.3, *) {
return MPNowPlayingInfoPropertyAssetURL
} else {
return ""
}
return MPNowPlayingInfoPropertyAssetURL
case .chapterCount(_):
return MPNowPlayingInfoPropertyChapterCount
@@ -175,11 +169,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyPlaybackRate
case .serviceIdentifier(_):
if #available(iOS 11.0, *) {
return MPNowPlayingInfoPropertyServiceIdentifier
} else {
return ""
}
return MPNowPlayingInfoPropertyServiceIdentifier
}
}
@@ -194,10 +184,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return options
case .assetUrl(let url):
if #available(iOS 10.3, *) {
return url
}
return false
return url
case .chapterCount(let count):
return count != nil ? NSNumber(value: count!) : nil
@@ -0,0 +1,102 @@
//
// AVPlayerItemNotificationObserver.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 12/03/2018.
//
import Foundation
import AVFoundation
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
func itemDidPlayToEndTime()
func itemFailedToPlayToEndTime()
func itemPlaybackStalled()
}
/**
Observes notifications posted by an AVPlayerItem.
Currently only listening for the AVPlayerItemDidPlayToEndTime notification.
*/
class AVPlayerItemNotificationObserver {
private let notificationCenter: NotificationCenter = NotificationCenter.default
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemNotificationObserverDelegate?
private(set) var isObserving: Bool = false
deinit {
stopObservingCurrentItem()
}
/**
Will start observing notifications from an item.
- parameter item: The item to observe.
- important: Cannot observe more than one item at a time.
*/
func startObserving(item: AVPlayerItem) {
stopObservingCurrentItem()
observingItem = item
isObserving = true
notificationCenter.addObserver(
self,
selector: #selector(itemDidPlayToEndTime),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: item
)
notificationCenter.addObserver(
self,
selector: #selector(itemFailedToPlayToEndTime),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: item
)
notificationCenter.addObserver(
self,
selector: #selector(itemPlaybackStalled),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: item
)
}
/**
Stop receiving notifications for the current item.
*/
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
}
notificationCenter.removeObserver(
self,
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: observingItem
)
notificationCenter.removeObserver(
self,
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: observingItem
)
notificationCenter.removeObserver(
self,
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: observingItem
)
self.observingItem = nil
isObserving = false
}
@objc private func itemDidPlayToEndTime() {
delegate?.itemDidPlayToEndTime()
}
@objc private func itemFailedToPlayToEndTime() {
delegate?.itemFailedToPlayToEndTime()
}
@objc private func itemPlaybackStalled() {
delegate?.itemPlaybackStalled()
}
}
@@ -8,17 +8,21 @@
import Foundation
import AVFoundation
protocol AVPlayerItemObserverDelegate: class {
protocol AVPlayerItemObserverDelegate: AnyObject {
/**
Called when the observed item updates the duration.
Called when the duration of the observed item is updated.
*/
func item(didUpdateDuration duration: Double)
/**
Called when the playback of the observed item is or is no longer likely to keep up.
*/
func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool)
/**
Called when the observed item receives metadata
*/
func item(didReceiveMetadata metadata: [AVMetadataItem])
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])
}
@@ -28,12 +32,12 @@ protocol AVPlayerItemObserverDelegate: class {
class AVPlayerItemObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private var currentMetadataOutput: AVPlayerItemMetadataOutput?
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
}
private(set) var isObserving: Bool = false
@@ -41,6 +45,10 @@ class AVPlayerItemObserver: NSObject {
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
override init() {
super.init()
}
deinit {
stopObservingCurrentItem()
}
@@ -51,23 +59,36 @@ class AVPlayerItemObserver: NSObject {
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
self.stopObservingCurrentItem()
stopObservingCurrentItem()
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
// Create and add a new metadata output to the item.
let metadataOutput = AVPlayerItemMetadataOutput()
metadataOutput.setDelegate(self, queue: .main)
item.add(metadataOutput)
self.currentMetadataOutput = metadataOutput
}
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
}
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
self.isObserving = false
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
// Remove all metadata outputs from the item.
observingItem.removeAllMetadataOutputs()
isObserving = false
self.observingItem = nil
self.currentMetadataOutput = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@@ -79,21 +100,37 @@ class AVPlayerItemObserver: NSObject {
switch observedKeyPath {
case AVPlayerItemKeyPath.duration:
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.loadedTimeRanges:
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.timedMetadata:
if let metadata = change?[.newKey] as? [AVMetadataItem] {
self.delegate?.item(didReceiveMetadata: metadata)
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
}
default: break
}
}
}
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
if output == currentMetadataOutput {
delegate?.item(didReceiveTimedMetadata: groups)
}
}
}
extension AVPlayerItem {
func removeAllMetadataOutputs() {
for output in self.outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
self.remove(output)
}
}
}
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
protocol AVPlayerObserverDelegate: class {
protocol AVPlayerObserverDelegate: AnyObject {
/**
Called when the AVPlayer.status changes.
@@ -20,90 +20,96 @@ protocol AVPlayerObserverDelegate: class {
Called when the AVPlayer.timeControlStatus changes.
*/
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
}
/**
Observing an AVPlayers status changes.
*/
class AVPlayerObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private struct AVPlayerKeyPath {
static let status = #keyPath(AVPlayer.status)
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
private(set) var isObserving: Bool = false
weak var delegate: AVPlayerObserverDelegate?
weak var player: AVPlayer? {
willSet {
self.stopObserving()
stopObserving()
}
}
deinit {
self.stopObserving()
stopObserving()
}
/**
Start receiving events from this observer.
*/
func startObserving() {
if (isObserving) { return };
guard let player = player else {
return
}
self.stopObserving()
self.isObserving = true
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
isObserving = true
player.addObserver(
self,
forKeyPath: AVPlayerKeyPath.status,
options: statusChangeOptions,
context: &AVPlayerObserver.context
)
player.addObserver(
self,
forKeyPath: AVPlayerKeyPath.timeControlStatus,
options: timeControlStatusChangeOptions,
context: &AVPlayerObserver.context
)
}
func stopObserving() {
guard let player = player, isObserving else {
return
}
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
self.isObserving = false
isObserving = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
switch observedKeyPath {
case AVPlayerKeyPath.status:
self.handleStatusChange(change)
handleStatusChange(change)
case AVPlayerKeyPath.timeControlStatus:
self.handleTimeControlStatusChange(change)
handleTimeControlStatusChange(change)
default:
break
}
}
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
}
else {
} else {
status = .unknown
}
delegate?.player(statusDidChange: status)
}
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.TimeControlStatus
if let statusNumber = change?[.newKey] as? NSNumber {
@@ -111,5 +117,4 @@ class AVPlayerObserver: NSObject {
delegate?.player(didChangeTimeControlStatus: status)
}
}
}
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
protocol AVPlayerTimeObserverDelegate: class {
protocol AVPlayerTimeObserverDelegate: AnyObject {
func audioDidStart()
func timeEvent(time: CMTime)
}
@@ -61,19 +61,25 @@ class AVPlayerTimeObserver {
return
}
unregisterForBoundaryTimeEvents()
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
self?.delegate?.audioDidStart()
})
boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(
forTimes: [AVPlayerTimeObserver.startBoundaryTime].map({
NSValue(time: $0)
}),
queue: nil,
using: { [weak self] in
self?.delegate?.audioDidStart()
}
)
}
/**
Unregister from the boundary events of the player.
*/
func unregisterForBoundaryTimeEvents() {
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
return
}
guard
let player = player,
let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken
else { return }
player.removeTimeObserver(boundaryTimeStartObserverToken)
self.boundaryTimeStartObserverToken = nil
}
+356
View File
@@ -0,0 +1,356 @@
//
// QueueManager.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
protocol QueueManagerDelegate: AnyObject {
func onReceivedFirstItem()
func onCurrentItemChanged()
func onSkippedToSameCurrentItem()
}
class QueueManager<Element> {
fileprivate let recursiveLock = NSRecursiveLock()
fileprivate func synchronizeThrows<T>(action: () throws -> T) throws -> T {
recursiveLock.lock()
let result = try action()
recursiveLock.unlock()
return result
}
fileprivate func synchronize <T>(action: () -> T) -> T {
recursiveLock.lock()
let result = action()
recursiveLock.unlock()
return result
}
weak var delegate: QueueManagerDelegate? = nil
var _currentIndex: Int = -1
/**
The index of the current item. `-1` when there is no current item
*/
private(set) var currentIndex: Int {
get {
return synchronize {
return _currentIndex
}
}
set {
return synchronize {
self._currentIndex = newValue
}
}
}
/**
All items held by the queue.
*/
private(set) var items: [Element] = [] {
didSet {
return synchronize {
if oldValue.count == 0 && items.count > 0 {
delegate?.onReceivedFirstItem()
}
}
}
}
public var nextItems: [Element] {
return synchronize {
return currentIndex == -1 || currentIndex == items.count - 1
? []
: Array(items[currentIndex + 1..<items.count])
}
}
public var previousItems: [Element] {
return synchronize {
return currentIndex <= 0
? []
: Array(items[0..<currentIndex])
}
}
/**
The current item for the queue.
*/
public var current: Element? {
return synchronize {
return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil
}
}
private func throwIfQueueEmpty() throws {
if items.count == 0 {
throw AudioPlayerError.QueueError.empty
}
}
private func throwIfIndexInvalid(
index: Int,
name: String = "index",
min: Int? = nil,
max: Int? = nil
) throws {
guard index >= (min ?? 0) && (max ?? items.count) > index else {
throw AudioPlayerError.QueueError.invalidIndex(
index: index,
message: "\(name.prefix(1).uppercased() + name.dropFirst())) has to be positive and smaller than the count of current items (\(items.count))"
)
}
}
/**
Add a single item to the queue.
- parameter item: The `AudioItem` to be added.
*/
public func add(_ item: Element) {
synchronize {
items.append(item)
}
}
/**
Add an array of items to the queue.
- parameter items: The `AudioItem`s to be added.
*/
public func add(_ items: [Element]) {
synchronize {
if (items.count == 0) { return }
self.items.append(contentsOf: items)
}
}
/**
Add an array of items to the queue at a given index.
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
public func add(_ items: [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 > 0 && currentIndex >= index) {
currentIndex += items.count
}
self.items.insert(contentsOf: items, at: index)
}
}
internal enum SkipDirection : Int {
case next = 1
case previous = -1
}
private func skip(direction: SkipDirection, wrap: Bool) -> Element? {
let count = items.count
if (current == nil || count == 0) {
return nil
}
if (count == 1) {
if (wrap) {
delegate?.onSkippedToSameCurrentItem()
}
} else {
var index = currentIndex + direction.rawValue
if (wrap) {
index = (items.count + index) % items.count;
}
let oldIndex = currentIndex
currentIndex = max(0, min(items.count - 1, index))
if (oldIndex != currentIndex) {
delegate?.onCurrentItemChanged()
}
}
return current
}
/**
Makes the next item in the queue active, or the last item when already at the end of the queue. When wrap is true and at the end of the queue, the first track in the queue is made active.
- parameter wrap: Whether to wrap to the start of the queue
- returns: The next (or current) item.
*/
@discardableResult
public func next(wrap: Bool = false) -> Element? {
synchronize {
return skip(direction: SkipDirection.next, wrap: wrap);
}
}
/**
Makes the previous item in the queue active, or the first item when already at the start of the queue. When wrap is true and at the start of the queue, the last track in the queue is made active.
- parameter wrap: Whether to wrap to the end of the queue
- returns: The previous item.
*/
@discardableResult
public func previous(wrap: Bool = false) -> Element? {
return synchronize {
return skip(direction: SkipDirection.previous, wrap: wrap);
}
}
/**
Jump to a position in the queue.
Will update the current item.
- parameter index: The index to jump to.
- throws: `AudioPlayerError.QueueError`
- returns: The item at the index.
*/
@discardableResult
public func jump(to index: Int) throws -> Element {
var skippedToSameCurrentItem = false
var currentItemChanged = false
let result = try synchronizeThrows {
try throwIfQueueEmpty();
try throwIfIndexInvalid(index: index)
if (index == currentIndex) {
skippedToSameCurrentItem = true
} else {
currentIndex = index
currentItemChanged = true
}
return current!
}
if (skippedToSameCurrentItem) {
delegate?.onSkippedToSameCurrentItem()
}
if (currentItemChanged) {
delegate?.onCurrentItemChanged()
}
return result
}
/**
Move an item in the queue.
- parameter fromIndex: The index of the item to be moved.
- parameter toIndex: The index to move the item to. If the index is larger than the size of the queue, the item is moved to the end of the queue instead.
- throws: `AudioPlayerError.QueueError`
*/
public func moveItem(fromIndex: Int, toIndex: Int) throws {
try synchronizeThrows {
try throwIfQueueEmpty();
try throwIfIndexInvalid(index: fromIndex, name: "fromIndex")
try throwIfIndexInvalid(index: toIndex, name: "toIndex", max: Int.max)
let item = items.remove(at: fromIndex)
self.items.insert(item, at: min(items.count, toIndex));
if (fromIndex == currentIndex) {
currentIndex = toIndex;
}
}
}
/**
Remove an item.
- parameter index: The index of the item to remove.
- throws: AudioPlayerError.QueueError
- returns: The removed item.
*/
@discardableResult
public func removeItem(at index: Int) throws -> Element {
var currentItemChanged = false
let result = try synchronizeThrows {
try throwIfQueueEmpty()
try throwIfIndexInvalid(index: index)
let result = items.remove(at: index)
if index == currentIndex {
currentIndex = items.count > 0 ? currentIndex % items.count : -1
currentItemChanged = true
} else if index < currentIndex {
currentIndex -= 1
}
return result;
}
if (currentItemChanged) {
delegate?.onCurrentItemChanged()
}
return result
}
/**
Replace the current item with a new one. If there is no current item, it is equivalent to calling `add(item:)`, `jump(to: itemIndex)`.
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: Element) {
var currentItemChanged = false
synchronize {
if currentIndex == -1 {
add(item)
if (currentIndex == -1) {
currentIndex = items.count - 1
}
} else {
items[currentIndex] = item
currentItemChanged = true
}
}
if (currentItemChanged) {
delegate?.onCurrentItemChanged()
}
}
/**
Remove all previous items in the queue.
If no previous items exist, no action will be taken.
*/
public func removePreviousItems() {
synchronize {
if (items.count == 0) { return };
guard currentIndex > 0 else { return }
items.removeSubrange(0..<currentIndex)
currentIndex = 0
}
}
/**
Remove upcoming items.
If no upcoming items exist, no action will be taken.
*/
public func removeUpcomingItems() {
synchronize {
if (items.count == 0) { return };
let nextIndex = currentIndex + 1
guard nextIndex < items.count else { return }
items.removeSubrange(nextIndex..<items.count)
}
}
/**
Removes all items for queue
*/
public func clearQueue() {
var currentItemChanged = false
synchronize {
let itemWasNil = currentIndex == -1;
currentIndex = -1
items.removeAll()
currentItemChanged = !itemWasNil
}
if (currentItemChanged) {
delegate?.onCurrentItemChanged()
}
}
}
+236
View File
@@ -0,0 +1,236 @@
//
// QueuedAudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
import MediaPlayer
/**
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
let queue: QueueManager = QueueManager<AudioItem>()
fileprivate var lastIndex: Int = -1
fileprivate var lastItem: AudioItem? = nil
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
queue.delegate = self
}
/// The repeat mode for the queue player.
public var repeatMode: RepeatMode = .off
public override var currentItem: AudioItem? {
queue.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
queue.currentIndex
}
override public func clear() {
queue.clearQueue()
super.clear()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
queue.items
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
queue.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem] {
queue.nextItems
}
/**
Will replace the current item with a new one and load it into the player.
- parameter item: The AudioItem to replace the current item.
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public override func load(item: AudioItem, playWhenReady: Bool? = nil) {
handlePlayWhenReady(playWhenReady) {
queue.replaceCurrentItem(with: item)
}
}
/**
Add a single item to the queue.
- parameter item: The item to add.
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func add(item: AudioItem, playWhenReady: Bool? = nil) {
handlePlayWhenReady(playWhenReady) {
queue.add(item)
}
}
/**
Add items to the queue.
- parameter items: The items to add to the queue.
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func add(items: [AudioItem], playWhenReady: Bool? = nil) {
handlePlayWhenReady(playWhenReady) {
queue.add(items)
}
}
public func add(items: [AudioItem], at index: Int) throws {
try queue.add(items, at: index)
}
/**
Step to the next item in the queue.
*/
public func next() {
let lastIndex = currentIndex
let playbackWasActive = wrapper.playbackActive;
_ = queue.next(wrap: repeatMode == .queue)
if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) {
event.playbackEnd.emit(data: .skippedToNext)
}
}
/**
Step to the previous item in the queue.
*/
public func previous() {
let lastIndex = currentIndex
let playbackWasActive = wrapper.playbackActive;
_ = queue.previous(wrap: repeatMode == .queue)
if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) {
event.playbackEnd.emit(data: .skippedToPrevious)
}
}
/**
Remove an item from the queue.
- parameter index: The index of the item to remove.
- throws: `AudioPlayerError.QueueError`
*/
public func removeItem(at index: Int) throws {
try queue.removeItem(at: index)
}
/**
Jump to a certain item in the queue.
- parameter index: The index of the item to jump to.
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
- throws: `AudioPlayerError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool? = nil) throws {
try handlePlayWhenReady(playWhenReady) {
if (index == currentIndex) {
seek(to: 0)
} else {
_ = try queue.jump(to: index)
}
event.playbackEnd.emit(data: .jumpedToIndex)
}
}
/**
Move an item in the queue from one position to another.
- parameter fromIndex: The index of the item to move.
- parameter toIndex: The index to move the item to.
- throws: `AudioPlayerError.QueueError`
*/
public func moveItem(fromIndex: Int, toIndex: Int) throws {
try queue.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
/**
Remove all upcoming items, those returned by `next()`
*/
public func removeUpcomingItems() {
queue.removeUpcomingItems()
}
/**
Remove all previous items, those returned by `previous()`
*/
public func removePreviousItems() {
queue.removePreviousItems()
}
func replay() {
seek(to: 0);
play()
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidPlayToEndTime() {
event.playbackEnd.emit(data: .playedUntilEnd)
if (repeatMode == .track) {
self.pause()
// quick workaround for race condition - schedule a call after 2 frames
DispatchQueue.main.asyncAfter(deadline: .now() + 0.016 * 2) { [weak self] in self?.replay() }
} else if (repeatMode == .queue) {
_ = queue.next(wrap: true)
} else if (currentIndex != items.count - 1) {
_ = queue.next(wrap: false)
} else {
wrapper.state = .ended
}
}
// MARK: - QueueManagerDelegate
func onCurrentItemChanged() {
let lastPosition = currentTime;
if let currentItem = currentItem {
super.load(item: currentItem)
} else {
super.clear()
}
event.currentItem.emit(
data: (
item: currentItem,
index: currentIndex == -1 ? nil : currentIndex,
lastItem: lastItem,
lastIndex: lastIndex == -1 ? nil : lastIndex,
lastPosition: lastPosition
)
)
lastItem = currentItem
lastIndex = currentIndex
}
func onSkippedToSameCurrentItem() {
if (wrapper.playbackActive) {
replay()
}
}
func onReceivedFirstItem() {
try! queue.jump(to: 0)
}
}
@@ -103,7 +103,7 @@ public struct FeedbackCommand: RemoteCommandProtocol {
}
}
public enum RemoteCommand {
public enum RemoteCommand: CustomStringConvertible {
case play
@@ -128,6 +128,23 @@ public enum RemoteCommand {
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
public var description: String {
switch self {
case .play: return "play"
case .pause: return "pause"
case .stop: return "stop"
case .togglePlayPause: return "togglePlayPause"
case .next: return "nextTrack"
case .previous: return "previousTrack"
case .changePlaybackPosition: return "changePlaybackPosition"
case .skipForward(_): return "skipForward"
case .skipBackward(_): return "skipBackward"
case .like(_, _, _): return "like"
case .dislike(_, _, _): return "dislike"
case .bookmark(_, _, _): return "bookmark"
}
}
/**
All values in an array for convenience.
@@ -19,31 +19,34 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
private var enabledCommands: [RemoteCommand] = []
/**
Create a new RemoteCommandController.
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
*/
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
self.center = remoteCommandCenter
center = remoteCommandCenter
}
internal func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
commands.forEach { (command) in
self.enable(command: command)
let commandsToDisable = enabledCommands.filter { command in
!commands.contains(where: { $0.description == command.description })
}
enabledCommands = commands
commands.forEach { self.enable(command: $0) }
disable(commands: commandsToDisable)
}
internal func disable(commands: [RemoteCommand]) {
commands.forEach { (command) in
self.disable(command: command)
}
commands.forEach { self.disable(command: $0) }
}
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
@@ -92,21 +95,21 @@ public class RemoteCommandController {
// MARK: - Handlers
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
public lazy var handlePlayCommand: RemoteCommandHandler = handlePlayCommandDefault
public lazy var handlePauseCommand: RemoteCommandHandler = handlePauseCommandDefault
public lazy var handleStopCommand: RemoteCommandHandler = handleStopCommandDefault
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = handleTogglePlayPauseCommandDefault
public lazy var handleSkipForwardCommand: RemoteCommandHandler = handleSkipForwardCommandDefault
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = handleSkipBackwardDefault
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = handleBookmarkCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
@@ -114,7 +117,7 @@ public class RemoteCommandController {
}
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
@@ -122,7 +125,7 @@ public class RemoteCommandController {
}
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.stop()
return .success
}
@@ -130,7 +133,7 @@ public class RemoteCommandController {
}
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
@@ -140,7 +143,7 @@ public class RemoteCommandController {
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
@@ -150,7 +153,7 @@ public class RemoteCommandController {
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
@@ -159,7 +162,7 @@ public class RemoteCommandController {
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let event = event as? MPChangePlaybackPositionCommandEvent,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
@@ -167,57 +170,37 @@ public class RemoteCommandController {
}
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.next()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
if let player = audioPlayer as? QueuedAudioPlayer {
player.next()
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.previous()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
if let player = audioPlayer as? QueuedAudioPlayer {
player.previous()
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.LoadError {
switch error {
case .invalidSourceUrl(_):
return MPRemoteCommandHandlerStatus.commandFailed
}
}
else if let error = error as? APError.QueueError {
switch error {
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
return MPRemoteCommandHandlerStatus.noSuchContent
}
}
return MPRemoteCommandHandlerStatus.commandFailed
return error is AudioPlayerError.QueueError
? MPRemoteCommandHandlerStatus.noSuchContent
: MPRemoteCommandHandlerStatus.commandFailed
}
}
+3 -3
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.13.1'
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.
@@ -20,7 +20,7 @@ DESC
'Jørgen Henrichsen' => 'jh.henrichs@gmail.com', }
s.source = { :git => 'https://github.com/DoubleSymmetry/SwiftAudioEx.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudioEx/Classes/**/*'
s.source_files = 'Sources/SwiftAudioEx/**/*'
end
View File
View File
-27
View File
@@ -1,27 +0,0 @@
//
// APError.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 25/03/2018.
//
import Foundation
public struct APError {
enum LoadError: Error {
case invalidSourceUrl(String)
}
enum PlaybackError: Error {
case noLoadedItem
}
enum QueueError: Error {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
}
}
@@ -1,353 +0,0 @@
//
// AVPlayerWrapper.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 06/03/2018.
// Copyright © 2018 Jørgen Henrichsen. All rights reserved.
//
import Foundation
import AVFoundation
import MediaPlayer
public enum PlaybackEndedReason: String {
case playedUntilEnd
case playerStopped
case skippedToNext
case skippedToPrevious
case jumpedToIndex
}
class AVPlayerWrapper: AVPlayerWrapperProtocol {
struct Constants {
static let assetPlayableKey = "playable"
}
// MARK: - Properties
var avPlayer: AVPlayer
let playerObserver: AVPlayerObserver
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
fileprivate var _playWhenReady: Bool = true
fileprivate var _initialTime: TimeInterval?
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
if oldValue != _state {
self.delegate?.AVWrapper(didChangeState: _state)
}
}
}
public init() {
self.avPlayer = AVPlayer()
self.playerObserver = AVPlayerObserver()
self.playerObserver.player = avPlayer
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
self.playerTimeObserver.player = avPlayer
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
self.playerItemObserver = AVPlayerItemObserver()
self.playerObserver.delegate = self
self.playerTimeObserver.delegate = self
self.playerItemNotificationObserver.delegate = self
self.playerItemObserver.delegate = self
// disabled since we're not making use of video playback
self.avPlayer.allowsExternalPlayback = false;
playerTimeObserver.registerForPeriodicTimeEvents()
}
// MARK: - AVPlayerWrapperProtocol
var state: AVPlayerWrapperState {
return _state
}
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
return avPlayer.reasonForWaitingToPlay
}
var currentItem: AVPlayerItem? {
return avPlayer.currentItem
}
var _pendingAsset: AVAsset? = nil
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
var currentTime: TimeInterval {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
}
var duration: TimeInterval {
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
return 0.0
}
var bufferedPosition: TimeInterval {
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
var timeEventFrequency: TimeEventFrequency = .everySecond {
didSet {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var rate: Float {
get { return avPlayer.rate }
set { avPlayer.rate = newValue }
}
var volume: Float {
get { return avPlayer.volume }
set { avPlayer.volume = newValue }
}
var isMuted: Bool {
get { return avPlayer.isMuted }
set { avPlayer.isMuted = newValue }
}
func play() {
_playWhenReady = true
avPlayer.play()
}
func pause() {
_playWhenReady = false
avPlayer.pause()
}
func togglePlaying() {
switch avPlayer.timeControlStatus {
case .playing, .waitingToPlayAtSpecifiedRate:
pause()
case .paused:
play()
@unknown default:
fatalError("Unknown AVPlayer.timeControlStatus")
}
}
func stop() {
pause()
reset(soft: false)
}
func seek(to seconds: TimeInterval) {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
reset(soft: true)
_playWhenReady = playWhenReady
if currentItem?.status == .failed {
recreateAVPlayer()
}
self._pendingAsset = AVURLAsset(url: url, options: options)
if let pendingAsset = _pendingAsset {
self._state = .loading
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
guard let self = self else {
return
}
var error: NSError? = nil
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
DispatchQueue.main.async {
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
switch status {
case .loaded:
if isPendingAsset {
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
currentItem.preferredForwardBufferDuration = self.bufferDuration
self.avPlayer.replaceCurrentItem(with: currentItem)
// Register for events
self.playerTimeObserver.registerForBoundaryTimeEvents()
self.playerObserver.startObserving()
self.playerItemNotificationObserver.startObserving(item: currentItem)
self.playerItemObserver.startObserving(item: currentItem)
for format in pendingAsset.availableMetadataFormats {
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
}
}
break
case .failed:
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
}
break
case .cancelled:
break
default:
break
}
}
})
}
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
// MARK: - Util
private func reset(soft: Bool) {
playerItemObserver.stopObservingCurrentItem()
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
}
/// Will recreate the AVPlayer instance. Used when the current one fails.
private func recreateAVPlayer() {
let player = AVPlayer()
playerObserver.player = player
playerTimeObserver.player = player
playerTimeObserver.registerForPeriodicTimeEvents()
avPlayer = player
delegate?.AVWrapperDidRecreateAVPlayer()
}
}
extension AVPlayerWrapper: AVPlayerObserverDelegate {
// MARK: - AVPlayerObserverDelegate
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
switch status {
case .paused:
if currentItem == nil {
_state = .idle
}
else {
self._state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .buffering
case .playing:
self._state = .playing
@unknown default:
break
}
}
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
else if let initialTime = _initialTime {
self.seek(to: initialTime)
}
break
case .failed:
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
break
case .unknown:
break
@unknown default:
break
}
}
}
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
// MARK: - AVPlayerTimeObserverDelegate
func audioDidStart() {
self._state = .playing
}
func timeEvent(time: CMTime) {
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
}
}
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
delegate?.AVWrapperItemDidPlayToEndTime()
}
}
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
// MARK: - AVPlayerItemObserverDelegate
func item(didUpdateDuration duration: Double) {
self.delegate?.AVWrapper(didUpdateDuration: duration)
}
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
}
}
@@ -1,23 +0,0 @@
//
// AVPlayerWrapperDelegate.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 26/10/2018.
//
import Foundation
import MediaPlayer
protocol AVPlayerWrapperDelegate: class {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
func AVWrapperItemDidPlayToEndTime()
func AVWrapperDidRecreateAVPlayer()
}
-366
View File
@@ -1,366 +0,0 @@
//
// AudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public class AudioPlayer: AVPlayerWrapperDelegate {
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
return _currentItem
}
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
The elapsed playback time of the current item.
*/
public var currentTime: Double {
return wrapper.currentTime
}
/**
The duration of the current AudioItem.
*/
public var duration: Double {
return wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
return wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
public var playerState: AudioPlayerState {
return wrapper.state
}
// MARK: - Setters for AVPlayerWrapper
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { _wrapper.bufferDuration = newValue }
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
public var timeEventFrequency: TimeEventFrequency {
get { return wrapper.timeEventFrequency }
set { _wrapper.timeEventFrequency = newValue }
}
/**
Indicates whether the player should automatically delay playback in order to minimize stalling
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { _wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { _wrapper.isMuted = newValue }
}
private var _rate: Float = 1.0
public var rate: Float {
get { return _rate }
set {
_rate = newValue
// Only set the rate on the wrapper if it is already playing.
if _wrapper.rate > 0 {
_wrapper.rate = newValue
}
}
}
// MARK: - Init
/**
Create a new AudioPlayer.
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self._wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
// MARK: - Player Actions
/**
Load an AudioItem into the manager.
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
let url: URL
switch item.getSourceType() {
case .stream:
if let itemUrl = URL(string: item.getSourceUrl()) {
url = itemUrl
}
else {
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
}
case .file:
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions())
self._currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
}
/**
Toggle playback status.
*/
public func togglePlaying() {
self.wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
self.wrapper.play()
}
/**
Pause playback
*/
public func pause() {
self.wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
public func stop() {
self.reset()
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
}
/**
Seek to a specific time in the item.
*/
public func seek(to seconds: TimeInterval) {
if automaticallyUpdateNowPlayingInfo {
self.updateNowPlayingCurrentTime(seconds)
}
self.wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
self.enableRemoteCommands(item.getCommands())
}
else {
self.enableRemoteCommands(remoteCommands)
}
}
// MARK: - NowPlayingInfo
/**
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
- Album title
- Album artwork
*/
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
loadArtwork(forItem: item)
}
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
private func updateNowPlayingDuration(_ duration: Double) {
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
}
private func updateNowPlayingRate(_ rate: Float) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
}
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
}
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
}
}
}
// MARK: - Private
func reset() {
self._currentItem = nil
}
private func setTimePitchingAlgorithmForCurrentItem() {
if let item = currentItem as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
}
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .ready, .loading:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
setTimePitchingAlgorithmForCurrentItem()
case .playing:
// When a track starts playing, reset the rate to the stored rate
self.rate = _rate;
fallthrough
case .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
default: break
}
self.event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
self.event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.event.fail.emit(data: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
}
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
self.event.receiveMetadata.emit(data: metadata)
}
func AVWrapperItemDidPlayToEndTime() {
self.event.playbackEnd.emit(data: .playedUntilEnd)
}
func AVWrapperDidRecreateAVPlayer() {
self.event.didRecreateAVPlayer.emit(data: ())
}
}
@@ -1,71 +0,0 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private let concurrentInfoQueue: DispatchQueueType
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var infoCenter: NowPlayingInfoCenter {
return _infoCenter
}
var info: [String: Any] {
return _info
}
public required init() {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = MPNowPlayingInfoCenter.default()
}
/// Used for testing purposes.
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = dispatchQueue
self._infoCenter = infoCenter
}
public required init(infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
keyValues.forEach { (keyValue) in
self._info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = self._info
}
}
public func set(keyValue: NowPlayingInfoKeyValue) {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info[keyValue.getKey()] = keyValue.getValue()
self._infoCenter.nowPlayingInfo = self._info
}
}
public func clear() {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info = [:]
self._infoCenter.nowPlayingInfo = self._info
}
}
}
@@ -1,63 +0,0 @@
//
// AVPlayerItemNotificationObserver.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 12/03/2018.
//
import Foundation
import AVFoundation
protocol AVPlayerItemNotificationObserverDelegate: class {
func itemDidPlayToEndTime()
}
/**
Observes notifications posted by an AVPlayerItem.
Currently only listening for the AVPlayerItemDidPlayToEndTime notification.
*/
class AVPlayerItemNotificationObserver {
private let notificationCenter: NotificationCenter = NotificationCenter.default
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemNotificationObserverDelegate?
private(set) var isObserving: Bool = false
deinit {
stopObservingCurrentItem()
}
/**
Will start observing notifications from an item.
- parameter item: The item to observe.
- important: Cannot observe more than one item at a time.
*/
func startObserving(item: AVPlayerItem) {
stopObservingCurrentItem()
observingItem = item
isObserving = true
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
}
/**
Stop receiving notifications for the current item.
*/
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
}
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
self.observingItem = nil
self.isObserving = false
}
@objc private func itemDidPlayToEndTime() {
delegate?.itemDidPlayToEndTime()
}
}
-251
View File
@@ -1,251 +0,0 @@
//
// QueueManager.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
protocol QueueManagerDelegate: AnyObject {
func onReceivedFirstItem()
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int)
}
class QueueManager<T> {
weak var delegate: QueueManagerDelegate? = nil
private var _items: [T] = [] {
didSet {
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
delegate?.onReceivedFirstItem()
}
}
}
/**
All items held by the queue.
*/
public var items: [T] {
return _items
}
public var nextItems: [T] {
guard _currentIndex + 1 < _items.count else {
return []
}
return Array(_items[_currentIndex + 1..<_items.count])
}
public var previousItems: [T] {
if (_currentIndex == 0) {
return []
}
return Array(_items[0..<_currentIndex])
}
private var _currentIndex: Int = 0 {
didSet {
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
}
}
/**
The index of the current item.
Will be populated event though there is no current item (When the queue is empty).
*/
public var currentIndex: Int {
return _currentIndex
}
/**
The current item for the queue.
*/
public var current: T? {
if _items.count > _currentIndex {
return _items[_currentIndex]
}
return nil
}
/**
Add a single item to the queue.
- parameter item: The `AudioItem` to be added.
*/
public func addItem(_ item: T) {
_items.append(item)
}
/**
Add an array of items to the queue.
- parameter items: The `AudioItem`s to be added.
*/
public func addItems(_ items: [T]) {
_items.append(contentsOf: items)
}
/**
Add an array of items to the queue at a given index.
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
public func addItems(_ items: [T], at index: Int) throws {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
}
/**
Get the next item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The next item.
*/
@discardableResult
public func next() throws -> T {
let nextIndex = _currentIndex + 1
guard _items.count > nextIndex else {
throw APError.QueueError.noNextItem
}
_currentIndex = nextIndex
return _items[nextIndex]
}
/**
Get the previous item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The previous item.
*/
@discardableResult
public func previous() throws -> T {
let previousIndex = _currentIndex - 1
guard previousIndex >= 0 else {
throw APError.QueueError.noPreviousItem
}
_currentIndex = previousIndex
return _items[previousIndex]
}
/**
Jump to a position in the queue.
Will update the current item.
- parameter index: The index to jump to.
- throws: `APError.QueueError`
- returns: The item at the index.
*/
@discardableResult
func jump(to index: Int) throws -> T {
guard index != currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(_items.count))")
}
_currentIndex = index
return _items[index]
}
/**
Move an item in the queue.
- parameter fromIndex: The index of the item to be moved.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
guard fromIndex != _currentIndex else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
}
guard fromIndex >= 0 && fromIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
guard toIndex >= 0 && toIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(_items.count)).")
}
let item = try removeItem(at: fromIndex)
try addItems([item], at: toIndex)
}
/**
Remove an item.
- parameter index: The index of the item to remove.
- throws: APError.QueueError
- returns: The removed item.
*/
@discardableResult
public func removeItem(at index: Int) throws -> T {
guard index != _currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be positive and smaller than the count of current items (\(_items.count)).")
}
if index < _currentIndex {
_currentIndex = _currentIndex - 1
}
return _items.remove(at: index)
}
/**
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: T) {
if current == nil {
self.addItem(item)
}
self._items[_currentIndex] = item
}
/**
Remove all previous items in the queue.
If no previous items exist, no action will be taken.
*/
public func removePreviousItems() {
guard currentIndex > 0 else { return }
_items.removeSubrange(0..<_currentIndex)
_currentIndex = 0
}
/**
Remove upcoming items.
If no upcoming items exist, no action will be taken.
*/
public func removeUpcomingItems() {
let nextIndex = _currentIndex + 1
guard nextIndex < _items.count else { return }
_items.removeSubrange(nextIndex..<_items.count)
}
/**
Removes all items for queue
*/
public func clearQueue() {
_currentIndex = 0
_items.removeAll()
}
}
@@ -1,220 +0,0 @@
//
// QueuedAudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
import MediaPlayer
/**
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
let queueManager: QueueManager = QueueManager<AudioItem>()
public init() {
super.init()
queueManager.delegate = self
}
/// The repeat mode for the queue player.
public var repeatMode: RepeatMode = .off
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
}
/**
Stops the player and clears the queue.
*/
public override func stop() {
super.stop()
self.event.queueIndex.emit(data: (currentIndex, nil))
}
override func reset() {
super.reset()
queueManager.clearQueue()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
return queueManager.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem] {
return queueManager.nextItems
}
/**
Will replace the current item with a new one and load it into the player.
- parameter item: The AudioItem to replace the current item.
- throws: APError.LoadError
*/
public override func load(item: AudioItem, playWhenReady: Bool) throws {
try super.load(item: item, playWhenReady: playWhenReady)
queueManager.replaceCurrentItem(with: item)
}
/**
Add a single item to the queue.
- parameter item: The item to add.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
*/
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItem(item)
try self.load(item: item, playWhenReady: playWhenReady)
}
else {
queueManager.addItem(item)
}
}
/**
Add items to the queue.
- parameter items: The items to add to the queue.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the first item in the list. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
*/
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItems(items)
try self.load(item: currentItem!, playWhenReady: playWhenReady)
}
else {
queueManager.addItems(items)
}
}
public func add(items: [AudioItem], at index: Int) throws {
try queueManager.addItems(items, at: index)
}
/**
Step to the next item in the queue.
- throws: `APError`
*/
public func next() throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
public func previous() throws {
event.playbackEnd.emit(data: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
}
/**
Remove an item from the queue.
- parameter index: The index of the item to remove.
- throws: `APError.QueueError`
*/
public func removeItem(at index: Int) throws {
try queueManager.removeItem(at: index)
}
/**
Jump to a certain item in the queue.
- parameter index: The index of the item to jump to.
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
event.playbackEnd.emit(data: .jumpedToIndex)
let item = try queueManager.jump(to: index)
try self.load(item: item, playWhenReady: playWhenReady)
}
/**
Move an item in the queue from one position to another.
- parameter fromIndex: The index of the item to move.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
/**
Remove all upcoming items, those returned by `next()`
*/
public func removeUpcomingItems() {
queueManager.removeUpcomingItems()
}
/**
Remove all previous items, those returned by `previous()`
*/
public func removePreviousItems() {
queueManager.removePreviousItems()
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidPlayToEndTime() {
super.AVWrapperItemDidPlayToEndTime()
switch repeatMode {
case .off: try? self.next()
case .track:
seek(to: 0)
play()
case .queue:
do {
try self.next()
} catch APError.QueueError.noNextItem {
do {
try jumpToItem(atIndex: 0, playWhenReady: true)
} catch { /* TODO: handle possible errors from load */ }
} catch { /* TODO: handle possible errors from load */ }
}
}
// MARK: - QueueManagerDelegate
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
// if _currentItem is nil, then this was triggered by a reset. ignore.
if _currentItem == nil { return }
self.event.queueIndex.emit(data: (oldIndex, newIndex))
}
func onReceivedFirstItem() {
self.event.queueIndex.emit(data: (nil, 0))
}
}
@@ -0,0 +1,32 @@
import XCTest
import AVFoundation
@testable import SwiftAudioEx
class AVPlayerItemNotificationObserverTests: XCTestCase {
var item: AVPlayerItem!
var observer: AVPlayerItemNotificationObserver!
override func setUp() {
super.setUp()
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer = AVPlayerItemNotificationObserver()
}
override func tearDown() {
item = nil
observer = nil
super.tearDown()
}
func testObserverHasObservedItemWhenStartedObserving() {
observer.startObserving(item: item)
XCTAssertNotNil(observer.observingItem)
}
func testObserverHasNoObservedItemWhenEndedObserving() {
observer.startObserving(item: item)
observer.stopObservingCurrentItem()
XCTAssertNil(observer.observingItem)
}
}
@@ -0,0 +1,38 @@
import XCTest
import AVFoundation
@testable import SwiftAudioEx
class AVPlayerItemObserverTests: XCTestCase {
var observer: AVPlayerItemObserver!
override func setUp() {
super.setUp()
observer = AVPlayerItemObserver()
}
override func tearDown() {
observer = nil
super.tearDown()
}
func testObservingItem() {
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer.startObserving(item: item)
XCTAssertNotNil(observer.observingItem)
}
func testIsObserving() {
XCTAssertFalse(observer.isObserving)
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer.startObserving(item: item)
XCTAssertTrue(observer.isObserving)
}
func testObservingInQuickSucccession() {
for _ in 0...1000 {
let item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer.startObserving(item: item)
}
}
}
@@ -0,0 +1,68 @@
import XCTest
import AVFoundation
@testable import SwiftAudioEx
class AVPlayerObserverTests: XCTestCase, AVPlayerObserverDelegate {
var status: AVPlayer.Status?
var timeControlStatus: AVPlayer.TimeControlStatus?
var player: AVPlayer!
var observer: AVPlayerObserver!
override func setUp() {
super.setUp()
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver()
observer.player = player
observer.delegate = self
}
override func tearDown() {
player = nil
observer = nil
super.tearDown()
}
func testObserverIsNotObserving() {
XCTAssertFalse(observer.isObserving)
}
func testObserverIsObservingWhenObservingStarted() {
observer.startObserving()
XCTAssertTrue(observer.isObserving)
}
func testObserverUpdatesDelegateWhenPlayerStarted() {
observer.startObserving()
player.replaceCurrentItem(with: AVPlayerItem(url: URL(fileURLWithPath: Source.path)))
player.play()
XCTAssertNotNil(self.status)
XCTAssertNotNil(self.timeControlStatus)
}
func testObserverIsObservingWhenObservingAgain() {
observer.startObserving()
observer.startObserving()
XCTAssertTrue(observer.isObserving)
}
func testObserverIsNotObservingWhenObservingStopped() {
observer.startObserving()
observer.stopObserving()
XCTAssertFalse(observer.isObserving)
}
// MARK: - AVPlayerObserverDelegate
func player(statusDidChange status: AVPlayer.Status) {
self.status = status
}
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
self.timeControlStatus = status
}
}
@@ -0,0 +1,46 @@
import XCTest
import AVFoundation
@testable import SwiftAudioEx
class AVPlayerTimeObserverTests: XCTestCase {
var player: AVPlayer!
var observer: AVPlayerTimeObserver!
override func setUp() {
super.setUp()
player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
observer.player = player
}
override func tearDown() {
player = nil
observer = nil
super.tearDown()
}
func testObserverHasBoundaryTokenWhenStartedBoundaryTimeObserving() {
observer.registerForBoundaryTimeEvents()
XCTAssertNotNil(observer.boundaryTimeStartObserverToken)
}
func testObserverHasNoBoundaryTokenWhenEndedBoundaryTimeObserving() {
observer.registerForBoundaryTimeEvents()
observer.unregisterForBoundaryTimeEvents()
XCTAssertNil(observer.boundaryTimeStartObserverToken)
}
func testObserverHasPeriodicTokenWhenStartedPeriodicTimeObserving() {
observer.registerForPeriodicTimeEvents()
XCTAssertNotNil(observer.periodicTimeObserverToken)
}
func testObserverHasNoPeriodicTokenWhenEndedPeriodicTimeObserving() {
observer.registerForPeriodicTimeEvents()
observer.unregisterForPeriodicEvents()
XCTAssertNil(observer.periodicTimeObserverToken)
}
}
@@ -0,0 +1,293 @@
import AVFoundation
import XCTest
@testable import SwiftAudioEx
class AVPlayerWrapperTests: XCTestCase {
var wrapper: AVPlayerWrapper!
var holder: AVPlayerWrapperDelegateHolder!
override func setUp() {
super.setUp()
wrapper = AVPlayerWrapper()
wrapper.volume = 0.0
wrapper.automaticallyWaitsToMinimizeStalling = false
holder = AVPlayerWrapperDelegateHolder()
wrapper.delegate = holder
}
override func tearDown() {
wrapper = nil
holder = nil
super.tearDown()
}
// MARK: - State tests
func testAVPlayerWrapperStateShouldBeIdle() {
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.idle)
}
func testAVPlayerWrapperStateWhenLoadingSourceShouldBeLoading() {
wrapper.load(from: Source.url, playWhenReady: false)
XCTAssertEqual(wrapper.state, AVPlayerWrapperState.loading)
}
func testAVPlayerWrapperStateWhenLoadingSourceShouldEventuallyBeReady() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .ready {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenPlayingSourceShouldBePlaying() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if state == .playing {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenPausingSourceShouldBePaused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing:
self.wrapper.pause()
case .paused:
expectation.fulfill()
default:
break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenTogglingFromPlayShouldBePaused() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing:
self.wrapper.togglePlaying()
case .paused:
expectation.fulfill()
default:
break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenStoppingShouldBeStopped() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing:
self.wrapper.stop()
case .stopped:
expectation.fulfill()
default:
break
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateLoadingWithInitialTimeShouldBePlaying() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
switch state {
case .playing:
expectation.fulfill()
default:
break
}
}
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
wait(for: [expectation], timeout: defaultTimeout)
}
// MARK: - Duration tests
func testAVPlayerWrapperDurationShouldBeZero() {
XCTAssertEqual(wrapper.duration, 0.0)
}
func testAVPlayerWrapperDurationLoadingSourceShouldNotBeZero() {
let expectation = XCTestExpectation()
holder.stateUpdate = { _ in
if self.wrapper.duration > 0 {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: defaultTimeout)
}
// MARK: - Current time tests
func testAVPlayerWrapperCurrentTimeShouldBeZero() {
XCTAssertEqual(wrapper.currentTime, 0)
}
// MARK: - Seeking
func testAVPlayerWrapperSeekingShouldSeek() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
self.wrapper.seek(to: seekTime)
}
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperSeekingShouldSeekWhileNotYetLoaded() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperSeekByShouldSeek() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
self.wrapper.seek(by: seekTime)
}
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: defaultTimeout)
}
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: defaultTimeout)
}
// MARK: - Rate tests
func testAVPlayerWrapperRateShouldBe1() {
XCTAssertEqual(wrapper.rate, 1)
}
func testAVPlayerWrapperRatePlayingSourceShouldBe1() {
let expectation = XCTestExpectation()
holder.stateUpdate = { state in
if self.wrapper.rate == 1.0 {
expectation.fulfill()
}
}
wrapper.load(from: Source.url, playWhenReady: true)
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 {
private let lockQueue = DispatchQueue(
label: "AVPlayerWrapperDelegateHolder.lockQueue",
target: .global()
)
func AVWrapperItemPlaybackStalled() {
}
func AVWrapperItemFailedToPlayToEndTime() {
}
func AVWrapper(didChangePlayWhenReady playWhenReady: Bool) {
}
func AVWrapper(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
}
func AVWrapper(didReceiveCommonMetadata metadata: [AVMetadataItem]) {
}
func AVWrapper(didReceiveChapterMetadata metadata: [AVTimedMetadataGroup]) {
}
func AVWrapperDidRecreateAVPlayer() {
}
func AVWrapperItemDidPlayToEndTime() {
}
private var _state: AVPlayerWrapperState? = nil
var state: AVPlayerWrapperState? {
get {
return lockQueue.sync {
return _state
}
}
set {
lockQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
if let newValue = newValue {
let changed = self._state != newValue;
if (changed) {
self._state = newValue
self.stateUpdate?(newValue)
}
}
}
}
}
var stateUpdate: ((_ state: AVPlayerWrapperState) -> Void)?
var didUpdateDuration: ((_ duration: Double) -> Void)?
var didSeekTo: ((_ seconds: 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)
}
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
@@ -9,7 +9,9 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
#if os(iOS)
@testable import SwiftAudioEx
class NonFailingAudioSession: AudioSession {
@@ -64,3 +66,5 @@ class FailingAudioSession: AudioSession {
}
#endif
@@ -8,7 +8,7 @@
import Foundation
@testable import SwiftAudio
@testable import SwiftAudioEx
final class MockDispatchQueue: DispatchQueueType {
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
@@ -9,10 +9,9 @@
import Foundation
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
var info: [String: Any] = [:]
required public init() {
@@ -30,6 +29,12 @@ class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
public func set(keyValue: NowPlayingInfoKeyValue) {
info[keyValue.getKey()] = keyValue.getValue()
}
func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
keyValues.forEach { (keyValue) in
info[keyValue.getKey()] = keyValue.getValue()
}
}
func getTitle() -> String? {
return info[MediaItemProperty.title(nil).getKey()] as? String
@@ -0,0 +1,48 @@
import XCTest
import MediaPlayer
@testable import SwiftAudioEx
class NowPlayingInfoControllerTests: XCTestCase {
var nowPlayingController: NowPlayingInfoController!
var infoCenter: NowPlayingInfoCenter_Mock!
override func setUp() {
super.setUp()
infoCenter = NowPlayingInfoCenter_Mock()
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: infoCenter)
}
override func tearDown() {
infoCenter = nil
nowPlayingController = nil
super.tearDown()
}
func testInfoDictionaryNotEmpty() {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
XCTAssertGreaterThan(nowPlayingController.info.count, 0)
}
func testInfoDictionaryEmptyAfterClear() {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
nowPlayingController.clear()
XCTAssertEqual(nowPlayingController.info.count, 0)
}
func testInfoCenterNotNil() {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
XCTAssertNotNil(nowPlayingController.infoCenter.nowPlayingInfo)
}
func testInfoCenterNotEmpty() {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
XCTAssertGreaterThan(nowPlayingController.infoCenter.nowPlayingInfo?.count ?? 0, 0)
}
func testInfoCenterEmptyAfterClear() {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
nowPlayingController.clear()
XCTAssertNil(nowPlayingController.infoCenter.nowPlayingInfo)
}
}
@@ -0,0 +1,42 @@
import XCTest
import MediaPlayer
@testable import SwiftAudioEx
class NowPlayingInfoTests: XCTestCase {
var audioPlayer: AudioPlayer!
var nowPlayingController: NowPlayingInfoController_Mock!
override func setUp() {
super.setUp()
nowPlayingController = NowPlayingInfoController_Mock()
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
audioPlayer.automaticallyUpdateNowPlayingInfo = true
audioPlayer.volume = 0
}
override func tearDown() {
audioPlayer = nil
nowPlayingController = nil
super.tearDown()
}
func testNowPlayingInfoControllerMetadataUpdate() {
let item = Source.getAudioItem()
audioPlayer.load(item: item, playWhenReady: false)
XCTAssertEqual(nowPlayingController.getTitle(), item.getTitle())
XCTAssertEqual(nowPlayingController.getArtist(), item.getArtist())
XCTAssertEqual(nowPlayingController.getAlbumTitle(), item.getAlbumTitle())
XCTAssertNotNil(nowPlayingController.getArtwork())
}
func testNowPlayingInfoControllerPlaybackValuesUpdate() {
let item = LongSource.getAudioItem()
audioPlayer.load(item: item, playWhenReady: true)
XCTAssertNotNil(nowPlayingController.getRate())
XCTAssertNotNil(nowPlayingController.getDuration())
XCTAssertNotNil(nowPlayingController.getCurrentTime())
}
}

Some files were not shown because too many files have changed in this diff Show More