Add a toggle in the developer options to optimise the media uploads. (#3408)

This commit is contained in:
Doug 2024-10-14 14:48:59 +01:00 committed by GitHub
parent b1b2297972
commit 98a5ee5b48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 109 additions and 34 deletions

View File

@ -42,9 +42,9 @@ final class AppSettings {
// Feature flags
case slidingSyncDiscovery
case optimizeMediaUploads
case publicSearchEnabled
case fuzzyRoomListSearchEnabled
case pinningEnabled
case enableOnlySignedDeviceIsolationMode
case identityPinningViolationNotificationsEnabled
case knockingEnabled
@ -281,6 +281,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.slidingSyncDiscovery, defaultValue: .native, storageType: .userDefaults(store))
var slidingSyncDiscovery: SlidingSyncDiscovery
@UserPreference(key: UserDefaultsKeys.optimizeMediaUploads, defaultValue: false, storageType: .userDefaults(store))
var optimizeMediaUploads
@UserPreference(key: UserDefaultsKeys.identityPinningViolationNotificationsEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store))
var identityPinningViolationNotificationsEnabled

View File

@ -810,6 +810,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let roomDetailsEditParameters = RoomDetailsEditScreenCoordinatorParameters(roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
navigationStackCoordinator: stackCoordinator,
userIndicatorController: userIndicatorController,
orientationManager: appMediator.windowManager)
@ -895,7 +896,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController,
roomProxy: roomProxy,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
title: url.lastPathComponent,
url: url)

View File

@ -165,6 +165,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(orientationManager: parameters.windowManager,
clientProxy: parameters.userSession.clientProxy,
mediaProvider: parameters.userSession.mediaProvider,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: parameters.appSettings),
navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: parameters.userIndicatorController))

View File

@ -542,7 +542,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
userSession: userSession,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
navigationStackCoordinator: startChatNavigationStackCoordinator,
userDiscoveryService: userDiscoveryService)
userDiscoveryService: userDiscoveryService,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings))
let coordinator = StartChatScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in

View File

@ -116,7 +116,7 @@ private class PreviewItem: NSObject, QLPreviewItem {
struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: UserIndicatorControllerMock.default,
roomProxy: JoinedRoomProxyMock(),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
title: "some random file name",
url: URL.picturesDirectory)
static var previews: some View {

View File

@ -11,6 +11,7 @@ import SwiftUI
struct RoomDetailsEditScreenCoordinatorParameters {
let roomProxy: JoinedRoomProxyProtocol
let mediaProvider: MediaProviderProtocol
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
weak var navigationStackCoordinator: NavigationStackCoordinator?
let userIndicatorController: UserIndicatorControllerProtocol
let orientationManager: OrientationManagerProtocol
@ -35,6 +36,7 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
viewModel = RoomDetailsEditScreenViewModel(roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider,
mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor,
userIndicatorController: parameters.userIndicatorController)
}

View File

@ -14,7 +14,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init()
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
@ -22,8 +22,10 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
init(roomProxy: JoinedRoomProxyProtocol,
mediaProvider: MediaProviderProtocol,
mediaUploadingPreprocessor: MediaUploadingPreprocessor,
userIndicatorController: UserIndicatorControllerProtocol) {
self.roomProxy = roomProxy
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.userIndicatorController = userIndicatorController
let roomAvatar = roomProxy.avatarURL
@ -76,7 +78,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
title: L10n.commonLoading,
persistent: true))
let mediaResult = await mediaPreprocessor.processMedia(at: url)
let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url)
switch mediaResult {
case .success(.image):

View File

@ -154,6 +154,7 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
return RoomDetailsEditScreenViewModel(roomProxy: roomProxy,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
userIndicatorController: UserIndicatorControllerMock.default)
}()
@ -164,6 +165,7 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
return RoomDetailsEditScreenViewModel(roomProxy: roomProxy,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
userIndicatorController: UserIndicatorControllerMock.default)
}()

View File

@ -46,6 +46,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var hideUnreadMessagesBadge: Bool { get set }
var fuzzyRoomListSearchEnabled: Bool { get set }
var hideTimelineMedia: Bool { get set }
var optimizeMediaUploads: Bool { get set }
var enableOnlySignedDeviceIsolationMode: Bool { get set }
var elementCallBaseURLOverride: URL? { get set }
var identityPinningViolationNotificationsEnabled: Bool { get set }

View File

