Java程序  |  384行  |  13.33 KB

/*
 * Copyright (C) 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.googlecode.android_scripting.webcam;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;

import android.app.Service;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.util.Base64;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.view.SurfaceHolder.Callback;

import com.googlecode.android_scripting.BaseApplication;
import com.googlecode.android_scripting.FutureActivityTaskExecutor;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.SingleThreadExecutor;
import com.googlecode.android_scripting.SimpleServer.SimpleServerObserver;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.future.FutureActivityTask;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcDefault;
import com.googlecode.android_scripting.rpc.RpcOptional;
import com.googlecode.android_scripting.rpc.RpcParameter;

/**
 * Manages access to camera streaming.
 * <br>
 * <h3>Usage Notes</h3>
 * <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.
 * <br><b>cameraStartPreview</b> is used to get access to the camera preview screen. It will generate "preview" events as images become available.
 * <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.
 * It is up to the caller to clean up these files after the fact. If no file element is provided,
 * the event will include the image data as a base64 encoded string.
 * <h3>Event details</h3>
 * <br>The data element of the preview event will be a map, with the following elements defined.
 * <ul>
 * <li><b>format</b> - currently always "jpeg"
 * <li><b>width</b> - image width (in pixels)
 * <li><b>height</b> - image height (in pixels)
 * <li><b>quality</b> - JPEG quality. Number from 1-100
 * <li><b>filename</b> - Name of file where image was saved. Only relevant if filepath defined.
 * <li><b>error</b> - included if there was an IOException saving file, ie, disk full or path write protected.
 * <li><b>encoding</b> - Data encoding. If filepath defined, will be "file" otherwise "base64"
 * <li><b>data</b> - Base64 encoded image data.
 * </ul>
 *<br>Note that "filename", "error" and "data" are mutual exclusive.
 *<br>
 *<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.
 *
 *
 */
public class WebCamFacade extends RpcReceiver {

  private final Service mService;
  private final Executor mJpegCompressionExecutor = new SingleThreadExecutor();
  private final ByteArrayOutputStream mJpegCompressionBuffer = new ByteArrayOutputStream();

  private volatile byte[] mJpegData;

  private CountDownLatch mJpegDataReady;
  private boolean mStreaming;
  private int mPreviewHeight;
  private int mPreviewWidth;
  private int mJpegQuality;

  private MjpegServer mJpegServer;
  private FutureActivityTask<SurfaceHolder> mPreviewTask;
  private Camera mCamera;
  private Parameters mParameters;
  private final EventFacade mEventFacade;
  private boolean mPreview;
  private File mDest;

  private final PreviewCallback mPreviewCallback = new PreviewCallback() {
    @Override
    public void onPreviewFrame(final byte[] data, final Camera camera) {
      mJpegCompressionExecutor.execute(new Runnable() {
        @Override
        public void run() {
          mJpegData = compressYuvToJpeg(data);
          mJpegDataReady.countDown();
          if (mStreaming) {
            camera.setOneShotPreviewCallback(mPreviewCallback);
          }
        }
      });
    }
  };

  private final PreviewCallback mPreviewEvent = new PreviewCallback() {
    @Override
    public void onPreviewFrame(final byte[] data, final Camera camera) {
      mJpegCompressionExecutor.execute(new Runnable() {
        @Override
        public void run() {
          mJpegData = compressYuvToJpeg(data);
          Map<String,Object> map = new HashMap<String, Object>();
          map.put("format", "jpeg");
          map.put("width", mPreviewWidth);
          map.put("height", mPreviewHeight);
          map.put("quality", mJpegQuality);
          if (mDest!=null) {
            try {
              File dest=File.createTempFile("prv",".jpg",mDest);
              OutputStream output = new FileOutputStream(dest);
              output.write(mJpegData);
              output.close();
              map.put("encoding","file");
              map.put("filename",dest.toString());
            } catch (IOException e) {
              map.put("error", e.toString());
            }
          }
          else {
            map.put("encoding","Base64");
            map.put("data", Base64.encodeToString(mJpegData, Base64.DEFAULT));
          }
          mEventFacade.postEvent("preview", map);
          if (mPreview) {
            camera.setOneShotPreviewCallback(mPreviewEvent);
          }
        }
      });
    }
  };

  public WebCamFacade(FacadeManager manager) {
    super(manager);
    mService = manager.getService();
    mJpegDataReady = new CountDownLatch(1);
    mEventFacade = manager.getReceiver(EventFacade.class);
  }

  private byte[] compressYuvToJpeg(final byte[] yuvData) {
    mJpegCompressionBuffer.reset();
    YuvImage yuvImage =
        new YuvImage(yuvData, ImageFormat.NV21, mPreviewWidth, mPreviewHeight, null);
    yuvImage.compressToJpeg(new Rect(0, 0, mPreviewWidth, mPreviewHeight), mJpegQuality,
        mJpegCompressionBuffer);
    return mJpegCompressionBuffer.toByteArray();
  }

