@apirtc/expo-apirtc-options-plugin 0.0.4 → 0.0.5
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/README.md +72 -38
- package/build/static/kotlin/BackgroundBlurModule.kt +420 -0
- package/build/static/kotlin/BackgroundBlurPackage.kt +16 -0
- package/build/static/kotlin/BackgroundImageProcessor.kt +433 -0
- package/build/static/kotlin/BlurVideoProcessor.kt +448 -0
- package/build/withAndroidPlugin.d.ts +1 -0
- package/build/withAndroidPlugin.js +72 -16
- package/build/withPlugin.d.ts +1 -0
- package/build/withPlugin.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
package com.reactnativeapirtc
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Paint
|
|
6
|
+
import android.graphics.PorterDuff
|
|
7
|
+
import android.graphics.PorterDuffXfermode
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import com.google.android.gms.tasks.Tasks
|
|
10
|
+
import com.google.mlkit.vision.common.InputImage
|
|
11
|
+
import com.google.mlkit.vision.segmentation.Segmentation
|
|
12
|
+
import com.google.mlkit.vision.segmentation.Segmenter
|
|
13
|
+
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
|
|
14
|
+
import org.webrtc.JavaI420Buffer
|
|
15
|
+
import org.webrtc.VideoFrame
|
|
16
|
+
import org.webrtc.VideoProcessor
|
|
17
|
+
import org.webrtc.VideoSink
|
|
18
|
+
import java.nio.ByteBuffer
|
|
19
|
+
import java.nio.ByteOrder
|
|
20
|
+
import java.util.concurrent.TimeUnit
|
|
21
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* BackgroundImageProcessor implements WebRTC's VideoProcessor interface.
|
|
25
|
+
*
|
|
26
|
+
* Replaces the background with a provided Bitmap image, keeping the person
|
|
27
|
+
* (detected via ML Kit Selfie Segmentation) sharp and in focus.
|
|
28
|
+
*
|
|
29
|
+
* Flow: Camera → onFrameCaptured → [segment + composite] → downstreamSink
|
|
30
|
+
*/
|
|
31
|
+
class BackgroundImageProcessor(private val backgroundBitmap: Bitmap) : VideoProcessor {
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
private const val TAG = "BgImageProcessor"
|
|
35
|
+
private const val SEGMENTATION_INTERVAL = 2
|
|
36
|
+
private const val SEG_WIDTH = 256
|
|
37
|
+
private const val SEG_HEIGHT = 192
|
|
38
|
+
private const val SEGMENTATION_TIMEOUT_MS = 200L
|
|
39
|
+
private const val SEGMENTATION_TIMEOUT_FIRST_MS = 600L
|
|
40
|
+
private const val MASK_DECAY = 0.5f
|
|
41
|
+
private const val MASK_THRESHOLD_LOW = 0.25f
|
|
42
|
+
private const val MASK_THRESHOLD_HIGH = 0.50f
|
|
43
|
+
private const val VERTICAL_FILL_EXTEND = 8
|
|
44
|
+
private const val VERTICAL_FILL_ANCHOR = 0.55f
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Volatile
|
|
48
|
+
private var downstreamSink: VideoSink? = null
|
|
49
|
+
|
|
50
|
+
private val enabled = AtomicBoolean(false)
|
|
51
|
+
private var segmenter: Segmenter? = null
|
|
52
|
+
private var frameCount = 0L
|
|
53
|
+
|
|
54
|
+
@Volatile private var cachedMaskBuffer: ByteBuffer? = null
|
|
55
|
+
@Volatile private var cachedMaskWidth = 0
|
|
56
|
+
@Volatile private var cachedMaskHeight = 0
|
|
57
|
+
@Volatile private var previousMaskFloats: FloatArray? = null
|
|
58
|
+
|
|
59
|
+
private var compositeBitmap: Bitmap? = null
|
|
60
|
+
private var scaledBackground: Bitmap? = null
|
|
61
|
+
private var cachedBgWidth = -1
|
|
62
|
+
private var cachedBgHeight = -1
|
|
63
|
+
private var cachedBgRotation = -1
|
|
64
|
+
|
|
65
|
+
// =====================================================================
|
|
66
|
+
// VideoProcessor interface
|
|
67
|
+
// =====================================================================
|
|
68
|
+
|
|
69
|
+
override fun setSink(sink: VideoSink?) {
|
|
70
|
+
downstreamSink = sink
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onCapturerStarted(success: Boolean) {}
|
|
74
|
+
|
|
75
|
+
override fun onCapturerStopped() {}
|
|
76
|
+
|
|
77
|
+
override fun onFrameCaptured(frame: VideoFrame) {
|
|
78
|
+
val sink = downstreamSink ?: return
|
|
79
|
+
|
|
80
|
+
if (!enabled.get()) {
|
|
81
|
+
sink.onFrame(frame)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
val processedFrame = processFrame(frame)
|
|
87
|
+
if (processedFrame != null) {
|
|
88
|
+
sink.onFrame(processedFrame)
|
|
89
|
+
processedFrame.release()
|
|
90
|
+
} else {
|
|
91
|
+
sink.onFrame(frame)
|
|
92
|
+
}
|
|
93
|
+
} catch (e: Exception) {
|
|
94
|
+
Log.e(TAG, "Error processing frame, forwarding original", e)
|
|
95
|
+
sink.onFrame(frame)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =====================================================================
|
|
100
|
+
// Enable / Disable
|
|
101
|
+
// =====================================================================
|
|
102
|
+
|
|
103
|
+
fun enable() {
|
|
104
|
+
if (enabled.getAndSet(true)) return
|
|
105
|
+
Log.d(TAG, "Enabling background image processor")
|
|
106
|
+
|
|
107
|
+
val options = SelfieSegmenterOptions.Builder()
|
|
108
|
+
.setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
|
|
109
|
+
.enableRawSizeMask()
|
|
110
|
+
.build()
|
|
111
|
+
segmenter = Segmentation.getClient(options)
|
|
112
|
+
|
|
113
|
+
frameCount = 0
|
|
114
|
+
cachedMaskBuffer = null
|
|
115
|
+
Log.d(TAG, "Background image processor enabled")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun disable() {
|
|
119
|
+
if (!enabled.getAndSet(false)) return
|
|
120
|
+
Log.d(TAG, "Disabling background image processor")
|
|
121
|
+
|
|
122
|
+
segmenter?.close()
|
|
123
|
+
segmenter = null
|
|
124
|
+
compositeBitmap?.recycle()
|
|
125
|
+
compositeBitmap = null
|
|
126
|
+
scaledBackground?.recycle()
|
|
127
|
+
scaledBackground = null
|
|
128
|
+
cachedMaskBuffer = null
|
|
129
|
+
previousMaskFloats = null
|
|
130
|
+
cachedBgWidth = -1
|
|
131
|
+
cachedBgHeight = -1
|
|
132
|
+
cachedBgRotation = -1
|
|
133
|
+
|
|
134
|
+
Log.d(TAG, "Background image processor disabled")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =====================================================================
|
|
138
|
+
// Frame Processing Pipeline
|
|
139
|
+
// =====================================================================
|
|
140
|
+
|
|
141
|
+
private fun processFrame(frame: VideoFrame): VideoFrame? {
|
|
142
|
+
val buffer = frame.buffer
|
|
143
|
+
buffer.retain()
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
val width = buffer.width
|
|
147
|
+
val height = buffer.height
|
|
148
|
+
|
|
149
|
+
val bitmap = i420ToBitmap(buffer, width, height) ?: return null
|
|
150
|
+
|
|
151
|
+
frameCount++
|
|
152
|
+
if (frameCount % SEGMENTATION_INTERVAL == 0L || cachedMaskBuffer == null) {
|
|
153
|
+
updateSegmentationMask(bitmap)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
val mask = cachedMaskBuffer ?: run {
|
|
157
|
+
bitmap.recycle()
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The raw frame buffer is always in the camera sensor orientation (landscape
|
|
162
|
+
// for most phones), and frame.rotation tells the renderer how many degrees to
|
|
163
|
+
// rotate the frame for display. We must pre-rotate the background by the
|
|
164
|
+
// OPPOSITE angle so that after the renderer applies its rotation the image
|
|
165
|
+
// appears upright and fills the display area.
|
|
166
|
+
val rotation = frame.rotation
|
|
167
|
+
if (scaledBackground == null ||
|
|
168
|
+
cachedBgWidth != width ||
|
|
169
|
+
cachedBgHeight != height ||
|
|
170
|
+
cachedBgRotation != rotation) {
|
|
171
|
+
scaledBackground?.recycle()
|
|
172
|
+
// Effective display dimensions after renderer rotation
|
|
173
|
+
val dispW = if (rotation == 90 || rotation == 270) height else width
|
|
174
|
+
val dispH = if (rotation == 90 || rotation == 270) width else height
|
|
175
|
+
// Center-crop to display aspect ratio, then pre-rotate to match raw frame
|
|
176
|
+
val cropped = centerCropBitmap(backgroundBitmap, dispW, dispH)
|
|
177
|
+
scaledBackground = if (rotation != 0) rotateBitmap(cropped, -rotation.toFloat()) else cropped
|
|
178
|
+
cachedBgWidth = width
|
|
179
|
+
cachedBgHeight = height
|
|
180
|
+
cachedBgRotation = rotation
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
applyImageComposite(bitmap, width, height, mask, cachedMaskWidth, cachedMaskHeight)
|
|
184
|
+
bitmap.recycle()
|
|
185
|
+
|
|
186
|
+
val comp = compositeBitmap ?: return null
|
|
187
|
+
val i420Buffer = bitmapToI420Buffer(comp, width, height)
|
|
188
|
+
return VideoFrame(i420Buffer, frame.rotation, frame.timestampNs)
|
|
189
|
+
|
|
190
|
+
} catch (e: Exception) {
|
|
191
|
+
Log.e(TAG, "processFrame error", e)
|
|
192
|
+
return null
|
|
193
|
+
} finally {
|
|
194
|
+
buffer.release()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private fun updateSegmentationMask(bitmap: Bitmap) {
|
|
199
|
+
val seg = segmenter ?: return
|
|
200
|
+
val isFirstMask = cachedMaskBuffer == null
|
|
201
|
+
val timeout = if (isFirstMask) SEGMENTATION_TIMEOUT_FIRST_MS else SEGMENTATION_TIMEOUT_MS
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
val scaled = Bitmap.createScaledBitmap(bitmap, SEG_WIDTH, SEG_HEIGHT, true)
|
|
205
|
+
val input = InputImage.fromBitmap(scaled, 0)
|
|
206
|
+
|
|
207
|
+
val result = Tasks.await(seg.process(input), timeout, TimeUnit.MILLISECONDS)
|
|
208
|
+
|
|
209
|
+
val maskBuf = result.buffer
|
|
210
|
+
cachedMaskWidth = result.width
|
|
211
|
+
cachedMaskHeight = result.height
|
|
212
|
+
val size = cachedMaskWidth * cachedMaskHeight
|
|
213
|
+
|
|
214
|
+
maskBuf.rewind()
|
|
215
|
+
val rawFloats = FloatArray(size) { if (maskBuf.remaining() >= 4) maskBuf.float else 0f }
|
|
216
|
+
|
|
217
|
+
// Column fill: connect upper and lower body in each column
|
|
218
|
+
val filled = fillVerticalGaps(rawFloats, cachedMaskWidth, cachedMaskHeight)
|
|
219
|
+
|
|
220
|
+
// Max-blend with decayed previous: fills transient holes, ghost fades in 2 frames
|
|
221
|
+
val prev = previousMaskFloats
|
|
222
|
+
val blended = if (prev != null && prev.size == size) {
|
|
223
|
+
FloatArray(size) { i -> maxOf(filled[i], MASK_DECAY * prev[i]) }
|
|
224
|
+
} else filled
|
|
225
|
+
previousMaskFloats = blended
|
|
226
|
+
|
|
227
|
+
val newMask = ByteBuffer.allocateDirect(size * 4)
|
|
228
|
+
newMask.order(ByteOrder.nativeOrder())
|
|
229
|
+
for (f in blended) newMask.putFloat(f)
|
|
230
|
+
newMask.rewind()
|
|
231
|
+
cachedMaskBuffer = newMask
|
|
232
|
+
|
|
233
|
+
if (scaled !== bitmap) scaled.recycle()
|
|
234
|
+
|
|
235
|
+
} catch (e: java.util.concurrent.TimeoutException) {
|
|
236
|
+
Log.d(TAG, "Segmentation timeout, using cached mask")
|
|
237
|
+
} catch (e: Exception) {
|
|
238
|
+
Log.w(TAG, "Segmentation error (frame #$frameCount)", e)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Composite: background image → punch out person area → draw sharp person in front.
|
|
244
|
+
*/
|
|
245
|
+
private fun fillVerticalGaps(floats: FloatArray, width: Int, height: Int): FloatArray {
|
|
246
|
+
val out = floats.copyOf()
|
|
247
|
+
for (x in 0 until width) {
|
|
248
|
+
var topY = -1
|
|
249
|
+
var bottomY = -1
|
|
250
|
+
for (y in 0 until height) {
|
|
251
|
+
if (floats[y * width + x] > VERTICAL_FILL_ANCHOR) {
|
|
252
|
+
if (topY == -1) topY = y
|
|
253
|
+
bottomY = y
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (topY != -1) {
|
|
257
|
+
val extendedBottom = minOf(bottomY + VERTICAL_FILL_EXTEND, height - 1)
|
|
258
|
+
for (y in topY..extendedBottom) {
|
|
259
|
+
val idx = y * width + x
|
|
260
|
+
if (out[idx] < 0.6f) out[idx] = 0.6f
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return out
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun applyImageComposite(
|
|
268
|
+
original: Bitmap, width: Int, height: Int,
|
|
269
|
+
maskBuffer: ByteBuffer, maskWidth: Int, maskHeight: Int
|
|
270
|
+
) {
|
|
271
|
+
val bg = scaledBackground ?: return
|
|
272
|
+
|
|
273
|
+
if (compositeBitmap == null ||
|
|
274
|
+
compositeBitmap!!.width != width ||
|
|
275
|
+
compositeBitmap!!.height != height) {
|
|
276
|
+
compositeBitmap?.recycle()
|
|
277
|
+
compositeBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Build mask bitmap from segmentation floats
|
|
281
|
+
val maskPixels = IntArray(maskWidth * maskHeight)
|
|
282
|
+
maskBuffer.rewind()
|
|
283
|
+
for (i in maskPixels.indices) {
|
|
284
|
+
val raw = if (maskBuffer.remaining() >= 4) maskBuffer.float else 0f
|
|
285
|
+
val conf = when {
|
|
286
|
+
raw < MASK_THRESHOLD_LOW -> 0f
|
|
287
|
+
raw > MASK_THRESHOLD_HIGH -> 1f
|
|
288
|
+
else -> (raw - MASK_THRESHOLD_LOW) / (MASK_THRESHOLD_HIGH - MASK_THRESHOLD_LOW)
|
|
289
|
+
}
|
|
290
|
+
val alpha = (conf * 255).toInt().coerceIn(0, 255)
|
|
291
|
+
maskPixels[i] = (alpha shl 24) or 0x00FFFFFF
|
|
292
|
+
}
|
|
293
|
+
val maskBmp = Bitmap.createBitmap(maskPixels, maskWidth, maskHeight, Bitmap.Config.ARGB_8888)
|
|
294
|
+
val scaledMask = if (maskWidth != width || maskHeight != height) {
|
|
295
|
+
val s = Bitmap.createScaledBitmap(maskBmp, width, height, true)
|
|
296
|
+
maskBmp.recycle()
|
|
297
|
+
s
|
|
298
|
+
} else maskBmp
|
|
299
|
+
|
|
300
|
+
// Build person layer: original pixels with alpha from mask.
|
|
301
|
+
// At edges, alpha is partial → lerp between BG image and original → no halo.
|
|
302
|
+
val personBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
303
|
+
val personCanvas = Canvas(personBitmap)
|
|
304
|
+
personCanvas.drawBitmap(original, 0f, 0f, null)
|
|
305
|
+
val dstInPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) }
|
|
306
|
+
personCanvas.drawBitmap(scaledMask, 0f, 0f, dstInPaint)
|
|
307
|
+
|
|
308
|
+
val canvas = Canvas(compositeBitmap!!)
|
|
309
|
+
canvas.drawBitmap(bg, 0f, 0f, null)
|
|
310
|
+
canvas.drawBitmap(personBitmap, 0f, 0f, null)
|
|
311
|
+
scaledMask.recycle()
|
|
312
|
+
personBitmap.recycle()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// =====================================================================
|
|
316
|
+
// Bitmap Helpers
|
|
317
|
+
// =====================================================================
|
|
318
|
+
|
|
319
|
+
private fun rotateBitmap(src: Bitmap, degrees: Float): Bitmap {
|
|
320
|
+
val matrix = android.graphics.Matrix().apply { postRotate(degrees) }
|
|
321
|
+
val rotated = Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
|
|
322
|
+
src.recycle()
|
|
323
|
+
return rotated
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun centerCropBitmap(src: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
|
|
327
|
+
val srcAspect = src.width.toFloat() / src.height
|
|
328
|
+
val dstAspect = targetWidth.toFloat() / targetHeight
|
|
329
|
+
|
|
330
|
+
val srcX: Int
|
|
331
|
+
val srcY: Int
|
|
332
|
+
val srcW: Int
|
|
333
|
+
val srcH: Int
|
|
334
|
+
|
|
335
|
+
if (srcAspect > dstAspect) {
|
|
336
|
+
srcH = src.height
|
|
337
|
+
srcW = (src.height * dstAspect).toInt()
|
|
338
|
+
srcX = (src.width - srcW) / 2
|
|
339
|
+
srcY = 0
|
|
340
|
+
} else {
|
|
341
|
+
srcW = src.width
|
|
342
|
+
srcH = (src.width / dstAspect).toInt()
|
|
343
|
+
srcX = 0
|
|
344
|
+
srcY = (src.height - srcH) / 2
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
val cropped = Bitmap.createBitmap(src, srcX, srcY, srcW, srcH)
|
|
348
|
+
val scaled = Bitmap.createScaledBitmap(cropped, targetWidth, targetHeight, true)
|
|
349
|
+
if (cropped !== src) cropped.recycle()
|
|
350
|
+
return scaled
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// =====================================================================
|
|
354
|
+
// Color Space Conversions (I420 ↔ Bitmap)
|
|
355
|
+
// =====================================================================
|
|
356
|
+
|
|
357
|
+
private fun i420ToBitmap(buffer: VideoFrame.Buffer, width: Int, height: Int): Bitmap? {
|
|
358
|
+
return try {
|
|
359
|
+
val i420 = buffer.toI420() ?: return null
|
|
360
|
+
|
|
361
|
+
val yPlane = i420.dataY
|
|
362
|
+
val uPlane = i420.dataU
|
|
363
|
+
val vPlane = i420.dataV
|
|
364
|
+
val yStride = i420.strideY
|
|
365
|
+
val uStride = i420.strideU
|
|
366
|
+
val vStride = i420.strideV
|
|
367
|
+
|
|
368
|
+
val nv21 = ByteArray(width * height * 3 / 2)
|
|
369
|
+
|
|
370
|
+
for (row in 0 until height) {
|
|
371
|
+
yPlane.position(row * yStride)
|
|
372
|
+
yPlane.get(nv21, row * width, width)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
val chromaHeight = height / 2
|
|
376
|
+
val chromaWidth = width / 2
|
|
377
|
+
for (row in 0 until chromaHeight) {
|
|
378
|
+
for (col in 0 until chromaWidth) {
|
|
379
|
+
val nv21Offset = width * height + row * width + col * 2
|
|
380
|
+
vPlane.position(row * vStride + col)
|
|
381
|
+
nv21[nv21Offset] = vPlane.get()
|
|
382
|
+
uPlane.position(row * uStride + col)
|
|
383
|
+
nv21[nv21Offset + 1] = uPlane.get()
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
i420.release()
|
|
387
|
+
|
|
388
|
+
val yuvImage = android.graphics.YuvImage(
|
|
389
|
+
nv21, android.graphics.ImageFormat.NV21, width, height, null
|
|
390
|
+
)
|
|
391
|
+
val out = java.io.ByteArrayOutputStream()
|
|
392
|
+
yuvImage.compressToJpeg(android.graphics.Rect(0, 0, width, height), 85, out)
|
|
393
|
+
val jpegBytes = out.toByteArray()
|
|
394
|
+
val decoded = android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
395
|
+
decoded?.copy(Bitmap.Config.ARGB_8888, true)?.also { decoded.recycle() }
|
|
396
|
+
} catch (e: Exception) {
|
|
397
|
+
Log.e(TAG, "I420→Bitmap error", e)
|
|
398
|
+
null
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private fun bitmapToI420Buffer(bitmap: Bitmap, width: Int, height: Int): JavaI420Buffer {
|
|
403
|
+
val argbBuffer = ByteBuffer.allocateDirect(width * height * 4)
|
|
404
|
+
bitmap.copyPixelsToBuffer(argbBuffer)
|
|
405
|
+
argbBuffer.rewind()
|
|
406
|
+
|
|
407
|
+
val i420Buffer = JavaI420Buffer.allocate(width, height)
|
|
408
|
+
val yBuf = i420Buffer.dataY
|
|
409
|
+
val uBuf = i420Buffer.dataU
|
|
410
|
+
val vBuf = i420Buffer.dataV
|
|
411
|
+
|
|
412
|
+
for (y in 0 until height) {
|
|
413
|
+
for (x in 0 until width) {
|
|
414
|
+
val idx = (y * width + x) * 4
|
|
415
|
+
val r = argbBuffer.get(idx).toInt() and 0xFF
|
|
416
|
+
val g = argbBuffer.get(idx + 1).toInt() and 0xFF
|
|
417
|
+
val b = argbBuffer.get(idx + 2).toInt() and 0xFF
|
|
418
|
+
|
|
419
|
+
val yVal = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16
|
|
420
|
+
yBuf.put(y * i420Buffer.strideY + x, yVal.coerceIn(0, 255).toByte())
|
|
421
|
+
|
|
422
|
+
if (y % 2 == 0 && x % 2 == 0) {
|
|
423
|
+
val uVal = ((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128
|
|
424
|
+
val vVal = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128
|
|
425
|
+
val chromaIdx = (y / 2) * i420Buffer.strideU + (x / 2)
|
|
426
|
+
uBuf.put(chromaIdx, uVal.coerceIn(0, 255).toByte())
|
|
427
|
+
vBuf.put(chromaIdx, vVal.coerceIn(0, 255).toByte())
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return i420Buffer
|
|
432
|
+
}
|
|
433
|
+
}
|