react-native-altibbi 0.1.0
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.
- package/LICENSE +20 -0
- package/README.md +190 -0
- package/android/build.gradle +104 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/altibbi/AltibbiModule.kt +18 -0
- package/android/src/main/java/com/altibbi/AltibbiPackage.kt +18 -0
- package/android/src/main/java/com/altibbi/OTCustomAudioDevice.java +1146 -0
- package/android/src/main/java/com/altibbi/OTPublisherLayout.java +61 -0
- package/android/src/main/java/com/altibbi/OTPublisherViewManager.java +30 -0
- package/android/src/main/java/com/altibbi/OTRN.java +101 -0
- package/android/src/main/java/com/altibbi/OTScreenCapturer.java +120 -0
- package/android/src/main/java/com/altibbi/OTSessionManager.java +1281 -0
- package/android/src/main/java/com/altibbi/OTSubscriberLayout.java +68 -0
- package/android/src/main/java/com/altibbi/OTSubscriberViewManager.java +30 -0
- package/android/src/main/java/com/altibbi/Socket.kt +294 -0
- package/android/src/main/java/com/altibbi/SocketEventEmitter.kt +25 -0
- package/android/src/main/java/com/altibbi/utils/EventUtils.java +189 -0
- package/android/src/main/java/com/altibbi/utils/Utils.java +135 -0
- package/ios/Altibbi-Bridging-Header.h +6 -0
- package/ios/Altibbi.mm +10 -0
- package/ios/Altibbi.swift +4 -0
- package/ios/OTCustomAudioDriver.swift +696 -0
- package/ios/OTPublisher.m +16 -0
- package/ios/OTPublisherManager.swift +21 -0
- package/ios/OTPublisherView.swift +28 -0
- package/ios/OTRN.swift +27 -0
- package/ios/OTScreenCapture.h +27 -0
- package/ios/OTScreenCapture.m +171 -0
- package/ios/OTSessionManager.m +127 -0
- package/ios/OTSessionManager.swift +866 -0
- package/ios/OTSubscriber.m +15 -0
- package/ios/OTSubscriberManager.swift +21 -0
- package/ios/OTSubscriberView.swift +29 -0
- package/ios/OpenTokReactNative.h +13 -0
- package/ios/OpenTokReactNative.m +13 -0
- package/ios/SocketReactNative.m +38 -0
- package/ios/SocketReactNative.swift +276 -0
- package/ios/Utils/EventUtils.swift +143 -0
- package/ios/Utils/Utils.swift +126 -0
- package/lib/commonjs/connection.js +200 -0
- package/lib/commonjs/connection.js.map +1 -0
- package/lib/commonjs/data.js +12 -0
- package/lib/commonjs/data.js.map +1 -0
- package/lib/commonjs/index.js +275 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/scoket.js +245 -0
- package/lib/commonjs/scoket.js.map +1 -0
- package/lib/commonjs/service.js +21 -0
- package/lib/commonjs/service.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/video/OT.js +57 -0
- package/lib/commonjs/video/OT.js.map +1 -0
- package/lib/commonjs/video/OTError.js +17 -0
- package/lib/commonjs/video/OTError.js.map +1 -0
- package/lib/commonjs/video/OTPublisher.js +171 -0
- package/lib/commonjs/video/OTPublisher.js.map +1 -0
- package/lib/commonjs/video/OTSession.js +205 -0
- package/lib/commonjs/video/OTSession.js.map +1 -0
- package/lib/commonjs/video/OTSubscriber.js +185 -0
- package/lib/commonjs/video/OTSubscriber.js.map +1 -0
- package/lib/commonjs/video/contexts/OTContext.js +11 -0
- package/lib/commonjs/video/contexts/OTContext.js.map +1 -0
- package/lib/commonjs/video/helpers/OTHelper.js +92 -0
- package/lib/commonjs/video/helpers/OTHelper.js.map +1 -0
- package/lib/commonjs/video/helpers/OTPublisherHelper.js +117 -0
- package/lib/commonjs/video/helpers/OTPublisherHelper.js.map +1 -0
- package/lib/commonjs/video/helpers/OTSessionHelper.js +206 -0
- package/lib/commonjs/video/helpers/OTSessionHelper.js.map +1 -0
- package/lib/commonjs/video/helpers/OTSubscriberHelper.js +121 -0
- package/lib/commonjs/video/helpers/OTSubscriberHelper.js.map +1 -0
- package/lib/commonjs/video/index.js +42 -0
- package/lib/commonjs/video/index.js.map +1 -0
- package/lib/commonjs/video/views/OTPublisherView.js +26 -0
- package/lib/commonjs/video/views/OTPublisherView.js.map +1 -0
- package/lib/commonjs/video/views/OTSubscriberView.js +25 -0
- package/lib/commonjs/video/views/OTSubscriberView.js.map +1 -0
- package/lib/module/connection.js +180 -0
- package/lib/module/connection.js.map +1 -0
- package/lib/module/data.js +6 -0
- package/lib/module/data.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/scoket.js +235 -0
- package/lib/module/scoket.js.map +1 -0
- package/lib/module/service.js +14 -0
- package/lib/module/service.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/video/OT.js +49 -0
- package/lib/module/video/OT.js.map +1 -0
- package/lib/module/video/OTError.js +10 -0
- package/lib/module/video/OTError.js.map +1 -0
- package/lib/module/video/OTPublisher.js +162 -0
- package/lib/module/video/OTPublisher.js.map +1 -0
- package/lib/module/video/OTSession.js +195 -0
- package/lib/module/video/OTSession.js.map +1 -0
- package/lib/module/video/OTSubscriber.js +175 -0
- package/lib/module/video/OTSubscriber.js.map +1 -0
- package/lib/module/video/contexts/OTContext.js +4 -0
- package/lib/module/video/contexts/OTContext.js.map +1 -0
- package/lib/module/video/helpers/OTHelper.js +82 -0
- package/lib/module/video/helpers/OTHelper.js.map +1 -0
- package/lib/module/video/helpers/OTPublisherHelper.js +110 -0
- package/lib/module/video/helpers/OTPublisherHelper.js.map +1 -0
- package/lib/module/video/helpers/OTSessionHelper.js +195 -0
- package/lib/module/video/helpers/OTSessionHelper.js.map +1 -0
- package/lib/module/video/helpers/OTSubscriberHelper.js +112 -0
- package/lib/module/video/helpers/OTSubscriberHelper.js.map +1 -0
- package/lib/module/video/index.js +7 -0
- package/lib/module/video/index.js.map +1 -0
- package/lib/module/video/views/OTPublisherView.js +18 -0
- package/lib/module/video/views/OTPublisherView.js.map +1 -0
- package/lib/module/video/views/OTSubscriberView.js +17 -0
- package/lib/module/video/views/OTSubscriberView.js.map +1 -0
- package/lib/typescript/src/connection.d.ts +40 -0
- package/lib/typescript/src/connection.d.ts.map +1 -0
- package/lib/typescript/src/data.d.ts +7 -0
- package/lib/typescript/src/data.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +12 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/scoket.d.ts +100 -0
- package/lib/typescript/src/scoket.d.ts.map +1 -0
- package/lib/typescript/src/service.d.ts +9 -0
- package/lib/typescript/src/service.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +22 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +178 -0
- package/react-native-altibbi.podspec +46 -0
- package/src/connection.ts +255 -0
- package/src/data.ts +21 -0
- package/src/index.tsx +80 -0
- package/src/scoket.ts +365 -0
- package/src/service.ts +20 -0
- package/src/types.ts +22 -0
- package/src/video/OT.js +65 -0
- package/src/video/OTError.js +14 -0
- package/src/video/OTPublisher.js +193 -0
- package/src/video/OTSession.js +168 -0
- package/src/video/OTSubscriber.js +148 -0
- package/src/video/contexts/OTContext.js +5 -0
- package/src/video/helpers/OTHelper.js +91 -0
- package/src/video/helpers/OTPublisherHelper.js +122 -0
- package/src/video/helpers/OTSessionHelper.js +233 -0
- package/src/video/helpers/OTSubscriberHelper.js +125 -0
- package/src/video/index.js +13 -0
- package/src/video/views/OTPublisherView.js +19 -0
- package/src/video/views/OTSubscriberView.js +18 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import OpenTok
|
|
3
|
+
|
|
4
|
+
class OTCustomAudioDriver: NSObject {
|
|
5
|
+
#if targetEnvironment(simulator)
|
|
6
|
+
static let kSampleRate: UInt16 = 44100
|
|
7
|
+
#else
|
|
8
|
+
static let kSampleRate: UInt16 = 48000
|
|
9
|
+
#endif
|
|
10
|
+
static let kOutputBus = AudioUnitElement(0)
|
|
11
|
+
static let kInputBus = AudioUnitElement(1)
|
|
12
|
+
static let kAudioDeviceHeadset = "AudioSessionManagerDevice_Headset"
|
|
13
|
+
static let kAudioDeviceBluetooth = "AudioSessionManagerDevice_Bluetooth"
|
|
14
|
+
static let kAudioDeviceSpeaker = "AudioSessionManagerDevice_Speaker"
|
|
15
|
+
|
|
16
|
+
var inputAudioFormat = OTAudioFormat()
|
|
17
|
+
var outputAudioFormat = OTAudioFormat()
|
|
18
|
+
let safetyQueue = DispatchQueue(label: "ot-audio-driver")
|
|
19
|
+
|
|
20
|
+
var deviceAudioBus: OTAudioBus?
|
|
21
|
+
|
|
22
|
+
func setAudioBus(_ audioBus: OTAudioBus?) -> Bool {
|
|
23
|
+
deviceAudioBus = audioBus
|
|
24
|
+
outputAudioFormat = OTAudioFormat()
|
|
25
|
+
outputAudioFormat.sampleRate = OTCustomAudioDriver.kSampleRate
|
|
26
|
+
outputAudioFormat.numChannels = 2
|
|
27
|
+
inputAudioFormat = OTAudioFormat()
|
|
28
|
+
inputAudioFormat.sampleRate = OTCustomAudioDriver.kSampleRate
|
|
29
|
+
inputAudioFormat.numChannels = 1
|
|
30
|
+
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var bufferList: UnsafeMutablePointer<AudioBufferList>?
|
|
35
|
+
var bufferSize: UInt32 = 0
|
|
36
|
+
var bufferNumFrames: UInt32 = 0
|
|
37
|
+
var playoutAudioUnitPropertyLatency: Float64 = 0
|
|
38
|
+
var playoutDelayMeasurementCounter: UInt32 = 0
|
|
39
|
+
var recordingDelayMeasurementCounter: UInt32 = 0
|
|
40
|
+
var recordingDelayHWAndOS: UInt32 = 0
|
|
41
|
+
var recordingDelay: UInt32 = 0
|
|
42
|
+
var recordingAudioUnitPropertyLatency: Float64 = 0
|
|
43
|
+
var playoutDelay: UInt32 = 0
|
|
44
|
+
var playing = false
|
|
45
|
+
var playoutInitialized = false
|
|
46
|
+
var recording = false
|
|
47
|
+
var recordingInitialized = false
|
|
48
|
+
var interruptedPlayback = false
|
|
49
|
+
var isRecorderInterrupted = false
|
|
50
|
+
var isPlayerInterrupted = false
|
|
51
|
+
var isResetting = false
|
|
52
|
+
var restartRetryCount = 0
|
|
53
|
+
fileprivate var recordingVoiceUnit: AudioUnit?
|
|
54
|
+
fileprivate var playoutVoiceUnit: AudioUnit?
|
|
55
|
+
|
|
56
|
+
fileprivate var previousAVAudioSessionCategory: AVAudioSession.Category?
|
|
57
|
+
fileprivate var avAudioSessionMode: AVAudioSession.Mode?
|
|
58
|
+
fileprivate var avAudioSessionPreffSampleRate = Double(0)
|
|
59
|
+
fileprivate var avAudioSessionChannels = 0
|
|
60
|
+
fileprivate var isAudioSessionSetup = false
|
|
61
|
+
|
|
62
|
+
var areListenerBlocksSetup = false
|
|
63
|
+
var streamFormat = AudioStreamBasicDescription()
|
|
64
|
+
|
|
65
|
+
override init() {
|
|
66
|
+
inputAudioFormat.sampleRate = OTCustomAudioDriver.kSampleRate
|
|
67
|
+
inputAudioFormat.numChannels = 1
|
|
68
|
+
outputAudioFormat.sampleRate = OTCustomAudioDriver.kSampleRate
|
|
69
|
+
outputAudioFormat.numChannels = 2
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deinit {
|
|
73
|
+
tearDownAudio()
|
|
74
|
+
removeObservers()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
fileprivate func restartAudio() {
|
|
79
|
+
safetyQueue.async {
|
|
80
|
+
self.doRestartAudio(numberOfAttempts: 3)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fileprivate func restartAudioAfterInterruption() {
|
|
85
|
+
if isRecorderInterrupted {
|
|
86
|
+
if startCapture() {
|
|
87
|
+
isRecorderInterrupted = false
|
|
88
|
+
restartRetryCount = 0
|
|
89
|
+
} else {
|
|
90
|
+
restartRetryCount += 1
|
|
91
|
+
if restartRetryCount < 3 {
|
|
92
|
+
safetyQueue.asyncAfter(deadline: DispatchTime.now(), execute: { [unowned self] in
|
|
93
|
+
self.restartAudioAfterInterruption()
|
|
94
|
+
})
|
|
95
|
+
} else {
|
|
96
|
+
isRecorderInterrupted = false
|
|
97
|
+
isPlayerInterrupted = false
|
|
98
|
+
restartRetryCount = 0
|
|
99
|
+
print("ERROR[OpenTok]:Unable to acquire audio session")
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if isPlayerInterrupted {
|
|
104
|
+
isPlayerInterrupted = false
|
|
105
|
+
let _ = startRendering()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fileprivate func doRestartAudio(numberOfAttempts: Int) {
|
|
110
|
+
|
|
111
|
+
if recording {
|
|
112
|
+
let _ = stopCapture()
|
|
113
|
+
disposeAudioUnit(audioUnit: &recordingVoiceUnit)
|
|
114
|
+
let _ = startCapture()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if playing {
|
|
118
|
+
let _ = self.stopRendering()
|
|
119
|
+
disposeAudioUnit(audioUnit: &playoutVoiceUnit)
|
|
120
|
+
let _ = self.startRendering()
|
|
121
|
+
}
|
|
122
|
+
isResetting = false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fileprivate func setupAudioUnit(withPlayout playout: Bool) -> Bool {
|
|
126
|
+
if !isAudioSessionSetup {
|
|
127
|
+
setupAudioSession()
|
|
128
|
+
isAudioSessionSetup = true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let bytesPerSample = UInt32(MemoryLayout<Int16>.size)
|
|
132
|
+
streamFormat.mFormatID = kAudioFormatLinearPCM
|
|
133
|
+
streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
|
|
134
|
+
if playout {
|
|
135
|
+
streamFormat.mBytesPerPacket = bytesPerSample*2
|
|
136
|
+
streamFormat.mBytesPerFrame = bytesPerSample*2
|
|
137
|
+
streamFormat.mChannelsPerFrame = 2
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
streamFormat.mBytesPerPacket = bytesPerSample
|
|
141
|
+
streamFormat.mBytesPerFrame = bytesPerSample
|
|
142
|
+
streamFormat.mChannelsPerFrame = 1
|
|
143
|
+
}
|
|
144
|
+
streamFormat.mFramesPerPacket = 1
|
|
145
|
+
streamFormat.mBitsPerChannel = 8 * bytesPerSample
|
|
146
|
+
streamFormat.mSampleRate = Float64(OTCustomAudioDriver.kSampleRate)
|
|
147
|
+
|
|
148
|
+
var audioUnitDescription = AudioComponentDescription()
|
|
149
|
+
audioUnitDescription.componentType = kAudioUnitType_Output
|
|
150
|
+
if playout {
|
|
151
|
+
audioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO
|
|
152
|
+
}
|
|
153
|
+
else{
|
|
154
|
+
audioUnitDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO
|
|
155
|
+
}
|
|
156
|
+
audioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO
|
|
157
|
+
audioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple
|
|
158
|
+
audioUnitDescription.componentFlags = 0
|
|
159
|
+
audioUnitDescription.componentFlagsMask = 0
|
|
160
|
+
|
|
161
|
+
let foundVpioUnitRef = AudioComponentFindNext(nil, &audioUnitDescription)
|
|
162
|
+
let result: OSStatus = {
|
|
163
|
+
if playout {
|
|
164
|
+
return AudioComponentInstanceNew(foundVpioUnitRef!, &playoutVoiceUnit)
|
|
165
|
+
} else {
|
|
166
|
+
return AudioComponentInstanceNew(foundVpioUnitRef!, &recordingVoiceUnit)
|
|
167
|
+
}
|
|
168
|
+
}()
|
|
169
|
+
|
|
170
|
+
if result != noErr {
|
|
171
|
+
print("Error seting up audio unit")
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
var value: UInt32 = 1
|
|
176
|
+
if playout {
|
|
177
|
+
AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO,
|
|
178
|
+
kAudioUnitScope_Output, OTCustomAudioDriver.kOutputBus, &value,
|
|
179
|
+
UInt32(MemoryLayout<UInt32>.size))
|
|
180
|
+
|
|
181
|
+
AudioUnitSetProperty(playoutVoiceUnit!, kAudioUnitProperty_StreamFormat,
|
|
182
|
+
kAudioUnitScope_Input, OTCustomAudioDriver.kOutputBus, &streamFormat,
|
|
183
|
+
UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
|
|
184
|
+
// Disable Input on playout
|
|
185
|
+
var enableInput = 0
|
|
186
|
+
AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input,
|
|
187
|
+
OTCustomAudioDriver.kInputBus, &enableInput, UInt32(MemoryLayout<UInt32>.size))
|
|
188
|
+
} else {
|
|
189
|
+
AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO,
|
|
190
|
+
kAudioUnitScope_Input, OTCustomAudioDriver.kInputBus, &value,
|
|
191
|
+
UInt32(MemoryLayout<UInt32>.size))
|
|
192
|
+
AudioUnitSetProperty(recordingVoiceUnit!, kAudioUnitProperty_StreamFormat,
|
|
193
|
+
kAudioUnitScope_Output, OTCustomAudioDriver.kInputBus, &streamFormat,
|
|
194
|
+
UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
|
|
195
|
+
// Disable Output on record
|
|
196
|
+
var enableOutput = 0
|
|
197
|
+
AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output,
|
|
198
|
+
OTCustomAudioDriver.kOutputBus, &enableOutput, UInt32(MemoryLayout<UInt32>.size))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if playout {
|
|
202
|
+
setupPlayoutCallback()
|
|
203
|
+
} else {
|
|
204
|
+
setupRecordingCallback()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setBluetoothAsPreferredInputDevice()
|
|
208
|
+
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fileprivate func setupPlayoutCallback() {
|
|
213
|
+
let selfPointer = Unmanaged.passUnretained(self).toOpaque()
|
|
214
|
+
var renderCallback = AURenderCallbackStruct(inputProc: renderCb, inputProcRefCon: selfPointer)
|
|
215
|
+
AudioUnitSetProperty(playoutVoiceUnit!,
|
|
216
|
+
kAudioUnitProperty_SetRenderCallback,
|
|
217
|
+
kAudioUnitScope_Input,
|
|
218
|
+
OTCustomAudioDriver.kOutputBus,
|
|
219
|
+
&renderCallback,
|
|
220
|
+
UInt32(MemoryLayout<AURenderCallbackStruct>.size))
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fileprivate func setupRecordingCallback() {
|
|
225
|
+
let selfPointer = Unmanaged.passUnretained(self).toOpaque()
|
|
226
|
+
var inputCallback = AURenderCallbackStruct(inputProc: recordCb, inputProcRefCon: selfPointer)
|
|
227
|
+
AudioUnitSetProperty(recordingVoiceUnit!,
|
|
228
|
+
kAudioOutputUnitProperty_SetInputCallback,
|
|
229
|
+
kAudioUnitScope_Global,
|
|
230
|
+
OTCustomAudioDriver.kInputBus,
|
|
231
|
+
&inputCallback,
|
|
232
|
+
UInt32(MemoryLayout<AURenderCallbackStruct>.size))
|
|
233
|
+
|
|
234
|
+
var value = 0
|
|
235
|
+
AudioUnitSetProperty(recordingVoiceUnit!,
|
|
236
|
+
kAudioUnitProperty_ShouldAllocateBuffer,
|
|
237
|
+
kAudioUnitScope_Output,
|
|
238
|
+
OTCustomAudioDriver.kInputBus,
|
|
239
|
+
&value,
|
|
240
|
+
UInt32(MemoryLayout<UInt32>.size))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fileprivate func disposeAudioUnit(audioUnit: inout AudioUnit?) {
|
|
244
|
+
if let unit = audioUnit {
|
|
245
|
+
AudioUnitUninitialize(unit)
|
|
246
|
+
AudioComponentInstanceDispose(unit)
|
|
247
|
+
}
|
|
248
|
+
audioUnit = nil
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fileprivate func tearDownAudio() {
|
|
252
|
+
print("Destoying audio units")
|
|
253
|
+
disposeAudioUnit(audioUnit: &playoutVoiceUnit)
|
|
254
|
+
disposeAudioUnit(audioUnit: &recordingVoiceUnit)
|
|
255
|
+
freeupAudioBuffers()
|
|
256
|
+
|
|
257
|
+
let session = AVAudioSession.sharedInstance()
|
|
258
|
+
do {
|
|
259
|
+
guard let previousAVAudioSessionCategory = previousAVAudioSessionCategory else { return }
|
|
260
|
+
if #available(iOS 10.0, *) {
|
|
261
|
+
try session.setCategory(previousAVAudioSessionCategory, mode: .default)
|
|
262
|
+
} else {
|
|
263
|
+
try session.setCategory(previousAVAudioSessionCategory)
|
|
264
|
+
}
|
|
265
|
+
guard let avAudioSessionMode = avAudioSessionMode else { return }
|
|
266
|
+
try session.setMode(avAudioSessionMode)
|
|
267
|
+
try session.setPreferredSampleRate(avAudioSessionPreffSampleRate)
|
|
268
|
+
try session.setPreferredInputNumberOfChannels(avAudioSessionChannels)
|
|
269
|
+
|
|
270
|
+
isAudioSessionSetup = false
|
|
271
|
+
} catch {
|
|
272
|
+
print("Error reseting AVAudioSession")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fileprivate func freeupAudioBuffers() {
|
|
277
|
+
if var data = bufferList?.pointee, data.mBuffers.mData != nil {
|
|
278
|
+
data.mBuffers.mData?.assumingMemoryBound(to: UInt16.self).deallocate()
|
|
279
|
+
data.mBuffers.mData = nil
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if let list = bufferList {
|
|
283
|
+
list.deallocate();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
bufferList = nil
|
|
287
|
+
bufferNumFrames = 0
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// MARK: - Audio Device Implementation
|
|
292
|
+
extension OTCustomAudioDriver: OTAudioDevice {
|
|
293
|
+
func captureFormat() -> OTAudioFormat {
|
|
294
|
+
return inputAudioFormat
|
|
295
|
+
}
|
|
296
|
+
func renderFormat() -> OTAudioFormat {
|
|
297
|
+
return outputAudioFormat
|
|
298
|
+
}
|
|
299
|
+
func renderingIsAvailable() -> Bool {
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
func renderingIsInitialized() -> Bool {
|
|
303
|
+
return playoutInitialized
|
|
304
|
+
}
|
|
305
|
+
func isRendering() -> Bool {
|
|
306
|
+
return playing
|
|
307
|
+
}
|
|
308
|
+
func isCapturing() -> Bool {
|
|
309
|
+
return recording
|
|
310
|
+
}
|
|
311
|
+
func estimatedRenderDelay() -> UInt16 {
|
|
312
|
+
return UInt16(playoutDelay)
|
|
313
|
+
}
|
|
314
|
+
func estimatedCaptureDelay() -> UInt16 {
|
|
315
|
+
return UInt16(recordingDelay)
|
|
316
|
+
}
|
|
317
|
+
func captureIsAvailable() -> Bool {
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
320
|
+
func captureIsInitialized() -> Bool {
|
|
321
|
+
return recordingInitialized
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func initializeRendering() -> Bool {
|
|
325
|
+
if playing { return false }
|
|
326
|
+
|
|
327
|
+
playoutInitialized = true
|
|
328
|
+
return playoutInitialized
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func startRendering() -> Bool {
|
|
332
|
+
if playing { return true }
|
|
333
|
+
playing = true
|
|
334
|
+
if playoutVoiceUnit == nil {
|
|
335
|
+
playing = setupAudioUnit(withPlayout: true)
|
|
336
|
+
if !playing {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let result = AudioOutputUnitStart(playoutVoiceUnit!)
|
|
342
|
+
|
|
343
|
+
if result != noErr {
|
|
344
|
+
print("Error creaing rendering unit")
|
|
345
|
+
playing = false
|
|
346
|
+
}
|
|
347
|
+
return playing
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func stopRendering() -> Bool {
|
|
351
|
+
if !playing {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
playing = false
|
|
356
|
+
|
|
357
|
+
if playoutVoiceUnit != nil {
|
|
358
|
+
let result = AudioOutputUnitStop(playoutVoiceUnit!)
|
|
359
|
+
if result != noErr {
|
|
360
|
+
print("Error creaing playout unit")
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if !recording && !isPlayerInterrupted && !isResetting {
|
|
366
|
+
tearDownAudio()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
func initializeCapture() -> Bool {
|
|
374
|
+
if recording { return false }
|
|
375
|
+
|
|
376
|
+
recordingInitialized = true
|
|
377
|
+
return recordingInitialized
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
func startCapture() -> Bool {
|
|
381
|
+
if recording {
|
|
382
|
+
return true
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
recording = true
|
|
386
|
+
|
|
387
|
+
if recordingVoiceUnit == nil {
|
|
388
|
+
recording = setupAudioUnit(withPlayout: false)
|
|
389
|
+
|
|
390
|
+
if !recording {
|
|
391
|
+
return false
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let result = AudioOutputUnitStart(recordingVoiceUnit!)
|
|
396
|
+
if result != noErr {
|
|
397
|
+
recording = false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return recording
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
func stopCapture() -> Bool {
|
|
404
|
+
if !recording {
|
|
405
|
+
return true
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
recording = false
|
|
409
|
+
|
|
410
|
+
let result = AudioOutputUnitStop(recordingVoiceUnit!)
|
|
411
|
+
|
|
412
|
+
if result != noErr {
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
freeupAudioBuffers()
|
|
417
|
+
|
|
418
|
+
if !recording && !isRecorderInterrupted && !isResetting {
|
|
419
|
+
tearDownAudio()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// MARK: - AVAudioSession
|
|
428
|
+
extension OTCustomAudioDriver {
|
|
429
|
+
@objc func onInterruptionEvent(notification: Notification) {
|
|
430
|
+
let type = notification.userInfo?[AVAudioSessionInterruptionTypeKey]
|
|
431
|
+
safetyQueue.async {
|
|
432
|
+
self.handleInterruptionEvent(type: type as? Int)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fileprivate func handleInterruptionEvent(type: Int?) {
|
|
437
|
+
guard let interruptionType = type else {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
switch UInt(interruptionType) {
|
|
442
|
+
case AVAudioSession.InterruptionType.began.rawValue:
|
|
443
|
+
if recording {
|
|
444
|
+
isRecorderInterrupted = true
|
|
445
|
+
let _ = stopCapture()
|
|
446
|
+
}
|
|
447
|
+
if playing {
|
|
448
|
+
isPlayerInterrupted = true
|
|
449
|
+
let _ = stopRendering()
|
|
450
|
+
}
|
|
451
|
+
case AVAudioSession.InterruptionType.ended.rawValue:
|
|
452
|
+
configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: OTCustomAudioDriver.kAudioDeviceBluetooth)
|
|
453
|
+
restartAudioAfterInterruption()
|
|
454
|
+
default:
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@objc func onRouteChangeEvent(notification: Notification) {
|
|
460
|
+
safetyQueue.async {
|
|
461
|
+
self.handleRouteChangeEvent(notification: notification)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@objc func appDidBecomeActive(notification: Notification) {
|
|
466
|
+
safetyQueue.async {
|
|
467
|
+
self.handleInterruptionEvent(type: Int(AVAudioSession.InterruptionType.ended.rawValue))
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fileprivate func handleRouteChangeEvent(notification: Notification) {
|
|
472
|
+
guard let reason = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt else {
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if reason == AVAudioSession.RouteChangeReason.routeConfigurationChange.rawValue {
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if reason == AVAudioSession.RouteChangeReason.override.rawValue ||
|
|
481
|
+
reason == AVAudioSession.RouteChangeReason.categoryChange.rawValue {
|
|
482
|
+
|
|
483
|
+
let oldRouteDesc = notification.userInfo?[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription
|
|
484
|
+
let outputs = oldRouteDesc.outputs
|
|
485
|
+
var oldOutputDeviceName: String? = nil
|
|
486
|
+
var currentOutputDeviceName: String? = nil
|
|
487
|
+
|
|
488
|
+
if outputs.count > 0 {
|
|
489
|
+
let portDesc = outputs[0]
|
|
490
|
+
oldOutputDeviceName = portDesc.portName
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if AVAudioSession.sharedInstance().currentRoute.outputs.count > 0 {
|
|
494
|
+
currentOutputDeviceName = AVAudioSession.sharedInstance().currentRoute.outputs[0].portName
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if oldOutputDeviceName == currentOutputDeviceName || currentOutputDeviceName == nil || oldOutputDeviceName == nil {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
restartAudio()
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
fileprivate func setupListenerBlocks() {
|
|
506
|
+
if areListenerBlocksSetup {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let notificationCenter = NotificationCenter.default
|
|
511
|
+
|
|
512
|
+
notificationCenter.addObserver(self, selector: #selector(OTCustomAudioDriver.onInterruptionEvent),
|
|
513
|
+
name: AVAudioSession.interruptionNotification, object: nil)
|
|
514
|
+
notificationCenter.addObserver(self, selector: #selector(OTCustomAudioDriver.onRouteChangeEvent(notification:)),
|
|
515
|
+
name: AVAudioSession.routeChangeNotification, object: nil)
|
|
516
|
+
notificationCenter.addObserver(self, selector: #selector(OTCustomAudioDriver.appDidBecomeActive(notification:)),
|
|
517
|
+
name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
518
|
+
|
|
519
|
+
areListenerBlocksSetup = true
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fileprivate func removeObservers() {
|
|
523
|
+
NotificationCenter.default.removeObserver(self)
|
|
524
|
+
areListenerBlocksSetup = false
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
fileprivate func setupAudioSession() {
|
|
528
|
+
let session = AVAudioSession.sharedInstance()
|
|
529
|
+
|
|
530
|
+
previousAVAudioSessionCategory = session.category
|
|
531
|
+
avAudioSessionMode = session.mode
|
|
532
|
+
avAudioSessionPreffSampleRate = session.preferredSampleRate
|
|
533
|
+
avAudioSessionChannels = session.inputNumberOfChannels
|
|
534
|
+
|
|
535
|
+
do {
|
|
536
|
+
try session.setPreferredOutputNumberOfChannels(2)
|
|
537
|
+
try session.setPreferredSampleRate(Double(OTCustomAudioDriver.kSampleRate))
|
|
538
|
+
try session.setPreferredIOBufferDuration(0.01)
|
|
539
|
+
let audioOptions = AVAudioSession.CategoryOptions.mixWithOthers.rawValue |
|
|
540
|
+
AVAudioSession.CategoryOptions.allowBluetooth.rawValue |
|
|
541
|
+
AVAudioSession.CategoryOptions.defaultToSpeaker.rawValue
|
|
542
|
+
if #available(iOS 10.0, *) {
|
|
543
|
+
try session.setCategory(.playAndRecord, mode: .videoChat, options: AVAudioSession.CategoryOptions(rawValue: audioOptions))
|
|
544
|
+
} else {
|
|
545
|
+
try session.setCategory(.playAndRecord, options: AVAudioSession.CategoryOptions(rawValue: audioOptions))
|
|
546
|
+
}
|
|
547
|
+
setupListenerBlocks()
|
|
548
|
+
|
|
549
|
+
try session.setActive(true)
|
|
550
|
+
try session.setPreferredOutputNumberOfChannels(2)
|
|
551
|
+
} catch let err as NSError {
|
|
552
|
+
print("Error setting up audio session \(err)")
|
|
553
|
+
} catch {
|
|
554
|
+
print("Error setting up audio session")
|
|
555
|
+
}
|
|
556
|
+
print("preferred output channels = \(session.preferredOutputNumberOfChannels)")
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// MARK: - Audio Route functions
|
|
561
|
+
extension OTCustomAudioDriver {
|
|
562
|
+
fileprivate func setBluetoothAsPreferredInputDevice() {
|
|
563
|
+
let btRoutes = [AVAudioSession.Port.bluetoothA2DP, AVAudioSession.Port.bluetoothLE, AVAudioSession.Port.bluetoothHFP]
|
|
564
|
+
AVAudioSession.sharedInstance().availableInputs?.forEach({ el in
|
|
565
|
+
if btRoutes.contains(el.portType) {
|
|
566
|
+
do {
|
|
567
|
+
try AVAudioSession.sharedInstance().setPreferredInput(el)
|
|
568
|
+
} catch {
|
|
569
|
+
print("Error setting BT as preferred input device")
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
fileprivate func configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: String) {
|
|
576
|
+
let session = AVAudioSession.sharedInstance()
|
|
577
|
+
|
|
578
|
+
if desiredAudioRoute == OTCustomAudioDriver.kAudioDeviceBluetooth {
|
|
579
|
+
setBluetoothAsPreferredInputDevice()
|
|
580
|
+
}
|
|
581
|
+
do {
|
|
582
|
+
if desiredAudioRoute == OTCustomAudioDriver.kAudioDeviceSpeaker {
|
|
583
|
+
try session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
|
|
584
|
+
} else {
|
|
585
|
+
try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
|
|
586
|
+
}
|
|
587
|
+
} catch let err as NSError {
|
|
588
|
+
print("Error setting audio route: \(err)")
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// MARK: - Render and Record C Callbacks
|
|
594
|
+
func renderCb(inRefCon:UnsafeMutableRawPointer,
|
|
595
|
+
ioActionFlags:UnsafeMutablePointer<AudioUnitRenderActionFlags>,
|
|
596
|
+
inTimeStamp:UnsafePointer<AudioTimeStamp>,
|
|
597
|
+
inBusNumber:UInt32,
|
|
598
|
+
inNumberFrames:UInt32,
|
|
599
|
+
ioData:UnsafeMutablePointer<AudioBufferList>?) -> OSStatus
|
|
600
|
+
{
|
|
601
|
+
let audioDevice: OTCustomAudioDriver = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue()
|
|
602
|
+
if !audioDevice.playing { return 0 }
|
|
603
|
+
|
|
604
|
+
let _ = audioDevice.deviceAudioBus!.readRenderData((ioData?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames)
|
|
605
|
+
updatePlayoutDelay(withAudioDevice: audioDevice)
|
|
606
|
+
|
|
607
|
+
return noErr
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
func recordCb(inRefCon:UnsafeMutableRawPointer,
|
|
611
|
+
ioActionFlags:UnsafeMutablePointer<AudioUnitRenderActionFlags>,
|
|
612
|
+
inTimeStamp:UnsafePointer<AudioTimeStamp>,
|
|
613
|
+
inBusNumber:UInt32,
|
|
614
|
+
inNumberFrames:UInt32,
|
|
615
|
+
ioData:UnsafeMutablePointer<AudioBufferList>?) -> OSStatus
|
|
616
|
+
{
|
|
617
|
+
let audioDevice: OTCustomAudioDriver = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue()
|
|
618
|
+
if audioDevice.bufferList == nil || inNumberFrames > audioDevice.bufferNumFrames {
|
|
619
|
+
if audioDevice.bufferList != nil {
|
|
620
|
+
audioDevice.bufferList!.pointee.mBuffers.mData?
|
|
621
|
+
.assumingMemoryBound(to: UInt16.self).deallocate()
|
|
622
|
+
audioDevice.bufferList?.deallocate()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
audioDevice.bufferList = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: 1)
|
|
626
|
+
audioDevice.bufferList?.pointee.mNumberBuffers = 1
|
|
627
|
+
audioDevice.bufferList?.pointee.mBuffers.mNumberChannels = 1
|
|
628
|
+
|
|
629
|
+
audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = inNumberFrames * UInt32(MemoryLayout<UInt16>.size)
|
|
630
|
+
audioDevice.bufferList?.pointee.mBuffers.mData = UnsafeMutableRawPointer(UnsafeMutablePointer<UInt16>.allocate(capacity: Int(inNumberFrames)))
|
|
631
|
+
audioDevice.bufferNumFrames = inNumberFrames
|
|
632
|
+
audioDevice.bufferSize = (audioDevice.bufferList?.pointee.mBuffers.mDataByteSize)!
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
AudioUnitRender(audioDevice.recordingVoiceUnit!,
|
|
636
|
+
ioActionFlags,
|
|
637
|
+
inTimeStamp,
|
|
638
|
+
1,
|
|
639
|
+
inNumberFrames,
|
|
640
|
+
audioDevice.bufferList!)
|
|
641
|
+
|
|
642
|
+
if audioDevice.recording {
|
|
643
|
+
audioDevice.deviceAudioBus!.writeCaptureData((audioDevice.bufferList?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if audioDevice.bufferSize != audioDevice.bufferList?.pointee.mBuffers.mDataByteSize {
|
|
647
|
+
audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = audioDevice.bufferSize
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
updateRecordingDelay(withAudioDevice: audioDevice)
|
|
651
|
+
|
|
652
|
+
return noErr
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
func updatePlayoutDelay(withAudioDevice audioDevice: OTCustomAudioDriver) {
|
|
656
|
+
audioDevice.playoutDelayMeasurementCounter += 1
|
|
657
|
+
if audioDevice.playoutDelayMeasurementCounter >= 100 {
|
|
658
|
+
// Update HW and OS delay every second, unlikely to change
|
|
659
|
+
audioDevice.playoutDelay = 0
|
|
660
|
+
let session = AVAudioSession.sharedInstance()
|
|
661
|
+
|
|
662
|
+
// HW output latency
|
|
663
|
+
let interval = session.outputLatency
|
|
664
|
+
audioDevice.playoutDelay += UInt32(interval * 1000000)
|
|
665
|
+
// HW buffer duration
|
|
666
|
+
let ioInterval = session.ioBufferDuration
|
|
667
|
+
audioDevice.playoutDelay += UInt32(ioInterval * 1000000)
|
|
668
|
+
audioDevice.playoutDelay += UInt32(audioDevice.playoutAudioUnitPropertyLatency * 1000000)
|
|
669
|
+
// To ms
|
|
670
|
+
audioDevice.playoutDelay = (audioDevice.playoutDelay - 500) / 1000
|
|
671
|
+
|
|
672
|
+
audioDevice.playoutDelayMeasurementCounter = 0
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
func updateRecordingDelay(withAudioDevice audioDevice: OTCustomAudioDriver) {
|
|
677
|
+
audioDevice.recordingDelayMeasurementCounter += 1
|
|
678
|
+
|
|
679
|
+
if audioDevice.recordingDelayMeasurementCounter >= 100 {
|
|
680
|
+
audioDevice.recordingDelayHWAndOS = 0
|
|
681
|
+
let session = AVAudioSession.sharedInstance()
|
|
682
|
+
let interval = session.inputLatency
|
|
683
|
+
|
|
684
|
+
audioDevice.recordingDelayHWAndOS += UInt32(interval * 1000000)
|
|
685
|
+
let ioInterval = session.ioBufferDuration
|
|
686
|
+
|
|
687
|
+
audioDevice.recordingDelayHWAndOS += UInt32(ioInterval * 1000000)
|
|
688
|
+
audioDevice.recordingDelayHWAndOS += UInt32(audioDevice.recordingAudioUnitPropertyLatency * 1000000)
|
|
689
|
+
|
|
690
|
+
audioDevice.recordingDelayHWAndOS = audioDevice.recordingDelayHWAndOS.advanced(by: -500) / 1000
|
|
691
|
+
|
|
692
|
+
audioDevice.recordingDelayMeasurementCounter = 0
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
audioDevice.recordingDelay = audioDevice.recordingDelayHWAndOS
|
|
696
|
+
}
|