diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ed70cdafff039bfedba294c781be865ca5f6c1e..839ab18b2845cf6f14f778d82f3ebdb68fe37fd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,15 @@ package="com.huawei.cloudapp"> + + + + + + 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + this.sendBroadcast(new Intent(GRANT_CAMERA_PERMISSION_SUCCESS_ACTION)); + } + } else if (requestCode == DEV_TYPE_MICROPHONE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + this.sendBroadcast(new Intent(GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION)); + } + } + } + /** * get data with intent */ @@ -383,6 +419,13 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl } } + private void initVirtualDeviceSession() { + if (!isGotCameraAndRecordPermission) return; + mVirtualDeviceSession = new VirtualDeviceSession(this); + mRingBufferVirtualDeviceIO = new RingBufferVirtualDeviceIO(); + mVirtualDeviceSession.setVirtualDeviceIoHook(mRingBufferVirtualDeviceIO); + } + private void processStateChangeMsg(int code) { CASLog.i(TAG, "processStateChangeMsg code = " + code); switch (code) { @@ -390,6 +433,9 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl lag.setVisibility(View.VISIBLE); loadingView.setVisibility(View.GONE); startSuccessThreadTask(); + if (mVirtualDeviceSession != null) { + mVirtualDeviceSession.start(); + } break; case CasState.CAS_SERVER_UNREACHABLE: showDialog(getResources().getString(R.string.cas_phone_connect_server_fail_tip_message)); @@ -603,6 +649,9 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl bIsStart = false; try { mCloudPhone.exitCloudPhone(); + if (mVirtualDeviceSession != null) { + mVirtualDeviceSession.stop(); + } } catch (Exception e) { CASLog.e(TAG, "stop cloud phone failed " + e.getMessage()); } @@ -781,6 +830,16 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl } } + class CloudPhoneVirtualDevDataListenerImpl implements CloudPhoneVirtualDevDataListener { + + @Override + public void onRecvVirtualDevData(byte[] data, int length) { + if (mRingBufferVirtualDeviceIO != null) { + mRingBufferVirtualDeviceIO.fillData(data); + } + } + } + public void switchCtrlViewShow() { if (ctrView == null) { return; diff --git a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl index bafce7ea95280ef05f3a4281790a8facbfd70533..561f92e280aef55202820acee091f9aecd7d6c94 100644 --- a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl +++ b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl @@ -72,5 +72,5 @@ interface ICASAidlInterface { void mute(in boolean isMute); - void sendDataToCloudApp(in byte[] data); + void sendData(byte type, in byte[] data); } diff --git a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl index f4e146b99a8afbdb3e9905222163cd784da098b9..c3a9fee9fd9cd60f08b207ac4e63c48fd09ab62a 100644 --- a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl +++ b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl @@ -26,4 +26,6 @@ interface ICASAidlListener { void onCmdRecv(int code, String msg); void onChannelDataRecv(in byte[] data); + + void onVirtualDevDataRecv(in byte[] data); } diff --git a/cloudphone/src/main/cpp/CasCommon.h b/cloudphone/src/main/cpp/CasCommon.h index 6b958c7a68adb664013e021d771c0ce15bcf5d16..01d70e112ddef7348292fc8751e7cad998d3e78d 100644 --- a/cloudphone/src/main/cpp/CasCommon.h +++ b/cloudphone/src/main/cpp/CasCommon.h @@ -39,6 +39,12 @@ const std::string KEY_BITRATE = "bitrate"; const std::string KEY_VIRTUAL_WIDTH = "virtual_width"; const std::string KEY_VIRTUAL_HEIGHT = "virtual_height"; +enum VirtualDevice { + VIRTUAL_CAMERA, + VIRTUAL_MICROPHONE, + VIRTUAL_SENSOR +}; + enum { CAS_CONNECTING = 0x0100, CAS_CONNECT_SUCCESS = 0x0200, diff --git a/cloudphone/src/main/cpp/CasController.cpp b/cloudphone/src/main/cpp/CasController.cpp index 08ebabf761dd7ca9a79b218a4b54ecabac93e072..0b46d5944b0ea503860530619900ccf9f72013ec 100644 --- a/cloudphone/src/main/cpp/CasController.cpp +++ b/cloudphone/src/main/cpp/CasController.cpp @@ -65,6 +65,7 @@ CasController::CasController() m_videoPacketStream = nullptr; m_audioPacketStream = nullptr; m_orientationStream = nullptr; + m_virtualDeviceStream = nullptr; m_controlStream = nullptr; m_channelStream = nullptr; m_cmdController = nullptr; @@ -85,6 +86,7 @@ CasController::~CasController() m_videoPacketStream = nullptr; m_audioPacketStream = nullptr; m_orientationStream = nullptr; + m_virtualDeviceStream = nullptr; m_controlStream = nullptr; m_channelStream = nullptr; m_cmdController = nullptr; @@ -398,6 +400,7 @@ bool CasController::CreateWorkers(ANativeWindow *nativeWindow, bool needVideoDec m_streamParser->SetServiceHandle(CasMsgType::Video, m_videoPacketStream); m_streamParser->SetServiceHandle(CasMsgType::Audio, m_audioPacketStream); m_streamParser->SetServiceHandle(CasMsgType::Channel, m_channelStream); + m_streamParser->SetServiceHandle(CasMsgType::VirtualDevice, m_virtualDeviceStream); if (needVideoDecode) { m_videoDecodeThread = new (std::nothrow) CasVideoHDecodeThread(nativeWindow); @@ -593,6 +596,12 @@ bool CasController::InitDataStream() return false; } + m_virtualDeviceStream = new (std::nothrow) CasDataPipe(); + if (m_virtualDeviceStream == nullptr) { + ERR("Failed to new virtual device packet stream."); + return false; + } + return true; } @@ -618,6 +627,10 @@ bool CasController::ClearDataStream() m_channelStream->Clear(); } + if (m_virtualDeviceStream != nullptr) { + m_virtualDeviceStream->Clear(); + } + INFO("Succeed to clear data stream "); return true; } @@ -745,6 +758,11 @@ int CasController::JniRecvData(CasMsgType type, uint8_t *data, int length) pPkt = m_channelStream->GetNextPkt(); } break; + case CasMsgType::VirtualDevice: + if (m_virtualDeviceStream != nullptr) { + pPkt = m_virtualDeviceStream->GetNextPkt(); + } + break; default: ERR("Invalid type %d, length %d.", type, length); return 0; diff --git a/cloudphone/src/main/cpp/CasController.h b/cloudphone/src/main/cpp/CasController.h index 2ef53ca96a575bac88cbdf8c4a9fdb6affb956bc..f7d61dba07abdda5ef9af1fd1cdd72e52c62e225 100644 --- a/cloudphone/src/main/cpp/CasController.h +++ b/cloudphone/src/main/cpp/CasController.h @@ -125,6 +125,7 @@ private: CasDataPipe *m_orientationStream = nullptr; CasDataPipe *m_controlStream = nullptr; CasDataPipe *m_channelStream = nullptr; + CasDataPipe *m_virtualDeviceStream = nullptr; CasCmdController *m_cmdController = nullptr; CasHeartbeatController *m_heartbeatController = nullptr; diff --git a/cloudphone/src/main/cpp/CasOpusBridge.cpp b/cloudphone/src/main/cpp/CasOpusBridge.cpp index 001f56ea001b1e986b5f823dc7244621385f5aeb..1f6f840777b562477f7586a5fa2835cf73f0ad57 100644 --- a/cloudphone/src/main/cpp/CasOpusBridge.cpp +++ b/cloudphone/src/main/cpp/CasOpusBridge.cpp @@ -97,7 +97,7 @@ extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIW } extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusEncoder(JNIEnv *env, - jclass type, jint sampleRate, jint channels, jint bitrate) + jclass type, jint sampleRate, jint channels) { int err = 0; g_encoder = opus_encoder_create(sampleRate, channels, APPLICATION, &err); @@ -106,8 +106,9 @@ extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIW g_encoder = nullptr; return JNI_ERR; } - - err = opus_encoder_ctl(g_encoder, OPUS_SET_BITRATE(bitrate)); + err += opus_encoder_ctl(g_encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND)); + err += opus_encoder_ctl(g_encoder, OPUS_SET_VBR(1)); + err += opus_encoder_ctl(g_encoder, OPUS_SET_COMPLEXITY(10)); if (err < 0) { ERR("Failed to set bitrate %s", opus_strerror(err)); g_encoder = nullptr; diff --git a/cloudphone/src/main/cpp/CasOpusBridge.h b/cloudphone/src/main/cpp/CasOpusBridge.h index db67993cf15fc489db68a004e95db14065bb104c..94f1dcafb5e7298262ef8481faf751c386dd84a1 100644 --- a/cloudphone/src/main/cpp/CasOpusBridge.h +++ b/cloudphone/src/main/cpp/CasOpusBridge.h @@ -58,7 +58,7 @@ JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_opus * Signature: (III)I */ JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusEncoder(JNIEnv *, jclass type, - jint, jint, jint); + jint, jint); /* * Class: com_huawei_cloudphone_jniwrapper_OpusJNIWrapper diff --git a/cloudphone/src/main/cpp/cas_common/CasMsg.h b/cloudphone/src/main/cpp/cas_common/CasMsg.h index 8a44a05ef95c3247a9c13428e72bc22ddce36e21..6414caf0d9f07f37a5ad8224bd4e706d4430cc5b 100644 --- a/cloudphone/src/main/cpp/cas_common/CasMsg.h +++ b/cloudphone/src/main/cpp/cas_common/CasMsg.h @@ -33,6 +33,10 @@ enum CasMsgType : uint8_t { Recorder = 11, KeyEventInput = 15, MotionEventInput = 16, + VirtualDevice = 20, + VirtualCamera = 21, + VirtualMicrophone = 22, + VirtualSensor = 23, End, }; @@ -52,6 +56,9 @@ enum CasMsgType : uint8_t { #define CAS_MSG_CHECKSUM_KEYEVENTINPUT GET_CAS_CHECKSUM(CasMsgType::KeyEventInput) #define CAS_MSG_CHECKSUM_MOTIONEVENTINPUT GET_CAS_CHECKSUM(CasMsgType::MotionEventInput) #define CAS_MSG_CHECKSUM_CHANNEL GET_CAS_CHECKSUM(CasMsgType::Channel) +#define CAS_MSG_CHECKSUM_VIRTUAL_CAMERA GET_CAS_CHECKSUM(CasMsgType::VirtualCamera) +#define CAS_MSG_CHECKSUM_VIRTUAL_MICROPHONE GET_CAS_CHECKSUM(CasMsgType::VirtualMicrophone) +#define CAS_MSG_CHECKSUM_VIRTUAL_SENSOR GET_CAS_CHECKSUM(CasMsgType::VirtualSensor) // 客户端通用消息头 typedef struct streamMsgHead { diff --git a/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp b/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp index 78b9cc20241e7cef35892b05eb8a43269180e954..12576549c179d3c4516fd0ff9a2e54938aa51692 100644 --- a/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp +++ b/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp @@ -72,6 +72,15 @@ int CasStreamBuildSender::SendDataToServer(CasMsgType type, const void *buf, siz case (Channel): msgHead.checksum = CAS_MSG_CHECKSUM_CHANNEL; break; + case (VirtualCamera): + msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_CAMERA; + break; + case (VirtualMicrophone): + msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_MICROPHONE; + break; + case (VirtualSensor): + msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_SENSOR; + break; default: { return -1; } diff --git a/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp b/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp index 6197e385529649277ad0cf25528c5be50fd36c7a..60c1257cef0d7331ff5601900387e9f2673c5d21 100644 --- a/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp +++ b/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp @@ -75,7 +75,7 @@ void CasStreamRecvParser::SetServiceHandle(unsigned char type, CasPktHandle *ser CasPktHandle *CasStreamRecvParser::GetServiceHandle(unsigned char type) { - return m_serviceHandles[type]; + return VirtualSensor >= type && type >= VirtualCamera ? m_serviceHandles[VirtualDevice] : m_serviceHandles[type]; } CasStreamParseThread::CasStreamParseThread(CasSocket *socket, CasStreamRecvParser *streamRecvParser) diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java b/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java new file mode 100644 index 0000000000000000000000000000000000000000..abee14c94b755883afa7165971bd03baf063e63c --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.api; + +public interface CloudPhoneVirtualDevDataListener { + void onRecvVirtualDevData(byte[] data, int length); +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java b/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java index 0311b01f72ef99490e84d0d7ee42579cbe5f7ee4..5cf371a09112b38286d00dd036e1a0b8f43165b5 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java @@ -115,6 +115,20 @@ public interface ICloudPhone { */ void registerOnOrientationChangeListener(CloudPhoneOrientationChangeListener listener); + /** + * 设置虚拟设备的回调 + * + * @param listener + */ + void registerOnVirtualDevDataListener(CloudPhoneVirtualDevDataListener listener); + + /** + * 发送虚拟设备数据 + * @param devType + * @param data + */ + void sendVirtualDeviceData(byte devType, byte[] data); + /** * 获取网络时延 * diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java b/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java index 1d0bf5b0a6b4770433864ef11f1212fd09ce728b..ca6d21cf730abd55f3c83ee90f435d5d9abc7bf3 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java @@ -47,6 +47,7 @@ import com.huawei.cloudphone.api.CloudPhoneOrientationChangeListener; import com.huawei.cloudphone.api.CloudPhoneParas; import com.huawei.cloudphone.api.CloudPhoneParas.DisplayMode; import com.huawei.cloudphone.api.CloudPhoneStateListener; +import com.huawei.cloudphone.api.CloudPhoneVirtualDevDataListener; import com.huawei.cloudphone.api.ICloudPhone; import com.huawei.cloudphone.common.CASLog; import com.huawei.cloudphone.common.CasConnectorInfo; @@ -113,6 +114,7 @@ public class CloudPhoneImpl implements ICloudPhone { private volatile boolean mIsReconnectTaskRun = false; private CloudPhoneStateListener mStateListener = null; private CloudPhoneOrientationChangeListener mOrientationChangeListener = null; + private CloudPhoneVirtualDevDataListener mVirtualDevDataListener = null; private CloudAppDataListener mChannelDataListener = null; private ServiceConnection mConnection = null; private CASListener mListener = null; @@ -295,6 +297,18 @@ public class CloudPhoneImpl implements ICloudPhone { mOrientationChangeListener = listener; } + @Override + public void registerOnVirtualDevDataListener(CloudPhoneVirtualDevDataListener listener) { + mVirtualDevDataListener = listener; + } + + @Override + public void sendVirtualDeviceData(byte devType, byte[] data) { + if (mCASClient != null) { + mCASClient.sendDataToVirtualDevice(devType, data); + } + } + @Override public int getRtt() { if (mCASClient != null) { @@ -495,7 +509,7 @@ public class CloudPhoneImpl implements ICloudPhone { mCASClient.pause(); mCurrentState = STATE_PAUSE; mBackGroundTimer = new Timer(); - mBackGroundTimer.schedule(new BackgroudTimerTask(), mBgTimeout); + mBackGroundTimer.schedule(new BackgroundTimerTask(), mBgTimeout); } } } @@ -767,9 +781,16 @@ public class CloudPhoneImpl implements ICloudPhone { mChannelDataListener.onRecvCloudAppData(data); } } + + @Override + public void onVirtualDevDataRecv(byte[] data) throws RemoteException { + if (mVirtualDevDataListener != null) { + mVirtualDevDataListener.onRecvVirtualDevData(data, data.length); + } + } } - private class BackgroudTimerTask extends TimerTask { + private class BackgroundTimerTask extends TimerTask { @Override public void run() { CASLog.i(TAG, "BackgroudTimerTask run"); diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java index 95421afd6d3038b8fdafaafa81fe72dae4ab04d7..f9d626541d1de10350b33a0298550c198191aea2 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java @@ -51,6 +51,11 @@ public class JNIWrapper { public static final byte NOTIFY = 17; public static final byte END = 19; + public static final byte VIRTUAL_DEVICE_DATA = 20; + public static final byte CAMERA_DATA = 21; + public static final byte MICROPHONE_DATA = 22; + public static final byte SENSOR_DATA = 23; + static { System.loadLibrary("cloudapp"); } diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java index 2871dbb310fc0792be0c3f8cac7cd0a0241708e6..a5dd9340199601eb1dc2746addbb9747f5dc26f3 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java @@ -29,7 +29,7 @@ public class OpusJNIWrapper { public static native int opusDecode(long decoder, byte[] inputBuffer, int inputBufferLen, short[] outBuffer, int outbufLen); - public static native int createOpusEncoder(int sampleRate, int channels, int bitrate); + public static native int createOpusEncoder(int sampleRate, int channels); public static native int destroyOpusEncoder(); diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java b/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java index 1f46aabf2ac1b744340d9429db539e116703c2d3..274b22c77ef71e53ae8f8071ae70feed48d52169 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java @@ -16,6 +16,8 @@ package com.huawei.cloudphone.service; +import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CHANNEL; + import android.os.IBinder; import android.os.RemoteException; import android.view.Surface; @@ -321,9 +323,20 @@ public class CASClient { return; } try { - mCasInterface.sendDataToCloudApp(data); + mCasInterface.sendData(CHANNEL, data); } catch (RemoteException e) { CASLog.e(TAG, "failed to send data to cloud app."); } } + + public void sendDataToVirtualDevice(byte devType, byte[] data) { + if (mCasInterface == null) { + return; + } + try { + mCasInterface.sendData(devType, data); + } catch (RemoteException e) { + CASLog.e(TAG, "failed to send data to virtual device."); + } + } } diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java b/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java index 0d0eb0a5497006d7cbf059aa376699094f8fef41..cf1682225eb3c82fed1e80e05393af37cd2163b7 100644 --- a/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java +++ b/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java @@ -40,15 +40,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; -import static com.huawei.cloudphone.jniwrapper.JNIWrapper.AUDIO; -import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CHANNEL; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_AES_IV; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_AUTH_TS; -import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_DECODE_METHOD; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_ENCRYPTED_DATA; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_BACKGROUND_TIMEOUT; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_IP; -import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_LOG_LEVEL; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_PORT; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_PROTOCOL_VERSION; import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_SDK_VERSION; @@ -91,6 +87,7 @@ public class CasProcessor extends ICASAidlInterface.Stub { */ private ICASAidlListener mListener = null; private NewRotationDirectionPacket mNewRotationDirPkt; + private NewVirtualDevDataPacket mVirtualDevDataPkt; @Override public void init() throws RemoteException { @@ -218,10 +215,12 @@ public class CasProcessor extends ICASAidlInterface.Stub { mAudioTrackerCallback = new AudioTrackerCallback(); mNewRotationDirPkt = new NewRotationDirectionPacket(); mChannelDataCallback = new NewChannelDataPacket(); + mVirtualDevDataPkt = new NewVirtualDevDataPacket(); mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.ORIENTATION, mNewRotationDirPkt); mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.AUDIO, mAudioTrackerCallback); mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.CHANNEL, mChannelDataCallback); + mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.VIRTUAL_DEVICE_DATA, mVirtualDevDataPkt); mUpstreamReceiveDispatcher.start(); return true; } @@ -232,6 +231,7 @@ public class CasProcessor extends ICASAidlInterface.Stub { mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.AUDIO); mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.ORIENTATION); mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.CHANNEL); + mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.VIRTUAL_DEVICE_DATA); mUpstreamReceiveDispatcher.stopBlocked(); mAudioTrackerCallback.closeAudioTrack(); mUpstreamReceiveDispatcher = null; @@ -268,8 +268,8 @@ public class CasProcessor extends ICASAidlInterface.Stub { } @Override - public void sendDataToCloudApp(byte[] data) throws RemoteException { - JniBridge.getInstance().sendData(CHANNEL, data, data.length); + public void sendData(byte devType, byte[] data) throws RemoteException { + JniBridge.getInstance().sendData(devType, data, data.length); } /** @@ -340,4 +340,17 @@ public class CasProcessor extends ICASAidlInterface.Stub { } } } + + private class NewVirtualDevDataPacket implements NewPacketCallback { + @Override + public void onNewPacket(byte[] data) { + if (mListener != null) { + try { + mListener.onVirtualDevDataRecv(data); + } catch (RemoteException e) { + CASLog.e(TAG, "call onVirtualDevDataRecv failed." + e.getMessage()); + } + } + } + } } diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java new file mode 100644 index 0000000000000000000000000000000000000000..a4ff95eaef3871ac06226f7ca0877503bde4ee2a --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice; + +import android.content.Context; +import android.hardware.SensorManager; +import android.view.SurfaceHolder; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceIO; +import com.huawei.cloudphone.virtualdevice.common.ParamBundle; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol; + +public class VirtualDeviceSession { + VirtualDeviceProtocol mVirtualDevice; + Context mContext; + ParamBundle mParamBundle; + IVirtualDeviceIO mVirtualDeviceIO; + + public VirtualDeviceSession(Context context) { + mParamBundle = new ParamBundle(); + mParamBundle.setAppContext(context); + mContext = context; + } + + public void setCameraSurfaceHolder(SurfaceHolder surfaceHolder) { + mParamBundle.setSurfaceHolder(surfaceHolder); + } + + public void setVirtualDeviceIoHook(IVirtualDeviceIO virtualDeviceIO) { + mVirtualDeviceIO = virtualDeviceIO; + } + + public void start() { + mVirtualDevice = new VirtualDeviceProtocol(mVirtualDeviceIO, mContext); + mVirtualDevice.startProcess(); + } + + public void stop() { + if (mVirtualDevice != null) { + mVirtualDevice.stopProcess(); + mVirtualDevice = null; + } + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..f488194d5c69d5b06d62718c9bdaada352a2d159 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java @@ -0,0 +1,227 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.camera; + +import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar; +import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar; + +import android.graphics.ImageFormat; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MediaFormat; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class AvcEncoder { + private static final String TAG = "AvcEncoder"; + private static final String MIME_TYPE = "video/avc"; + private static final int TIMEOUT_USC = 1000; + private static final int MICROSECONDS_PER_SECOND = 1000000; + private static final int I_FRAME_INTERVAL = 15; + + private static int colorFormat = COLOR_FormatYUV420Planar; + private MediaCodec mediaCodec; + private int mWidth; + private int mHeight; + private int mFrameRate; + private long genIndex; + private byte[] configByte; + private AvcEncoderDataHandler dataHandler; + + public void getIFrame() { + mediaCodec.flush(); + } + + public AvcEncoder(int width, int height, int frameRate, int bitrate, AvcEncoderDataHandler dataHandler) { + Log.i(TAG, "Create AvcEncoder, width = " + width + ", height = " + height + + ", frameRate = " + frameRate + ", bitrate = " + bitrate); + this.dataHandler = dataHandler; + this.mWidth = width; + this.mHeight = height; + this.mFrameRate = frameRate; + this.genIndex = 0; + MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); + mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); + + try { + mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); + } catch (IOException e) { + Log.e(TAG, "AvcEncoder create encoder by type failed. ", e); + } + mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + mediaCodec.start(); + } + + public void stopEncoder() { + Log.i(TAG, "Stop encoder."); + try { + mediaCodec.stop(); + mediaCodec.release(); + } catch (IllegalStateException e) { + Log.e(TAG, "AvcEncoder stop Encoder failed. ", e); + } + } + + public void offerEncoder(byte[] frame, byte[] encodeBuff) { + if ((frame == null) || (encodeBuff == null)) { + Log.e(TAG, "Offer Encoder input param invalid. "); + return; + } + inputBuffer(frame); + int outBufLen = 0; + while (true) { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int outBufId = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USC); + Log.i(TAG, "bufferInfo: size = " + bufferInfo.size + ", offset = " + bufferInfo.offset + + ", flags = " + bufferInfo.flags + ", presentationTimeUs = " + bufferInfo.presentationTimeUs); + if (outBufId >= 0) { + ByteBuffer outBuf = mediaCodec.getOutputBuffer(outBufId); + byte[] h264Buf = new byte[bufferInfo.size]; + if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { + configByte = new byte[bufferInfo.size]; + outBuf.get(configByte); + } else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) { + outBuf.get(h264Buf); + System.arraycopy(configByte, 0, encodeBuff, 0, configByte.length); + System.arraycopy(h264Buf, 0, encodeBuff, configByte.length, h264Buf.length); + outBufLen = bufferInfo.size + configByte.length; + dataHandler.handleData(encodeBuff, outBufLen); + } else { + outBuf.get(h264Buf); + System.arraycopy(h264Buf, 0, encodeBuff, 0, h264Buf.length); + outBufLen = bufferInfo.size; + dataHandler.handleData(encodeBuff, outBufLen); + } + mediaCodec.releaseOutputBuffer(outBufId, false); + } else if (outBufId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + Log.e(TAG, "Output format changed : " + outBufId); + return; + } else if (outBufId == MediaCodec.INFO_TRY_AGAIN_LATER) { + Log.e(TAG, "Codec waiting : " + outBufId); + return; + } else { + Log.e(TAG, "Unknown outBufId : " + outBufId); + return; + } + } + } + + private void inputBuffer(byte[] frame) { + long pts = computePresentationTime(genIndex); + genIndex++; + byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2]; + if (colorFormat == COLOR_FormatYUV420Planar) { + swapYV12ToI420(frame, yuv420sp, mWidth, mHeight); + }else if (colorFormat == COLOR_FormatYUV420SemiPlanar) { + swapNV21ToNV12(frame, yuv420sp, mWidth, mHeight); + } + int inBuffId = mediaCodec.dequeueInputBuffer(TIMEOUT_USC); + if (inBuffId < 0) { + Log.e(TAG, "Dequeue input buffer error, inBuffId = " + inBuffId); + return; + } + ByteBuffer inBuff = mediaCodec.getInputBuffer(inBuffId); + inBuff.clear(); + inBuff.put(yuv420sp); + mediaCodec.queueInputBuffer(inBuffId, 0, yuv420sp.length, pts, 0); + } + + private void swapYV12ToI420(byte[] yv12Data, byte[] i420Data, int width, int height) { + if (yv12Data == null || i420Data == null) { + return; + } + int frameSize = width * height; + System.arraycopy(yv12Data, 0, i420Data, 0, frameSize); + System.arraycopy(yv12Data, frameSize + frameSize / 4, i420Data, frameSize ,frameSize / 4); + System.arraycopy(yv12Data, frameSize, i420Data, frameSize + frameSize / 4, frameSize / 4); + } + + private void swapNV21ToNV12(byte[] nv21Data, byte[] nv12Data, int width, int height) { + if (nv21Data == null || nv12Data == null) { + return; + } + int frameSize = width * height; + System.arraycopy(nv21Data, 0, nv12Data, 0, frameSize); + for (int i = 0; i < frameSize / 2; i += 2) { + nv12Data[frameSize + i - 1] = nv21Data[i + frameSize]; + } + for (int i = 0; i < frameSize / 2; i += 2) { + nv12Data[frameSize + i] = nv21Data[i + frameSize - 1]; + } + } + + private long computePresentationTime(long frameIndex) { + return frameIndex * MICROSECONDS_PER_SECOND / mFrameRate; + } + + public static int getImageFormat() { + return colorFormat == COLOR_FormatYUV420Planar ? ImageFormat.YV12 : ImageFormat.NV21; + } + + public static boolean isSupportH264() { + MediaCodecInfo codecInfo = selectCodec(MIME_TYPE); + if (codecInfo == null) { + Log.e(TAG, "Couldn't find a codec for " + MIME_TYPE); + return false; + } + return selectColorFormat(codecInfo, MIME_TYPE); + } + + private static MediaCodecInfo selectCodec(String mimeType) { + for (int i = 0; i < MediaCodecList.getCodecCount(); i++) { + MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); + if(!codecInfo.isEncoder()) { + continue; + } + String[] types = codecInfo.getSupportedTypes(); + for (int j = 0; j < types.length; j++) { + if (types[j].equalsIgnoreCase(mimeType)) { + return codecInfo; + } + } + } + return null; + } + + private static boolean selectColorFormat(MediaCodecInfo codecInfo, String mimeType) { + MediaCodecInfo.CodecCapabilities capabilities; + try { + capabilities = codecInfo.getCapabilitiesForType(mimeType); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Couldn't get capabilities for " + MIME_TYPE); + return false; + } + for (int i = 0; i < capabilities.colorFormats.length; i++) { + int colorFormat = capabilities.colorFormats[i]; + if (colorFormat == COLOR_FormatYUV420SemiPlanar) { + AvcEncoder.colorFormat = COLOR_FormatYUV420SemiPlanar; + return true; + } else if (colorFormat == COLOR_FormatYUV420Planar) { + AvcEncoder.colorFormat = COLOR_FormatYUV420Planar; + return true; + } + } + Log.e(TAG, "Couldn't get a color format for " + codecInfo.getName() + "/" + mimeType); + return false; + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..703683387bf72bba99515e25453650db59a82061 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.camera; + +public interface AvcEncoderDataHandler { + + /** + * 处理编码数据 + * @param data + * @param length + */ + void handleData(byte[] data, int length); +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java new file mode 100644 index 0000000000000000000000000000000000000000..c033b7d11f65d1bb0a0fe32dfd1c4144e9c72c62 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java @@ -0,0 +1,290 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.camera; + +import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO; +import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE; +import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO; +import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED; +import static android.hardware.Camera.Parameters.FOCUS_MODE_INFINITY; +import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO; + +import android.graphics.ImageFormat; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.graphics.YuvImage; +import android.hardware.Camera; +import android.opengl.GLES11Ext; +import android.util.Log; +import android.view.SurfaceHolder; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; +import com.huawei.cloudphone.virtualdevice.common.ParamBundle; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class VirtualCamera implements Camera.PreviewCallback { + + private static final String TAG = "VirtualCamera"; + private Camera.Parameters mParameters = null; + private int mCameraId = 0; + private Camera mCamera = null; + private static final int LOG_LENGTH_LIMIT = 2000; + private static final int SIZE_WIDTH_LIMIT = 1280; + private static final int JPEG_BUFFER_SIZE = 8 * 1024 * 1024; + private static final int DEFAULT_WIDTH = 640; + private static final int DEFAULT_HEIGHT = 480; + + private int mWidth = DEFAULT_WIDTH; + private int mHeight = DEFAULT_HEIGHT; + private int mFps = 30; + private int mBitrate = 4000000; + private SurfaceHolder mSurfaceHolder; + private boolean mIsUseH264; + + private ByteArrayOutputStream jpegBuffer = new ByteArrayOutputStream(JPEG_BUFFER_SIZE); + private Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + private IVirtualDeviceDataListener mCameraListener = null; + private byte[] mPreviewBuffer = null; + private SurfaceTexture mSurfaceTexture = null; + private byte[] mH264Buffer; + private AvcEncoder mAvcCodec = null; + + public int open(int cameraId) { + mPreviewBuffer = null; + mSurfaceTexture = null; + mCameraId = cameraId; + Camera.getCameraInfo(mCameraId, cameraInfo); + try { + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.setPreviewCallback(null); + mCamera.release(); + mCamera = null; + } + Camera camera = Camera.open(mCameraId); + Camera.Parameters param = camera.getParameters(); + mParameters = setDefaultParameters(param); + camera.release(); + mIsUseH264 = AvcEncoder.isSupportH264(); + } catch (Exception e) { + Log.e(TAG, "open camera failed, ", e); + return -1; + } + return 0; + } + + public int startPreview() { + mCamera = Camera.open(mCameraId); + Camera.Parameters param = mCamera.getParameters(); + if (mIsUseH264) { + param.setPreviewFormat(AvcEncoder.getImageFormat()); + } else { + param.setPreviewFormat(ImageFormat.NV21); + } + + Camera.Size size = param.getPreviewSize(); + if (mWidth <= 0 || mHeight <= 0 || mFps <= 0) { + param.setPreviewSize(size.width, size.height); + } else { + param.setPreviewSize(mWidth, mHeight); + } + + mCamera.setParameters(param); + List mFpsRangeList = new LinkedList(); + mFpsRangeList = param.getSupportedPreviewFpsRange(); + for (int[] ele : mFpsRangeList) { + Log.i(TAG, "startPreview: support fps range is " + ele[0] + "-" + ele[1]); + if ((mFps * 1000 >= ele[0]) && (mFps * 1000 >= ele[1])) { + param.setPreviewFpsRange(ele[0], ele[1]); + } + } + + try { + mSurfaceHolder = ParamBundle.getSurfaceHolder(); + if (mSurfaceHolder != null) { + mCamera.setPreviewCallback(this); + mCamera.setPreviewDisplay(mSurfaceHolder); + mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } else { + mSurfaceTexture = new SurfaceTexture(GLES11Ext.GL_TEXTURE_BINDING_EXTERNAL_OES); + mCamera.setPreviewTexture(mSurfaceTexture); + } + } catch (IOException e) { + Log.e(TAG, "startPreview: failed to set preview params", e); + return -1; + } + + int buffSize = mWidth * mHeight * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8; + mPreviewBuffer = new byte[buffSize]; + mCamera.addCallbackBuffer(mPreviewBuffer); + mCamera.setPreviewCallbackWithBuffer(this); + + if (mIsUseH264) { + mAvcCodec = new AvcEncoder(mWidth, mHeight, param.getPreviewFrameRate(), mBitrate + , new H264DataHandler()); + mH264Buffer = new byte[mWidth * mHeight * 3 / 2]; + mAvcCodec.getIFrame(); + } + mCamera.startPreview(); + return 0; + } + + public void stopPreview() { + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.setPreviewCallback(null); + mCamera.release(); + mCamera = null; + } + if (mAvcCodec != null) { + mAvcCodec.stopEncoder(); + mAvcCodec = null; + } + } + + public void setOnRecvData(IVirtualDeviceDataListener listener) { + mCameraListener = listener; + } + + public void setResolution(int width, int height) { + mWidth = width == -1 ? DEFAULT_WIDTH : width; + mHeight = height == -1 ? DEFAULT_HEIGHT : height; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public int getFacing() { + return cameraInfo.facing; + } + + public int getOrientation() { + return cameraInfo.orientation; + } + + public byte[] getParameters() { + return mParameters.flatten().getBytes(); + } + + public void setParameters(byte[] data) { + if (mParameters == null) { + return; + } + mParameters.unflatten(new String(data)); + } + + public void setFps(int fps) { + mFps = fps; + } + + public boolean isSupportH264() { + return AvcEncoder.isSupportH264(); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + Camera.Size size = camera.getParameters().getPreviewSize(); + if (mIsUseH264) compressWithH264(data); + else compressWithMPEG(data, size.width, size.height); + if (mSurfaceTexture != null) { + camera.addCallbackBuffer(mPreviewBuffer); + } + } + + private void compressWithMPEG(byte[] data, int width, int height) { + YuvImage image = new YuvImage(data, ImageFormat.NV21, width, height, null); + jpegBuffer.reset(); + int quality = 50; + if (!image.compressToJpeg(new Rect(0, 0, width, height), quality, jpegBuffer)) { + Log.e(TAG, "compressWithMPEG: failed to compress to jpeg"); + return; + } + if (mCameraListener != null) { + byte[] jpegData = jpegBuffer.toByteArray(); + mCameraListener.onRecvData(jpegData, jpegData.length, mCameraId); + } + } + + private void compressWithH264(byte[] data) { + mAvcCodec.offerEncoder(data, mH264Buffer); + } + + private Camera.Parameters setDefaultParameters(Camera.Parameters param) { + Set supportFocusModes = new HashSet<>(Arrays.asList(FOCUS_MODE_AUTO + , FOCUS_MODE_CONTINUOUS_PICTURE, FOCUS_MODE_CONTINUOUS_VIDEO, FOCUS_MODE_FIXED + , FOCUS_MODE_INFINITY, FOCUS_MODE_MACRO)); + List focusModes = param.getSupportedFocusModes(); + + StringBuilder sb = new StringBuilder(); + for (String str : focusModes) { + if (supportFocusModes.contains(str)) { + sb.append(str + ","); + } + } + sb.deleteCharAt(sb.length() - 1); + param.set("focus-mode-values", sb.toString()); + + List picSize = param.getSupportedPictureSizes(); + sb = new StringBuilder(); + for (Camera.Size size : picSize) { + if (size.width < SIZE_WIDTH_LIMIT) { + sb.append(size.width + "x" + size.height + ","); + } + } + sb.deleteCharAt(sb.length() - 1); + param.set("picture-size-values", sb.toString()); + + List previewSize = param.getSupportedPreviewSizes(); + sb = new StringBuilder(); + for (Camera.Size size : previewSize) { + if (size.width < SIZE_WIDTH_LIMIT) { + sb.append(size.width + "x" + size.height + ","); + } + } + sb.deleteCharAt(sb.length() - 1); + param.set("preview-size-values", sb.toString()); + + List videoSize = param.getSupportedVideoSizes(); + sb = new StringBuilder(); + for (Camera.Size size : videoSize) { + if (size.width < SIZE_WIDTH_LIMIT) { + sb.append(size.width + "x" + size.height + ","); + } + } + sb.deleteCharAt(sb.length() - 1); + param.set("video-size-values", sb.toString()); + return param; + } + + class H264DataHandler implements AvcEncoderDataHandler { + @Override + public void handleData(byte[] data, int length) { + if (mCameraListener != null) mCameraListener.onRecvData(data, length, mCameraId); + } + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java new file mode 100644 index 0000000000000000000000000000000000000000..00317b5044b59b2b519185b92d5c5eaeb2b21aef --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java @@ -0,0 +1,228 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.camera; + +import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CAMERA_DATA; +import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader; + +public class VirtualCameraManager extends VirtualDeviceManager { + private static final String TAG = "VirtualCameraManager"; + + private static final short OPT_CAMERA_GET_PARAM_REQ = 0x1; + private static final short OPT_CAMERA_GET_PARAM_RSP = 0x1001; + private static final short OPT_CAMERA_SET_PARAM_REQ = 0x2; + private static final short OPT_CAMERA_SET_PARAM_RSP = 0x1002; + private static final short OPT_CAMERA_GET_INFO_REQ = 0x3; + private static final short OPT_CAMERA_GET_INFO_RSP = 0x1003; + private static final short OPT_CAMERA_START_PREVIEW_REQ = 0x4; + private static final short OPT_CAMERA_START_PREVIEW_RSP = 0x1004; + private static final short OPT_CAMERA_STOP_PREVIEW_REQ = 0x5; + private static final short OPT_CAMERA_STOP_PREVIEW_RSP = 0x1005; + private static final short OPT_CAMERA_IS_SUPPORT_H264_REQ = 0x6; + private static final short OPT_CAMERA_IS_SUPPORT_H264_RSP = 0x1006; + private static final short OPT_CAMERA_GET_SIZE_REQ = 0x7; + private static final short OPT_CAMERA_GET_SIZE_RSP = 0x1007; + private static final short OPT_CAMERA_FRAME = 0x8; + private static final short OPT_CAMERA_INVALID = 0x9; + + private static final int RSP_RESULT_LENGTH = 2; + private static final int RSP_INFO_LENGTH = 3; + private static final int RSP_SIZE_LENGTH = 4; + private static final int RSP_SUPPORT_H264_LENGTH = 1; + public static final String GRANT_CAMERA_PERMISSION_SUCCESS_ACTION = "android.intent.action.GRANT_CAMERA_PERMISSION_SUCCESS"; + + private VirtualCamera mVirtualCamera; + private VirtualDeviceProtocol mVirtualDeviceProtocol; + private Context mContext; + private int mWidth; + private int mHeight; + private int mFrameRate; + private int mDevId; + + public VirtualCameraManager(VirtualDeviceProtocol virtualDeviceProtocol, Context context) { + mVirtualCamera = new VirtualCamera(); + mVirtualDeviceProtocol = virtualDeviceProtocol; + mContext = context; + } + + public void stop() { + mVirtualCamera.stopPreview(); + } + + public void processMsg(MsgHeader header, byte[] body) { + switch (header.mOptType) { + case OPT_CAMERA_GET_PARAM_REQ: + Log.i(TAG, "processMsg: get param."); + handleGetParamReq(header, body); + break; + case OPT_CAMERA_SET_PARAM_REQ: + Log.i(TAG, "processMsg: set param."); + handleSetParamReq(header, body); + break; + case OPT_CAMERA_GET_INFO_REQ: + Log.i(TAG, "processMsg: get info."); + handleGetInfoReq(header, body); + break; + case OPT_CAMERA_START_PREVIEW_REQ: + Log.i(TAG, "processMsg: start preview."); + handleStartPreviewReq(header, body); + break; + case OPT_CAMERA_STOP_PREVIEW_REQ: + Log.i(TAG, "processMsg: stop preview."); + handleStopPreviewReq(header, body); + break; + case OPT_CAMERA_IS_SUPPORT_H264_REQ: + Log.i(TAG, "processMsg: is support h264."); + handleIsSupportH264Req(header, body); + break; + case OPT_CAMERA_GET_SIZE_REQ: + Log.i(TAG, "processMsg: get size."); + handleGetSizeReq(header, body); + break; + default: + Log.e(TAG, "processMsg: Invalid msg type"); + } + } + + private void handleGetParamReq(MsgHeader header, byte[] body) { + int result = mVirtualCamera.open(header.mDeviceId); + byte[] param = mVirtualCamera.getParameters(); + byte[] rspBody = new byte[param.length + RSP_RESULT_LENGTH]; + rspBody[0] = 0x0; + rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1); + System.arraycopy(param, 0, rspBody, RSP_RESULT_LENGTH, param.length); + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + param.length; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_PARAM_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + private void handleSetParamReq(MsgHeader header, byte[] body) { + mVirtualCamera.setParameters(body); + byte[] rspBody = new byte[RSP_RESULT_LENGTH]; + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH; + rspBody[0] = 0x0; + rspBody[1] = 0x0; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_SET_PARAM_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + private void handleGetInfoReq(MsgHeader header, byte[] body) { + int face = mVirtualCamera.getFacing(); + int orientation = mVirtualCamera.getOrientation(); + byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_INFO_LENGTH]; + rspBody[0] = 0x0; + rspBody[1] = 0x0; + rspBody[2] = (byte) (face & 0x00FF); + rspBody[3] = (byte) (orientation >> 8); + rspBody[4] = (byte) (orientation & 0x00FF); + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + RSP_INFO_LENGTH; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_INFO_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + public void initCamera() { + if (mVirtualCamera.open(mDevId) != 0) { + Log.e(TAG, "initCamera: Failed to open camera"); + return; + } + mVirtualCamera.setOnRecvData(new CameraDataListener()); + mVirtualCamera.setResolution(mWidth, mHeight); + mVirtualCamera.setFps(mFrameRate); + if (mVirtualCamera.startPreview() != 0) { + Log.e(TAG, "initCamera: Failed to start preview"); + } + } + + private void handleStartPreviewReq(MsgHeader header, byte[] body) { + mWidth = (short)((body[0] << 8) | (body[1] & 0x00FF)); + mHeight = (short)((body[2] << 8) | (body[3] & 0x00FF)); + mFrameRate = (body[4] << 8) | (body[5] & 0x00FF); + mDevId = header.mDeviceId; + Context context = mContext.getApplicationContext(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; + if (context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((Activity) mContext, + new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, + DEV_TYPE_CAMERA); + } else { + initCamera(); + } + } + + private void handleStopPreviewReq(MsgHeader header, byte[] body) { + mVirtualCamera.setOnRecvData(null); + mVirtualCamera.stopPreview(); + byte[] rspBody = new byte[RSP_RESULT_LENGTH]; + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_STOP_PREVIEW_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + private void handleIsSupportH264Req(MsgHeader header, byte[] body) { + boolean isSupportH264 = mVirtualCamera.isSupportH264(); + byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_SUPPORT_H264_LENGTH]; + rspBody[2] = (byte) ((isSupportH264 ? 1 : 0) & 0x00FF); + int rspMsgLen = MSG_HEADER_LEN + RSP_SUPPORT_H264_LENGTH; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_IS_SUPPORT_H264_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + private void handleGetSizeReq(MsgHeader header, byte[] body) { + int width = -1; + int height = -1; + + byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_SIZE_LENGTH]; + rspBody[0] = 0x0; + rspBody[1] = 0x0; + rspBody[2] = (byte) (width >> 8); + rspBody[3] = (byte) (width & 0x00FF); + rspBody[4] = (byte) (height >> 8); + rspBody[5] = (byte) (height & 0x00FF); + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + RSP_SIZE_LENGTH; + MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_SIZE_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA); + } + + + class CameraDataListener implements IVirtualDeviceDataListener { + @Override + public void onRecvData(Object... args) { + byte[] data = (byte[]) args[0]; + int length = (int) args[1]; + int devId = (int) args[2]; + int repMsgLen = length + MSG_HEADER_LEN; + MsgHeader header = new MsgHeader(OPT_CAMERA_FRAME, DEV_TYPE_CAMERA, (short) devId, repMsgLen); + byte[] repBody = new byte[length]; + System.arraycopy(data, 0, repBody, 0, length); + mVirtualDeviceProtocol.sendMsg(header, repBody, CAMERA_DATA); + } + } + +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c7d0f5ef775ecbbd8ffddea257469f42a6b4814a --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +public interface IVirtualDeviceDataListener { + + /** + * 接受数据 + * @param args + */ + void onRecvData(Object...args); +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java new file mode 100644 index 0000000000000000000000000000000000000000..2bb0b98013c144ad22524b34ea6b7965f21014a2 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +public interface IVirtualDeviceIO { + + /** + * 读取N个字节的数据 + * @param data + * @param offset + * @param length + * @return + */ + int readN(byte[] data, int offset, int length); + + /** + * 发送N个字节数据 + * @param data + * @param offset + * @param length + * @param deviceType + * @return + */ + int writeN(byte[] data, int offset, int length, int deviceType); +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java new file mode 100644 index 0000000000000000000000000000000000000000..d244a7e16d7194b004cde2bd4e881992ad278708 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +import android.content.Context; +import android.view.SurfaceHolder; + +public class ParamBundle { + static private Context mContext; + static private SurfaceHolder mSurfaceHolder; + + public static void setAppContext(Context context) { + mContext = context; + } + + public static void setSurfaceHolder(SurfaceHolder surfaceHolder) { + mSurfaceHolder = surfaceHolder; + } + + public static SurfaceHolder getSurfaceHolder() { + return mSurfaceHolder; + } + + public static Context getAppContext() { + return mContext; + } + + public static void resetParam() { + mContext = null; + mSurfaceHolder = null; + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java new file mode 100644 index 0000000000000000000000000000000000000000..97e9e8a80be8aa3fea69688706b8c182635bbb57 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +import java.util.Arrays; + +public class RingBuffer { + private final static int DEFAULT_SIZE = 1024; + private Object[] buffer; + private int head = 0; + private int tail = 0; + private int bufferSize; + + public RingBuffer() { + this.bufferSize = DEFAULT_SIZE; + this.buffer = new Object[bufferSize]; + } + + public RingBuffer(int size) { + this.bufferSize = size; + this.buffer = new Object[bufferSize]; + } + + private Boolean isEmpty() { + return head == tail; + } + + private Boolean isFull() { + return (tail + 1) % bufferSize == head; + } + + public void clear() { + Arrays.fill(buffer, null); + this.head = 0; + this.tail = 0; + } + + public Boolean put(Object v) { + if (isFull()) { + return false; + } + buffer[tail] = v; + tail = (tail + 1) % bufferSize; + return true; + } + + public Object get() { + if (isEmpty()) { + return null; + } + Object result = buffer[head]; + head = (head + 1) % bufferSize; + return result; + } + + public Object[] getAll() { + if (isEmpty()) { + return new Object[0]; + } + int copyTail = tail; + int count = head < copyTail ? copyTail - head : bufferSize - head + copyTail; + Object[] result = new String[count]; + if (head < copyTail) { + if (copyTail - head >= 0) + System.arraycopy(buffer, head, result, 0, copyTail - head); + } else { + if (bufferSize - head >= 0) + System.arraycopy(buffer, head, result, 0, bufferSize - head); + if (copyTail >= 0) System.arraycopy(buffer, 0, result, bufferSize - head, copyTail); + } + head = copyTail; + return result; + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java new file mode 100644 index 0000000000000000000000000000000000000000..4a8f7ed6c89cae4c4fd99214de7630ca58b70307 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +import com.huawei.cloudphone.api.CloudPhoneManager; + +import java.nio.ByteBuffer; + +public class RingBufferVirtualDeviceIO implements IVirtualDeviceIO{ + private RingBuffer mRingBuffer; + private int mDataLen; + private int mDataOffset; + private byte[] mDataBuffer; + private Object mObjectLock = new Object(); + private static final int VIRTUAL_CAMERA = 0; + private static final int VIRTUAL_MICROPHONE = 1; + private static final int VIRTUAL_SENSOR = 2; + + public RingBufferVirtualDeviceIO() { + mDataOffset = 0; + mDataBuffer = null; + mDataLen = 0; + mRingBuffer = new RingBuffer(); + } + + + @Override + public int readN(byte[] data, int offset, int length) { + int ret; + synchronized (mObjectLock) { + if (mDataOffset < mDataLen && mDataBuffer != null) { + int oneTimeReadLength = Math.min(length, mDataLen - mDataOffset); + System.arraycopy(mDataBuffer, mDataOffset, data, offset, oneTimeReadLength); + mDataOffset += oneTimeReadLength; + ret = oneTimeReadLength; + } else { + ByteBuffer byteBuffer = (ByteBuffer) mRingBuffer.get(); + if (byteBuffer != null) { + mDataBuffer = new byte[byteBuffer.capacity()]; + byteBuffer.get(mDataBuffer, 0, mDataBuffer.length); + mDataOffset = 0; + mDataLen = mDataBuffer.length; + } + ret = 0; + } + } + return ret; + } + + @Override + public int writeN(byte[] data, int offset, int length, int deviceType) { + synchronized (mObjectLock) { + CloudPhoneManager.createCloudPhoneInstance().sendVirtualDeviceData((byte) deviceType, data); + } + return length; + } + + public void fillData(byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + mRingBuffer.put(buffer); + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..48d8d5e4e3d0013b6358d8ec194b6ee743e6c4b2 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader; + +public class VirtualDeviceManager { + public static final short DEV_TYPE_CAMERA = 1; + public static final short DEV_TYPE_MICROPHONE = 2; + public static final short DEV_TYPE_SENSOR = 0; + + public void processMsg(MsgHeader header, byte[] body) { + } + + public void start() { + } + + public void stop() { + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..9e3983ec84667e9d029245d017758110074887cb --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java @@ -0,0 +1,259 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.common; + +import static com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager.DEV_TYPE_CAMERA; +import static com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager.GRANT_CAMERA_PERMISSION_SUCCESS_ACTION; +import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager.DEV_TYPE_MICROPHONE; +import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager.DEV_TYPE_SENSOR; +import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophoneManager.GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.SensorManager; +import android.util.Log; + +import com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager; +import com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophoneManager; +import com.huawei.cloudphone.virtualdevice.sensor.VirtualSensorManager; + +import java.util.HashMap; +import java.util.Map; + +public class VirtualDeviceProtocol { + private static final String TAG = "VirtualDeviceProtocol"; + public static int MSG_HEADER_LEN = 16; + public static int WAITING_INTERVAL = 200; + private IVirtualDeviceIO mVirtualDeviceIO; + private boolean mIsTaskRun; + private PacketParseThread mPktProcessThread; + static Object mSendMsgLock = new Object(); + private Map virtualDeviceManagers; + private Context mContext; + private BroadcastReceiver mPermissionResultReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (GRANT_CAMERA_PERMISSION_SUCCESS_ACTION.equals(intent.getAction())) { + ((VirtualCameraManager)virtualDeviceManagers.get(DEV_TYPE_CAMERA)).initCamera(); + } else if (GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION.equals(intent.getAction())) { + ((VirtualMicrophoneManager)virtualDeviceManagers.get(DEV_TYPE_MICROPHONE)).initMicrophone(); + } + } + }; + + public VirtualDeviceProtocol(IVirtualDeviceIO virtualDeviceIO ,Context context) { + mVirtualDeviceIO = virtualDeviceIO; + mContext = context; + initVirtualDeviceManagers(); + } + + private void initVirtualDeviceManagers() { + virtualDeviceManagers = new HashMap(); + virtualDeviceManagers.put(DEV_TYPE_CAMERA, new VirtualCameraManager(this, mContext)); + virtualDeviceManagers.put(DEV_TYPE_MICROPHONE, new VirtualMicrophoneManager(this, mContext)); + virtualDeviceManagers.put(DEV_TYPE_SENSOR, new VirtualSensorManager(this, + (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE))); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(GRANT_CAMERA_PERMISSION_SUCCESS_ACTION); + intentFilter.addAction(GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION); + mContext.registerReceiver(mPermissionResultReceiver, intentFilter); + } + + public void processMsg(MsgHeader header, byte[] body) { + VirtualDeviceManager virtualDeviceManager = virtualDeviceManagers.get(header.mDeviceType); + if (virtualDeviceManager == null) { + Log.e(TAG, "processMsg: Error msg type :" + header.mDeviceType); + return; + } + virtualDeviceManager.processMsg(header, body); + } + + + public void startProcess() { + mIsTaskRun = true; + mPktProcessThread = new PacketParseThread(); + mPktProcessThread.start(); + for (Map.Entry entry : virtualDeviceManagers.entrySet()) { + entry.getValue().start(); + } + mContext.unregisterReceiver(mPermissionResultReceiver); + } + + public void stopProcess() { + mIsTaskRun = false; + try { + mPktProcessThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "stopProcess: failed to stop mPktProcessThread.", e); + } + for (Map.Entry entry : virtualDeviceManagers.entrySet()) { + entry.getValue().stop(); + } + } + + public void sendMsg(MsgHeader header, byte[] body, int deviceType) { + synchronized (mSendMsgLock) { + byte[] headerData = header.getData(); + writeN(headerData, 0, headerData.length, deviceType); + if(body != null) { + writeN(body, 0, body.length, deviceType); + } + } + } + + private int readN(byte[] data, int offset, int len) { + int readLen = 0; + while(len > 0) { + if(!mIsTaskRun) { + break; + } + int retLen = mVirtualDeviceIO.readN(data, offset, len); + if(retLen > 0) { + len -= retLen; + offset += retLen; + readLen += retLen; + continue; + } + try { + Thread.sleep(WAITING_INTERVAL); + } catch (InterruptedException e) { + Log.e(TAG, "readN: sleep is interrupt", e); + } + } + return readLen; + } + + private int writeN(byte[] data, int offset, int len, int deviceType) { + int writeLen = 0; + while(len > 0) { + if(!mIsTaskRun) { + break; + } + int retLen = mVirtualDeviceIO.writeN(data, offset, len, deviceType); + if(retLen > 0) { + len -= retLen; + offset += retLen; + writeLen += retLen; + continue; + } + try { + Thread.sleep(WAITING_INTERVAL); + } catch (InterruptedException e) { + Log.e(TAG, "readN: sleep is interrupt", e); + } + } + return writeLen; + } + + class PacketParseThread extends Thread { + byte[] header = new byte[MSG_HEADER_LEN]; + @Override + public void run(){ + while (mIsTaskRun) { + if (readN(header, 0, MSG_HEADER_LEN) != MSG_HEADER_LEN) { + Log.e(TAG, "Read msg header error"); + continue; + } + MsgHeader msgHeader = new MsgHeader(header); + int bodyLen = msgHeader.mPayloadLength; + byte[] body = new byte[bodyLen]; + if (readN(body, 0, bodyLen) != bodyLen) { + Log.e(TAG, "Read msg header error"); + continue; + } + processMsg(msgHeader, body); + } + } + } + + public static class MsgHeader { + public short mVersion; + public short mOptType; + public short mDeviceType; + public short mDeviceId; + public short mNextProtocol; + public short mHopLimit; + public int mPayloadLength; + private byte[] mData = null; + + public MsgHeader(byte[] data) { + initParam(data); + mData = new byte[data.length]; + System.arraycopy(data, 0, mData, 0, data.length); + } + + public MsgHeader(short optType, short devType, short devId, int msgLen) { + mData = new byte[MSG_HEADER_LEN]; + mData[0] = 0; + mData[1] = 0x01; + + mData[2] = (byte) (optType >> 8); + mData[3] = (byte) (optType & 0x00FF); + + mData[4] = (byte) (devType >> 8); + mData[5] = (byte) (devType & 0xFF); + + mData[6] = (byte) (devId >> 8); + mData[7] = (byte) (devId & 0xFF); + + mData[8] = (byte) ((msgLen & 0xFF000000) >> 24); + mData[9] = (byte) ((msgLen & 0x00FF0000) >> 16); + mData[10] = (byte) ((msgLen & 0x0000FF00) >> 8); + mData[11] = (byte) (msgLen & 0x000000FF); + + mData[12] = 0x00; + mData[13] = 0x00; + + mData[14] = 0x00; + mData[15] = 0x00; + + initParam(mData); + } + + private void initParam(byte[] data) { + mVersion = (short) ((data[0] << 8) | (data[1] & 0x0FF)); + mOptType = (short) ((data[2] << 8) | (data[3] & 0x0FF)); + mDeviceType = (short) ((data[4] << 8) | (data[5] & 0x0FF)); + mDeviceId = (short) ((data[6] << 8) | (data[7] & 0x0FF)); + mPayloadLength = ((data[8] << 24) | ((data[9] & 0x0FF) << 16) + | ((data[10] & 0x0FF) << 8) | (data[11] & 0x0FF)) - MSG_HEADER_LEN; + mNextProtocol = (short) ((data[12] << 8) | (data[13] & 0x0FF)); + mHopLimit = (short) ((data[14] << 8) | (data[15] & 0x0FF)); + } + + public byte[] getData() { + return mData; + } + + @Override + public String toString() { + return "MsgHeader{" + + "mVersion=" + mVersion + + ", mOptType=" + mOptType + + ", mDeviceType=" + mDeviceType + + ", mDeviceId=" + mDeviceId + + ", mPayloadLength=" + mPayloadLength + + ", mNextProtocol=" + mNextProtocol + + ", mHopLimit=" + mHopLimit + + "}"; + } + } +} + + diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java new file mode 100644 index 0000000000000000000000000000000000000000..fca6edf5976b8a14a77370a1b66685dc70eac7f0 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.microphone; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.util.Log; + +import com.huawei.cloudphone.jniwrapper.OpusJNIWrapper; +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; + +import java.util.Map; +import java.util.Objects; + +public class VirtualMicrophone { + private static final String TAG = "VirtualMicrophone"; + private static final int AUDIO_INPUT = MediaRecorder.AudioSource.MIC; + static final int DEFAULT_SAMPLE_RATE = 48000; + static final int DEFAULT_CHANNEL = 1; + static final int DEFAULT_BUFFER_SIZE = 320; + static final String SAMPLE_RATE = "sample_rate"; + static final String MAX_FRAME_SIZE = "max_frame_size"; + static final String CHANNEL = "channel"; + static final String FORMAT = "format"; + + private AudioRecord mAudioRecord; + private int mRecordBuffSize = 320; + private int mMaxFrameSize; + private int mSampleRate; + private int mChannel; + private int mSampleFormat; + private boolean mIsReadTaskRunning; + private ReadThread mThread; + private IVirtualDeviceDataListener mListener; + private boolean mIsStart = false; + + public VirtualMicrophone() { + mAudioRecord = null; + mRecordBuffSize = DEFAULT_BUFFER_SIZE; + mSampleRate = DEFAULT_SAMPLE_RATE; + mChannel = DEFAULT_CHANNEL; + mSampleFormat = AudioFormat.ENCODING_PCM_16BIT; + mIsReadTaskRunning = false; + mListener = null; + } + + public int start() { + if (mIsStart) return 0; + if (OpusJNIWrapper.createOpusEncoder(mSampleRate, DEFAULT_CHANNEL) != 0) { + return -1; + } + mRecordBuffSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, AudioFormat.ENCODING_PCM_16BIT); + mAudioRecord = new AudioRecord(AUDIO_INPUT, mSampleRate, mChannel, mSampleFormat, mRecordBuffSize); + try { + mAudioRecord.startRecording(); + } catch (IllegalStateException e) { + Log.e(TAG, "start failed, ", e); + return -1; + } + mIsReadTaskRunning = true; + mThread = new ReadThread(); + mThread.start(); + mIsStart = true; + return 0; + } + + public int stop() { + if (!mIsStart) return 0; + mIsReadTaskRunning = false; + try { + if (mThread != null) { + mThread.join(); + mThread = null; + } + mAudioRecord.stop(); + } catch (IllegalStateException | InterruptedException e) { + Log.e(TAG, "stop failed, ", e); + return -1; + } + + mAudioRecord.release(); + mAudioRecord = null; + OpusJNIWrapper.destroyOpusEncoder(); + mIsStart = false; + return 0; + } + + public void setOnRecvDataListener(IVirtualDeviceDataListener listener) { + mListener = listener; + } + + public int setParameters(Map params) { + try { + mSampleRate = Integer.parseInt(Objects.requireNonNull(params.get(SAMPLE_RATE))); + if (mSampleRate < 0 ) mSampleFormat = DEFAULT_SAMPLE_RATE; + mMaxFrameSize = Integer.parseInt(Objects.requireNonNull(params.get(MAX_FRAME_SIZE))); + mChannel = Integer.parseInt(Objects.requireNonNull(params.get(CHANNEL))); + if (Integer.parseInt(Objects.requireNonNull(params.get(FORMAT))) == 1) + mSampleFormat = AudioFormat.ENCODING_PCM_16BIT; + } catch (RuntimeException e) { + Log.e(TAG, "setParameters: failed to set param", e); + return -1; + } + Log.i(TAG, "mSampleRate = " + mSampleRate + ", mMaxFrameSize = " + mMaxFrameSize + + ", mChannel = " + mChannel + ", mSampleFormat = " + mSampleFormat); + return 0; + } + + class ReadThread extends Thread { + @Override + public void run() { + short[] bytes = new short[mRecordBuffSize]; + byte[] outBuff = new byte[mRecordBuffSize]; + while (mIsReadTaskRunning) { + mAudioRecord.read(bytes, 0, bytes.length); + if (mListener != null) { + int outBufLen = OpusJNIWrapper.opusEncode(bytes, bytes.length, outBuff); + if (outBufLen > 0) { + mListener.onRecvData(outBuff, 0, outBufLen); + } + } + } + } + } + +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java new file mode 100644 index 0000000000000000000000000000000000000000..b80a81b620400dedab6b8817ff8c1d014cf2b452 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.microphone; + +import static com.huawei.cloudphone.jniwrapper.JNIWrapper.MICROPHONE_DATA; +import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN; +import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.CHANNEL; +import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.FORMAT; +import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.MAX_FRAME_SIZE; +import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.SAMPLE_RATE; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader; + +import java.util.HashMap; +import java.util.Map; + +public class VirtualMicrophoneManager extends VirtualDeviceManager { + private static final String TAG = "VirtualMICManager"; + private static final short DEV_MIC_ID = 0; + + private static final short OPT_MICROPHONE_START_RECORD_REQ = 0x1; + private static final short OPT_MICROPHONE_START_RECORD_RSP = 0x1001; + private static final short OPT_MICROPHONE_STOP_RECORD_REQ = 0x2; + private static final short OPT_MICROPHONE_STOP_RECORD_RSP = 0x1002; + private static final short OPT_MICROPHONE_SET_PARAM_REQ = 0x3; + private static final short OPT_MICROPHONE_SET_PARAM_RSP = 0x1003; + private static final short OPT_MICROPHONE_DATA = 0x10; + + private static final int RSP_RESULT_LENGTH = 2; + private static final int RSP_AUDIO_TYPE_LENGTH = 2; + public static final String GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION = "android.intent.action.GRANT_MICROPHONE_PERMISSION_SUCCESS"; + + private VirtualMicrophone mVirtualMicrophone; + private VirtualDeviceProtocol mVirtualDeviceProtocol; + private Context mContext; + + public VirtualMicrophoneManager(VirtualDeviceProtocol virtualDeviceProtocol, Context context) { + mVirtualDeviceProtocol = virtualDeviceProtocol; + mVirtualMicrophone = new VirtualMicrophone(); + mContext = context; + } + + public void processMsg(MsgHeader header, byte[] msgBody) { + switch (header.mOptType) { + case OPT_MICROPHONE_START_RECORD_REQ: + Log.i(TAG, "processMsg: start record"); + handleStartRecordReq(msgBody); + break; + case OPT_MICROPHONE_STOP_RECORD_REQ: + Log.i(TAG, "processMsg: stop record"); + handleStopRecordReq(msgBody); + break; + case OPT_MICROPHONE_SET_PARAM_REQ: + Log.i(TAG, "processMsg: set param"); + handleSetParamReq(msgBody); + break; + } + } + + public void initMicrophone() { + mVirtualMicrophone.setOnRecvDataListener(new MicrophoneDataListener()); + if (mVirtualMicrophone.start() != 0) { + Log.e(TAG, "initMicrophone: failed to start microphone"); + } + } + + private void handleStartRecordReq(byte[] msgBody) { + Context context = mContext.getApplicationContext(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; + if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((Activity) mContext, + new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE}, + DEV_TYPE_MICROPHONE); + } else { + initMicrophone(); + } + } + + private void handleStopRecordReq(byte[] msgBody) { + int result = mVirtualMicrophone.stop(); + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH; + byte[] rspBody = new byte[RSP_RESULT_LENGTH]; + rspBody[0] = 0x0; + rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1); + MsgHeader rspHeader = new MsgHeader(OPT_MICROPHONE_STOP_RECORD_RSP, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, MICROPHONE_DATA); + } + + private void handleSetParamReq(byte[] msgBody) { + int maxFrameSize = (msgBody[0] << 8) | (msgBody[1] & 0x0FF); + int format = (msgBody[2] << 8) | (msgBody[3] & 0x0FF); + int sampleRate = (msgBody[4] << 8) | (msgBody[5] & 0x0FF); + int channel = (msgBody[6] << 8) | (msgBody[7] & 0x0FF); + Log.i(TAG, "mSampleRate = " + sampleRate + ", mMaxFrameSize = " + maxFrameSize + + ", mChannel = " + channel + ", mSampleFormat = " + format); + + Map paramMap = new HashMap<>(); + paramMap.put(FORMAT, Integer.toString(format)); + paramMap.put(MAX_FRAME_SIZE, Integer.toString(maxFrameSize)); + paramMap.put(SAMPLE_RATE, Integer.toString(sampleRate)); + paramMap.put(CHANNEL, Integer.toString(channel)); + + int result = mVirtualMicrophone.setParameters(paramMap); + byte[] rspBody = new byte[RSP_RESULT_LENGTH]; + rspBody[0] = 0x0; + rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1); + + int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH; + MsgHeader header = new MsgHeader(OPT_MICROPHONE_SET_PARAM_RSP, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(header, rspBody, MICROPHONE_DATA); + } + + public void stop() { + mVirtualMicrophone.stop(); + } + + public class MicrophoneDataListener implements IVirtualDeviceDataListener { + + @Override + public void onRecvData(Object... args) { + byte[] data = (byte[]) args[0]; + int offset = (int) args[1]; + int length = (int) args[2]; + int rspMsgLen = length + MSG_HEADER_LEN; + MsgHeader header = new MsgHeader(OPT_MICROPHONE_DATA, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen); + byte[] rspBody = new byte[length]; + System.arraycopy(data, offset, rspBody, 0, length); + mVirtualDeviceProtocol.sendMsg(header, rspBody, MICROPHONE_DATA); + } + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java new file mode 100644 index 0000000000000000000000000000000000000000..9850e196da9c1ba21a281e5677e73484c8d9a76a --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.sensor; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; + +public class VirtualSensor implements SensorEventListener { + private static final String TAG = "VirtualSensor"; + private SensorManager mSensorManager; + private Sensor mSensor; + private Sensor mAccelerationSensor; + private IVirtualDeviceDataListener mListener = null; + private HandlerThread mHandlerThread; + private boolean mIsStart = false; + + public VirtualSensor(SensorManager sensorManager) { + mSensorManager = sensorManager; + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + mAccelerationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + void registerSensorDataListener(IVirtualDeviceDataListener listener) { + mListener = listener; + } + + public void startProcess() { + Log.i(TAG, "startProcess"); + if (mIsStart) { + return; + } + mHandlerThread = new HandlerThread("sensorThread"); + mHandlerThread.start(); + Handler handler = new Handler(mHandlerThread.getLooper()); + mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_GAME, handler); + mSensorManager.registerListener(this, mAccelerationSensor, SensorManager.SENSOR_DELAY_GAME, handler); + mIsStart = true; + } + + public void stopProcess() { + Log.i(TAG, "stopProcess"); + if (!mIsStart) { + return; + } + mSensorManager.unregisterListener(this, mSensor); + mSensorManager.unregisterListener(this, mAccelerationSensor); + try { + mHandlerThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "stopProcess: failed to stop mHandlerThread", e); + } + mIsStart = false; + } + + @Override + public void onSensorChanged(SensorEvent sensorEvent) { + if (mListener == null) { + return; + } + float x = sensorEvent.values[0]; + float y = sensorEvent.values[1]; + float z = sensorEvent.values[2]; + int type = sensorEvent.sensor.getType(); + if (type == Sensor.TYPE_ACCELEROMETER) { + mListener.onRecvData(x, y, z, sensorEvent.accuracy, 0); + } else if (type == Sensor.TYPE_GYROSCOPE) { + mListener.onRecvData(x, y, z, sensorEvent.accuracy, 1); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { + } +} diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java new file mode 100644 index 0000000000000000000000000000000000000000..c363a67e4aee6741349a214f5ffde461f6fe3345 --- /dev/null +++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. + * + * 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.huawei.cloudphone.virtualdevice.sensor; + +import static com.huawei.cloudphone.jniwrapper.JNIWrapper.SENSOR_DATA; +import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN; + +import android.hardware.SensorManager; +import android.util.Log; + +import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol; +import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader; + +public class VirtualSensorManager extends VirtualDeviceManager { + private static final String TAG = "VirtualSensorManager"; + + public static final short DEV_GYROSCOPE = 0x0; + public static final short DEV_ACCELERATION = 0x1; + public static final short OPT_SENSOR_ENABLE_REQ = 0x1; + public static final short OPT_SENSOR_ENABLE_RSP = 0x1001; + public static final short OPT_SENSOR_DISABLE_REQ = 0x2; + public static final short OPT_SENSOR_DISABLE_RSP = 0x1002; + public static final short OPT_SENSOR_DATA = 0x10; + + private VirtualSensor mVirtualSensor; + private VirtualDeviceProtocol mVirtualDeviceProtocol; + + public VirtualSensorManager(VirtualDeviceProtocol virtualDeviceProtocol, SensorManager sensorManager) { + mVirtualDeviceProtocol = virtualDeviceProtocol; + mVirtualSensor = new VirtualSensor(sensorManager); + } + + public void processMsg(MsgHeader header, byte[] body) { + switch (header.mOptType) { + case OPT_SENSOR_ENABLE_RSP: + Log.i(TAG, "processMsg: enable sensor"); + mVirtualSensor.registerSensorDataListener(new SensorDataListener()); + mVirtualSensor.startProcess(); + break; + case OPT_SENSOR_DISABLE_RSP: + Log.i(TAG, "processMsg: disable sensor"); + mVirtualSensor.stopProcess(); + break; + default: + Log.e(TAG, "processMsg: error opt type"); + } + } + + public void start() { + int rspMsgLen = MSG_HEADER_LEN; + MsgHeader header = new MsgHeader(OPT_SENSOR_ENABLE_REQ, DEV_TYPE_SENSOR, DEV_GYROSCOPE, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA); + header = new MsgHeader(OPT_SENSOR_ENABLE_REQ, DEV_TYPE_SENSOR, DEV_ACCELERATION, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA); + } + + public void stop() { + int rspMsgLen = MSG_HEADER_LEN; + MsgHeader header = new MsgHeader(OPT_SENSOR_DISABLE_REQ, DEV_TYPE_SENSOR, DEV_GYROSCOPE, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA); + header = new MsgHeader(OPT_SENSOR_DISABLE_REQ, DEV_TYPE_SENSOR, DEV_ACCELERATION, rspMsgLen); + mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA); + } + + class SensorDataListener implements IVirtualDeviceDataListener { + @Override + public void onRecvData(Object... args) { + String xStr = Float.valueOf((float) args[0]).toString(); + String yStr = Float.valueOf((float) args[1]).toString(); + String zStr = Float.valueOf((float) args[2]).toString(); + String accurateStr = Integer.valueOf((int) args[3]).toString(); + String body = xStr + ":" + yStr + ":" + zStr + ":" + accurateStr + ":"; + int type = (int) args[4]; + int bodyLen = body.getBytes().length; + int rspMsgLen = bodyLen + MSG_HEADER_LEN; + MsgHeader header = new MsgHeader(OPT_SENSOR_DATA, DEV_TYPE_SENSOR, (short)type, rspMsgLen); + byte[] rspBody = new byte[bodyLen]; + System.arraycopy(body.getBytes(), 0, rspBody, 0, bodyLen); + mVirtualDeviceProtocol.sendMsg(header, rspBody, SENSOR_DATA); + + } + } +}