/*
UVCDevice.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.uvc.core;
import android.hardware.usb.UsbDevice;
import android.view.Surface;
import android.view.TextureView;
import com.serenegiant.usb.IFrameCallback;
import com.serenegiant.usb.IPreviewFrameCallback;
import com.serenegiant.usb.Size;
import com.serenegiant.usb.USBMonitor;
import com.serenegiant.usb.UVCCamera;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
public class UVCDevice {
private static final double DEFAULT_MAX_FPS = 30.0d;
private static final int VS_FORMAT_MJPEG = 0x06;
private static final int[] SUPPORTED_PAYLOAD_FORMATS = {
VS_FORMAT_MJPEG
};
private final Logger mLogger = Logger.getLogger("uvc.dplugin");
private final UsbDevice mDevice;
private final UVCDeviceManager mDeviceMgr;
private USBMonitor.UsbControlBlock mCtrlBlock;
private UVCCamera mCamera;
private final String mId;
private final List<PreviewListener> mPreviewListeners = new ArrayList<PreviewListener>();
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
final Object mLockPermission = new Object();
private boolean mIsPermitted;
private boolean mIsInitialized;
private boolean mIsOpen;
private boolean mHasStartedPreview;
private PreviewOption mCurrentOption;
private long mMinFrameInterval;
private double mMaxFps;
private long mLastFrameTime = -1;
private PendingPermissionRequest mPermissionRequest;
UVCDevice(final UsbDevice device, final UVCDeviceManager deviceMgr) {
mDevice = device;
mId = Integer.toString(device.getDeviceId());
mDeviceMgr = deviceMgr;
setPreviewFrameRate(DEFAULT_MAX_FPS);
}
public String getId() {
return mId;
}
public String getName() {
return mDevice.getDeviceName();
}
public int getVendorId() {
return mDevice.getVendorId();
}
public int getProductId() {
return mDevice.getProductId();
}
public boolean isOpen() {
return mIsOpen;
}
public boolean isInitialized() {
return mIsInitialized;
}
public boolean canPreview() {
return mCurrentOption != null;
}
public boolean hasStartedPreview() {
return mHasStartedPreview;
}
boolean isSameDevice(final UsbDevice usbDevice) {
return usbDevice.getDeviceName().equals(mDevice.getDeviceName());
}
void requestPermission() throws InterruptedException {
synchronized (mLockPermission) {
if (mPermissionRequest == null) {
mPermissionRequest = new PendingPermissionRequest();
}
mDeviceMgr.requestPermission(mDevice);
while (mPermissionRequest != null
&& mPermissionRequest.isWaitingResult()) {
mLockPermission.wait(100);
}
mIsPermitted = mPermissionRequest.isPermitted();
mLogger.info("requestPermission: isPermitted = " + mIsPermitted);
mPermissionRequest = null;
}
}
void notifyPermission(final USBMonitor.UsbControlBlock ctrlBlock) {
mLogger.info("notifyPermission: ctrlBlock = " + ctrlBlock);
synchronized (mLockPermission) {
if (mPermissionRequest != null) {
mPermissionRequest.setResult(ctrlBlock != null);
mCtrlBlock = ctrlBlock;
}
mLockPermission.notifyAll();
}
}
synchronized boolean connect() {
if (mIsInitialized) {
return true;
}
if (!mIsPermitted) {
try {
mLogger.info("Requesting permission...");
requestPermission();
mLogger.info("Received the response for permission request: result = " + mIsPermitted);
} catch (InterruptedException e) {
return false;
}
}
if (!open()) { // Check supported video formats.
return false;
}
mLogger.info("UVC device: name = " + getName() + ", supported format = " + mCamera.getSupportedSize());
mIsInitialized = true;
return true;
}
private boolean open() {
if (mIsOpen) {
return true;
}
if (!mIsPermitted) {
return false;
}
mCamera = new UVCCamera();
mCamera.open(mCtrlBlock);
mIsOpen = true;
List<Size> previewSizeList = mCamera.getSupportedSizeList();
mLogger.info("Supported preview sizes: " + previewSizeList.size());
Size size = selectSize(previewSizeList);
if (size == null) {
mLogger.warning("Preview size fof supported format (MJPEG or YUY2) is not found.");
return false;
}
mLogger.info("Selected Preview size: type = " + size.type + ", width = " + size.width + ", height = " + size.height);
if (mCurrentOption == null) {
mCurrentOption = new PreviewOption(size.width, size.height);
}
final int width = mCurrentOption.getWidth();
final int height = mCurrentOption.getHeight();
final int frameFormat = UVCCamera.FRAME_FORMAT_MJPEG;
final int pixelFormat = UVCCamera.PIXEL_FORMAT_RAW;
if (!setPreviewSize(width, height)) {
mIsOpen = false;
mCurrentOption = null;
return false;
}
mCamera.setPreviewFrameCallback(new IPreviewFrameCallback() {
@Override
public void onFrame(final byte[] frame) {
if (checkFrameInterval()) {
return;
}
notifyPreviewFrame(frame, frameFormat, width, height);
}
}, pixelFormat);
return true;
}
private boolean supportsFormat(final Size previewSize) {
for (int format : SUPPORTED_PAYLOAD_FORMATS) {
if (previewSize.type == format) {
return true;
}
}
return false;
}
private Size selectSize(final List<Size> sizeList) {
if (sizeList.size() == 0) {
return null;
}
for (int format : SUPPORTED_PAYLOAD_FORMATS) {
Size size = selectSize(sizeList, format);
if (size != null) {
return size;
}
}
return null;
}
private Size selectSize(final List<Size> sizeList, final int format) {
List<Size> list = new ArrayList<>();
int i = 0;
for (Size size : sizeList) {
if (size.type == format) {
list.add(size);
}
}
if (list.size() == 0) {
return null;
}
Collections.sort(list, new Comparator<Size>() {
@Override
public int compare(final Size s1, final Size s2) {
return s2.width * s2.height - s1.width * s1.height;
}
});
return list.get(0);
}
private boolean checkFrameInterval() {
if (mMinFrameInterval <= 0) {
return false;
}
long currentFrameTime = System.currentTimeMillis();
if (mLastFrameTime < 0 || currentFrameTime - mLastFrameTime >= mMinFrameInterval) {
mLastFrameTime = currentFrameTime;
return false;
}
return true;
}
private void notifyPreviewFrame(final byte[] frame, final int frameFormat,
final int width, final int height) {
synchronized (mPreviewListeners) {
for (Iterator<PreviewListener> it = mPreviewListeners.iterator(); it.hasNext(); ) {
final PreviewListener l = it.next();
mExecutor.execute(new Runnable() {
@Override
public void run() {
l.onFrame(UVCDevice.this, frame, frameFormat, width, height);
}
});
}
}
}
synchronized boolean disconnect() {
if (!mIsInitialized) {
return false;
}
if (!mIsOpen) {
return false;
}
stopPreview();
mCamera.close();
mCamera.destroy();
mCamera = null;
mCtrlBlock.close();
mCtrlBlock = null;
mCurrentOption = null;
mIsOpen = false;
mIsPermitted = false;
mIsInitialized = false;
return true;
}
void addPreviewListener(final PreviewListener listener) {
synchronized (mPreviewListeners) {
for (Iterator<PreviewListener> it = mPreviewListeners.iterator(); it.hasNext(); ) {
if (it.next() == listener) {
return;
}
}
mPreviewListeners.add(listener);
}
}
public boolean setPreviewSize(final int width, final int height) {
if (!isSupportedPreviewSize(width, height)) {
return false;
}
try {
if (mIsOpen) {
mCamera.setPreviewSize(width, height, UVCCamera.FRAME_FORMAT_MJPEG);
}
mCurrentOption = new PreviewOption(width, height);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public double getFrameRate() {
return mMaxFps;
}
public void setPreviewFrameRate(final double maxFrameRate) {
mMaxFps = maxFrameRate;
mMinFrameInterval = (long) (1000 / maxFrameRate);
}
public boolean setNearestPreviewSize(final int requestedWidth,
final int requestedHeight) {
PreviewOption option = getNearestPreviewSize(requestedWidth, requestedHeight);
return setPreviewSize(option.getWidth(), option.getHeight());
}
public PreviewOption getNearestPreviewSize(final int requestedWidth,
final int requestedHeight) {
List<PreviewOption> options = getPreviewOptions();
final float ratio = requestedWidth / requestedHeight;
final int area = requestedWidth * requestedHeight;
Collections.sort(options, new Comparator<PreviewOption>() {
@Override
public int compare(final PreviewOption op1, final PreviewOption op2) {
if (op1.getRatio() == op2.getRatio()) {
int d1 = Math.abs(area - op1.getWidth() * op2.getHeight());
int d2 = Math.abs(area - op2.getWidth() * op2.getHeight());
return d1 - d2;
} else {
float d1 = Math.abs(ratio - op1.getRatio());
float d2 = Math.abs(ratio - op2.getRatio());
return d1 > d2 ? 1 : d1 == d2 ? 0 : -1;
}
}
});
return options.get(0);
}
public void setPreviewDisplay(final TextureView display) {
Surface surface = new Surface(display.getSurfaceTexture());
mCamera.setPreviewDisplay(surface);
}
public void clearPreviewDisplay() {
mCamera.setPreviewDisplay((Surface) null);
}
private void clearPreviewListeners() {
synchronized (mPreviewListeners) {
mPreviewListeners.clear();
}
}
public synchronized boolean startPreview() {
if (!mIsOpen) {
mLogger.warning("UVCDevice.startPreview: device is not open. name = " + getName());
return false;
}
if (mHasStartedPreview) {
mLogger.info("UVCDevice.startPreview: preview is started already. name = " + getName());
return true;
}
mLogger.info("UVCDevice.startPreview: preview is starting... name = " + getName());
mCamera.startPreview();
mLogger.info("UVCDevice.startPreview: preview has started. name = " + getName());
mHasStartedPreview = true;
return true;
}
public synchronized boolean stopPreview() {
if (!mIsOpen) {
mLogger.warning("UVCDevice.stopPreview: device is not open. name = " + getName());
return false;
}
if (!mHasStartedPreview) {
mLogger.info("UVCDevice.stopPreview: preview is stopped already. name = " + getName());
return true;
}
mLogger.info("UVCDevice.stopPreview: preview is stopping... name = " + getName());
mCamera.stopPreview();
mLogger.info("UVCDevice.stopPreview: preview has stopped. name = " + getName());
mHasStartedPreview = false;
return true;
}
public int getPreviewWidth() {
return mCurrentOption.getWidth();
}
public int getPreviewHeight() {
return mCurrentOption.getHeight();
}
public synchronized List<PreviewOption> getPreviewOptions() {
if (!mIsOpen) {
return null;
}
List<PreviewOption> options = new ArrayList<PreviewOption>();
List<Size> supportedSizes = mCamera.getSupportedSizeList();
for (Size size : supportedSizes) {
if (!supportsFormat(size)) {
continue;
}
options.add(new PreviewOption(size));
}
return options;
}
private boolean isSupportedPreviewSize(final int width, final int height) {
List<Size> sizes = mCamera.getSupportedSizeList();
for (Size size : sizes) {
if (width == size.width && height == size.height) {
return true;
}
}
return false;
}
private static class PendingPermissionRequest {
Boolean mIsPermitted;
boolean isWaitingResult() {
return mIsPermitted == null;
}
void setResult(final boolean isPermitted) {
mIsPermitted = isPermitted;
}
boolean isPermitted() {
return mIsPermitted;
}
}
interface PreviewListener {
void onFrame(UVCDevice device, byte[] frame, int frameFormat, int width, int height);
}
public static class PreviewOption {
private final int mWidth;
private final int mHeight;
private PreviewOption(final int width, final int height) {
mWidth = width;
mHeight = height;
}
private PreviewOption(final Size size) {
this(size.width, size.height);
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
public float getRatio() {
return mWidth / mHeight;
}
}
}