  @Rpc(description = "Starts an MJPEG stream and returns a Tuple of address and port for the stream.")
  public InetSocketAddress webcamStart(
      @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("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)
      throws Exception {
    try {
      openCamera(resolutionLevel, jpegQuality);
      return startServer(port);
    } catch (Exception e) {
      webcamStop();
      throw e;
    }
  }

  private InetSocketAddress startServer(Integer port) {
    mJpegServer = new MjpegServer(new JpegProvider() {
      @Override
      public byte[] getJpeg() {
        try {
          mJpegDataReady.await();
        } catch (InterruptedException e) {
          Log.e(e);
        }
        return mJpegData;
      }
    });
    mJpegServer.addObserver(new SimpleServerObserver() {
      @Override
      public void onDisconnect() {
        if (mJpegServer.getNumberOfConnections() == 0 && mStreaming) {
          stopStream();
        }
      }

      @Override
      public void onConnect() {
        if (!mStreaming) {
          startStream();
        }
      }
    });
    return mJpegServer.startPublic(port);
  }

  private void stopServer() {
    if (mJpegServer != null) {
      mJpegServer.shutdown();
      mJpegServer = null;
    }
  }

  @Rpc(description = "Adjusts the quality of the webcam stream while it is running.")
  public void webcamAdjustQuality(
      @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel,
      @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality)
      throws Exception {
    if (mStreaming == false) {
      throw new IllegalStateException("Webcam not streaming.");
    }
    stopStream();
    releaseCamera();
    openCamera(resolutionLevel, jpegQuality);
    startStream();
  }

  private void openCamera(Integer resolutionLevel, Integer jpegQuality) throws IOException,
      InterruptedException {
    mCamera = Camera.open();
    mParameters = mCamera.getParameters();
    mParameters.setPictureFormat(ImageFormat.JPEG);
    mParameters.setPreviewFormat(ImageFormat.JPEG);
    List<Size> supportedPreviewSizes = mParameters.getSupportedPreviewSizes();
    Collections.sort(supportedPreviewSizes, new Comparator<Size>() {
      @Override
      public int compare(Size o1, Size o2) {
        return o1.width - o2.width;
      }
    });
    Size previewSize =
        supportedPreviewSizes.get(Math.min(resolutionLevel, supportedPreviewSizes.size() - 1));
    mPreviewHeight = previewSize.height;
    mPreviewWidth = previewSize.width;
    mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
    mJpegQuality = Math.min(Math.max(jpegQuality, 0), 100);
    mCamera.setParameters(mParameters);
    // TODO(damonkohler): Rotate image based on orientation.
    mPreviewTask = createPreviewTask();
    mCamera.startPreview();
  }

  private void startStream() {
    mStreaming = true;
    mCamera.setOneShotPreviewCallback(mPreviewCallback);
  }

  private void stopStream() {
    mJpegDataReady = new CountDownLatch(1);
    mStreaming = false;
    if (mPreviewTask != null) {
      mPreviewTask.finish();
      mPreviewTask = null;
    }
  }

  private void releaseCamera() {
    if (mCamera != null) {
      mCamera.release();
      mCamera = null;
    }
    mParameters = null;
  }

  @Rpc(description = "Stops the webcam stream.")
  public void webcamStop() {
    stopServer();
    stopStream();
    releaseCamera();
  }

  private FutureActivityTask<SurfaceHolder> createPreviewTask() throws IOException,
      InterruptedException {
    FutureActivityTask<SurfaceHolder> task = new FutureActivityTask<SurfaceHolder>() {
      @Override
      public void onCreate() {
        super.onCreate();
        final SurfaceView view = new SurfaceView(getActivity());
        getActivity().setContentView(view);
        getActivity().getWindow().setSoftInputMode(
            WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
        //view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        view.getHolder().addCallback(new Callback() {
          @Override
          public void surfaceDestroyed(SurfaceHolder holder) {
          }

          @Override
          public void surfaceCreated(SurfaceHolder holder) {
            setResult(view.getHolder());
          }

          @Override
          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
          }
        });
      }
    };
    FutureActivityTaskExecutor taskExecutor =
        ((BaseApplication) mService.getApplication()).getTaskExecutor();
    taskExecutor.execute(task);
    mCamera.setPreviewDisplay(task.getResult());
    return task;
  }

  @Rpc(description = "Start Preview Mode. Throws 'preview' events.",returns="True if successful")
  public boolean cameraStartPreview(
          @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("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)
      throws InterruptedException {
    mDest=null;
    if (filepath!=null && (filepath.length()>0)) {
      mDest = new File(filepath);
      if (!mDest.exists()) mDest.mkdirs();
      if (!(mDest.isDirectory() && mDest.canWrite())) {
        return false;
      }
    }

    try {
      openCamera(resolutionLevel, jpegQuality);
    } catch (IOException e) {
      Log.e(e);
      return false;
    }
    startPreview();
    return true;
  }

  @Rpc(description = "Stop the preview mode.")
  public void cameraStopPreview() {
    stopPreview();
  }

  private void startPreview() {
    mPreview = true;
    mCamera.setOneShotPreviewCallback(mPreviewEvent);
  }

  private void stopPreview() {
    mPreview = false;
    if (mPreviewTask!=null)
    {
      mPreviewTask.finish();
      mPreviewTask=null;
    }
    releaseCamera();
  }

  @Override
  public void shutdown() {
    mPreview=false;
    webcamStop();
  }
}