1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.googlecode.android_scripting.facade.webcam; 18 19 import android.app.Service; 20 import android.graphics.ImageFormat; 21 import android.graphics.Rect; 22 import android.graphics.YuvImage; 23 import android.hardware.Camera; 24 import android.hardware.Camera.Parameters; 25 import android.hardware.Camera.PreviewCallback; 26 import android.hardware.Camera.Size; 27 import android.util.Base64; 28 import android.view.SurfaceHolder; 29 import android.view.SurfaceHolder.Callback; 30 import android.view.SurfaceView; 31 import android.view.WindowManager; 32 33 import com.googlecode.android_scripting.BaseApplication; 34 import com.googlecode.android_scripting.FutureActivityTaskExecutor; 35 import com.googlecode.android_scripting.Log; 36 import com.googlecode.android_scripting.SimpleServer.SimpleServerObserver; 37 import com.googlecode.android_scripting.SingleThreadExecutor; 38 import com.googlecode.android_scripting.facade.EventFacade; 39 import com.googlecode.android_scripting.facade.FacadeManager; 40 import com.googlecode.android_scripting.future.FutureActivityTask; 41 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 42 import com.googlecode.android_scripting.rpc.Rpc; 43 import com.googlecode.android_scripting.rpc.RpcDefault; 44 import com.googlecode.android_scripting.rpc.RpcOptional; 45 import com.googlecode.android_scripting.rpc.RpcParameter; 46 47 import java.io.ByteArrayOutputStream; 48 import java.io.File; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.OutputStream; 52 import java.net.InetSocketAddress; 53 import java.util.Collections; 54 import java.util.Comparator; 55 import java.util.HashMap; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.concurrent.CountDownLatch; 59 import java.util.concurrent.Executor; 60 61 /** 62 * Manages access to camera streaming. 63 * <br> 64 * <h3>Usage Notes</h3> 65 * <br><b>webCamStart</b> and <b>webCamStop</b> are used to start and stop an Mpeg stream on a 66 * given port. <b>webcamAdjustQuality</b> is used to ajust the quality of the streaming video. 67 * <br><b>cameraStartPreview</b> is used to get access to the camera preview screen. It will 68 * generate "preview" events as images become available. 69 * <br>The preview has two modes: data or file. If you pass a non-blank, writable file path to 70 * the <b>cameraStartPreview</b> it will store jpg images in that folder. 71 * It is up to the caller to clean up these files after the fact. If no file element is provided, 72 * the event will include the image data as a base64 encoded string. 73 * <h3>Event details</h3> 74 * <br>The data element of the preview event will be a map, with the following elements defined. 75 * <ul> 76 * <li><b>format</b> - currently always "jpeg" 77 * <li><b>width</b> - image width (in pixels) 78 * <li><b>height</b> - image height (in pixels) 79 * <li><b>quality</b> - JPEG quality. Number from 1-100 80 * <li><b>filename</b> - Name of file where image was saved. Only relevant if filepath defined. 81 * <li><b>error</b> - included if there was an IOException saving file, ie, disk full or path write 82 * protected. 83 * <li><b>encoding</b> - Data encoding. If filepath defined, will be "file" otherwise "base64" 84 * <li><b>data</b> - Base64 encoded image data. 85 * </ul> 86 * <br>Note that "filename", "error" and "data" are mutual exclusive. 87 * <br> 88 * <br>The webcam and preview modes use the same resources, so you can't use them both at the same 89 * time. Stop one mode before starting the other. 90 */ 91 public class WebCamFacade extends RpcReceiver { 92 93 private final Service mService; 94 private final Executor mJpegCompressionExecutor = new SingleThreadExecutor(); 95 private final ByteArrayOutputStream mJpegCompressionBuffer = new ByteArrayOutputStream(); 96 97 private volatile byte[] mJpegData; 98 99 private CountDownLatch mJpegDataReady; 100 private boolean mStreaming; 101 private int mPreviewHeight; 102 private int mPreviewWidth; 103 private int mJpegQuality; 104 105 private MjpegServer mJpegServer; 106 private FutureActivityTask<SurfaceHolder> mPreviewTask; 107 private Camera mCamera; 108 private Parameters mParameters; 109 private final EventFacade mEventFacade; 110 private boolean mPreview; 111 private File mDest; 112 113 private final PreviewCallback mPreviewCallback = new PreviewCallback() { 114 @Override 115 public void onPreviewFrame(final byte[] data, final Camera camera) { 116 mJpegCompressionExecutor.execute(new Runnable() { 117 @Override 118 public void run() { 119 mJpegData = compressYuvToJpeg(data); 120 mJpegDataReady.countDown(); 121 if (mStreaming) { 122 camera.setOneShotPreviewCallback(mPreviewCallback); 123 } 124 } 125 }); 126 } 127 }; 128 129 private final PreviewCallback mPreviewEvent = new PreviewCallback() { 130 @Override 131 public void onPreviewFrame(final byte[] data, final Camera camera) { 132 mJpegCompressionExecutor.execute(new Runnable() { 133 @Override 134 public void run() { 135 mJpegData = compressYuvToJpeg(data); 136 Map<String, Object> map = new HashMap<String, Object>(); 137 map.put("format", "jpeg"); 138 map.put("width", mPreviewWidth); 139 map.put("height", mPreviewHeight); 140 map.put("quality", mJpegQuality); 141 if (mDest != null) { 142 try { 143 File dest = File.createTempFile("prv", ".jpg", mDest); 144 OutputStream output = new FileOutputStream(dest); 145 output.write(mJpegData); 146 output.close(); 147 map.put("encoding", "file"); 148 map.put("filename", dest.toString()); 149 } catch (IOException e) { 150 map.put("error", e.toString()); 151 } 152 } else { 153 map.put("encoding", "Base64"); 154 map.put("data", Base64.encodeToString(mJpegData, Base64.DEFAULT)); 155 } 156 mEventFacade.postEvent("preview", map); 157 if (mPreview) { 158 camera.setOneShotPreviewCallback(mPreviewEvent); 159 } 160 } 161 }); 162 } 163 }; 164 WebCamFacade(FacadeManager manager)165 public WebCamFacade(FacadeManager manager) { 166 super(manager); 167 mService = manager.getService(); 168 mJpegDataReady = new CountDownLatch(1); 169 mEventFacade = manager.getReceiver(EventFacade.class); 170 } 171 compressYuvToJpeg(final byte[] yuvData)172 private byte[] compressYuvToJpeg(final byte[] yuvData) { 173 mJpegCompressionBuffer.reset(); 174 YuvImage yuvImage = 175 new YuvImage(yuvData, ImageFormat.NV21, mPreviewWidth, mPreviewHeight, null); 176 yuvImage.compressToJpeg(new Rect(0, 0, mPreviewWidth, mPreviewHeight), mJpegQuality, 177 mJpegCompressionBuffer); 178 return mJpegCompressionBuffer.toByteArray(); 179 } 180 181 /** 182 * Starts an MJPEG stream and returns a Tuple of address and port for the stream. 183 * @param resolutionLevel increasing this number provides higher resolution 184 * @param jpegQuality a number from 0-100 185 * @param port If port is specified, the webcam service will bind to port, otherwise it will 186 * pick any available port. 187 * @return a Tuple of address and port for the stream 188 * @throws Exception upon failure to open the webcam or start the server 189 */ 190 @Rpc(description = "Starts an MJPEG stream and returns a Tuple of address and port " 191 + "for the stream.") webcamStart( @pcParametername = "resolutionLevel", description = "increasing this number provides higher resolution") @pcDefault"0") Integer resolutionLevel, @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality, @RpcParameter(name = "port", description = "If port is specified, the webcam service will bind to " + "port, otherwise it will pick any available port.") @RpcDefault("0") Integer port)192 public InetSocketAddress webcamStart( 193 @RpcParameter(name = "resolutionLevel", 194 description = "increasing this number provides higher resolution") 195 @RpcDefault("0") 196 Integer resolutionLevel, 197 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") 198 @RpcDefault("20") 199 Integer jpegQuality, 200 @RpcParameter(name = "port", 201 description = "If port is specified, the webcam service will bind to " 202 + "port, otherwise it will pick any available port.") 203 @RpcDefault("0") 204 Integer port) 205 throws Exception { 206 try { 207 openCamera(resolutionLevel, jpegQuality); 208 return startServer(port); 209 } catch (Exception e) { 210 webcamStop(); 211 throw e; 212 } 213 } 214 startServer(Integer port)215 private InetSocketAddress startServer(Integer port) { 216 mJpegServer = new MjpegServer(new JpegProvider() { 217 @Override 218 public byte[] getJpeg() { 219 try { 220 mJpegDataReady.await(); 221 } catch (InterruptedException e) { 222 Log.e(e); 223 } 224 return mJpegData; 225 } 226 }); 227 mJpegServer.addObserver(new SimpleServerObserver() { 228 @Override 229 public void onDisconnect() { 230 if (mJpegServer.getNumberOfConnections() == 0 && mStreaming) { 231 stopStream(); 232 } 233 } 234 235 @Override 236 public void onConnect() { 237 if (!mStreaming) { 238 startStream(); 239 } 240 } 241 }); 242 return mJpegServer.startPublic(port); 243 } 244 stopServer()245 private void stopServer() { 246 if (mJpegServer != null) { 247 mJpegServer.shutdown(); 248 mJpegServer = null; 249 } 250 } 251 252 /** 253 * Adjusts the quality of the webcam stream while it is running. 254 * @param resolutionLevel increasing this number provides higher resolution 255 * @param jpegQuality a number from 0-100 256 * @throws Exception if the webcam is not streaming or the camera is unable to open 257 */ 258 @Rpc(description = "Adjusts the quality of the webcam stream while it is running.") webcamAdjustQuality( @pcParametername = "resolutionLevel", description = "increasing this number provides higher resolution") @pcDefault"0") Integer resolutionLevel, @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality)259 public void webcamAdjustQuality( 260 @RpcParameter(name = "resolutionLevel", 261 description = "increasing this number provides higher resolution") 262 @RpcDefault("0") 263 Integer resolutionLevel, 264 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") 265 @RpcDefault("20") 266 Integer jpegQuality) 267 throws Exception { 268 if (!mStreaming) { 269 throw new IllegalStateException("Webcam not streaming."); 270 } 271 stopStream(); 272 releaseCamera(); 273 openCamera(resolutionLevel, jpegQuality); 274 startStream(); 275 } 276 openCamera(Integer resolutionLevel, Integer jpegQuality)277 private void openCamera(Integer resolutionLevel, Integer jpegQuality) throws IOException, 278 InterruptedException { 279 mCamera = Camera.open(); 280 mParameters = mCamera.getParameters(); 281 mParameters.setPictureFormat(ImageFormat.JPEG); 282 mParameters.setPreviewFormat(ImageFormat.JPEG); 283 List<Size> supportedPreviewSizes = mParameters.getSupportedPreviewSizes(); 284 Collections.sort(supportedPreviewSizes, new Comparator<Size>() { 285 @Override 286 public int compare(Size o1, Size o2) { 287 return o1.width - o2.width; 288 } 289 }); 290 Size previewSize = 291 supportedPreviewSizes.get( 292 Math.min(resolutionLevel, supportedPreviewSizes.size() - 1)); 293 mPreviewHeight = previewSize.height; 294 mPreviewWidth = previewSize.width; 295 mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 296 mJpegQuality = Math.min(Math.max(jpegQuality, 0), 100); 297 mCamera.setParameters(mParameters); 298 // TODO(damonkohler): Rotate image based on orientation. 299 mPreviewTask = createPreviewTask(); 300 mCamera.startPreview(); 301 } 302 startStream()303 private void startStream() { 304 mStreaming = true; 305 mCamera.setOneShotPreviewCallback(mPreviewCallback); 306 } 307 stopStream()308 private void stopStream() { 309 mJpegDataReady = new CountDownLatch(1); 310 mStreaming = false; 311 if (mPreviewTask != null) { 312 mPreviewTask.finish(); 313 mPreviewTask = null; 314 } 315 } 316 releaseCamera()317 private void releaseCamera() { 318 if (mCamera != null) { 319 mCamera.release(); 320 mCamera = null; 321 } 322 mParameters = null; 323 } 324 325 /** Stops the webcam stream. */ 326 @Rpc(description = "Stops the webcam stream.") webcamStop()327 public void webcamStop() { 328 stopServer(); 329 stopStream(); 330 releaseCamera(); 331 } 332 createPreviewTask()333 private FutureActivityTask<SurfaceHolder> createPreviewTask() throws IOException, 334 InterruptedException { 335 FutureActivityTask<SurfaceHolder> task = new FutureActivityTask<SurfaceHolder>() { 336 @Override 337 public void onCreate() { 338 super.onCreate(); 339 final SurfaceView view = new SurfaceView(getActivity()); 340 getActivity().setContentView(view); 341 getActivity().getWindow().setSoftInputMode( 342 WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); 343 //view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 344 view.getHolder().addCallback(new Callback() { 345 @Override 346 public void surfaceDestroyed(SurfaceHolder holder) { 347 } 348 349 @Override 350 public void surfaceCreated(SurfaceHolder holder) { 351 setResult(view.getHolder()); 352 } 353 354 @Override 355 public void surfaceChanged(SurfaceHolder holder, 356 int format, 357 int width, 358 int height) { 359 } 360 }); 361 } 362 }; 363 FutureActivityTaskExecutor taskExecutor = 364 ((BaseApplication) mService.getApplication()).getTaskExecutor(); 365 taskExecutor.execute(task); 366 mCamera.setPreviewDisplay(task.getResult()); 367 return task; 368 } 369 370 /** 371 * Start Preview Mode. Throws 'preview' events. 372 * @param resolutionLevel increasing this number provides higher resolution 373 * @param jpegQuality a number from 0-100 374 * @param filepath the path to store jpeg files 375 * @return a Tuple of address and port for the stream 376 * @throws InterruptedException if interrupted while trying to open the camera 377 */ 378 @Rpc(description = "Start Preview Mode. Throws 'preview' events.", 379 returns = "True if successful") cameraStartPreview( @pcParametername = "resolutionLevel", description = "increasing this number provides higher resolution") @pcDefault"0") Integer resolutionLevel, @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality, @RpcParameter(name = "filepath", description = "Path to store jpeg files.") @RpcOptional String filepath)380 public boolean cameraStartPreview( 381 @RpcParameter(name = "resolutionLevel", 382 description = "increasing this number provides higher resolution") 383 @RpcDefault("0") 384 Integer resolutionLevel, 385 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") 386 @RpcDefault("20") 387 Integer jpegQuality, 388 @RpcParameter(name = "filepath", description = "Path to store jpeg files.") 389 @RpcOptional 390 String filepath) 391 throws InterruptedException { 392 mDest = null; 393 if (filepath != null && (filepath.length() > 0)) { 394 mDest = new File(filepath); 395 if (!mDest.exists()) mDest.mkdirs(); 396 if (!(mDest.isDirectory() && mDest.canWrite())) { 397 return false; 398 } 399 } 400 401 try { 402 openCamera(resolutionLevel, jpegQuality); 403 } catch (IOException e) { 404 Log.e(e); 405 return false; 406 } 407 startPreview(); 408 return true; 409 } 410 411 /** Stops the preview mode. */ 412 @Rpc(description = "Stop the preview mode.") cameraStopPreview()413 public void cameraStopPreview() { 414 stopPreview(); 415 } 416 startPreview()417 private void startPreview() { 418 mPreview = true; 419 mCamera.setOneShotPreviewCallback(mPreviewEvent); 420 } 421 stopPreview()422 private void stopPreview() { 423 mPreview = false; 424 if (mPreviewTask != null) { 425 mPreviewTask.finish(); 426 mPreviewTask = null; 427 } 428 releaseCamera(); 429 } 430 431 @Override shutdown()432 public void shutdown() { 433 mPreview = false; 434 webcamStop(); 435 } 436 } 437