@ -62,6 +62,12 @@ struct DeveloperOptionsScreen: View {
}
}
Section("Media") {
Toggle(isOn: $context.optimizeMediaUploads) {
Text("Optimise for upload")
}
}
Section {
Toggle(isOn: $context.enableOnlySignedDeviceIsolationMode) {
Text("Exclude insecure devices when sending/receiving messages")

View File

@ -12,6 +12,7 @@ struct UserDetailsEditScreenCoordinatorParameters {
let orientationManager: OrientationManagerProtocol
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
weak var navigationStackCoordinator: NavigationStackCoordinator?
let userIndicatorController: UserIndicatorControllerProtocol
}
@ -26,6 +27,7 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
viewModel = UserDetailsEditScreenViewModel(clientProxy: parameters.clientProxy,
mediaProvider: parameters.mediaProvider,
mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor,
userIndicatorController: parameters.userIndicatorController)
}

View File

@ -14,7 +14,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
private let actionsSubject: PassthroughSubject<UserDetailsEditScreenViewModelAction, Never> = .init()
private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
var actions: AnyPublisher<UserDetailsEditScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
@ -22,8 +22,10 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
init(clientProxy: ClientProxyProtocol,
mediaProvider: MediaProviderProtocol,
mediaUploadingPreprocessor: MediaUploadingPreprocessor,
userIndicatorController: UserIndicatorControllerProtocol) {
self.clientProxy = clientProxy
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.userIndicatorController = userIndicatorController
super.init(initialViewState: UserDetailsEditScreenViewState(userID: clientProxy.userID,
@ -88,7 +90,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
title: L10n.commonLoading,
persistent: true))
let mediaResult = await mediaPreprocessor.processMedia(at: url)
let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url)
switch mediaResult {
case .success(.image):

View File

@ -117,6 +117,7 @@ struct UserDetailsEditScreen: View {
struct UserDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = UserDetailsEditScreenViewModel(clientProxy: ClientProxyMock(.init(userID: "@stefan:matrix.org")),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaUploadingPreprocessor: .init(appSettings: ServiceLocator.shared.settings),
userIndicatorController: UserIndicatorControllerMock.default)
static var previews: some View {

View File

@ -14,6 +14,7 @@ struct StartChatScreenCoordinatorParameters {
let userIndicatorController: UserIndicatorControllerProtocol
weak var navigationStackCoordinator: NavigationStackCoordinator?
let userDiscoveryService: UserDiscoveryServiceProtocol
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
}
enum StartChatScreenCoordinatorAction {
@ -134,7 +135,6 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
// MARK: - Private
let mediaUploadingPreprocessor = MediaUploadingPreprocessor()
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
let stackCoordinator = NavigationStackCoordinator()
@ -159,7 +159,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
Task { [weak self] in
guard let self else { return }
do {
let media = try await mediaUploadingPreprocessor.processMedia(at: url).get()
let media = try await parameters.mediaUploadingPreprocessor.processMedia(at: url).get()
var parameters = createRoomParameters.value
parameters.avatarImageMedia = media
createRoomParameters.send(parameters)

View File

@ -82,6 +82,8 @@ private struct VideoProcessingInfo {
}
struct MediaUploadingPreprocessor {
let appSettings: AppSettings
enum Constants {
static let maximumThumbnailSize = CGSize(width: 800, height: 600)
static let thumbnailCompressionQuality = 0.8
@ -368,8 +370,9 @@ struct MediaUploadingPreprocessor {
/// - Returns: the URL for the resulting video and its media info as a `VideoProcessingResult`
private func convertVideoToMP4(_ url: URL, targetFileSize: UInt = 0) async -> Result<VideoProcessingInfo, MediaUploadingPreprocessorError> {
let asset = AVURLAsset(url: url)
let presetName = appSettings.optimizeMediaUploads ? AVAssetExportPreset640x480 : AVAssetExportPreset1920x1080
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1920x1080) else {
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
return .failure(.failedConvertingVideo)
}

View File

@ -581,7 +581,8 @@ class MockScreen: Identifiable {
userSession: userSession,
userIndicatorController: UserIndicatorControllerMock(),
navigationStackCoordinator: navigationStackCoordinator,
userDiscoveryService: userDiscoveryMock)
userDiscoveryService: userDiscoveryMock,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings))
let coordinator = StartChatScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -595,7 +596,8 @@ class MockScreen: Identifiable {
userSession: userSession,
userIndicatorController: UserIndicatorControllerMock(),
navigationStackCoordinator: navigationStackCoordinator,
userDiscoveryService: userDiscoveryMock))
userDiscoveryService: userDiscoveryMock,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings)))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .createRoom:

View File

@ -10,7 +10,19 @@ import XCTest
@testable import ElementX
final class MediaUploadingPreprocessorTests: XCTestCase {
let mediaUploadingPreprocessor = MediaUploadingPreprocessor()
var appSettings: AppSettings!
var mediaUploadingPreprocessor: MediaUploadingPreprocessor!
override func setUp() {
AppSettings.resetAllSettings()
appSettings = AppSettings()
ServiceLocator.shared.register(appSettings: appSettings)
mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings)
}
override func tearDown() {
AppSettings.resetAllSettings()
}
func testAudioFileProcessing() async {
guard let url = Bundle(for: Self.self).url(forResource: "test_audio.mp3", withExtension: nil) else {
@ -70,6 +82,23 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 34206, accuracy: 100)
XCTAssertEqual(videoInfo.thumbnailInfo?.width, 800)
XCTAssertEqual(videoInfo.thumbnailInfo?.height, 450)
// Repeat with optimised media setting
appSettings.optimizeMediaUploads = true
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url),
case let .video(_, _, optimizedVideoInfo) = optimizedResult else {
XCTFail("Failed processing asset")
return
}
// Check optimised video info
XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4")
XCTAssertEqual(optimizedVideoInfo.blurhash, "K32PJbx^I7jYaebHMvV?o$")
XCTAssertEqual(optimizedVideoInfo.size ?? 0, 4_090_898, accuracy: 100) // Note: This is slightly stupid because it is larger now 🤦
XCTAssertEqual(optimizedVideoInfo.width, 640)
XCTAssertEqual(optimizedVideoInfo.height, 360)
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
}
func testPortraitMp4VideoProcessing() async {
@ -110,6 +139,23 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 83220, accuracy: 100)
XCTAssertEqual(videoInfo.thumbnailInfo?.width, 337)
XCTAssertEqual(videoInfo.thumbnailInfo?.height, 600)
// Repeat with optimised media setting
appSettings.optimizeMediaUploads = true
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url),
case let .video(_, _, optimizedVideoInfo) = optimizedResult else {
XCTFail("Failed processing asset")
return
}
// Check optimised video info
XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4")
XCTAssertEqual(optimizedVideoInfo.blurhash, "K7BDNJD*0L%#sl_2~C9ZE1")
XCTAssertEqual(optimizedVideoInfo.size ?? 0, 6_520_897, accuracy: 100)
XCTAssertEqual(optimizedVideoInfo.width, 360)
XCTAssertEqual(optimizedVideoInfo.height, 640)
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
}
func testLandscapeImageProcessing() async {
@ -185,28 +231,10 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
XCTAssertEqual(originalImage.size, convertedImage.size)
// Check that the GPS data has been stripped
guard let imageSource = CGImageSourceCreateWithData(originalImageData as NSData, nil) else {
XCTFail("Invalid test asset")
return
}
guard let originalMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else {
XCTFail("Test asset is expected to contain metadata")
return
}
let originalMetadata = metadata(from: originalImageData)
XCTAssertNotNil(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
guard let convertedImageSource = CGImageSourceCreateWithData(convertedImageData as NSData, nil) else {
XCTFail("Invalid converted asset")
return
}
guard let convertedMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(convertedImageSource, 0, nil) else {
XCTFail("Test asset is expected to contain metadata")
return
}
let convertedMetadata = metadata(from: convertedImageData)
XCTAssertNil(convertedMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
// Check that the thumbnail is generated correctly
@ -223,5 +251,22 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
}
let thumbnailMetadata = metadata(from: thumbnailData)
XCTAssertNil(thumbnailMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
}
private func metadata(from imageData: Data) -> NSDictionary {
guard let imageSource = CGImageSourceCreateWithData(imageData as NSData, nil) else {
XCTFail("Invalid asset")
return [:]
}
guard let convertedMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else {
XCTFail("Test asset is expected to contain metadata")
return [:]
}
return convertedMetadata
}
}

View File

@ -112,6 +112,7 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase {
userIndicatorController = UserIndicatorControllerMock.default
viewModel = .init(roomProxy: JoinedRoomProxyMock(roomProxyConfiguration),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
userIndicatorController: userIndicatorController)
}
}