/*
UVCMediaStreamRecordingProfile.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.profile;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import com.serenegiant.usb.UVCCamera;
import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice;
import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager;
import org.deviceconnect.android.deviceplugin.uvc.utils.MixedReplaceMediaServer;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.MediaStreamRecordingProfile;
import org.deviceconnect.android.profile.api.DConnectApi;
import org.deviceconnect.android.profile.api.DeleteApi;
import org.deviceconnect.android.profile.api.GetApi;
import org.deviceconnect.android.profile.api.PutApi;
import org.deviceconnect.message.DConnectMessage;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
/**
* UVC MediaStream Recording Profile.
*
* @author NTT DOCOMO, INC.
*/
public class UVCMediaStreamRecordingProfile extends MediaStreamRecordingProfile {
private static final String RECORDER_ID = "0";
private static final String RECORDER_MIME_TYPE_MJPEG = "video/x-mjpeg";
private static final String[] RECORDER_MIME_TYPE_LIST = {
RECORDER_MIME_TYPE_MJPEG
};
private static final String RECORDER_CONFIG = ""; // No config.
private final Logger mLogger = Logger.getLogger("uvc.dplugin");
private final UVCDeviceManager mDeviceMgr;
private final Map<String, PreviewContext> mContexts = new HashMap<String, PreviewContext>();
private final UVCDeviceManager.PreviewListener mPreviewListener
= new UVCDeviceManager.PreviewListener() {
@Override
public void onFrame(final UVCDevice device, final byte[] frame, final int frameFormat,
final int width, final int height) {
//mLogger.info("onFrame: " + frame.length);
if (frameFormat != UVCCamera.FRAME_FORMAT_MJPEG) {
mLogger.warning("onFrame: unsupported frame format: " + frameFormat);
return;
}
PreviewContext context = mContexts.get(device.getId());
if (context != null) {
final byte[] media = context.willResize() ? context.resize(frame) : frame;
context.mServer.offerMedia(media);
}
}
};
private final UVCDeviceManager.ConnectionListener mConnectionListener
= new UVCDeviceManager.ConnectionListener() {
@Override
public void onConnect(final UVCDevice device) {
// Nothing to do.
}
@Override
public void onConnectionFailed(final UVCDevice device) {
// Nothing to do.
}
@Override
public void onDisconnect(final UVCDevice device) {
stopMediaServer(device.getId());
}
};
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final DConnectApi mGetMediaRecorderApi = new GetApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_MEDIARECORDER;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
UVCDevice device = mDeviceMgr.getDevice(getServiceID(request));
if (device == null) {
MessageUtils.setNotFoundServiceError(response);
sendResponse(response);
return;
}
if (!device.isInitialized()) {
MessageUtils.setIllegalDeviceStateError(response,
"UVC device is not permitted by user: " + device.getName());
sendResponse(response);
return;
}
setMediaRecorders(response, device);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mGetOptionsApi = new GetApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_OPTIONS;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
UVCDevice device = mDeviceMgr.getDevice(getServiceID(request));
if (device == null) {
MessageUtils.setNotFoundServiceError(response);
sendResponse(response);
return;
}
String target = getTarget(request);
if (target != null && !RECORDER_ID.equals(target)) {
MessageUtils.setInvalidRequestParameterError(response,
"No such target: " + target);
sendResponse(response);
return;
}
if (!device.isOpen()) {
if (!mDeviceMgr.connectDevice(device)) {
MessageUtils.setIllegalDeviceStateError(response, "Failed to open UVC device: " + device.getId());
sendResponse(response);
return;
}
}
if (!device.canPreview()) {
MessageUtils.setNotSupportAttributeError(response, "UVC device does not support MJPEG format: " + device.getId());
sendResponse(response);
return;
}
setOptions(response, device);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mPutOptionsApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_OPTIONS;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
try {
String serviceId = getServiceID(request);
Integer imageWidth = getImageWidth(request);
Integer imageHeight = getImageHeight(request);
Integer previewWidth = getPreviewWidth(request);
Integer previewHeight = getPreviewHeight(request);
Double previewMaxFrameRate = getPreviewMaxFrameRate(request);
UVCDevice device = mDeviceMgr.getDevice(serviceId);
if (device == null) {
MessageUtils.setNotFoundServiceError(response);
return;
}
if (imageWidth != null && imageHeight == null) {
MessageUtils.setNotFoundServiceError(response,
"imageHeight must not be null if imageWidth is not null.");
return;
}
if (imageWidth == null && imageHeight != null) {
MessageUtils.setNotFoundServiceError(response,
"imageWidth must not be null if imageHeight is not null.");
return;
}
if (previewWidth != null && previewHeight == null) {
MessageUtils.setNotFoundServiceError(response,
"previewHeight must not be null if previewWidth is not null.");
return;
}
if (previewWidth == null && previewHeight != null) {
MessageUtils.setNotFoundServiceError(response,
"previewWidth must not be null if previewHeight is not null.");
return;
}
if (!device.isOpen()) {
if (!mDeviceMgr.connectDevice(device)) {
MessageUtils.setIllegalDeviceStateError(response, "Failed to open UVC device: " + device.getId());
return;
}
}
if (!device.canPreview()) {
MessageUtils.setNotSupportAttributeError(response, "UVC device does not support MJPEG format: " + device.getId());
return;
}
if (previewWidth != null && previewHeight != null) {
if (device.setPreviewSize(previewWidth, previewHeight)) {
setResult(response, DConnectMessage.RESULT_OK);
} else {
MessageUtils.setUnknownError(response, "Failed to change preview size: "
+ imageWidth + " x " + imageHeight);
return;
}
}
if (previewMaxFrameRate != null) {
device.setPreviewFrameRate(previewMaxFrameRate);
}
} finally {
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutPreviewApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_PREVIEW;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
try {
UVCDevice device = mDeviceMgr.getDevice(getServiceID(request));
if (device == null) {
MessageUtils.setNotFoundServiceError(response);
return;
}
if (!device.isOpen()) {
if (!mDeviceMgr.connectDevice(device)) {
MessageUtils.setIllegalDeviceStateError(response, "Failed to open UVC device: " + device.getId());
return;
}
}
if (!device.canPreview()) {
MessageUtils.setNotSupportAttributeError(response, "UVC device does not support MJPEG format: " + device.getId());
return;
}
if (device.startPreview()) {
PreviewContext context = startMediaServer(device.getId());
if (context.mServer.getUrl() == null) {
MessageUtils.setIllegalServerStateError(response, "Failed to start UVC preview server.");
return;
}
context.mWidth = device.getPreviewWidth();
context.mHeight = device.getPreviewHeight();
setResult(response, DConnectMessage.RESULT_OK);
setUri(response, context.mServer.getUrl());
} else {
MessageUtils.setIllegalDeviceStateError(response, "Failed to start the preview of UVC device: " + device.getId());
}
} finally {
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mDeletePreviewApi = new DeleteApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_PREVIEW;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
try {
UVCDevice device = mDeviceMgr.getDevice(getServiceID(request));
if (device == null) {
MessageUtils.setNotFoundServiceError(response);
return;
}
device.stopPreview();
stopMediaServer(device.getId());
setResult(response, DConnectMessage.RESULT_OK);
} finally {
sendResponse(response);
}
}
});
return false;
}
};
public UVCMediaStreamRecordingProfile(final UVCDeviceManager deviceMgr) {
mDeviceMgr = deviceMgr;
mDeviceMgr.addPreviewListener(mPreviewListener);
mDeviceMgr.addConnectionListener(mConnectionListener);
addApi(mGetMediaRecorderApi);
addApi(mGetOptionsApi);
addApi(mPutOptionsApi);
addApi(mPutPreviewApi);
addApi(mDeletePreviewApi);
}
private static void setMediaRecorders(final Intent response, final UVCDevice device) {
List<Bundle> recorderList = new ArrayList<Bundle>();
Bundle recorder = new Bundle();
setMediaRecorder(recorder, device);
recorderList.add(recorder);
setRecorders(response, recorderList);
}
private static void setMediaRecorder(final Bundle recorder, final UVCDevice device) {
setRecorderId(recorder, RECORDER_ID);
setRecorderName(recorder, device.getName());
setRecorderState(recorder, device.hasStartedPreview() ? RecorderState.RECORDING
: RecorderState.INACTIVE);
setRecorderPreviewWidth(recorder, device.getPreviewWidth());
setRecorderPreviewHeight(recorder, device.getPreviewHeight());
setRecorderPreviewMaxFrameRate(recorder, device.getFrameRate());
setRecorderMIMEType(recorder, RECORDER_MIME_TYPE_MJPEG);
setRecorderConfig(recorder, RECORDER_CONFIG);
}
private static void setOptions(final Intent response, final UVCDevice device) {
List<UVCDevice.PreviewOption> options = device.getPreviewOptions();
if (options != null && options.size() > 0) {
// previewSizes
List<Bundle> previewSizes = new ArrayList<Bundle>();
for (UVCDevice.PreviewOption option : options) {
Bundle size = new Bundle();
setWidth(size, option.getWidth());
setHeight(size, option.getHeight());
previewSizes.add(size);
}
setPreviewSizes(response, previewSizes);
}
// mimeType
setMIMEType(response, RECORDER_MIME_TYPE_LIST);
}
private synchronized PreviewContext startMediaServer(final String id) {
PreviewContext context = mContexts.get(id);
if (context == null) {
MixedReplaceMediaServer server = new MixedReplaceMediaServer();
server.setServerName("UVC Video Server");
server.setContentType("image/jpg");
server.start();
context = new PreviewContext(server);
mContexts.put(id, context);
}
return context;
}
public synchronized void stopMediaServer(final String id) {
PreviewContext context = mContexts.remove(id);
if (context != null) {
context.mServer.stop();
}
}
public synchronized void stopPreviewAllUVCDevice() {
List<UVCDevice> deviceList = mDeviceMgr.getDeviceList();
for (UVCDevice device : deviceList) {
device.stopPreview();
stopMediaServer(device.getId());
}
}
private static class PreviewContext {
Integer mWidth;
Integer mHeight;
final MixedReplaceMediaServer mServer;
final Logger mLogger = Logger.getLogger("uvc.dplugin");
PreviewContext(final MixedReplaceMediaServer server) {
if (server == null) {
throw new IllegalArgumentException();
}
mServer = server;
}
boolean willResize() {
return mWidth != null || mHeight != null;
}
byte[] resize(final byte[] frame) {
Bitmap src = BitmapFactory.decodeByteArray(frame, 0, frame.length);
if (src == null) {
mLogger.warning("MotionJPEG Frame could not be decoded to bitmap.");
return null;
}
int w = mWidth != null ? mWidth : src.getWidth();
int h = mHeight != null ? mHeight : src.getHeight();
Bitmap resizedBitmap = Bitmap.createScaledBitmap(src, w, h, true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] resizedBytes = baos.toByteArray();
resizedBitmap.recycle();
return resizedBytes;
}
}
}