Compare commits

...

8 Commits

Author SHA1 Message Date
David Chavez 23fdb9b9db Release 0.15.3 2022-09-14 15:02:30 +02:00
Jonathan Puckey 24c19aa661 Remove unnecessary buffer settings from tests. (#26)
As expected, the tests run successfully without these set too. See https://github.com/doublesymmetry/react-native-track-player/pull/1695
2022-09-06 09:15:36 +02:00
Jonathan Puckey 38429c6ca8 Reset AVPlayerWrapper on failure to load pending asset (#25)
See https://github.com/doublesymmetry/react-native-track-player/issues/1538
2022-09-06 09:13:38 +02:00
Jonathan Puckey 72f9c5d147 Fix order of AVPlayerWrapperState.state (#21) 2022-09-06 09:02:45 +02:00
David Chavez bd93898809 fix(tests): run workflows on PRs (#27) 2022-09-06 08:58:59 +02:00
David Chavez 8276f38b1b Release 0.15.2 2022-05-07 21:14:56 +02:00
David Akpan fcd5790e1e fix/ios-hls-live-duration (#18) 2022-05-07 08:59:17 +02:00
Jacob Spizziri ead7c0962e fix(audioplayer): fix loadArtwork method to unset artwork value if no image is given (#17)
https://github.com/doublesymmetry/react-native-track-player/issues/1511
2022-04-30 00:51:14 +02:00
6 changed files with 115 additions and 102 deletions
+9 -2
View File
@@ -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 }}
+29 -31
View File
@@ -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 -26
View File
@@ -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))
}
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.15.1'
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.
*/
@@ -85,7 +109,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
@@ -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) {
+2
View File
@@ -300,6 +300,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
} else {
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
}
}
}