@mentra/glasses-media 3.2.0-dev.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +35 -0
  2. package/android/build.gradle +84 -0
  3. package/android/src/main/AndroidManifest.xml +6 -0
  4. package/android/src/main/java/com/mentra/glassesmedia/network/InternetHold.kt +501 -0
  5. package/android/src/main/java/com/mentra/glassesmedia/network/Ipv4Prefix.kt +51 -0
  6. package/android/src/main/java/com/mentra/glassesmedia/network/NetworkFacts.kt +81 -0
  7. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkChangeDetector.kt +355 -0
  8. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkError.kt +109 -0
  9. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkObserver.kt +160 -0
  10. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkReadiness.kt +103 -0
  11. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkRequestSpec.kt +53 -0
  12. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedNetworkState.kt +127 -0
  13. package/android/src/main/java/com/mentra/glassesmedia/network/ScopedSoftApNetwork.kt +573 -0
  14. package/android/src/main/java/com/mentra/glassesmedia/source/BitmapFont.kt +99 -0
  15. package/android/src/main/java/com/mentra/glassesmedia/source/CloudflareWhepSource.kt +487 -0
  16. package/android/src/main/java/com/mentra/glassesmedia/source/DecodedTrackRelay.kt +147 -0
  17. package/android/src/main/java/com/mentra/glassesmedia/source/FirstFrameGate.kt +45 -0
  18. package/android/src/main/java/com/mentra/glassesmedia/source/GlassesMediaSource.kt +143 -0
  19. package/android/src/main/java/com/mentra/glassesmedia/source/LocalWhipIngestSource.kt +772 -0
  20. package/android/src/main/java/com/mentra/glassesmedia/source/MediaListeners.kt +57 -0
  21. package/android/src/main/java/com/mentra/glassesmedia/source/SdpAdapter.kt +17 -0
  22. package/android/src/main/java/com/mentra/glassesmedia/source/SelectedIcePair.kt +139 -0
  23. package/android/src/main/java/com/mentra/glassesmedia/source/SoftApIceDiagnosis.kt +144 -0
  24. package/android/src/main/java/com/mentra/glassesmedia/source/SoftApIcePolicy.kt +46 -0
  25. package/android/src/main/java/com/mentra/glassesmedia/source/SoftApSdpGuard.kt +160 -0
  26. package/android/src/main/java/com/mentra/glassesmedia/source/SyntheticFrameFactory.kt +205 -0
  27. package/android/src/main/java/com/mentra/glassesmedia/source/SyntheticI420Source.kt +214 -0
  28. package/android/src/main/java/com/mentra/glassesmedia/source/TrackRegistry.kt +33 -0
  29. package/android/src/main/java/com/mentra/glassesmedia/source/VideoSourceArm.kt +110 -0
  30. package/android/src/main/java/com/mentra/glassesmedia/source/WhipIngestProtocol.kt +207 -0
  31. package/android/src/main/java/com/mentra/glassesmedia/source/WhipIngestServer.kt +356 -0
  32. package/android/src/main/java/com/mentra/glassesmedia/telemetry/AvSyncProbe.kt +155 -0
  33. package/android/src/main/java/com/mentra/glassesmedia/telemetry/ChromaProbe.kt +78 -0
  34. package/android/src/main/java/com/mentra/glassesmedia/telemetry/PipelineStats.kt +372 -0
  35. package/android/src/main/java/com/mentra/glassesmedia/telemetry/PipelineTicker.kt +79 -0
  36. package/android/src/main/java/com/mentra/glassesmedia/telemetry/ProcessCpu.kt +15 -0
  37. package/android/src/main/java/com/mentra/glassesmedia/telemetry/RingPercentile.kt +48 -0
  38. package/android/src/main/java/com/mentra/glassesmedia/telemetry/VideoQuality.kt +178 -0
  39. package/android/src/main/java/com/mentra/glassesmedia/telemetry/VideoRateVerdict.kt +145 -0
  40. package/android/src/main/java/com/mentra/glassesmedia/trace/SoftApTrace.kt +143 -0
  41. package/android/src/main/java/com/mentra/glassesmedia/video/FrameGeometry.kt +22 -0
  42. package/android/src/main/java/com/mentra/glassesmedia/video/I420Packer.kt +72 -0
  43. package/expo-module.config.json +12 -0
  44. package/index.js +1 -0
  45. package/ios/CoreKit/Sources/GlassesMediaCore/GlassesMediaSource.swift +104 -0
  46. package/ios/CoreKit/Sources/GlassesMediaCore/LocalMediaPolicy.swift +51 -0
  47. package/ios/CoreKit/Sources/GlassesMediaCore/WhipIngestServer.swift +145 -0
  48. package/ios/CoreKit/Sources/GlassesMediaCore/WhipRequest.swift +59 -0
  49. package/ios/DecodedGlassesMediaSource.swift +9 -0
  50. package/ios/DecodedPcm.swift +33 -0
  51. package/ios/GlassesHotspotNetwork.swift +185 -0
  52. package/ios/GlassesMedia.podspec +18 -0
  53. package/ios/LocalWhipIngestSource.swift +325 -0
  54. package/ios/ReceiveOnlyAudioDevice.swift +106 -0
  55. package/ios/WhepVideoSource.swift +332 -0
  56. package/package.json +27 -0
