Compare commits

...

29 Commits

Author SHA1 Message Date
David Chavez 40ea7ad2f9 Release 0.14.7 2022-02-24 08:49:31 +01:00
David Chavez f2f1c1236c Add tests for new seek improvements 2022-02-24 08:48:54 +01:00
Terkel a75f0d0201 fix: make moveItem public and accessible from outside the class (#9) 2022-02-23 21:40:39 +01:00
Jacob Spizziri 9e4e7f6807 fix(seek): fix an issue causing seek to fail if called immediatly after load (#11) 2022-02-23 21:27:38 +01:00
David Chavez dbd3b03989 Release 0.14.6 2021-11-06 14:38:13 +01:00
David Chavez 7e19604df7 Create LICENSE (#5)
* Create LICENSE

* Update LICENSE
2021-11-06 14:29:06 +01:00
David Chavez 481130dc58 Release 0.14.5 2021-10-25 14:08:31 +02:00
David Chavez 300b34afa3 Do not emit paused state when changing tracks 2021-10-25 14:08:01 +02:00
David Chavez da3af0e9db Release 0.14.4 2021-09-28 10:58:23 +02:00
David Chavez d9eb313c1b Deprecate syncRemoteCommandsWithCommandCenter 2021-09-28 10:57:36 +02:00
David Chavez cca7f68da4 Increase deployment target for Test Target 2021-09-28 10:12:22 +02:00
David Chavez 7ed74b80ec Release 0.14.2 2021-09-28 10:04:01 +02:00
David Chavez 2773e4bfec Trigger skip and jump events only when actually taking action 2021-09-28 09:57:24 +02:00
David Chavez 77dc8f4ff1 Fix flickering elapsed time on a lock screen after pause 2021-09-28 09:41:04 +02:00
David Chavez accdf2c00c Rename exposed SPM package name 2021-09-28 09:31:31 +02:00
David Chavez 542d3a5764 Remove syncRemoteCommandsWithCommandCenter
Removed in favor of a didSet on remoteCommands property
2021-09-28 09:28:14 +02:00
David Chavez 4131e54f3e Create FUNDING.yml 2021-09-28 09:19:46 +02:00
David Chavez 03c4a7310f Release 0.14.2 2021-09-25 00:17:58 +02:00
David Chavez 9d2d2594a1 Replace commands based on diff to avoid iOS 15 issues 2021-09-25 00:17:31 +02:00
David Chavez 4e790876cb Release 0.14.1 2021-09-23 10:51:50 +02:00
David Chavez b19d01bdfc Allow manual resyncing of command center commands 2021-09-23 10:51:21 +02:00
David Chavez 3c8ecb353c Release 0.14.0 2021-09-16 17:50:55 +02:00
David Chavez cafd513468 Raise minimum deployment target to iOS11
Due to breaking change in Swift 5.5 & Xcode 13
2021-09-16 17:50:43 +02:00
David Chavez 7b8a4f318d Add tests for repeat mode 2021-08-19 16:27:41 +02:00
David Chavez acab6473b2 Release 0.13.2 2021-08-12 11:40:50 +02:00
David Chavez 57b6fb08f3 Fix tests 2021-08-12 11:40:21 +02:00
David Chavez 68a15ab3a6 Release 0.13.1 2021-08-12 11:18:08 +02:00
David Chavez 92053a2bd0 Add event for queue index changes 2021-08-12 11:16:56 +02:00
David Chavez aeef676164 Persist rate accross player vs item 2021-08-12 11:16:46 +02:00
35 changed files with 405 additions and 101 deletions
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: DoubleSymmetry
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+16 -14
View File
@@ -44,8 +44,8 @@
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA39266028E200C7A389 /* SwiftAudio */; };
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA3B26602C0E00C7A389 /* SwiftAudio */; };
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */; };
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
/* End PBXBuildFile section */
@@ -94,7 +94,7 @@
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftAudio_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -103,7 +103,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -111,7 +111,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */,
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
9B05AA312660276400C7A389 /* Quick in Frameworks */,
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
);
@@ -222,7 +222,7 @@
9B05AA2F2660276400C7A389 /* Frameworks */ = {
isa = PBXGroup;
children = (
9B05AA38266028D600C7A389 /* SwiftAudio */,
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -244,7 +244,7 @@
);
name = SwiftAudio_Example;
packageProductDependencies = (
9B05AA39266028E200C7A389 /* SwiftAudio */,
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
);
productName = SwiftAudio;
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
@@ -267,7 +267,7 @@
packageProductDependencies = (
9B05AA302660276400C7A389 /* Quick */,
9B05AA322660276400C7A389 /* Nimble */,
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
);
productName = Tests;
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
@@ -532,7 +532,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -551,7 +551,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -577,6 +577,7 @@
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -598,6 +599,7 @@
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -672,13 +674,13 @@
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
9B05AA39266028E200C7A389 /* SwiftAudio */ = {
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
9B05AA3B26602C0E00C7A389 /* SwiftAudio */ = {
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
/* End XCSwiftPackageProductDependency section */
};
+1 -1
View File
@@ -7,7 +7,7 @@
//
import Foundation
import SwiftAudio
import SwiftAudioEx
class AudioController {
+1 -1
View File
@@ -7,7 +7,7 @@
//
import UIKit
import SwiftAudio
import SwiftAudioEx
class QueueViewController: UIViewController {
+7 -2
View File
@@ -7,7 +7,7 @@
//
import UIKit
import SwiftAudio
import SwiftAudioEx
import AVFoundation
import MediaPlayer
@@ -31,6 +31,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
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)
@@ -106,7 +107,7 @@ class ViewController: UIViewController {
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print(data)
print("state=\(data)")
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
@@ -126,6 +127,10 @@ class ViewController: UIViewController {
}
}
}
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
print("playEndReason=\(data)")
}
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerItemNotificationObserverTests: QuickSpec {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerItemObserverTests: QuickSpec {
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerTimeObserverTests: QuickSpec {
+12 -1
View File
@@ -1,7 +1,7 @@
import AVFoundation
import XCTest
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerWrapperTests: XCTestCase {
@@ -145,6 +145,17 @@ class AVPlayerWrapperTests: XCTestCase {
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
let expectation = XCTestExpectation()
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioPlayerEventTests: QuickSpec {
+3 -3
View File
@@ -3,7 +3,7 @@ import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioPlayerTests: XCTestCase {
@@ -124,8 +124,8 @@ class AudioPlayerTests: XCTestCase {
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
XCTAssert(audioPlayer.rate == 0.0)
func test_AudioPlayer__rate__should_be_1() {
XCTAssert(audioPlayer.rate == 1.0)
}
func test_AudioPlayer__rate__playing_source__should_be_1() {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioSessionControllerTests: QuickSpec {
+1 -1
View File
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class NonFailingAudioSession: AudioSession {
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation
@testable import SwiftAudio
@testable import SwiftAudioEx
final class MockDispatchQueue: DispatchQueueType {
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
@@ -9,7 +9,7 @@
import Foundation
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoControllerTests: QuickSpec {
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
class NowPlayingInfoTests: QuickSpec {
+1 -1
View File
@@ -1,7 +1,7 @@
import Quick
import Nimble
@testable import SwiftAudio
@testable import SwiftAudioEx
class QueueManagerTests: QuickSpec {
+141 -1
View File
@@ -1,7 +1,7 @@
import Quick
import Nimble
@testable import SwiftAudio
@testable import SwiftAudioEx
class QueuedAudioPlayerTests: QuickSpec {
override func spec() {
@@ -166,6 +166,146 @@ class QueuedAudioPlayerTests: QuickSpec {
}
}
describe("its repeat mode") {
context("when adding 2 items") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
}
context("then setting repeat mode off") {
beforeEach {
audioPlayer.repeatMode = .off
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to next item") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should stop playback normally") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
}
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.currentIndex).to(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("then calling next() again") {
it("should fail") {
expect(try audioPlayer.next()).to(throwError())
}
}
}
}
context("then setting repeat mode track") {
beforeEach {
audioPlayer.repeatMode = .track
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should restart current item") {
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item but should not play") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
}
}
}
context("then setting repeat mode queue") {
beforeEach {
audioPlayer.repeatMode = .queue
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to first track and should play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.currentIndex).to(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("then calling next() again") {
beforeEach {
try? audioPlayer.next()
}
it("should move to first track and should play") {
expect(audioPlayer.nextItems.count).to(equal(1))
expect(audioPlayer.currentIndex).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
}
}
}
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
//
import Foundation
import SwiftAudio
import SwiftAudioEx
import UIKit
struct Source {
+42
View File
@@ -0,0 +1,42 @@
MIT License
Copyright (c) 2021 Double Symmetry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+7 -7
View File
@@ -2,18 +2,18 @@
import PackageDescription
let package = Package(
name: "SwiftAudio",
platforms: [.iOS(.v10)],
name: "SwiftAudioEx",
platforms: [.iOS(.v11)],
products: [
.library(
name: "SwiftAudio",
targets: ["SwiftAudio"]),
name: "SwiftAudioEx",
targets: ["SwiftAudioEx"]),
],
dependencies: [],
targets: [
.target(
name: "SwiftAudio",
name: "SwiftAudioEx",
dependencies: [],
path: "SwiftAudio/Classes")
path: "SwiftAudioEx/Classes")
]
)
)
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.13.0'
s.version = '0.14.7'
s.summary = 'Easy audio streaming for iOS'
s.description = <<-DESC
SwiftAudioEx is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
@@ -20,7 +20,7 @@ DESC
'Jørgen Henrichsen' => 'jh.henrichs@gmail.com', }
s.source = { :git => 'https://github.com/DoubleSymmetry/SwiftAudioEx.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudioEx/Classes/**/*'
end
+1
View File
@@ -22,6 +22,7 @@ public struct APError {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
case noNextWhenRepeatModeTrack
}
}
@@ -37,6 +37,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
*/
fileprivate var _playWhenReady: Bool = true
fileprivate var _initialTime: TimeInterval?
/// True when the track was paused for the purpose of switching tracks
fileprivate var _pausedForLoad: Bool = false
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
@@ -100,7 +103,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
!seconds.isNaN {
return seconds
}
return 0.0
@@ -162,16 +165,21 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
func seek(to seconds: TimeInterval) {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
// if the player is loading then we need to defer seeking until it's ready.
if (self._state == AVPlayerWrapperState.loading) {
self._initialTime = seconds
} else {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
}
@@ -236,7 +244,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
_pausedForLoad = true
self.pause()
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
@@ -277,6 +288,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
if currentItem == nil {
_state = .idle
}
else if _pausedForLoad == true {}
else {
self._state = .paused
}
@@ -293,6 +305,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
switch status {
case .readyToPlay:
self._state = .ready
self._pausedForLoad = false
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
+32 -5
View File
@@ -42,7 +42,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
public var remoteCommands: [RemoteCommand] = [] {
didSet {
if let item = currentItem {
self.enableRemoteCommands(forItem: item)
}
}
}
// MARK: - Getters from AVPlayerWrapper
@@ -115,9 +121,17 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
set { _wrapper.isMuted = newValue }
}
private var _rate: Float = 1.0
public var rate: Float {
get { return wrapper.rate }
set { _wrapper.rate = newValue }
get { return _rate }
set {
_rate = newValue
// Only set the rate on the wrapper if it is already playing.
if _wrapper.rate > 0 {
_wrapper.rate = newValue
}
}
}
// MARK: - Init
@@ -226,6 +240,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.enableRemoteCommands(remoteCommands)
}
}
/**
Syncs the current remoteCommands with the iOS command center.
Can be used to update item states - e.g. like, dislike and bookmark.
*/
@available(*, deprecated, message: "Directly set .remoteCommands instead")
public func syncRemoteCommandsWithCommandCenter() {
self.enableRemoteCommands(remoteCommands)
}
// MARK: - NowPlayingInfo
@@ -259,8 +282,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingDuration(duration)
updateNowPlayingRate(rate)
}
@@ -311,7 +334,11 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
updateNowPlayingPlaybackValues()
}
setTimePitchingAlgorithmForCurrentItem()
case .playing, .paused:
case .playing:
// When a track starts playing, reset the rate to the stored rate
self.rate = _rate;
fallthrough
case .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
@@ -21,10 +21,8 @@ protocol AudioSession {
var availableCategories: [AVAudioSession.Category] { get }
@available(iOS 10.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
@available(iOS 11.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
+8 -1
View File
@@ -18,6 +18,7 @@ extension AudioPlayer {
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
public struct EventHolder {
@@ -70,7 +71,13 @@ extension AudioPlayer {
- Note: It can be necessary to set the AVAudioSession's category again when this event is emitted.
*/
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()
/**
Emitted when a new track starts and the queue index changes.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: It is only fired for instances of a QueuedAudioPlayer.
*/
public let queueIndex: AudioPlayer.Event<QueueIndexEventData> = AudioPlayer.Event()
}
public typealias EventClosure<EventData> = (EventData) -> Void
@@ -31,7 +31,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The URL pointing to the now playing item's underlying asset.
This constant is used by the system UI when video thumbnails or audio waveform visualizations are applicable.
*/
@available(iOS 10.3, *)
case assetUrl(URL?)
/**
@@ -116,7 +115,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The service provider associated with the now-playing item.
Value is a unique NSString that identifies the service provider for the now-playing item. If the now-playing item belongs to a channel or subscription service, this key can be used to coordinate various types of now-playing content from the service provider.
*/
@available(iOS 11.0, *)
case serviceIdentifier(String?)
@@ -130,11 +128,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyAvailableLanguageOptions
case .assetUrl(_):
if #available(iOS 10.3, *) {
return MPNowPlayingInfoPropertyAssetURL
} else {
return ""
}
return MPNowPlayingInfoPropertyAssetURL
case .chapterCount(_):
return MPNowPlayingInfoPropertyChapterCount
@@ -175,11 +169,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyPlaybackRate
case .serviceIdentifier(_):
if #available(iOS 11.0, *) {
return MPNowPlayingInfoPropertyServiceIdentifier
} else {
return ""
}
return MPNowPlayingInfoPropertyServiceIdentifier
}
}
@@ -194,10 +184,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return options
case .assetUrl(let url):
if #available(iOS 10.3, *) {
return url
}
return false
return url
case .chapterCount(let count):
return count != nil ? NSNumber(value: count!) : nil
+1 -1
View File
@@ -7,7 +7,7 @@
import Foundation
protocol QueueManagerDelegate: class {
protocol QueueManagerDelegate: AnyObject {
func onReceivedFirstItem()
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int)
}
+47 -15
View File
@@ -11,10 +11,15 @@ import MediaPlayer
/**
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer {
public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
let queueManager: QueueManager = QueueManager<AudioItem>()
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
queueManager.delegate = self
}
/// The repeat mode for the queue player.
public var repeatMode: RepeatMode = .off
@@ -34,6 +39,7 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
public override func stop() {
super.stop()
self.event.queueIndex.emit(data: (currentIndex, nil))
}
override func reset() {
@@ -117,18 +123,29 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func next() throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
do {
let nextItem = try queueManager.next()
event.playbackEnd.emit(data: .skippedToNext)
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
} catch APError.QueueError.noNextItem {
if repeatMode == .queue {
event.playbackEnd.emit(data: .skippedToNext)
try jumpToItem(atIndex: 0, playWhenReady: true)
} else {
throw APError.QueueError.noNextItem
}
} catch {
throw error
}
}
/**
Step to the previous item in the queue.
*/
public func previous() throws {
event.playbackEnd.emit(data: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
event.playbackEnd.emit(data: .skippedToPrevious)
try self.load(item: previousItem, playWhenReady: repeatMode != .track)
}
/**
@@ -149,8 +166,8 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
event.playbackEnd.emit(data: .jumpedToIndex)
let item = try queueManager.jump(to: index)
event.playbackEnd.emit(data: .jumpedToIndex)
try self.load(item: item, playWhenReady: playWhenReady)
}
@@ -161,7 +178,7 @@ public class QueuedAudioPlayer: AudioPlayer {
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
public func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
@@ -185,18 +202,33 @@ public class QueuedAudioPlayer: AudioPlayer {
super.AVWrapperItemDidPlayToEndTime()
switch repeatMode {
case .off: try? self.next()
case .off:
do {
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
} catch { /* playback finished */ }
case .track:
seek(to: 0)
play()
case .queue:
do {
try self.next()
} catch APError.QueueError.noNextItem {
do {
try jumpToItem(atIndex: 0, playWhenReady: true)
} catch { /* TODO: handle possible errors from load */ }
} catch { /* TODO: handle possible errors from load */ }
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
} catch {
try? jumpToItem(atIndex: 0, playWhenReady: true)
}
}
}
// MARK: - QueueManagerDelegate
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
// if _currentItem is nil, then this was triggered by a reset. ignore.
if _currentItem == nil { return }
self.event.queueIndex.emit(data: (oldIndex, newIndex))
}
func onReceivedFirstItem() {
self.event.queueIndex.emit(data: (nil, 0))
}
}
@@ -103,7 +103,7 @@ public struct FeedbackCommand: RemoteCommandProtocol {
}
}
public enum RemoteCommand {
public enum RemoteCommand: CustomStringConvertible {
case play
@@ -128,6 +128,23 @@ public enum RemoteCommand {
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
public var description: String {
switch self {
case .play: return "play"
case .pause: return "pause"
case .stop: return "stop"
case .togglePlayPause: return "togglePlayPause"
case .next: return "nextTrack"
case .previous: return "previousTrack"
case .changePlaybackPosition: return "changePlaybackPosition"
case .skipForward(_): return "skipForward"
case .skipBackward(_): return "skipBackward"
case .like(_, _, _): return "like"
case .dislike(_, _, _): return "dislike"
case .bookmark(_, _, _): return "bookmark"
}
}
/**
All values in an array for convenience.
@@ -19,7 +19,8 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
private var enabledCommands: [RemoteCommand] = []
/**
Create a new RemoteCommandController.
@@ -30,10 +31,18 @@ public class RemoteCommandController {
}
internal func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
let commandsToDisable = enabledCommands.filter { command in
!commands.contains(where: { $0.description == command.description })
}
self.enabledCommands = commands
commands.forEach { (command) in
self.enable(command: command)
}
commandsToDisable.forEach { (command) in
self.disable(command: command)
}
}
internal func disable(commands: [RemoteCommand]) {
@@ -44,6 +53,7 @@ public class RemoteCommandController {
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
@@ -213,7 +223,7 @@ public class RemoteCommandController {
}
else if let error = error as? APError.QueueError {
switch error {
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
return MPRemoteCommandHandlerStatus.noSuchContent
}
}