From d715c6051097749c08f12d0d5a25f11559edc48e Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Tue, 29 Dec 2020 16:44:54 +0200 Subject: [PATCH 1/9] feat: add accurate seek api --- Example/RemoteCommandExample.swift | 6 ++-- Sources/Core/Components/PlayerCommand.swift | 2 +- Sources/Core/Components/PlayerContext.swift | 8 ++--- .../ModernAVPlayerRemoteCommandFactory.swift | 6 ++-- Sources/Core/ModernAVPlayer.swift | 32 ++++++++++++++++--- Sources/Core/State/BufferingState.swift | 10 +++--- Sources/Core/State/FailedState.swift | 2 +- Sources/Core/State/InitState.swift | 2 +- Sources/Core/State/LoadedState.swift | 6 ++-- Sources/Core/State/LoadingMediaState.swift | 2 +- Sources/Core/State/PausedState.swift | 6 ++-- Sources/Core/State/PlayingState.swift | 6 ++-- Sources/Core/State/StoppedState.swift | 2 +- Sources/Core/State/WaitingNetworkState.swift | 2 +- 14 files changed, 61 insertions(+), 31 deletions(-) diff --git a/Example/RemoteCommandExample.swift b/Example/RemoteCommandExample.swift index 693ec92a..67c0d31d 100644 --- a/Example/RemoteCommandExample.swift +++ b/Example/RemoteCommandExample.swift @@ -126,7 +126,7 @@ public struct RemoteCommandFactoryExample { guard let e = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } let position = e.positionTime - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) @@ -145,7 +145,7 @@ public struct RemoteCommandFactoryExample { else { return .commandFailed } let position = max(self.player.currentTime - skipTime, 0) - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) @@ -164,7 +164,7 @@ public struct RemoteCommandFactoryExample { else { return .commandFailed } let position = self.player.currentTime + skipTime - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) diff --git a/Sources/Core/Components/PlayerCommand.swift b/Sources/Core/Components/PlayerCommand.swift index 7e133116..8071c304 100644 --- a/Sources/Core/Components/PlayerCommand.swift +++ b/Sources/Core/Components/PlayerCommand.swift @@ -30,6 +30,6 @@ public protocol PlayerCommand { func load(media: PlayerMedia, autostart: Bool, position: Double?) func pause() func play() - func seek(position: Double) + func seek(position: Double, isAccurate: Bool) func stop() } diff --git a/Sources/Core/Components/PlayerContext.swift b/Sources/Core/Components/PlayerContext.swift index 5cdae301..bb58700c 100644 --- a/Sources/Core/Components/PlayerContext.swift +++ b/Sources/Core/Components/PlayerContext.swift @@ -146,14 +146,14 @@ final class ModernAVPlayerContext: NSObject, PlayerContext { state.pause() } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { guard let item = currentItem else { unaivalableCommand(reason: .loadMediaFirst); return } let seekService = ModernAVPlayerSeekService(preferredTimescale: config.preferredTimescale) let seekPosition = seekService.boundedPosition(position, item: item) if let boundedPosition = seekPosition.value { - state.seek(position: boundedPosition) + state.seek(position: boundedPosition, isAccurate: isAccurate) } else if let reason = seekPosition.reason { unaivalableCommand(reason: reason) } else { @@ -161,9 +161,9 @@ final class ModernAVPlayerContext: NSObject, PlayerContext { } } - func seek(offset: Double) { + func seek(offset: Double, isAccurate: Bool) { let position = currentTime + offset - seek(position: position) + seek(position: position, isAccurate: isAccurate) } func stop() { diff --git a/Sources/Core/Components/RemoteCommand/ModernAVPlayerRemoteCommandFactory.swift b/Sources/Core/Components/RemoteCommand/ModernAVPlayerRemoteCommandFactory.swift index 002dfa75..0b9fd134 100644 --- a/Sources/Core/Components/RemoteCommand/ModernAVPlayerRemoteCommandFactory.swift +++ b/Sources/Core/Components/RemoteCommand/ModernAVPlayerRemoteCommandFactory.swift @@ -165,7 +165,7 @@ public class ModernAVPlayerRemoteCommandFactory { let position = e.positionTime ModernAVPlayerLogger.instance.log(message: "Remote command: seek to \(position)", domain: .service) - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) @@ -191,7 +191,7 @@ public class ModernAVPlayerRemoteCommandFactory { ModernAVPlayerLogger.instance.log(message: "Remote command: skipBackward", domain: .service) let position = max(self.player.currentTime - skipTime, 0) - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) @@ -217,7 +217,7 @@ public class ModernAVPlayerRemoteCommandFactory { ModernAVPlayerLogger.instance.log(message: "Remote command: skipForward", domain: .service) let position = self.player.currentTime + skipTime - self.player.seek(position: position) + self.player.seek(position: position, isAccurate: false) return .success } command.addTarget(handler: handler) diff --git a/Sources/Core/ModernAVPlayer.swift b/Sources/Core/ModernAVPlayer.swift index bde2657d..7727a442 100644 --- a/Sources/Core/ModernAVPlayer.swift +++ b/Sources/Core/ModernAVPlayer.swift @@ -102,21 +102,45 @@ public final class ModernAVPlayer: NSObject, ModernAVPlayerExposable { /// /// Sets the media current time to the specified position /// - /// - Note: position is bounded between 0 and end time or available ranges + /// - Note: position is bounded between 0 and end time or available ranges, resulting seek position may differ slightly for efficiency /// - parameter position: time to seek /// public func seek(position: Double) { - context.seek(position: position) + context.seek(position: position, isAccurate: false) + } + + /// + /// Sets the media current time to the specified position + /// + /// - Note: position is bounded between 0 and end time or available ranges + /// - parameter position: time to seek + /// - parameter isAccurate: pass true if you desire presice seeking, may incur additional decoding delay + /// which can impact seeking performance, default is false + /// + public func seek(position: Double, isAccurate: Bool = false) { + context.seek(position: position, isAccurate: isAccurate) } /// /// Apply offset to the media current time /// - /// - Note: this method compute position then call then seek(position:) + /// - Note: this method compute position then call then seek(position:), resulting seek position may differ slightly for efficiency /// - parameter offset: offset to apply /// public func seek(offset: Double) { - context.seek(offset: offset) + context.seek(offset: offset, isAccurate: false) + } + + /// + /// Apply offset to the media current time + /// + /// - Note: this method compute position then call then seek(position:), resulting seek position may differ slightly for efficiency + /// - parameter offset: offset to apply + /// - parameter isAccurate: pass true if you desire presice seeking, + /// may incur additional decoding delay which can impact seeking performance, default is false + /// + public func seek(offset: Double, isAccurate: Bool = false) { + context.seek(offset: offset, isAccurate: isAccurate) } /// Stops playback of the current item then seek to 0 diff --git a/Sources/Core/State/BufferingState.swift b/Sources/Core/State/BufferingState.swift index 75438a5e..573a5bc2 100644 --- a/Sources/Core/State/BufferingState.swift +++ b/Sources/Core/State/BufferingState.swift @@ -103,10 +103,12 @@ final class BufferingState: NSObject, PlayerState { context.player.play() } - func seekCommand(position: Double) { + func seekCommand(position: Double, isAccurate: Bool) { context.currentItem?.cancelPendingSeeks() let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - context.player.seek(to: time) { [weak self] completed in + let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity + let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [weak self] completed in guard completed, let strongSelf = self else { return } strongSelf.context.delegate?.playerContext(didCurrentTimeChange: strongSelf.context.currentTime) strongSelf.playCommand() @@ -130,8 +132,8 @@ final class BufferingState: NSObject, PlayerState { context.delegate?.playerContext(unavailableActionReason: .alreadyTryingToPlay) } - func seek(position: Double) { - seekCommand(position: position) + func seek(position: Double, isAccurate: Bool) { + seekCommand(position: position, isAccurate: isAccurate) } func stop() { diff --git a/Sources/Core/State/FailedState.swift b/Sources/Core/State/FailedState.swift index f092a463..18d4cc75 100644 --- a/Sources/Core/State/FailedState.swift +++ b/Sources/Core/State/FailedState.swift @@ -80,7 +80,7 @@ final class FailedState: PlayerState { context.changeState(state: state) } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let debug = "Unable to seek, load a media first" ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand) context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst) diff --git a/Sources/Core/State/InitState.swift b/Sources/Core/State/InitState.swift index 2230957a..597667e6 100644 --- a/Sources/Core/State/InitState.swift +++ b/Sources/Core/State/InitState.swift @@ -64,7 +64,7 @@ struct InitState: PlayerState { context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst) } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let debug = "Load item before seeking" ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand) context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst) diff --git a/Sources/Core/State/LoadedState.swift b/Sources/Core/State/LoadedState.swift index 07d8661c..f8ee1039 100644 --- a/Sources/Core/State/LoadedState.swift +++ b/Sources/Core/State/LoadedState.swift @@ -74,9 +74,11 @@ struct LoadedState: PlayerState { state.playCommand() } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - context.player.seek(to: time) { [context] completed in + let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity + let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [context] completed in guard completed else { return } context.delegate?.playerContext(didCurrentTimeChange: context.currentTime) context.nowPlaying.overrideInfoCenter(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, diff --git a/Sources/Core/State/LoadingMediaState.swift b/Sources/Core/State/LoadingMediaState.swift index 131c29ac..306c157d 100644 --- a/Sources/Core/State/LoadingMediaState.swift +++ b/Sources/Core/State/LoadingMediaState.swift @@ -113,7 +113,7 @@ final class LoadingMediaState: PlayerState { context.delegate?.playerContext(unavailableActionReason: .waitLoadedMedia) } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let debug = "Wait media to be loaded before seeking" ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand) context.delegate?.playerContext(unavailableActionReason: .waitLoadedMedia) diff --git a/Sources/Core/State/PausedState.swift b/Sources/Core/State/PausedState.swift index c9352466..88aef02b 100644 --- a/Sources/Core/State/PausedState.swift +++ b/Sources/Core/State/PausedState.swift @@ -94,9 +94,11 @@ class PausedState: PlayerState { } } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - context.player.seek(to: time) { [weak self] completed in + let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity + let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [weak self] completed in guard completed, let context = self?.context else { return } context.delegate?.playerContext(didCurrentTimeChange: context.currentTime) context.nowPlaying.overrideInfoCenter(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, diff --git a/Sources/Core/State/PlayingState.swift b/Sources/Core/State/PlayingState.swift index 9e73dacc..30c7665f 100644 --- a/Sources/Core/State/PlayingState.swift +++ b/Sources/Core/State/PlayingState.swift @@ -113,10 +113,10 @@ final class PlayingState: PlayerState { context.delegate?.playerContext(unavailableActionReason: .alreadyPlaying) } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let state = BufferingState(context: context) changeState(state: state) - state.seekCommand(position: position) + state.seekCommand(position: position, isAccurate: isAccurate) } func stop() { @@ -142,7 +142,7 @@ final class PlayingState: PlayerState { $0.didItemPlayToEndTime(media: media, endTime: strongSelf.context.currentTime) } if strongSelf.context.loopMode { - strongSelf.seek(position: 0) + strongSelf.seek(position: 0, isAccurate: false) } else { strongSelf.stop() } diff --git a/Sources/Core/State/StoppedState.swift b/Sources/Core/State/StoppedState.swift index 68f260fd..6be1f3e4 100644 --- a/Sources/Core/State/StoppedState.swift +++ b/Sources/Core/State/StoppedState.swift @@ -34,7 +34,7 @@ final class StoppedState: PausedState { init(context: PlayerContext) { super.init(context: context, type: .stopped) - seek(position: 0) + seek(position: 0, isAccurate: false) } override func contextUpdated() { diff --git a/Sources/Core/State/WaitingNetworkState.swift b/Sources/Core/State/WaitingNetworkState.swift index 4ab0764e..d432be73 100644 --- a/Sources/Core/State/WaitingNetworkState.swift +++ b/Sources/Core/State/WaitingNetworkState.swift @@ -110,7 +110,7 @@ final class WaitingNetworkState: PlayerState { context.delegate?.playerContext(unavailableActionReason: .waitEstablishedNetwork) } - func seek(position: Double) { + func seek(position: Double, isAccurate: Bool) { let debug = "Reload a media first before seeking" ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand) context.delegate?.playerContext(unavailableActionReason: .waitEstablishedNetwork) From bcc9446c04ce84964c88261683578a6c4b1bea41 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Mon, 11 Jan 2021 16:33:25 +0200 Subject: [PATCH 2/9] chore: update changelog --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49443bbe..6683c839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,10 @@ All notable changes to this project will be documented in this file. --- -## [1.7.3] -* Fix: - * Resume audio on device background mode -* Breaking change: - * `PlayerConfiguration` has a new `AVAudioSessionCategoryOptions` attribute - +## [X.X.X] +* Feature: + * Accurate seek api + ## [1.7.2] * Feature: * tvOS support [@yaroslavlvov] From 1827a1c55f9ee10ea75db4d1f626a802d9a6e3b4 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Mon, 11 Jan 2021 16:38:23 +0200 Subject: [PATCH 3/9] fix: changelog --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6683c839..42e958e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,15 @@ All notable changes to this project will be documented in this file. --- ## [X.X.X] -* Feature: - * Accurate seek api - + * Feature: + * Accurate seek api + +## [1.7.3] + * Fix: + * Resume audio on device background mode + * Breaking change: + * `PlayerConfiguration` has a new `AVAudioSessionCategoryOptions` attribute + ## [1.7.2] * Feature: * tvOS support [@yaroslavlvov] From a281ac2deb33019d0c208d93374bd1838bff416c Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Mon, 11 Jan 2021 16:39:24 +0200 Subject: [PATCH 4/9] fix: changelog --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42e958e0..5b27134f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,14 @@ All notable changes to this project will be documented in this file. --- ## [X.X.X] - * Feature: - * Accurate seek api +* Feature: + * Accurate seek api ## [1.7.3] - * Fix: - * Resume audio on device background mode - * Breaking change: - * `PlayerConfiguration` has a new `AVAudioSessionCategoryOptions` attribute +* Fix: + * Resume audio on device background mode +* Breaking change: + * `PlayerConfiguration` has a new `AVAudioSessionCategoryOptions` attribute ## [1.7.2] * Feature: From f8f1ec3b53e77a3eae88019f56062fc1a84ba495 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Thu, 21 Jan 2021 12:30:19 +0200 Subject: [PATCH 5/9] chore: add deprecated message to redundant seek api --- Sources/Core/ModernAVPlayer.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Core/ModernAVPlayer.swift b/Sources/Core/ModernAVPlayer.swift index 7727a442..a55e7775 100644 --- a/Sources/Core/ModernAVPlayer.swift +++ b/Sources/Core/ModernAVPlayer.swift @@ -105,6 +105,7 @@ public final class ModernAVPlayer: NSObject, ModernAVPlayerExposable { /// - Note: position is bounded between 0 and end time or available ranges, resulting seek position may differ slightly for efficiency /// - parameter position: time to seek /// + @available(*, deprecated, message: "use func seek(position: Double, isAccurate: Bool = false) instead") public func seek(position: Double) { context.seek(position: position, isAccurate: false) } @@ -127,6 +128,7 @@ public final class ModernAVPlayer: NSObject, ModernAVPlayerExposable { /// - Note: this method compute position then call then seek(position:), resulting seek position may differ slightly for efficiency /// - parameter offset: offset to apply /// + @available(*, deprecated, message: "use func seek(offset: Double, isAccurate: Bool = false) instead") public func seek(offset: Double) { context.seek(offset: offset, isAccurate: false) } From 5aa8da5486d35c05c018b63007769899304326c3 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Thu, 21 Jan 2021 12:33:00 +0200 Subject: [PATCH 6/9] refactor: remove redundant tolarance variable that is passed to avplayer seek --- Sources/Core/State/BufferingState.swift | 5 ++--- Sources/Core/State/LoadedState.swift | 5 ++--- Sources/Core/State/PausedState.swift | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Sources/Core/State/BufferingState.swift b/Sources/Core/State/BufferingState.swift index 573a5bc2..9151149b 100644 --- a/Sources/Core/State/BufferingState.swift +++ b/Sources/Core/State/BufferingState.swift @@ -106,9 +106,8 @@ final class BufferingState: NSObject, PlayerState { func seekCommand(position: Double, isAccurate: Bool) { context.currentItem?.cancelPendingSeeks() let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity - let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity - context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [weak self] completed in + let tolerance: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: tolerance, toleranceAfter: tolerance) { [weak self] completed in guard completed, let strongSelf = self else { return } strongSelf.context.delegate?.playerContext(didCurrentTimeChange: strongSelf.context.currentTime) strongSelf.playCommand() diff --git a/Sources/Core/State/LoadedState.swift b/Sources/Core/State/LoadedState.swift index f8ee1039..0712621c 100644 --- a/Sources/Core/State/LoadedState.swift +++ b/Sources/Core/State/LoadedState.swift @@ -76,9 +76,8 @@ struct LoadedState: PlayerState { func seek(position: Double, isAccurate: Bool) { let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity - let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity - context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [context] completed in + let tolerance: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: tolerance, toleranceAfter: tolerance) { [context] completed in guard completed else { return } context.delegate?.playerContext(didCurrentTimeChange: context.currentTime) context.nowPlaying.overrideInfoCenter(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, diff --git a/Sources/Core/State/PausedState.swift b/Sources/Core/State/PausedState.swift index 88aef02b..2b581335 100644 --- a/Sources/Core/State/PausedState.swift +++ b/Sources/Core/State/PausedState.swift @@ -96,9 +96,8 @@ class PausedState: PlayerState { func seek(position: Double, isAccurate: Bool) { let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale) - let toleranceBefore: CMTime = isAccurate ? .zero : .positiveInfinity - let toleranceAfter: CMTime = isAccurate ? .zero : .positiveInfinity - context.player.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) { [weak self] completed in + let tolerance: CMTime = isAccurate ? .zero : .positiveInfinity + context.player.seek(to: time, toleranceBefore: tolerance, toleranceAfter: tolerance) { [weak self] completed in guard completed, let context = self?.context else { return } context.delegate?.playerContext(didCurrentTimeChange: context.currentTime) context.nowPlaying.overrideInfoCenter(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, From a689fc840140f0ceddcabc2822737f1b4a89b070 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Thu, 21 Jan 2021 12:35:22 +0200 Subject: [PATCH 7/9] fix: try to force play() inside rateObserving service if rate is 0, before reloading AVPLayerItem --- Sources/Core/Services/RateObservingService.swift | 6 +++++- Sources/Core/State/BufferingState.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Core/Services/RateObservingService.swift b/Sources/Core/Services/RateObservingService.swift index 85e4fe10..171dd567 100644 --- a/Sources/Core/Services/RateObservingService.swift +++ b/Sources/Core/Services/RateObservingService.swift @@ -39,6 +39,7 @@ final class ModernAVPlayerRateObservingService: RateObservingService { // MARK: - Inputs private let item: AVPlayerItem + private let player: AVPlayer private let timeInterval: TimeInterval private let timeout: TimeInterval private let timerFactory: TimerFactory @@ -55,12 +56,13 @@ final class ModernAVPlayerRateObservingService: RateObservingService { // MARK: - Lifecycle - init(config: PlayerConfiguration, item: AVPlayerItem, timerFactory: TimerFactory = ModernAVPlayerTimerFactory()) { + init(config: PlayerConfiguration, item: AVPlayerItem, timerFactory: TimerFactory = ModernAVPlayerTimerFactory(), player: AVPlayer) { ModernAVPlayerLogger.instance.log(message: "Init", domain: .lifecycleService) timeInterval = config.rateObservingTickTime timeout = config.rateObservingTimeout self.timerFactory = timerFactory self.item = item + self.player = player } deinit { @@ -102,6 +104,8 @@ final class ModernAVPlayerRateObservingService: RateObservingService { timer?.invalidate() onTimeout?() } else { + //try to force play if playback stalled + player.play() ModernAVPlayerLogger.instance.log(message: "Remaining time: \(remainingTime)", domain: .service) } } diff --git a/Sources/Core/State/BufferingState.swift b/Sources/Core/State/BufferingState.swift index 9151149b..ad6d3d77 100644 --- a/Sources/Core/State/BufferingState.swift +++ b/Sources/Core/State/BufferingState.swift @@ -49,7 +49,7 @@ final class BufferingState: NSObject, PlayerState { guard let item = context.currentItem else { fatalError("item should exist") } self.context = context - self.rateObservingService = rateObservingService ?? ModernAVPlayerRateObservingService(config: context.config, item: item) + self.rateObservingService = rateObservingService ?? ModernAVPlayerRateObservingService(config: context.config, item: item, player: context.player) self.interruptionAudioService = interruptionAudioService super.init() From 2f3183eef3f50ec726a90a746ff075be9053ebc9 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Wed, 16 Jun 2021 13:20:58 +0300 Subject: [PATCH 8/9] Revert "fix: try to force play() inside rateObserving service if rate is 0, before reloading AVPLayerItem" This reverts commit a689fc840140f0ceddcabc2822737f1b4a89b070. --- Sources/Core/Services/RateObservingService.swift | 6 +----- Sources/Core/State/BufferingState.swift | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/Core/Services/RateObservingService.swift b/Sources/Core/Services/RateObservingService.swift index 171dd567..85e4fe10 100644 --- a/Sources/Core/Services/RateObservingService.swift +++ b/Sources/Core/Services/RateObservingService.swift @@ -39,7 +39,6 @@ final class ModernAVPlayerRateObservingService: RateObservingService { // MARK: - Inputs private let item: AVPlayerItem - private let player: AVPlayer private let timeInterval: TimeInterval private let timeout: TimeInterval private let timerFactory: TimerFactory @@ -56,13 +55,12 @@ final class ModernAVPlayerRateObservingService: RateObservingService { // MARK: - Lifecycle - init(config: PlayerConfiguration, item: AVPlayerItem, timerFactory: TimerFactory = ModernAVPlayerTimerFactory(), player: AVPlayer) { + init(config: PlayerConfiguration, item: AVPlayerItem, timerFactory: TimerFactory = ModernAVPlayerTimerFactory()) { ModernAVPlayerLogger.instance.log(message: "Init", domain: .lifecycleService) timeInterval = config.rateObservingTickTime timeout = config.rateObservingTimeout self.timerFactory = timerFactory self.item = item - self.player = player } deinit { @@ -104,8 +102,6 @@ final class ModernAVPlayerRateObservingService: RateObservingService { timer?.invalidate() onTimeout?() } else { - //try to force play if playback stalled - player.play() ModernAVPlayerLogger.instance.log(message: "Remaining time: \(remainingTime)", domain: .service) } } diff --git a/Sources/Core/State/BufferingState.swift b/Sources/Core/State/BufferingState.swift index ad6d3d77..9151149b 100644 --- a/Sources/Core/State/BufferingState.swift +++ b/Sources/Core/State/BufferingState.swift @@ -49,7 +49,7 @@ final class BufferingState: NSObject, PlayerState { guard let item = context.currentItem else { fatalError("item should exist") } self.context = context - self.rateObservingService = rateObservingService ?? ModernAVPlayerRateObservingService(config: context.config, item: item, player: context.player) + self.rateObservingService = rateObservingService ?? ModernAVPlayerRateObservingService(config: context.config, item: item) self.interruptionAudioService = interruptionAudioService super.init() From 0f20b0474420878a5681b3ed0336b0b7702d38c9 Mon Sep 17 00:00:00 2001 From: Yaroslav Lvov Date: Wed, 16 Jun 2021 13:44:02 +0300 Subject: [PATCH 9/9] chore: update tests --- Tests/PlayerContextTests.swift | 12 ++++++------ Tests/State/BufferingStateSpecs.swift | 8 ++++---- Tests/State/FailedStateTests.swift | 2 +- Tests/State/InitStateTests.swift | 2 +- Tests/State/LoadedStateSpecs.swift | 4 ++-- Tests/State/LoadingMediaStateSpecs.swift | 2 +- Tests/State/PausedStateSpecs.swift | 4 ++-- Tests/State/PlayingStateSpecs.swift | 2 +- Tests/State/StoppedStateSpecs.swift | 4 ++-- Tests/State/WaitingNetworkStateTests.swift | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Tests/PlayerContextTests.swift b/Tests/PlayerContextTests.swift index 4cd27716..f7325aef 100644 --- a/Tests/PlayerContextTests.swift +++ b/Tests/PlayerContextTests.swift @@ -190,7 +190,7 @@ final class PlayerContextTests: XCTestCase { context.changeState(state: state) // ACT - context.seek(position: 0) + context.seek(position: 0, isAccurate: false) // ASSERT Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst))) @@ -204,7 +204,7 @@ final class PlayerContextTests: XCTestCase { duration: duration, status: nil) // ACT - context.seek(position: seekPosition) + context.seek(position: seekPosition, isAccurate: false) // ASSERT Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.seekOverstepPosition))) @@ -219,10 +219,10 @@ final class PlayerContextTests: XCTestCase { context.changeState(state: state) // ACT - context.seek(position: seekPosition) + context.seek(position: seekPosition, isAccurate: false) // ASSERT - Verify(state, 1, .seek(position: .value(seekPosition))) + Verify(state, 1, .seek(position: .value(seekPosition), isAccurate: false)) } func testValidSeekOffset() { @@ -236,11 +236,11 @@ final class PlayerContextTests: XCTestCase { context.changeState(state: state) // ACT - context.seek(offset: offset) + context.seek(offset: offset, isAccurate: false) // ASSERT let expected = seekPosition.seconds + offset - Verify(state, 1, .seek(position: .value(expected))) + Verify(state, 1, .seek(position: .value(expected), isAccurate: false)) } func testLoadMedia() { diff --git a/Tests/State/BufferingStateSpecs.swift b/Tests/State/BufferingStateSpecs.swift index 264cbfd1..4d9b32ca 100644 --- a/Tests/State/BufferingStateSpecs.swift +++ b/Tests/State/BufferingStateSpecs.swift @@ -145,7 +145,7 @@ final class BufferingStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = true // ACT - self.bufferingState.seekCommand(position: 42) + self.bufferingState.seekCommand(position: 42, isAccurate: false) } it("should call didCurrentTimeChange delegate method") { @@ -168,7 +168,7 @@ final class BufferingStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = false // ACT - self.bufferingState.seekCommand(position: 42) + self.bufferingState.seekCommand(position: 42, isAccurate: false) } it("should not call didCurrentTimeChange delegate method") { @@ -246,7 +246,7 @@ final class BufferingStateSpecs: QuickSpec { it("should cancel pending seek") { // ACT - self.bufferingState.seek(position: CMTime.zero.seconds) + self.bufferingState.seek(position: CMTime.zero.seconds, isAccurate: false) // ASSERT expect(self.item.cancelPendingSeeksCallCount).to(equal(1)) @@ -255,7 +255,7 @@ final class BufferingStateSpecs: QuickSpec { it("should call player seek command") { // ACT - self.bufferingState.seek(position: CMTime.zero.seconds) + self.bufferingState.seek(position: CMTime.zero.seconds, isAccurate: false) // ASSERT expect(self.mockPlayer.seekCompletionCallCount).to(equal(1)) diff --git a/Tests/State/FailedStateTests.swift b/Tests/State/FailedStateTests.swift index 350eadba..5196f0f0 100644 --- a/Tests/State/FailedStateTests.swift +++ b/Tests/State/FailedStateTests.swift @@ -90,7 +90,7 @@ final class FailedStateTests: XCTestCase { func testSeek() { // ACT - state.seek(position: 0) + state.seek(position: 0, isAccurate: false) // ASSERT Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst))) diff --git a/Tests/State/InitStateTests.swift b/Tests/State/InitStateTests.swift index 89a2c70e..58668d15 100644 --- a/Tests/State/InitStateTests.swift +++ b/Tests/State/InitStateTests.swift @@ -98,7 +98,7 @@ final class InitStateTests: XCTestCase { func testSeekCall() { // ACT - state.seek(position: 42) + state.seek(position: 42, isAccurate: false) // ASSERT Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst))) diff --git a/Tests/State/LoadedStateSpecs.swift b/Tests/State/LoadedStateSpecs.swift index cc6f276e..0e9b98b9 100644 --- a/Tests/State/LoadedStateSpecs.swift +++ b/Tests/State/LoadedStateSpecs.swift @@ -118,7 +118,7 @@ final class LoadedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = true // ACT - self.loadedState.seek(position: 42) + self.loadedState.seek(position: 42, isAccurate: false) } it("should call seek on player") { @@ -140,7 +140,7 @@ final class LoadedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = false // ACT - self.loadedState.seek(position: 42) + self.loadedState.seek(position: 42, isAccurate: false) } it("should call seek on player") { diff --git a/Tests/State/LoadingMediaStateSpecs.swift b/Tests/State/LoadingMediaStateSpecs.swift index 2e399f52..05358836 100644 --- a/Tests/State/LoadingMediaStateSpecs.swift +++ b/Tests/State/LoadingMediaStateSpecs.swift @@ -173,7 +173,7 @@ final class LoadingMediaStateTests: XCTestCase { func testSeekCall() { // ACT - state.seek(position: 42) + state.seek(position: 42, isAccurate: false) // ASSERT Verify(contextDelegate, .once, .playerContext(unavailableActionReason: .value(.waitLoadedMedia))) diff --git a/Tests/State/PausedStateSpecs.swift b/Tests/State/PausedStateSpecs.swift index 0ad50cb7..0136bae7 100644 --- a/Tests/State/PausedStateSpecs.swift +++ b/Tests/State/PausedStateSpecs.swift @@ -170,7 +170,7 @@ final class PausedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = true // ACT - self.tested.seek(position: self.position.seconds) + self.tested.seek(position: self.position.seconds, isAccurate: false) } it("should call player seek") { @@ -203,7 +203,7 @@ final class PausedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = false // ACT - self.tested.seek(position: self.position.seconds) + self.tested.seek(position: self.position.seconds, isAccurate: false) } it("should call player seek") { diff --git a/Tests/State/PlayingStateSpecs.swift b/Tests/State/PlayingStateSpecs.swift index c2d53cb0..8058440c 100644 --- a/Tests/State/PlayingStateSpecs.swift +++ b/Tests/State/PlayingStateSpecs.swift @@ -121,7 +121,7 @@ final class PlayingStateSpecs: QuickSpec { let position: Double = 42 // ACT - self.playingState.seek(position: position) + self.playingState.seek(position: position, isAccurate: false) // ASSERT expect(self.tested.state).to(beAnInstanceOf(BufferingState.self)) diff --git a/Tests/State/StoppedStateSpecs.swift b/Tests/State/StoppedStateSpecs.swift index ee074faf..3dc940b3 100644 --- a/Tests/State/StoppedStateSpecs.swift +++ b/Tests/State/StoppedStateSpecs.swift @@ -191,7 +191,7 @@ final class StoppedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = true // ACT - self.tested.seek(position: self.position.seconds) + self.tested.seek(position: self.position.seconds, isAccurate: false) } it("should call player seek") { @@ -226,7 +226,7 @@ final class StoppedStateSpecs: QuickSpec { self.mockPlayer.seekCompletionHandlerReturn = false // ACT - self.tested.seek(position: self.position.seconds) + self.tested.seek(position: self.position.seconds, isAccurate: false) } it("should call player seek") { diff --git a/Tests/State/WaitingNetworkStateTests.swift b/Tests/State/WaitingNetworkStateTests.swift index d3a0e9d2..6caf64c6 100644 --- a/Tests/State/WaitingNetworkStateTests.swift +++ b/Tests/State/WaitingNetworkStateTests.swift @@ -82,7 +82,7 @@ final class WaitingNetworkStateTest: XCTestCase { func testSeek() { // ACT - state.seek(position: 0) + state.seek(position: 0, isAccurate: false) // ASSERT Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.waitEstablishedNetwork)))