diff --git a/app/build.gradle b/app/build.gradle index fe3a5a8..b5dae27 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,12 +16,17 @@ android { } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { applicationId "tv.remo.android.controller" minSdkVersion 16 targetSdkVersion 28 versionCode 13 - versionName "0.15.0" + versionName "0.16.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/java/tv/remo/android/controller/RemoApplication.kt b/app/src/main/java/tv/remo/android/controller/RemoApplication.kt index d81920d..fc6480a 100644 --- a/app/src/main/java/tv/remo/android/controller/RemoApplication.kt +++ b/app/src/main/java/tv/remo/android/controller/RemoApplication.kt @@ -1,18 +1,51 @@ package tv.remo.android.controller import android.app.Application +import android.util.Log +import org.btelman.controlsdk.services.ControlSDKService +import org.btelman.logutil.kotlin.LogLevel +import org.btelman.logutil.kotlin.LogUtil +import org.btelman.logutil.kotlin.LogUtilInstance /** * Created by Brendon on 7/28/2019. */ class RemoApplication : Application() { + private val log = LogUtil("RemoApplication", logID) override fun onCreate() { super.onCreate() + LogUtilInstance(ControlSDKService.CONTROL_SERVICE, LogLevel.VERBOSE).also { + Log.d("RemoApplication", "Setup ControlSDK logger") + LogUtil.addCustomLogUtilInstance(ControlSDKService::class.java.name, it) + } + + log.d{ + "Remo.TV ${BuildConfig.VERSION_NAME} onCreate..." + } + Instance = this } companion object{ var Instance : RemoApplication? = null + val logID = "Remo.TV".also {name-> + LogUtilInstance(ControlSDKService.CONTROL_SERVICE, LogLevel.VERBOSE).also { + Log.d("RemoApplication", "Setup Remo.TV logger") + LogUtil.addCustomLogUtilInstance(name, it) + } + } + + fun getLogger(tag : String) : LogUtil{ + return LogUtil(tag, logID) + } + + fun getLogger(obj : Any, extra : String? = null) : LogUtil{ + var log = obj.javaClass.simpleName + extra?.let{ + log += " : $extra" + } + return getLogger(log) + } } } \ No newline at end of file diff --git a/app/src/main/java/tv/remo/android/controller/activities/MainActivity.kt b/app/src/main/java/tv/remo/android/controller/activities/MainActivity.kt index 8c3b4d3..0fee352 100644 --- a/app/src/main/java/tv/remo/android/controller/activities/MainActivity.kt +++ b/app/src/main/java/tv/remo/android/controller/activities/MainActivity.kt @@ -8,24 +8,29 @@ import android.os.Handler import android.os.Looper import android.view.View import androidx.appcompat.app.AppCompatActivity -import com.google.gson.Gson import kotlinx.android.synthetic.main.activity_main.* import org.btelman.controlsdk.enums.Operation -import org.btelman.controlsdk.interfaces.ControlSdkApi +import org.btelman.controlsdk.hardware.components.CommunicationDriverComponent +import org.btelman.controlsdk.interfaces.ControlSdkServiceWrapper import org.btelman.controlsdk.models.ComponentHolder import org.btelman.controlsdk.services.ControlSDKServiceConnection import org.btelman.controlsdk.services.observeAutoCreate +import org.btelman.controlsdk.tts.SystemDefaultTTSComponent import tv.remo.android.controller.R import tv.remo.android.controller.sdk.RemoSettingsUtil -import tv.remo.android.controller.sdk.models.api.Message -import tv.remo.android.controller.sdk.utils.ChatUtil +import tv.remo.android.controller.sdk.components.RemoSocketComponent +import tv.remo.android.controller.sdk.components.StatusBroadcasterComponent +import tv.remo.android.controller.sdk.components.audio.RemoAudioProcessor +import tv.remo.android.controller.sdk.components.video.RemoVideoProcessor import tv.remo.android.controller.sdk.utils.ComponentBuilderUtil class MainActivity : AppCompatActivity(), View.OnClickListener { + private val listenerControllerList = ArrayList>() private var recording = false private val arrayList = ArrayList>() - private var controlSDKServiceApi: ControlSdkApi? = null + private var controlSDKServiceApi: ControlSdkServiceWrapper? = null private lateinit var handler : Handler + private val telemetryEnabled = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -33,6 +38,9 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { setContentView(R.layout.activity_main) setupControlSDK() setupUI() + window.decorView.post { + buildStatusList() + } } override fun onClick(v: View?) { @@ -49,6 +57,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { override fun onDestroy() { super.onDestroy() + handleListenerAddOrRemove(Operation.NOT_OK) controlSDKServiceApi?.disconnectFromService() } @@ -57,11 +66,25 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { controlSDKServiceApi?.getServiceStateObserver()?.observeAutoCreate(this, operationObserver) controlSDKServiceApi?.getServiceBoundObserver()?.observeAutoCreate(this){ connected -> powerButton.isEnabled = connected == Operation.OK + handleListenerAddOrRemove(connected) } controlSDKServiceApi?.connectToService() createComponentHolders() } + private fun handleListenerAddOrRemove(connected : Operation) { + if(connected == Operation.OK){ + listenerControllerList.forEach { + controlSDKServiceApi?.addListenerOrController(it) + } + } + else if(connected == Operation.NOT_OK){ + listenerControllerList.forEach { + controlSDKServiceApi?.removeListenerOrController(it) + } + } + } + private fun setupUI() { remoChatView.setOnTouchListener { _, _ -> handleSleepLayoutTouch() @@ -69,6 +92,15 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } settingsButton.setOnClickListener(this) powerButton?.setOnClickListener(this) + + } + + private fun buildStatusList() { + websiteConnectionStatusView.registerStatusEvents(RemoSocketComponent::class.java) + hardwareConnectionStatusView.registerStatusEvents(CommunicationDriverComponent::class.java) + audioConnectionStatusView.registerStatusEvents(RemoAudioProcessor::class.java) + videoConnectionStatusView.registerStatusEvents(RemoVideoProcessor::class.java) + ttsConnectionStatusView.registerStatusEvents(SystemDefaultTTSComponent::class.java) } val operationObserver : (Operation) -> Unit = { serviceStatus -> @@ -113,14 +145,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } } - private fun UnitTestRunChat(){ - val json = "{\"message\":\"test\",\"sender\":\"ReconDelta090\",\"sender_id\":\"user-6a9591cc-f3d3-4e47-a208-e749679a899a\",\"chat_id\":\"chat-8a05f730-c663-434d-9f24-6d8c24453c5f\",\"server_id\":\"serv-46437781-4a9b-4531-9db1-74bc2f818b58\",\"id\":\"mesg-ec54bf9a-23d4-4fa6-b6f1-9a5c8e3e2440\",\"time_stamp\":1574296097994,\"broadcast\":\"\",\"channel_id\":\"chan-7a304995-cba0-463c-81a6-ffeffc059058\",\"display_message\":true,\"badges\":[\"owner\"],\"type\":\"\"}" - Gson().fromJson(json, Message::class.java).also { rawMessage -> - ChatUtil.broadcastChatMessage(this, rawMessage) - ChatUtil.broadcastChatMessage(this, rawMessage) //should just be ignored - } - } - private fun launchSettings() { if(controlSDKServiceApi?.getServiceStateObserver()?.value == Operation.OK){ powerCycle() @@ -139,10 +163,10 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } Operation.LOADING -> {} //do nothing Operation.OK -> { + controlSDKServiceApi?.disable() arrayList.forEach { controlSDKServiceApi?.detachFromLifecycle(it) } - controlSDKServiceApi?.disable() } null -> powerButton.setTextColor(parseColorForOperation(null)) } @@ -154,6 +178,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { arrayList.addAll(ComponentBuilderUtil.createTTSComponents(settings)) arrayList.addAll(ComponentBuilderUtil.createStreamingComponents(settings)) arrayList.addAll(ComponentBuilderUtil.createHardwareComponents(settings)) + listenerControllerList.add(ComponentHolder(StatusBroadcasterComponent::class.java, null)) } } @@ -172,6 +197,18 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { or View.SYSTEM_UI_FLAG_FULLSCREEN) } + + @Suppress("DEPRECATION") + fun parseColorForOperation(state : Operation?) : Int{ + return when(state){ + Operation.OK -> resources.getColor(R.color.powerIndicatorOn) + Operation.NOT_OK -> resources.getColor(R.color.powerIndicatorOff) + Operation.LOADING -> resources.getColor(R.color.powerIndicatorInProgress) + null -> resources.getColor(R.color.powerIndicatorError) + else -> Color.BLACK + } + } + // Shows the system bars by removing all the flags // except for the ones that make the content appear under the system bars. private fun showSystemUI() { @@ -182,17 +219,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { fun getIntent(context: Context) : Intent { return Intent(context, MainActivity::class.java) } - - fun parseColorForOperation(state : Operation?) : Int{ - val color : Int = when(state){ - Operation.OK -> Color.GREEN - Operation.NOT_OK -> Color.RED - Operation.LOADING -> Color.YELLOW - null -> Color.CYAN - else -> Color.BLACK - } - return color - } } } diff --git a/app/src/main/java/tv/remo/android/controller/fragments/SettingsCamera.kt b/app/src/main/java/tv/remo/android/controller/fragments/SettingsCamera.kt index abb8610..bd3df82 100644 --- a/app/src/main/java/tv/remo/android/controller/fragments/SettingsCamera.kt +++ b/app/src/main/java/tv/remo/android/controller/fragments/SettingsCamera.kt @@ -1,31 +1,106 @@ package tv.remo.android.controller.fragments import android.content.Context -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.view.View +import android.widget.Toast import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.SwitchPreferenceCompat -import tv.remo.android.controller.Camera2Util import tv.remo.android.controller.R import tv.remo.android.controller.sdk.RemoSettingsUtil import tv.remo.android.controller.sdk.utils.ValueUtil +import tv.remo.android.controller.utils.CameraUtil +import tv.remo.android.controller.utils.v21.Camera2Util import tv.remo.android.settingsutil.fragments.BasePreferenceFragmentCompat +import tv.remo.android.settingsutil.interfaces.SwitchBarCapableActivity import java.util.* class SettingsCamera : BasePreferenceFragmentCompat( R.xml.settings_camera, R.string.cameraSettingsEnableKey ) { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - super.onCreatePreferences(savedInstanceState, rootKey) + + private val cameraId: Int + get() { + return RemoSettingsUtil.with(context!!).cameraDeviceId.getPref() + } + + private val cameraAllowed : Boolean + get() { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context!!.checkSelfPermission(android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as SwitchBarCapableActivity).getSwitchBar().apply { + addOnSwitchChangeListener{ _, isChecked -> + if(isChecked) + checkPermissions() + } + } + RemoSettingsUtil.with(context!!) { val currCameraId = it.cameraDeviceId.getPref() + setupCameraIdList(currCameraId) checkForCamera2SupportAndReact(it.cameraDeviceId.getPref()) updateUIFromCameraSelection(currCameraId) addListeners() + if(it.cameraEnabled.getPref()) + checkPermissions() + } + } + + private fun setupCameraIdList(currCameraId: Int) { + val cameras = CameraUtil.getCameraCount(context!!) + findPreference(getString(R.string.cameraDeviceIdKey))?.apply { + createEntries(cameras).also { + entries = it + entryValues = it + } + if(currCameraId >= cameras){ + Toast.makeText(context!!, "Last selected camera is no longer available...", + Toast.LENGTH_LONG).show() + value = entries[0] as String + } + } + } + + private fun createEntries(size: Int): Array { + return Array(size){ + it.toString() + } + } + + private fun checkPermissions() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !cameraAllowed){ + preferenceManager.preferenceScreen.isEnabled = false + requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 111) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if(requestCode == 111){ + if(cameraAllowed){ + preferenceManager.preferenceScreen.isEnabled = true + updateUIFromCameraSelection(cameraId) + } + else{ + Toast.makeText(context!!, + "Camera permissions required to modify camera settings!", + Toast.LENGTH_SHORT).show() + } } } @@ -52,18 +127,15 @@ class SettingsCamera : BasePreferenceFragmentCompat( } } - private fun updateUIFromCameraSelection(newValue: Int) { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val sizes = try { - Camera2Util.GetCameraSizes(requireContext(), newValue) - } catch (e: IndexOutOfBoundsException) { - ArrayList>().also { - it.add(Pair(640, 480)) - } + private fun updateUIFromCameraSelection(cameraIndex: Int) { + val sizes = try { + CameraUtil.getCameraSizes(requireContext(), cameraIndex) + } catch (e: IndexOutOfBoundsException) { + ArrayList>().also { + it.add(Pair(640, 480)) } - setNewResolutionList(sizes) } + setNewResolutionList(sizes) } private fun setNewResolutionList(sizes: ArrayList>) { @@ -76,6 +148,11 @@ class SettingsCamera : BasePreferenceFragmentCompat( sizes.removeAll { pair -> blacklist.contains(pair) } + if(sizes.isEmpty()) { + //this one always will work. + // We also need a fallback in case permissions have not been accepted + sizes.add(Pair(640, 480)) + } //init values for listPreference val values = initResolutionArray(sizes.size) @@ -83,7 +160,7 @@ class SettingsCamera : BasePreferenceFragmentCompat( //create a list of known resolutions that are okay. Unknown ones are marked with (not tested) val knownWorkingResolutions = - listOf(Pair(640, 480), Pair(1280, 720), Pair(1280, 960), Pair(1920, 1080)) + listOf(Pair(640, 480), Pair(1280, 720), Pair(960, 720), Pair(1280, 960), Pair(1920, 1080)) //iterate through the array to add sizes to values and userFacingValues arrays sizes.forEachIndexed { i, pair -> @@ -127,12 +204,6 @@ class SettingsCamera : BasePreferenceFragmentCompat( val featureSwitch = findPreference(getString(R.string.useCamera2)) featureSwitch?.isEnabled = supportsCamera2 featureSwitch?.isChecked = supportsCamera2 - if (!supportsCamera2) - findPreference( - getString(R.string.cameraResolutionKey) - )?.let { - it.value = "640x480" - } return supportsCamera2 } @@ -165,17 +236,7 @@ class SettingsCamera : BasePreferenceFragmentCompat( private fun validateCamera2Support(context: Context, cameraId: Int): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - 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 Camera2Util.checkFullyCompatible(context, cameraId) } return false } diff --git a/app/src/main/java/tv/remo/android/controller/ui/CustomImageView.kt b/app/src/main/java/tv/remo/android/controller/ui/CustomImageView.kt new file mode 100644 index 0000000..3fb9e36 --- /dev/null +++ b/app/src/main/java/tv/remo/android/controller/ui/CustomImageView.kt @@ -0,0 +1,16 @@ +package tv.remo.android.controller.ui + +import android.content.Context +import android.util.AttributeSet + +/** + * Image View that follows other UI behavior if disabled + */ +open class CustomImageView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + imageAlpha = if(enabled) 0xFF else 0x3F + } +} \ No newline at end of file diff --git a/app/src/main/java/tv/remo/android/controller/ui/RemoStatusView.kt b/app/src/main/java/tv/remo/android/controller/ui/RemoStatusView.kt new file mode 100644 index 0000000..17e13fe --- /dev/null +++ b/app/src/main/java/tv/remo/android/controller/ui/RemoStatusView.kt @@ -0,0 +1,150 @@ +package tv.remo.android.controller.ui + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.graphics.PorterDuff +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.util.SparseIntArray +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.btelman.controlsdk.enums.ComponentStatus +import org.btelman.controlsdk.models.Component +import tv.remo.android.controller.R +import tv.remo.android.controller.RemoApplication +import tv.remo.android.controller.sdk.components.StatusBroadcasterComponent + +/** + * Status view that will communicate directly with a chosen component + * + * TODO replace with ViewModel + */ +class RemoStatusView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : CustomImageView(context, attrs, defStyleAttr), Runnable{ + + private var log = RemoApplication.getLogger(this) + private var colorLookup = SparseIntArray().also{ + appendColor(context, it, R.color.colorIndicatorDisabledFromSettings) + appendColor(context, it, R.color.colorIndicatorDisabled) + appendColor(context, it, R.color.colorIndicatorConnecting) + appendColor(context, it, R.color.colorIndicatorStable) + appendColor(context, it, R.color.colorIndicatorUnstable) + appendColor(context, it, R.color.colorIndicatorError) + } + + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { + onBroadcastDataReceived(it) + } + } + } + + private val broadcastManager = LocalBroadcastManager.getInstance(context) + + @Suppress("DEPRECATION") + private fun appendColor(context: Context, it: SparseIntArray, resId: Int) { + val color : Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.getColor(resId) + } + else{ + context.resources.getColor(resId) + } + it.append(resId, color) + } + + private var uiHandler : Handler = Handler(Looper.getMainLooper()) + private var component: String? = null + private var status : ComponentStatus? = null + val values = ComponentStatus.values() + init{ + uiHandler.post(this) + post { + onCreated() + } + } + + private fun onCreated() { + + } + + fun onDestroy(){ + uiHandler.removeCallbacksAndMessages(null) + } + + fun registerStatusEvents(statusClassName : Class){ + broadcastManager.unregisterReceiver(receiver) + val filter = IntentFilter(StatusBroadcasterComponent.ACTION_SERVICE_STATUS) + StatusBroadcasterComponent.generateComponentStatusAction(statusClassName).also { + filter.addAction(it) + log.d { "switching log to $it" } + log = RemoApplication.getLogger(this, it) + } + broadcastManager.registerReceiver(receiver, filter) + } + + fun unregisterStatusEvents(){ + broadcastManager.unregisterReceiver(receiver) + } + + private fun onBroadcastDataReceived(intent: Intent) { + log.v{ + "onBroadcastDataReceived ${intent.action} ${intent.getSerializableExtra(StatusBroadcasterComponent.STATUS_NAME)}" + } + if(intent.action == StatusBroadcasterComponent.ACTION_SERVICE_STATUS) return + val componentStatus = intent.getSerializableExtra(StatusBroadcasterComponent.STATUS_NAME) + status = componentStatus as? ComponentStatus + } + + fun setDrawableColor(id : Int){ + val color : Int = colorLookup.get(id, Color.BLACK) + setColorFilter(color, PorterDuff.Mode.MULTIPLY) + } + + fun postStatus(status: ComponentStatus){ + post { + this.status = status + setStatus(status) + } + } + + private fun setStatus(componentStatus: ComponentStatus){ + when(componentStatus){ + ComponentStatus.DISABLED_FROM_SETTINGS -> setDrawableColor(R.color.colorIndicatorDisabledFromSettings) + ComponentStatus.DISABLED -> setDrawableColor(R.color.colorIndicatorDisabled) + ComponentStatus.CONNECTING -> setDrawableColor(R.color.colorIndicatorConnecting) + ComponentStatus.STABLE -> setDrawableColor(R.color.colorIndicatorStable) + ComponentStatus.INTERMITTENT -> setDrawableColor(R.color.colorIndicatorUnstable) + ComponentStatus.ERROR -> setDrawableColor(R.color.colorIndicatorError) + } + } + + override fun run() { + status?.let { + setStatus(it) + uiHandler.postDelayed(this, 100) + } ?: run{ + setStatus(loopStatus()) + uiHandler.postDelayed(this, 1000) + } + } + + var i = 0 + private fun loopStatus(): ComponentStatus { + var status : ComponentStatus = ComponentStatus.DISABLED_FROM_SETTINGS + values.forEachIndexed { index, componentStatus -> + if (index == i) { + status = componentStatus + } + } + i++ + if(i > values.size) + i = 0 + return status + } +} diff --git a/app/src/main/java/tv/remo/android/controller/utils/CameraUtil.kt b/app/src/main/java/tv/remo/android/controller/utils/CameraUtil.kt new file mode 100644 index 0000000..5b9fb93 --- /dev/null +++ b/app/src/main/java/tv/remo/android/controller/utils/CameraUtil.kt @@ -0,0 +1,66 @@ +package tv.remo.android.controller.utils + +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import tv.remo.android.controller.sdk.utils.ValueUtil +import tv.remo.android.controller.utils.v16.Camera1Util +import tv.remo.android.controller.utils.v21.Camera2Util +import java.util.* + +/** + * Created by Brendon on 2/27/2020. + */ +object CameraUtil{ + fun getCameraSizes(requireContext: Context, cameraIndex: Int): ArrayList> { + val sizes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && isFullyCamera2Compatible(requireContext, cameraIndex)) { + Camera2Util.getCameraSizes(requireContext, cameraIndex) + } else{ + Camera1Util.getCameraSizes(requireContext, cameraIndex) + } + return filterSizes(sizes) + } + + private fun filterSizes(sizes: ArrayList>): ArrayList> { + val finalList = ArrayList>() + val ratio16by9 = 16f / 9f + val ratio4by3 = 4f / 3f + val ratio3by2 = 3f / 2f + sizes.forEach { + val height = it.first + val width = it.second + val ratio = (width * 1f / height * 1f) + val outsideOfAllowedSizes = width < 640 || height < 480 + || width > 1920 || height > 1080 + val isAllowedRatio = ratio == ratio16by9 + || ratio == ratio4by3 + || ratio == ratio3by2 + if (isAllowedRatio && !outsideOfAllowedSizes) { + finalList.add(Pair(width, height)) + } + + //logging only + val gcm = ValueUtil.gcm(width, height) + Log.d( + "RemoApplication", + "PictureSize: ${width}x${height} : ${width / gcm}:${height / gcm}" + ) + } + return finalList + } + + fun getCameraCount(context: Context) : Int{ + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Camera2Util.getCameras(context) + }else{ + Camera1Util.getCameras() + } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun isFullyCamera2Compatible(context: Context, cameraIndex: Int): Boolean { + return Camera2Util.checkFullyCompatible(context, cameraIndex); + } +} \ No newline at end of file diff --git a/app/src/main/java/tv/remo/android/controller/utils/v16/Camera1Util.kt b/app/src/main/java/tv/remo/android/controller/utils/v16/Camera1Util.kt new file mode 100644 index 0000000..b7c2b2d --- /dev/null +++ b/app/src/main/java/tv/remo/android/controller/utils/v16/Camera1Util.kt @@ -0,0 +1,28 @@ +package tv.remo.android.controller.utils.v16 + +import android.content.Context +import android.hardware.Camera +import java.util.* + +/** + * Created by Brendon on 2/27/2020. + */ +object Camera1Util { + fun getCameraSizes(requireContext: Context, cameraIndex: Int): ArrayList> { + val list = ArrayList>() + kotlin.runCatching { + val camera = Camera.open(cameraIndex) + camera.runCatching { + parameters.supportedPreviewSizes.forEach{ + list.add(Pair(it.height, it.width)) + } + } + camera.release() + } + return list + } + + fun getCameras(): Int { + return Camera.getNumberOfCameras() + } +} \ No newline at end of file diff --git a/app/src/main/java/tv/remo/android/controller/Camera2Util.kt b/app/src/main/java/tv/remo/android/controller/utils/v21/Camera2Util.kt similarity index 50% rename from app/src/main/java/tv/remo/android/controller/Camera2Util.kt rename to app/src/main/java/tv/remo/android/controller/utils/v21/Camera2Util.kt index abc9cc4..b0c6ac0 100644 --- a/app/src/main/java/tv/remo/android/controller/Camera2Util.kt +++ b/app/src/main/java/tv/remo/android/controller/utils/v21/Camera2Util.kt @@ -1,4 +1,4 @@ -package tv.remo.android.controller +package tv.remo.android.controller.utils.v21 import android.content.Context import android.graphics.ImageFormat @@ -8,12 +8,11 @@ import android.hardware.camera2.CameraManager import android.os.Build import android.util.Log import androidx.annotation.RequiresApi -import tv.remo.android.controller.sdk.utils.ValueUtil @RequiresApi(Build.VERSION_CODES.LOLLIPOP) object Camera2Util { @Throws(ArrayIndexOutOfBoundsException::class) - fun GetCameraSizes(context : Context, cameraIndex : Int = 0) : ArrayList> { + fun getCameraSizes(context : Context, cameraIndex : Int = 0) : ArrayList> { val list = ArrayList>() val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager try { @@ -25,35 +24,9 @@ object Camera2Util { // Images supported size by device val pictureSizes = map!!.getOutputSizes(ImageFormat.YUV_420_888) - - val ratio16by9 = 16f / 9f - val ratio4by3 = 4f / 3f - val ratio3by2 = 3f / 2f pictureSizes.forEach { - val height = it.height - val width = it.width - val ratio = (width * 1f / height * 1f) - val outsideOfAllowedSizes = width < 640 || height < 480 - || width > 1920 || height > 1080 - val isAllowedRatio = ratio == ratio16by9 - || ratio == ratio4by3 - || ratio == ratio3by2 - if (isAllowedRatio && !outsideOfAllowedSizes) { - list.add(Pair(width, height)) - } - - //logging only - val gcm = ValueUtil.gcm(width, height) - Log.d( - "RemoApplication", - "PictureSize: ${width}x${height} : ${width / gcm}:${height / gcm}" - ) + list.add(Pair(it.height, it.width)) } - /*TODO switch to video recording // Video supported size by device - val videoSizes = map.getOutputSizes(MediaRecorder::class.java) - videoSizes.forEach { - Log.d("RemoApplication", "VideoSize: ${it.width}x${it.height}") - }*/ } catch (e: CameraAccessException) { e.printStackTrace() Log.e("RemoApplication", "setUpCameraOutputs: catch: " + e.message) @@ -62,4 +35,21 @@ object Camera2Util { } return list } + + fun checkFullyCompatible(context: Context, cameraIndex: Int): Boolean { + kotlin.runCatching { + val cm = (context.getSystemService(Context.CAMERA_SERVICE) as CameraManager) + val hardwareLevel = cm.getCameraCharacteristics( + cm.cameraIdList[cameraIndex] + )[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL] + return hardwareLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY + && hardwareLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED + } + return false + } + + fun getCameras(context: Context) : Int{ + val cm = (context.getSystemService(Context.CAMERA_SERVICE) as CameraManager) + return cm.cameraIdList.size + } } diff --git a/app/src/main/res/drawable/ic_phone_android_black_24dp.xml b/app/src/main/res/drawable/ic_phone_android_black_24dp.xml index b32b05e..9130560 100644 --- a/app/src/main/res/drawable/ic_phone_android_black_24dp.xml +++ b/app/src/main/res/drawable/ic_phone_android_black_24dp.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/drawable/ic_volume_up_white_24dp.xml b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml new file mode 100644 index 0000000..6fc27fc --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 479b085..85edb7a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,9 +8,57 @@ android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".activities.MainActivity"> + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index dc5f801..bf163e0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,7 +1,19 @@ - #3255fb - #3255fb - #8c12cb + #6200EA + #4A148C + #304FFE #ececec + + #000000 + #FFFFFF + #00C853 + #FFD600 + #FF6D00 + #D50000 + + #D50000 + #FFD600 + #00C853 + #AA00FF diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..bab3d65 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 24dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9839634..db3fa6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,9 +53,7 @@ Auto Focus Orientation Camera2 Disabled - Device is using legacy camera API. - Resolution is locked at 640x480. Disabled since device OS is below 5.0 - or selected camera id does not support new camera api + Device is using legacy camera API. Some features may be locked Microphone Volume, Bitrate diff --git a/app/src/main/res/xml/settings_camera.xml b/app/src/main/res/xml/settings_camera.xml index 5966ed0..f5ad506 100644 --- a/app/src/main/res/xml/settings_camera.xml +++ b/app/src/main/res/xml/settings_camera.xml @@ -16,7 +16,6 @@ app:isPreferenceVisible="false"/> with(context: Context, func : (RemoSettingsUtil)->T) : T{ - val settingsUtil = INSTANCE ?: create(context) + val settingsUtil = with(context) return func(settingsUtil) } + + fun with(context: Context) : RemoSettingsUtil{ + return INSTANCE ?: create(context) + } } } 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 b025166..d28e063 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 @@ -7,6 +7,7 @@ import com.google.gson.Gson import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocket +import org.btelman.controlsdk.enums.ComponentStatus import org.btelman.controlsdk.enums.ComponentType import org.btelman.controlsdk.models.Component import org.btelman.controlsdk.models.ComponentEventObject @@ -15,10 +16,7 @@ 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 -import tv.remo.android.controller.sdk.utils.SocketListener -import tv.remo.android.controller.sdk.utils.isUrl +import tv.remo.android.controller.sdk.utils.* /** * Remo Socket component @@ -69,33 +67,33 @@ class RemoSocketComponent : Component() , RemoCommandSender { } private fun subToSocketEvents(listener: SocketListener) { - listener.on(SocketListener.ON_OPEN){ - sendHandshakeAuth() - }.on(SocketListener.ON_CLOSE){ - Log.d("TAG","onClosing $it") - }.on(SocketListener.ON_ERROR){ - handler.postDelayed ({ - //attempt a reconnect every second - attemptReconnect() - }, 1000) - Log.d("TAG","onFailure $it") - }.on("ROBOT_VALIDATED"){ - sendChannelsRequest(it) - }.on("SEND_ROBOT_SERVER_INFO"){ - verifyAndSubToChannel(it) - }.on(SocketListener.ON_MESSAGE){ - Log.d("SOCKET", it) - }.on("MESSAGE_RECEIVED"){ - sendChatUpwards(it) - }.on("BUTTON_COMMAND"){ - sendCommandUpwards(it) - }.on("LOCAL_MODERATION"){ - processChatModeration(it) - } + listener.on(SocketListener.ON_CLOSE) { + status = ComponentStatus.ERROR + Log.d("TAG", "onClosing $it") + }.on(SocketListener.ON_OPEN, this::sendHandshakeAuth) + .on(SocketListener.ON_ERROR, this::handleConnectionError) + .on("ROBOT_VALIDATED", this::sendChannelsRequest) + .on("SEND_ROBOT_SERVER_INFO", this::verifyAndSubToChannel) + .on("MESSAGE_RECEIVED", this::sendChatUpwards) + .on("BUTTON_COMMAND", this::sendCommandUpwards) + .on("LOCAL_MODERATION", this::processChatModeration) + .on(SocketListener.ON_MESSAGE) { + Log.d("SOCKET", it) + } //.on("SEND_CHAT") //TODO? of type Messages } + private fun handleConnectionError(value: String) { + status = ComponentStatus.ERROR + handler.postDelayed({ + //attempt a reconnect every second + attemptReconnect() + }, 1000) + Log.d("TAG", "onFailure $value") + } + private fun attemptReconnect() { + status = ComponentStatus.CONNECTING url ?: return socket?.close(1000, "service ended normally") request = Request.Builder().url(url!!).build() @@ -151,13 +149,6 @@ class RemoSocketComponent : Component() , RemoCommandSender { } } - private fun String.startsWith(vararg prefix : String) : Boolean{ - prefix.forEach { - if(this.startsWith(it, false)) return true - } - return false - } - private fun searchAndSendCommand(message: Message) : Boolean{ //TODO get list of mods or users that have access to commands if(message.message.startsWith(".", "/")){ @@ -215,13 +206,14 @@ class RemoSocketComponent : Component() , RemoCommandSender { } private fun sendChannelsRequest(json : String) { + status = ComponentStatus.STABLE val jsonObject = JSONObject(json) val host = jsonObject.getString("host") val str = "{\"e\":\"GET_CHANNELS\",\"d\":{\"server_id\":\"$host\"}}" socket?.send(str) } - private fun sendHandshakeAuth() { + private fun sendHandshakeAuth(value : String) { val json = "{\"e\": \"AUTHENTICATE_ROBOT\", \"d\": {\"token\": \"$apiKey\"}}" socket?.send(json) } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/StatusBroadcasterComponent.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/StatusBroadcasterComponent.kt new file mode 100644 index 0000000..f69b675 --- /dev/null +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/StatusBroadcasterComponent.kt @@ -0,0 +1,66 @@ +package tv.remo.android.controller.sdk.components + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.btelman.controlsdk.enums.ComponentStatus +import org.btelman.controlsdk.enums.ServiceStatus +import org.btelman.controlsdk.interfaces.IListener +import org.btelman.controlsdk.services.ControlSDKService +import org.btelman.logutil.kotlin.LogUtil + +/** + * Broadcast the statuses of each component, and some service level events + */ +class StatusBroadcasterComponent : IListener { + private val log = LogUtil("StatusBroadcasterComponent", ControlSDKService.loggerID) + var localBroadcastManager : LocalBroadcastManager? = null + override fun onInitializeComponent(applicationContext: Context, bundle: Bundle?) { + localBroadcastManager = LocalBroadcastManager.getInstance(applicationContext) + } + + override fun onRemoved() { + localBroadcastManager = null + } + + override fun onServiceStateChange(status: ServiceStatus) { + super.onServiceStateChange(status) + setServiceStatus(status) + } + + fun setServiceStatus(status : ServiceStatus){ + val intent = Intent(ACTION_SERVICE_STATUS).apply{ + putExtra(STATUS_NAME, status) + } + localBroadcastManager?.sendBroadcast(intent) + } + + override fun onComponentStatus(clazz: Class<*>, componentStatus: ComponentStatus) { + log.d{ + "STATUS_EVENT $componentStatus from ${clazz.name}" + } + super.onComponentStatus(clazz, componentStatus) + val intent = Intent(ACTION_COMPONENT_STATUS).apply { + putExtra(CLASS_NAME, clazz.name) + putExtra(STATUS_NAME, componentStatus) + } + //create an intent that will only contain a single class of statuses + val intentClassLevel = Intent(generateComponentStatusAction(clazz)).apply { + putExtra(STATUS_NAME, componentStatus) + } + //now send both of them + localBroadcastManager?.sendBroadcast(intent) + localBroadcastManager?.sendBroadcast(intentClassLevel) + } + + companion object{ + fun generateComponentStatusAction(clazz : Class) : String{ + return "${clazz.name}.${ACTION_COMPONENT_STATUS}" + } + const val ACTION_SERVICE_STATUS = "control.sdk.ACTION_SERVICE_STATUS" + const val ACTION_COMPONENT_STATUS = "ACTION_COMPONENT_STATUS" + const val CLASS_NAME = "component.class" + const val STATUS_NAME = "component.status.name" + } +} \ No newline at end of file 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 6e22bd8..ebc87f8 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 @@ -2,6 +2,7 @@ package tv.remo.android.controller.sdk.components import android.content.Context import android.os.Bundle +import kotlinx.coroutines.runBlocking import org.btelman.controlsdk.models.ComponentEventObject import org.btelman.controlsdk.streaming.models.StreamInfo import tv.remo.android.controller.sdk.interfaces.CommandStreamHandler @@ -43,25 +44,39 @@ class StreamCommandHandler(val context: Context?, val streamHandler : CommandStr context?:return when (data) { ".stream sleep" -> { - if(!sleepMode){ - sleepMode = true - streamHandler.acquireRetriever().disable() - } + handleSleep() } ".stream wakeup" -> { - if(sleepMode) { - sleepMode = false - streamHandler.acquireRetriever().enable(context, streamHandler.pullStreamInfo()) - } + handleWakeup() } ".stream reset" -> { - sleepMode = false reload() } } processSubscribedArrayForCommand(data) } + private fun handleWakeup() { + if(sleepMode) { + sleepMode = false + runBlocking { + streamHandler.acquireRetriever().also { + it.updateStreamInfo(streamHandler.pullStreamInfo()) + it.enable().await() + } + } + } + } + + private fun handleSleep() { + if(!sleepMode){ + sleepMode = true + runBlocking { + streamHandler.acquireRetriever().disable().await() + } + } + } + /** * Iterate through the array and trigger the subscribers if conditions are met */ @@ -92,6 +107,7 @@ class StreamCommandHandler(val context: Context?, val streamHandler : CommandStr } private fun reload() { + sleepMode = false streamHandler.resetComponents() } 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 57718e7..d5d0045 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 @@ -2,6 +2,7 @@ package tv.remo.android.controller.sdk.components.audio import android.content.Context import android.os.Bundle +import kotlinx.coroutines.runBlocking import org.btelman.controlsdk.enums.ComponentType import org.btelman.controlsdk.models.ComponentEventObject import org.btelman.controlsdk.streaming.components.AudioComponent @@ -10,8 +11,6 @@ import org.btelman.controlsdk.streaming.models.StreamInfo import org.btelman.controlsdk.tts.TTSBaseComponent import tv.remo.android.controller.sdk.R import tv.remo.android.controller.sdk.RemoSettingsUtil -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 @@ -107,8 +106,11 @@ class RemoAudioComponent : AudioComponent() , CommandStreamHandler { } //we only care about rebooting ffmpeg - processor.disable() - processor.enable(context!!, streamInfo) + runBlocking { + processor.disable().await() + processor.updateStreamInfo(streamInfo) + processor.enable().await() + } } } } diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera1Override.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera1Override.kt new file mode 100644 index 0000000..7b0f444 --- /dev/null +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/Camera1Override.kt @@ -0,0 +1,11 @@ +package tv.remo.android.controller.sdk.components.video + +import android.hardware.Camera +import org.btelman.controlsdk.streaming.video.retrievers.api16.Camera1SurfaceTextureComponent + +@Suppress("DEPRECATION") +class Camera1Override : Camera1SurfaceTextureComponent() { + override fun updateCameraParams(parameters: Camera.Parameters): Camera.Parameters { + return super.updateCameraParams(parameters) + } +} \ No newline at end of file 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 index f9b0c32..d5d5707 100644 --- 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 @@ -1,237 +1,27 @@ 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.hardware.camera2.CameraMetadata +import android.hardware.camera2.CaptureRequest 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 org.btelman.controlsdk.streaming.video.retrievers.api21.Camera2Component 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 +class Camera2Override : Camera2Component(), ImageReader.OnImageAvailableListener { 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!! + override fun enableInternal() { + super.enableInternal() + focusMode = RemoSettingsUtil.with(context!!).cameraFocus.getPref() } - /** - * 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) { + override fun setUpCaptureRequestBuilder(builder: CaptureRequest.Builder) { builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(15, 30)) val focusMode = when(focusMode){ "video" -> { @@ -258,41 +48,4 @@ class Camera2Override : BaseVideoRetriever(), ImageReader.OnImageAvailableListen 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 index 0309420..94c6ae4 100644 --- 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 @@ -1,71 +1,19 @@ 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 androidx.annotation.RequiresApi import org.btelman.controlsdk.streaming.video.retrievers.BaseVideoRetriever -import org.btelman.controlsdk.streaming.video.retrievers.api16.Camera1SurfaceTextureComponent +import org.btelman.controlsdk.streaming.video.retrievers.CameraCompatRetriever /** - * 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 + * Override to use different camera classes */ -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) +class CameraCompatOverride : CameraCompatRetriever(){ + @RequiresApi(21) + override fun createCamera2(): BaseVideoRetriever? { + return Camera2Override() } - 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 + override fun createCamera1(): BaseVideoRetriever? { + return Camera1Override() } } \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoProcessor.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoProcessor.kt index d138873..a1bb9a3 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoProcessor.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/components/video/RemoVideoProcessor.kt @@ -1,6 +1,5 @@ package tv.remo.android.controller.sdk.components.video -import android.content.Context import org.btelman.controlsdk.streaming.models.StreamInfo import org.btelman.controlsdk.streaming.video.processors.FFmpegVideoProcessor import tv.remo.android.controller.sdk.RemoSettingsUtil @@ -11,13 +10,6 @@ import tv.remo.android.controller.sdk.models.StringPref */ class RemoVideoProcessor : FFmpegVideoProcessor() { - private var streamProps: StreamInfo? = null - - override fun enable(context: Context, streamInfo: StreamInfo) { - super.enable(context, streamInfo) - this.streamProps = streamInfo - } - override fun getFilterOptions(props: StreamInfo): String { var filter = "" context?.let { 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 4814db3..e02c567 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 @@ -22,6 +22,7 @@ object Licenses { License("OkHttp", LicenseType.APACHE2_0), License("FFmpeg, github.com/btelman96/ffmpeg-android", LicenseType.LGPL3), License("github.com/btelman96/AndroidUvcDemo", LicenseType.GPL3 - , "https://raw.githubusercontent.com/btelman96/AndroidUvcDemo/master/LICENCE") + , "https://raw.githubusercontent.com/btelman96/AndroidUvcDemo/master/LICENCE"), + License("LogUtilKt, https://github.com/btelman96/LogUtilKt", LicenseType.APACHE2_0, "https://github.com/btelman96/LogUtilKt/blob/master/LICENSE") ) } \ No newline at end of file diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/models/api/Message.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/models/api/Message.kt index 9a85c94..189cdb6 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/models/api/Message.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/models/api/Message.kt @@ -1,36 +1,49 @@ package tv.remo.android.controller.sdk.models.api +import com.google.gson.Gson import com.google.gson.annotations.SerializedName import java.io.Serializable +import java.util.* data class Message( @SerializedName("badges") - val badges: List, + var badges: List, @SerializedName("broadcast") - val broadcast: String, + var broadcast: String, @SerializedName("chat_id") - val chatId: String, + var chatId: String, @SerializedName("display_message") - val displayMessage: Boolean, + var displayMessage: Boolean, @SerializedName("id") - val id: String, + var id: String, @SerializedName("message") var message: String, @SerializedName("sender") - val sender: String, + var sender: String, @SerializedName("sender_id") - val senderId: String, + var senderId: String, @SerializedName("server_id") - val serverId: String, + var serverId: String, @SerializedName("channel_id") - val channelId: String, + var channelId: String, @SerializedName("time_stamp") - val timeStamp: Long, + var timeStamp: Long, @SerializedName("type") - val type: String + var type: String ) : Serializable{ companion object{ const val serialversionUID = 3124135231234895L + fun createDummyMessage(username : String, message: String) : Message{ + val json = "{\"message\":\"test\",\"sender\":\"ReconDelta090\",\"sender_id\":\"user-6a9591cc-f3d3-4e47-a208-e749679a899a\",\"chat_id\":\"chat-8a05f730-c663-434d-9f24-6d8c24453c5f\",\"server_id\":\"serv-46437781-4a9b-4531-9db1-74bc2f818b58\",\"id\":\"mesg-ec54bf9a-23d4-4fa6-b6f1-9a5c8e3e2440\",\"time_stamp\":1574296097994,\"broadcast\":\"\",\"channel_id\":\"chan-7a304995-cba0-463c-81a6-ffeffc059058\",\"display_message\":true,\"badges\":[\"owner\"],\"type\":\"\"}" + return Gson().fromJson(json, Message::class.java).also { + it.sender = username + it.displayMessage = true + it.message = message + it.timeStamp = System.currentTimeMillis() + val id = UUID.randomUUID().toString() + it.id = id + } + } } } \ No newline at end of file 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 35d3e22..7c2f12e 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 @@ -76,13 +76,13 @@ object ComponentBuilderUtil { return Bundle().apply { val resolution = settings.cameraResolution.getPref().split("x") val streamInfo = StreamInfo( - settings.videoUrl - ,settings.audioUrl - ,deviceInfo = CameraDeviceInfo.fromCamera(settings.cameraDeviceId.getPref()) - ,orientation = Orientation.valueOf(settings.cameraOrientation.getPref()) - ,bitrate = settings.cameraBitrate.getPref().toIntOrNull() ?: 512 - ,width = resolution[0].toInt() - ,height = resolution[1].toInt() + settings.videoUrl, + settings.audioUrl, + deviceInfo = CameraDeviceInfo.fromCamera(settings.cameraDeviceId.getPref()), + orientation = Orientation.valueOf(settings.cameraOrientation.getPref()), + bitrate = settings.cameraBitrate.getPref().toIntOrNull() ?: 512, + width = resolution[0].toInt(), + height = resolution[1].toInt() ) //use our customized remo classes VideoRetrieverFactory.putClassInBundle(CameraCompatOverride::class.java, this) diff --git a/sdk/src/main/java/tv/remo/android/controller/sdk/utils/StringUtil.kt b/sdk/src/main/java/tv/remo/android/controller/sdk/utils/StringUtil.kt index 4f010d7..b876bcc 100644 --- a/sdk/src/main/java/tv/remo/android/controller/sdk/utils/StringUtil.kt +++ b/sdk/src/main/java/tv/remo/android/controller/sdk/utils/StringUtil.kt @@ -28,4 +28,11 @@ fun String.isUrl() : Boolean{ return true } return false +} + +fun String.startsWith(vararg prefix : String) : Boolean{ + prefix.forEach { + if(this.startsWith(it, false)) return true + } + return false } \ No newline at end of file diff --git a/settingsutil/src/main/res/drawable/circle.xml b/settingsutil/src/main/res/drawable/circle.xml index 475d442..7c5f085 100644 --- a/settingsutil/src/main/res/drawable/circle.xml +++ b/settingsutil/src/main/res/drawable/circle.xml @@ -4,7 +4,7 @@ android:shape="oval"> + android:color="#000"/>