Compare commits

...

22 Commits

Author SHA1 Message Date
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
David Chavez 9c32a86bfa Rename library 2021-08-11 22:25:48 +02:00
David Chavez ce04a796ee Call super on reset of QueuedAudioPlayer 2021-06-26 13:25:00 +02:00
David Chavez 7370ad05e6 Add support for metadata updates (#3) 2021-05-29 22:54:41 +02:00
David Chavez aedae222b0 Move to Github provided machines 2021-05-29 22:27:47 +02:00
David Chavez b2cb178d21 Fix tests 2021-05-29 17:23:51 +02:00
David Chavez 8e62c63fff Disable allowsExternalPlayback 2021-05-29 17:02:43 +02:00
David Chavez f585d7021c Thread safe mutation of now playing info center 2021-05-29 16:59:14 +02:00
David Chavez 32809366fa Allow pausing to be respected if done while loading 2021-05-29 16:43:16 +02:00
David Chavez a6c67e858d Add QueueManagerDelegate to track queue changes 2021-05-29 16:25:31 +02:00
David Chavez 071d0e8017 Add ability to set a repeat mode for QueuedAudioPlayer 2021-05-29 13:55:00 +02:00
David Chavez 6755694566 Add back the podspec 2021-05-29 13:18:02 +02:00
David Chavez 7f117b0670 Remove Codecov (#2) 2021-05-27 23:07:23 +02:00
55 changed files with 491 additions and 138 deletions
+2 -7
View File
@@ -2,10 +2,7 @@ name: validate
on: [push]
jobs:
unit-tests:
runs-on: [self-hosted, macOS, arm64e]
defaults:
run:
shell: "/usr/bin/arch -arch arm64e /bin/bash {0}"
runs-on: macos-latest
strategy:
matrix:
destination:
@@ -20,6 +17,4 @@ jobs:
cd Example
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
env:
destination: ${{ matrix.destination }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.2.1
destination: ${{ matrix.destination }}
+16 -12
View File
@@ -44,8 +44,9 @@
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA39266028E200C7A389 /* SwiftAudio */; };
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA3B26602C0E00C7A389 /* SwiftAudio */; };
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79326C522D0004BAF2F /* SwiftAudioEx */; };
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79526C52382004BAF2F /* SwiftAudioEx */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -94,6 +95,7 @@
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -101,7 +103,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -109,7 +111,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */,
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */,
9B05AA312660276400C7A389 /* Quick in Frameworks */,
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
);
@@ -136,6 +138,7 @@
07756B68218A4E870023935E /* AudioSession.swift */,
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */,
);
path = Mocks;
sourceTree = "<group>";
@@ -241,7 +244,7 @@
);
name = SwiftAudio_Example;
packageProductDependencies = (
9B05AA39266028E200C7A389 /* SwiftAudio */,
9B77D79326C522D0004BAF2F /* SwiftAudioEx */,
);
productName = SwiftAudio;
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
@@ -264,7 +267,7 @@
packageProductDependencies = (
9B05AA302660276400C7A389 /* Quick */,
9B05AA322660276400C7A389 /* Nimble */,
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
9B77D79526C52382004BAF2F /* SwiftAudioEx */,
);
productName = Tests;
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
@@ -376,6 +379,7 @@
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */,
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */,
@@ -528,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",
@@ -547,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",
@@ -668,13 +672,13 @@
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
9B05AA39266028E200C7A389 /* SwiftAudio */ = {
9B77D79326C522D0004BAF2F /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
9B05AA3B26602C0E00C7A389 /* SwiftAudio */ = {
9B77D79526C52382004BAF2F /* 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 {
+1 -1
View File
@@ -7,7 +7,7 @@
//
import UIKit
import SwiftAudio
import SwiftAudioEx
import AVFoundation
import MediaPlayer
@@ -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 {
@@ -47,6 +47,12 @@ class AVPlayerItemObserverTests: QuickSpec {
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var receivedMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
receivedMetadata?(metadata)
}
var updateDuration: ((_ duration: Double) -> Void)?
+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 {
+5 -1
View File
@@ -1,7 +1,7 @@
import AVFoundation
import XCTest
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerWrapperTests: XCTestCase {
@@ -182,6 +182,10 @@ class AVPlayerWrapperTests: XCTestCase {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
}
func AVWrapperDidRecreateAVPlayer() {
}
+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 {
@@ -0,0 +1,17 @@
//
// MockDispatchQueue.swift
// SwiftAudio_Tests
//
// Created by David Chavez on 29.05.21.
// Copyright © 2021 Double Symmmery. All rights reserved.
//
import Foundation
@testable import SwiftAudioEx
final class MockDispatchQueue: DispatchQueueType {
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
work()
}
}
@@ -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 {
@@ -12,7 +12,7 @@ class NowPlayingInfoControllerTests: QuickSpec {
var nowPlayingController: NowPlayingInfoController!
beforeEach {
nowPlayingController = NowPlayingInfoController(infoCenter: NowPlayingInfoCenter_Mock())
nowPlayingController = NowPlayingInfoController(dispatchQueue: MockDispatchQueue(), infoCenter: NowPlayingInfoCenter_Mock())
}
describe("its info dictionary") {
+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 {
+6 -6
View File
@@ -3,17 +3,17 @@ import PackageDescription
let package = Package(
name: "SwiftAudio",
platforms: [.iOS(.v10)],
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")
]
)
)
+5 -2
View File
@@ -88,7 +88,7 @@ let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.add(item: audioItem, playWhenReady: true) // Since this is the first item, we can supply playWhenReady: true to immedietaly start playing when the item is loaded.
```
When a track is done playing, the player will load the next track and update the queue, as long as `automaticallyPlayNextSong` is `true` (default).
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:
@@ -114,6 +114,9 @@ Current options for configuring the `AudioPlayer`:
- `rate`
- `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
@@ -185,4 +188,4 @@ Jørgen Henrichsen
## License
SwiftAudio is available under the MIT license. See the LICENSE file for more info.
SwiftAudio is available under the MIT license. See the LICENSE file for more info.
@@ -1,50 +0,0 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var infoCenter: NowPlayingInfoCenter {
return _infoCenter
}
var info: [String: Any] {
return _info
}
public required init() {
self._infoCenter = MPNowPlayingInfoCenter.default()
}
public required init(infoCenter: NowPlayingInfoCenter) {
self._infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
keyValues.forEach { (keyValue) in
_info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = _info
}
public func set(keyValue: NowPlayingInfoKeyValue) {
_info[keyValue.getKey()] = keyValue.getValue()
self._infoCenter.nowPlayingInfo = _info
}
public func clear() {
self._info = [:]
self._infoCenter.nowPlayingInfo = _info
}
}
+26
View File
@@ -0,0 +1,26 @@
#
# Be sure to run `pod lib lint SwiftAudioEx.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.14.1'
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.
DESC
s.homepage = 'https://github.com/DoubleSymmetry/SwiftAudioEx'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.authors = { 'David Chavez' => 'david@dcvz.io',
'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 = '11.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudioEx/Classes/**/*'
end
@@ -22,6 +22,7 @@ public struct APError {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
case noNextWhenRepeatModeTrack
}
}
@@ -59,6 +59,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
self.playerTimeObserver.delegate = self
self.playerItemNotificationObserver.delegate = self
self.playerItemObserver.delegate = self
// disabled since we're not making use of video playback
self.avPlayer.allowsExternalPlayback = false;
playerTimeObserver.registerForPeriodicTimeEvents()
}
@@ -133,10 +136,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
func play() {
_playWhenReady = true
avPlayer.play()
}
func pause() {
_playWhenReady = false
avPlayer.pause()
}
@@ -205,6 +210,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
self.playerObserver.startObserving()
self.playerItemNotificationObserver.startObserving(item: currentItem)
self.playerItemObserver.startObserving(item: currentItem)
for format in pendingAsset.availableMetadataFormats {
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
}
}
break
@@ -337,5 +345,9 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
func item(didUpdateDuration duration: Double) {
self.delegate?.AVWrapper(didUpdateDuration: duration)
}
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
}
}
@@ -6,6 +6,7 @@
//
import Foundation
import MediaPlayer
protocol AVPlayerWrapperDelegate: class {
@@ -15,6 +16,7 @@ protocol AVPlayerWrapperDelegate: class {
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
func AVWrapperItemDidPlayToEndTime()
func AVWrapperDidRecreateAVPlayer()
@@ -115,9 +115,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 +234,14 @@ 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.
*/
public func syncRemoteCommandsWithCommandCenter() {
self.enableRemoteCommands(remoteCommands)
}
// MARK: - NowPlayingInfo
@@ -311,7 +327,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()
}
@@ -338,6 +358,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
}
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
self.event.receiveMetadata.emit(data: metadata)
}
func AVWrapperItemDidPlayToEndTime() {
self.event.playbackEnd.emit(data: .playedUntilEnd)
@@ -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
@@ -6,6 +6,7 @@
//
import Foundation
import MediaPlayer
extension AudioPlayer {
@@ -15,7 +16,9 @@ extension AudioPlayer {
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
public struct EventHolder {
@@ -55,6 +58,12 @@ extension AudioPlayer {
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
/**
Emitted when the player receives metadata.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let receiveMetadata: AudioPlayer.Event<MetadataEventData> = AudioPlayer.Event()
/**
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
@@ -62,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
@@ -0,0 +1,71 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private let concurrentInfoQueue: DispatchQueueType
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var infoCenter: NowPlayingInfoCenter {
return _infoCenter
}
var info: [String: Any] {
return _info
}
public required init() {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = MPNowPlayingInfoCenter.default()
}
/// Used for testing purposes.
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = dispatchQueue
self._infoCenter = infoCenter
}
public required init(infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
keyValues.forEach { (keyValue) in
self._info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = self._info
}
}
public func set(keyValue: NowPlayingInfoKeyValue) {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info[keyValue.getKey()] = keyValue.getValue()
self._infoCenter.nowPlayingInfo = self._info
}
}
public func clear() {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info = [:]
self._infoCenter.nowPlayingInfo = self._info
}
}
}
@@ -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
@@ -14,6 +14,11 @@ protocol AVPlayerItemObserverDelegate: class {
Called when the observed item updates the duration.
*/
func item(didUpdateDuration duration: Double)
/**
Called when the observed item receives metadata
*/
func item(didReceiveMetadata metadata: [AVMetadataItem])
}
@@ -28,6 +33,7 @@ class AVPlayerItemObserver: NSObject {
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
}
private(set) var isObserving: Bool = false
@@ -50,6 +56,7 @@ class AVPlayerItemObserver: NSObject {
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
}
func stopObservingCurrentItem() {
@@ -58,6 +65,7 @@ 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.timedMetadata, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
@@ -78,6 +86,11 @@ class AVPlayerItemObserver: NSObject {
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
self.delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.timedMetadata:
if let metadata = change?[.newKey] as? [AVMetadataItem] {
self.delegate?.item(didReceiveMetadata: metadata)
}
default: break
}
@@ -7,10 +7,22 @@
import Foundation
protocol QueueManagerDelegate: AnyObject {
func onReceivedFirstItem()
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int)
}
class QueueManager<T> {
private var _items: [T] = []
weak var delegate: QueueManagerDelegate? = nil
private var _items: [T] = [] {
didSet {
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
delegate?.onReceivedFirstItem()
}
}
}
/**
All items held by the queue.
@@ -33,7 +45,11 @@ class QueueManager<T> {
return Array(_items[0..<_currentIndex])
}
private var _currentIndex: Int = 0
private var _currentIndex: Int = 0 {
didSet {
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
}
}
/**
The index of the current item.
@@ -11,15 +11,17 @@ 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>()
/**
Set wether the player should automatically play the next song when a song is finished.
Default is `true`.
*/
public var automaticallyPlayNextSong: Bool = true
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
queueManager.delegate = self
}
/// The repeat mode for the queue player.
public var repeatMode: RepeatMode = .off
public override var currentItem: AudioItem? {
return queueManager.current
@@ -37,9 +39,11 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
public override func stop() {
super.stop()
self.event.queueIndex.emit(data: (currentIndex, nil))
}
override func reset() {
super.reset()
queueManager.clearQueue()
}
@@ -120,8 +124,19 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
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()
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
} catch APError.QueueError.noNextItem {
if repeatMode == .queue {
try jumpToItem(atIndex: 0, playWhenReady: true)
} else {
throw APError.QueueError.noNextItem
}
} catch {
throw error
}
}
/**
@@ -130,7 +145,7 @@ public class QueuedAudioPlayer: AudioPlayer {
public func previous() throws {
event.playbackEnd.emit(data: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
try self.load(item: previousItem, playWhenReady: repeatMode != .track)
}
/**
@@ -185,9 +200,30 @@ public class QueuedAudioPlayer: AudioPlayer {
override func AVWrapperItemDidPlayToEndTime() {
super.AVWrapperItemDidPlayToEndTime()
if automaticallyPlayNextSong {
try? self.next()
switch repeatMode {
case .off: try? self.next()
case .track:
seek(to: 0)
play()
case .queue:
do {
try self.next()
} 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))
}
}
@@ -213,7 +213,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
}
}
+15
View File
@@ -0,0 +1,15 @@
//
// RepeatMode.swift
// SwiftAudio
//
// Created by David Chavez on 29.05.21.
// Copyright © 2021 Double Symmmery. All rights reserved.
//
import Foundation
public enum RepeatMode: Int {
case off
case track
case queue
}
@@ -0,0 +1,18 @@
//
// DispatchQueueType.swift
// SwiftAudio
//
// Created by David Chavez on 29.05.21.
//
import Foundation
public protocol DispatchQueueType {
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void)
}
extension DispatchQueue: DispatchQueueType {
public func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
async(group: nil, qos: .unspecified, flags: flags, execute: work)
}
}