• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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 
WebCamFacade(FacadeManager manager)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 
compressYuvToJpeg(final byte[] yuvData)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.")
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)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 
startServer(Integer port)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 
stopServer()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.")
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)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 
openCamera(Integer resolutionLevel, Integer jpegQuality)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 
startStream()270   private void startStream() {
271     mStreaming = true;
272     mCamera.setOneShotPreviewCallback(mPreviewCallback);
273   }
274 
stopStream()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 
releaseCamera()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.")
webcamStop()293   public void webcamStop() {
294     stopServer();
295     stopStream();
296     releaseCamera();
297   }
298 
createPreviewTask()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")
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)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.")
cameraStopPreview()359   public void cameraStopPreview() {
360     stopPreview();
361   }
362 
startPreview()363   private void startPreview() {
364     mPreview = true;
365     mCamera.setOneShotPreviewCallback(mPreviewEvent);
366   }
367 
stopPreview()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
shutdown()379   public void shutdown() {
380     mPreview=false;
381     webcamStop();
382   }
383 }
384