Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40ea7ad2f9 | |||
| f2f1c1236c | |||
| a75f0d0201 | |||
| 9e4e7f6807 | |||
| dbd3b03989 | |||
| 7e19604df7 | |||
| 481130dc58 | |||
| 300b34afa3 | |||
| da3af0e9db | |||
| d9eb313c1b | |||
| cca7f68da4 | |||
| 7ed74b80ec | |||
| 2773e4bfec | |||
| 77dc8f4ff1 | |||
| accdf2c00c | |||
| 542d3a5764 | |||
| 4131e54f3e | |||
| 03c4a7310f | |||
| 9d2d2594a1 | |||
| 4e790876cb | |||
| b19d01bdfc | |||
| 3c8ecb353c | |||
| cafd513468 | |||
| 7b8a4f318d | |||
| acab6473b2 | |||
| 57b6fb08f3 | |||
| 68a15ab3a6 | |||
| 92053a2bd0 | |||
| aeef676164 | |||
| 9c32a86bfa | |||
| ce04a796ee | |||
| 7370ad05e6 | |||
| aedae222b0 | |||
| b2cb178d21 | |||
| 8e62c63fff | |||
| f585d7021c | |||
| 32809366fa | |||
| a6c67e858d | |||
| 071d0e8017 | |||
| 6755694566 | |||
| 7f117b0670 |
@@ -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']
|
||||
@@ -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 }}
|
||||
@@ -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 */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -93,7 +94,8 @@
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -101,7 +103,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
|
||||
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -109,7 +111,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */,
|
||||
9B1D5E1E27C76F5C004CA883 /* 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>";
|
||||
@@ -219,7 +222,7 @@
|
||||
9B05AA2F2660276400C7A389 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9B05AA38266028D600C7A389 /* SwiftAudio */,
|
||||
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -241,7 +244,7 @@
|
||||
);
|
||||
name = SwiftAudio_Example;
|
||||
packageProductDependencies = (
|
||||
9B05AA39266028E200C7A389 /* SwiftAudio */,
|
||||
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
|
||||
);
|
||||
productName = SwiftAudio;
|
||||
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
|
||||
@@ -264,7 +267,7 @@
|
||||
packageProductDependencies = (
|
||||
9B05AA302660276400C7A389 /* Quick */,
|
||||
9B05AA322660276400C7A389 /* Nimble */,
|
||||
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
|
||||
9B1D5E1D27C76F5C004CA883 /* 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",
|
||||
@@ -573,6 +577,7 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -594,6 +599,7 @@
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -668,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 */
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class AudioController {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueViewController: UIViewController {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)?
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -182,6 +193,10 @@ class AVPlayerWrapperTests: XCTestCase {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Quick
|
||||
import Nimble
|
||||
import MediaPlayer
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerEventTests: QuickSpec {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudio
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
|
||||
class QueueManagerTests: QuickSpec {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudio
|
||||
import SwiftAudioEx
|
||||
import UIKit
|
||||
|
||||
struct Source {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.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.
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
+36
-11
@@ -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 {
|
||||
@@ -59,6 +62,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()
|
||||
}
|
||||
@@ -97,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
|
||||
@@ -133,10 +139,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func play() {
|
||||
_playWhenReady = true
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
_playWhenReady = false
|
||||
avPlayer.pause()
|
||||
}
|
||||
|
||||
@@ -157,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -205,6 +218,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
|
||||
|
||||
@@ -228,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)
|
||||
}
|
||||
|
||||
@@ -269,6 +288,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
if currentItem == nil {
|
||||
_state = .idle
|
||||
}
|
||||
else if _pausedForLoad == true {}
|
||||
else {
|
||||
self._state = .paused
|
||||
}
|
||||
@@ -285,6 +305,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
self._pausedForLoad = false
|
||||
if _playWhenReady && (_initialTime ?? 0) == 0 {
|
||||
self.play()
|
||||
}
|
||||
@@ -337,5 +358,9 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -338,6 +365,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)
|
||||
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-16
@@ -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
|
||||
+13
@@ -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.
|
||||
+58
-17
@@ -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()
|
||||
}
|
||||
|
||||
@@ -119,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,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)
|
||||
}
|
||||
|
||||
@@ -163,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,9 +200,35 @@ public class QueuedAudioPlayer: AudioPlayer {
|
||||
|
||||
override func AVWrapperItemDidPlayToEndTime() {
|
||||
super.AVWrapperItemDidPlayToEndTime()
|
||||
if automaticallyPlayNextSong {
|
||||
try? self.next()
|
||||
|
||||
switch repeatMode {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -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.
|
||||
+13
-3
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user