mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
New voice message recording mode (#2051)
* Lock voice message recording * Use the VoiceMessageCache to store the recording file * Rework on some composer toolbar buttons * Update accessibility labels for voice message recording button * PreviewTests
This commit is contained in:
@@ -22,6 +22,12 @@ class VoiceMessageCache: VoiceMessageCacheProtocol {
|
||||
FileManager.default.temporaryDirectory.appendingPathComponent("media/voice-message")
|
||||
}
|
||||
|
||||
var urlForRecording: URL {
|
||||
// Make sure the directory exist
|
||||
setupTemporaryFilesFolder()
|
||||
return temporaryFilesFolderURL.appendingPathComponent("voice-message-recording").appendingPathExtension(preferredFileExtension)
|
||||
}
|
||||
|
||||
func fileURL(for mediaSource: MediaSourceProxy) -> URL? {
|
||||
let url = cacheURL(for: mediaSource)
|
||||
return FileManager.default.fileExists(atPath: url.path()) ? url : nil
|
||||
@@ -69,6 +75,7 @@ class VoiceMessageCache: VoiceMessageCacheProtocol {
|
||||
} else {
|
||||
try FileManager.default.copyItem(at: source, to: destination)
|
||||
}
|
||||
try (destination as NSURL).setResourceValue(URLFileProtection.complete, forKey: .fileProtectionKey)
|
||||
}
|
||||
|
||||
private func cacheURL(for mediaSource: MediaSourceProxy) -> URL {
|
||||
|
||||
@@ -22,6 +22,9 @@ enum VoiceMessageCacheError: Error {
|
||||
}
|
||||
|
||||
protocol VoiceMessageCacheProtocol {
|
||||
/// URL to use for recording
|
||||
var urlForRecording: URL { get }
|
||||
|
||||
/// Returns the URL of the cached audio file for a given media source
|
||||
/// - Parameter mediaSource: the media source
|
||||
/// - Returns: the URL of the cached audio file or nil if the file doesn't exist
|
||||
|
||||
@@ -33,8 +33,12 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
private let mp4accMimeType = "audio/m4a"
|
||||
private let waveformSamplesCount = 100
|
||||
|
||||
var isRecording: Bool {
|
||||
audioRecorder.isRecording
|
||||
}
|
||||
|
||||
var recordingURL: URL? {
|
||||
audioRecorder.audioFileUrl
|
||||
audioRecorder.audioFileURL
|
||||
}
|
||||
|
||||
var recordingDuration: TimeInterval {
|
||||
@@ -67,9 +71,10 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
|
||||
func startRecording() async {
|
||||
await stopPlayback()
|
||||
previewAudioPlayer?.reset()
|
||||
recordingCancelled = false
|
||||
|
||||
await audioRecorder.record(with: .uuid(UUID()))
|
||||
await audioRecorder.record(audioFileURL: voiceMessageCache.urlForRecording)
|
||||
}
|
||||
|
||||
func stopRecording() async {
|
||||
@@ -78,22 +83,26 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
}
|
||||
|
||||
func cancelRecording() async {
|
||||
MXLog.info("Cancel recording.")
|
||||
recordingCancelled = true
|
||||
await audioRecorder.stopRecording()
|
||||
await audioRecorder.deleteRecording()
|
||||
previewAudioPlayerState = nil
|
||||
previewAudioPlayer?.reset()
|
||||
}
|
||||
|
||||
func deleteRecording() async {
|
||||
MXLog.info("Delete recording.")
|
||||
await stopPlayback()
|
||||
await audioRecorder.deleteRecording()
|
||||
previewAudioPlayer?.reset()
|
||||
previewAudioPlayerState = nil
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
func startPlayback() async -> Result<Void, VoiceMessageRecorderError> {
|
||||
guard let previewAudioPlayerState, let url = audioRecorder.audioFileUrl else {
|
||||
guard let previewAudioPlayerState, let url = audioRecorder.audioFileURL else {
|
||||
return .failure(.previewNotAvailable)
|
||||
}
|
||||
|
||||
@@ -132,7 +141,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
}
|
||||
|
||||
func buildRecordingWaveform() async -> Result<[UInt16], VoiceMessageRecorderError> {
|
||||
guard let url = audioRecorder.audioFileUrl else {
|
||||
guard let url = audioRecorder.audioFileURL else {
|
||||
return .failure(.missingRecordingFile)
|
||||
}
|
||||
// build the waveform
|
||||
@@ -150,13 +159,18 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
}
|
||||
|
||||
func sendVoiceMessage(inRoom roomProxy: RoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> {
|
||||
guard let url = audioRecorder.audioFileUrl else {
|
||||
guard let url = audioRecorder.audioFileURL else {
|
||||
return .failure(VoiceMessageRecorderError.missingRecordingFile)
|
||||
}
|
||||
|
||||
// convert the file
|
||||
let sourceFilename = url.deletingPathExtension().lastPathComponent
|
||||
let oggFile = URL.temporaryDirectory.appendingPathComponent(sourceFilename).appendingPathExtension("ogg")
|
||||
defer {
|
||||
// delete the temporary file
|
||||
try? FileManager.default.removeItem(at: oggFile)
|
||||
}
|
||||
|
||||
do {
|
||||
try audioConverter.convertToOpusOgg(sourceURL: url, destinationURL: oggFile)
|
||||
} catch {
|
||||
@@ -180,8 +194,6 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
audioInfo: audioInfo,
|
||||
waveform: waveform,
|
||||
progressSubject: nil) { _ in }
|
||||
// delete the temporary file
|
||||
try? FileManager.default.removeItem(at: oggFile)
|
||||
|
||||
if case .failure(let error) = result {
|
||||
MXLog.error("Failed to send the voice message. \(error)")
|
||||
@@ -219,7 +231,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
actionsSubject.send(.didFailWithError(error: VoiceMessageRecorderError.previewNotAvailable))
|
||||
return
|
||||
}
|
||||
guard let recordingURL = audioRecorder.audioFileUrl, let previewAudioPlayerState else {
|
||||
guard let recordingURL = audioRecorder.audioFileURL, let previewAudioPlayerState else {
|
||||
actionsSubject.send(.didFailWithError(error: VoiceMessageRecorderError.previewNotAvailable))
|
||||
return
|
||||
}
|
||||
@@ -235,7 +247,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
|
||||
private func finalizeRecording() async -> Result<Void, VoiceMessageRecorderError> {
|
||||
MXLog.info("finalize audio recording")
|
||||
guard let url = audioRecorder.audioFileUrl, audioRecorder.currentTime > 0 else {
|
||||
guard let url = audioRecorder.audioFileURL, audioRecorder.currentTime > 0 else {
|
||||
return .failure(.previewNotAvailable)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ enum VoiceMessageRecorderAction {
|
||||
protocol VoiceMessageRecorderProtocol {
|
||||
var audioRecorder: AudioRecorderProtocol { get }
|
||||
var previewAudioPlayerState: AudioPlayerState? { get }
|
||||
var isRecording: Bool { get }
|
||||
var recordingURL: URL? { get }
|
||||
var recordingDuration: TimeInterval { get }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user