• 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.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