Skip to content

Commit

Permalink
feat: save playback speed (#67)
Browse files Browse the repository at this point in the history
* chore: save playback speed in CourseStorage

* chore: add full screen for youtube on iPad

* chore: save value in userdefaults

* chore: little refactor

* fix: fix warning

* chore: added CourseStorage as injection

* chore: moved playback speed to user settings

---------

Co-authored-by: Anton Yarmolenko <[email protected]>
  • Loading branch information
rnr and rnr authored Aug 22, 2024
1 parent 30bbc49 commit f903937
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 39 deletions.
5 changes: 4 additions & 1 deletion Core/Core/Data/Model/UserSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ public struct UserSettings: Codable, Hashable {
public var wifiOnly: Bool
public var streamingQuality: StreamingQuality
public var downloadQuality: DownloadQuality
public var videoPlaybackSpeed: Float

public init(
wifiOnly: Bool,
streamingQuality: StreamingQuality,
downloadQuality: DownloadQuality
downloadQuality: DownloadQuality,
playbackSpeed: Float
) {
self.wifiOnly = wifiOnly
self.streamingQuality = streamingQuality
self.downloadQuality = downloadQuality
self.videoPlaybackSpeed = playbackSpeed
}
}

Expand Down
41 changes: 32 additions & 9 deletions Course/Course/Presentation/Video/PlayerViewControllerHolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import AVKit
import Combine
import Core

public protocol PlayerViewControllerHolderProtocol: AnyObject {
var url: URL? { get }
Expand All @@ -23,11 +24,11 @@ public protocol PlayerViewControllerHolderProtocol: AnyObject {
blockID: String,
courseID: String,
selectedCourseTab: Int,
videoResolution: CGSize,
pipManager: PipManagerProtocol,
playerTracker: any PlayerTrackerProtocol,
playerDelegate: PlayerDelegateProtocol?,
playerService: PlayerServiceProtocol
playerService: PlayerServiceProtocol,
appStorage: CoreStorage?
)
func getTimePublisher() -> AnyPublisher<Double, Never>
func getErrorPublisher() -> AnyPublisher<Error, Never>
Expand Down Expand Up @@ -69,10 +70,10 @@ public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
private let playerTracker: any PlayerTrackerProtocol
private let playerDelegate: PlayerDelegateProtocol?
private let playerService: PlayerServiceProtocol
private let videoResolution: CGSize
private let errorPublisher = PassthroughSubject<Error, Never>()
private var isViewedOnce: Bool = false
private var cancellations: [AnyCancellable] = []
private var appStorage: CoreStorage?

let pipManager: PipManagerProtocol

Expand All @@ -83,7 +84,21 @@ public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
playerController.canStartPictureInPictureAutomaticallyFromInline = true
playerController.delegate = playerDelegate
playerController.player = playerTracker.player as? AVPlayer
playerController.player?.currentItem?.preferredMaximumResolution = videoResolution
playerController.player?.currentItem?.preferredMaximumResolution = (
appStorage?.userSettings?.streamingQuality ?? .auto
).resolution

if let speed = appStorage?.userSettings?.videoPlaybackSpeed {
if #available(iOS 16.0, *) {
if let playbackSpeed = playerController.speeds.first(where: { $0.rate == speed }) {
playerController.selectSpeed(playbackSpeed)
}
} else {
// Fallback on earlier versions
playerController.player?.rate = speed
}
}

return playerController
}()

Expand All @@ -92,21 +107,21 @@ public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
blockID: String,
courseID: String,
selectedCourseTab: Int,
videoResolution: CGSize,
pipManager: PipManagerProtocol,
playerTracker: any PlayerTrackerProtocol,
playerDelegate: PlayerDelegateProtocol?,
playerService: PlayerServiceProtocol
playerService: PlayerServiceProtocol,
appStorage: CoreStorage?
) {
self.url = url
self.blockID = blockID
self.courseID = courseID
self.selectedCourseTab = selectedCourseTab
self.videoResolution = videoResolution
self.pipManager = pipManager
self.playerTracker = playerTracker
self.playerDelegate = playerDelegate
self.playerService = playerService
self.appStorage = appStorage
addObservers()
}

Expand All @@ -131,6 +146,7 @@ public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
.sink {[weak self] rate in
guard rate > 0 else { return }
self?.pausePipIfNeed()
self?.saveSelectedRate(rate: rate)
}
.store(in: &cancellations)
pipManager.pipRatePublisher()?
Expand All @@ -141,6 +157,13 @@ public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
.store(in: &cancellations)
}

private func saveSelectedRate(rate: Float) {
if var storage = appStorage, var userSettings = storage.userSettings, userSettings.videoPlaybackSpeed != rate {
userSettings.videoPlaybackSpeed = rate
storage.userSettings = userSettings
}
}

