mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-19 16:13:42 +02:00
#4094 - Added voice message attachment decryption, transcoding and sampling caching layer.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
import DSWaveformImage
|
||||
|
||||
enum VoiceMessageAttachmentCacheManagerError: Error {
|
||||
case invalidEventId
|
||||
case invalidAttachmentType
|
||||
case decryptionError(Error)
|
||||
case preparationError(Error)
|
||||
case conversionError(Error)
|
||||
case samplingError
|
||||
}
|
||||
|
||||
/**
|
||||
Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array.
|
||||
*/
|
||||
private class CompletionWrapper {
|
||||
let completion: (Result<(URL, [Float]), Error>) -> Void
|
||||
|
||||
init(_ completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceMessageAttachmentCacheManager {
|
||||
|
||||
static let sharedManager = VoiceMessageAttachmentCacheManager()
|
||||
|
||||
private let workQueue: DispatchQueue
|
||||
|
||||
private var completionCallbacks = [String: [CompletionWrapper]]()
|
||||
private var samples = [String: [Int: [Float]]]()
|
||||
private var finalURLs = [String: URL]()
|
||||
|
||||
private init() {
|
||||
workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated)
|
||||
}
|
||||
|
||||
func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
|
||||
workQueue.async {
|
||||
self.enqueueLoadAttachment(attachment, numberOfSamples: numberOfSamples, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func enqueueLoadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
|
||||
guard attachment.type == MXKAttachmentTypeVoiceMessage else {
|
||||
DispatchQueue.main.async {
|
||||
completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let identifier = attachment.eventId else {
|
||||
DispatchQueue.main.async {
|
||||
completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] {
|
||||
DispatchQueue.main.async {
|
||||
completion(Result.success((finalURL, samples)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if var callbacks = completionCallbacks[identifier] {
|
||||
callbacks.append(CompletionWrapper(completion))
|
||||
completionCallbacks[identifier] = callbacks
|
||||
return
|
||||
} else {
|
||||
completionCallbacks[identifier] = [CompletionWrapper(completion)]
|
||||
}
|
||||
|
||||
func sampleFileAtURL(_ url: URL) {
|
||||
let analyser = WaveformAnalyzer(audioAssetURL: url)
|
||||
analyser?.samples(count: numberOfSamples, completionHandler: { samples in
|
||||
self.workQueue.async {
|
||||
guard let samples = samples else {
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError)
|
||||
return
|
||||
}
|
||||
|
||||
if var existingSamples = self.samples[identifier] {
|
||||
existingSamples[numberOfSamples] = samples
|
||||
} else {
|
||||
self.samples[identifier] = [numberOfSamples: samples]
|
||||
}
|
||||
|
||||
self.invokeSuccessCallbacksForIdentifier(identifier, url: url, samples: samples)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let finalURL = finalURLs[identifier] {
|
||||
sampleFileAtURL(finalURL)
|
||||
return
|
||||
}
|
||||
|
||||
func convertFileAtPath(_ path: String?) {
|
||||
guard let filePath = path else {
|
||||
return
|
||||
}
|
||||
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a")
|
||||
|
||||
VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.finalURLs[identifier] = newURL
|
||||
sampleFileAtURL(newURL)
|
||||
case .failure(let error):
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error))
|
||||
MXLog.error("Failed failed decoding audio message with: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attachment.isEncrypted {
|
||||
attachment.decrypt(toTempFile: { filePath in
|
||||
convertFileAtPath(filePath)
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed decrypting attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
attachment.prepare({
|
||||
convertFileAtPath(attachment.cacheFilePath)
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed preparing attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, samples: [Float]) {
|
||||
guard let callbacks = completionCallbacks[identifier] else {
|
||||
return
|
||||
}
|
||||
|
||||
for wrapper in callbacks {
|
||||
DispatchQueue.main.async {
|
||||
wrapper.completion(Result.success((url, samples)))
|
||||
}
|
||||
}
|
||||
|
||||
completionCallbacks[identifier] = nil
|
||||
}
|
||||
|
||||
private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) {
|
||||
guard let callbacks = completionCallbacks[identifier] else {
|
||||
return
|
||||
}
|
||||
|
||||
for wrapper in callbacks {
|
||||
DispatchQueue.main.async {
|
||||
wrapper.completion(Result.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
completionCallbacks[identifier] = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user