-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathVoiceMessageInProgressDraft.swift
More file actions
153 lines (123 loc) · 4.89 KB
/
Copy pathVoiceMessageInProgressDraft.swift
File metadata and controls
153 lines (123 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import SignalServiceKit
import SignalUI
/// Represents a voice note that's actively being recorded.
///
/// In most cases you'll immediately send (or discard) voice notes you
/// record. When this happens, an instance of this type will be passed to
/// the voice note sending logic.
///
/// In some cases, an external event may interrupt an active recording. When
/// that happens, we convert this object into a durable
/// ``VoiceMessageInterruptedDraft``. That draft is visible in the compose
/// box for the user to return to later.
final class VoiceMessageInProgressDraft: VoiceMessageSendableDraft {
private let threadUniqueId: String
private let audioFileUrl: URL
private let audioActivity: AudioActivity
private let audioSession: AudioSession
private let sleepManager: any DeviceSleepManager
init(thread: TSThread, audioSession: AudioSession, sleepManager: any DeviceSleepManager) {
self.threadUniqueId = thread.uniqueId
self.audioFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: "m4a",
isAvailableWhileDeviceLocked: false,
)
self.audioActivity = AudioActivity(audioDescription: "Voice Message Recording", behavior: .recordAudio)
self.audioSession = audioSession
self.sleepManager = sleepManager
}
deinit {
Task { [sleepManager, sleepBlockObject] in
await sleepManager.removeBlock(blockObject: sleepBlockObject)
}
}
private let sleepBlockObject = DeviceSleepBlockObject(blockReason: "voice message")
private var audioRecorder: AVAudioRecorder?
var isRecording: Bool { audioRecorder?.isRecording ?? false }
func startRecording() throws {
AssertIsOnMainThread()
guard !isRecording else {
throw OWSAssertionError("Attempted to start recording while recording is in progress")
}
guard audioSession.startAudioActivity(audioActivity) else {
throw OWSAssertionError("Couldn't configure audio session")
}
let audioRecorder: AVAudioRecorder
do {
audioRecorder = try AVAudioRecorder(
url: audioFileUrl,
settings: [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 1,
AVEncoderBitRateKey: 32000,
],
)
self.audioRecorder = audioRecorder
} catch {
throw OWSAssertionError("Couldn't create audioRecorder: \(error)")
}
MainActor.assumeIsolated {
sleepManager.addBlock(blockObject: sleepBlockObject)
}
audioRecorder.isMeteringEnabled = true
guard audioRecorder.prepareToRecord() else {
throw OWSAssertionError("audioRecorder couldn't prepareToRecord.")
}
guard audioRecorder.record() else {
throw OWSAssertionError("audioRecorder couldn't record.")
}
}
func stopRecording() {
AssertIsOnMainThread()
MainActor.assumeIsolated {
sleepManager.removeBlock(blockObject: sleepBlockObject)
}
guard let audioRecorder else { return }
self.audioRecorder = nil
self.duration = audioRecorder.currentTime
audioRecorder.stop()
// This is expensive. We can safely do it in the background.
DispatchQueue.sharedUserInteractive.async {
self.audioSession.endAudioActivity(self.audioActivity)
}
}
func stopRecordingAsync() {
AssertIsOnMainThread()
MainActor.assumeIsolated {
sleepManager.removeBlock(blockObject: sleepBlockObject)
}
guard let audioRecorder else { return }
self.audioRecorder = nil
self.duration = audioRecorder.currentTime
// This is expensive. We can safely do it in the background
// if we're not relying on the recorded audio (e.g. we canceled)
DispatchQueue.sharedUserInteractive.async {
audioRecorder.stop()
self.audioSession.endAudioActivity(self.audioActivity)
}
}
private(set) var duration: TimeInterval?
func updateMeters() {
audioRecorder?.updateMeters()
}
func averagePower() -> Float {
return audioRecorder?.averagePower(forChannel: 0) ?? -120.0
}
func convertToDraft(transaction: DBWriteTransaction) -> VoiceMessageInterruptedDraft {
let directoryUrl = VoiceMessageInterruptedDraftStore.saveDraft(
audioFileUrl: audioFileUrl,
threadUniqueId: threadUniqueId,
transaction: transaction,
)
return VoiceMessageInterruptedDraft(threadUniqueId: threadUniqueId, directoryUrl: directoryUrl)
}
func prepareForSending() throws -> URL {
return audioFileUrl
}
}