Files
letro-ios/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift

128 lines
4.9 KiB
Swift

//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum VoiceMessageMediaManagerError: Error {
case unsupportedMimeTye
case missingURL
}
private final class VoiceMessageConversionRequest {
var continuations: [CheckedContinuation<URL, Error>] = []
}
class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
private let mediaProvider: MediaProviderProtocol
private let voiceMessageCache: VoiceMessageCacheProtocol
private let audioConverter: AudioConverterProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol?
private let processingQueue: DispatchQueue
private var conversionRequests = [MediaSourceProxy: VoiceMessageConversionRequest]()
private let supportedVoiceMessageMimeType = "audio/ogg"
init(mediaProvider: MediaProviderProtocol,
voiceMessageCache: VoiceMessageCacheProtocol = VoiceMessageCache(),
audioConverter: AudioConverterProtocol = AudioConverter(),
processingQueue: DispatchQueue = .global(),
backgroundTaskService: BackgroundTaskServiceProtocol?) {
self.mediaProvider = mediaProvider
self.voiceMessageCache = voiceMessageCache
self.audioConverter = audioConverter
self.processingQueue = processingQueue
self.backgroundTaskService = backgroundTaskService
}
deinit {
voiceMessageCache.clearCache()
}
func loadVoiceMessageFromSource(_ source: MediaSourceProxy, body: String?) async throws -> URL {
let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
defer { loadFileBgTask?.stop() }
guard let mimeType = source.mimeType, mimeType.starts(with: supportedVoiceMessageMimeType) else {
throw VoiceMessageMediaManagerError.unsupportedMimeTye
}
// Do we already have a converted version?
if let fileURL = voiceMessageCache.fileURL(for: source) {
return fileURL
}
// Otherwise, load the file from source
guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, body: body) else {
throw MediaProviderError.failedRetrievingFile
}
return try await enqueueVoiceMessageConversionRequest(forSource: source) { [audioConverter, voiceMessageCache] in
// Do we already have a converted version?
if let fileURL = voiceMessageCache.fileURL(for: source) {
return fileURL
}
// Convert from ogg
guard let url = fileHandle.url else {
throw VoiceMessageMediaManagerError.missingURL
}
let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue)
try audioConverter.convertToMPEG4AAC(sourceURL: url, destinationURL: convertedFileURL)
// Cache the file and return the url
let result = voiceMessageCache.cache(mediaSource: source, using: convertedFileURL, move: true)
switch result {
case .success(let url):
return url
case .failure(let error):
throw error
}
}
}
// MARK: - Private
private func enqueueVoiceMessageConversionRequest(forSource source: MediaSourceProxy, operation: @escaping () throws -> URL) async throws -> URL {
if let conversionRequests = conversionRequests[source] {
return try await withCheckedThrowingContinuation { continuation in
conversionRequests.continuations.append(continuation)
}
}
let conversionRequest = VoiceMessageConversionRequest()
conversionRequests[source] = conversionRequest
defer {
conversionRequests[source] = nil
}
do {
let result = try await Task.dispatch(on: processingQueue) {
try operation()
}
conversionRequest.continuations.forEach { $0.resume(returning: result) }
return result
} catch {
conversionRequest.continuations.forEach { $0.resume(throwing: error) }
throw error
}
}
}