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 * 87 */ 88 public class WebCamFacade extends RpcReceiver { 89 90 private final Service mService; 91 private final Executor mJpegCompressionExecutor = new SingleThreadExecutor(); 92 private final ByteArrayOutputStream mJpegCompressionBuffer = new ByteArrayOutputStream(); 93 94 private volatile byte[] mJpegData; 95 96 private CountDownLatch mJpegDataReady; 97 private boolean mStreaming; 98 private int mPreviewHeight; 99 private int mPreviewWidth; 100 private int mJpegQuality; 101 102 private MjpegServer mJpegServer; 103 private FutureActivityTask<SurfaceHolder> mPreviewTask; 104 private Camera mCamera; 105 private Parameters mParameters; 106 private final EventFacade mEventFacade; 107 private boolean mPreview; 108 private File mDest; 109 110 private final PreviewCallback mPreviewCallback = new PreviewCallback() { 111 @Override 112 public void onPreviewFrame(final byte[] data, final Camera camera) { 113 mJpegCompressionExecutor.execute(new Runnable() { 114 @Override 115 public void run() { 116 mJpegData = compressYuvToJpeg(data); 117 mJpegDataReady.countDown(); 118 if (mStreaming) { 119 camera.setOneShotPreviewCallback(mPreviewCallback); 120 } 121 } 122 }); 123 } 124 }; 125 126 private final PreviewCallback mPreviewEvent = new PreviewCallback() { 127 @Override 128 public void onPreviewFrame(final byte[] data, final Camera camera) { 129 mJpegCompressionExecutor.execute(new Runnable() { 130 @Override 131 public void run() { 132 mJpegData = compressYuvToJpeg(data); 133 Map<String,Object> map = new HashMap<String, Object>(); 134 map.put("format", "jpeg"); 135 map.put("width", mPreviewWidth); 136 map.put("height", mPreviewHeight); 137 map.put("quality", mJpegQuality); 138 if (mDest!=null) { 139 try { 140 File dest=File.createTempFile("prv",".jpg",mDest); 141 OutputStream output = new FileOutputStream(dest); 142 output.write(mJpegData); 143 output.close(); 144 map.put("encoding","file"); 145 map.put("filename",dest.toString()); 146 } catch (IOException e) { 147 map.put("error", e.toString()); 148 } 149 } 150 else { 151 map.put("encoding","Base64"); 152 map.put("data", Base64.encodeToString(mJpegData, Base64.DEFAULT)); 153 } 154 mEventFacade.postEvent("preview", map); 155 if (mPreview) { 156 camera.setOneShotPreviewCallback(mPreviewEvent); 157 } 158 } 159 }); 160 } 161 }; 162 163 public WebCamFacade(FacadeManager manager) { 164 super(manager); 165 mService = manager.getService(); 166 mJpegDataReady = new CountDownLatch(1); 167 mEventFacade = manager.getReceiver(EventFacade.class); 168 } 169 170 private byte[] compressYuvToJpeg(final byte[] yuvData) { 171 mJpegCompressionBuffer.reset(); 172 YuvImage yuvImage = 173 new YuvImage(yuvData, ImageFormat.NV21, mPreviewWidth, mPreviewHeight, null); 174 yuvImage.compressToJpeg(new Rect(0, 0, mPreviewWidth, mPreviewHeight), mJpegQuality, 175 mJpegCompressionBuffer); 176 return mJpegCompressionBuffer.toByteArray(); 177 } 178 179 @Rpc(description = "Starts an MJPEG stream and returns a Tuple of address and port for the stream.") 180 public InetSocketAddress webcamStart( 181 @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel, 182 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality, 183 @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) 184 throws Exception { 185 try { 186 openCamera(resolutionLevel, jpegQuality); 187 return startServer(port); 188 } catch (Exception e) { 189 webcamStop(); 190 throw e; 191 } 192 } 193 194 private InetSocketAddress startServer(Integer port) { 195 mJpegServer = new MjpegServer(new JpegProvider() { 196 @Override 197 public byte[] getJpeg() { 198 try { 199 mJpegDataReady.await(); 200 } catch (InterruptedException e) { 201 Log.e(e); 202 } 203 return mJpegData; 204 } 205 }); 206 mJpegServer.addObserver(new SimpleServerObserver() { 207 @Override 208 public void onDisconnect() { 209 if (mJpegServer.getNumberOfConnections() == 0 && mStreaming) { 210 stopStream(); 211 } 212 } 213 214 @Override 215 public void onConnect() { 216 if (!mStreaming) { 217 startStream(); 218 } 219 } 220 }); 221 return mJpegServer.startPublic(port); 222 } 223 224 private void stopServer() { 225 if (mJpegServer != null) { 226 mJpegServer.shutdown(); 227 mJpegServer = null; 228 } 229 } 230 231 @Rpc(description = "Adjusts the quality of the webcam stream while it is running.") 232 public void webcamAdjustQuality( 233 @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel, 234 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality) 235 throws Exception { 236 if (mStreaming == false) { 237 throw new IllegalStateException("Webcam not streaming."); 238 } 239 stopStream(); 240 releaseCamera(); 241 openCamera(resolutionLevel, jpegQuality); 242 startStream(); 243 } 244 245 private void openCamera(Integer resolutionLevel, Integer jpegQuality) throws IOException, 246 InterruptedException { 247 mCamera = Camera.open(); 248 mParameters = mCamera.getParameters(); 249 mParameters.setPictureFormat(ImageFormat.JPEG); 250 mParameters.setPreviewFormat(ImageFormat.JPEG); 251 List<Size> supportedPreviewSizes = mParameters.getSupportedPreviewSizes(); 252 Collections.sort(supportedPreviewSizes, new Comparator<Size>() { 253 @Override 254 public int compare(Size o1, Size o2) { 255 return o1.width - o2.width; 256 } 257 }); 258 Size previewSize = 259 supportedPreviewSizes.get(Math.min(resolutionLevel, supportedPreviewSizes.size() - 1)); 260 mPreviewHeight = previewSize.height; 261 mPreviewWidth = previewSize.width; 262 mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 263 mJpegQuality = Math.min(Math.max(jpegQuality, 0), 100); 264 mCamera.setParameters(mParameters); 265 // TODO(damonkohler): Rotate image based on orientation. 266 mPreviewTask = createPreviewTask(); 267 mCamera.startPreview(); 268 } 269 270 private void startStream() { 271 mStreaming = true; 272 mCamera.setOneShotPreviewCallback(mPreviewCallback); 273 } 274 275 private void stopStream() { 276 mJpegDataReady = new CountDownLatch(1); 277 mStreaming = false; 278 if (mPreviewTask != null) { 279 mPreviewTask.finish(); 280 mPreviewTask = null; 281 } 282 } 283 284 private void releaseCamera() { 285 if (mCamera != null) { 286 mCamera.release(); 287 mCamera = null; 288 } 289 mParameters = null; 290 } 291 292 @Rpc(description = "Stops the webcam stream.") 293 public void webcamStop() { 294 stopServer(); 295 stopStream(); 296 releaseCamera(); 297 } 298 299 private FutureActivityTask<SurfaceHolder> createPreviewTask() throws IOException, 300 InterruptedException { 301 FutureActivityTask<SurfaceHolder> task = new FutureActivityTask<SurfaceHolder>() { 302 @Override 303 public void onCreate() { 304 super.onCreate(); 305 final SurfaceView view = new SurfaceView(getActivity()); 306 getActivity().setContentView(view); 307 getActivity().getWindow().setSoftInputMode( 308 WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); 309 //view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 310 view.getHolder().addCallback(new Callback() { 311 @Override 312 public void surfaceDestroyed(SurfaceHolder holder) { 313 } 314 315 @Override 316 public void surfaceCreated(SurfaceHolder holder) { 317 setResult(view.getHolder()); 318 } 319 320 @Override 321 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 322 } 323 }); 324 } 325 }; 326 FutureActivityTaskExecutor taskExecutor = 327 ((BaseApplication) mService.getApplication()).getTaskExecutor(); 328 taskExecutor.execute(task); 329 mCamera.setPreviewDisplay(task.getResult()); 330 return task; 331 } 332 333 @Rpc(description = "Start Preview Mode. Throws 'preview' events.",returns="True if successful") 334 public boolean cameraStartPreview( 335 @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel, 336 @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality, 337 @RpcParameter(name = "filepath", description = "Path to store jpeg files.") @RpcOptional String filepath) 338 throws InterruptedException { 339 mDest=null; 340 if (filepath!=null && (filepath.length()>0)) { 341 mDest = new File(filepath); 342 if (!mDest.exists()) mDest.mkdirs(); 343 if (!(mDest.isDirectory() && mDest.canWrite())) { 344 return false; 345 } 346 } 347 348 try { 349 openCamera(resolutionLevel, jpegQuality); 350 } catch (IOException e) { 351 Log.e(e); 352 return false; 353 } 354 startPreview(); 355 return true; 356 } 357 358 @Rpc(description = "Stop the preview mode.") 359 public void cameraStopPreview() { 360 stopPreview(); 361 } 362 363 private void startPreview() { 364 mPreview = true; 365 mCamera.setOneShotPreviewCallback(mPreviewEvent); 366 } 367 368 private void stopPreview() { 369 mPreview = false; 370 if (mPreviewTask!=null) 371 { 372 mPreviewTask.finish(); 373 mPreviewTask=null; 374 } 375 releaseCamera(); 376 } 377 378 @Override 379 public void shutdown() { 380 mPreview=false; 381 webcamStop(); 382 } 383 } 384