I am using mobile vision API for scanning barcode. I am using TextureView
to render camera on it. I read this github thread https://github.com/googlesamples/android-vision/issues/15 it seems mobile vision is not compatible with TextureView
I see that CameraSource
is for SurfaceView
which is not compatible with TextureView
, as there is no method to preview frames on TextureView
. https://developers.google.com/android/reference/com/google/android/gms/vision/CameraSource.html#start(android.view.SurfaceHolder)
I tried to do using below approach and checked that receiveDetections
is not being called. Does anyone have any idea how can I integrate mobile vision API with TextureView
and Camera
.
@RuntimePermissions
public class ScanBarcodeActivity extends BaseActivity {
private TextureView textureView;
private BarcodeDetector barcodeDetector;
private Camera camera;
private String TAG = LogUtils.makeLogTag(ScanBarcodeActivity.class);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityScanBarcodeBinding activityScanBarcodeBinding = setContentView(this, R.layout.activity_scan_barcode);
textureView = activityScanBarcodeBinding.textureView;
barcodeDetector = new BarcodeDetector.Builder(this).build();
ScanBarcodeActivityPermissionsDispatcher.requestCameraPermissionWithCheck(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// NOTE: delegate the permission handling to generated method
ScanBarcodeActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
if(permissions[0].equals(Manifest.permission.CAMERA)) {
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initializeCamera(); }
}
}
@NeedsPermission(Manifest.permission.CAMERA)
void requestCameraPermission() {
initializeCamera();
}
void initializeCamera() {
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
camera = Camera.open();
/* Set Auto focus */
Camera.Parameters parameters = camera.getParameters();
List<String> focusModes = parameters.getSupportedFocusModes();
if(focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)){
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else
if(focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)){
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
}
camera.setParameters(parameters);
try {
camera.setPreviewTexture(surface);
} catch (IOException io) {
LogUtils.LOGD(TAG, io.getMessage());
}
camera.startPreview();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
try {
camera.stopPreview();
camera.release();
} catch (Exception e) {
LogUtils.LOGD(TAG, e.getMessage());
}
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
});
barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
@Override
public void release() {
}
@Override
public void receiveDetections(Detector.Detections<Barcode> detections) {
final SparseArray<Barcode> barcodes = detections.getDetectedItems();
if (barcodes.size() != 0) {
LogUtils.LOGD(TAG, barcodes.valueAt(0).displayValue);
}
}
});
}
@OnShowRationale(Manifest.permission.CAMERA)
void showRationaleForCamera(final PermissionRequest request) {
new AlertDialog.Builder(this)
.setMessage(R.string.permission_camera_rationale)
.setPositiveButton(R.string.button_allow, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
request.proceed();
}
})
.setNegativeButton(R.string.button_deny, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
request.cancel();
}
})
.show();
}
@OnPermissionDenied(Manifest.permission.CAMERA)
void showPermissionDeniedForCamera() {
Toast.makeText(this, R.string.permission_camera_denied, Toast.LENGTH_SHORT).show();
}
@OnNeverAskAgain(Manifest.permission.CAMERA)
void showNeverAskAgainForCamera() {
Toast.makeText(this, R.string.permission_camera_never_ask_again, Toast.LENGTH_SHORT).show();
}
}
The BarcodeReader Vision example uses SurfaceView by defualt for a very simple reason: Compatibility. SurfaceView is available from API Level 1, but TextureView is available from API Level 14.
Luckily, it is possible to create a BarcodeReader that supports both SurfaceView and TextureView, without losing compatibility at all.
I do not remember where exactly, but Google has created a Class based on TextureView that enhances its behabiour preventing stretched images. It is called "AutoFitTextureView" and I have made it for you:
public class AutoFitTextureView extends TextureView {
private int mRatioWidth = 0;
private int mRatioHeight = 0;
public AutoFitTextureView(Context context) {
this(context, null);
}
public AutoFitTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
* calculated from the parameters. Note that the actual sizes of parameters don't matter, that
* is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
*
* @param width Relative horizontal size
* @param height Relative vertical size
*/
public void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
}
Now you can use that new Class instead of SurfaceView on CameraSourcePreview class:
Take a look at the commented lines that were for SurfaceView
public class CameraSourcePreview extends ViewGroup {
private static final String TAG = "CameraSourcePreview";
private Context mContext;
//private SurfaceView mSurfaceView;
private AutoFitTextureView mAutoFitTextureView;
private boolean mStartRequested;
private boolean mSurfaceAvailable;
private CameraSource mCameraSource;
private GraphicOverlay mOverlay;
public CameraSourcePreview(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mStartRequested = false;
mSurfaceAvailable = false;
//mSurfaceView = new SurfaceView(context);
//mSurfaceView.getHolder().addCallback(new SurfaceCallback());
//addView(mSurfaceView);
mAutoFitTextureView = new AutoFitTextureView(context);
mAutoFitTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
addView(mAutoFitTextureView);
}
@RequiresPermission(Manifest.permission.CAMERA)
public void start(CameraSource cameraSource) throws IOException, SecurityException {
if (cameraSource == null) {
stop();
}
mCameraSource = cameraSource;
if (mCameraSource != null) {
mStartRequested = true;
startIfReady();
}
}
@RequiresPermission(Manifest.permission.CAMERA)
public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException {
mOverlay = overlay;
start(cameraSource);
}
public void stop() {
if (mCameraSource != null) {
mCameraSource.stop();
}
}
public void release() {
if (mCameraSource != null) {
mCameraSource.release();
mCameraSource = null;
}
}
@RequiresPermission(Manifest.permission.CAMERA)
private void startIfReady() throws IOException, SecurityException {
if (mStartRequested && mSurfaceAvailable) {
//mCameraSource.start(mSurfaceView.getHolder());
mCameraSource.start(mAutoFitTextureView);
if (mOverlay != null) {
Size size = mCameraSource.getPreviewSize();
int min = Math.min(size.getWidth(), size.getHeight());
int max = Math.max(size.getWidth(), size.getHeight());
if (isPortraitMode()) {
// Swap width and height sizes when in portrait, since it will be rotated by
// 90 degrees
mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing());
} else {
mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing());
}
mOverlay.clear();
}
mStartRequested = false;
}
}
private final TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
mSurfaceAvailable = true;
mOverlay.bringToFront();
try {startIfReady();} catch (IOException e) {Log.e(TAG, "Could not start camera source.", e);}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
mSurfaceAvailable = false;
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture texture) {}
};
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = 320;
int height = 240;
if (mCameraSource != null) {
Size size = mCameraSource.getPreviewSize();
if (size != null) {
width = size.getWidth();
height = size.getHeight();
}
}
// Swap width and height sizes when in portrait, since it will be rotated 90 degrees
if (isPortraitMode()) {
int tmp = width;
//noinspection SuspiciousNameCombination
width = height;
height = tmp;
}
final int layoutWidth = right - left;
final int layoutHeight = bottom - top;
// Computes height and width for potentially doing fit width.
int childWidth = layoutWidth;
int childHeight = (int)(((float) layoutWidth / (float) width) * height);
// If height is too tall using fit width, does fit height instead.
if (childHeight > layoutHeight) {
childHeight = layoutHeight;
childWidth = (int)(((float) layoutHeight / (float) height) * width);
}
for (int i = 0; i < getChildCount(); ++i) {
getChildAt(i).layout(0, 0, childWidth, childHeight);
}
try {
startIfReady();
} catch (SecurityException se) {
Log.e(TAG,"Do not have permission to start the camera", se);
} catch (IOException e) {
Log.e(TAG, "Could not start camera source.", e);
}
}
private boolean isPortraitMode() {
int orientation = mContext.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return false;
}
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return true;
}
Log.d(TAG, "isPortraitMode returning false by default");
return false;
}
}
Finally, you should add a new start method to the CameraSource class:
notice that this new method is called from CameraSourcePreview class
public CameraSource start(AutoFitTextureView textureView) throws IOException {
synchronized (mCameraLock) {
if(mCamera != null) {
return this;
}
mCamera = createCamera();
mCamera.setPreviewTexture(textureView.getSurfaceTexture());
mCamera.startPreview();
mProcessingThread = new Thread(mFrameProcessor);
mFrameProcessor.setActive(true);
mProcessingThread.start();
}
return this;
}
Now you have your own BarcodeReader with your TextureView. I have tested all the code and working on S4 Lollipop and Nexus5 Marshmallow.
Hope helping you!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With