@@ -0,0 +1,325 @@
1
+ import AVFoundation
2
+ import CoreVideo
3
+ import Foundation
4
+ import WebRTC
5
+
6
+ /// The same local WHIP contract as Android: glasses offer, phone answers, publishers receive
7
+ /// decoded buffers. All peer lifecycle transitions and generation checks run on `queue`.
8
+ public final class LocalWhipIngestSource: NSObject, DecodedGlassesMediaSource {
9
+ public var onFrame: ((CVPixelBuffer) -> Void)?
10
+ public var onPcm: ((Data, Int, Int) -> Void)?
11
+ public var onStateChange: ((SourceState, String) -> Void)?
12
+ private let queue = DispatchQueue(label: "com.mentra.glassesmedia.whip.peer")
13
+ private let queueKey = DispatchSpecificKey<Bool>()
14
+ private var currentState: SourceState = .idle
15
+ private var url: String?
16
+ public var state: SourceState {
17
+ DispatchQueue.getSpecific(key: queueKey) == true ? currentState : queue.sync { currentState }
18
+ }
19
+
20
+ public var ingestUrl: String? {
21
+ DispatchQueue.getSpecific(key: queueKey) == true ? url : queue.sync { url }
22
+ }
23
+
24
+ private var address = ""
25
+ private var generation = 0
26
+ private var factory: RTCPeerConnectionFactory?
27
+ private var peer: RTCPeerConnection?
28
+ private var server: WhipIngestServer?
29
+ private var video: RTCVideoTrack?
30
+ private var audio: [RTCAudioTrack] = []
31
+ private var renderer: LocalMediaRenderer?
32
+ private var pcmEnabled = false
33
+ private var pendingAnswer: ((Result<String, Error>) -> Void)?
34
+ private var localDescriptionSet = false
35
+ private var firstFrameSeen = false
36
+ private var stopping = false
37
+ private var stopReplies: [() -> Void] = []
38
+ private let videoSlot = DispatchSemaphore(value: 1)
39
+ private let audioSlots = DispatchSemaphore(value: 8)
40
+
41
+ override public init() {
42
+ super.init()
43
+ queue.setSpecific(key: queueKey, value: true)
44
+ }
45
+
46
+ public func start(config: SourceConfig) {
47
+ prepare(config: config) { _ in }
48
+ }
49
+
50
+ public func restart(config _: SourceConfig) {
51
+ // A new URL must be delivered to the glasses by the owner. Never silently rotate it.
52
+ queue.async { self.transition(.failed, "local_restart_requires_republish") }
53
+ }
54
+
55
+ public func forceRestart() {
56
+ queue.async { self.transition(.failed, "local_restart_requires_republish") }
57
+ }
58
+
59
+ /// Resolves only after the HTTP listener is bound. The owner can then tell the glasses to publish.
60
+ public func prepare(config: SourceConfig, completion: @escaping (Result<String, Error>) -> Void) {
61
+ queue.async {
62
+ guard self.server == nil, !self.stopping, let address = config.bindAddress, LocalMediaPolicy.isPrivate(address) else {
63
+ completion(.failure(LocalMediaError("A free receiver and the phone's hotspot address are required"))); return
64
+ }
65
+ self.address = address
66
+ self.generation += 1
67
+ let gen = self.generation
68
+ self.transition(.connecting, "listening")
69
+ RTCInitializeSSL()
70
+ let factory = RTCPeerConnectionFactory(encoderFactory: RTCDefaultVideoEncoderFactory(), decoderFactory: RTCDefaultVideoDecoderFactory(), audioDevice: ReceiveOnlyAudioDevice())
71
+ let options = RTCPeerConnectionFactoryOptions()
72
+ options.ignoreCellularNetworkAdapter = true
73
+ options.ignoreVPNNetworkAdapter = true
74
+ options.ignoreLoopbackNetworkAdapter = true
75
+ options.ignoreEthernetNetworkAdapter = true
76
+ factory.setOptions(options)
77
+ self.factory = factory
78
+ let server = WhipIngestServer(
79
+ negotiate: { [weak self] offer, reply in
80
+ guard let self else { reply(.failure(LocalMediaError("Receiver released"))); return }
81
+ self.queue.async { self.negotiate(offer, completion: reply) }
82
+ },
83
+ terminate: { [weak self] in
84
+ self?.queue.async {
85
+ guard let self else { return }
86
+ self.closePeer()
87
+ if self.currentState != .idle { self.transition(.failed, "publisher_terminated") }
88
+ }
89
+ },
90
+ publisherFailed: { [weak self] in self?.state == .failed }
91
+ )
92
+ self.server = server
93
+ server.start(address: address) { [weak self] result in
94
+ guard let self else { completion(.failure(LocalMediaError("Receiver released"))); return }
95
+ self.queue.async {
96
+ guard gen == self.generation, !self.stopping else { completion(.failure(LocalMediaError("Receiver cancelled"))); return }
97
+ switch result {
98
+ case let .success(url): self.url = url
99
+ case let .failure(error): self.transition(.failed, error.localizedDescription)
100
+ }
101
+ completion(result)
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ public func setPcmDeliveryEnabled(_ enabled: Bool) {
108
+ queue.async { self.pcmEnabled = enabled }
109
+ }
110
+
111
+ public func stop() {
112
+ stop(completion: {})
113
+ }
114
+
115
+ public func stop(completion: @escaping () -> Void) {
116
+ queue.async {
117
+ self.stopReplies.append(completion)
118
+ guard !self.stopping else { return }
119
+ self.stopping = true
120
+ self.generation += 1
121
+ self.url = nil
122
+ self.closePeer()
123
+ self.transition(.idle, "stop")
124
+ let server = self.server
125
+ self.server = nil
126
+ let finish = {
127
+ self.queue.async {
128
+ self.factory = nil
129
+ self.stopping = false
130
+ let replies = self.stopReplies
131
+ self.stopReplies.removeAll()
132
+ replies.forEach { $0() }
133
+ }
134
+ }
135
+ if let server { server.stop(completion: finish) } else { finish() }
136
+ }
137
+ }
138
+
139
+ private func negotiate(_ offer: String, completion: @escaping (Result<String, Error>) -> Void) {
140
+ guard let factory, !stopping, currentState != .idle else { completion(.failure(LocalMediaError("Receiver stopped"))); return }
141
+ do {
142
+ let localOffer = try LocalMediaPolicy.localSdp(offer, address: address, answer: false)
143
+ closePeer()
144
+ generation += 1
145
+ let gen = generation
146
+ transition(.connecting, "offer")
147
+ let configuration = RTCConfiguration()
148
+ configuration.iceServers = []
149
+ configuration.sdpSemantics = .unifiedPlan
150
+ configuration.bundlePolicy = .maxBundle
151
+ configuration.tcpCandidatePolicy = .disabled
152
+ configuration.continualGatheringPolicy = .gatherOnce
153
+ let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
154
+ guard let peer = factory.peerConnection(with: configuration, constraints: constraints, delegate: self) else { throw LocalMediaError("Peer creation failed") }
155
+ self.peer = peer
156
+ pendingAnswer = completion
157
+ renderer = LocalMediaRenderer(
158
+ frame: { [weak self] buffer in self?.deliverFrame(buffer, generation: gen) },
159
+ pcm: { [weak self] pcm, rate, channels in self?.deliverPcm(pcm, rate: rate, channels: channels, generation: gen) }
160
+ )
161
+ peer.setRemoteDescription(RTCSessionDescription(type: .offer, sdp: localOffer)) { [weak self, weak peer] error in
162
+ self?.queue.async {
163
+ guard let self, let peer, gen == self.generation, self.peer === peer else { return }
164
+ if let error { self.fail(error); return }
165
+ peer.answer(for: constraints) { [weak self, weak peer] answer, error in
166
+ self?.queue.async {
167
+ guard let self, let peer, gen == self.generation, self.peer === peer else { return }
168
+ guard let answer, error == nil else { self.fail(error ?? LocalMediaError("Answer creation failed")); return }
169
+ peer.setLocalDescription(answer) { [weak self, weak peer] error in
170
+ self?.queue.async {
171
+ guard let self, let peer, gen == self.generation, self.peer === peer else { return }
172
+ if let error { self.fail(error); return }
173
+ self.localDescriptionSet = true
174
+ self.finishAnswerIfGathered()
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ queue.asyncAfter(deadline: .now() + 9) { [weak self] in
182
+ guard let self, gen == self.generation, self.pendingAnswer != nil else { return }
183
+ self.fail(LocalMediaError("WHIP negotiation timed out"))
184
+ }
185
+ } catch { completion(.failure(error)); transition(.failed, error.localizedDescription) }
186
+ }
187
+
188
+ private func finishAnswerIfGathered() {
189
+ guard localDescriptionSet, let peer, peer.iceGatheringState == .complete, let completion = pendingAnswer else { return }
190
+ do {
191
+ let answer = try LocalMediaPolicy.localSdp(peer.localDescription?.sdp ?? "", address: address, answer: true)
192
+ pendingAnswer = nil
193
+ completion(.success(answer))
194
+ let gen = generation
195
+ queue.asyncAfter(deadline: .now() + 6) { [weak self] in
196
+ guard let self, gen == self.generation, !self.firstFrameSeen else { return }
197
+ self.fail(LocalMediaError("No decoded video after WHIP negotiation"))
198
+ }
199
+ } catch { fail(error) }
200
+ }
201
+
202
+ private func closePeer() {
203
+ generation += 1
204
+ let completion = pendingAnswer
205
+ pendingAnswer = nil
206
+ completion?(.failure(LocalMediaError("Negotiation cancelled")))
207
+ if let renderer {
208
+ video?.remove(renderer)
209
+ audio.forEach { $0.remove(renderer) }
210
+ }
211
+ video = nil
212
+ audio.removeAll()
213
+ renderer = nil
214
+ let previous = peer
215
+ peer = nil
216
+ previous?.close()
217
+ localDescriptionSet = false
218
+ firstFrameSeen = false
219
+ }
220
+
221
+ private func fail(_ error: Error) {
222
+ let completion = pendingAnswer
223
+ pendingAnswer = nil
224
+ completion?(.failure(error))
225
+ closePeer()
226
+ transition(.failed, error.localizedDescription)
227
+ }
228
+
229
+ private func attach(_ track: RTCMediaStreamTrack) {
230
+ guard let renderer else { return }
231
+ if let track = track as? RTCVideoTrack, video !== track {
232
+ video?.remove(renderer)
233
+ video = track
234
+ track.add(renderer)
235
+ }
236
+ if let track = track as? RTCAudioTrack, !audio.contains(where: { $0 === track }) {
237
+ audio.append(track)
238
+ track.isEnabled = true
239
+ track.add(renderer)
240
+ }
241
+ }
242
+
243
+ private func deliverFrame(_ buffer: CVPixelBuffer, generation gen: Int) {
244
+ // Bound queued media and return immediately: close() can wait for WebRTC render callbacks.
245
+ guard videoSlot.wait(timeout: .now()) == .success else { return }
246
+ queue.async { [self] in
247
+ defer { videoSlot.signal() }
248
+ guard gen == generation, peer != nil, !stopping else { return }
249
+ firstFrameSeen = true
250
+ transition(.live, "first_frame")
251
+ onFrame?(buffer)
252
+ }
253
+ }
254
+
255
+ private func deliverPcm(_ pcm: Data, rate: Int, channels: Int, generation gen: Int) {
256
+ guard audioSlots.wait(timeout: .now()) == .success else { return }
257
+ queue.async { [self] in
258
+ defer { audioSlots.signal() }
259
+ guard gen == generation, peer != nil, pcmEnabled, !stopping else { return }
260
+ onPcm?(pcm, rate, channels)
261
+ }
262
+ }
263
+
264
+ private func transition(_ state: SourceState, _ reason: String) {
265
+ guard currentState != state else { return }
266
+ currentState = state
267
+ NSLog("GLASSES-MEDIA source=\(state.rawValue) reason=\(reason)")
268
+ onStateChange?(state, reason)
269
+ }
270
+ }
271
+
272
+ extension LocalWhipIngestSource: RTCPeerConnectionDelegate {
273
+ public func peerConnection(_: RTCPeerConnection, didChange _: RTCSignalingState) {}
274
+ public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
275
+ queue.async { guard self.peer === peerConnection else { return }; stream.videoTracks.forEach(self.attach); stream.audioTracks.forEach(self.attach) }
276
+ }
277
+
278
+ public func peerConnection(_: RTCPeerConnection, didRemove _: RTCMediaStream) {}
279
+ public func peerConnectionShouldNegotiate(_: RTCPeerConnection) {}
280
+ public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
281
+ queue.async {
282
+ guard self.peer === peerConnection else { return }
283
+ if newState == .failed { self.fail(LocalMediaError("Local ICE failed")) }
284
+ if newState == .disconnected {
285
+ self.firstFrameSeen = false
286
+ self.transition(.connecting, "ice_disconnected")
287
+ let gen = self.generation
288
+ self.queue.asyncAfter(deadline: .now() + 6) {
289
+ guard gen == self.generation, self.peer === peerConnection, !self.firstFrameSeen else { return }
290
+ self.fail(LocalMediaError("Local ICE disconnected"))
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ public func peerConnection(_ peerConnection: RTCPeerConnection, didChange _: RTCIceGatheringState) {
297
+ queue.async { guard self.peer === peerConnection else { return }; self.finishAnswerIfGathered() }
298
+ }
299
+
300
+ public func peerConnection(_: RTCPeerConnection, didGenerate _: RTCIceCandidate) {}
301
+ public func peerConnection(_: RTCPeerConnection, didRemove _: [RTCIceCandidate]) {}
302
+ public func peerConnection(_: RTCPeerConnection, didOpen _: RTCDataChannel) {}
303
+ public func peerConnection(_ peerConnection: RTCPeerConnection, didStartReceivingOn transceiver: RTCRtpTransceiver) {
304
+ queue.async { guard self.peer === peerConnection, let track = transceiver.receiver.track else { return }; self.attach(track) }
305
+ }
306
+ }
307
+
308
+ private final class LocalMediaRenderer: NSObject, RTCVideoRenderer, RTCAudioRenderer {
309
+ let frame: (CVPixelBuffer) -> Void
310
+ let pcm: (Data, Int, Int) -> Void
311
+ init(frame: @escaping (CVPixelBuffer) -> Void, pcm: @escaping (Data, Int, Int) -> Void) {
312
+ self.frame = frame; self.pcm = pcm
313
+ }
314
+
315
+ func setSize(_: CGSize) {}
316
+ func renderFrame(_ frame: RTCVideoFrame?) {
317
+ guard let frame, let buffer = frame.buffer as? RTCCVPixelBuffer else { return }
318
+ self.frame(buffer.pixelBuffer)
319
+ }
320
+
321
+ func render(pcmBuffer: AVAudioPCMBuffer) {
322
+ guard let data = DecodedPcm.pcm16(pcmBuffer) else { return }
323
+ pcm(data, Int(pcmBuffer.format.sampleRate), Int(pcmBuffer.format.channelCount))
324
+ }
325
+ }
@@ -0,0 +1,106 @@
1
+ import AudioToolbox
2
+ import Foundation
3
+ import WebRTC
4
+
5
+ /// Pulls the receive mixer on a dedicated thread so remote PCM renderers run, but never opens
6
+ /// the phone microphone, loudspeaker, or AVAudioSession. The publisher owns audio routing.
7
+ final class ReceiveOnlyAudioDevice: NSObject, RTCAudioDevice {
8
+ let deviceInputSampleRate: Double = 48000
9
+ let inputIOBufferDuration: TimeInterval = 0.01
10
+ let inputNumberOfChannels = 1
11
+ let inputLatency: TimeInterval = 0
12
+ let deviceOutputSampleRate: Double = 48000
13
+ let outputIOBufferDuration: TimeInterval = 0.01
14
+ let outputNumberOfChannels = 1
15
+ let outputLatency: TimeInterval = 0
16
+ private(set) var isInitialized = false
17
+ private(set) var isPlayoutInitialized = false
18
+ private(set) var isPlaying = false
19
+ let isRecordingInitialized = false
20
+ let isRecording = false
21
+ private let condition = NSCondition()
22
+ private var quitting = false
23
+ private var exited = true
24
+ private var playing = false
25
+
26
+ func initialize(with delegate: RTCAudioDeviceDelegate) -> Bool {
27
+ condition.lock()
28
+ quitting = false
29
+ playing = false
30
+ exited = false
31
+ condition.unlock()
32
+ isInitialized = true
33
+ let thread = Thread { [self, delegate] in
34
+ let storage = UnsafeMutablePointer<Int16>.allocate(capacity: 480)
35
+ defer {
36
+ storage.deallocate()
37
+ condition.lock(); exited = true; condition.broadcast(); condition.unlock()
38
+ }
39
+ var sampleTime: Double = 0
40
+ while true {
41
+ condition.lock()
42
+ while !playing && !quitting {
43
+ condition.wait()
44
+ }
45
+ let stop = quitting
46
+ condition.unlock()
47
+ if stop { return }
48
+ var flags = AudioUnitRenderActionFlags()
49
+ var stamp = AudioTimeStamp()
50
+ stamp.mSampleTime = sampleTime
51
+ stamp.mFlags = .sampleTimeValid
52
+ var buffers = AudioBufferList(mNumberBuffers: 1, mBuffers: AudioBuffer(mNumberChannels: 1, mDataByteSize: 960, mData: storage))
53
+ _ = delegate.getPlayoutData(&flags, &stamp, 0, 480, &buffers)
54
+ sampleTime += 480
55
+ Thread.sleep(forTimeInterval: 0.01)
56
+ }
57
+ }
58
+ thread.name = "Mentra local audio receive"
59
+ thread.qualityOfService = .userInitiated
60
+ thread.start()
61
+ return true
62
+ }
63
+
64
+ func terminateDevice() -> Bool {
65
+ condition.lock()
66
+ quitting = true
67
+ playing = false
68
+ condition.broadcast()
69
+ while !exited {
70
+ condition.wait()
71
+ }
72
+ condition.unlock()
73
+ isInitialized = false
74
+ isPlaying = false
75
+ isPlayoutInitialized = false
76
+ return true
77
+ }
78
+
79
+ func initializePlayout() -> Bool {
80
+ isPlayoutInitialized = true; return true
81
+ }
82
+
83
+ func startPlayout() -> Bool {
84
+ condition.lock(); playing = true; condition.broadcast(); condition.unlock()
85
+ isPlaying = true
86
+ return true
87
+ }
88
+
89
+ func stopPlayout() -> Bool {
90
+ condition.lock(); playing = false; condition.unlock()
91
+ isPlaying = false
92
+ return true
93
+ }
94
+
95
+ func initializeRecording() -> Bool {
96
+ false
97
+ }
98
+
99
+ func startRecording() -> Bool {
100
+ false
101
+ }
102
+
103
+ func stopRecording() -> Bool {
104
+ true
105
+ }
106
+ }