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