public func pausePipIfNeed() {
if !isPlayingInPip {
pipManager.pauseCurrentPipVideo()
Expand Down Expand Up @@ -204,7 +227,6 @@ extension PlayerViewControllerHolder {
blockID: "",
courseID: "",
selectedCourseTab: 0,
videoResolution: .zero,
pipManager: PipManagerProtocolMock(),
playerTracker: PlayerTrackerProtocolMock(url: URL(string: "")),
playerDelegate: nil,
Expand All @@ -213,7 +235,8 @@ extension PlayerViewControllerHolder {
blockID: "",
interactor: CourseInteractor.mock,
router: CourseRouterMock()
)
),
appStorage: CoreStorageMock()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Combine
import Foundation
import YouTubePlayerKit
import Core

public class YoutubePlayerViewControllerHolder: PlayerViewControllerHolderProtocol {
public let url: URL?
Expand All @@ -33,7 +34,6 @@ public class YoutubePlayerViewControllerHolder: PlayerViewControllerHolderProtoc
}
private let playerTracker: any PlayerTrackerProtocol
private let playerService: PlayerServiceProtocol
private let videoResolution: CGSize
private let errorPublisher = PassthroughSubject<Error, Never>()
private var isViewedOnce: Bool = false
private var cancellations: [AnyCancellable] = []
Expand All @@ -49,23 +49,23 @@ public class YoutubePlayerViewControllerHolder: PlayerViewControllerHolderProtoc
blockID: String,
courseID: String,
selectedCourseTab: Int,
videoResolution: CGSize,
pipManager: PipManagerProtocol,
playerTracker: any PlayerTrackerProtocol,
playerDelegate: PlayerDelegateProtocol?,
playerService: PlayerServiceProtocol
playerService: PlayerServiceProtocol,
appStorage: CoreStorage?
) {
self.url = url
self.blockID = blockID
self.courseID = courseID
self.selectedCourseTab = selectedCourseTab
self.videoResolution = videoResolution
self.pipManager = pipManager
self.playerTracker = playerTracker
self.playerService = playerService
let youtubePlayer = playerTracker.player as? YouTubePlayer
var configuration = youtubePlayer?.configuration
configuration?.autoPlay = !pipManager.isPipActive
configuration?.fullscreenMode = .web
if let configuration = configuration {
youtubePlayer?.update(configuration: configuration)
}
Expand Down Expand Up @@ -171,7 +171,6 @@ extension YoutubePlayerViewControllerHolder {
blockID: "",
courseID: "",
selectedCourseTab: 0,
videoResolution: .zero,
pipManager: PipManagerProtocolMock(),
playerTracker: PlayerTrackerProtocolMock(url: URL(string: "")),
playerDelegate: nil,
Expand All @@ -180,7 +179,8 @@ extension YoutubePlayerViewControllerHolder {
blockID: "",
interactor: CourseInteractor.mock,
router: CourseRouterMock()
)
),
appStorage: nil
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())
let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder)

await viewModel.getSubtitles(subtitlesUrl: "url")
Expand All @@ -59,7 +59,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())
let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder)

await viewModel.getSubtitles(subtitlesUrl: "url")
Expand All @@ -78,7 +78,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())
let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder)

viewModel.languages = [
Expand All @@ -103,7 +103,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())

Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in}))

Expand All @@ -119,7 +119,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())
let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder)

Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError()))
Expand Down Expand Up @@ -148,7 +148,7 @@ final class VideoPlayerViewModelTests: XCTestCase {

let tracker = PlayerTrackerProtocolMock(url: nil)
let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service)
let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock())
let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder)

Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError))
Expand Down
10 changes: 4 additions & 6 deletions OpenEdX/DI/ScreenAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,11 @@ class ScreenAssembly: Assembly {
blockID: blockID,
courseID: courseID,
selectedCourseTab: selectedCourseTab,
videoResolution: .zero,
pipManager: r.resolve(PipManagerProtocol.self)!,
playerTracker: r.resolve(YoutubePlayerTracker.self, argument: url)!,
playerDelegate: nil,
playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)!
playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)!,
appStorage: nil
)
}

Expand All @@ -436,20 +436,18 @@ class ScreenAssembly: Assembly {
return holder
}

let storage = r.resolve(CoreStorage.self)!
let quality = storage.userSettings?.streamingQuality ?? .auto
let tracker = r.resolve(PlayerTracker.self, argument: url)!
let delegate = r.resolve(PlayerDelegateProtocol.self, argument: pipManager)!
let holder = PlayerViewControllerHolder(
url: url,
blockID: blockID,
courseID: courseID,
selectedCourseTab: selectedCourseTab,
videoResolution: quality.resolution,
pipManager: pipManager,
playerTracker: tracker,
playerDelegate: delegate,
playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)!
playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)!,
appStorage: r.resolve(CoreStorage.self)!
)
delegate.playerHolder = holder
return holder
Expand Down
13 changes: 10 additions & 3 deletions OpenEdX/Data/AppStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,22 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto

public var userSettings: UserSettings? {
get {
guard let userSettings = userDefaults.data(forKey: KEY_SETTINGS) else {
let defaultSettings = UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto)
if let userSettings = userDefaults.data(forKey: KEY_SETTINGS),
let settings = try? JSONDecoder().decode(UserSettings.self, from: userSettings) {
return settings
} else {
let defaultSettings = UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto,
playbackSpeed: 1.0
)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(defaultSettings) {
userDefaults.set(encoded, forKey: KEY_SETTINGS)
}
return defaultSettings
}
return try? JSONDecoder().decode(UserSettings.self, from: userSettings)
}
set(newValue) {
if let settings = newValue {
Expand Down
4 changes: 2 additions & 2 deletions Profile/Profile/Data/ProfileRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public class ProfileRepository: ProfileRepositoryProtocol {
if let userSettings = storage.userSettings {
return userSettings
} else {
return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto)
return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto, playbackSpeed: 1.0)
}
}

Expand Down Expand Up @@ -241,7 +241,7 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
public func deleteAccount(password: String) async throws -> Bool { return false }

public func getSettings() -> UserSettings {
return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto)
return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto, playbackSpeed: 1.0)
}
public func saveSettings(_ settings: UserSettings) {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down Expand Up @@ -61,7 +62,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down Expand Up @@ -95,7 +97,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down Expand Up @@ -129,7 +132,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down Expand Up @@ -163,7 +167,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down Expand Up @@ -197,7 +202,8 @@ final class SettingsViewModelTests: XCTestCase {
willReturn: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
downloadQuality: .auto,
playbackSpeed: 1.0
)
)
)
Expand Down

0 comments on commit f903937

Please sign in to comment.