Compare commits

...

22 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
48 changed files with 1106 additions and 1344 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
+15 -12
View File
@@ -1,26 +1,29 @@
name: validate
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
unit-tests:
runs-on: macos-latest
runs-on: blaze/macos-14
strategy:
matrix:
destination:
[
'platform=iOS Simulator,name=iPhone 12 Pro',
]
target: [macos]
include:
- target: macos
destination: '-destination "platform=macOS,name=Any Mac"'
steps:
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Run Tests
run: |-
xcodebuild test -scheme SwiftAudioEx -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 }}
+168 -156
View File
@@ -3,99 +3,92 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713062067EB4F00F789B3 /* Double + Extensions.swift */; };
070713092067EFFB00F789B3 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713082067EFFB00F789B3 /* AudioController.swift */; };
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; };
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; };
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; };
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 */; };
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
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 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>"; };
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>"; };
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
9B8819592BC8657A00E20DCE /* SwiftAudio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio.app; sourceTree = BUILT_PRODUCTS_DIR; };
9B88195C2BC8657A00E20DCE /* SwiftAudioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftAudioApp.swift; sourceTree = "<group>"; };
9B8819602BC8657B00E20DCE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9B8819622BC8657B00E20DCE /* SwiftAudio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftAudio.entitlements; sourceTree = "<group>"; };
9B8819642BC8657B00E20DCE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
9B88196B2BC865E100E20DCE /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
9B88196C2BC866A300E20DCE /* AudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
9B88196F2BC866A300E20DCE /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
9B8819702BC866A300E20DCE /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
9B8819792BC9883200E20DCE /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = "<group>"; };
9B88197B2BC98F5000E20DCE /* QueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
607FACCD1AFB9204008FA782 /* Frameworks */ = {
9B8819562BC8657A00E20DCE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
9B8819782BC866E800E20DCE /* SwiftAudioEx in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
607FACC71AFB9204008FA782 = {
9B8819502BC8657A00E20DCE = {
isa = PBXGroup;
children = (
607FACD21AFB9204008FA782 /* Example for SwiftAudio */,
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 */,
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>";
};
9B05AA2F2660276400C7A389 /* Frameworks */ = {
9B8819762BC866E800E20DCE /* Frameworks */ = {
isa = PBXGroup;
children = (
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -103,131 +96,106 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
607FACCF1AFB9204008FA782 /* SwiftAudio_Example */ = {
9B8819582BC8657A00E20DCE /* SwiftAudio */ = {
isa = PBXNativeTarget;
buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftAudio_Example" */;
buildConfigurationList = 9B8819682BC8657B00E20DCE /* Build configuration list for PBXNativeTarget "SwiftAudio" */;
buildPhases = (
607FACCC1AFB9204008FA782 /* Sources */,
607FACCD1AFB9204008FA782 /* Frameworks */,
607FACCE1AFB9204008FA782 /* Resources */,
9B8819552BC8657A00E20DCE /* Sources */,
9B8819562BC8657A00E20DCE /* Frameworks */,
9B8819572BC8657A00E20DCE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SwiftAudio_Example;
name = SwiftAudio;
packageProductDependencies = (
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
9B8819772BC866E800E20DCE /* SwiftAudioEx */,
);
productName = SwiftAudio;
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
productReference = 9B8819592BC8657A00E20DCE /* SwiftAudio.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
607FACC81AFB9204008FA782 /* Project object */ = {
9B8819512BC8657A00E20DCE /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = CocoaPods;
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1530;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
};
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 = (
);
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
mainGroup = 9B8819502BC8657A00E20DCE;
productRefGroup = 9B88195A2BC8657A00E20DCE /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
607FACCF1AFB9204008FA782 /* SwiftAudio_Example */,
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 */,
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets 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 */,
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */,
070713092067EFFB00F789B3 /* AudioController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.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 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;
@@ -236,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;
@@ -254,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;
@@ -291,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;
@@ -309,70 +285,106 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
607FACF01AFB9204008FA782 /* Debug */ = {
9B8819692BC8657B00E20DCE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SwiftAudio/SwiftAudio.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftAudio/Preview Content\"";
DEVELOPMENT_TEAM = 7U2TUNKNQX;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.doublesymmetry.SwiftAudio;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
607FACF11AFB9204008FA782 /* Release */ = {
9B88196A2BC8657B00E20DCE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SwiftAudio/SwiftAudio.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftAudio/Preview Content\"";
DEVELOPMENT_TEAM = 7U2TUNKNQX;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "com.doublesymmetry.demo.--PRODUCT-NAME-rfc1034identifier-";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.doublesymmetry.SwiftAudio;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
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 */,
9B8819692BC8657B00E20DCE /* Debug */,
9B88196A2BC8657B00E20DCE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
@@ -380,11 +392,11 @@
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
9B8819772BC866E800E20DCE /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudioEx;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
rootObject = 9B8819512BC8657A00E20DCE /* Project object */;
}
@@ -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

+3 -5
View File
@@ -9,20 +9,18 @@
import Foundation
import SwiftAudioEx
class AudioController {
static let shared = AudioController()
let player: QueuedAudioPlayer
let audioSessionController = AudioSessionController.shared
let sources: [AudioItem] = [
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Longing.mp3", artist: "David Chavez", title: "Longing", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Soul%20Searching.mp3", artist: "David Chavez", title: "Soul Searching (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/Rhythm%20City%20(Demo).mp3", artist: "David Chavez", title: "Rhythm City (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://rntp.dev/example/hls/whip/playlist.m3u8", title: "Whip", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")),
DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
]
@@ -38,7 +36,7 @@ class AudioController {
.previous,
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
player.repeatMode = .queue
DispatchQueue.main.async {
self.player.add(items: self.sources)
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 CocoaPods. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SwiftAudio" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>
@@ -1,208 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="ufC-wZ-h7g">
<objects>
<viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="RX3-VR-CL6">
<rect key="frame" x="32" y="533" width="311" height="34"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Q1-U9-TUC">
<rect key="frame" x="0.0" y="0.0" width="103.5" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="Prev"/>
<connections>
<action selector="previous:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fFb-iW-sFr"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
<rect key="frame" x="103.5" y="0.0" width="104" height="34"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<state key="normal" title="Play"/>
<connections>
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nhf-qB-91A">
<rect key="frame" x="207.5" y="0.0" width="103.5" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="Next"/>
<connections>
<action selector="next:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Tha-3J-gVM"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="34" id="T4q-HG-vqM"/>
</constraints>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="l9B-hM-Ajc">
<rect key="frame" x="302" y="20" width="57" height="34"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
<state key="normal" title="Queue"/>
<connections>
<segue destination="vDz-qW-uY8" kind="presentation" identifier="QueueSegue" id="eke-1c-Fsm"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
<rect key="frame" x="67.5" y="84" width="240" height="240"/>
<constraints>
<constraint firstAttribute="width" constant="240" id="5Sj-BZ-sg4"/>
<constraint firstAttribute="height" constant="240" id="Hij-Yw-6Lg"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3CL-8o-zYW">
<rect key="frame" x="16" y="462" width="39" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RVb-HZ-QCX">
<rect key="frame" x="320" y="462" width="39" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
<rect key="frame" x="14" y="424" width="347" height="31"/>
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="maximumTrackTintColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="thumbTintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="HeH-aB-VXZ"/>
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="NfP-3T-dnw"/>
<action selector="scrubbingValueChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="MLD-nW-rXm"/>
<action selector="startScrubbing:" destination="vXZ-lx-hvc" eventType="touchDown" id="lD9-dR-QTO"/>
</connections>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dfk-yr-rwm">
<rect key="frame" x="16" y="354" width="343" height="21.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T7Y-1Q-7UU">
<rect key="frame" x="16" y="379.5" width="343" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="16"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="1ML-yD-9Rf">
<rect key="frame" x="177.5" y="587" width="20" height="20"/>
</activityIndicatorView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ErrorText" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iCe-6A-2My">
<rect key="frame" x="158.5" y="588.5" width="58.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="0eh-sL-186"/>
<constraint firstItem="iCe-6A-2My" firstAttribute="centerY" secondItem="1ML-yD-9Rf" secondAttribute="centerY" id="4Fp-kE-AAg"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="54L-0h-0ba"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="9Uh-K9-988"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="BhV-UD-qhh"/>
<constraint firstItem="iCe-6A-2My" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="Dhm-Bn-wZH"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="GhI-f1-DkR"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="HoH-i0-yof"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="Nw7-WM-LFd"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="O0h-NL-iXW"/>
<constraint firstItem="1ML-yD-9Rf" firstAttribute="top" secondItem="EOo-zV-6l2" secondAttribute="bottom" constant="20" id="Uop-aD-I5b"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="top" secondItem="FCd-3e-22D" secondAttribute="bottom" constant="30" id="W4w-6K-AW8"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="top" secondItem="T7Y-1Q-7UU" secondAttribute="bottom" constant="25" id="XgV-XL-QCL"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="YUE-uf-Rp1"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="ZkD-u2-Zbr"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="top" secondItem="dfk-yr-rwm" secondAttribute="bottom" constant="4" id="baR-zV-tgo"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="eNt-u9-qot"/>
<constraint firstItem="1ML-yD-9Rf" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="fdl-RK-Hq8"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="16" id="hEd-b2-Ggo"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="top" secondItem="l9B-hM-Ajc" secondAttribute="bottom" constant="30" id="ikz-ZP-jNM"/>
<constraint firstAttribute="trailingMargin" secondItem="RX3-VR-CL6" secondAttribute="trailing" constant="16" id="kSP-Mq-R5P"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="m6u-7a-ffF"/>
<constraint firstItem="3CL-8o-zYW" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="sGK-bn-zxD"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="RX3-VR-CL6" secondAttribute="bottom" constant="100" id="vd2-dd-hVu"/>
<constraint firstItem="3CL-8o-zYW" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="wOy-Rx-rvK"/>
</constraints>
</view>
<connections>
<outlet property="artistLabel" destination="T7Y-1Q-7UU" id="b5S-lt-PqG"/>
<outlet property="elapsedTimeLabel" destination="3CL-8o-zYW" id="7Wg-7X-Vrd"/>
<outlet property="errorLabel" destination="iCe-6A-2My" id="T4b-0b-wdM"/>
<outlet property="imageView" destination="FCd-3e-22D" id="gKL-za-haV"/>
<outlet property="loadIndicator" destination="1ML-yD-9Rf" id="Xes-Ag-vhg"/>
<outlet property="playButton" destination="EOo-zV-6l2" id="2d1-ad-s1k"/>
<outlet property="remainingTimeLabel" destination="RVb-HZ-QCX" id="8hp-CK-XjF"/>
<outlet property="slider" destination="RWN-If-dGG" id="Yxw-Gf-bR3"/>
<outlet property="titleLabel" destination="dfk-yr-rwm" id="Hk3-m5-IOi"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="117.59999999999999" y="118.29085457271366"/>
</scene>
<!--Queue View Controller-->
<scene sceneID="5Fm-oE-9Zc">
<objects>
<viewController id="vDz-qW-uY8" customClass="QueueViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="kv3-s6-lb0"/>
<viewControllerLayoutGuide type="bottom" id="Fhe-7w-8BG"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="y7Y-Gm-oyZ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dzA-9p-ejh">
<rect key="frame" x="310" y="20" width="49" height="34"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeButton:" destination="vDz-qW-uY8" eventType="touchUpInside" id="0TB-bG-he7"/>
</connections>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HPi-Pd-J9K">
<rect key="frame" x="0.0" y="74" width="375" height="593"/>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
</subviews>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="1" green="0.1857388616" blue="0.57339501380000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="HPi-Pd-J9K" secondAttribute="trailing" id="CdI-lT-19N"/>
<constraint firstItem="Fhe-7w-8BG" firstAttribute="top" secondItem="HPi-Pd-J9K" secondAttribute="bottom" id="Gb9-C1-ajx"/>
<constraint firstItem="HPi-Pd-J9K" firstAttribute="leading" secondItem="y7Y-Gm-oyZ" secondAttribute="leading" id="aN2-LD-yxR"/>
<constraint firstItem="HPi-Pd-J9K" firstAttribute="top" secondItem="dzA-9p-ejh" secondAttribute="bottom" constant="20" id="aSx-t1-T3e"/>
<constraint firstItem="dzA-9p-ejh" firstAttribute="top" secondItem="kv3-s6-lb0" secondAttribute="bottom" id="nAL-i2-VQS"/>
<constraint firstAttribute="trailing" secondItem="dzA-9p-ejh" secondAttribute="trailing" constant="16" id="qrg-S3-JJ2"/>
</constraints>
</view>
<connections>
<outlet property="tableView" destination="HPi-Pd-J9K" id="P8P-at-xLc"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zk4-9r-5Oh" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="917.60000000000002" y="117.39130434782609"/>
</scene>
</scenes>
</document>
@@ -1,15 +1,13 @@
//
// Double + Extensions.swift
// SwiftAudio_Example
// Extensions.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 25/03/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
// Created by Brandon Sneed on 3/30/24.
//
import Foundation
extension Double {
private var formatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
@@ -21,5 +19,4 @@ extension Double {
func secondsToString() -> String {
return formatter.string(from: self) ?? ""
}
}
@@ -1,53 +0,0 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
-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 SwiftAudioEx
class QueueViewController: UIViewController {
let controller = AudioController.shared
@IBOutlet weak var tableView: UITableView!
let cellReuseId: String = "QueueCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib.init(nibName: "QueueTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: cellReuseId)
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@IBAction func closeButton(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return 1
case 1:
return controller.player.nextItems.count
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) as! QueueTableViewCell
let item: AudioItem?
switch indexPath.section {
case 0:
item = controller.player.currentItem
case 1:
item = controller.player.nextItems[indexPath.row]
default:
item = nil
}
if let item = item {
cell.titleLabel.text = item.getTitle()
cell.artistLabel.text = item.getArtist()
}
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "Playing Now"
case 1: return "Up Next"
default: return nil
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+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()
}
}
}
-169
View File
@@ -1,169 +0,0 @@
//
// ViewController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 03/11/2018.
// Copyright (c) 2018 Jørgen Henrichsen. All rights reserved.
//
import UIKit
import SwiftAudioEx
import AVFoundation
import MediaPlayer
class ViewController: UIViewController {
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var remainingTimeLabel: UILabel!
@IBOutlet weak var elapsedTimeLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var artistLabel: UILabel!
@IBOutlet weak var loadIndicator: UIActivityIndicatorView!
@IBOutlet weak var errorLabel: UILabel!
private var isScrubbing: Bool = false
private let controller = AudioController.shared
override func viewDidLoad() {
super.viewDidLoad()
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
handleAudioPlayerStateChange(data: controller.player.playerState)
DispatchQueue.main.async {
self.render()
}
}
// MARK: - Actions
@IBAction func togglePlay(_ sender: Any) {
if !controller.audioSessionController.audioSessionIsActive {
try? controller.audioSessionController.activateSession()
}
controller.player.playWhenReady = playButton.currentTitle == "Play"
}
@IBAction func previous(_ sender: Any) {
controller.player.previous()
}
@IBAction func next(_ sender: Any) {
controller.player.next()
}
@IBAction func startScrubbing(_ sender: UISlider) {
isScrubbing = true
}
@IBAction func scrubbing(_ sender: UISlider) {
controller.player.seek(to: Double(slider.value))
}
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
let value = Double(slider.value)
elapsedTimeLabel.text = value.secondsToString()
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
}
// MARK: - Render
func renderTimeValues() {
self.slider.maximumValue = Float(self.controller.player.duration)
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
}
func render() {
let player = self.controller.player
// Render play button
self.playButton.setTitle(
!player.playWhenReady || player.playerState == .failed
? "Play"
: "Pause",
for: .normal
)
// Render metadata
if let item = player.currentItem {
self.titleLabel.text = item.getTitle()
self.artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
// Render time values
self.renderTimeValues()
// Render error label
if (player.playerState == .failed) {
self.errorLabel.isHidden = false
self.errorLabel.text = "Playback failed."
} else {
self.errorLabel.text = ""
self.errorLabel.isHidden = true
}
// Render load indicator:
if (
(player.playerState == .loading || player.playerState == .buffering)
&& self.controller.player.playWhenReady // Avoid showing indicator before user has pressed play
) {
self.loadIndicator.startAnimating()
} else {
self.loadIndicator.stopAnimating()
}
}
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print("state=\(data)")
DispatchQueue.main.async {
self.render()
}
}
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
print("playWhenReady=\(data)")
DispatchQueue.main.async {
self.render()
}
}
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
print("playEndReason=\(data)")
}
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
DispatchQueue.main.async {
self.renderTimeValues()
}
}
}
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
isScrubbing = false
}
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
DispatchQueue.main.async {
self.renderTimeValues()
}
}
func handleAVPlayerRecreated() {
try? controller.audioSessionController.set(category: .playback)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import PackageDescription
let package = Package(
name: "SwiftAudioEx",
platforms: [.iOS(.v11)],
platforms: [.iOS(.v11), .macOS(.v11)],
products: [
.library(
name: "SwiftAudioEx",
+48
View File
@@ -2,11 +2,26 @@
# SwiftAudioEx
[![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)
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!
@@ -16,11 +31,13 @@ XCode project navigator and Build/Run it in a simulator (or on an actual
device).
## Requirements
iOS 11.0+
## Installation
### Swift Package Manager
[Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode.
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.
@@ -40,6 +57,7 @@ let package = Package(
```
### CocoaPods
SwiftAudioEx is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
@@ -48,16 +66,21 @@ pod 'SwiftAudioEx', '~> 1.0.0'
```
### Carthage
SwiftAudioEx supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
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)
@@ -66,6 +89,7 @@ player.load(item: audioItem, playWhenReady: true) // Load the item and start pla
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
To subscribe to an event:
```swift
class MyCustomViewController: UIViewController {
@@ -83,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)
@@ -93,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.
@@ -101,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.
@@ -117,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)
//...
@@ -133,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,
@@ -169,19 +212,24 @@ 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
@@ -245,7 +245,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
if (pendingAsset != self.asset) { return; }
let commonData = pendingAsset.commonMetadata
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
if (!commonData.isEmpty) {
self.delegate?.AVWrapper(didReceiveCommonMetadata: commonData)
}
if pendingAsset.availableChapterLocales.count > 0 {
for locale in pendingAsset.availableChapterLocales {
+19 -14
View File
@@ -7,7 +7,14 @@
import Foundation
import AVFoundation
#if os(iOS)
import UIKit
public typealias AudioItemImage = UIImage
#elseif os(macOS)
import AppKit
public typealias AudioItemImage = NSImage
#endif
public enum SourceType {
case stream
@@ -15,19 +22,17 @@ public enum SourceType {
}
public protocol AudioItem {
func getSourceUrl() -> String
func getArtist() -> String?
func getTitle() -> String?
func getAlbumTitle() -> String?
func getSourceType() -> SourceType
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void)
}
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
public protocol TimePitching {
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
}
@@ -42,8 +47,8 @@ public protocol AssetOptionsProviding {
func getAssetOptions() -> [String: Any]
}
public class DefaultAudioItem: AudioItem {
public class DefaultAudioItem: AudioItem, Identifiable {
public var audioUrl: String
public var artist: String?
@@ -54,9 +59,9 @@ public class DefaultAudioItem: AudioItem {
public var sourceType: SourceType
public var artwork: UIImage?
public var artwork: AudioItemImage?
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: AudioItemImage? = nil) {
self.audioUrl = audioUrl
self.artist = artist
self.title = title
@@ -85,7 +90,7 @@ public class DefaultAudioItem: AudioItem {
sourceType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
public func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) {
handler(artwork)
}
@@ -96,12 +101,12 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
@@ -116,12 +121,12 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
initialTime = 0.0
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) {
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, initialTime: TimeInterval) {
self.initialTime = initialTime
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
@@ -137,12 +142,12 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
public var options: [String: Any]
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) {
options = [:]
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, options: [String: Any]) {
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, options: [String: Any]) {
self.options = options
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
+49 -24
View File
@@ -42,6 +42,33 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
}
/**
Handles the `playWhenReady` setting while executing a given action.
This method takes an optional `Bool` value and a closure representing an action to execute.
If the `Bool` value is not `nil`, `self.playWhenReady` is set accordingly either before or
after executing the action.
- Parameters:
- playWhenReady: Optional `Bool` to set `self.playWhenReady`.
- If `true`, `self.playWhenReady` will be set after executing the action.
- If `false`, `self.playWhenReady` will be set before executing the action.
- If `nil`, `self.playWhenReady` will not be changed.
- action: A closure representing the action to execute. This closure can throw an error.
- Throws: This function will propagate any errors thrown by the `action` closure.
*/
internal func handlePlayWhenReady(_ playWhenReady: Bool?, action: () throws -> Void) rethrows {
if playWhenReady == false {
self.playWhenReady = false
}
try action()
if playWhenReady == true, playbackError == nil {
self.playWhenReady = true
}
}
// MARK: - Getters from AVPlayerWrapper
@@ -170,32 +197,30 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func load(item: AudioItem, playWhenReady: Bool? = nil) {
currentItem = item
handlePlayWhenReady(playWhenReady) {
currentItem = item
if let playWhenReady = playWhenReady {
self.playWhenReady = playWhenReady
if (automaticallyUpdateNowPlayingInfo) {
// Reset playback values without updating, because that will happen in
// the loadNowPlayingMetaValues call straight after:
nowPlayingInfoController.setWithoutUpdate(keyValues: [
MediaItemProperty.duration(nil),
NowPlayingInfoProperty.playbackRate(nil),
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
])
loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
wrapper.load(
from: item.getSourceUrl(),
type: item.getSourceType(),
playWhenReady: self.playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions()
)
}
if (automaticallyUpdateNowPlayingInfo) {
// Reset playback values without updating, because that will happen in
// the loadNowPlayingMetaValues call straight after:
nowPlayingInfoController.setWithoutUpdate(keyValues: [
MediaItemProperty.duration(nil),
NowPlayingInfoProperty.playbackRate(nil),
NowPlayingInfoProperty.elapsedPlaybackTime(nil)
])
loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
wrapper.load(
from: item.getSourceUrl(),
type: item.getSourceType(),
playWhenReady: self.playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions()
)
}
/**
@@ -8,7 +8,7 @@
import Foundation
import AVFoundation
#if os(iOS)
protocol AudioSession {
var isOtherAudioPlaying: Bool { get }
@@ -30,3 +30,4 @@ protocol AudioSession {
}
extension AVAudioSession: AudioSession {}
#endif
@@ -13,6 +13,8 @@ public enum InterruptionType: Equatable {
case ended(shouldResume: Bool)
}
#if os(iOS)
public protocol AudioSessionControllerDelegate: AnyObject {
func handleInterruption(type: InterruptionType)
}
@@ -129,3 +131,5 @@ public class AudioSessionController {
}
}
#endif
@@ -14,7 +14,7 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
Called when the duration of the observed item is updated.
*/
func item(didUpdateDuration duration: Double)
/**
Called when the playback of the observed item is or is no longer likely to keep up.
*/
@@ -32,7 +32,7 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
class AVPlayerItemObserver: NSObject {
private static var context = 0
private let metadataOutput = AVPlayerItemMetadataOutput()
private var currentMetadataOutput: AVPlayerItemMetadataOutput?
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
@@ -47,7 +47,6 @@ class AVPlayerItemObserver: NSObject {
override init() {
super.init()
metadataOutput.setDelegate(self, queue: .main)
}
deinit {
@@ -68,15 +67,13 @@ class AVPlayerItemObserver: NSObject {
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context)
// We must slightly delay adding the metadata output due to the fact that
// stop observation is not a synchronous action and metadataOutput may not
// be removed from last item before we try to attach it to a new one.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in
guard let `self` = self else { return }
item.add(self.metadataOutput)
}
// 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
@@ -85,10 +82,13 @@ class AVPlayerItemObserver: NSObject {
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context)
observingItem.remove(metadataOutput)
// Remove all metadata outputs from the item.
observingItem.removeAllMetadataOutputs()
isObserving = false
self.observingItem = nil
self.currentMetadataOutput = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@@ -102,17 +102,17 @@ class AVPlayerItemObserver: NSObject {
if let duration = change?[.newKey] as? CMTime {
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.loadedTimeRanges:
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.playbackLikelyToKeepUp:
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
}
default: break
}
@@ -121,6 +121,16 @@ class AVPlayerItemObserver: NSObject {
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
delegate?.item(didReceiveTimedMetadata: groups)
if output == currentMetadataOutput {
delegate?.item(didReceiveTimedMetadata: groups)
}
}
}
extension AVPlayerItem {
func removeAllMetadataOutputs() {
for output in self.outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
self.remove(output)
}
}
}
+17 -18
View File
@@ -13,7 +13,7 @@ protocol QueueManagerDelegate: AnyObject {
func onSkippedToSameCurrentItem()
}
class QueueManager<T> {
class QueueManager<Element> {
fileprivate let recursiveLock = NSRecursiveLock()
@@ -54,7 +54,7 @@ class QueueManager<T> {
/**
All items held by the queue.
*/
private(set) var items: [T] = [] {
private(set) var items: [Element] = [] {
didSet {
return synchronize {
if oldValue.count == 0 && items.count > 0 {
@@ -64,7 +64,7 @@ class QueueManager<T> {
}
}
public var nextItems: [T] {
public var nextItems: [Element] {
return synchronize {
return currentIndex == -1 || currentIndex == items.count - 1
? []
@@ -72,7 +72,7 @@ class QueueManager<T> {
}
}
public var previousItems: [T] {
public var previousItems: [Element] {
return synchronize {
return currentIndex <= 0
? []
@@ -83,7 +83,7 @@ class QueueManager<T> {
/**
The current item for the queue.
*/
public var current: T? {
public var current: Element? {
return synchronize {
return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil
}
@@ -114,7 +114,7 @@ class QueueManager<T> {
- parameter item: The `AudioItem` to be added.
*/
public func add(_ item: T) {
public func add(_ item: Element) {
synchronize {
items.append(item)
}
@@ -125,7 +125,7 @@ class QueueManager<T> {
- parameter items: The `AudioItem`s to be added.
*/
public func add(_ items: [T]) {
public func add(_ items: [Element]) {
synchronize {
if (items.count == 0) { return }
self.items.append(contentsOf: items)
@@ -138,14 +138,14 @@ class QueueManager<T> {
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
public func add(_ items: [T], at index: Int) throws {
public func add(_ items: [Element], at index: Int) throws {
try synchronizeThrows {
if (items.count == 0) { return }
guard index >= 0 && self.items.count >= index else {
throw AudioPlayerError.QueueError.invalidIndex(index: index, message: "Index to insert at has to be non-negative and equal to or smaller than the number of items: (\(items.count))")
}
// Correct index when items were inserted in front of it:
if (self.items.count > 1 && currentIndex >= index) {
if (self.items.count > 0 && currentIndex >= index) {
currentIndex += items.count
}
self.items.insert(contentsOf: items, at: index)
@@ -157,7 +157,7 @@ class QueueManager<T> {
case previous = -1
}
private func skip(direction: SkipDirection, wrap: Bool) -> T? {
private func skip(direction: SkipDirection, wrap: Bool) -> Element? {
let count = items.count
if (current == nil || count == 0) {
return nil
@@ -174,9 +174,7 @@ class QueueManager<T> {
let oldIndex = currentIndex
currentIndex = max(0, min(items.count - 1, index))
if (oldIndex != currentIndex) {
defer {
delegate?.onCurrentItemChanged()
}
delegate?.onCurrentItemChanged()
}
}
return current
@@ -188,7 +186,7 @@ class QueueManager<T> {
- returns: The next (or current) item.
*/
@discardableResult
public func next(wrap: Bool = false) -> T? {
public func next(wrap: Bool = false) -> Element? {
synchronize {
return skip(direction: SkipDirection.next, wrap: wrap);
}
@@ -201,7 +199,7 @@ class QueueManager<T> {
- returns: The previous item.
*/
@discardableResult
public func previous(wrap: Bool = false) -> T? {
public func previous(wrap: Bool = false) -> Element? {
return synchronize {
return skip(direction: SkipDirection.previous, wrap: wrap);
}
@@ -216,7 +214,7 @@ class QueueManager<T> {
- returns: The item at the index.
*/
@discardableResult
public func jump(to index: Int) throws -> T {
public func jump(to index: Int) throws -> Element {
var skippedToSameCurrentItem = false
var currentItemChanged = false
let result = try synchronizeThrows {
@@ -268,7 +266,8 @@ class QueueManager<T> {
- throws: AudioPlayerError.QueueError
- returns: The removed item.
*/
public func removeItem(at index: Int) throws -> T {
@discardableResult
public func removeItem(at index: Int) throws -> Element {
var currentItemChanged = false
let result = try synchronizeThrows {
try throwIfQueueEmpty()
@@ -294,7 +293,7 @@ class QueueManager<T> {
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: T) {
public func replaceCurrentItem(with item: Element) {
var currentItemChanged = false
synchronize {
if currentIndex == -1 {
+15 -17
View File
@@ -68,10 +68,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public override func load(item: AudioItem, playWhenReady: Bool? = nil) {
if let playWhenReady = playWhenReady {
self.playWhenReady = playWhenReady
handlePlayWhenReady(playWhenReady) {
queue.replaceCurrentItem(with: item)
}
queue.replaceCurrentItem(with: item)
}
/**
@@ -81,10 +80,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func add(item: AudioItem, playWhenReady: Bool? = nil) {
if let playWhenReady = playWhenReady {
self.playWhenReady = playWhenReady
handlePlayWhenReady(playWhenReady) {
queue.add(item)
}
queue.add(item)
}
/**
@@ -94,10 +92,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- parameter playWhenReady: Optional, whether to start playback when the item is ready.
*/
public func add(items: [AudioItem], playWhenReady: Bool? = nil) {
if let playWhenReady = playWhenReady {
self.playWhenReady = playWhenReady
handlePlayWhenReady(playWhenReady) {
queue.add(items)
}
queue.add(items)
}
public func add(items: [AudioItem], at index: Int) throws {
@@ -147,15 +144,14 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- throws: `AudioPlayerError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool? = nil) throws {
if let playWhenReady = playWhenReady {
self.playWhenReady = playWhenReady
try handlePlayWhenReady(playWhenReady) {
if (index == currentIndex) {
seek(to: 0)
} else {
_ = try queue.jump(to: index)
}
event.playbackEnd.emit(data: .jumpedToIndex)
}
if (index == currentIndex) {
seek(to: 0)
} else {
_ = try queue.jump(to: index)
}
event.playbackEnd.emit(data: .jumpedToIndex)
}
/**
@@ -193,6 +189,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
override func AVWrapperItemDidPlayToEndTime() {
event.playbackEnd.emit(data: .playedUntilEnd)
if (repeatMode == .track) {
self.pause()
// quick workaround for race condition - schedule a call after 2 frames
DispatchQueue.main.asyncAfter(deadline: .now() + 0.016 * 2) { [weak self] in self?.replay() }
} else if (repeatMode == .queue) {
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '1.0.0'
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.
@@ -41,7 +41,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenPlayingSourceShouldBePlaying() {
@@ -52,7 +52,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenPausingSourceShouldBePaused() {
@@ -68,7 +68,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenTogglingFromPlayShouldBePaused() {
@@ -84,7 +84,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateWhenStoppingShouldBeStopped() {
@@ -100,7 +100,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperStateLoadingWithInitialTimeShouldBePlaying() {
@@ -114,7 +114,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
// MARK: - Duration tests
@@ -131,7 +131,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
// MARK: - Current time tests
@@ -152,7 +152,7 @@ class AVPlayerWrapperTests: XCTestCase {
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperSeekingShouldSeekWhileNotYetLoaded() {
@@ -163,7 +163,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperSeekByShouldSeek() {
@@ -176,7 +176,7 @@ class AVPlayerWrapperTests: XCTestCase {
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperLoadingSourceWithInitialTimeShouldSeek() {
@@ -185,7 +185,7 @@ class AVPlayerWrapperTests: XCTestCase {
expectation.fulfill()
}
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: 4.0)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
// MARK: - Rate tests
@@ -202,7 +202,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
}
wrapper.load(from: Source.url, playWhenReady: true)
wait(for: [expectation], timeout: 20.0)
wait(for: [expectation], timeout: defaultTimeout)
}
func testAVPlayerWrapperTimeObserverWhenUpdatedShouldUpdateTheObserversPeriodicObserverTimeInterval() {
@@ -23,7 +23,7 @@ class AudioPlayerEventTests: XCTestCase {
func testEventAddListener() {
let listener = EventListener()
event.addListener(listener, listener.handleEvent)
waitTrue(self.event.invokers.count > 0, timeout: 5)
waitTrue(self.event.invokers.count > 0, timeout: defaultTimeout)
}
func testEventRemoveListener() {
@@ -32,7 +32,7 @@ class AudioPlayerEventTests: XCTestCase {
listener = nil
event.emit(data: ())
waitEqual(self.event.invokers.count, 0, timeout: 5)
waitEqual(self.event.invokers.count, 0, timeout: defaultTimeout)
}
func testEventAddMultipleListeners() {
@@ -44,7 +44,7 @@ class AudioPlayerEventTests: XCTestCase {
return listener
}
waitEqual(self.event.invokers.count, listeners.count, timeout: 5)
waitEqual(self.event.invokers.count, listeners.count, timeout: defaultTimeout)
}
func testEventRemoveOneListener() {
@@ -59,6 +59,6 @@ class AudioPlayerEventTests: XCTestCase {
let listenerToRemove = listeners[listeners.count / 2]
event.removeListener(listenerToRemove)
waitEqual(self.event.invokers.count, listeners.count - 1, timeout: 5)
waitEqual(self.event.invokers.count, listeners.count - 1, timeout: defaultTimeout)
}
}
+56 -56
View File
@@ -62,7 +62,7 @@ class AudioPlayerTests: XCTestCase {
XCTAssertFalse(audioPlayer.playWhenReady)
audioPlayer.load(item: FiveSecondSourceWithInitialTimeOfFourSeconds.getAudioItem())
wait(for: [expectation], timeout: 5)
wait(for: [expectation], timeout: defaultTimeout)
XCTAssertTrue(seekCompleted)
XCTAssertTrue(audioPlayer.currentTime >= 4)
@@ -72,7 +72,7 @@ class AudioPlayerTests: XCTestCase {
func testSetDurationAfterLoading() {
audioPlayer.load(item: FiveSecondSource.getAudioItem())
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: defaultTimeout)
}
func testOnUpdateDurationReceivedAfterLoading() {
@@ -87,7 +87,7 @@ class AudioPlayerTests: XCTestCase {
audioPlayer.load(item: FiveSecondSource.getAudioItem())
wait(for: [expectation], timeout: 5) // Adjust the timeout as needed
wait(for: [expectation], timeout: defaultTimeout) // Adjust the timeout as needed
XCTAssertTrue(receivedUpdateDuration)
}
@@ -95,17 +95,17 @@ class AudioPlayerTests: XCTestCase {
func testResetDurationAfterLoadingAgain() {
audioPlayer.load(item: FiveSecondSource.getAudioItem())
XCTAssertEqual(audioPlayer.duration, 0)
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: 5)
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: 5)
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: 5)
waitEqual(self.audioPlayer.duration, 5, accuracy: 0.1, timeout: defaultTimeout)
audioPlayer.clear()
XCTAssertEqual(audioPlayer.duration, 0)
}
@@ -130,7 +130,7 @@ class AudioPlayerTests: XCTestCase {
)
audioPlayer.load(item: item, playWhenReady: true)
wait(for: [expectation], timeout: 5) // Adjust the timeout as needed
wait(for: [expectation], timeout: defaultTimeout) // Adjust the timeout as needed
XCTAssertNotNil(audioPlayer.playbackError)
XCTAssertEqual(audioPlayer.playerState, .failed)
@@ -168,10 +168,10 @@ class AudioPlayerTests: XCTestCase {
)
audioPlayer.load(item: item, playWhenReady: true)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: defaultTimeout)
audioPlayer.play()
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
}
func testRetryLoadingAfterFailureWithPlayWhenReady() {
@@ -185,10 +185,10 @@ class AudioPlayerTests: XCTestCase {
)
audioPlayer.load(item: item, playWhenReady: true)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: defaultTimeout)
audioPlayer.playWhenReady = true
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
}
func testRetryLoadingAfterFailureWithReload() {
@@ -202,10 +202,10 @@ class AudioPlayerTests: XCTestCase {
)
audioPlayer.load(item: item, playWhenReady: true)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed], timeout: defaultTimeout)
audioPlayer.reload(startFromCurrentTime: true)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .failed], timeout: defaultTimeout)
}
func testLoadResourceSucceedsAfterPreviousFailure() {
@@ -218,13 +218,13 @@ class AudioPlayerTests: XCTestCase {
let failItem = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream)
audioPlayer.load(item: failItem, playWhenReady: false)
waitTrue(didReceiveFail, timeout: 5)
waitEqual(self.audioPlayer.playerState, .failed, timeout: 5)
waitEqual(self.playerStateEventListener.states, [.loading, .failed], timeout: 5)
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: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .loading, .playing], timeout: 5)
waitTrue(self.audioPlayer.playbackError == nil, timeout: defaultTimeout)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .failed, .idle, .loading, .playing], timeout: defaultTimeout)
}
func testLoadResourceSucceedsAfterPreviousFailureWithPlayWhenReady() {
@@ -237,11 +237,11 @@ class AudioPlayerTests: XCTestCase {
let item = DefaultAudioItem(audioUrl: nonExistingUrl, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .stream)
audioPlayer.load(item: item, playWhenReady: true)
waitTrue(didReceiveFail, timeout: 5)
waitEqual(self.audioPlayer.playerState, .failed, timeout: 5)
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: 5)
waitTrue(self.audioPlayer.playbackError == nil, timeout: defaultTimeout)
}
// MARK: - States
@@ -257,115 +257,115 @@ class AudioPlayerTests: XCTestCase {
func testReadyStateAfterLoadSource() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ready, timeout: defaultTimeout)
}
func testPlayingStateAfterLoadSourceWithPlayWhenReady() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
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: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.pause()
expectedEvents.append(.paused)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.play()
expectedEvents.append(.playing)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.clear()
expectedEvents.append(.idle)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
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: 5)
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
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: 5)
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: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.pause()
expectedEvents.append(.paused)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
expectedEvents.append(.playing)
audioPlayer.play()
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.stop()
expectedEvents.append(.stopped)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
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: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.clear()
expectedEvents.append(.idle)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
audioPlayer.load(item: Source.getAudioItem())
expectedEvents.append(contentsOf: [.loading, .playing])
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, expectedEvents, timeout: defaultTimeout)
}
func testPlayingStateAfterPlay() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ready, timeout: defaultTimeout)
audioPlayer.play()
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
func testPausedStateAfterPause() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
audioPlayer.pause()
waitEqual(self.audioPlayer.playerState, .paused, timeout: 5)
waitEqual(self.audioPlayer.playerState, .paused, timeout: defaultTimeout)
}
func testPausedStateAfterSettingPlayWhenReadyToFalse() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
audioPlayer.playWhenReady = false
waitEqual(self.audioPlayer.playerState, .paused, timeout: 5)
waitEqual(self.audioPlayer.playerState, .paused, timeout: defaultTimeout)
}
func testPlayingStateAfterSettingPlayWhenReadyToTrue() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
waitEqual(self.audioPlayer.playerState, .ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ready, timeout: defaultTimeout)
audioPlayer.playWhenReady = true
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
func testStoppedStateAfterStop() {
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
audioPlayer.stop()
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
}
// MARK: - State (Current Time)
@@ -383,7 +383,7 @@ class AudioPlayerTests: XCTestCase {
}
audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
waitTrue(onSecondsElapseTime > 0, timeout: 5)
waitTrue(onSecondsElapseTime > 0, timeout: defaultTimeout)
}
// MARK: - Buffer
@@ -411,22 +411,22 @@ class AudioPlayerTests: XCTestCase {
func testSeekingBeforeLoadingComplete() {
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
XCTAssertTrue(audioPlayer.playerState == .loading)
XCTAssertTrue(audioPlayer.playerState == .buffering)
audioPlayer.seek(to: 4.75)
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: 5)
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: defaultTimeout)
}
func testSeekingAfterLoadingComplete() {
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
audioPlayer.seek(to: 4.75)
waitTrue(self.audioPlayer.currentTime > 4.75, timeout: 5)
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: 5)
waitEqual(self.audioPlayer.currentTime, 4.75, timeout: defaultTimeout)
}
func testSeekingWhenStopped() {
@@ -435,7 +435,7 @@ class AudioPlayerTests: XCTestCase {
waitForSeek(audioPlayer, to: 2)
audioPlayer.stop()
audioPlayer.seek(to: 4.75)
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 0, timeout: defaultTimeout)
}
// MARK: - Rate
@@ -466,7 +466,7 @@ class AudioPlayerTests: XCTestCase {
audioPlayer.load(item: FiveSecondSource.getAudioItem(), playWhenReady: true)
audioPlayer.rate = 10
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
if let start = start, let end = end {
let duration = end.timeIntervalSince(start)
@@ -498,7 +498,7 @@ class AudioPlayerTests: XCTestCase {
}
audioPlayer.seek(to: 4.75)
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
if let start = start, let end = end {
let duration = end.timeIntervalSince(start)
@@ -1,5 +1,8 @@
import XCTest
import AVFoundation
#if os(iOS)
@testable import SwiftAudioEx
class AudioSessionControllerTests: XCTestCase {
@@ -89,3 +92,5 @@ class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelega
self.interruptionType = type
}
}
#endif
@@ -9,6 +9,8 @@
import Foundation
import AVFoundation
#if os(iOS)
@testable import SwiftAudioEx
@@ -64,3 +66,5 @@ class FailingAudioSession: AudioSession {
}
#endif
@@ -75,16 +75,15 @@ class QueuedAudioPlayerTests: XCTestCase {
XCTAssertNil(audioPlayer.currentItem)
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.idle)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .idle], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .idle], timeout: defaultTimeout)
}
func testLoadAfterRemoval() {
testRemovingItemAfterAdding()
audioPlayer.load(item: Source.getAudioItem())
audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
XCTAssertNotEqual(audioPlayer.currentItem?.getSourceUrl(), FiveSecondSource.getAudioItem().getSourceUrl())
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .idle, .loading, .playing], timeout: 5)
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.playing)
waitTrue(self.playerStateEventListener.statesWithoutBuffering.contains(.playing), timeout: defaultTimeout)
}
func testAddingMultipleItems() {
@@ -100,7 +99,16 @@ class QueuedAudioPlayerTests: XCTestCase {
XCTAssertEqual(audioPlayer.items.count, 1)
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), ShortSource.getAudioItem().getSourceUrl())
}
// Covers: https://github.com/doublesymmetry/SwiftAudioEx/pull/81
func testAddingItemWhenOnlyOneTrackInQueue() throws {
audioPlayer.add(item: FiveSecondSource.getAudioItem())
audioPlayer.play()
try audioPlayer.add(items: [ShortSource.getAudioItem()], at: 0)
XCTAssertEqual(audioPlayer.items.count, 2)
XCTAssertEqual(audioPlayer.currentIndex, 1)
}
// MARK: - Next Items
func testNextItemsEmptyOnCreate() {
@@ -166,25 +174,24 @@ class QueuedAudioPlayerTests: XCTestCase {
// Test next
audioPlayer.next()
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .paused, .loading, .paused], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .paused, .loading, .paused], timeout: defaultTimeout)
XCTAssertEqual(audioPlayer.previousItems.count, 1)
waitEqual(self.playbackEndEventListener.lastReason, .skippedToNext, timeout: 5)
waitEqual(self.playbackEndEventListener.lastReason, .skippedToNext, timeout: defaultTimeout)
// Test stop
audioPlayer.stop()
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
// Test stop again
audioPlayer.stop()
waitEqual(self.audioPlayer.playerState, .stopped, timeout: 5)
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
waitEqual(self.audioPlayer.playerState, .stopped, timeout: defaultTimeout)
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
// Test previous
audioPlayer.previous()
waitEqual(self.audioPlayer.playerState, .loading, timeout: 5)
// should not have emitted playbackEnd .skippedToPrevious because playback was already stopped previously
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: 5)
XCTAssertEqual(self.audioPlayer.playerState, .loading)
waitEqual(self.playbackEndEventListener.reasons, [.skippedToNext, .playerStopped], timeout: defaultTimeout)
}
@@ -218,14 +225,14 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.pause()
// It should have gone into .paused state from .loading and then into .ready because playback can be started
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: 5)
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: defaultTimeout)
}
// MARK: - Stop
func testStopOnEmptyQueue() {
audioPlayer.stop()
waitEqual(self.playerStateEventListener.states, [.stopped], timeout: 5)
waitEqual(self.playerStateEventListener.states, [.stopped], timeout: defaultTimeout)
// It should not have emitted a playbackEnd event
XCTAssertNil(playbackEndEventListener.lastReason)
@@ -239,10 +246,10 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.stop()
// It should have emitted a playbackEnd .playerStopped event
waitEqual(self.playbackEndEventListener.lastReason, .playerStopped, timeout: 5)
waitEqual(self.playbackEndEventListener.lastReason, .playerStopped, timeout: defaultTimeout)
// It should have mutated player state from .loading to .stopped
waitEqual(self.playerStateEventListener.states, [.loading, .stopped], timeout: 5)
waitEqual(self.playerStateEventListener.states, [.loading, .stopped], timeout: defaultTimeout)
}
// MARK: - Load
@@ -253,7 +260,7 @@ class QueuedAudioPlayerTests: XCTestCase {
XCTAssertNotNil(audioPlayer.currentItem)
// It should have started loading, but not playing yet
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: 5)
waitEqual(self.playerStateEventListener.states, [.loading, .paused, .ready], timeout: defaultTimeout)
}
func testLoadItemAfterPlaying() {
@@ -262,12 +269,12 @@ class QueuedAudioPlayerTests: XCTestCase {
XCTAssertNotNil(audioPlayer.currentItem)
// It should have started playing
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .playing], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering, [.loading, .playing], timeout: defaultTimeout)
audioPlayer.load(item: Source.getAudioItem())
XCTAssertEqual(audioPlayer.items.count, 1)
XCTAssertEqual(audioPlayer.currentItem?.getSourceUrl(), Source.getAudioItem().getSourceUrl())
waitEqual(self.playerStateEventListener.statesWithoutBuffering.prefix(4), [.loading, .playing, .loading, .playing], timeout: 5)
waitEqual(self.playerStateEventListener.statesWithoutBuffering.prefix(4), [.loading, .playing, .loading, .playing], timeout: defaultTimeout)
}
// MARK: - Next
@@ -282,10 +289,10 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
// should go to previous item and not play
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
}
func testNextWhenPausedWithoutPlaying() {
@@ -293,10 +300,10 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.pause()
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
// should go to previous item and not play
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
}
func testNextWhenPlaying() {
@@ -304,10 +311,10 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.add(items: [FiveSecondSource.getAudioItem(), FiveSecondSource.getAudioItem()])
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
// should go to previous item and play
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
}
// MARK: - Previous
@@ -323,11 +330,11 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.next()
audioPlayer.previous()
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
// should go to previous item and play
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
}
func testPreviousWhenPaused() {
@@ -336,11 +343,11 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.pause()
audioPlayer.previous()
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.previousItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
// should go to previous item and not play
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ready, timeout: defaultTimeout)
}
// MARK: - Move
@@ -354,7 +361,7 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.repeatMode = RepeatMode.off
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ended, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.ended, timeout: defaultTimeout)
}
func testMoveItemsRepeatModeQueue() {
@@ -366,9 +373,9 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.repeatMode = RepeatMode.queue
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitTrue(self.audioPlayer.currentTime > 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, AudioPlayerState.playing, timeout: defaultTimeout)
}
func testMoveItemsRepeatModeTrack() {
@@ -380,10 +387,10 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.repeatMode = RepeatMode.track
waitForSeek(audioPlayer, to: 4.6)
waitTrue(self.audioPlayer.currentTime < 4.6, timeout: 5)
waitTrue(self.audioPlayer.currentTime > 0, timeout: 5)
waitTrue(self.audioPlayer.currentTime < 4.6, timeout: defaultTimeout)
waitTrue(self.audioPlayer.currentTime > 0, timeout: defaultTimeout)
XCTAssertEqual(audioPlayer.currentIndex, 1)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
// MARK: - Repeat Mode (Off - Two Items)
@@ -398,16 +405,16 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeOffTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
// Allow final track to end
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
waitEqual(self.currentItemEventListener.index, 1, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.index, 1, timeout: defaultTimeout)
}
func testNextWhenRepeatModeOff() {
@@ -415,16 +422,16 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.play()
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
// Calling next on the final track
audioPlayer.next()
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentTime, 5, accuracy: 0.1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
}
// MARK: - Repeat Mode (Track - Two Items)
@@ -439,19 +446,19 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeTrackTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
func testNextWhenRepeatModeTrack() {
setupRepeatModeTrackTests()
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
}
@@ -467,28 +474,28 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeQueueTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
// Allow the final track to end
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: defaultTimeout)
}
func testNextWhenRepeatModeQueue() {
setupRepeatModeQueueTests()
audioPlayer.next()
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
}
func testNextTwiceWhenRepeatModeQueue() {
@@ -498,13 +505,13 @@ class QueuedAudioPlayerTests: XCTestCase {
audioPlayer.next()
XCTAssertEqual(audioPlayer.currentIndex, 1)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 0, timeout: defaultTimeout)
audioPlayer.next()
XCTAssertEqual(audioPlayer.currentIndex, 0)
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.nextItems.count, 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
// MARK: - Repeat Mode (Off - One Item)
@@ -518,15 +525,15 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeOffOneItemTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .ended, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .ended, timeout: defaultTimeout)
}
func testNextWhenRepeatModeOffOneItem() {
setupRepeatModeOffOneItemTests()
audioPlayer.next()
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
// TODO: Test this more thoroughly?
}
@@ -541,19 +548,19 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeTrackOneItemTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.currentItemEventListener.lastIndex, nil, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitEqual(self.currentItemEventListener.lastIndex, nil, timeout: defaultTimeout)
}
func testNextWhenRepeatModeTrackOneItem() {
setupRepeatModeTrackOneItemTests()
audioPlayer.next()
waitEqual(self.audioPlayer.currentTime, 0, timeout: 5)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.currentTime, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.nextItems.count, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
// MARK: - Repeat Mode (Queue - One Item)
@@ -567,11 +574,11 @@ class QueuedAudioPlayerTests: XCTestCase {
setupRepeatModeQueueOneItemTests()
waitForSeek(audioPlayer, to: 4.6)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitTrue(self.audioPlayer.currentTime > 4.5, timeout: 5)
waitTrue(self.audioPlayer.currentTime < 1, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitTrue(self.audioPlayer.currentTime > 4.5, timeout: defaultTimeout)
waitTrue(self.audioPlayer.currentTime < 1, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
func testNextWhenRepeatModeQueueOneItem() {
@@ -579,10 +586,10 @@ class QueuedAudioPlayerTests: XCTestCase {
waitForSeek(audioPlayer, to: 2)
audioPlayer.next()
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitTrue(self.audioPlayer.currentTime < 1.9, timeout: 5)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: 5)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
waitTrue(self.audioPlayer.currentTime < 1.9, timeout: defaultTimeout)
waitEqual(self.audioPlayer.currentIndex, 0, timeout: defaultTimeout)
waitEqual(self.audioPlayer.playerState, .playing, timeout: defaultTimeout)
}
}
+52 -35
View File
@@ -4,68 +4,85 @@ import XCTest
@testable import SwiftAudioEx
extension XCTestCase {
var defaultTimeout: TimeInterval {
if ProcessInfo.processInfo.environment["CI"] != nil {
return 20
} else {
return 5
}
}
func waitForSeek(_ audioPlayer: AudioPlayer, to time: Double) {
let seekEventListener = QueuedAudioPlayer.SeekEventListener()
audioPlayer.event.seek.addListener(seekEventListener, seekEventListener.handleEvent)
audioPlayer.seek(to: time)
waitEqual(seekEventListener.eventResult.0, time, accuracy: 0.1, timeout: 5)
waitEqual(seekEventListener.eventResult.1, true, timeout: 5)
}
func waitTrue(_ expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval) {
let expectation = XCTestExpectation(description: "Value should eventually equal expected value")
DispatchQueue.global().async {
while !expression() {
usleep(100_000) // Sleep for 100 milliseconds
}
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
waitEqual(seekEventListener.eventResult.0, time, accuracy: 0.1, timeout: defaultTimeout)
waitEqual(seekEventListener.eventResult.1, true, timeout: defaultTimeout)
}
func waitEqual<T: Equatable>(_ expression1: @autoclosure @escaping () -> T, _ expression2: @autoclosure @escaping () -> T, timeout: TimeInterval) {
let expectation = XCTestExpectation(description: "Value should eventually equal expected value")
DispatchQueue.global().async {
while expression1() != expression2() {
usleep(100_000) // Sleep for 100 milliseconds
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if expression1() == expression2() {
expectation.fulfill()
timer.invalidate()
}
expectation.fulfill()
}
RunLoop.current.add(timer, forMode: .default)
wait(for: [expectation], timeout: timeout)
timer.invalidate()
}
func waitEqual<T: Equatable>(_ expression1: @autoclosure @escaping () -> T, _ expression2: @autoclosure @escaping () -> T, accuracy: T, timeout: TimeInterval) where T: FloatingPoint {
let expectation = XCTestExpectation(description: "Value should eventually equal expected value with accuracy")
DispatchQueue.global().async {
let startTime = Date()
while abs(expression1() - expression2()) > accuracy {
if Date().timeIntervalSince(startTime) >= timeout {
break
}
usleep(100_000) // Sleep for 100 milliseconds
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if abs(expression1() - expression2()) < accuracy {
expectation.fulfill()
timer.invalidate()
}
expectation.fulfill()
}
return wait(for: [expectation], timeout: timeout)
RunLoop.current.add(timer, forMode: .default)
wait(for: [expectation], timeout: timeout)
timer.invalidate()
}
func waitEqual<T1: Equatable, T2: Equatable>(_ expression1: @autoclosure @escaping () -> (T1, T2), _ expression2: @autoclosure @escaping () -> (T1, T2), timeout: TimeInterval) {
let expectation = XCTestExpectation(description: "Values should eventually be equal")
DispatchQueue.global().async {
while expression1() != expression2() {
usleep(100_000) // Sleep for 100 milliseconds
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if expression1() == expression2() {
expectation.fulfill()
timer.invalidate()
}
expectation.fulfill()
}
RunLoop.current.add(timer, forMode: .default)
wait(for: [expectation], timeout: timeout)
timer.invalidate()
}
func waitTrue(_ expression: @autoclosure @escaping () -> Bool, timeout: TimeInterval) {
let expectation = XCTestExpectation(description: "Expression should eventually be true")
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if expression() {
expectation.fulfill()
timer.invalidate()
}
}
RunLoop.current.add(timer, forMode: .default)
wait(for: [expectation], timeout: timeout)
timer.invalidate()
}
}
@@ -1,13 +1,12 @@
import Foundation
import SwiftAudioEx
import UIKit
struct Source {
static let path: String = Bundle.module.path(forResource: "TestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: Source.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: AudioItemImage())
}
}