Skip to content

Commit ab942a4

Browse files
authored
Add HLS quality support (#134)
* add HLS quality support * update default localization * Update VideoPlayerViewModelTests.swift * change 360 quality localizations * fix resolution changing logic * rename the getVideoResolution function
1 parent 3b7efd4 commit ab942a4

17 files changed

+77
-27
lines changed

Core/Core/Data/CoreStorage.swift

+11
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,14 @@ public protocol CoreStorage {
1515
var userSettings: UserSettings? {get set}
1616
func clear()
1717
}
18+
19+
public struct CoreStorageMock: CoreStorage {
20+
public var accessToken: String? = nil
21+
public var refreshToken: String? = nil
22+
public var cookiesDate: String? = nil
23+
public var user: DataLayer.User? = nil
24+
public var userSettings: UserSettings? = nil
25+
public func clear() {}
26+
27+
public init() {}
28+
}

Core/Core/Data/Model/UserSettings.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import Foundation
99

1010
public struct UserSettings: Codable {
1111
public var wifiOnly: Bool
12-
public var downloadQuality: VideoQuality
12+
public var streamingQuality: StreamingQuality
1313

14-
public init(wifiOnly: Bool, downloadQuality: VideoQuality) {
14+
public init(wifiOnly: Bool, streamingQuality: StreamingQuality) {
1515
self.wifiOnly = wifiOnly
16-
self.downloadQuality = downloadQuality
16+
self.streamingQuality = streamingQuality
1717
}
1818
}
1919

20-
public enum VideoQuality: Codable {
20+
public enum StreamingQuality: Codable {
2121
case auto
2222
case low
2323
case medium

Course/Course/Presentation/Video/EncodedVideoPlayer.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public struct EncodedVideoPlayer: View {
6060
PlayerViewController(
6161
videoURL: viewModel.url,
6262
controller: viewModel.controller,
63+
bitrate: viewModel.getVideoResolution(),
6364
progress: { progress in
6465
if progress >= 0.8 {
6566
if !isViewedOnce {
@@ -141,7 +142,8 @@ struct EncodedVideoPlayer_Previews: PreviewProvider {
141142
languages: [],
142143
playerStateSubject: CurrentValueSubject<VideoPlayerState?, Never>(nil),
143144
interactor: CourseInteractor(repository: CourseRepositoryMock()),
144-
router: CourseRouterMock(),
145+
router: CourseRouterMock(),
146+
appStorage: CoreStorageMock(),
145147
connectivity: Connectivity()
146148
),
147149
isOnScreen: true

Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift

+18-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel {
2424
playerStateSubject: CurrentValueSubject<VideoPlayerState?, Never>,
2525
interactor: CourseInteractorProtocol,
2626
router: CourseRouter,
27+
appStorage: CoreStorage,
2728
connectivity: ConnectivityProtocol
2829
) {
2930
self.url = url
@@ -32,7 +33,8 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel {
3233
courseID: courseID,
3334
languages: languages,
3435
interactor: interactor,
35-
router: router,
36+
router: router,
37+
appStorage: appStorage,
3638
connectivity: connectivity)
3739

3840
playerStateSubject.sink(receiveValue: { [weak self] state in
@@ -46,4 +48,19 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel {
4648
}
4749
}).store(in: &subscription)
4850
}
51+
52+
func getVideoResolution() -> CGSize {
53+
switch appStorage.userSettings?.streamingQuality {
54+
case .auto:
55+
return CGSize(width: 1280, height: 720)
56+
case .low:
57+
return CGSize(width: 640, height: 360)
58+
case .medium:
59+
return CGSize(width: 854, height: 480)
60+
case .high:
61+
return CGSize(width: 1280, height: 720)
62+
case .none:
63+
return CGSize(width: 1280, height: 720)
64+
}
65+
}
4966
}

Course/Course/Presentation/Video/PlayerViewController.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,21 @@ import _AVKit_SwiftUI
1111
struct PlayerViewController: UIViewControllerRepresentable {
1212

1313
var videoURL: URL?
14+
var videoResolution: CGSize
1415
var controller: AVPlayerViewController
1516
var progress: ((Float) -> Void)
1617
var seconds: ((Double) -> Void)
1718

1819
init(
19-
videoURL: URL?, controller: AVPlayerViewController,
20+
videoURL: URL?,
21+
controller: AVPlayerViewController,
22+
bitrate: CGSize,
2023
progress: @escaping ((Float) -> Void),
2124
seconds: @escaping ((Double) -> Void)
2225
) {
2326
self.videoURL = videoURL
2427
self.controller = controller
28+
self.videoResolution = bitrate
2529
self.progress = progress
2630
self.seconds = seconds
2731
}
@@ -76,6 +80,7 @@ struct PlayerViewController: UIViewControllerRepresentable {
7680
playerController.player?.allowsExternalPlayback = true
7781
}
7882
playerController.player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!))
83+
playerController.player?.currentItem?.preferredMaximumResolution = videoResolution
7984
addPeriodicTimeObserver(playerController, currentProgress: { progress, seconds in
8085
self.progress(progress)
8186
self.seconds(seconds)

Course/Course/Presentation/Video/SubtittlesView.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ struct SubtittlesView_Previews: PreviewProvider {
120120
blockID: "", courseID: "",
121121
languages: [],
122122
interactor: CourseInteractor(repository: CourseRepositoryMock()),
123-
router: CourseRouterMock(),
123+
router: CourseRouterMock(),
124+
appStorage: CoreStorageMock(),
124125
connectivity: Connectivity()
125126
), scrollTo: {_ in }
126127
)

Course/Course/Presentation/Video/VideoPlayerViewModel.swift

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class VideoPlayerViewModel: ObservableObject {
1717
private let interactor: CourseInteractorProtocol
1818
public let connectivity: ConnectivityProtocol
1919
public let router: CourseRouter
20+
public let appStorage: CoreStorage
2021

2122
private var subtitlesDownloaded: Bool = false
2223
@Published var subtitles: [Subtitle] = []
@@ -37,13 +38,15 @@ public class VideoPlayerViewModel: ObservableObject {
3738
languages: [SubtitleUrl],
3839
interactor: CourseInteractorProtocol,
3940
router: CourseRouter,
41+
appStorage: CoreStorage,
4042
connectivity: ConnectivityProtocol
4143
) {
4244
self.blockID = blockID
4345
self.courseID = courseID
4446
self.languages = languages
4547
self.interactor = interactor
4648
self.router = router
49+
self.appStorage = appStorage
4750
self.connectivity = connectivity
4851
self.prepareLanguages()
4952
}

Course/Course/Presentation/Video/YouTubeVideoPlayer.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider {
8888
languages: [],
8989
playerStateSubject: CurrentValueSubject<VideoPlayerState?, Never>(nil),
9090
interactor: CourseInteractor(repository: CourseRepositoryMock()),
91-
router: CourseRouterMock(),
91+
router: CourseRouterMock(),
92+
appStorage: CoreStorageMock(),
9293
connectivity: Connectivity()),
9394
isOnScreen: true)
9495
}

Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {
3232
playerStateSubject: CurrentValueSubject<VideoPlayerState?, Never>,
3333
interactor: CourseInteractorProtocol,
3434
router: CourseRouter,
35+
appStorage: CoreStorage,
3536
connectivity: ConnectivityProtocol
3637
) {
3738
self.url = url
@@ -61,6 +62,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {
6162
languages: languages,
6263
interactor: interactor,
6364
router: router,
65+
appStorage: appStorage,
6466
connectivity: connectivity
6567
)
6668

Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ final class VideoPlayerViewModelTests: XCTestCase {
3737
courseID: "",
3838
languages: [],
3939
interactor: interactor,
40-
router: router,
40+
router: router,
41+
appStorage: CoreStorageMock(),
4142
connectivity: connectivity)
4243

4344
await viewModel.getSubtitles(subtitlesUrl: "url")
@@ -64,6 +65,7 @@ final class VideoPlayerViewModelTests: XCTestCase {
6465
languages: [],
6566
interactor: interactor,
6667
router: router,
68+
appStorage: CoreStorageMock(),
6769
connectivity: connectivity)
6870

6971
await viewModel.getSubtitles(subtitlesUrl: "url")
@@ -85,6 +87,7 @@ final class VideoPlayerViewModelTests: XCTestCase {
8587
languages: [],
8688
interactor: interactor,
8789
router: router,
90+
appStorage: CoreStorageMock(),
8891
connectivity: connectivity)
8992

9093
viewModel.languages = [
@@ -112,6 +115,7 @@ final class VideoPlayerViewModelTests: XCTestCase {
112115
languages: [],
113116
interactor: interactor,
114117
router: router,
118+
appStorage: CoreStorageMock(),
115119
connectivity: connectivity)
116120

117121
Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in}))
@@ -131,6 +135,7 @@ final class VideoPlayerViewModelTests: XCTestCase {
131135
languages: [],
132136
interactor: interactor,
133137
router: router,
138+
appStorage: CoreStorageMock(),
134139
connectivity: connectivity)
135140

136141
Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError()))
@@ -155,6 +160,7 @@ final class VideoPlayerViewModelTests: XCTestCase {
155160
languages: [],
156161
interactor: interactor,
157162
router: router,
163+
appStorage: CoreStorageMock(),
158164
connectivity: connectivity)
159165

160166
Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError))

OpenEdX/DI/ScreenAssembly.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ class ScreenAssembly: Assembly {
281281
playerStateSubject: playerStateSubject,
282282
interactor: r.resolve(CourseInteractorProtocol.self)!,
283283
router: r.resolve(CourseRouter.self)!,
284+
appStorage: r.resolve(CoreStorage.self)!,
284285
connectivity: r.resolve(ConnectivityProtocol.self)!
285286
)
286287
}
@@ -295,7 +296,8 @@ class ScreenAssembly: Assembly {
295296
languages: languages,
296297
playerStateSubject: playerStateSubject,
297298
interactor: r.resolve(CourseInteractorProtocol.self)!,
298-
router: r.resolve(CourseRouter.self)!,
299+
router: r.resolve(CourseRouter.self)!,
300+
appStorage: r.resolve(CoreStorage.self)!,
299301
connectivity: r.resolve(ConnectivityProtocol.self)!
300302
)
301303
}

