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 @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
* [gem] Cocoapods -> 1.10.1
* [gem] Fastlane -> 2.171.0

## [1.7.4]
* Improvements:
* Support seek tolerance API

## [1.7.3]
* Fix:
* Resume audio on device background mode
Expand Down
2 changes: 2 additions & 0 deletions Sources/Core/Components/PlayerCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
// THE SOFTWARE.

import Foundation
import CoreMedia

public protocol PlayerCommand {
func load(media: PlayerMedia, autostart: Bool, position: Double?)
func pause()
func play()
func seek(position: Double)
func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime)
func stop()
}
20 changes: 20 additions & 0 deletions Sources/Core/Components/PlayerContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,32 @@ final class ModernAVPlayerContext: NSObject, PlayerContext {
assertionFailure("boundedPosition should return at least value or reason")
}
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
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, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter)
} else if let reason = seekPosition.reason {
unaivalableCommand(reason: reason)
} else {
assertionFailure("boundedPosition should return at least value or reason")
}
}

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

func seek(offset: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let position = currentTime + offset
seek(position: position, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter)
}

func stop() {
state.stop()
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/Core/ModernAVPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ public final class ModernAVPlayer: NSObject, ModernAVPlayerExposable {
context.seek(position: position)
}

///
/// 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
/// - patameter toleranceBefore: CMTime for toleranceBefore
/// - patameter toleranceAfter: CMTime for toleranceAfter
///
public func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
context.seek(position: position, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter)
}

///
/// Apply offset to the media current time
///
Expand Down
14 changes: 14 additions & 0 deletions Sources/Core/State/BufferingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ final class BufferingState: NSObject, PlayerState {
strongSelf.playCommand()
}
}

func seekCommand(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
context.currentItem?.cancelPendingSeeks()
let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale)
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()
}
}

// MARK: - Shared actions

Expand All @@ -133,6 +143,10 @@ final class BufferingState: NSObject, PlayerState {
func seek(position: Double) {
seekCommand(position: position)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
seekCommand(position: position, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter)
}

func stop() {
changeState(StoppedState(context: context))
Expand Down
6 changes: 6 additions & 0 deletions Sources/Core/State/FailedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ final class FailedState: PlayerState {
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let debug = "Unable to seek, load a media first"
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst)
}

func stop() {
let debug = "Unable to stop, load a media first"
Expand Down
6 changes: 6 additions & 0 deletions Sources/Core/State/InitState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ struct InitState: PlayerState {
context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let debug = "Load item before seeking"
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .loadMediaFirst)
}

func stop() {
context.changeState(state: StoppedState(context: context))
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/Core/State/LoadedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ struct LoadedState: PlayerState {
value: context.currentTime)
}
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale)
context.player.seek(to: time) { [context] completed in
guard completed else { return }
context.delegate?.playerContext(didCurrentTimeChange: context.currentTime)
context.nowPlaying.overrideInfoCenter(for: MPNowPlayingInfoPropertyElapsedPlaybackTime,
value: context.currentTime)
}
}

func stop() {
context.changeState(state: StoppedState(context: context))
Expand Down
6 changes: 6 additions & 0 deletions Sources/Core/State/LoadingMediaState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ final class LoadingMediaState: PlayerState {
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .waitLoadedMedia)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let debug = "Wait media to be loaded before seeking"
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .waitLoadedMedia)
}

func stop() {
cancelMediaLoading()
Expand Down
10 changes: 10 additions & 0 deletions Sources/Core/State/PausedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ class PausedState: PlayerState {
value: context.currentTime)
}
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let time = CMTime(seconds: position, preferredTimescale: context.config.preferredTimescale)
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,
value: context.currentTime)
}
}

func stop() {
context.changeState(state: StoppedState(context: context))
Expand Down
6 changes: 6 additions & 0 deletions Sources/Core/State/PlayingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ final class PlayingState: PlayerState {
state.seekCommand(position: position)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let state = BufferingState(context: context)
changeState(state: state)
state.seekCommand(position: position, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter)
}

func stop() {
let state = StoppedState(context: context)
changeState(state: state)
Expand Down
6 changes: 6 additions & 0 deletions Sources/Core/State/WaitingNetworkState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ final class WaitingNetworkState: PlayerState {
context.delegate?.playerContext(unavailableActionReason: .waitEstablishedNetwork)
}

func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
let debug = "Reload a media first before seeking"
ModernAVPlayerLogger.instance.log(message: debug, domain: .unavailableCommand)
context.delegate?.playerContext(unavailableActionReason: .waitEstablishedNetwork)
}

func stop() {
context.changeState(state: StoppedState(context: context))
}
Expand Down
12 changes: 12 additions & 0 deletions Tests/Generated/Mock.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,12 @@ open class PlayerContextMock: PlayerContext, Mock {
let perform = methodPerformValue(.m_seek__position_position(Parameter<Double>.value(`position`))) as? (Double) -> Void
perform?(`position`)
}

public func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
addInvocation(.m_seek__position_position(Parameter<Double>.value(`position`)))
let perform = methodPerformValue(.m_seek__position_position(Parameter<Double>.value(`position`))) as? (Double) -> Void
perform?(`position`)
}

