diff --git a/README.md b/README.md index 7fa8dd4..400ef9b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ For setup guide and button templates, please check out the [WIKI](https://github ### Android Studio -- Version 3.5 or higher. May not be buildable on lower versions +- Version 3.6 or higher. May not be buildable on lower versions - Android SDK (should be installed with Android Studio) - Java 7 or 8 JDK (might be installed with Android Studio) diff --git a/app/build.gradle b/app/build.gradle index 1d9105f..fe3a5a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "tv.remo.android.controller" minSdkVersion 16 targetSdkVersion 28 - versionCode 12 - versionName "0.14.0" + versionCode 13 + versionName "0.15.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/java/tv/remo/android/controller/fragments/SettingsDisplay.kt b/app/src/main/java/tv/remo/android/controller/fragments/SettingsDisplay.kt index 860fcac..24ad628 100644 --- a/app/src/main/java/tv/remo/android/controller/fragments/SettingsDisplay.kt +++ b/app/src/main/java/tv/remo/android/controller/fragments/SettingsDisplay.kt @@ -1,7 +1,26 @@ package tv.remo.android.controller.fragments +import android.content.Intent +import android.os.Bundle +import androidx.preference.Preference import tv.remo.android.controller.R import tv.remo.android.settingsutil.fragments.BasePreferenceFragmentCompat class SettingsDisplay : BasePreferenceFragmentCompat( R.xml.settings_display -) +){ + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + super.onCreatePreferences(savedInstanceState, rootKey) + handleDozeKey() + } + + private fun handleDozeKey() { + activity?:return + val pref = preferenceManager.findPreference( + getString(R.string.dozeSystemSettingsKey) + ) ?: return + val launchIntent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS") + if(launchIntent.resolveActivity(activity!!.packageManager) == null) return + pref.intent = launchIntent + pref.isVisible = true + } +} diff --git a/app/src/main/java/tv/remo/android/controller/fragments/SettingsRobot.kt b/app/src/main/java/tv/remo/android/controller/fragments/SettingsRobot.kt index e15ab0d..a637157 100644 --- a/app/src/main/java/tv/remo/android/controller/fragments/SettingsRobot.kt +++ b/app/src/main/java/tv/remo/android/controller/fragments/SettingsRobot.kt @@ -1,6 +1,8 @@ package tv.remo.android.controller.fragments +import android.bluetooth.BluetoothAdapter import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle import android.view.View import android.widget.Toast @@ -12,6 +14,8 @@ import org.btelman.controlsdk.utils.ClassScanner import tv.remo.android.controller.R import tv.remo.android.settingsutil.fragments.BasePreferenceFragmentCompat import tv.remo.android.settingsutil.preferences.ListSettingsPreference +import java.util.* +import kotlin.collections.ArrayList class SettingsRobot : BasePreferenceFragmentCompat( R.xml.settings_robot, @@ -46,12 +50,22 @@ class SettingsRobot : BasePreferenceFragmentCompat( pref ?: return //skip if null val classes = ClassScanner.getClassesWithAnnotation(context!!, annotation) val classNames = ArrayList() - classes.forEach { - classNames.add(it.name) - } val simpleClassNames = ArrayList() classes.forEach { - simpleClassNames.add(it.simpleName) + var deviceSupportsHardware = true + if(annotation == DriverComponent::class.java){ + //check via hardcoding for now. + if(it.name.toLowerCase(Locale.US).contains("bluetooth")){ + //driver is a bluetooth driver + if(!context!!.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) + deviceSupportsHardware = false //no support found + //TODO check for ble? + } + } + if(deviceSupportsHardware){ + classNames.add(it.name) + simpleClassNames.add(it.simpleName) + } } val nameArray = simpleClassNames.toArray(Array(0){""}) val valueArray = classNames.toArray(Array(0){""}) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0991568..9839634 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Camera Device ID (0 is back) Resolution, Orientation, Bitrate Resolution + Auto Focus Orientation Camera2 Disabled Device is using legacy camera API. diff --git a/app/src/main/res/xml/settings_camera.xml b/app/src/main/res/xml/settings_camera.xml index 04d11eb..5966ed0 100644 --- a/app/src/main/res/xml/settings_camera.xml +++ b/app/src/main/res/xml/settings_camera.xml @@ -22,6 +22,14 @@ app:useSimpleSummaryProvider="true" app:entries="@array/resolution_pref_list" app:entryValues="@array/resolution_pref_list"/> + - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 66af967..644ce58 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1a28833..9c34e47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/licensehelper/src/main/java/org/btelman/licensehelper/LicenseType.kt b/licensehelper/src/main/java/org/btelman/licensehelper/LicenseType.kt index df20b5f..ec80c04 100644 --- a/licensehelper/src/main/java/org/btelman/licensehelper/LicenseType.kt +++ b/licensehelper/src/main/java/org/btelman/licensehelper/LicenseType.kt @@ -7,6 +7,7 @@ enum class LicenseType { APACHE2_0, MIT, GPL3, + LGPL3, BSD3Clause; fun getDefaultLink(): String { @@ -14,6 +15,7 @@ enum class LicenseType { APACHE2_0 -> "https://www.apache.org/licenses/LICENSE-2.0.txt" MIT -> "https://opensource.org/licenses/MIT" GPL3 -> "https://www.gnu.org/licenses/gpl-3.0.txt" + LGPL3 -> "https://www.gnu.org/licenses/lgpl-3.0.txt" BSD3Clause -> "https://opensource.org/licenses/BSD-3-Clause" } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/RemoSettingsUtil.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/RemoSettingsUtil.kt index f508d7c..56dfb7c 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/RemoSettingsUtil.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/RemoSettingsUtil.kt @@ -41,6 +41,7 @@ class RemoSettingsUtil(context : Context, sharedPreferences: SharedPreferences) //Camera related settings val cameraEnabled = BooleanPref(context, sharedPreferences, R.string.cameraSettingsEnableKey, false) val cameraResolution = StringPref(context, sharedPreferences, R.string.cameraResolutionKey, "640x480") + val cameraFocus = StringPref(context, sharedPreferences, R.string.cameraFocusKey, "auto") val cameraOrientation = StringPref(context, sharedPreferences, R.string.cameraOrientationKey, "DIR_90") val cameraDeviceId = IntPref(context, sharedPreferences, R.string.cameraDeviceIdKey, 0) val cameraBitrate = StringPref(context, sharedPreferences, R.string.cameraBitrateKey, "1024") @@ -68,6 +69,10 @@ class RemoSettingsUtil(context : Context, sharedPreferences: SharedPreferences) val keepScreenOn = BooleanPref(context, sharedPreferences, R.string.displayPersistKey, false) val hideScreenControls = BooleanPref(context, sharedPreferences, R.string.autoHideControlsEnabledKey, false) + //misc settings + val streamSleepMode = BooleanPref(context, sharedPreferences, R.string.streamAutoSleepEnabledKey, false) + val streamSleepTimeOut = IntPref(context, sharedPreferences, R.string.streamAutoSleepTimeoutKey, 5*60) //5 minutes + companion object{ private var INSTANCE : RemoSettingsUtil? = null diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoCommandHandler.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoCommandHandler.kt index 00cc271..172e19e 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoCommandHandler.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoCommandHandler.kt @@ -7,6 +7,7 @@ import org.btelman.controlsdk.models.Component import org.btelman.controlsdk.models.ComponentEventObject import org.btelman.controlsdk.tts.TTSBaseComponent import tv.remo.android.controller.sdk.RemoSettingsUtil +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender import tv.remo.android.controller.sdk.models.api.User import tv.remo.android.controller.sdk.utils.ChatUtil import kotlin.system.exitProcess @@ -21,7 +22,7 @@ import kotlin.system.exitProcess * Commands can come from a button or from the chat. Commands from chat are only able to be used * by the owner and moderators */ -class RemoCommandHandler : Component(){ +class RemoCommandHandler : Component(), RemoCommandSender { private var devModeEnabled = false private var stationary = false private var serverOwner = "" diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoSocketComponent.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoSocketComponent.kt index 4f3a2f5..b025166 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoSocketComponent.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/RemoSocketComponent.kt @@ -13,6 +13,7 @@ import org.btelman.controlsdk.models.ComponentEventObject import org.btelman.controlsdk.tts.TTSBaseComponent import org.json.JSONObject import tv.remo.android.controller.sdk.RemoSettingsUtil +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender import tv.remo.android.controller.sdk.models.api.* import tv.remo.android.controller.sdk.utils.ChatUtil import tv.remo.android.controller.sdk.utils.EndpointBuilder @@ -24,7 +25,7 @@ import tv.remo.android.controller.sdk.utils.isUrl * * Note: Do not instantiate in the activity! Must pass it to the ControlSDK Service */ -class RemoSocketComponent : Component() { +class RemoSocketComponent : Component() , RemoCommandSender { private var socket: WebSocket? = null var apiKey : String? = null var activeChannelId : String? = null diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/StreamCommandHandler.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/StreamCommandHandler.kt index 95d3791..6e22bd8 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/StreamCommandHandler.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/StreamCommandHandler.kt @@ -5,6 +5,7 @@ import android.os.Bundle import org.btelman.controlsdk.models.ComponentEventObject import org.btelman.controlsdk.streaming.models.StreamInfo import tv.remo.android.controller.sdk.interfaces.CommandStreamHandler +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender import tv.remo.android.controller.sdk.models.api.Channel import tv.remo.android.controller.sdk.utils.EndpointBuilder @@ -20,7 +21,7 @@ class StreamCommandHandler(val context: Context?, val streamHandler : CommandStr private val subscriptionList = streamHandler.onRegisterCustomCommands() fun handleExternalMessage(message: ComponentEventObject){ - if(message.source is RemoSocketComponent || message.source is RemoCommandHandler){ + if(message.source is RemoCommandSender){ handleSocketCommand(message) } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/audio/RemoAudioComponent.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/audio/RemoAudioComponent.kt index 83bbdf9..57718e7 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/audio/RemoAudioComponent.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/audio/RemoAudioComponent.kt @@ -14,6 +14,7 @@ import tv.remo.android.controller.sdk.components.RemoCommandHandler import tv.remo.android.controller.sdk.components.RemoSocketComponent import tv.remo.android.controller.sdk.components.StreamCommandHandler import tv.remo.android.controller.sdk.interfaces.CommandStreamHandler +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender import tv.remo.android.controller.sdk.models.CommandSubscriptionData import tv.remo.android.controller.sdk.utils.ChatUtil @@ -38,7 +39,7 @@ class RemoAudioComponent : AudioComponent() , CommandStreamHandler { } override fun handleExternalMessage(message: ComponentEventObject): Boolean { - if(message.source is RemoSocketComponent || message.source is RemoCommandHandler){ + if(message.source is RemoCommandSender){ commandHandler?.handleExternalMessage(message) } return super.handleExternalMessage(message) diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/hardware/HardwareWatchdogComponent.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/hardware/HardwareWatchdogComponent.kt index 66d9ea6..dfa6938 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/hardware/HardwareWatchdogComponent.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/hardware/HardwareWatchdogComponent.kt @@ -1,8 +1,13 @@ package tv.remo.android.controller.sdk.components.hardware +import android.content.Context +import android.os.Bundle import org.btelman.controlsdk.enums.ComponentType import org.btelman.controlsdk.models.Component import org.btelman.controlsdk.models.ComponentEventObject +import tv.remo.android.controller.sdk.RemoSettingsUtil +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender +import tv.remo.android.controller.sdk.utils.ChatUtil /** * Watchdog component that will send a stop command @@ -10,7 +15,27 @@ import org.btelman.controlsdk.models.ComponentEventObject * * Not useful if non drive motors are still being controlled after timeout, so solution should really be handled on the bot */ -class HardwareWatchdogComponent : Component() { +class HardwareWatchdogComponent : Component(), RemoCommandSender{ + private var sleepMode = false + /** + * time in seconds to wait to sleep the stream + */ + private var streamSleepTime: Long = 5*60 //default if settings fails + private var sleepEnabled = false + + override fun onInitializeComponent(applicationContext: Context, bundle: Bundle?) { + super.onInitializeComponent(applicationContext, bundle) + reloadSleepSettings(applicationContext) + } + + private fun reloadSleepSettings(maybeContext: Context?) { + val context = maybeContext?:return + RemoSettingsUtil.with(context){ settings -> + sleepEnabled = settings.streamSleepMode.getPref() + streamSleepTime = settings.streamSleepTimeOut.getPref().toLong() + } + } + override fun disableInternal() { //not needed } @@ -23,13 +48,102 @@ class HardwareWatchdogComponent : Component() { if(message.type == ComponentType.HARDWARE){ when(message.what){ EVENT_MAIN -> { - resetTimeout() + (message.data as? String)?.let { + if(it != "stop" && !it.startsWith(".")){ + maybeResetSleepTimer() + } + resetTimeout() + } } } } + else if(message.source is RemoCommandSender){ + (message.data as? String)?.let { + handleStringCommand(message.data as String) + } + } return super.handleExternalMessage(message) } + private fun handleStringCommand(data: String) { + context?:return + when (data) { + ".stream sleep" -> { + sleepMode = true + killSleepTimer() + } + ".stream wakeup" -> { + maybeResetSleepTimer() + sleepMode = false + } + ".stream reset" -> { + maybeResetSleepTimer() + sleepMode = false + } + else -> { + if(data.startsWith(".stream sleeptime ")){ + handleSleepCommand(data.replace(".stream sleeptime ", "").trim()) + } + } + } + } + + private fun handleSleepCommand(data: String) { + var maybeTime : Int? = null + kotlin.runCatching { + maybeTime = data.toInt() + } + maybeTime?.let { time -> + val chatMessage : String = if(time > 0){ + saveSleepTime(time) + "Setting sleeptime to $maybeTime seconds" + } else{ + saveSleepTime(-1) + "Setting sleeptime to disabled (time < 0)" + } + killSleepTimer() + maybeResetSleepTimer() + ChatUtil.sendToSiteChat(eventDispatcher, chatMessage) + } ?: run{ + ChatUtil.sendToSiteChat(eventDispatcher, ".stream sleeptime {seconds}") + } + } + + private fun saveSleepTime(time: Int){ + val context = context?:return + RemoSettingsUtil.with(context){ settings -> + settings.streamSleepMode.savePref(time > 0) + settings.streamSleepTimeOut.savePref(time) + reloadSleepSettings(context) + } + } + + + private fun killSleepTimer(){ + handler.removeCallbacks(sleepRobot) + } + + /** + * Reset the sleep counter when called. + */ + private fun maybeResetSleepTimer() { + if(sleepEnabled){ + if(sleepMode){ + sleepMode = false //apparently we don't get sent our own events back, so track it here + //we want events sent to the bot to wake it up automatically + eventDispatcher?.handleMessage(ComponentType.HARDWARE, EVENT_MAIN, ".stream wakeup", this as RemoCommandSender) + } + killSleepTimer() + handler.postDelayed(sleepRobot, streamSleepTime*1000) + } + } + + private val sleepRobot = Runnable { + eventDispatcher?.handleMessage(ComponentType.HARDWARE, EVENT_MAIN, ".stream sleep", this as RemoCommandSender) + sleepMode = true //apparently we don't get sent our own events back, so track it here + killSleepTimer() + } + private val runnable = Runnable { eventDispatcher?.handleMessage(ComponentType.HARDWARE, EVENT_MAIN, "stop", this) } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera2Override.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera2Override.kt new file mode 100644 index 0000000..f9b0c32 --- /dev/null +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera2Override.kt @@ -0,0 +1,298 @@ +package tv.remo.android.controller.sdk.components.video + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.ImageFormat +import android.hardware.camera2.* +import android.media.Image +import android.media.ImageReader +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.util.Range +import androidx.annotation.NonNull +import androidx.annotation.RequiresApi +import org.btelman.controlsdk.streaming.models.ImageDataPacket +import org.btelman.controlsdk.streaming.models.StreamInfo +import org.btelman.controlsdk.streaming.video.retrievers.BaseVideoRetriever +import tv.remo.android.controller.sdk.RemoSettingsUtil + +/** + * Copied from org.btelman.controlsdk.streaming.video.retrievers.api21.Camera2SurfaceTextureComponent + */ +@RequiresApi(21) +class Camera2Override : BaseVideoRetriever(), ImageReader.OnImageAvailableListener { + + private var data: ByteArray? = null + private var width = 0 + private var height = 0 + + private var focusMode = "video" + + var reader : ImageReader? = null + + private var mPreviewBuilder: CaptureRequest.Builder? = null + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private var mBackgroundThread: HandlerThread? = null + + /** + * A [Handler] for running tasks in the background. + */ + private var mBackgroundHandler: Handler? = null + + /** + * [CameraDevice.StateCallback] is called when [CameraDevice] changes its status. + */ + private val mStateCallback = object : CameraDevice.StateCallback() { + + override fun onOpened(@NonNull cameraDevice: CameraDevice) { + mCameraDevice = cameraDevice + startPreview() + } + + override fun onDisconnected(@NonNull cameraDevice: CameraDevice) { + closePreviewSession() + cameraDevice.close() + mCameraDevice = null + } + + override fun onError(@NonNull cameraDevice: CameraDevice, error: Int) { + closePreviewSession() + cameraDevice.close() + mCameraDevice = null + } + + } + + override fun enable(context: Context, streamInfo: StreamInfo) { + super.enable(context, streamInfo) + setupCamera(streamInfo) + RemoSettingsUtil.with(context){ + focusMode = it.cameraFocus.getPref() + } + } + + override fun disable() { + super.disable() + releaseCamera() + } + + @SuppressLint("MissingPermission") //Already handled. No way to call this + fun setupCamera(streamInfo : StreamInfo?) { + startBackgroundThread() + width = streamInfo?.width ?: 640 + height = streamInfo?.height ?: 640 + reader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2) + reader?.setOnImageAvailableListener(this, mBackgroundHandler) + val manager = context!!.getSystemService(Context.CAMERA_SERVICE) as CameraManager + try { + val list = manager.cameraIdList + val requestedId = streamInfo?.deviceInfo?.getCameraId()?:0 + if(requestedId+1 > list.size){ + throw java.lang.Exception("Attempted to open camera $requestedId. Only ${list.size} cameras exist! 0 is first camera") + } + manager.openCamera(list[requestedId], mStateCallback, null) + } catch (e: CameraAccessException) { + e.printStackTrace() + //TODO throw error and kill service? + } catch (e: NullPointerException) { + e.printStackTrace() + //TODO throw error and kill service? + } catch (e: InterruptedException) { + e.printStackTrace() + //TODO throw error and kill service? + throw RuntimeException("Interrupted while trying to lock camera opening.") + } + } + + private fun releaseCamera() { + stopBackgroundThread() + reader?.close() + closePreviewSession() + mCameraDevice?.close() + } + + private var latestPackage : ImageDataPacket? = null + + override fun grabImageData(): ImageDataPacket? { + return latestPackage + } + + /** + * A reference to the opened [android.hardware.camera2.CameraDevice]. + */ + private var mCameraDevice: CameraDevice? = null + + /** + * A reference to the current [android.hardware.camera2.CameraCaptureSession] for + * preview. + */ + private var mPreviewSession: CameraCaptureSession? = null + + override fun onImageAvailable(reader: ImageReader?) { + var image: Image? = null + try { + image = reader?.acquireLatestImage() + image?.let { + latestPackage = ImageDataPacket(convertYuv420888ToYuv(image), ImageFormat.YUV_420_888) + notifyFrameUpdated() + } + } finally { + image?.close() + } + } + + fun convertYuv420888ToYuv(image: Image): ByteArray { + val yPlane = image.planes[0] + val ySize = yPlane.buffer.remaining() + + val uPlane = image.planes[1] + val vPlane = image.planes[2] + + // be aware that this size does not include the padding at the end, if there is any + // (e.g. if pixel stride is 2 the size is ySize / 2 - 1) + val uSize = uPlane.buffer.remaining() + val vSize = vPlane.buffer.remaining() + + if(data?.size != ySize + ySize / 2) + data = ByteArray(ySize + ySize / 2) + + yPlane.buffer.get(data, 0, ySize) + + val ub = uPlane.buffer + val vb = vPlane.buffer + + val uvPixelStride = uPlane.pixelStride //stride guaranteed to be the same for u and v planes + if (uvPixelStride == 1) { + uPlane.buffer.get(data, ySize, uSize) + vPlane.buffer.get(data, ySize + uSize, vSize) + } + else{ + // if pixel stride is 2 there is padding between each pixel + // converting it to NV21 by filling the gaps of the v plane with the u values + vb.get(data, ySize, vSize) + var i = 0 + while (i < uSize) { + data!![ySize + i + 1] = ub.get(i) + i += 2 + } + } + return data!! + } + + /** + * Start the camera preview. + */ + private fun startPreview() { + if (null == mCameraDevice) { + return + } + try { + closePreviewSession() + mPreviewBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD) + mPreviewBuilder!!.addTarget(reader?.surface!!) + + mCameraDevice!!.createCaptureSession(listOf(/*previewSurface, */reader?.surface), + object : CameraCaptureSession.StateCallback() { + + override fun onConfigured(@NonNull session: CameraCaptureSession) { + mPreviewSession = session + updatePreview() + } + + override fun onConfigureFailed(@NonNull session: CameraCaptureSession) { + + } + }, mBackgroundHandler) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * Update the camera preview. [.startPreview] needs to be called in advance. + */ + private fun updatePreview() { + if (null == mCameraDevice) { + return + } + try { + mPreviewBuilder?.let { setUpCaptureRequestBuilder(it) } + val thread = HandlerThread("CameraPreview") + thread.start() + mPreviewSession!!.setRepeatingRequest(mPreviewBuilder!!.build() + ,null + , mBackgroundHandler) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun setUpCaptureRequestBuilder(builder: CaptureRequest.Builder) { + builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(15, 30)) + val focusMode = when(focusMode){ + "video" -> { + CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO + } + "picture" -> { + CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE + } + "auto" -> { + null + } + "off" -> { + CameraMetadata.CONTROL_MODE_OFF + } + else -> { + CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO + } + } + focusMode?.let { + builder.set(CaptureRequest.CONTROL_AF_MODE, focusMode) + if(it == CameraMetadata.CONTROL_MODE_OFF) + builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 10f) + } ?: run{ + builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO) + } + } + + private fun closePreviewSession() { + if (mPreviewSession != null) { + mPreviewSession?.close() + mPreviewSession = null + } + } + + /** + * Starts a background thread and its [Handler]. + */ + private fun startBackgroundThread() { + mBackgroundThread = HandlerThread("CameraBackground") + mBackgroundThread?.start() + mBackgroundHandler = Handler(mBackgroundThread?.looper) + } + + /** + * Stops the background thread and its [Handler]. + */ + private fun stopBackgroundThread() { + mBackgroundThread?.quitSafely() + try { + mBackgroundThread?.join() + mBackgroundThread = null + mBackgroundHandler = null + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + + companion object{ + //checks for if this class can be run on this device + fun isSupported() : Boolean{ + return Build.VERSION.SDK_INT >= 21 + } + } +} \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/CameraCompatOverride.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/CameraCompatOverride.kt new file mode 100644 index 0000000..0309420 --- /dev/null +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/CameraCompatOverride.kt @@ -0,0 +1,71 @@ +package tv.remo.android.controller.sdk.components.video + +import android.annotation.TargetApi +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.os.Build +import android.util.Log +import org.btelman.controlsdk.streaming.models.ImageDataPacket +import org.btelman.controlsdk.streaming.models.StreamInfo +import org.btelman.controlsdk.streaming.video.retrievers.BaseVideoRetriever +import org.btelman.controlsdk.streaming.video.retrievers.api16.Camera1SurfaceTextureComponent + +/** + * Handle compatibility between camera1 and camera2 usage, since some api21 devices are + * not compatible, which makes frame grabbing really slow. Usage of Camera1 or Camera2 classes are + * still supported, but may not work on every device + * ex. Samsung Galaxy S4 + */ +class CameraCompatOverride : BaseVideoRetriever(){ + private var retriever : BaseVideoRetriever? = null + + override fun grabImageData(): ImageDataPacket? { + return retriever?.grabImageData() + } + + override fun enable(context: Context, streamInfo: StreamInfo) { + super.enable(context, streamInfo) + val cameraInfo = streamInfo.deviceInfo + val cameraId = cameraInfo.getCameraId() + retriever = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && validateCamera2Support(context, cameraId)){ + Log.d("CameraRetriever", "Using Camera2 API") + Camera2Override() + } else{ + Log.d("CameraRetriever", + "Using Camera1 API. Device API too low or LIMITED capabilities") + Camera1SurfaceTextureComponent() + } + retriever?.enable(context, streamInfo) + } + + override fun listenForFrame(func: () -> Unit) { + retriever?.listenForFrame(func) + } + + override fun removeListenerForFrame() { + retriever?.removeListenerForFrame() + } + + override fun disable() { + super.disable() + retriever?.disable() + retriever = null + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun validateCamera2Support(context: Context, cameraId: Int): Boolean { + try { + val cm = (context.getSystemService(Context.CAMERA_SERVICE) as CameraManager) + val hardwareLevel = cm.getCameraCharacteristics( + cm.cameraIdList[cameraId] + )[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL] + return hardwareLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY + && hardwareLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED + } catch (_: Exception) { + + } + return false + } +} \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoComponent.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoComponent.kt index e442e07..86f9a69 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoComponent.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoComponent.kt @@ -12,6 +12,7 @@ import tv.remo.android.controller.sdk.components.RemoSocketComponent import tv.remo.android.controller.sdk.components.StreamCommandHandler import tv.remo.android.controller.sdk.components.StreamCommandHandler.Companion.rebuildStream import tv.remo.android.controller.sdk.interfaces.CommandStreamHandler +import tv.remo.android.controller.sdk.interfaces.RemoCommandSender import tv.remo.android.controller.sdk.models.CommandSubscriptionData import tv.remo.android.controller.sdk.utils.ChatUtil @@ -36,7 +37,7 @@ class RemoVideoComponent : VideoComponent(), CommandStreamHandler { } override fun handleExternalMessage(message: ComponentEventObject): Boolean { - if(message.source is RemoSocketComponent || message.source is RemoCommandHandler){ + if(message.source is RemoCommandSender){ commandHandler?.handleExternalMessage(message) } return super.handleExternalMessage(message) diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/interfaces/RemoCommandSender.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/interfaces/RemoCommandSender.kt new file mode 100644 index 0000000..1ba1be5 --- /dev/null +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/interfaces/RemoCommandSender.kt @@ -0,0 +1,6 @@ +package tv.remo.android.controller.sdk.interfaces + +/** + * Interface that can be added to components for some other components to see it as a valid command + */ +interface RemoCommandSender \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/BooleanPref.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/BooleanPref.kt index c117333..d844dca 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/BooleanPref.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/BooleanPref.kt @@ -12,4 +12,8 @@ class BooleanPref( override fun getPref(): Boolean { return sharedPreferences.getBoolean(key, defaultValue) } + + override fun savePref(value: Boolean) { + sharedPreferences.edit().putBoolean(key, value).apply() + } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/ClassPref.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/ClassPref.kt index 8c11fd4..c8b0d58 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/ClassPref.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/ClassPref.kt @@ -17,4 +17,8 @@ class ClassPref(context : Context, sharedPreferences: SharedPreferences, resId: defaultValue } } + + override fun savePref(value: Class<*>) { + sharedPreferences.edit().putString(key, value.name).apply() + } } \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/IntPref.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/IntPref.kt index fa9c4f6..7715539 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/IntPref.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/IntPref.kt @@ -19,4 +19,8 @@ class IntPref( sharedPreferences.getString(key, defaultValue.toString())?.toInt() ?: defaultValue } } + + override fun savePref(value: Int) { + sharedPreferences.edit().putInt(key, value).apply() + } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/Licenses.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/Licenses.kt index 801e918..4814db3 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/Licenses.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/Licenses.kt @@ -20,7 +20,7 @@ object Licenses { License("ControlSDK, github.com/btelman96/ControlSDK", LicenseType.APACHE2_0), License("github.com/felHR85/UsbSerial", LicenseType.MIT), License("OkHttp", LicenseType.APACHE2_0), - License("FFmpeg, github.com/btelman96/ffmpeg-android-java", LicenseType.GPL3), + License("FFmpeg, github.com/btelman96/ffmpeg-android", LicenseType.LGPL3), License("github.com/btelman96/AndroidUvcDemo", LicenseType.GPL3 , "https://raw.githubusercontent.com/btelman96/AndroidUvcDemo/master/LICENCE") ) diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/Pref.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/Pref.kt index c03cc28..80ec004 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/Pref.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/Pref.kt @@ -15,4 +15,5 @@ abstract class Pref( val key : String = context.getString(resId) ) { abstract fun getPref() : T + abstract fun savePref(value : T) } \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/StringPref.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/StringPref.kt index ce9cd58..4d7b3e7 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/StringPref.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/StringPref.kt @@ -12,4 +12,8 @@ open class StringPref( override fun getPref(): String { return sharedPreferences.getString(key, defaultValue)?: defaultValue } + + override fun savePref(value: String) { + sharedPreferences.edit().putString(key, value).apply() + } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/utils/ComponentBuilderUtil.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/utils/ComponentBuilderUtil.kt index 3c9855c..35d3e22 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/utils/ComponentBuilderUtil.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/utils/ComponentBuilderUtil.kt @@ -7,6 +7,7 @@ import org.btelman.controlsdk.models.ComponentHolder import org.btelman.controlsdk.streaming.enums.Orientation import org.btelman.controlsdk.streaming.factories.AudioProcessorFactory import org.btelman.controlsdk.streaming.factories.VideoProcessorFactory +import org.btelman.controlsdk.streaming.factories.VideoRetrieverFactory import org.btelman.controlsdk.streaming.models.CameraDeviceInfo import org.btelman.controlsdk.streaming.models.StreamInfo import org.btelman.controlsdk.tts.SystemDefaultTTSComponent @@ -16,6 +17,7 @@ import tv.remo.android.controller.sdk.components.RemoSocketComponent import tv.remo.android.controller.sdk.components.audio.RemoAudioComponent import tv.remo.android.controller.sdk.components.audio.RemoAudioProcessor import tv.remo.android.controller.sdk.components.hardware.HardwareWatchdogComponent +import tv.remo.android.controller.sdk.components.video.CameraCompatOverride import tv.remo.android.controller.sdk.components.video.RemoVideoComponent import tv.remo.android.controller.sdk.components.video.RemoVideoProcessor @@ -83,6 +85,7 @@ object ComponentBuilderUtil { ,height = resolution[1].toInt() ) //use our customized remo classes + VideoRetrieverFactory.putClassInBundle(CameraCompatOverride::class.java, this) VideoProcessorFactory.putClassInBundle(RemoVideoProcessor::class.java, this) AudioProcessorFactory.putClassInBundle(RemoAudioProcessor::class.java, this) streamInfo.addToExistingBundle(this) diff --git a/sdk/src/main/res/values/arrays.xml b/sdk/src/main/res/values/arrays.xml index 00167f9..0037e7c 100644 --- a/sdk/src/main/res/values/arrays.xml +++ b/sdk/src/main/res/values/arrays.xml @@ -20,6 +20,21 @@ 768x432 1280x720 + + + System Default + Video mode + Picture mode + Off + + + + auto + video + picture + off + + Portrait Landscape Right diff --git a/sdk/src/main/res/values/preferences.xml b/sdk/src/main/res/values/preferences.xml index b391297..00ba1da 100644 --- a/sdk/src/main/res/values/preferences.xml +++ b/sdk/src/main/res/values/preferences.xml @@ -12,6 +12,7 @@ genericCameraSettingsGroupKey cameraSettingsEnable cameraResolution + cameraFocus cameraOrientation cameraBitrate UseCamera2 @@ -47,4 +48,6 @@ ffmpegVFPrefsKey ffmpegInputOptions ffmpegOutputOptions + streamAutoSleepEnabled + streamAutoSleepTimeout \ No newline at end of file