@capgo/capacitor-screen-recorder 8.3.11 → 8.3.13
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/android/src/main/java/ee/forgr/plugin/screenrecorder/ScreenRecorderPlugin.java +1 -1
- package/ios/Sources/ScreenRecorderPlugin/ReplayKitAudioMixer.swift +862 -0
- package/ios/Sources/ScreenRecorderPlugin/ScreenRecorderPlugin.swift +1 -1
- package/ios/Sources/ScreenRecorderPlugin/Wyler.swift +30 -16
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import dev.bmcreations.scrcast.config.Options;
|
|
|
10
10
|
@CapacitorPlugin(name = "ScreenRecorder")
|
|
11
11
|
public class ScreenRecorderPlugin extends Plugin {
|
|
12
12
|
|
|
13
|
-
private final String pluginVersion = "8.3.
|
|
13
|
+
private final String pluginVersion = "8.3.13";
|
|
14
14
|
|
|
15
15
|
private CapgoScrCast videoRecorder;
|
|
16
16
|
private CapgoScrCast audioRecorder;
|
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CoreMedia
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
private struct QueuedMic {
|
|
6
|
+
let buffer: CMSampleBuffer
|
|
7
|
+
var consumedFrames: Int
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
final class ReplayKitAudioTrackMixer {
|
|
11
|
+
private var micQueue: [QueuedMic] = []
|
|
12
|
+
private var pendingAppQueue: [CMSampleBuffer] = []
|
|
13
|
+
private var referenceAppBuffer: CMSampleBuffer?
|
|
14
|
+
private let maxMicQueueSize = 128
|
|
15
|
+
private let maxPendingAppQueueSize = 128
|
|
16
|
+
|
|
17
|
+
// Satisfies SwiftLint required_deinit without custom teardown logic.
|
|
18
|
+
deinit {
|
|
19
|
+
// No teardown required; this deinit exists only for SwiftLint compliance.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func handleMic(_ sampleBuffer: CMSampleBuffer) -> [CMSampleBuffer] {
|
|
23
|
+
micQueue.append(QueuedMic(buffer: sampleBuffer, consumedFrames: 0))
|
|
24
|
+
var outputs: [CMSampleBuffer] = []
|
|
25
|
+
while micQueue.count > maxMicQueueSize {
|
|
26
|
+
let queuedMic = micQueue.removeFirst()
|
|
27
|
+
if let normalized = Self.micTailForWriter(queuedMic, reference: referenceAppBuffer) {
|
|
28
|
+
outputs.append(normalized)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
outputs.append(contentsOf: emitReadySamples())
|
|
32
|
+
return outputs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func handleApp(_ sampleBuffer: CMSampleBuffer) -> [CMSampleBuffer] {
|
|
36
|
+
if referenceAppBuffer == nil {
|
|
37
|
+
referenceAppBuffer = sampleBuffer
|
|
38
|
+
}
|
|
39
|
+
pendingAppQueue.append(sampleBuffer)
|
|
40
|
+
var outputs: [CMSampleBuffer] = []
|
|
41
|
+
while pendingAppQueue.count > maxPendingAppQueueSize {
|
|
42
|
+
_ = pendingAppQueue.removeFirst()
|
|
43
|
+
}
|
|
44
|
+
outputs.append(contentsOf: emitReadySamples())
|
|
45
|
+
return outputs
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func drain() -> [CMSampleBuffer] {
|
|
49
|
+
var outputs: [CMSampleBuffer] = []
|
|
50
|
+
while !micQueue.isEmpty || !pendingAppQueue.isEmpty {
|
|
51
|
+
let remainingCount = micQueue.count + pendingAppQueue.count
|
|
52
|
+
outputs.append(contentsOf: emitReadySamples())
|
|
53
|
+
if micQueue.count + pendingAppQueue.count == remainingCount {
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var remaining: [(CMTime, CMSampleBuffer)] = []
|
|
59
|
+
for queuedMic in micQueue {
|
|
60
|
+
if let normalized = Self.micTailForWriter(queuedMic, reference: referenceAppBuffer) {
|
|
61
|
+
remaining.append((Self.startTime(of: normalized), normalized))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for app in pendingAppQueue {
|
|
65
|
+
remaining.append((Self.startTime(of: app), app))
|
|
66
|
+
}
|
|
67
|
+
remaining.sort { CMTimeCompare($0.0, $1.0) < 0 }
|
|
68
|
+
outputs.append(contentsOf: remaining.map(\.1))
|
|
69
|
+
|
|
70
|
+
micQueue.removeAll()
|
|
71
|
+
pendingAppQueue.removeAll()
|
|
72
|
+
return outputs
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func emitReadySamples() -> [CMSampleBuffer] {
|
|
76
|
+
var outputs: [CMSampleBuffer] = []
|
|
77
|
+
|
|
78
|
+
while let queuedMic = micQueue.first {
|
|
79
|
+
let nextAppStart = pendingAppQueue.first.map { Self.startTime(of: $0) }
|
|
80
|
+
guard let appStart = nextAppStart else { break }
|
|
81
|
+
if CMTimeCompare(Self.endTime(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames), appStart) <= 0 {
|
|
82
|
+
micQueue.removeFirst()
|
|
83
|
+
if let normalized = Self.micTailForWriter(queuedMic, reference: referenceAppBuffer) {
|
|
84
|
+
outputs.append(normalized)
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while let app = pendingAppQueue.first,
|
|
92
|
+
let queuedMic = micQueue.first,
|
|
93
|
+
Self.rangesOverlap(queuedMic, app) {
|
|
94
|
+
if let leadingMic = Self.flushLeadingMicOnly(queuedMic: &micQueue[0], app: app, reference: referenceAppBuffer) {
|
|
95
|
+
outputs.append(leadingMic)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if let result = ReplayKitAudioMixer.mix(
|
|
99
|
+
app: app,
|
|
100
|
+
mic: queuedMic.buffer,
|
|
101
|
+
micFrameOffset: queuedMic.consumedFrames
|
|
102
|
+
) {
|
|
103
|
+
pendingAppQueue.removeFirst()
|
|
104
|
+
outputs.append(result.sample)
|
|
105
|
+
if result.micFramesConsumed >= CMSampleBufferGetNumSamples(queuedMic.buffer) {
|
|
106
|
+
micQueue.removeFirst()
|
|
107
|
+
} else {
|
|
108
|
+
micQueue[0].consumedFrames = result.micFramesConsumed
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
pendingAppQueue.removeFirst()
|
|
112
|
+
var fallbackSamples: [(CMTime, CMSampleBuffer)] = [
|
|
113
|
+
(Self.startTime(of: app), app)
|
|
114
|
+
]
|
|
115
|
+
if let micOverlap = Self.micOverlapSlice(
|
|
116
|
+
queuedMic: micQueue[0],
|
|
117
|
+
app: app,
|
|
118
|
+
reference: referenceAppBuffer
|
|
119
|
+
) {
|
|
120
|
+
fallbackSamples.append((Self.startTime(of: micOverlap), micOverlap))
|
|
121
|
+
}
|
|
122
|
+
fallbackSamples.sort { CMTimeCompare($0.0, $1.0) < 0 }
|
|
123
|
+
outputs.append(contentsOf: fallbackSamples.map(\.1))
|
|
124
|
+
Self.advanceMicPastOverlap(queuedMic: &micQueue[0], app: app)
|
|
125
|
+
if micQueue[0].consumedFrames >= CMSampleBufferGetNumSamples(micQueue[0].buffer) {
|
|
126
|
+
micQueue.removeFirst()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while let app = pendingAppQueue.first {
|
|
132
|
+
if let queuedMic = micQueue.first {
|
|
133
|
+
if CMTimeCompare(Self.startTime(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames),
|
|
134
|
+
Self.endTime(of: app)) > 0 {
|
|
135
|
+
pendingAppQueue.removeFirst()
|
|
136
|
+
outputs.append(app)
|
|
137
|
+
} else {
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
pendingAppQueue.removeFirst()
|
|
142
|
+
outputs.append(app)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return outputs
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static func startTime(of buffer: CMSampleBuffer, consumedFrames: Int = 0) -> CMTime {
|
|
150
|
+
guard consumedFrames > 0 else {
|
|
151
|
+
return CMSampleBufferGetPresentationTimeStamp(buffer)
|
|
152
|
+
}
|
|
153
|
+
let sampleRate = sampleRate(of: buffer)
|
|
154
|
+
let consumedDuration = CMTime(value: CMTimeValue(consumedFrames), timescale: CMTimeScale(sampleRate))
|
|
155
|
+
return CMTimeAdd(CMSampleBufferGetPresentationTimeStamp(buffer), consumedDuration)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private static func endTime(of buffer: CMSampleBuffer, consumedFrames: Int = 0) -> CMTime {
|
|
159
|
+
CMTimeRangeGetEnd(timeRange(of: buffer, consumedFrames: consumedFrames))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static func sampleRate(of buffer: CMSampleBuffer) -> Double {
|
|
163
|
+
guard let format = CMSampleBufferGetFormatDescription(buffer),
|
|
164
|
+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format)?.pointee else {
|
|
165
|
+
return 48_000
|
|
166
|
+
}
|
|
167
|
+
return asbd.mSampleRate
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static func timeRange(of buffer: CMSampleBuffer, consumedFrames: Int = 0) -> CMTimeRange {
|
|
171
|
+
let start = startTime(of: buffer, consumedFrames: consumedFrames)
|
|
172
|
+
let totalFrames = CMSampleBufferGetNumSamples(buffer)
|
|
173
|
+
let remainingFrames = max(0, totalFrames - consumedFrames)
|
|
174
|
+
let timescale = CMTimeScale(sampleRate(of: buffer))
|
|
175
|
+
return CMTimeRange(start: start, duration: CMTime(value: CMTimeValue(remainingFrames), timescale: timescale))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static func rangesOverlap(_ queuedMic: QueuedMic, _ app: CMSampleBuffer) -> Bool {
|
|
179
|
+
let intersection = CMTimeRangeGetIntersection(
|
|
180
|
+
timeRange(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames),
|
|
181
|
+
otherRange: timeRange(of: app)
|
|
182
|
+
)
|
|
183
|
+
return intersection.duration.isValid && intersection.duration > .zero
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private static func flushLeadingMicOnly(
|
|
187
|
+
queuedMic: inout QueuedMic,
|
|
188
|
+
app: CMSampleBuffer,
|
|
189
|
+
reference: CMSampleBuffer?
|
|
190
|
+
) -> CMSampleBuffer? {
|
|
191
|
+
let micRange = timeRange(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames)
|
|
192
|
+
let intersection = CMTimeRangeGetIntersection(micRange, otherRange: timeRange(of: app))
|
|
193
|
+
guard intersection.duration.isValid, intersection.duration > .zero else { return nil }
|
|
194
|
+
guard CMTimeCompare(micRange.start, intersection.start) < 0 else { return nil }
|
|
195
|
+
|
|
196
|
+
let sampleRate = sampleRate(of: queuedMic.buffer)
|
|
197
|
+
let bufferStart = CMSampleBufferGetPresentationTimeStamp(queuedMic.buffer)
|
|
198
|
+
let leadingEndFrame = frameOffset(
|
|
199
|
+
for: intersection.start,
|
|
200
|
+
relativeTo: bufferStart,
|
|
201
|
+
sampleRate: sampleRate
|
|
202
|
+
)
|
|
203
|
+
let leadingFrameCount = leadingEndFrame - queuedMic.consumedFrames
|
|
204
|
+
guard leadingFrameCount > 0 else { return nil }
|
|
205
|
+
|
|
206
|
+
guard let leadingSlice = ReplayKitAudioMixer.slice(
|
|
207
|
+
queuedMic.buffer,
|
|
208
|
+
fromFrame: queuedMic.consumedFrames,
|
|
209
|
+
frameCount: leadingFrameCount
|
|
210
|
+
) else {
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
queuedMic.consumedFrames = leadingEndFrame
|
|
215
|
+
return normalizeForWriter(leadingSlice, reference: reference)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private static func frameOffset(for time: CMTime, relativeTo start: CMTime, sampleRate: Double) -> Int {
|
|
219
|
+
let delta = CMTimeSubtract(time, start)
|
|
220
|
+
return max(0, Int((CMTimeGetSeconds(delta) * sampleRate).rounded()))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private static func advanceMicPastOverlap(queuedMic: inout QueuedMic, app: CMSampleBuffer) {
|
|
224
|
+
let intersection = CMTimeRangeGetIntersection(
|
|
225
|
+
timeRange(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames),
|
|
226
|
+
otherRange: timeRange(of: app)
|
|
227
|
+
)
|
|
228
|
+
guard intersection.duration.isValid, intersection.duration > .zero else { return }
|
|
229
|
+
|
|
230
|
+
let sampleRate = sampleRate(of: queuedMic.buffer)
|
|
231
|
+
let bufferStart = CMSampleBufferGetPresentationTimeStamp(queuedMic.buffer)
|
|
232
|
+
let endFrame = frameOffset(
|
|
233
|
+
for: CMTimeRangeGetEnd(intersection),
|
|
234
|
+
relativeTo: bufferStart,
|
|
235
|
+
sampleRate: sampleRate
|
|
236
|
+
)
|
|
237
|
+
queuedMic.consumedFrames = max(queuedMic.consumedFrames, endFrame)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static func micOverlapSlice(
|
|
241
|
+
queuedMic: QueuedMic,
|
|
242
|
+
app: CMSampleBuffer,
|
|
243
|
+
reference: CMSampleBuffer?
|
|
244
|
+
) -> CMSampleBuffer? {
|
|
245
|
+
let intersection = CMTimeRangeGetIntersection(
|
|
246
|
+
timeRange(of: queuedMic.buffer, consumedFrames: queuedMic.consumedFrames),
|
|
247
|
+
otherRange: timeRange(of: app)
|
|
248
|
+
)
|
|
249
|
+
guard intersection.duration.isValid, intersection.duration > .zero else { return nil }
|
|
250
|
+
|
|
251
|
+
let sampleRate = sampleRate(of: queuedMic.buffer)
|
|
252
|
+
let bufferStart = CMSampleBufferGetPresentationTimeStamp(queuedMic.buffer)
|
|
253
|
+
let startFrame = max(
|
|
254
|
+
queuedMic.consumedFrames,
|
|
255
|
+
frameOffset(for: intersection.start, relativeTo: bufferStart, sampleRate: sampleRate)
|
|
256
|
+
)
|
|
257
|
+
let endFrame = frameOffset(
|
|
258
|
+
for: CMTimeRangeGetEnd(intersection),
|
|
259
|
+
relativeTo: bufferStart,
|
|
260
|
+
sampleRate: sampleRate
|
|
261
|
+
)
|
|
262
|
+
let frameCount = endFrame - startFrame
|
|
263
|
+
guard frameCount > 0 else { return nil }
|
|
264
|
+
|
|
265
|
+
guard let overlap = ReplayKitAudioMixer.slice(
|
|
266
|
+
queuedMic.buffer,
|
|
267
|
+
fromFrame: startFrame,
|
|
268
|
+
frameCount: frameCount
|
|
269
|
+
) else {
|
|
270
|
+
return nil
|
|
271
|
+
}
|
|
272
|
+
return normalizeForWriter(overlap, reference: reference)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static func micTailForWriter(_ queuedMic: QueuedMic, reference: CMSampleBuffer?) -> CMSampleBuffer? {
|
|
276
|
+
guard let tail = ReplayKitAudioMixer.slice(queuedMic.buffer, fromFrame: queuedMic.consumedFrames) else {
|
|
277
|
+
return nil
|
|
278
|
+
}
|
|
279
|
+
return normalizeForWriter(tail, reference: reference)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private static func normalizeForWriter(_ buffer: CMSampleBuffer, reference: CMSampleBuffer?) -> CMSampleBuffer? {
|
|
283
|
+
guard let reference = reference else { return buffer }
|
|
284
|
+
return ReplayKitAudioMixer.matchFormat(buffer, to: reference) ?? buffer
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
enum ReplayKitAudioMixer {
|
|
289
|
+
private static let logger = Logger(subsystem: "CapgoScreenRecorder", category: "ReplayKitAudioMixer")
|
|
290
|
+
|
|
291
|
+
struct MixResult {
|
|
292
|
+
let sample: CMSampleBuffer
|
|
293
|
+
let micFramesConsumed: Int
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static func mix(app: CMSampleBuffer, mic: CMSampleBuffer, micFrameOffset: Int = 0) -> MixResult? {
|
|
297
|
+
guard CMSampleBufferDataIsReady(app), CMSampleBufferDataIsReady(mic) else { return nil }
|
|
298
|
+
|
|
299
|
+
guard let appFormat = CMSampleBufferGetFormatDescription(app),
|
|
300
|
+
let micFormat = CMSampleBufferGetFormatDescription(mic),
|
|
301
|
+
let appAsbd = CMAudioFormatDescriptionGetStreamBasicDescription(appFormat)?.pointee,
|
|
302
|
+
let micAsbd = CMAudioFormatDescriptionGetStreamBasicDescription(micFormat)?.pointee,
|
|
303
|
+
appAsbd.mFormatID == kAudioFormatLinearPCM,
|
|
304
|
+
micAsbd.mFormatID == kAudioFormatLinearPCM,
|
|
305
|
+
(appAsbd.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0,
|
|
306
|
+
(micAsbd.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0,
|
|
307
|
+
(appAsbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
308
|
+
(micAsbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
309
|
+
appAsbd.mBitsPerChannel == 16,
|
|
310
|
+
micAsbd.mBitsPerChannel == 16 else {
|
|
311
|
+
return nil
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
var micBuffer = mic
|
|
315
|
+
var effectiveMicOffset = micFrameOffset
|
|
316
|
+
if micAsbd.mSampleRate != appAsbd.mSampleRate {
|
|
317
|
+
guard let resampled = resampleMicBuffer(mic, toAppSampleRate: appAsbd.mSampleRate) else {
|
|
318
|
+
logger.error("Failed to resample microphone audio from \(micAsbd.mSampleRate) Hz to \(appAsbd.mSampleRate) Hz")
|
|
319
|
+
return nil
|
|
320
|
+
}
|
|
321
|
+
micBuffer = resampled
|
|
322
|
+
effectiveMicOffset = Int(
|
|
323
|
+
(Double(micFrameOffset) * appAsbd.mSampleRate / micAsbd.mSampleRate).rounded()
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
guard let micFormat = CMSampleBufferGetFormatDescription(micBuffer),
|
|
328
|
+
let resampledMicAsbd = CMAudioFormatDescriptionGetStreamBasicDescription(micFormat)?.pointee else {
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let appRange = timeRange(of: app)
|
|
333
|
+
let micRange = timeRange(of: micBuffer)
|
|
334
|
+
let intersection = CMTimeRangeGetIntersection(appRange, otherRange: micRange)
|
|
335
|
+
guard intersection.duration.isValid, intersection.duration > .zero else { return nil }
|
|
336
|
+
|
|
337
|
+
let sampleRate = appAsbd.mSampleRate
|
|
338
|
+
let micStartFrame = max(
|
|
339
|
+
effectiveMicOffset,
|
|
340
|
+
frameOffset(for: intersection.start, relativeTo: micRange.start, sampleRate: sampleRate)
|
|
341
|
+
)
|
|
342
|
+
let appStartFrame = frameOffset(for: intersection.start, relativeTo: appRange.start, sampleRate: sampleRate)
|
|
343
|
+
let mixFrames = frameCount(for: intersection.duration, sampleRate: sampleRate)
|
|
344
|
+
guard mixFrames > 0 else { return nil }
|
|
345
|
+
|
|
346
|
+
let appFrames = CMSampleBufferGetNumSamples(app)
|
|
347
|
+
let micFrames = CMSampleBufferGetNumSamples(micBuffer)
|
|
348
|
+
let appAvailable = appFrames - appStartFrame
|
|
349
|
+
let micAvailable = micFrames - micStartFrame
|
|
350
|
+
let frames = min(mixFrames, appAvailable, micAvailable)
|
|
351
|
+
guard frames > 0 else { return nil }
|
|
352
|
+
|
|
353
|
+
guard let appBlock = CMSampleBufferGetDataBuffer(app),
|
|
354
|
+
let micBlock = CMSampleBufferGetDataBuffer(micBuffer) else {
|
|
355
|
+
return nil
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
guard let output = copyPCMBuffer(app),
|
|
359
|
+
let outputBlock = CMSampleBufferGetDataBuffer(output) else {
|
|
360
|
+
return nil
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let appChannels = Int(appAsbd.mChannelsPerFrame)
|
|
364
|
+
let micChannels = Int(resampledMicAsbd.mChannelsPerFrame)
|
|
365
|
+
let appBytesPerFrame = Int(appAsbd.mBytesPerFrame)
|
|
366
|
+
let micBytesPerFrame = Int(resampledMicAsbd.mBytesPerFrame)
|
|
367
|
+
let outputBytes = frames * appBytesPerFrame
|
|
368
|
+
let appByteOffset = appStartFrame * appBytesPerFrame
|
|
369
|
+
let micByteOffset = micStartFrame * micBytesPerFrame
|
|
370
|
+
let micBytes = frames * micBytesPerFrame
|
|
371
|
+
|
|
372
|
+
var appSamples = [Int16](repeating: 0, count: outputBytes / MemoryLayout<Int16>.size)
|
|
373
|
+
var micSamples = [Int16](repeating: 0, count: micBytes / MemoryLayout<Int16>.size)
|
|
374
|
+
|
|
375
|
+
let appCopyStatus = appSamples.withUnsafeMutableBytes { destination in
|
|
376
|
+
CMBlockBufferCopyDataBytes(
|
|
377
|
+
appBlock,
|
|
378
|
+
atOffset: appByteOffset,
|
|
379
|
+
dataLength: outputBytes,
|
|
380
|
+
destination: destination.baseAddress!
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
let micCopyStatus = micSamples.withUnsafeMutableBytes { destination in
|
|
384
|
+
CMBlockBufferCopyDataBytes(
|
|
385
|
+
micBlock,
|
|
386
|
+
atOffset: micByteOffset,
|
|
387
|
+
dataLength: micBytes,
|
|
388
|
+
destination: destination.baseAddress!
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
guard appCopyStatus == noErr, micCopyStatus == noErr else { return nil }
|
|
392
|
+
|
|
393
|
+
for frame in 0..<frames {
|
|
394
|
+
var micAccumulator: Int32 = 0
|
|
395
|
+
for micChannel in 0..<micChannels {
|
|
396
|
+
micAccumulator += Int32(micSamples[frame * micChannels + micChannel])
|
|
397
|
+
}
|
|
398
|
+
let micSample = micAccumulator / Int32(micChannels)
|
|
399
|
+
for channel in 0..<appChannels {
|
|
400
|
+
let index = frame * appChannels + channel
|
|
401
|
+
let mixed = max(Int32(Int16.min), min(Int32(Int16.max), Int32(appSamples[index]) + micSample))
|
|
402
|
+
appSamples[index] = Int16(mixed)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let replaceStatus = appSamples.withUnsafeMutableBytes { source in
|
|
407
|
+
CMBlockBufferReplaceDataBytes(
|
|
408
|
+
with: source.baseAddress!,
|
|
409
|
+
blockBuffer: outputBlock,
|
|
410
|
+
offsetIntoDestination: appByteOffset,
|
|
411
|
+
dataLength: outputBytes
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
guard replaceStatus == noErr else {
|
|
415
|
+
logger.error("Failed to write mixed PCM into output block buffer")
|
|
416
|
+
return nil
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let intersectionEnd = CMTimeRangeGetEnd(intersection)
|
|
420
|
+
let originalMicStart = CMSampleBufferGetPresentationTimeStamp(mic)
|
|
421
|
+
let consumedInOriginalMic = max(
|
|
422
|
+
micFrameOffset,
|
|
423
|
+
frameOffset(for: intersectionEnd, relativeTo: originalMicStart, sampleRate: micAsbd.mSampleRate)
|
|
424
|
+
)
|
|
425
|
+
return MixResult(sample: output, micFramesConsumed: consumedInOriginalMic)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private static func resampleMicBuffer(_ mic: CMSampleBuffer, toAppSampleRate targetRate: Double) -> CMSampleBuffer? {
|
|
429
|
+
guard CMSampleBufferDataIsReady(mic),
|
|
430
|
+
let formatDesc = CMSampleBufferGetFormatDescription(mic),
|
|
431
|
+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc)?.pointee,
|
|
432
|
+
asbd.mFormatID == kAudioFormatLinearPCM,
|
|
433
|
+
(asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
434
|
+
asbd.mBitsPerChannel == 16,
|
|
435
|
+
asbd.mSampleRate != targetRate,
|
|
436
|
+
let block = CMSampleBufferGetDataBuffer(mic) else {
|
|
437
|
+
return nil
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let channels = AVAudioChannelCount(asbd.mChannelsPerFrame)
|
|
441
|
+
guard let inputFormat = AVAudioFormat(
|
|
442
|
+
commonFormat: .pcmFormatInt16,
|
|
443
|
+
sampleRate: asbd.mSampleRate,
|
|
444
|
+
channels: channels,
|
|
445
|
+
interleaved: true
|
|
446
|
+
), let outputFormat = AVAudioFormat(
|
|
447
|
+
commonFormat: .pcmFormatInt16,
|
|
448
|
+
sampleRate: targetRate,
|
|
449
|
+
channels: channels,
|
|
450
|
+
interleaved: true
|
|
451
|
+
), let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
|
|
452
|
+
return nil
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let inputFrames = CMSampleBufferGetNumSamples(mic)
|
|
456
|
+
let inputBytes = inputFrames * Int(asbd.mBytesPerFrame)
|
|
457
|
+
var rawBytes = [UInt8](repeating: 0, count: inputBytes)
|
|
458
|
+
let copyStatus = rawBytes.withUnsafeMutableBytes { destination in
|
|
459
|
+
CMBlockBufferCopyDataBytes(
|
|
460
|
+
block,
|
|
461
|
+
atOffset: 0,
|
|
462
|
+
dataLength: inputBytes,
|
|
463
|
+
destination: destination.baseAddress!
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
guard copyStatus == noErr else { return nil }
|
|
467
|
+
|
|
468
|
+
guard let inputPCM = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: AVAudioFrameCount(inputFrames)),
|
|
469
|
+
let inputChannelData = inputPCM.int16ChannelData else {
|
|
470
|
+
return nil
|
|
471
|
+
}
|
|
472
|
+
inputPCM.frameLength = AVAudioFrameCount(inputFrames)
|
|
473
|
+
rawBytes.withUnsafeBytes { source in
|
|
474
|
+
let samples = source.bindMemory(to: Int16.self)
|
|
475
|
+
for index in 0..<inputFrames {
|
|
476
|
+
for channel in 0..<Int(channels) {
|
|
477
|
+
inputChannelData[0][index * Int(channels) + channel] = samples[index * Int(channels) + channel]
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let outputFrameCapacity = AVAudioFrameCount(
|
|
483
|
+
(Double(inputFrames) * targetRate / asbd.mSampleRate).rounded(.up)
|
|
484
|
+
)
|
|
485
|
+
guard let outputPCM = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: outputFrameCapacity),
|
|
486
|
+
let outputChannelData = outputPCM.int16ChannelData else {
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
var conversionError: NSError?
|
|
491
|
+
var suppliedInput = false
|
|
492
|
+
let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
|
|
493
|
+
if suppliedInput {
|
|
494
|
+
outStatus.pointee = .endOfStream
|
|
495
|
+
return nil
|
|
496
|
+
}
|
|
497
|
+
suppliedInput = true
|
|
498
|
+
outStatus.pointee = .haveData
|
|
499
|
+
return inputPCM
|
|
500
|
+
}
|
|
501
|
+
converter.convert(to: outputPCM, error: &conversionError, withInputFrom: inputBlock)
|
|
502
|
+
guard conversionError == nil, outputPCM.frameLength > 0 else { return nil }
|
|
503
|
+
|
|
504
|
+
let outputFrames = Int(outputPCM.frameLength)
|
|
505
|
+
let outputBytesPerFrame = Int(outputFormat.streamDescription.pointee.mBytesPerFrame)
|
|
506
|
+
let outputBytes = outputFrames * outputBytesPerFrame
|
|
507
|
+
var outputBytesArray = [UInt8](repeating: 0, count: outputBytes)
|
|
508
|
+
outputBytesArray.withUnsafeMutableBytes { destination in
|
|
509
|
+
let samples = destination.bindMemory(to: Int16.self)
|
|
510
|
+
for index in 0..<outputFrames {
|
|
511
|
+
for channel in 0..<Int(channels) {
|
|
512
|
+
samples[index * Int(channels) + channel] = outputChannelData[0][index * Int(channels) + channel]
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
var outputBlockBuffer: CMBlockBuffer?
|
|
518
|
+
let blockStatus = CMBlockBufferCreateWithMemoryBlock(
|
|
519
|
+
allocator: kCFAllocatorDefault,
|
|
520
|
+
memoryBlock: nil,
|
|
521
|
+
blockLength: outputBytes,
|
|
522
|
+
blockAllocator: kCFAllocatorDefault,
|
|
523
|
+
customBlockSource: nil,
|
|
524
|
+
offsetToData: 0,
|
|
525
|
+
dataLength: outputBytes,
|
|
526
|
+
flags: 0,
|
|
527
|
+
blockBufferOut: &outputBlockBuffer
|
|
528
|
+
)
|
|
529
|
+
guard blockStatus == noErr, let outputBlock = outputBlockBuffer else { return nil }
|
|
530
|
+
|
|
531
|
+
let replaceStatus = outputBytesArray.withUnsafeMutableBytes { source in
|
|
532
|
+
CMBlockBufferReplaceDataBytes(
|
|
533
|
+
with: source.baseAddress!,
|
|
534
|
+
blockBuffer: outputBlock,
|
|
535
|
+
offsetIntoDestination: 0,
|
|
536
|
+
dataLength: outputBytes
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
guard replaceStatus == noErr else { return nil }
|
|
540
|
+
|
|
541
|
+
var outputAsbd = asbd
|
|
542
|
+
outputAsbd.mSampleRate = targetRate
|
|
543
|
+
var outputFormatDescription: CMAudioFormatDescription?
|
|
544
|
+
let formatStatus = CMAudioFormatDescriptionCreate(
|
|
545
|
+
allocator: kCFAllocatorDefault,
|
|
546
|
+
asbd: &outputAsbd,
|
|
547
|
+
layoutSize: 0,
|
|
548
|
+
layout: nil,
|
|
549
|
+
magicCookieSize: 0,
|
|
550
|
+
magicCookie: nil,
|
|
551
|
+
extensions: nil,
|
|
552
|
+
formatDescriptionOut: &outputFormatDescription
|
|
553
|
+
)
|
|
554
|
+
guard formatStatus == noErr, let resampledFormat = outputFormatDescription else { return nil }
|
|
555
|
+
|
|
556
|
+
let timescale = CMTimeScale(targetRate)
|
|
557
|
+
var timing = CMSampleTimingInfo(
|
|
558
|
+
duration: CMTime(value: 1, timescale: timescale),
|
|
559
|
+
presentationTimeStamp: CMSampleBufferGetPresentationTimeStamp(mic),
|
|
560
|
+
decodeTimeStamp: CMSampleBufferGetDecodeTimeStamp(mic)
|
|
561
|
+
)
|
|
562
|
+
var outputBuffer: CMSampleBuffer?
|
|
563
|
+
let createStatus = CMSampleBufferCreate(
|
|
564
|
+
allocator: kCFAllocatorDefault,
|
|
565
|
+
dataBuffer: outputBlock,
|
|
566
|
+
dataReady: true,
|
|
567
|
+
makeDataReadyCallback: nil,
|
|
568
|
+
refcon: nil,
|
|
569
|
+
formatDescription: resampledFormat,
|
|
570
|
+
sampleCount: outputFrames,
|
|
571
|
+
sampleTimingEntryCount: 1,
|
|
572
|
+
sampleTimingArray: &timing,
|
|
573
|
+
sampleSizeEntryCount: 0,
|
|
574
|
+
sampleSizeArray: nil,
|
|
575
|
+
sampleBufferOut: &outputBuffer
|
|
576
|
+
)
|
|
577
|
+
guard createStatus == noErr else { return nil }
|
|
578
|
+
return outputBuffer
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
static func slice(_ buffer: CMSampleBuffer, fromFrame frameOffset: Int, frameCount: Int? = nil) -> CMSampleBuffer? {
|
|
582
|
+
guard CMSampleBufferDataIsReady(buffer) else { return nil }
|
|
583
|
+
|
|
584
|
+
let totalFrames = CMSampleBufferGetNumSamples(buffer)
|
|
585
|
+
guard frameOffset < totalFrames else { return nil }
|
|
586
|
+
if frameOffset <= 0, frameCount == nil {
|
|
587
|
+
return buffer
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let framesToCopy = min(frameCount ?? (totalFrames - frameOffset), totalFrames - frameOffset)
|
|
591
|
+
guard framesToCopy > 0 else { return nil }
|
|
592
|
+
|
|
593
|
+
guard let format = CMSampleBufferGetFormatDescription(buffer),
|
|
594
|
+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format)?.pointee,
|
|
595
|
+
asbd.mFormatID == kAudioFormatLinearPCM,
|
|
596
|
+
(asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
597
|
+
let sourceBlock = CMSampleBufferGetDataBuffer(buffer) else {
|
|
598
|
+
return nil
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let bytesPerFrame = Int(asbd.mBytesPerFrame)
|
|
602
|
+
let byteOffset = frameOffset * bytesPerFrame
|
|
603
|
+
let remainingBytes = framesToCopy * bytesPerFrame
|
|
604
|
+
let timescale = CMTimeScale(asbd.mSampleRate)
|
|
605
|
+
|
|
606
|
+
var tailBytes = [UInt8](repeating: 0, count: remainingBytes)
|
|
607
|
+
let copyStatus = tailBytes.withUnsafeMutableBytes { destination in
|
|
608
|
+
CMBlockBufferCopyDataBytes(
|
|
609
|
+
sourceBlock,
|
|
610
|
+
atOffset: byteOffset,
|
|
611
|
+
dataLength: remainingBytes,
|
|
612
|
+
destination: destination.baseAddress!
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
guard copyStatus == noErr else { return nil }
|
|
616
|
+
|
|
617
|
+
var blockBuffer: CMBlockBuffer?
|
|
618
|
+
let blockStatus = CMBlockBufferCreateWithMemoryBlock(
|
|
619
|
+
allocator: kCFAllocatorDefault,
|
|
620
|
+
memoryBlock: nil,
|
|
621
|
+
blockLength: remainingBytes,
|
|
622
|
+
blockAllocator: kCFAllocatorDefault,
|
|
623
|
+
customBlockSource: nil,
|
|
624
|
+
offsetToData: 0,
|
|
625
|
+
dataLength: remainingBytes,
|
|
626
|
+
flags: 0,
|
|
627
|
+
blockBufferOut: &blockBuffer
|
|
628
|
+
)
|
|
629
|
+
guard blockStatus == noErr, let outputBlock = blockBuffer else { return nil }
|
|
630
|
+
|
|
631
|
+
let replaceStatus = tailBytes.withUnsafeMutableBytes { source in
|
|
632
|
+
CMBlockBufferReplaceDataBytes(
|
|
633
|
+
with: source.baseAddress!,
|
|
634
|
+
blockBuffer: outputBlock,
|
|
635
|
+
offsetIntoDestination: 0,
|
|
636
|
+
dataLength: remainingBytes
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
guard replaceStatus == noErr else { return nil }
|
|
640
|
+
|
|
641
|
+
let presentationTimeStamp = CMTimeAdd(
|
|
642
|
+
CMSampleBufferGetPresentationTimeStamp(buffer),
|
|
643
|
+
CMTime(value: CMTimeValue(frameOffset), timescale: timescale)
|
|
644
|
+
)
|
|
645
|
+
var timing = CMSampleTimingInfo(
|
|
646
|
+
duration: CMTime(value: 1, timescale: timescale),
|
|
647
|
+
presentationTimeStamp: presentationTimeStamp,
|
|
648
|
+
decodeTimeStamp: CMSampleBufferGetDecodeTimeStamp(buffer)
|
|
649
|
+
)
|
|
650
|
+
var outputBuffer: CMSampleBuffer?
|
|
651
|
+
let createStatus = CMSampleBufferCreate(
|
|
652
|
+
allocator: kCFAllocatorDefault,
|
|
653
|
+
dataBuffer: outputBlock,
|
|
654
|
+
dataReady: true,
|
|
655
|
+
makeDataReadyCallback: nil,
|
|
656
|
+
refcon: nil,
|
|
657
|
+
formatDescription: format,
|
|
658
|
+
sampleCount: framesToCopy,
|
|
659
|
+
sampleTimingEntryCount: 1,
|
|
660
|
+
sampleTimingArray: &timing,
|
|
661
|
+
sampleSizeEntryCount: 0,
|
|
662
|
+
sampleSizeArray: nil,
|
|
663
|
+
sampleBufferOut: &outputBuffer
|
|
664
|
+
)
|
|
665
|
+
guard createStatus == noErr else { return nil }
|
|
666
|
+
return outputBuffer
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
static func matchFormat(_ buffer: CMSampleBuffer, to reference: CMSampleBuffer) -> CMSampleBuffer? {
|
|
670
|
+
guard CMSampleBufferDataIsReady(buffer), CMSampleBufferDataIsReady(reference) else { return nil }
|
|
671
|
+
guard let bufferFormat = CMSampleBufferGetFormatDescription(buffer),
|
|
672
|
+
let referenceFormat = CMSampleBufferGetFormatDescription(reference),
|
|
673
|
+
let bufferAsbd = CMAudioFormatDescriptionGetStreamBasicDescription(bufferFormat)?.pointee,
|
|
674
|
+
let referenceAsbd = CMAudioFormatDescriptionGetStreamBasicDescription(referenceFormat)?.pointee,
|
|
675
|
+
bufferAsbd.mFormatID == kAudioFormatLinearPCM,
|
|
676
|
+
referenceAsbd.mFormatID == kAudioFormatLinearPCM,
|
|
677
|
+
(bufferAsbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
678
|
+
(referenceAsbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
679
|
+
bufferAsbd.mBitsPerChannel == 16,
|
|
680
|
+
referenceAsbd.mBitsPerChannel == 16,
|
|
681
|
+
bufferAsbd.mSampleRate == referenceAsbd.mSampleRate else {
|
|
682
|
+
return nil
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let bufferChannels = Int(bufferAsbd.mChannelsPerFrame)
|
|
686
|
+
let referenceChannels = Int(referenceAsbd.mChannelsPerFrame)
|
|
687
|
+
if bufferChannels == referenceChannels {
|
|
688
|
+
return buffer
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
guard bufferChannels == 1, referenceChannels > 1,
|
|
692
|
+
let block = CMSampleBufferGetDataBuffer(buffer) else {
|
|
693
|
+
return nil
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let frames = CMSampleBufferGetNumSamples(buffer)
|
|
697
|
+
let inputBytes = frames * Int(bufferAsbd.mBytesPerFrame)
|
|
698
|
+
let outputBytes = frames * referenceChannels * MemoryLayout<Int16>.size
|
|
699
|
+
var monoSamples = [Int16](repeating: 0, count: inputBytes / MemoryLayout<Int16>.size)
|
|
700
|
+
let copyStatus = monoSamples.withUnsafeMutableBytes { destination in
|
|
701
|
+
CMBlockBufferCopyDataBytes(block, atOffset: 0, dataLength: inputBytes, destination: destination.baseAddress!)
|
|
702
|
+
}
|
|
703
|
+
guard copyStatus == noErr else { return nil }
|
|
704
|
+
|
|
705
|
+
var stereoSamples = [Int16]()
|
|
706
|
+
stereoSamples.reserveCapacity(frames * referenceChannels)
|
|
707
|
+
for frame in 0..<frames {
|
|
708
|
+
let sample = monoSamples[frame]
|
|
709
|
+
for _ in 0..<referenceChannels {
|
|
710
|
+
stereoSamples.append(sample)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
var blockBuffer: CMBlockBuffer?
|
|
715
|
+
let blockStatus = CMBlockBufferCreateWithMemoryBlock(
|
|
716
|
+
allocator: kCFAllocatorDefault,
|
|
717
|
+
memoryBlock: nil,
|
|
718
|
+
blockLength: outputBytes,
|
|
719
|
+
blockAllocator: kCFAllocatorDefault,
|
|
720
|
+
customBlockSource: nil,
|
|
721
|
+
offsetToData: 0,
|
|
722
|
+
dataLength: outputBytes,
|
|
723
|
+
flags: 0,
|
|
724
|
+
blockBufferOut: &blockBuffer
|
|
725
|
+
)
|
|
726
|
+
guard blockStatus == noErr, let outputBlock = blockBuffer else { return nil }
|
|
727
|
+
|
|
728
|
+
let replaceStatus = stereoSamples.withUnsafeMutableBytes { source in
|
|
729
|
+
CMBlockBufferReplaceDataBytes(
|
|
730
|
+
with: source.baseAddress!,
|
|
731
|
+
blockBuffer: outputBlock,
|
|
732
|
+
offsetIntoDestination: 0,
|
|
733
|
+
dataLength: outputBytes
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
guard replaceStatus == noErr else { return nil }
|
|
737
|
+
|
|
738
|
+
let referenceTimescale = CMTimeScale(referenceAsbd.mSampleRate)
|
|
739
|
+
var timing = CMSampleTimingInfo(
|
|
740
|
+
duration: CMTime(value: 1, timescale: referenceTimescale),
|
|
741
|
+
presentationTimeStamp: CMSampleBufferGetPresentationTimeStamp(buffer),
|
|
742
|
+
decodeTimeStamp: CMSampleBufferGetDecodeTimeStamp(buffer)
|
|
743
|
+
)
|
|
744
|
+
var outputBuffer: CMSampleBuffer?
|
|
745
|
+
let createStatus = CMSampleBufferCreate(
|
|
746
|
+
allocator: kCFAllocatorDefault,
|
|
747
|
+
dataBuffer: outputBlock,
|
|
748
|
+
dataReady: true,
|
|
749
|
+
makeDataReadyCallback: nil,
|
|
750
|
+
refcon: nil,
|
|
751
|
+
formatDescription: referenceFormat,
|
|
752
|
+
sampleCount: frames,
|
|
753
|
+
sampleTimingEntryCount: 1,
|
|
754
|
+
sampleTimingArray: &timing,
|
|
755
|
+
sampleSizeEntryCount: 0,
|
|
756
|
+
sampleSizeArray: nil,
|
|
757
|
+
sampleBufferOut: &outputBuffer
|
|
758
|
+
)
|
|
759
|
+
guard createStatus == noErr else { return nil }
|
|
760
|
+
return outputBuffer
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private static func copyPCMBuffer(_ buffer: CMSampleBuffer) -> CMSampleBuffer? {
|
|
764
|
+
guard CMSampleBufferDataIsReady(buffer) else { return nil }
|
|
765
|
+
guard let format = CMSampleBufferGetFormatDescription(buffer),
|
|
766
|
+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format)?.pointee,
|
|
767
|
+
asbd.mFormatID == kAudioFormatLinearPCM,
|
|
768
|
+
(asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0,
|
|
769
|
+
let sourceBlock = CMSampleBufferGetDataBuffer(buffer) else {
|
|
770
|
+
return nil
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let frames = CMSampleBufferGetNumSamples(buffer)
|
|
774
|
+
let totalBytes = frames * Int(asbd.mBytesPerFrame)
|
|
775
|
+
guard totalBytes > 0 else { return nil }
|
|
776
|
+
|
|
777
|
+
var bytes = [UInt8](repeating: 0, count: totalBytes)
|
|
778
|
+
let copyStatus = bytes.withUnsafeMutableBytes { destination in
|
|
779
|
+
CMBlockBufferCopyDataBytes(
|
|
780
|
+
sourceBlock,
|
|
781
|
+
atOffset: 0,
|
|
782
|
+
dataLength: totalBytes,
|
|
783
|
+
destination: destination.baseAddress!
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
guard copyStatus == noErr else { return nil }
|
|
787
|
+
|
|
788
|
+
var blockBuffer: CMBlockBuffer?
|
|
789
|
+
let blockStatus = CMBlockBufferCreateWithMemoryBlock(
|
|
790
|
+
allocator: kCFAllocatorDefault,
|
|
791
|
+
memoryBlock: nil,
|
|
792
|
+
blockLength: totalBytes,
|
|
793
|
+
blockAllocator: kCFAllocatorDefault,
|
|
794
|
+
customBlockSource: nil,
|
|
795
|
+
offsetToData: 0,
|
|
796
|
+
dataLength: totalBytes,
|
|
797
|
+
flags: 0,
|
|
798
|
+
blockBufferOut: &blockBuffer
|
|
799
|
+
)
|
|
800
|
+
guard blockStatus == noErr, let outputBlock = blockBuffer else { return nil }
|
|
801
|
+
|
|
802
|
+
let replaceStatus = bytes.withUnsafeMutableBytes { source in
|
|
803
|
+
CMBlockBufferReplaceDataBytes(
|
|
804
|
+
with: source.baseAddress!,
|
|
805
|
+
blockBuffer: outputBlock,
|
|
806
|
+
offsetIntoDestination: 0,
|
|
807
|
+
dataLength: totalBytes
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
guard replaceStatus == noErr else { return nil }
|
|
811
|
+
|
|
812
|
+
let timescale = CMTimeScale(asbd.mSampleRate)
|
|
813
|
+
var timing = CMSampleTimingInfo(
|
|
814
|
+
duration: CMTime(value: 1, timescale: timescale),
|
|
815
|
+
presentationTimeStamp: CMSampleBufferGetPresentationTimeStamp(buffer),
|
|
816
|
+
decodeTimeStamp: CMSampleBufferGetDecodeTimeStamp(buffer)
|
|
817
|
+
)
|
|
818
|
+
var outputBuffer: CMSampleBuffer?
|
|
819
|
+
let createStatus = CMSampleBufferCreate(
|
|
820
|
+
allocator: kCFAllocatorDefault,
|
|
821
|
+
dataBuffer: outputBlock,
|
|
822
|
+
dataReady: true,
|
|
823
|
+
makeDataReadyCallback: nil,
|
|
824
|
+
refcon: nil,
|
|
825
|
+
formatDescription: format,
|
|
826
|
+
sampleCount: frames,
|
|
827
|
+
sampleTimingEntryCount: 1,
|
|
828
|
+
sampleTimingArray: &timing,
|
|
829
|
+
sampleSizeEntryCount: 0,
|
|
830
|
+
sampleSizeArray: nil,
|
|
831
|
+
sampleBufferOut: &outputBuffer
|
|
832
|
+
)
|
|
833
|
+
guard createStatus == noErr else { return nil }
|
|
834
|
+
return outputBuffer
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private static func timeRange(of buffer: CMSampleBuffer) -> CMTimeRange {
|
|
838
|
+
let start = CMSampleBufferGetPresentationTimeStamp(buffer)
|
|
839
|
+
let duration = CMSampleBufferGetDuration(buffer)
|
|
840
|
+
if duration.isValid, duration > .zero {
|
|
841
|
+
return CMTimeRange(start: start, duration: duration)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
guard let format = CMSampleBufferGetFormatDescription(buffer),
|
|
845
|
+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format)?.pointee else {
|
|
846
|
+
return CMTimeRange(start: start, duration: .zero)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
let frames = CMSampleBufferGetNumSamples(buffer)
|
|
850
|
+
let timescale = CMTimeScale(asbd.mSampleRate)
|
|
851
|
+
return CMTimeRange(start: start, duration: CMTime(value: CMTimeValue(frames), timescale: timescale))
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private static func frameOffset(for time: CMTime, relativeTo start: CMTime, sampleRate: Double) -> Int {
|
|
855
|
+
let delta = CMTimeSubtract(time, start)
|
|
856
|
+
return max(0, Int((CMTimeGetSeconds(delta) * sampleRate).rounded()))
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private static func frameCount(for duration: CMTime, sampleRate: Double) -> Int {
|
|
860
|
+
max(0, Int((CMTimeGetSeconds(duration) * sampleRate).rounded()))
|
|
861
|
+
}
|
|
862
|
+
}
|
|
@@ -7,7 +7,7 @@ import Capacitor
|
|
|
7
7
|
*/
|
|
8
8
|
@objc(ScreenRecorderPlugin)
|
|
9
9
|
public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
10
|
-
private let pluginVersion: String = "8.3.
|
|
10
|
+
private let pluginVersion: String = "8.3.13"
|
|
11
11
|
public let identifier = "ScreenRecorderPlugin"
|
|
12
12
|
public let jsName = "ScreenRecorder"
|
|
13
13
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -55,8 +55,9 @@ public final class ScreenRecorder {
|
|
|
55
55
|
private var didCreateOutputFile = false
|
|
56
56
|
private var videoWriter: AVAssetWriter?
|
|
57
57
|
private var videoWriterInput: AVAssetWriterInput?
|
|
58
|
-
private var
|
|
59
|
-
private var
|
|
58
|
+
private var audioWriterInput: AVAssetWriterInput?
|
|
59
|
+
private var audioTrackMixer: ReplayKitAudioTrackMixer?
|
|
60
|
+
private let audioMixerQueue = DispatchQueue(label: "CapgoScreenRecorder.audioMixer")
|
|
60
61
|
private var saveToCameraRoll = false
|
|
61
62
|
private var recordAudio = false
|
|
62
63
|
private var videoFormat: VideoContainerFormat = .mp4
|
|
@@ -93,8 +94,8 @@ public final class ScreenRecorder {
|
|
|
93
94
|
attemptDidCreate = self.didCreateOutputFile
|
|
94
95
|
addVideoWriterInput(size: size)
|
|
95
96
|
if recordAudio {
|
|
96
|
-
self.
|
|
97
|
-
self.
|
|
97
|
+
self.audioWriterInput = createAndAddAudioInput()
|
|
98
|
+
self.audioTrackMixer = ReplayKitAudioTrackMixer()
|
|
98
99
|
}
|
|
99
100
|
startCapture(handler: handler)
|
|
100
101
|
} catch let err {
|
|
@@ -111,8 +112,8 @@ public final class ScreenRecorder {
|
|
|
111
112
|
private func resetWriterState() {
|
|
112
113
|
videoWriter = nil
|
|
113
114
|
videoWriterInput = nil
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
audioWriterInput = nil
|
|
116
|
+
audioTrackMixer = nil
|
|
116
117
|
videoOutputURL = nil
|
|
117
118
|
didCreateOutputFile = false
|
|
118
119
|
}
|
|
@@ -183,8 +184,7 @@ public final class ScreenRecorder {
|
|
|
183
184
|
recorder.startCapture(handler: { (sampleBuffer, sampleType, passedError) in
|
|
184
185
|
if let passedError = passedError {
|
|
185
186
|
self.videoWriterInput?.markAsFinished()
|
|
186
|
-
self.
|
|
187
|
-
self.appAudioWriterInput?.markAsFinished()
|
|
187
|
+
self.audioWriterInput?.markAsFinished()
|
|
188
188
|
if self.videoWriter?.status == .writing {
|
|
189
189
|
self.videoWriter?.cancelWriting()
|
|
190
190
|
}
|
|
@@ -201,12 +201,20 @@ public final class ScreenRecorder {
|
|
|
201
201
|
case .video:
|
|
202
202
|
self.handleSampleBuffer(sampleBuffer: sampleBuffer)
|
|
203
203
|
case .audioApp:
|
|
204
|
-
if self.recordAudio {
|
|
205
|
-
self.
|
|
204
|
+
if self.recordAudio, let mixer = self.audioTrackMixer {
|
|
205
|
+
self.audioMixerQueue.sync {
|
|
206
|
+
for sample in mixer.handleApp(sampleBuffer) {
|
|
207
|
+
self.add(sample: sample, to: self.audioWriterInput)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
206
210
|
}
|
|
207
211
|
case .audioMic:
|
|
208
|
-
if self.recordAudio {
|
|
209
|
-
self.
|
|
212
|
+
if self.recordAudio, let mixer = self.audioTrackMixer {
|
|
213
|
+
self.audioMixerQueue.sync {
|
|
214
|
+
for sample in mixer.handleMic(sampleBuffer) {
|
|
215
|
+
self.add(sample: sample, to: self.audioWriterInput)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
210
218
|
}
|
|
211
219
|
default:
|
|
212
220
|
break
|
|
@@ -248,8 +256,7 @@ public final class ScreenRecorder {
|
|
|
248
256
|
|
|
249
257
|
if let error = error {
|
|
250
258
|
self.videoWriterInput?.markAsFinished()
|
|
251
|
-
self.
|
|
252
|
-
self.appAudioWriterInput?.markAsFinished()
|
|
259
|
+
self.audioWriterInput?.markAsFinished()
|
|
253
260
|
if self.videoWriter?.status == .writing {
|
|
254
261
|
self.videoWriter?.cancelWriting()
|
|
255
262
|
}
|
|
@@ -258,9 +265,16 @@ public final class ScreenRecorder {
|
|
|
258
265
|
return
|
|
259
266
|
}
|
|
260
267
|
|
|
268
|
+
self.audioMixerQueue.sync {
|
|
269
|
+
if let mixer = self.audioTrackMixer {
|
|
270
|
+
for sample in mixer.drain() {
|
|
271
|
+
self.add(sample: sample, to: self.audioWriterInput)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
261
276
|
self.videoWriterInput?.markAsFinished()
|
|
262
|
-
self.
|
|
263
|
-
self.appAudioWriterInput?.markAsFinished()
|
|
277
|
+
self.audioWriterInput?.markAsFinished()
|
|
264
278
|
|
|
265
279
|
guard let writer = self.videoWriter else {
|
|
266
280
|
complete(nil)
|