open func stop() {
addInvocation(.m_stop)
Expand Down Expand Up @@ -2366,6 +2372,12 @@ open class PlayerStateMock: PlayerState, Mock {
let perform = methodPerformValue(.m_seek__position_position(Parameter<Double>.value(`position`))) as? (Double) -> Void
perform?(`position`)
}

public func seek(position: Double, toleranceBefore: CMTime, toleranceAfter: CMTime) {
addInvocation(.m_seek__position_position(Parameter<Double>.value(`position`)))
let perform = methodPerformValue(.m_seek__position_position(Parameter<Double>.value(`position`))) as? (Double) -> Void
perform?(`position`)
}

open func stop() {
addInvocation(.m_stop)
Expand Down
56 changes: 56 additions & 0 deletions Tests/PlayerContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ final class PlayerContextTests: XCTestCase {
// ASSERT
Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}

func testSeekToleranceWithNoCurrentItem() {
// ARRANGE
context.changeState(state: state)

// ACT
context.seek(position: 0, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)

// ASSERT
Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}

func testOverstepSeekPosition() {
// ARRANGE
Expand All @@ -209,6 +220,19 @@ final class PlayerContextTests: XCTestCase {
// ASSERT
Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.seekOverstepPosition)))
}

func testOverstepSeekTolerancePosition() {
// ARRANGE
let seekPosition: Double = 43
let duration = CMTime(seconds: 42, preferredTimescale: config.preferredTimescale)
player.overrideCurrentItem = MockPlayerItem(url: URL(fileURLWithPath: ""),
duration: duration, status: nil)

// ACT
context.seek(position: seekPosition, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
// ASSERT
Verify(delegate, 1, .playerContext(unavailableActionReason: .value(.seekOverstepPosition)))
}

func testValidSeekPosition() {
// ARRANGE
Expand All @@ -224,6 +248,21 @@ final class PlayerContextTests: XCTestCase {
// ASSERT
Verify(state, 1, .seek(position: .value(seekPosition)))
}

func testValidSeekTolerancePosition() {
// ARRANGE
let seekPosition: Double = 21
let duration = CMTime(seconds: 42, preferredTimescale: config.preferredTimescale)
player.overrideCurrentItem = MockPlayerItem(url: URL(fileURLWithPath: ""),
duration: duration, status: nil)
context.changeState(state: state)

// ACT
context.seek(position: seekPosition, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
// ASSERT
Verify(state, 1, .seek(position: .value(seekPosition)))

}

func testValidSeekOffset() {
// ARRANGE
Expand All @@ -242,6 +281,23 @@ final class PlayerContextTests: XCTestCase {
let expected = seekPosition.seconds + offset
Verify(state, 1, .seek(position: .value(expected)))
}

func testValidSeekToleranceOffset() {
// ARRANGE
let seekPosition = CMTime(seconds: 21, preferredTimescale: config.preferredTimescale)
let duration = CMTime(seconds: 42, preferredTimescale: config.preferredTimescale)
let offset: Double = 10
player.overrideCurrentTime = seekPosition
player.overrideCurrentItem = MockPlayerItem(url: URL(fileURLWithPath: ""),
duration: duration, status: nil)
context.changeState(state: state)

// ACT
context.seek(offset: offset, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
// ASSERT
let expected = seekPosition.seconds + offset
Verify(state, 1, .seek(position: .value(expected)))
}

func testLoadMedia() {
// ARRANGE
Expand Down
8 changes: 8 additions & 0 deletions Tests/State/FailedStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ final class FailedStateTests: XCTestCase {
// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}

func testSeekTolerance() {
// ACT
state.seek(position: 0, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)

// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}

func testFailedUsedAVPlayerItem() {
// ACT
Expand Down
8 changes: 8 additions & 0 deletions Tests/State/InitStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,12 @@ final class InitStateTests: XCTestCase {
// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}

func testSeekToleranceCall() {
// ACT
state.seek(position: 42, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)

// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.loadMediaFirst)))
}
}
7 changes: 7 additions & 0 deletions Tests/State/WaitingNetworkStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ final class WaitingNetworkStateTest: XCTestCase {
// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.waitEstablishedNetwork)))
}

func testSeekTolerance() {
// ACT
state.seek(position: 0, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
// ASSERT
Verify(contextDelegate, 1, .playerContext(unavailableActionReason: .value(.waitEstablishedNetwork)))
}

func testPause() {
// ACT
Expand Down