2021-06-25 09:09:41 +00:00
|
|
|
//
|
|
|
|
// 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)
|
2021-06-25 10:18:40 +00:00
|
|
|
case invalidNumberOfSamples
|
2021-06-25 09:09:41 +00:00
|
|
|
case samplingError
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array.
|
|
|
|
*/
|
|
|
|
private class CompletionWrapper {
|
2021-07-14 07:26:21 +00:00
|
|
|
let completion: (Result<VoiceMessageAttachmentCacheManagerLoadResult, Error>) -> Void
|
2021-06-25 09:09:41 +00:00
|
|
|
|
2021-07-14 07:26:21 +00:00
|
|
|
init(_ completion: @escaping (Result<VoiceMessageAttachmentCacheManagerLoadResult, Error>) -> Void) {
|
2021-06-25 09:09:41 +00:00
|
|
|
self.completion = completion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-14 07:26:21 +00:00
|
|
|
struct VoiceMessageAttachmentCacheManagerLoadResult {
|
|
|
|
let eventIdentifier: String
|
|
|
|
let url: URL
|
|
|
|
let duration: TimeInterval
|
|
|
|
let samples: [Float]
|
|
|
|
}
|
|
|
|
|
2021-06-25 09:09:41 +00:00
|
|
|
class VoiceMessageAttachmentCacheManager {
|
|
|
|
|
|
|
|
static let sharedManager = VoiceMessageAttachmentCacheManager()
|
|
|
|
|
2021-07-16 10:31:38 +00:00
|
|
|
private var completionCallbacks = [String: [CompletionWrapper]]()
|
2021-06-25 09:09:41 +00:00
|
|
|
private var samples = [String: [Int: [Float]]]()
|
2021-07-02 05:38:56 +00:00
|
|
|
private var durations = [String: TimeInterval]()
|
2021-06-25 09:09:41 +00:00
|
|
|
private var finalURLs = [String: URL]()
|
|
|
|
|
2021-07-06 21:03:56 +00:00
|
|
|
private init() {
|
2021-07-16 10:31:38 +00:00
|
|
|
|
2021-07-06 21:03:56 +00:00
|
|
|
}
|
2021-06-25 09:09:41 +00:00
|
|
|
|
2021-07-14 07:26:21 +00:00
|
|
|
func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<VoiceMessageAttachmentCacheManagerLoadResult, Error>) -> Void) {
|
2021-06-25 09:09:41 +00:00
|
|
|
guard attachment.type == MXKAttachmentTypeVoiceMessage else {
|
2021-06-25 10:18:40 +00:00
|
|
|
completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType))
|
2021-06-25 09:09:41 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let identifier = attachment.eventId else {
|
2021-06-25 10:18:40 +00:00
|
|
|
completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard numberOfSamples > 0 else {
|
|
|
|
completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidNumberOfSamples))
|
2021-06-25 09:09:41 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-16 10:31:38 +00:00
|
|
|
if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] {
|
|
|
|
let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples)
|
|
|
|
completion(Result.success(result))
|
|
|
|
return
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
2021-07-16 10:31:38 +00:00
|
|
|
|
|
|
|
self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion)
|
2021-06-25 10:18:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 07:26:21 +00:00
|
|
|
private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<VoiceMessageAttachmentCacheManagerLoadResult, Error>) -> Void) {
|
2021-07-16 10:31:38 +00:00
|
|
|
|
|
|
|
if var callbacks = completionCallbacks[identifier] {
|
2021-06-25 09:09:41 +00:00
|
|
|
callbacks.append(CompletionWrapper(completion))
|
2021-07-16 10:31:38 +00:00
|
|
|
completionCallbacks[identifier] = callbacks
|
2021-06-25 09:09:41 +00:00
|
|
|
return
|
|
|
|
} else {
|
2021-07-16 10:31:38 +00:00
|
|
|
completionCallbacks[identifier] = [CompletionWrapper(completion)]
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
|
2021-07-02 05:38:56 +00:00
|
|
|
func sampleFileAtURL(_ url: URL, duration: TimeInterval) {
|
2021-06-25 09:09:41 +00:00
|
|
|
let analyser = WaveformAnalyzer(audioAssetURL: url)
|
|
|
|
analyser?.samples(count: numberOfSamples, completionHandler: { samples in
|
2021-06-25 11:42:52 +00:00
|
|
|
// Dispatch back from the WaveformAnalyzer's internal queue
|
2021-07-16 10:31:38 +00:00
|
|
|
DispatchQueue.main.async {
|
2021-06-25 09:09:41 +00:00
|
|
|
guard let samples = samples else {
|
|
|
|
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if var existingSamples = self.samples[identifier] {
|
|
|
|
existingSamples[numberOfSamples] = samples
|
2021-07-16 06:06:36 +00:00
|
|
|
self.samples[identifier] = existingSamples
|
2021-06-25 09:09:41 +00:00
|
|
|
} else {
|
|
|
|
self.samples[identifier] = [numberOfSamples: samples]
|
|
|
|
}
|
|
|
|
|
2021-07-02 05:38:56 +00:00
|
|
|
self.invokeSuccessCallbacksForIdentifier(identifier, url: url, duration: duration, samples: samples)
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-02 05:38:56 +00:00
|
|
|
if let finalURL = finalURLs[identifier], let duration = durations[identifier] {
|
|
|
|
sampleFileAtURL(finalURL, duration: duration)
|
2021-06-25 09:09:41 +00:00
|
|
|
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
|
2021-07-02 05:38:56 +00:00
|
|
|
VoiceMessageAudioConverter.mediaDurationAt(newURL) { result in
|
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
if let duration = try? result.get() {
|
2021-07-12 12:50:44 +00:00
|
|
|
self.durations[identifier] = duration
|
2021-07-02 05:38:56 +00:00
|
|
|
sampleFileAtURL(newURL, duration: duration)
|
|
|
|
} else {
|
|
|
|
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: Failed to retrieve media duration")
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed getting audio duration with: \(error)")
|
|
|
|
}
|
|
|
|
}
|
2021-06-25 09:09:41 +00:00
|
|
|
case .failure(let error):
|
|
|
|
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error))
|
2021-07-02 05:38:56 +00:00
|
|
|
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed decoding audio message with: \(error)")
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-02 05:38:56 +00:00
|
|
|
private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) {
|
2021-07-16 10:31:38 +00:00
|
|
|
guard let callbacks = completionCallbacks[identifier] else {
|
2021-06-25 09:09:41 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-14 07:26:21 +00:00
|
|
|
let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: url, duration: duration, samples: samples)
|
|
|
|
|
2021-06-25 10:18:40 +00:00
|
|
|
let copy = callbacks.map { $0 }
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
for wrapper in copy {
|
2021-07-14 07:26:21 +00:00
|
|
|
wrapper.completion(Result.success(result))
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-16 10:31:38 +00:00
|
|
|
self.completionCallbacks[identifier] = nil
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) {
|
2021-07-16 10:31:38 +00:00
|
|
|
guard let callbacks = completionCallbacks[identifier] else {
|
2021-06-25 09:09:41 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-25 10:18:40 +00:00
|
|
|
let copy = callbacks.map { $0 }
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
for wrapper in copy {
|
2021-06-25 09:09:41 +00:00
|
|
|
wrapper.completion(Result.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-16 10:31:38 +00:00
|
|
|
self.completionCallbacks[identifier] = nil
|
2021-06-25 09:09:41 +00:00
|
|
|
}
|
|
|
|
}
|