Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23fdb9b9db | |||
| 24c19aa661 | |||
| 38429c6ca8 | |||
| 72f9c5d147 | |||
| bd93898809 |
@@ -1,5 +1,12 @@
|
||||
name: validate
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: macos-latest
|
||||
@@ -17,4 +24,4 @@ jobs:
|
||||
cd Example
|
||||
xcodebuild test -scheme SwiftAudio-Example -destination "${destination}" -enableCodeCoverage YES
|
||||
env:
|
||||
destination: ${{ matrix.destination }}
|
||||
destination: ${{ matrix.destination }}
|
||||
|
||||
@@ -6,34 +6,32 @@ import XCTest
|
||||
@testable import SwiftAudioEx
|
||||
|
||||
class AudioPlayerTests: XCTestCase {
|
||||
|
||||
|
||||
var audioPlayer: AudioPlayer!
|
||||
var listener: AudioPlayerEventListener!
|
||||
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
audioPlayer = AudioPlayer()
|
||||
audioPlayer.volume = 0.0
|
||||
audioPlayer.bufferDuration = 0.001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
|
||||
}
|
||||
|
||||
|
||||
override func tearDown() {
|
||||
audioPlayer = nil
|
||||
listener = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__should_be_idle() {
|
||||
XCTAssert(audioPlayer.playerState == AudioPlayerState.idle)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_loading() {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
XCTAssertEqual(audioPlayer.playerState, AudioPlayerState.loading)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source__should_be_ready() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -45,7 +43,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__load_source_playWhenReady__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -57,7 +55,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__play_source__should_be_playing() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { state in
|
||||
@@ -70,7 +68,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__pausing_source__should_be_paused() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -83,7 +81,7 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__state__stopping_source__should_be_idle() {
|
||||
let expectation = XCTestExpectation()
|
||||
var hasBeenPlaying: Bool = false
|
||||
@@ -102,13 +100,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Current time
|
||||
|
||||
|
||||
func test_AudioPlayer__currentTime__should_be_0() {
|
||||
XCTAssert(audioPlayer.currentTime == 0.0)
|
||||
}
|
||||
|
||||
|
||||
// Commented out -- Keeps failing in CI at Bitrise, but succeeds locally, even with Bitrise CLI.
|
||||
// func test_AudioPlayer__currentTime__playing_source__shold_be_greater_than_0() {
|
||||
// let expectation = XCTestExpectation()
|
||||
@@ -121,13 +119,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
// try? audioPlayer.load(item: LongSource.getAudioItem(), playWhenReady: true)
|
||||
// wait(for: [expectation], timeout: 20.0)
|
||||
// }
|
||||
|
||||
|
||||
// MARK: - Rate
|
||||
|
||||
|
||||
func test_AudioPlayer__rate__should_be_1() {
|
||||
XCTAssert(audioPlayer.rate == 1.0)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__rate__playing_source__should_be_1() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -143,13 +141,13 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Current item
|
||||
|
||||
|
||||
func test_AudioPlayer__currentItem__should_be_nil() {
|
||||
XCTAssertNil(audioPlayer.currentItem)
|
||||
}
|
||||
|
||||
|
||||
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
|
||||
let expectation = XCTestExpectation()
|
||||
listener.stateUpdate = { [weak audioPlayer] state in
|
||||
@@ -165,11 +163,11 @@ class AudioPlayerTests: XCTestCase {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
wait(for: [expectation], timeout: 20.0)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class AudioPlayerEventListener {
|
||||
|
||||
|
||||
var state: AudioPlayerState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
@@ -177,35 +175,35 @@ class AudioPlayerEventListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
|
||||
var seekCompletion: (() -> Void)?
|
||||
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
|
||||
audioPlayer.event.seek.addListener(self, handleSeek)
|
||||
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
audioPlayer?.event.stateChange.removeListener(self)
|
||||
audioPlayer?.event.seek.removeListener(self)
|
||||
audioPlayer?.event.secondElapse.removeListener(self)
|
||||
}
|
||||
|
||||
|
||||
func handleDidUpdateState(state: AudioPlayerState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
|
||||
func handleSeek(data: AudioPlayer.SeekEventData) {
|
||||
seekCompletion?()
|
||||
}
|
||||
|
||||
|
||||
func handleSecondsElapse(data: AudioPlayer.SecondElapseEventData) {
|
||||
self.secondsElapse?(data)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -24,15 +24,13 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
var audioPlayer: QueuedAudioPlayer!
|
||||
beforeEach {
|
||||
audioPlayer = QueuedAudioPlayer()
|
||||
audioPlayer.bufferDuration = 0.0001
|
||||
audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
audioPlayer.volume = 0.0
|
||||
}
|
||||
describe("its current item") {
|
||||
it("should be nil") {
|
||||
expect(audioPlayer.currentItem).to(beNil())
|
||||
}
|
||||
|
||||
|
||||
context("when adding one item") {
|
||||
var item: AudioItem!
|
||||
beforeEach {
|
||||
@@ -42,18 +40,18 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should not be nil") {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
}
|
||||
|
||||
|
||||
context("then loading a new item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
|
||||
it("should have replaced the item") {
|
||||
expect(audioPlayer.currentItem?.getSourceUrl()).toNot(equal(item.getSourceUrl()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("when adding multiple items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
|
||||
@@ -63,12 +61,12 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("its next items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
|
||||
@@ -76,7 +74,7 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should contain 1 item") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(1))
|
||||
}
|
||||
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
@@ -84,7 +82,7 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
it("should contain 0 items") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("then calling previous()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.previous()
|
||||
@@ -94,17 +92,17 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing one item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.removeItem(at: 1)
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then jumping to the last item") {
|
||||
beforeEach {
|
||||
try? audioPlayer.jumpToItem(atIndex: 1)
|
||||
@@ -113,43 +111,43 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing upcoming items") {
|
||||
beforeEach {
|
||||
audioPlayer.removeUpcomingItems()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.nextItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("its previous items") {
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("when adding 2 items") {
|
||||
beforeEach {
|
||||
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
|
||||
|
||||
context("then calling next()") {
|
||||
beforeEach {
|
||||
try? audioPlayer.next()
|
||||
@@ -158,27 +156,27 @@ class QueuedAudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.previousItems.count).to(equal(1))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then removing all previous items") {
|
||||
beforeEach {
|
||||
audioPlayer.removePreviousItems()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("then stopping") {
|
||||
beforeEach {
|
||||
audioPlayer.stop()
|
||||
}
|
||||
|
||||
|
||||
it("should be empty") {
|
||||
expect(audioPlayer.previousItems.count).to(equal(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioEx'
|
||||
s.version = '0.15.2'
|
||||
s.version = '0.15.3'
|
||||
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.
|
||||
|
||||
@@ -64,6 +64,30 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate(set) var lastPlayerTimeControlStatus: AVPlayer.TimeControlStatus = AVPlayer.TimeControlStatus.paused {
|
||||
didSet {
|
||||
if oldValue != lastPlayerTimeControlStatus {
|
||||
switch lastPlayerTimeControlStatus {
|
||||
case .paused:
|
||||
if pendingAsset == nil {
|
||||
state = .idle
|
||||
}
|
||||
else if currentItem != nil && pausedForLoad != true {
|
||||
state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
if pendingAsset != nil {
|
||||
state = .buffering
|
||||
}
|
||||
case .playing:
|
||||
state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
@@ -194,40 +218,38 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let isPendingAsset = (self.pendingAsset != nil && pendingAsset.isEqual(self.pendingAsset))
|
||||
if (pendingAsset != self.pendingAsset) { return; }
|
||||
switch status {
|
||||
case .loaded:
|
||||
if isPendingAsset {
|
||||
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
||||
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: currentItem)
|
||||
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
self.playerItemObserver.startObserving(item: currentItem)
|
||||
let item = AVPlayerItem(
|
||||
asset: pendingAsset,
|
||||
automaticallyLoadedAssetKeys: [Constants.assetPlayableKey]
|
||||
)
|
||||
item.preferredForwardBufferDuration = self.bufferDuration
|
||||
self.avPlayer.replaceCurrentItem(with: item)
|
||||
// Register for events
|
||||
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
self.playerObserver.startObserving()
|
||||
self.playerItemNotificationObserver.startObserving(item: item)
|
||||
self.playerItemObserver.startObserving(item: item)
|
||||
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
|
||||
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: [group])
|
||||
}
|
||||
if pendingAsset.availableChapterLocales.count > 0 {
|
||||
for locale in pendingAsset.availableChapterLocales {
|
||||
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: chapters)
|
||||
}
|
||||
} else {
|
||||
for format in pendingAsset.availableMetadataFormats {
|
||||
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
|
||||
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
|
||||
self.delegate?.AVWrapper(didReceiveMetadata: [group])
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case .failed:
|
||||
if isPendingAsset {
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
self.pendingAsset = nil
|
||||
}
|
||||
self.reset(soft: false)
|
||||
self.delegate?.AVWrapper(failedWithError: error)
|
||||
break
|
||||
|
||||
case .cancelled:
|
||||
@@ -282,21 +304,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
state = .idle
|
||||
}
|
||||
else if pausedForLoad != true {
|
||||
state = .paused
|
||||
}
|
||||
case .waitingToPlayAtSpecifiedRate:
|
||||
state = .buffering
|
||||
case .playing:
|
||||
state = .playing
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
lastPlayerTimeControlStatus = status;
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
|
||||
Reference in New Issue
Block a user