Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.

---
## [X.X.X]
* Feature:
* Accurate seek api

## [1.7.3]
* Fix:
* Resume audio on device background mode
Expand Down
6 changes: 3 additions & 3 deletions Example/RemoteCommandExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/Components/PlayerCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
8 changes: 4 additions & 4 deletions Sources/Core/Components/PlayerContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,24 @@ 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 {
assertionFailure("boundedPosition should return at least value or reason")
}
}

func seek(offset: Double) {
func seek(offset: Double, isAccurate: Bool) {
let position = currentTime + offset
seek(position: position)
seek(position: position, isAccurate: isAccurate)
}

func stop() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
34 changes: 30 additions & 4 deletions Sources/Core/ModernAVPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,47 @@ 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
///
@available(*, deprecated, message: "use func seek(position: Double, isAccurate: Bool = false) instead")
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
///
@available(*, deprecated, message: "use func seek(offset: Double, isAccurate: Bool = false) instead")
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
Expand Down
9 changes: 5 additions & 4 deletions Sources/Core/State/BufferingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ 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 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()
Expand All @@ -130,8 +131,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() {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/State/FailedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/State/InitState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions Sources/Core/State/LoadedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ 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 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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/State/LoadingMediaState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions Sources/Core/State/PausedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ 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 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,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Core/State/PlayingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/State/StoppedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/State/WaitingNetworkState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions Tests/PlayerContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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)))
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down
8 changes: 4 additions & 4 deletions Tests/State/BufferingStateSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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") {
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Tests/State/FailedStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
2 changes: 1 addition & 1 deletion Tests/State/InitStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
4 changes: 2 additions & 2 deletions Tests/State/LoadedStateSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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") {
Expand Down
2 changes: 1 addition & 1 deletion Tests/State/LoadingMediaStateSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
Loading