1 package org.visp.android; 2 3 import java.util.List; 4 5 import org.visp.R; 6 import org.visp.core.VpImageRGBa; 7 import org.visp.core.VpImageUChar; 8 9 import android.app.Activity; 10 import android.app.AlertDialog; 11 import android.content.Context; 12 import android.content.DialogInterface; 13 import android.content.res.TypedArray; 14 import android.graphics.Bitmap; 15 import android.graphics.Canvas; 16 import android.graphics.Rect; 17 import android.util.AttributeSet; 18 import android.util.Log; 19 import android.view.SurfaceHolder; 20 import android.view.SurfaceView; 21 22 /** 23 * This is a basic class, implementing the interaction with Camera and ViSP library. 24 * The main responsibility of it - is to control when camera can be enabled, process the frame, 25 * call external listener to make any adjustments to the frame and then draw the resulting 26 * frame to the screen. 27 * The clients shall implement VpCameraViewListener. 28 */ 29 public abstract class CameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback { 30 31 private static final String TAG = "CameraBridge"; 32 private static final int MAX_UNSPECIFIED = -1; 33 private static final int STOPPED = 0; 34 private static final int STARTED = 1; 35 36 private int mState = STOPPED; 37 private Bitmap mCacheBitmap; 38 private VpCameraViewListener2 mListener; 39 private boolean mSurfaceExist; 40 private final Object mSyncObject = new Object(); 41 42 protected int mFrameWidth; 43 protected int mFrameHeight; 44 protected int mMaxHeight; 45 protected int mMaxWidth; 46 protected float mScale = 0; 47 protected int mPreviewFormat = RGBA; 48 protected int mCameraIndex = CAMERA_ID_ANY; 49 protected boolean mEnabled; 50 protected FpsMeter mFpsMeter = null; 51 52 public static final int CAMERA_ID_ANY = -1; 53 public static final int CAMERA_ID_BACK = 99; 54 public static final int CAMERA_ID_FRONT = 98; 55 public static final int RGBA = 1; 56 public static final int GRAY = 2; 57 CameraBridgeViewBase(Context context, int cameraId)58 public CameraBridgeViewBase(Context context, int cameraId) { 59 super(context); 60 mCameraIndex = cameraId; 61 getHolder().addCallback(this); 62 mMaxWidth = MAX_UNSPECIFIED; 63 mMaxHeight = MAX_UNSPECIFIED; 64 } 65 CameraBridgeViewBase(Context context, AttributeSet attrs)66 public CameraBridgeViewBase(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 69 int count = attrs.getAttributeCount(); 70 Log.d(TAG, "Attr count: " + Integer.valueOf(count)); 71 72 TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase); 73 if (styledAttrs.getBoolean(R.styleable.CameraBridgeViewBase_show_fps, false)) 74 enableFpsMeter(); 75 76 mCameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1); 77 78 getHolder().addCallback(this); 79 mMaxWidth = MAX_UNSPECIFIED; 80 mMaxHeight = MAX_UNSPECIFIED; 81 styledAttrs.recycle(); 82 } 83 84 /** 85 * Sets the camera index 86 * @param cameraIndex new camera index 87 */ setCameraIndex(int cameraIndex)88 public void setCameraIndex(int cameraIndex) { 89 this.mCameraIndex = cameraIndex; 90 } 91 92 public interface VpCameraViewListener { 93 /** 94 * This method is invoked when camera preview has started. After this method is invoked 95 * the frames will start to be delivered to client via the onCameraFrame() callback. 96 * @param width - the width of the frames that will be delivered 97 * @param height - the height of the frames that will be delivered 98 */ onCameraViewStarted(int width, int height)99 public void onCameraViewStarted(int width, int height); 100 101 /** 102 * This method is invoked when camera preview has been stopped for some reason. 103 * No frames will be delivered via onCameraFrame() callback after this method is called. 104 */ onCameraViewStopped()105 public void onCameraViewStopped(); 106 107 /** 108 * This method is invoked when delivery of the frame needs to be done. 109 * The returned values - is a modified frame which needs to be displayed on the screen. 110 * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc) 111 */ onCameraFrame(VpImageUChar image)112 public VpImageUChar onCameraFrame(VpImageUChar image); onCameraFrame(VpImageRGBa image)113 public VpImageRGBa onCameraFrame(VpImageRGBa image); 114 } 115 116 public interface VpCameraViewListener2 { 117 /** 118 * This method is invoked when camera preview has started. After this method is invoked 119 * the frames will start to be delivered to client via the onCameraFrame() callback. 120 * @param width - the width of the frames that will be delivered 121 * @param height - the height of the frames that will be delivered 122 */ onCameraViewStarted(int width, int height)123 public void onCameraViewStarted(int width, int height); 124 125 /** 126 * This method is invoked when camera preview has been stopped for some reason. 127 * No frames will be delivered via onCameraFrame() callback after this method is called. 128 */ onCameraViewStopped()129 public void onCameraViewStopped(); 130 131 /** 132 * This method is invoked when delivery of the frame needs to be done. 133 * The returned values - is a modified frame which needs to be displayed on the screen. 134 * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc) 135 */ onCameraFrame(VpCameraViewFrame inputFrame)136 public long onCameraFrame(VpCameraViewFrame inputFrame); 137 }; 138 139 protected class VpCameraViewListenerAdapter implements VpCameraViewListener2 { VpCameraViewListenerAdapter(VpCameraViewListener oldStypeListener)140 public VpCameraViewListenerAdapter(VpCameraViewListener oldStypeListener) { 141 mOldStyleListener = oldStypeListener; 142 } 143 onCameraViewStarted(int width, int height)144 public void onCameraViewStarted(int width, int height) { 145 mOldStyleListener.onCameraViewStarted(width, height); 146 } 147 onCameraViewStopped()148 public void onCameraViewStopped() { 149 mOldStyleListener.onCameraViewStopped(); 150 } 151 onCameraFrame(VpCameraViewFrame inputFrame)152 public long onCameraFrame(VpCameraViewFrame inputFrame) { 153 long result = -1; 154 switch (mPreviewFormat) { 155 case RGBA: 156 result = mOldStyleListener.onCameraFrame(inputFrame.rgba()).nativeObj; 157 break; 158 case GRAY: 159 result = mOldStyleListener.onCameraFrame(inputFrame.gray()).nativeObj; 160 break; 161 default: 162 Log.e(TAG, "Invalid frame format! Only RGBA and Gray Scale are supported!"); 163 }; 164 165 return result; 166 } 167 setFrameFormat(int format)168 public void setFrameFormat(int format) { 169 mPreviewFormat = format; 170 } 171 172 private int mPreviewFormat = RGBA; 173 private VpCameraViewListener mOldStyleListener; 174 }; 175 176 /** 177 * This class interface is abstract representation of single frame from camera for onCameraFrame callback 178 * Attention: Do not use objects, that represents this interface out of onCameraFrame callback! 179 */ 180 public interface VpCameraViewFrame { 181 182 /** 183 * This method returns RGBA VpImageRGBa with frame 184 */ rgba()185 public VpImageRGBa rgba(); 186 187 /** 188 * This method returns single channel gray scale VpImageUChar with frame 189 */ gray()190 public VpImageUChar gray(); 191 }; 192 surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)193 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { 194 Log.d(TAG, "call surfaceChanged event"); 195 synchronized(mSyncObject) { 196 if (!mSurfaceExist) { 197 mSurfaceExist = true; 198 checkCurrentState(); 199 } else { 200 /** Surface changed. We need to stop camera and restart with new parameters */ 201 /* Pretend that old surface has been destroyed */ 202 mSurfaceExist = false; 203 checkCurrentState(); 204 /* Now use new surface. Say we have it now */ 205 mSurfaceExist = true; 206 checkCurrentState(); 207 } 208 } 209 } 210 surfaceCreated(SurfaceHolder holder)211 public void surfaceCreated(SurfaceHolder holder) { 212 /* Do nothing. Wait until surfaceChanged delivered */ 213 } 214 surfaceDestroyed(SurfaceHolder holder)215 public void surfaceDestroyed(SurfaceHolder holder) { 216 synchronized(mSyncObject) { 217 mSurfaceExist = false; 218 checkCurrentState(); 219 } 220 } 221 222 /** 223 * This method is provided for clients, so they can enable the camera connection. 224 * The actual onCameraViewStarted callback will be delivered only after both this method is called and surface is available 225 */ enableView()226 public void enableView() { 227 synchronized(mSyncObject) { 228 mEnabled = true; 229 checkCurrentState(); 230 } 231 } 232 233 /** 234 * This method is provided for clients, so they can disable camera connection and stop 235 * the delivery of frames even though the surface view itself is not destroyed and still stays on the scren 236 */ disableView()237 public void disableView() { 238 synchronized(mSyncObject) { 239 mEnabled = false; 240 checkCurrentState(); 241 } 242 } 243 244 /** 245 * This method enables label with fps value on the screen 246 */ enableFpsMeter()247 public void enableFpsMeter() { 248 if (mFpsMeter == null) { 249 mFpsMeter = new FpsMeter(); 250 mFpsMeter.setResolution(mFrameWidth, mFrameHeight); 251 } 252 } 253 disableFpsMeter()254 public void disableFpsMeter() { 255 mFpsMeter = null; 256 } 257 258 /** 259 * 260 * @param listener 261 */ 262 setVpCameraViewListener(VpCameraViewListener2 listener)263 public void setVpCameraViewListener(VpCameraViewListener2 listener) { 264 mListener = listener; 265 } 266 setVpCameraViewListener(VpCameraViewListener listener)267 public void setVpCameraViewListener(VpCameraViewListener listener) { 268 VpCameraViewListenerAdapter adapter = new VpCameraViewListenerAdapter(listener); 269 adapter.setFrameFormat(mPreviewFormat); 270 mListener = adapter; 271 } 272 273 /** 274 * This method sets the maximum size that camera frame is allowed to be. When selecting 275 * size - the biggest size which less or equal the size set will be selected. 276 * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The 277 * preview frame will be selected with 176x152 size. 278 * This method is useful when need to restrict the size of preview frame for some reason (for example for video recording) 279 * @param maxWidth - the maximum width allowed for camera frame. 280 * @param maxHeight - the maximum height allowed for camera frame 281 */ setMaxFrameSize(int maxWidth, int maxHeight)282 public void setMaxFrameSize(int maxWidth, int maxHeight) { 283 mMaxWidth = maxWidth; 284 mMaxHeight = maxHeight; 285 } 286 SetCaptureFormat(int format)287 public void SetCaptureFormat(int format) 288 { 289 mPreviewFormat = format; 290 if (mListener instanceof VpCameraViewListenerAdapter) { 291 VpCameraViewListenerAdapter adapter = (VpCameraViewListenerAdapter) mListener; 292 adapter.setFrameFormat(mPreviewFormat); 293 } 294 } 295 296 /** 297 * Called when mSyncObject lock is held 298 */ checkCurrentState()299 private void checkCurrentState() { 300 Log.d(TAG, "call checkCurrentState"); 301 int targetState; 302 303 if (mEnabled && mSurfaceExist && getVisibility() == VISIBLE) { 304 targetState = STARTED; 305 } else { 306 targetState = STOPPED; 307 } 308 309 if (targetState != mState) { 310 /* The state change detected. Need to exit the current state and enter target state */ 311 processExitState(mState); 312 mState = targetState; 313 processEnterState(mState); 314 } 315 } 316 processEnterState(int state)317 private void processEnterState(int state) { 318 Log.d(TAG, "call processEnterState: " + state); 319 switch(state) { 320 case STARTED: 321 onEnterStartedState(); 322 if (mListener != null) { 323 mListener.onCameraViewStarted(mFrameWidth, mFrameHeight); 324 } 325 break; 326 case STOPPED: 327 onEnterStoppedState(); 328 if (mListener != null) { 329 mListener.onCameraViewStopped(); 330 } 331 break; 332 }; 333 } 334 processExitState(int state)335 private void processExitState(int state) { 336 Log.d(TAG, "call processExitState: " + state); 337 switch(state) { 338 case STARTED: 339 onExitStartedState(); 340 break; 341 case STOPPED: 342 onExitStoppedState(); 343 break; 344 }; 345 } 346 onEnterStoppedState()347 private void onEnterStoppedState() { 348 /* nothing to do */ 349 } 350 onExitStoppedState()351 private void onExitStoppedState() { 352 /* nothing to do */ 353 } 354 355 // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x 356 // Bitmap must be constructed before surface onEnterStartedState()357 private void onEnterStartedState() { 358 Log.d(TAG, "call onEnterStartedState"); 359 /* Connect camera */ 360 if (!connectCamera(getWidth(), getHeight())) { 361 AlertDialog ad = new AlertDialog.Builder(getContext()).create(); 362 ad.setCancelable(false); // This blocks the 'BACK' button 363 ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed."); 364 ad.setButton(DialogInterface.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { 365 public void onClick(DialogInterface dialog, int which) { 366 dialog.dismiss(); 367 ((Activity) getContext()).finish(); 368 } 369 }); 370 ad.show(); 371 372 } 373 } 374 onExitStartedState()375 private void onExitStartedState() { 376 disconnectCamera(); 377 if (mCacheBitmap != null) { 378 mCacheBitmap.recycle(); 379 } 380 } 381 382 /** 383 * This method shall be called by the subclasses when they have valid 384 * object and want it to be delivered to external client (via callback) and 385 * then displayed on the screen. 386 * @param frame - the current frame to be delivered 387 */ deliverAndDrawFrame(VpCameraViewFrame frame)388 protected void deliverAndDrawFrame(VpCameraViewFrame frame) { 389 long modified = -1; 390 391 if (mListener != null) { 392 modified = mListener.onCameraFrame(frame); 393 } else { 394 modified = frame.rgba().nativeObj; 395 } 396 397 boolean bmpValid = true; 398 if (modified != -1) { 399 if (mPreviewFormat == RGBA){ 400 try { 401 Utils.vpImageUCharToBitmap(modified, mCacheBitmap); 402 } catch(Exception e) { 403 Log.e(TAG, "Image type: " + mPreviewFormat); 404 Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight()); 405 Log.e(TAG, "Utils.vpImageUCharToBitmap() throws an exception: " + e.getMessage()); 406 bmpValid = false; 407 } 408 } else if (mPreviewFormat == GRAY){ 409 try { 410 Utils.vpImageRGBaToBitmap(modified, mCacheBitmap); 411 } catch(Exception e) { 412 Log.e(TAG, "Image type: " + mPreviewFormat); 413 Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight()); 414 Log.e(TAG, "Utils.vpImageRGBaToBitmap() throws an exception: " + e.getMessage()); 415 bmpValid = false; 416 } 417 } 418 } 419 420 if (bmpValid && mCacheBitmap != null) { 421 Canvas canvas = getHolder().lockCanvas(); 422 if (canvas != null) { 423 canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR); 424 Log.d(TAG, "mStretch value: " + mScale); 425 426 if (mScale != 0) { 427 canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()), 428 new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2), 429 (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2), 430 (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()), 431 (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null); 432 } else { 433 canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()), 434 new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2, 435 (canvas.getHeight() - mCacheBitmap.getHeight()) / 2, 436 (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(), 437 (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null); 438 } 439 440 if (mFpsMeter != null) { 441 // Using default tick frequency for linux 442 // Refer: https://stackoverflow.com/questions/28292807/what-is-exactly-a-clock-tick-in-the-context-of-android-cpu-usage 443 mFpsMeter.measure(100,System.currentTimeMillis()*100); 444 mFpsMeter.draw(canvas, 20, 30); 445 } 446 getHolder().unlockCanvasAndPost(canvas); 447 } 448 } 449 } 450 451 /** 452 * This method is invoked shall perform concrete operation to initialize the camera. 453 * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be 454 * initialized with the size of the Camera frames that will be delivered to external processor. 455 * @param width - the width of this SurfaceView 456 * @param height - the height of this SurfaceView 457 */ connectCamera(int width, int height)458 protected abstract boolean connectCamera(int width, int height); 459 460 /** 461 * Disconnects and release the particular camera object being connected to this surface view. 462 * Called when syncObject lock is held 463 */ disconnectCamera()464 protected abstract void disconnectCamera(); 465 466 // NOTE: On Android 4.1.x the function must be called before SurfaceTexture constructor! AllocateCache()467 protected void AllocateCache() 468 { 469 mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888); 470 } 471 472 public interface ListItemAccessor { getWidth(Object obj)473 public int getWidth(Object obj); getHeight(Object obj)474 public int getHeight(Object obj); 475 }; 476 477 /** 478 * This helper method can be called by subclasses to select camera preview size. 479 * It goes over the list of the supported preview sizes and selects the maximum one which 480 * fits both values set via setMaxFrameSize() and surface frame allocated for this view 481 * @param supportedSizes 482 * @param surfaceWidth 483 * @param surfaceHeight 484 * @return optimal frame size 485 */ calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight)486 protected int[] calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) { 487 int calcWidth = 0; 488 int calcHeight = 0; 489 490 int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth; 491 int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight; 492 493 for (Object size : supportedSizes) { 494 int width = accessor.getWidth(size); 495 int height = accessor.getHeight(size); 496 497 if (width <= maxAllowedWidth && height <= maxAllowedHeight) { 498 if (width >= calcWidth && height >= calcHeight) { 499 calcWidth = (int) width; 500 calcHeight = (int) height; 501 } 502 } 503 } 504 505 int size[] = {calcWidth, calcHeight}; 506 return size; 507 } 508 } 509