/*
ThetaMediaStreamRecordingProfile
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.theta.profile;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import org.deviceconnect.android.deviceplugin.theta.core.LiveCamera;
import org.deviceconnect.android.deviceplugin.theta.core.LivePreviewTask;
import org.deviceconnect.android.deviceplugin.theta.core.ThetaDevice;
import org.deviceconnect.android.deviceplugin.theta.core.ThetaDeviceClient;
import org.deviceconnect.android.deviceplugin.theta.core.ThetaDeviceException;
import org.deviceconnect.android.deviceplugin.theta.core.ThetaObject;
import org.deviceconnect.android.deviceplugin.theta.utils.BitmapUtils;
import org.deviceconnect.android.deviceplugin.theta.utils.MixedReplaceMediaServer;
import org.deviceconnect.android.event.Event;
import org.deviceconnect.android.event.EventError;
import org.deviceconnect.android.event.EventManager;
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.PostApi;
import org.deviceconnect.android.profile.api.PutApi;
import org.deviceconnect.android.provider.FileManager;
import org.deviceconnect.message.DConnectMessage;
import java.io.ByteArrayOutputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Theta MediaStream Recording Profile.
*
* @author NTT DOCOMO, INC.
*/
public abstract class ThetaMediaStreamRecordingProfile extends MediaStreamRecordingProfile {
private static final String PARAM_WIDTH = "width";
private static final String PARAM_HEIGHT = "height";
private static final String SEGMENT_LIVE_PREVIEW = "1";
private final ThetaDeviceClient mClient;
private final FileManager mFileMgr;
private final Object mLockObj = new Object();
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private LivePreviewTask mLivePreviewTask;
private MixedReplaceMediaServer mServer;
protected final DConnectApi mGetMediaRecorderApi = new GetApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_MEDIARECORDER;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = getServiceID(request);
mClient.fetchRecorder(serviceId, new ThetaDeviceClient.DefaultListener() {
@Override
public void onRecorder(final ThetaDevice.Recorder recorder) {
List<Bundle> recorders = new LinkedList<Bundle>();
if (recorder != null) {
Bundle r = new Bundle();
setRecorderId(r, recorder.getId());
setRecorderName(r, recorder.getName());
setRecorderImageWidth(r, recorder.getImageWidth());
setRecorderImageHeight(r, recorder.getImageHeight());
if (recorder.supportsPreview()) {
setRecorderPreviewWidth(r, recorder.getPreviewWidth());
setRecorderPreviewHeight(r, recorder.getPreviewHeight());
setRecorderPreviewMaxFrameRate(r, recorder.getPreviewMaxFrameRate());
}
setRecorderMIMEType(r, recorder.getMimeType());
setRecorderConfig(r, "");
try {
ThetaDevice.RecorderState state = recorder.getState();
switch (state) {
case RECORDING:
setRecorderState(r, RecorderState.RECORDING);
break;
case INACTIVE:
setRecorderState(r, RecorderState.INACTIVE);
break;
default:
break;
}
} catch (ThetaDeviceException e) {
onFailed(e);
return;
}
recorders.add(r);
}
setRecorders(response, recorders.toArray(new Bundle[recorders.size()]));
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
@Override
public void onFailed(final ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
sendResponse(response);
}
});
return false;
}
};
protected final DConnectApi mPostTakePhotoApi = new PostApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_TAKE_PHOTO;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String target = getTarget(request);
mClient.takePicture(serviceId, target, new ThetaDeviceClient.DefaultListener() {
@Override
public void onTakenPicture(final ThetaObject picture) {
try {
picture.fetch(ThetaObject.DataType.MAIN);
byte[] data = picture.getMainData();
picture.clear(ThetaObject.DataType.MAIN);
mFileMgr.saveFile(picture.getFileName(), data, true, new FileManager.SaveFileCallback() {
@Override
public void onSuccess(final String uri) {
String path = "/" + picture.getFileName();
setUri(response, uri);
setPath(response, path);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
sendOnPhotoEvent(serviceId, picture.getMimeType(), uri, path);
}
@Override
public void onFail(final Throwable e) {
MessageUtils.setUnknownError(response, e.getMessage());
sendResponse(response);
}
});
} catch (ThetaDeviceException e) {
onFailed(e);
}
}
@Override
public void onFailed(final ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
case ThetaDeviceException.NOT_FOUND_RECORDER:
MessageUtils.setInvalidRequestParameterError(response, cause.getMessage());
break;
case ThetaDeviceException.NOT_SUPPORTED_FEATURE:
MessageUtils.setNotSupportAttributeError(response, cause.getMessage());
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
sendResponse(response);
}
});
return false;
}
};
protected final DConnectApi mPutOnPhotoApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ON_PHOTO;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
EventError error = EventManager.INSTANCE.addEvent(request);
if (error == EventError.NONE) {
setResult(response, DConnectMessage.RESULT_OK);
} else if (error == EventError.INVALID_PARAMETER) {
MessageUtils.setInvalidRequestParameterError(response);
} else {
MessageUtils.setUnknownError(response);
}
return true;
}
};
protected final DConnectApi mDeleteOnPhotoApi = new DeleteApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ON_PHOTO;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
EventError error = EventManager.INSTANCE.removeEvent(request);
if (error == EventError.NONE) {
setResult(response, DConnectMessage.RESULT_OK);
} else if (error == EventError.INVALID_PARAMETER) {
MessageUtils.setInvalidRequestParameterError(response);
} else if (error == EventError.FAILED) {
MessageUtils.setUnknownError(response, "Failed to delete event from cache");
} else if (error == EventError.NOT_FOUND) {
MessageUtils.setUnknownError(response, "Not found event.");
} else {
MessageUtils.setUnknownError(response);
}
return true;
}
};
protected final DConnectApi mPostRecordApi = new PostApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_RECORD;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String target = getTarget(request);
Long timeslice = getTimeSlice(request);
if (timeslice != null && timeslice < 0) {
MessageUtils.setInvalidRequestParameterError(response,
"Invalid timeslice");
return true;
}
mClient.startVideoRecording(serviceId, target, new ThetaDeviceClient.DefaultListener() {
@Override
public void onStartedVideoRecording(final ThetaDevice.Recorder recorder,
final boolean hasStarted) {
if (hasStarted) {
MessageUtils.setIllegalDeviceStateError(response, "Video recording has started already.");
sendResponse(response);
} else {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
sendOnRecordingChangeEvent(serviceId, recorder, RecordingState.RECORDING);
}
}
@Override
public void onFailed(final ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
case ThetaDeviceException.NOT_FOUND_RECORDER:
MessageUtils.setInvalidRequestParameterError(response, "recorder is not found.");
break;
case ThetaDeviceException.NOT_SUPPORTED_FEATURE:
MessageUtils.setNotSupportAttributeError(response, cause.getMessage());
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
sendResponse(response);
}
});
return false;
}
};
protected final DConnectApi mPutStopApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_STOP;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String target = getTarget(request);
mClient.stopVideoRecording(serviceId, target, new ThetaDeviceClient.DefaultListener() {
@Override
public void onStoppedVideoRecording(final ThetaDevice.Recorder recorder,
final boolean hasStopped) {
if (hasStopped) {
MessageUtils.setIllegalDeviceStateError(response, "Video recording has stopped already.");
sendResponse(response);
} else {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
sendOnRecordingChangeEvent(serviceId, recorder, RecordingState.STOP);
}
}
@Override
public void onFailed(final ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
case ThetaDeviceException.NOT_FOUND_RECORDER:
MessageUtils.setInvalidRequestParameterError(response, "recorder is not found.");
break;
case ThetaDeviceException.NOT_SUPPORTED_FEATURE:
MessageUtils.setNotSupportAttributeError(response, cause.getMessage());
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
sendResponse(response);
}
});
return false;
}
};
protected final DConnectApi mPutPreviewApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_PREVIEW;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String target = getTarget(request);
mClient.execute(new Runnable() {
@Override
public void run() {
try {
ThetaDevice device = mClient.getConnectedDevice(serviceId);
ThetaDevice.Recorder recorder = device.getRecorder();
if (recorder == null) {
MessageUtils.setIllegalDeviceStateError(response, "device is not initialized.");
return;
}
if (target != null && !target.equals(recorder.getId())) {
MessageUtils.setInvalidRequestParameterError(response, "target is invalid.");
return;
}
if (!recorder.supportsPreview()) {
MessageUtils.setNotSupportAttributeError(response,
recorder.getName() + " does not support preview.");
return;
}
String uri = startLivePreview(device, getWidth(request), getHeight(request));
setUri(response, uri);
setResult(response, DConnectMessage.RESULT_OK);
} catch (ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
} finally {
sendResponse(response);
}
}
});
return false;
}
};
protected final DConnectApi mDeletePreviewApi = new DeleteApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_PREVIEW;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String target = getTarget(request);
mClient.execute(new Runnable() {
@Override
public void run() {
try {
ThetaDevice device = mClient.getConnectedDevice(serviceId);
ThetaDevice.Recorder recorder = device.getRecorder();
if (recorder == null) {
MessageUtils.setIllegalDeviceStateError(response, "device is not initialized.");
return;
}
if (target != null && !target.equals(recorder.getId())) {
MessageUtils.setInvalidRequestParameterError(response, "target is invalid.");
return;
}
if (!recorder.supportsPreview()) {
MessageUtils.setNotSupportAttributeError(response,
recorder.getName() + " does not support preview.");
return;
}
stopLivePreview();
setResult(response, DConnectMessage.RESULT_OK);
} catch (ThetaDeviceException cause) {
switch (cause.getReason()) {
case ThetaDeviceException.NOT_FOUND_THETA:
MessageUtils.setNotFoundServiceError(response);
break;
default:
MessageUtils.setUnknownError(response, cause.getMessage());
break;
}
} finally {
sendResponse(response);
}
}
});
return false;
}
};
protected final DConnectApi mPutOnRecordingChangeApi = new PutApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ON_RECORDING_CHANGE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
EventError error = EventManager.INSTANCE.addEvent(request);
if (error == EventError.NONE) {
setResult(response, DConnectMessage.RESULT_OK);
} else if (error == EventError.INVALID_PARAMETER) {
MessageUtils.setInvalidRequestParameterError(response);
} else {
MessageUtils.setUnknownError(response);
}
return true;
}
};
protected final DConnectApi mDeleteOnRecordingChangeApi = new DeleteApi() {
@Override
public String getAttribute() {
return ATTRIBUTE_ON_RECORDING_CHANGE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
EventError error = EventManager.INSTANCE.removeEvent(request);
if (error == EventError.NONE) {
setResult(response, DConnectMessage.RESULT_OK);
} else if (error == EventError.INVALID_PARAMETER) {
MessageUtils.setInvalidRequestParameterError(response);
} else if (error == EventError.FAILED) {
MessageUtils.setUnknownError(response, "Failed to delete event from cache.");
} else if (error == EventError.NOT_FOUND) {
MessageUtils.setUnknownError(response, "Not found event.");
} else {
MessageUtils.setUnknownError(response);
}
return true;
}
};
/**
* Constructor.
*
* @param client an instance of {@link ThetaDeviceClient}
* @param fileMgr an instance of {@link FileManager}
*/
protected ThetaMediaStreamRecordingProfile(final ThetaDeviceClient client,
final FileManager fileMgr) {
mClient = client;
mFileMgr = fileMgr;
addApi(mGetMediaRecorderApi);
addApi(mPostTakePhotoApi);
addApi(mPutOnPhotoApi);
addApi(mDeleteOnPhotoApi);
addApi(mPostRecordApi);
addApi(mPutStopApi);
addApi(mPutOnRecordingChangeApi);
addApi(mDeleteOnRecordingChangeApi);
}
private String startLivePreview(final LiveCamera liveCamera, final Integer width, final Integer height) {
synchronized (mLockObj) {
if (mServer == null) {
mServer = new MixedReplaceMediaServer();
mServer.setServerName("Live Preview Server");
mServer.start();
}
final String segment = SEGMENT_LIVE_PREVIEW;
if (mLivePreviewTask == null) {
mLivePreviewTask = new LivePreviewTask(liveCamera) {
@Override
protected void onFrame(final byte[] frame) {
byte[] b;
if (width != null || height != null) {
b = resizeFrame(frame, width, height);
} else {
b = frame;
}
offerFrame(segment, b);
}
};
mExecutor.execute(mLivePreviewTask);
}
return mServer.getUrl() + "/" + segment;
}
}
private byte[] resizeFrame(final byte[] frame, final Integer newWidth, final Integer newHeight) {
Bitmap preview = BitmapFactory.decodeByteArray(frame, 0, frame.length);
int w = newWidth != null ? newWidth : preview.getWidth();
int h = newHeight != null ? newHeight : preview.getHeight();
Bitmap resized = BitmapUtils.resize(preview, w, h);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
resized.compress(Bitmap.CompressFormat.JPEG, 80, baos);
return baos.toByteArray();
}
private void offerFrame(final String segment, final byte[] frame) {
synchronized (mLockObj) {
if (mServer != null) {
mServer.offerMedia(segment, frame);
}
}
}
private void stopLivePreview() {
synchronized (mLockObj) {
if (mLivePreviewTask != null) {
mLivePreviewTask.stop();
mLivePreviewTask = null;
}
if (mServer != null) {
mServer.stop();
mServer = null;
}
}
}
private void sendOnPhotoEvent(final String serviceId, final String mimeType, final String uri, final String path) {
List<Event> events = getOnPhotoEventList(serviceId);
mLogger.info("Send onphoto events: " + events.size());
for (Iterator<Event> it = events.iterator(); it.hasNext(); ) {
Event event = it.next();
Intent message = EventManager.createEventMessage(event);
Bundle photoInfo = new Bundle();
setUri(photoInfo, uri);
setPath(photoInfo, path);
setMIMEType(photoInfo, mimeType);
setPhoto(message, photoInfo);
sendEvent(message, event.getAccessToken());
}
}
private void sendOnRecordingChangeEvent(final String serviceId,
final ThetaDevice.Recorder recorder,
final RecordingState state) {
List<Event> events = getOnRecordingChangeEventList(serviceId);
mLogger.info("Send onrecordingchange events: " + events.size());
for (Iterator<Event> it = events.iterator(); it.hasNext(); ) {
Event event = it.next();
Intent message = EventManager.createEventMessage(event);
Bundle media = new Bundle();
setStatus(media, state);
setMIMEType(media, recorder.getMimeType());
setMedia(message, media);
sendEvent(message, event.getAccessToken());
}
}
private List<Event> getOnPhotoEventList(final String serviceId) {
return EventManager.INSTANCE.getEventList(serviceId, PROFILE_NAME, null, ATTRIBUTE_ON_PHOTO);
}
private List<Event> getOnRecordingChangeEventList(final String serviceId) {
return EventManager.INSTANCE.getEventList(serviceId, PROFILE_NAME, null, ATTRIBUTE_ON_RECORDING_CHANGE);
}
private static Integer getWidth(final Intent request) {
return parseInteger(request, PARAM_WIDTH);
}
private static Integer getHeight(final Intent request) {
return parseInteger(request, PARAM_HEIGHT);
}
public void forcedStopRecording() {
/** 動画記録停止処理 */
mClient.execute(new Runnable() {
@Override
public void run() {
try {
ThetaDevice device = mClient.getCurrentConnectDevice();
ThetaDevice.Recorder recorder = device.getRecorder();
if (recorder != null && recorder.supportsVideoRecording()) {
ThetaDevice.RecorderState state = recorder.getState();
switch (state) {
case RECORDING:
device.stopVideoRecording();
break;
case INACTIVE:
default:
break;
}
}
} catch (ThetaDeviceException e) {
// Not operation.
}
}
});
/** プレビュー停止処理 */
mClient.execute(new Runnable() {
@Override
public void run() {
try {
ThetaDevice device = mClient.getCurrentConnectDevice();
ThetaDevice.Recorder recorder = device.getRecorder();
if (recorder != null && recorder.supportsPreview()) {
stopLivePreview();
}
} catch (ThetaDeviceException cause) {
// Not operation.
}
}
});
}
}