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