OpenEdX/Data/AppStorage.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage {
9595
public var userSettings: UserSettings? {
9696
get {
9797
guard let userSettings = userDefaults.data(forKey: KEY_SETTINGS) else {
98-
let defaultSettings = UserSettings(wifiOnly: true, downloadQuality: .auto)
98+
let defaultSettings = UserSettings(wifiOnly: true, streamingQuality: .auto)
9999
let encoder = JSONEncoder()
100100
if let encoded = try? encoder.encode(defaultSettings) {
101101
userDefaults.set(encoded, forKey: KEY_SETTINGS)

Profile/Profile/Data/ProfileRepository.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public class ProfileRepository: ProfileRepositoryProtocol {
144144
if let userSettings = storage.userSettings {
145145
return userSettings
146146
} else {
147-
return UserSettings(wifiOnly: true, downloadQuality: VideoQuality.auto)
147+
return UserSettings(wifiOnly: true, streamingQuality: StreamingQuality.auto)
148148
}
149149
}
150150

@@ -233,7 +233,7 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
233233
public func deleteAccount(password: String) async throws -> Bool { return false }
234234

235235
public func getSettings() -> UserSettings {
236-
return UserSettings(wifiOnly: true, downloadQuality: .auto)
236+
return UserSettings(wifiOnly: true, streamingQuality: .auto)
237237
}
238238
public func saveSettings(_ settings: UserSettings) {}
239239
}

Profile/Profile/Presentation/Settings/SettingsViewModel.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ public class SettingsViewModel: ObservableObject {
2222
}
2323
}
2424

25-
@Published var selectedQuality: VideoQuality {
25+
@Published var selectedQuality: StreamingQuality {
2626
willSet {
2727
if newValue != selectedQuality {
28-
userSettings.downloadQuality = newValue
28+
userSettings.streamingQuality = newValue
2929
interactor.saveSettings(userSettings)
3030
}
3131
}
3232
}
33-
let quality = Array([VideoQuality.auto, VideoQuality.low, VideoQuality.medium, VideoQuality.high].enumerated())
33+
let quality = Array([StreamingQuality.auto, StreamingQuality.low, StreamingQuality.medium, StreamingQuality.high].enumerated())
3434

3535
var errorMessage: String? {
3636
didSet {
@@ -51,11 +51,11 @@ public class SettingsViewModel: ObservableObject {
5151

5252
self.userSettings = interactor.getSettings()
5353
self.wifiOnly = userSettings.wifiOnly
54-
self.selectedQuality = userSettings.downloadQuality
54+
self.selectedQuality = userSettings.streamingQuality
5555
}
5656
}
5757

58-
extension VideoQuality {
58+
extension StreamingQuality {
5959

6060
func title() -> String {
6161
switch self {

Profile/Profile/SwiftGen/Strings.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ public enum ProfileLocalization {
104104
public static let title = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TITLE", fallback: "Comfirm log out")
105105
}
106106
public enum Settings {
107-
/// Smallest video quality
108-
public static let quality360Description = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_DESCRIPTION", fallback: "Smallest video quality")
107+
/// Lower data usage
108+
public static let quality360Description = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_DESCRIPTION", fallback: "Lower data usage")
109109
/// 360p
110110
public static let quality360Title = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_TITLE", fallback: "360p")
111111
/// 540p
@@ -128,8 +128,8 @@ public enum ProfileLocalization {
128128
public static let version = ProfileLocalization.tr("Localizable", "SETTINGS.VERSION", fallback: "Version:")
129129
/// Auto (Recommended)
130130
public static let videoQualityDescription = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_DESCRIPTION", fallback: "Auto (Recommended)")
131-
/// Video download quality
132-
public static let videoQualityTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_TITLE", fallback: "Video download quality")
131+
/// Video streaming quality
132+
public static let videoQualityTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_TITLE", fallback: "Video streaming quality")
133133
/// Video settings
134134
public static let videoSettingsTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_SETTINGS_TITLE", fallback: "Video settings")
135135
/// Only download content when wi-fi is turned on

Profile/Profile/en.lproj/Localizable.strings

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@
5858
"SETTINGS.VIDEO_SETTINGS_TITLE" = "Video settings";
5959
"SETTINGS.WIFI_TITLE" = "Wi-fi only download";
6060
"SETTINGS.WIFI_DESCRIPTION" = "Only download content when wi-fi is turned on";
61-
"SETTINGS.VIDEO_QUALITY_TITLE" = "Video download quality";
61+
"SETTINGS.VIDEO_QUALITY_TITLE" = "Video streaming quality";
6262
"SETTINGS.VIDEO_QUALITY_DESCRIPTION" = "Auto (Recommended)";
6363

6464
"SETTINGS.QUALITY_AUTO_TITLE" = "Auto";
6565
"SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Recommended";
6666
"SETTINGS.QUALITY_360_TITLE" = "360p";
67-
"SETTINGS.QUALITY_360_DESCRIPTION" = "Smallest video quality";
67+
"SETTINGS.QUALITY_360_DESCRIPTION" = "Lower data usage";
6868
"SETTINGS.QUALITY_540_TITLE" = "540p";
6969
"SETTINGS.QUALITY_720_TITLE" = "720p";
7070
"SETTINGS.QUALITY_720_DESCRIPTION" = "Best quality";

Profile/Profile/uk.lproj/Localizable.strings

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@
5858
"SETTINGS.VIDEO_SETTINGS_TITLE" = "Налаштування відео";
5959
"SETTINGS.WIFI_TITLE" = "Тільки Wi-fi";
6060
"SETTINGS.WIFI_DESCRIPTION" = "Завантажувати відео, лише коли Wi-Fi увімкнено";
61-
"SETTINGS.VIDEO_QUALITY_TITLE" = "Якість відео";
61+
"SETTINGS.VIDEO_QUALITY_TITLE" = "Якість потокового відео";
6262
"SETTINGS.VIDEO_QUALITY_DESCRIPTION" = "Авто (Рекомендовано)";
6363

6464
"SETTINGS.QUALITY_AUTO_TITLE" = "Авто";
6565
"SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Рекомендовано";
6666
"SETTINGS.QUALITY_360_TITLE" = "360p";
67-
"SETTINGS.QUALITY_360_DESCRIPTION" = "Найменша якість відео";
67+
"SETTINGS.QUALITY_360_DESCRIPTION" = "економія трафіку";
6868
"SETTINGS.QUALITY_540_TITLE" = "540p";
6969
"SETTINGS.QUALITY_720_TITLE" = "720p";
7070
"SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Найкраща якість";

0 commit comments

Comments
 (0)