-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathNativeCameraPlugin.java
More file actions
669 lines (564 loc) · 18.7 KB
/
NativeCameraPlugin.java
File metadata and controls
669 lines (564 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
//
// © 2026-present https://github.com/cengiz-pz
//
package org.godotengine.plugin.nativecamera;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.OutputConfiguration;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import org.godotengine.godot.Godot;
import org.godotengine.godot.Dictionary;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.SignalInfo;
import org.godotengine.godot.plugin.UsedByGodot;
import org.godotengine.plugin.nativecamera.model.CameraInfo;
import org.godotengine.plugin.nativecamera.model.FeedRequest;
import org.godotengine.plugin.nativecamera.model.FrameInfo;
public class NativeCameraPlugin extends GodotPlugin {
public static final String CLASS_NAME = NativeCameraPlugin.class.getSimpleName();
static final String LOG_TAG = "godot::" + CLASS_NAME;
private static final SignalInfo CAMERA_PERMISSION_GRANTED_SIGNAL = new SignalInfo("camera_permission_granted");
private static final SignalInfo CAMERA_PERMISSION_DENIED_SIGNAL = new SignalInfo("camera_permission_denied");
private static final SignalInfo FRAME_AVAILABLE_SIGNAL = new SignalInfo("frame_available", Dictionary.class);
private static final int CAMERA_PERMISSION_REQUEST = 1001;
private CameraDevice camera;
private CameraCaptureSession session;
private ImageReader reader;
private HandlerThread bgThread;
private Handler bgHandler;
private byte[] frameBuffer;
private volatile int framesToSkipDivisor;
private volatile int rotation;
private volatile boolean isGrayscale;
private volatile boolean mirrorHorizontal;
private volatile boolean mirrorVertical;
/** Target width for post-capture scaling; 0 means disabled. */
private volatile int scaleWidth;
/** Target height for post-capture scaling; 0 means disabled. */
private volatile int scaleHeight;
private int frameCounter = 0;
private volatile boolean running = false;
public NativeCameraPlugin(Godot godot) {
super(godot);
}
@Override
public String getPluginName() {
return CLASS_NAME;
}
@Override
public Set<SignalInfo> getPluginSignals() {
Set<SignalInfo> signals = new HashSet<>();
signals.add(CAMERA_PERMISSION_GRANTED_SIGNAL);
signals.add(CAMERA_PERMISSION_DENIED_SIGNAL);
signals.add(FRAME_AVAILABLE_SIGNAL);
return signals;
}
@UsedByGodot
public boolean has_camera_permission() {
Activity activity = getActivity();
return (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED);
}
@UsedByGodot
public void request_camera_permission() {
Activity activity = getActivity();
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
Log.w(LOG_TAG, "request_camera_permission(): Camera permission already granted");
return;
}
ActivityCompat.requestPermissions(
activity,
new String[]{Manifest.permission.CAMERA},
CAMERA_PERMISSION_REQUEST
);
}
@UsedByGodot
public Object[] get_all_cameras() {
Activity activity = getActivity();
List<Dictionary> resultList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
Log.e(LOG_TAG, "get_all_cameras(): Camera permission not granted");
return resultList.toArray();
}
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
String[] cameraIds = manager.getCameraIdList();
for (String cameraId : cameraIds) {
try {
CameraInfo cameraInfo = new CameraInfo(cameraId, manager.getCameraCharacteristics(cameraId));
resultList.add(cameraInfo.buildRawData());
} catch (Exception e) {
Log.w(LOG_TAG, "get_all_cameras(): Skipping camera " + cameraId, e);
}
}
} catch (CameraAccessException | SecurityException e) {
Log.e(LOG_TAG, "get_all_cameras(): Failed to generate camera list", e);
}
return resultList.toArray();
}
@UsedByGodot
public void start(Dictionary requestDict) {
Activity activity = getActivity();
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
Log.e(LOG_TAG, "start(): Camera permission not granted");
return;
}
if (running) {
return;
}
running = true;
startThread();
FeedRequest feedRequest = new FeedRequest(requestDict);
framesToSkipDivisor = feedRequest.getFramesToSkip() + 1;
rotation = feedRequest.getRotation(); // degrees
isGrayscale = feedRequest.isGrayscale();
mirrorHorizontal = feedRequest.isMirrorHorizontal();
mirrorVertical = feedRequest.isMirrorVertical();
scaleWidth = feedRequest.getScaleWidth();
scaleHeight = feedRequest.getScaleHeight();
openCamera(feedRequest);
}
@UsedByGodot
public void stop() {
running = false; // Immediate stop flag
if (session != null) {
session.close();
session = null;
}
if (camera != null) {
camera.close();
camera = null;
}
if (reader != null) {
reader.close();
reader = null;
}
stopThread();
}
private void startThread() {
bgThread = new HandlerThread("CameraCaptureThread");
bgThread.start();
bgHandler = new Handler(bgThread.getLooper());
}
private void stopThread() {
if (bgThread != null) {
bgThread.quitSafely();
try {
bgThread.join();
bgThread = null;
bgHandler = null;
} catch (InterruptedException e) {
Log.e(LOG_TAG, "stopThread(): Failed", e);
}
}
}
private void openCamera(FeedRequest request) {
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
reader = ImageReader.newInstance(request.getWidth(), request.getHeight(), ImageFormat.YUV_420_888, 2);
reader.setOnImageAvailableListener(this::onImageAvailable, bgHandler);
manager.openCamera(request.getCameraId(), deviceCallback, bgHandler);
} catch (CameraAccessException | SecurityException e) {
Log.e(LOG_TAG, "openCamera(): Failed", e);
}
}
void emitFrame(byte[] buffer, int width, int height, int rotation, boolean isGrayscale) {
Activity activity = getActivity();
Log.d(LOG_TAG, String.format(
"emitFrame(): Emitting frame buffer size: %d image size: %dx%d, rotation: %d, gray?: %b",
buffer.length, width, height, rotation, isGrayscale
));
// Run on Android UI thread -> Godot main thread
activity.runOnUiThread(() -> {
emitSignal(FRAME_AVAILABLE_SIGNAL.getName(), new FrameInfo(buffer.clone(), width,
height, rotation, isGrayscale).buildRawData());
});
}
private final CameraDevice.StateCallback deviceCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice cameraDevice) {
camera = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(CameraDevice cameraDevice) {
cameraDevice.close();
}
@Override
public void onError(CameraDevice cameraDevice, int error) {
cameraDevice.close();
}
};
private void createCameraPreviewSession() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Use modern SessionConfiguration for API 28+
OutputConfiguration outputConfig = new OutputConfiguration(reader.getSurface());
// Ensure the callback runs on our background thread
Executor executor = bgHandler::post;
SessionConfiguration sessionConfig = new SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
Collections.singletonList(outputConfig),
executor,
sessionCallback
);
camera.createCaptureSession(sessionConfig);
} else {
// This is necessary for devices running Android 8.1 or lower
createLegacyCaptureSession();
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@SuppressWarnings("deprecation")
private void createLegacyCaptureSession() throws CameraAccessException {
camera.createCaptureSession(
Collections.singletonList(reader.getSurface()),
sessionCallback,
bgHandler
);
}
private final CameraCaptureSession.StateCallback sessionCallback =
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession captureSession) {
session = captureSession;
try {
CaptureRequest.Builder req = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
req.addTarget(reader.getSurface());
req.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
session.setRepeatingRequest(req.build(), null, bgHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
}
};
private void onImageAvailable(ImageReader reader) {
if (!running) {
return;
}
try {
Image image = reader.acquireLatestImage();
if (image == null) {
return;
}
if (!running) {
image.close();
return;
}
frameCounter++;
if (frameCounter % framesToSkipDivisor != 0) {
image.close();
return;
}
int width = image.getWidth();
int height = image.getHeight();
// Calculate size RGBA8 = 4 bytes, Grayscale = 1 byte per pixel
int requiredSize = isGrayscale ? (width * height) : (width * height * 4);
if (frameBuffer == null || frameBuffer.length != requiredSize) {
frameBuffer = new byte[requiredSize];
}
Image.Plane yPlane = image.getPlanes()[0];
ByteBuffer yBuffer = yPlane.getBuffer();
int yRowStride = yPlane.getRowStride();
int yPixelStride = yPlane.getPixelStride(); // Usually 1
byte[] output = frameBuffer;
int offset = 0;
if (isGrayscale) {
if (yPixelStride == 1 && yRowStride == width) {
yBuffer.get(output, 0, width * height);
} else {
for (int y = 0; y < height; y++) {
int rowStart = y * yRowStride;
for (int x = 0; x < width; x++) {
output[offset++] = yBuffer.get(rowStart + x * yPixelStride);
}
}
}
} else {
// Color processing (YUV -> RGBA conversion)
Image.Plane uPlane = image.getPlanes()[1];
Image.Plane vPlane = image.getPlanes()[2];
ByteBuffer uBuffer = uPlane.getBuffer();
ByteBuffer vBuffer = vPlane.getBuffer();
int uRowStride = uPlane.getRowStride();
int vRowStride = vPlane.getRowStride();
int uPixelStride = uPlane.getPixelStride();
int vPixelStride = vPlane.getPixelStride();
for (int y = 0; y < height; y++) {
int yRowStart = y * yRowStride;
int uvRowStart = (y / 2) * uRowStride; // UV is subsampled vertically
for (int x = 0; x < width; x++) {
// Get Y
int yVal = yBuffer.get(yRowStart + x * yPixelStride) & 0xFF;
// Get U and V (Subsampled 2x2)
int uvCol = (x / 2) * uPixelStride;
int uVal = (uBuffer.get(uvRowStart + uvCol) & 0xFF) - 128;
int vVal = (vBuffer.get(uvRowStart + uvCol) & 0xFF) - 128;
// YUV to RGB Conversion
// R = Y + 1.402 * V
// G = Y - 0.34414 * U - 0.71414 * V
// B = Y + 1.772 * U
int r = (int) (yVal + 1.402f * vVal);
int g = (int) (yVal - 0.34414f * uVal - 0.71414f * vVal);
int b = (int) (yVal + 1.772f * uVal);
// Clamp and Write RGBA (4 bytes)
output[offset++] = (byte) (r < 0 ? 0 : (r > 255 ? 255 : r)); // R
output[offset++] = (byte) (g < 0 ? 0 : (g > 255 ? 255 : g)); // G
output[offset++] = (byte) (b < 0 ? 0 : (b > 255 ? 255 : b)); // B
output[offset++] = (byte) 255; // Alpha (Opaque)
}
}
}
if (rotation != 0) {
RotationResult result;
if (isGrayscale) {
result = rotateGray(output, width, height, rotation);
} else {
result = rotateRGBA(output, width, height, rotation);
}
output = result.buffer;
width = result.width;
height = result.height;
}
if (mirrorHorizontal || mirrorVertical) {
if (isGrayscale) {
output = mirrorGray(output, width, height, mirrorHorizontal, mirrorVertical);
} else {
output = mirrorRGBA(output, width, height, mirrorHorizontal, mirrorVertical);
}
}
// Scaling is applied last — after rotation and mirroring — so that
// scale_width and scale_height always describe the final emitted dimensions.
if (scaleWidth > 0 && scaleHeight > 0 && (scaleWidth != width || scaleHeight != height)) {
if (isGrayscale) {
output = scaleGray(output, width, height, scaleWidth, scaleHeight);
} else {
output = scaleRGBA(output, width, height, scaleWidth, scaleHeight);
}
width = scaleWidth;
height = scaleHeight;
}
if (running) {
emitFrame(output, width, height, rotation, isGrayscale);
}
image.close();
} catch (Exception e) {
Log.e(LOG_TAG, "onImageAvailable(): Error while processing frame", e);
}
}
@Override
public void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onMainRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == CAMERA_PERMISSION_REQUEST) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(LOG_TAG, "Camera Permission Granted by user");
emitSignal(CAMERA_PERMISSION_GRANTED_SIGNAL.getName());
} else {
Log.d(LOG_TAG, "Camera Permission Denied by user");
emitSignal(CAMERA_PERMISSION_DENIED_SIGNAL.getName());
}
}
}
@Override
public void onGodotSetupCompleted() {
super.onGodotSetupCompleted();
// TODO: Godot is ready
}
@Override
public void onMainDestroy() {
// TODO: Plugin cleanup
}
private static class RotationResult {
byte[] buffer;
int width;
int height;
RotationResult(byte[] buffer, int width, int height) {
this.buffer = buffer;
this.width = width;
this.height = height;
}
}
private static RotationResult rotateRGBA(
byte[] src,
int width,
int height,
int rotation
) {
rotation = ((rotation % 360) + 360) % 360;
if (rotation == 0) {
return new RotationResult(src, width, height);
}
int newWidth = (rotation == 90 || rotation == 270) ? height : width;
int newHeight = (rotation == 90 || rotation == 270) ? width : height;
byte[] dst = new byte[src.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int srcIndex = (y * width + x) * 4;
int dx = 0;
int dy = 0;
switch (rotation) {
case 90:
dx = height - 1 - y;
dy = x;
break;
case 180:
dx = width - 1 - x;
dy = height - 1 - y;
break;
case 270:
dx = y;
dy = width - 1 - x;
break;
}
int dstIndex = (dy * newWidth + dx) * 4;
dst[dstIndex] = src[srcIndex];
dst[dstIndex + 1] = src[srcIndex + 1];
dst[dstIndex + 2] = src[srcIndex + 2];
dst[dstIndex + 3] = src[srcIndex + 3];
}
}
return new RotationResult(dst, newWidth, newHeight);
}
private static RotationResult rotateGray(
byte[] src,
int width,
int height,
int rotation
) {
rotation = ((rotation % 360) + 360) % 360;
if (rotation == 0) {
return new RotationResult(src, width, height);
}
int newWidth = (rotation == 90 || rotation == 270) ? height : width;
int newHeight = (rotation == 90 || rotation == 270) ? width : height;
byte[] dst = new byte[src.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int srcIndex = y * width + x;
int dx = 0;
int dy = 0;
switch (rotation) {
case 90:
dx = height - 1 - y;
dy = x;
break;
case 180:
dx = width - 1 - x;
dy = height - 1 - y;
break;
case 270:
dx = y;
dy = width - 1 - x;
break;
}
int dstIndex = dy * newWidth + dx;
dst[dstIndex] = src[srcIndex];
}
}
return new RotationResult(dst, newWidth, newHeight);
}
/**
* Mirrors an RGBA (4 bytes/pixel) frame buffer horizontally, vertically, or both.
* Dimensions are unchanged; only pixel positions are swapped.
*/
private static byte[] mirrorRGBA(byte[] src, int width, int height,
boolean horizontal, boolean vertical) {
byte[] dst = new byte[src.length];
for (int y = 0; y < height; y++) {
int dy = vertical ? (height - 1 - y) : y;
for (int x = 0; x < width; x++) {
int dx = horizontal ? (width - 1 - x) : x;
int srcIdx = (y * width + x) * 4;
int dstIdx = (dy * width + dx) * 4;
dst[dstIdx] = src[srcIdx];
dst[dstIdx + 1] = src[srcIdx + 1];
dst[dstIdx + 2] = src[srcIdx + 2];
dst[dstIdx + 3] = src[srcIdx + 3];
}
}
return dst;
}
/**
* Mirrors a grayscale (1 byte/pixel) frame buffer horizontally, vertically, or both.
* Dimensions are unchanged; only pixel positions are swapped.
*/
private static byte[] mirrorGray(byte[] src, int width, int height,
boolean horizontal, boolean vertical) {
byte[] dst = new byte[src.length];
for (int y = 0; y < height; y++) {
int dy = vertical ? (height - 1 - y) : y;
for (int x = 0; x < width; x++) {
int dx = horizontal ? (width - 1 - x) : x;
dst[dy * width + dx] = src[y * width + x];
}
}
return dst;
}
/**
* Scales an RGBA (4 bytes/pixel) frame buffer to {@code dstW × dstH} using
* nearest-neighbour interpolation. Applied after rotation and mirroring.
*/
static byte[] scaleRGBA(byte[] src, int srcW, int srcH, int dstW, int dstH) {
byte[] dst = new byte[dstW * dstH * 4];
for (int dy = 0; dy < dstH; dy++) {
int sy = dy * srcH / dstH;
for (int dx = 0; dx < dstW; dx++) {
int sx = dx * srcW / dstW;
int srcIdx = (sy * srcW + sx) * 4;
int dstIdx = (dy * dstW + dx) * 4;
dst[dstIdx] = src[srcIdx];
dst[dstIdx + 1] = src[srcIdx + 1];
dst[dstIdx + 2] = src[srcIdx + 2];
dst[dstIdx + 3] = src[srcIdx + 3];
}
}
return dst;
}
/**
* Scales a grayscale (1 byte/pixel) frame buffer to {@code dstW × dstH} using
* nearest-neighbour interpolation. Applied after rotation and mirroring.
*/
static byte[] scaleGray(byte[] src, int srcW, int srcH, int dstW, int dstH) {
byte[] dst = new byte[dstW * dstH];
for (int dy = 0; dy < dstH; dy++) {
int sy = dy * srcH / dstH;
for (int dx = 0; dx < dstW; dx++) {
int sx = dx * srcW / dstW;
dst[dy * dstW + dx] = src[sy * srcW + sx];
}
}
return dst;
}
}