Home | History | Annotate | Download | only in webcam
      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 
    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