Compare commits

...

48 Commits

Author SHA1 Message Date
Raymond Yang 4a8fb55fc8 拿掉LOG 2023-05-22 13:37:27 +08:00
Raymond Yang fdb88fc47a 更新Gradle Plugin 2023-05-22 13:36:56 +08:00
Raymond Yang ba12fd3856 1.優化for loop
2.優化VideoView get/set
2023-05-22 10:56:24 +08:00
Raymond Yang 8449706b64 1.加入TextureView支援
2.將取得畫面寬高的計算方式改成不使用baseView.post
2023-05-22 09:43:53 +08:00
Raymond Yang 02a26445be 同步HiSharpDX的程式碼 2023-05-19 14:02:47 +08:00
Raymond Yang 0021b30842 將viewpager改成scrollview 2023-03-27 16:52:08 +08:00
Raymond Yang 992dd860d9 補上UI文件 2023-03-09 18:00:13 +08:00
Raymond Yang 6378f4d80f 1.將重試功能改成VideoView內部呼叫
2.加上16分割
2023-03-09 17:52:39 +08:00
Raymond Yang f7aa9f74d2 VideoViewAdapter.kt加上對所有的SplitViewFragment.kt下指令的功能 2023-03-09 15:02:27 +08:00
Raymond Yang 0ffafcbaf7 VideoView.kt加上重試時的狀態 2023-03-09 14:59:52 +08:00
Raymond Yang 07afc01354 releaseSurface、pause加上isInit判斷 2023-03-09 14:48:25 +08:00
Raymond Yang d77fc98280 調整releaseSurface的呼叫時機 2023-03-09 09:06:25 +08:00
Raymond Yang 2897cdca0d 加入獨立監控假畫面 2023-03-08 17:57:29 +08:00
Raymond Yang c9ca4156aa 在surfaceDestroyed裡面呼叫pause 2023-03-08 13:37:26 +08:00
Raymond Yang 975e2eba79 GstLibrary改成kotlin 2023-03-08 11:36:48 +08:00
Raymond Yang 20949aa24d Rename .java to .kt 2023-03-08 11:36:47 +08:00
Raymond Yang e3c958c632 加入初始化旗標 2023-03-07 11:55:54 +08:00
Raymond Yang 48db07290c 調整onResume 2023-03-06 17:08:20 +08:00
Raymond Yang 1b80ae3349 更新Gradle Plugin 2023-03-06 10:11:18 +08:00
Raymond Yang ed8f2a7c0f 使用暴力方式解決閃退問題:加大ViewPager可以容許的page數量 2023-03-06 09:31:20 +08:00
Raymond Yang d8d351831c 加上exitProcess 2023-02-10 10:36:51 +08:00
Raymond Yang 77df0eb8a5 修正GstLibrary的LOG TAG名稱 2023-02-10 09:23:00 +08:00
Raymond Yang 0dca3ce9e8 C lib加上tag區分 2023-02-09 16:16:49 +08:00
Raymond Yang 363bfdbfbd 調整delay時間算法 2023-02-09 11:27:55 +08:00
Raymond Yang dc1da2449b 當MainActivity退出時,呼叫GStreamer的finalize 2023-02-08 14:41:33 +08:00
Raymond Yang 53763bfa34 將延遲參數常數化 2023-02-08 10:28:40 +08:00
Raymond Yang 4cf5ae5094 補上VideoViewAdapter 2023-02-07 17:01:44 +08:00
Raymond Yang ced178513b 將滑動控制加上播放/暫停 2023-02-07 17:01:07 +08:00
Raymond Yang 88954a6599 GStreamer更新到1.22.0 2023-02-07 16:02:56 +08:00
Raymond Yang df105dd518 將stopRetryCount函數加上removeCallbacks,停止重連功能 2023-02-07 13:45:41 +08:00
Raymond Yang 2f4e3659f9 更新Kotlin plugin 2023-02-06 14:03:39 +08:00
Raymond Yang 2e8d8a6381 將分割設定值存入Shared Preference內 2023-02-06 10:55:45 +08:00
Raymond Yang 87f731168f 如果目前的分割模式是點選的類型,則不刷新分割模式 2023-02-03 17:46:27 +08:00
Raymond Yang c7d0785cef 將延遲時間改成500毫秒,再繼續觀察 2023-02-03 17:40:22 +08:00
Raymond Yang a37e79d841 加上註解 2023-02-03 12:24:43 +08:00
Raymond Yang 4e09a449ca 將Device修改成HiSharpDX的資料結構 2023-02-03 12:07:06 +08:00
Raymond Yang 85218c443f 將跳轉加上延遲 2023-02-02 17:13:20 +08:00
Raymond Yang b4346f142b 將跳轉加上coroutine 2023-02-02 12:20:35 +08:00
Raymond Yang 9fc8673715 將停止播放函數改成手動呼叫,測試看看是否還會容易閃退 2023-02-02 10:20:00 +08:00
Raymond Yang de152da749 在surface create時,呼叫gstLibrary.setSurfaceHolder(holder) 2023-02-01 14:48:37 +08:00
Raymond Yang db673d3553 修正log內容 2023-02-01 13:35:46 +08:00
Raymond Yang 792ebc70c2 將VideoView的retryCount封裝 2023-02-01 10:38:22 +08:00
Raymond Yang 90344d14b0 VideoView加上預設狀態 2023-02-01 09:13:01 +08:00
Raymond Yang c3f8366475 加入點擊 2023-01-31 11:56:05 +08:00
Raymond Yang 15475923cb 整理專案結構 2023-01-31 09:49:26 +08:00
Raymond Yang a29f0e7bbf 加入重連機制 2023-01-30 16:16:20 +08:00
Raymond Yang ac15111ab3 加入重連機制 2023-01-30 16:15:34 +08:00
Raymond Yang cdd2c3775d 狀態加上緩衝中 2023-01-30 11:56:20 +08:00
215 changed files with 3428 additions and 695 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="16" /> <bytecodeTargetLevel target="17" />
</component> </component>
</project> </project>
+1 -1
View File
@@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" /> <option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="semeru-16" /> <option name="gradleJvm" value="jbr-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
+1 -2
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DesignSurface"> <component name="DesignSurface">
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
@@ -16,5 +15,5 @@
</option> </option>
</component> </component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_16_PREVIEW" project-jdk-name="11" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK" />
</project> </project>
+10 -9
View File
@@ -6,6 +6,7 @@ plugins {
} }
android { android {
namespace 'com.ray650128.gstreamer_demo_app'
ndkVersion "21.3.6528147" ndkVersion "21.3.6528147"
compileSdk 33 compileSdk 33
@@ -26,28 +27,28 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '17'
} }
lint { lint {
abortOnError false abortOnError false
checkReleaseBuilds false checkReleaseBuilds false
} }
viewBinding.enabled = true viewBinding.enabled = true
namespace 'com.ray650128.gstreamer_demo_app'
} }
dependencies { dependencies {
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.core:core-ktx:1.10.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@@ -55,6 +56,6 @@ dependencies {
implementation project(path: ':gstreamer_player') implementation project(path: ':gstreamer_player')
// Android Jetpack lib // Android Jetpack lib
implementation("androidx.fragment:fragment-ktx:1.5.5") implementation("androidx.fragment:fragment-ktx:1.5.7")
implementation("androidx.activity:activity-ktx:1.6.1") implementation("androidx.activity:activity-ktx:1.7.1")
} }
+9 -4
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.ray650128.gstreamer_demo_app">
<application <application
android:name=".MyApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -10,14 +10,19 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Gstreamer"> android:theme="@style/Theme.Gstreamer">
<activity <activity
android:name="com.ray650128.gstreamer_demo_app.MainActivity" android:name="com.ray650128.gstreamer_demo_app.ui.mainScreen.MainActivity"
android:exported="true"> android:exported="true"
android:screenOrientation="nosensor">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.monitoringScreen.MonitoringActivity"
android:exported="false"
android:screenOrientation="nosensor" />
</application> </application>
</manifest> </manifest>
@@ -0,0 +1,12 @@
package com.ray650128.gstreamer_demo_app
object Constants {
//region Split mode
const val SPLIT_MODE_SIXTEEN = 16
const val SPLIT_MODE_NINE = 9
const val SPLIT_MODE_FOUR = 4
const val SPLIT_MODE_SINGLE = 1
//endregion
const val CONF_DELAY_BASE_MILLIS = 1000L
}
@@ -0,0 +1,94 @@
package com.ray650128.gstreamer_demo_app
import android.content.Context
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowManager
/**
* 螢幕參數工具類別
* @author Raymond Yang
*/
class DisplayUtils(private var context: Context) {
private val TAG = DisplayUtils::class.java.simpleName
/**
* 取得螢幕寬度
* @return 螢幕寬度值
*/
fun getScreenWidth(): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
windowMetrics.bounds.width()
} else {
val metric = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metric)
metric.widthPixels
}
}
/**
* 取得螢幕高度
* @return 螢幕高度值
*/
fun getScreenHeight(): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
windowMetrics.bounds.height()
} else {
val metric = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metric)
metric.heightPixels
}
}
/**
* 取得狀態列高度
* @return 狀態列高度值
*/
fun getStatusBarHeight(): Int {
var result = 0
val resourceId: Int = context.resources.getIdentifier(
"status_bar_height",
"dimen",
"android"
)
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
return result
}
/**
* 取得導航列高度
* @return 導航列高度值
*/
fun getNavigationBarHeight(): Int {
val resources = context.resources
val resourceId: Int = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else 0
}
/**
* 取得視窗範圍高度
* @return 視窗範圍高度值
*/
fun getWindowHeight(): Int {
val screen = getScreenHeight()
val statusBar = getStatusBarHeight()
val navBar = getNavigationBarHeight()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
screen - statusBar - navBar
} else {
screen
}
}
}
@@ -1,97 +0,0 @@
package com.ray650128.gstreamer_demo_app
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.ray650128.gstreamer_demo_app.model.Device
class MainViewModel: ViewModel() {
private val uriList: List<Device> by lazy {
listOf(
Device(
name = "192.168.0.73",
rtspUrl = "rtsp://admin:hs22601576@@192.168.0.73:554/media/video2"
),
Device(
name = "192.168.0.77",
rtspUrl = "rtsp://admin:admin@192.168.0.77:554/media/video2"
),
Device(
name = "192.168.0.79",
rtspUrl = "rtsp://admin:1q2w3e4r!@192.168.0.79:554/media/video2"
),
Device(
name = "192.168.0.88",
rtspUrl = "rtsp://admin:1q2w3e4r~@211.23.78.226:8588/media/video2"
),
Device(
name = "192.168.0.74",
rtspUrl = "rtsp://admin:admin@211.23.78.226:8574/v02"
),
Device(
name = "192.168.0.75",
rtspUrl = "rtsp://admin:admin@211.23.78.226:8575/v02"
),
Device(
name = "192.168.0.76",
rtspUrl = "rtsp://admin:123456@211.23.78.226:8576/profile2"
),
Device(
name = "192.168.0.82",
rtspUrl = "rtsp://admin:123456@192.168.0.82:554/profile2"
),
Device(
name = "192.168.0.84",
rtspUrl = "rtsp://admin:123456@192.168.0.84:554/profile2"
),
Device(
name = "192.168.0.95",
rtspUrl = "rtsp://admin:123456@192.168.0.95:554/profile2"
)
)
}
private var splitModeInt: Int = PAGE_MODE_ONE
val splitMode: MutableLiveData<Int> by lazy { MutableLiveData<Int>() }
val cameraList: MediatorLiveData<List<List<Device>>> by lazy {
MediatorLiveData<List<List<Device>>>().apply {
addSource(splitMode) {
postValue(updateCameraList(uriList))
}
}
}
init {
setSplitMode(PAGE_MODE_ONE)
}
fun setSplitMode(mode: Int) {
splitModeInt = mode
splitMode.postValue(splitModeInt)
}
private fun updateCameraList(dbData: List<Device>?): List<List<Device>>? {
if (dbData.isNullOrEmpty()) return null
val tmpData = ArrayList<List<Device>>()
for (index in uriList.indices step (splitModeInt)) {
if (index == uriList.size) break
val tmpSubData = ArrayList<Device>()
for (subIndex in 0 until (splitModeInt)) {
val dataIndex = index + subIndex
if (dataIndex !in uriList.indices) break
tmpSubData.add(uriList[dataIndex])
}
tmpData.add(tmpSubData)
}
return tmpData
}
companion object {
const val PAGE_MODE_ONE = 1
const val PAGE_MODE_FOUR = 4
const val PAGE_MODE_NINE = 9
}
}
@@ -0,0 +1,49 @@
package com.ray650128.gstreamer_demo_app
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class MyApplication : Application(), Application.ActivityLifecycleCallbacks {
override fun onCreate() {
super.onCreate()
instance = this
registerActivityLifecycleCallbacks(this)
}
private var currentActivity: Activity? = null
override fun onActivityCreated(p0: Activity, p1: Bundle?) {}
override fun onActivityStarted(p0: Activity) {}
override fun onActivityResumed(p0: Activity) {
currentActivity = p0
}
override fun onActivityPaused(p0: Activity) {}
override fun onActivityStopped(p0: Activity) {}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
override fun onActivityDestroyed(p0: Activity) {
if (currentActivity?.equals(p0) == true) {
Log.e("MyApplication", "EXIT...")
onTerminate()
exitProcess(0)
}
}
companion object {
var instance: Application? = null
fun getAppContext(): Context {
return instance!!.applicationContext
}
}
}
@@ -0,0 +1,24 @@
package com.ray650128.gstreamer_demo_app
import android.content.Context
import com.ray650128.gstreamer_demo_app.ui.mainScreen.MainViewModel
/**
* Shared Preferences 工具類別
* @author Raymond Yang
*/
object PreferenceUtil {
private const val MAIN_KEY = "GST_DEMO_APP"
private const val IS_FIRST_OPEN_KEY = "IS_FIRST_OPEN_KEY"
private const val LAST_SPLIT_MODE = "LAST_SPLIT_MODE"
private val sharedPreferences = MyApplication.getAppContext().getSharedPreferences(MAIN_KEY, Context.MODE_PRIVATE)
var isFirstOpen: Boolean
get() = sharedPreferences.getBoolean(IS_FIRST_OPEN_KEY, true)
set(value) = sharedPreferences.edit().putBoolean(IS_FIRST_OPEN_KEY, value).apply()
var lastSplitMode: Int
get() = sharedPreferences.getInt(LAST_SPLIT_MODE, Constants.SPLIT_MODE_SINGLE)
set(value) = sharedPreferences.edit().putInt(LAST_SPLIT_MODE, value).apply()
}
@@ -1,206 +0,0 @@
package com.ray650128.gstreamer_demo_app
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import com.ray650128.gstreamer_demo_app.databinding.FragmentSplitViewBinding
import com.ray650128.gstreamer_demo_app.model.Device
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.sqrt
class SplitViewFragment : Fragment() {
private var mPageNum: Int = 0
private var splitMode = MainViewModel.PAGE_MODE_ONE
private var streamType = VideoView.SUB_STREAM
private var isClickable = true
private lateinit var binding: FragmentSplitViewBinding
private var data: ArrayList<Device> = ArrayList()
private var videoViews: ArrayList<VideoView> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
mPageNum = requireArguments().getInt(ARG_PAGE_NUM)
splitMode = requireArguments().getInt(ARG_SPLIT_MODE)
isClickable = requireArguments().getBoolean(ARG_CLICKABLE)
streamType = requireArguments().getInt(ARG_STREAM_TYPE)
data = requireArguments().getParcelableArrayList(ARG_PAGE_DATA) ?: arrayListOf(Device())
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentSplitViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
override fun onPause() {
super.onPause()
stopAll()
Log.d("${TAG}_$mPageNum", "onPause()")
}
override fun onResume() {
super.onResume()
playAll()
Log.d("${TAG}_$mPageNum", "onResume()")
}
private fun initView() {
// 生成 VideoView 分割畫面
binding.apply {
val maxRow = sqrt(splitMode.toFloat()).toInt()
val maxCol = sqrt(splitMode.toFloat()).toInt()
//Log.d(TAG, "maxRow: $maxRow, maxCol: $maxCol")
baseView.rowCount = maxRow
baseView.columnCount = maxCol
Log.e("${TAG}_$mPageNum", "baseView.rowCount: ${baseView.rowCount}, baseView.columnCount: ${baseView.columnCount}")
baseView.post {
for (col in 0 until maxCol) {
for (row in 0 until maxRow) {
val videoView = VideoView(requireContext())
val layoutParam = GridLayout.LayoutParams().apply {
topMargin = 0.dp
bottomMargin = 0.dp
marginEnd = 0.dp
marginStart = 0.dp
// 調整間距
when (splitMode) {
MainViewModel.PAGE_MODE_FOUR -> {
when (col) {
0 -> bottomMargin = 2.dp
1 -> topMargin = 2.dp
}
when (row) {
0 -> marginEnd = 2.dp
1 -> marginStart = 2.dp
}
width = (baseView.width / maxRow) - maxRow.dp
height = (baseView.height / maxCol) - maxCol.dp
}
MainViewModel.PAGE_MODE_NINE -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginEnd = 4.dp
marginStart = 4.dp
}
width = (baseView.width / maxRow) - maxRow.dp
height = (baseView.height / maxCol) - maxCol.dp
}
MainViewModel.PAGE_MODE_ONE -> {
width = (baseView.width / maxRow)
height = (baseView.height / maxCol)
}
}
}
baseView.addView(videoView, layoutParam)
videoViews.add(videoView)
}
}
setAllUrl()
}
}
if (isClickable) {
for (position in videoViews.indices) {
videoViews[position].setOnClickListener {
if (position >= data.size) return@setOnClickListener
/*if (!videoViews[position].isPlaying) {
if (!videoViews[position].isLoading) {
videoViews[position].play()
}
return@setSafeOnClickListener
}
stopAll()
val item = data[position]
val bundle = Bundle().apply {
putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
}
gotoActivity(MonitoringActivity::class.java, bundle)*/
//Log.d("${TAG}_$mPageNum", "check: $item")
}
}
}
}
private fun setAllUrl() {
for (index in data.indices) {
videoViews[index].setData(data[index])
videoViews[index].setTextVisible((splitMode != MainViewModel.PAGE_MODE_NINE))
}
}
fun playAll() = MainScope().launch(Dispatchers.Main) {
if (videoViews.isEmpty()) return@launch
for (index in data.indices) {
videoViews[index].play()
delay(300)
}
}
fun stopAll() = MainScope().launch(Dispatchers.Main) {
for (index in data.indices) {
videoViews[index].stop()
delay(300)
}
}
companion object {
private val TAG = SplitViewFragment::class.java.simpleName
private const val ARG_PAGE_NUM = "page_number"
private const val ARG_SPLIT_MODE = "split_mode"
private const val ARG_CLICKABLE = "clickable"
private const val ARG_PAGE_DATA = "page_data"
private const val ARG_STREAM_TYPE = "stream_type"
/**
* 透過傳入的參數,生成新的 Fragment 實例
*
* @param pageNumber 該 Fragment 頁碼
* @param splitMode 畫面分割模式(9/4/1分割)
* @return 透過傳入的參數,生成新的 Fragment 實例
*/
fun newInstance(
pageNumber: Int,
splitMode: Int,
isClickable: Boolean = true,
pageData: ArrayList<Device>,
streamType: Int = VideoView.SUB_STREAM
): SplitViewFragment {
val fragment = SplitViewFragment()
val args = Bundle()
args.putInt(ARG_PAGE_NUM, pageNumber)
args.putInt(ARG_SPLIT_MODE, splitMode)
args.putBoolean(ARG_CLICKABLE, isClickable)
args.putParcelableArrayList(ARG_PAGE_DATA, pageData)
args.putInt(ARG_STREAM_TYPE, streamType)
fragment.arguments = args
return fragment
}
}
}
@@ -1,162 +0,0 @@
package com.ray650128.gstreamer_demo_app
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.hisharp.gstreamer_player.GstCallback
import com.hisharp.gstreamer_player.GstLibrary
import com.hisharp.gstreamer_player.GstStatus
import com.ray650128.gstreamer_demo_app.databinding.ItemVideoViewBinding
import com.ray650128.gstreamer_demo_app.model.Device
class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet, intRes: Int) : super(context, attributeSet, intRes) {
initView(context)
}
private lateinit var view: ItemVideoViewBinding
private var data: Device? = null
var isLoading: Boolean = false
set(value) {
view.pbLoading.isVisible = value
field = value
}
var isPlaying: Boolean = false
set(value) {
view.imgPause.isVisible = !value
field = value
}
private val videoView: SurfaceView by lazy { view.videoView }
private lateinit var gstLibrary: GstLibrary
private var streamType = MAIN_STREAM
private var retryCount = 0
private val mHandler: MyHandler by lazy {
MyHandler(Looper.getMainLooper())
}
private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context)
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true
videoView.holder.addCallback(this)
gstLibrary = GstLibrary(context)
gstLibrary.setOnStatusChangeListener(this)
}
fun setData(device: Device?) {
if (device == null) {
view.textDeviceName.isVisible = false
isPlaying = false
return
}
this.data = device
this.tag = device.name
view.textDeviceName.text = device.name
view.textDeviceName.isVisible = true
gstLibrary.setRtspUrl(this.data?.rtspUrl)
Log.d("${TAG}_$tag", "Set device to: $device")
}
fun setTextVisible(isVisible: Boolean) {
view.textDeviceName.isVisible = isVisible
}
fun play() {
videoView.postInvalidate()
gstLibrary.play()
retryCount = 0
}
fun stop() {
isPlaying = false
if (this::gstLibrary.isInitialized) {
gstLibrary.stop()
}
retryCount = 999
}
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d("${TAG}_$tag", "Surface created: " + holder.surface)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d("${TAG}_$tag", "Surface changed to format: $format, width: $width, height: $height")
if (this::gstLibrary.isInitialized) {
gstLibrary.setSurfaceHolder(holder)
}
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
Log.d("${TAG}_$tag", "Surface destroyed")
// TODO: 如果呼叫 releaseSurface(),會閃退
/*if (this::gstLibrary.isInitialized) {
gstLibrary.releaseSurface()
}*/
}
override fun onStatus(gstStatus: GstStatus?) { // onStatus 不是在主執行緒,因此透過 Handler 發訊息到主執行緒去執行
when (gstStatus) {
GstStatus.PLAYING -> mHandler.sendMessage(Message().apply { what = MSG_PLAY })
GstStatus.PAUSE -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
GstStatus.ERROR_WHEN_OPENING -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
else -> {}
}
Log.e("${TAG}_$tag", "onStatus: $gstStatus")
}
override fun onMessage(message: String?) {
Log.e("${TAG}_$tag", "onMessage: $message")
}
inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
when(msg.what) {
MSG_PAUSE -> {
isPlaying = false
isLoading = false
}
MSG_PLAY -> {
isLoading = false
isPlaying = true
}
}
}
}
companion object {
private val TAG = VideoView::class.java.simpleName
const val MAIN_STREAM = 1
const val SUB_STREAM = 2
private const val MSG_PAUSE = 1
private const val MSG_PLAY = 2
}
}
@@ -0,0 +1,55 @@
package com.ray650128.gstreamer_demo_app.extensions
import android.util.Log
import com.ray650128.gstreamer_demo_app.model.Device
/**
* IpCam 擴充函式-取得完整串流路徑
* @param streamPathNum 串流路徑編號
* @author Raymond Yang
*/
fun Device.getStreamPath(streamPathNum: Int): String? {
val stringBuilder = getPath(this) ?: return null
// 加入串流路徑
when (streamPathNum) {
1 -> if (this.stream1.isNotEmpty()) {
if (this.stream1.first() != '/') {
stringBuilder.append("/")
}
stringBuilder.append(this.stream1)
}
2 -> if (this.stream2.isNotEmpty()) {
if (this.stream2.first() != '/') {
stringBuilder.append("/")
}
stringBuilder.append(this.stream2)
}
else -> return null
}
Log.d("+++URL", stringBuilder.toString())
return stringBuilder.toString()
}
private fun getPath(ipCam: Device): StringBuilder? {
if (ipCam.ip.isEmpty()) return null
val stringBuilder = StringBuilder()
// 加入 URL schema
stringBuilder.append("rtsp://")
// 加入帳號密碼
if (ipCam.account.isNotEmpty()) {
if (ipCam.password.isEmpty()) {
stringBuilder.append("${ipCam.account}@")
} else {
stringBuilder.append("${ipCam.account}:${ipCam.password}@")
}
}
// 加入 IP 及 port
stringBuilder.append(ipCam.ip)
if (ipCam.rtspPort.isNotEmpty()) {
stringBuilder.append(":${ipCam.rtspPort}")
}
return stringBuilder
}
@@ -5,7 +5,13 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Device( data class Device(
val name: String = "", var ip: String = "",
val rtspUrl: String = "", var rtspPort: String = "",
val deviceName: String = "",
var account: String = "",
var password: String = "",
var stream1: String = "",
var stream2: String = "",
//val rtspUrl: String = "",
var isPlaying: Boolean = false var isPlaying: Boolean = false
): Parcelable ): Parcelable
@@ -0,0 +1,484 @@
package com.ray650128.gstreamer_demo_app.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/**
* A view group that allows users to switch between multiple screens (layouts) in the same way as
* the Android home screen (Launcher application).
* <p>
* You can add and remove views using the normal methods {@link ViewGroup#addView(View)},
* {@link ViewGroup#removeView(View)} etc. You may want to listen for updates by calling
* {@link HorizontalPager#setOnScreenSwitchListener(OnScreenSwitchListener)} in order to perform
* operations once a new screen has been selected.
*
* Modifications from original version (ysamlan): Animate argument in setCurrentScreen and duration
* in snapToScreen; onInterceptTouchEvent handling to support nesting a vertical Scrollview inside
* the RealViewSwitcher; allowing snapping to a view even during an ongoing scroll; snap to
* next/prev view on 25% scroll change; density-independent swipe sensitivity; width-independent
* pager animation durations on scrolling to properly handle large screens without excessively
* long animations.
*
* Other modifications:
* (aveyD) Handle orientation changes properly and fully snap to the right position.
*
* @author Marc Reichelt, <a href="http://www.marcreichelt.de/">http://www.marcreichelt.de/</a>
* @version 0.1.0
*/
public final class HorizontalPager extends ViewGroup {
/*
* How long to animate between screens when programmatically setting with setCurrentScreen using
* the animate parameter
*/
private static final int ANIMATION_SCREEN_SET_DURATION_MILLIS = 500;
// What fraction (1/x) of the screen the user must swipe to indicate a page change
private static final int FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE = 4;
private static final int INVALID_SCREEN = -1;
/*
* Velocity of a swipe (in density-independent pixels per second) to force a swipe to the
* next/previous screen. Adjusted into mDensityAdjustedSnapVelocity on init.
*/
private static final int SNAP_VELOCITY_DIP_PER_SECOND = 600;
// Argument to getVelocity for units to give pixels per second (1 = pixels per millisecond).
private static final int VELOCITY_UNIT_PIXELS_PER_SECOND = 1000;
private static final int TOUCH_STATE_REST = 0;
private static final int TOUCH_STATE_HORIZONTAL_SCROLLING = 1;
private static final int TOUCH_STATE_VERTICAL_SCROLLING = -1;
private int mCurrentScreen;
private int mDensityAdjustedSnapVelocity;
private boolean mFirstLayout = true;
private float mLastMotionX;
private float mLastMotionY;
private OnScreenSwitchListener mOnScreenSwitchListener;
private int mMaximumVelocity;
private int mNextScreen = INVALID_SCREEN;
private Scroller mScroller;
private int mTouchSlop;
private int mTouchState = TOUCH_STATE_REST;
private VelocityTracker mVelocityTracker;
private int mLastSeenLayoutWidth = -1;
/**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public HorizontalPager(final Context context) {
super(context);
init();
}
/**
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file, supplying attributes
* that were specified in the XML file. This version uses a default style of
* 0, so the only attribute values applied are those in the Context's Theme
* and the given AttributeSet.
*
* <p>
* The method onFinishInflate() will be called after all children have been
* added.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @see #View(Context, AttributeSet, int)
*/
public HorizontalPager(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Sets up the scroller and touch/fling sensitivity parameters for the pager.
*/
private void init() {
mScroller = new Scroller(getContext());
// Calculate the density-dependent snap velocity in pixels
DisplayMetrics displayMetrics = new DisplayMetrics();
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
.getMetrics(displayMetrics);
mDensityAdjustedSnapVelocity =
(int) (displayMetrics.density * SNAP_VELOCITY_DIP_PER_SECOND);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("ViewSwitcher can only be used in EXACTLY mode.");
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("ViewSwitcher can only be used in EXACTLY mode.");
}
// The children are given the same width and height as the workspace
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
if (mFirstLayout) {
scrollTo(mCurrentScreen * width, 0);
mFirstLayout = false;
}
else if (width != mLastSeenLayoutWidth) { // Width has changed
/*
* Recalculate the width and scroll to the right position to be sure we're in the right
* place in the event that we had a rotation that didn't result in an activity restart
* (code by aveyD). Without this you can end up between two pages after a rotation.
*/
Display display =
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
int displayWidth = display.getWidth();
mNextScreen = Math.max(0, Math.min(getCurrentScreen(), getChildCount() - 1));
final int newX = mNextScreen * displayWidth;
final int delta = newX - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0, 0);
}
mLastSeenLayoutWidth = width;
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r,
final int b) {
int childLeft = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
final int childWidth = child.getMeasuredWidth();
child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
childLeft += childWidth;
}
}
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
/*
* By Yoni Samlan: Modified onInterceptTouchEvent based on standard ScrollView's
* onIntercept. The logic is designed to support a nested vertically scrolling view inside
* this one; once a scroll registers for X-wise scrolling, handle it in this view and don't
* let the children, but once a scroll registers for y-wise scrolling, let the children
* handle it exclusively.
*/
final int action = ev.getAction();
boolean intercept = false;
switch (action) {
case MotionEvent.ACTION_MOVE:
/*
* If we're in a horizontal scroll event, take it (intercept further events). But if
* we're mid-vertical-scroll, don't even try; let the children deal with it. If we
* haven't found a scroll event yet, check for one.
*/
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
/*
* We've already started a horizontal scroll; set intercept to true so we can
* take the remainder of all touch events in onTouchEvent.
*/
intercept = true;
} else if (mTouchState == TOUCH_STATE_VERTICAL_SCROLLING) {
// Let children handle the events for the duration of the scroll event.
intercept = false;
} else { // We haven't picked up a scroll event yet; check for one.
/*
* If we detected a horizontal scroll event, start stealing touch events (mark
* as scrolling). Otherwise, see if we had a vertical scroll event -- if so, let
* the children handle it and don't look to intercept again until the motion is
* done.
*/
final float x = ev.getX();
final int xDiff = (int) Math.abs(x - mLastMotionX);
boolean xMoved = xDiff > mTouchSlop;
if (xMoved) {
// Scroll if the user moved far enough along the X axis
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
mLastMotionX = x;
}
final float y = ev.getY();
final int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > mTouchSlop;
if (yMoved) {
mTouchState = TOUCH_STATE_VERTICAL_SCROLLING;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Release the drag.
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_DOWN:
/*
* No motion yet, but register the coordinates so we can check for intercept at the
* next MOVE event.
*/
mLastMotionY = ev.getY();
mLastMotionX = ev.getX();
break;
default:
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(final MotionEvent ev) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished will be false if
* being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionX = x;
if (mScroller.isFinished()) {
mTouchState = TOUCH_STATE_REST;
} else {
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
}
break;
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(x - mLastMotionX);
boolean xMoved = xDiff > mTouchSlop;
if (xMoved) {
// Scroll if the user moved far enough along the X axis
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
}
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
// Scroll to follow the motion event
final int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
final int scrollX = getScrollX();
if (deltaX < 0) {
if (scrollX > 0) {
scrollBy(Math.max(-scrollX, deltaX), 0);
}
} else if (deltaX > 0) {
final int availableToScroll =
getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX), 0);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(VELOCITY_UNIT_PIXELS_PER_SECOND,
mMaximumVelocity);
int velocityX = (int) velocityTracker.getXVelocity();
if (velocityX > mDensityAdjustedSnapVelocity && mCurrentScreen > 0) {
// Fling hard enough to move left
snapToScreen(mCurrentScreen - 1);
} else if (velocityX < -mDensityAdjustedSnapVelocity
&& mCurrentScreen < getChildCount() - 1) {
// Fling hard enough to move right
snapToScreen(mCurrentScreen + 1);
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
default:
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
} else if (mNextScreen != INVALID_SCREEN) {
mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
// Notify observer about screen change
if (mOnScreenSwitchListener != null) {
mOnScreenSwitchListener.onScreenSwitched(mCurrentScreen);
}
mNextScreen = INVALID_SCREEN;
}
}
/**
* Returns the index of the currently displayed screen.
*
* @return The index of the currently displayed screen.
*/
public int getCurrentScreen() {
return mCurrentScreen;
}
/**
* Sets the current screen.
*
* @param currentScreen The new screen.
* @param animate True to smoothly scroll to the screen, false to snap instantly
*/
public void setCurrentScreen(final int currentScreen, final boolean animate) {
mCurrentScreen = Math.max(0, Math.min(currentScreen, getChildCount() - 1));
if (animate) {
snapToScreen(currentScreen, ANIMATION_SCREEN_SET_DURATION_MILLIS);
} else {
scrollTo(mCurrentScreen * getWidth(), 0);
}
invalidate();
}
/**
* Sets the {@link OnScreenSwitchListener}.
*
* @param onScreenSwitchListener The listener for switch events.
*/
public void setOnScreenSwitchListener(final OnScreenSwitchListener onScreenSwitchListener) {
mOnScreenSwitchListener = onScreenSwitchListener;
}
/**
* Snaps to the screen we think the user wants (the current screen for very small movements; the
* next/prev screen for bigger movements).
*/
private void snapToDestination() {
final int screenWidth = getWidth();
int scrollX = getScrollX();
int whichScreen = mCurrentScreen;
int deltaX = scrollX - (screenWidth * mCurrentScreen);
// Check if they want to go to the prev. screen
if ((deltaX < 0) && mCurrentScreen != 0
&& ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < -deltaX)) {
whichScreen--;
// Check if they want to go to the next screen
} else if ((deltaX > 0) && (mCurrentScreen + 1 != getChildCount())
&& ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < deltaX)) {
whichScreen++;
}
snapToScreen(whichScreen);
}
/**
* Snap to a specific screen, animating automatically for a duration proportional to the
* distance left to scroll.
*
* @param whichScreen Screen to snap to
*/
private void snapToScreen(final int whichScreen) {
snapToScreen(whichScreen, -1);
}
/**
* Snaps to a specific screen, animating for a specific amount of time to get there.
*
* @param whichScreen Screen to snap to
* @param duration -1 to automatically time it based on scroll distance; a positive number to
* make the scroll take an exact duration.
*/
private void snapToScreen(final int whichScreen, final int duration) {
/*
* Modified by Yoni Samlan: Allow new snapping even during an ongoing scroll animation. This
* is intended to make HorizontalPager work as expected when used in conjunction with a
* RadioGroup used as "tabbed" controls. Also, make the animation take a percentage of our
* normal animation time, depending how far they've already scrolled.
*/
mNextScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
final int newX = mNextScreen * getWidth();
final int delta = newX - getScrollX();
if (duration < 0) {
// E.g. if they've scrolled 80% of the way, only animation for 20% of the duration
mScroller.startScroll(getScrollX(), 0, delta, 0, (int) (Math.abs(delta)
/ (float) getWidth() * ANIMATION_SCREEN_SET_DURATION_MILLIS));
} else {
mScroller.startScroll(getScrollX(), 0, delta, 0, duration);
}
invalidate();
}
/**
* Listener for the event that the HorizontalPager switches to a new view.
*/
public static interface OnScreenSwitchListener {
/**
* Notifies listeners about the new screen. Runs after the animation completed.
*
* @param screen The new screen index.
*/
void onScreenSwitched(int screen);
}
}
@@ -1,20 +1,28 @@
package com.ray650128.gstreamer_demo_app package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.ray650128.gstreamer_demo_app.Constants
import com.ray650128.gstreamer_demo_app.R
import com.ray650128.gstreamer_demo_app.databinding.ActivityMainBinding import com.ray650128.gstreamer_demo_app.databinding.ActivityMainBinding
import com.ray650128.gstreamer_demo_app.model.Device import com.ray650128.gstreamer_demo_app.model.Device
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.freedesktop.gstreamer.GStreamer
/**
* MainActivity.kt
* 應用程式主畫面
*/
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -25,11 +33,9 @@ class MainActivity : AppCompatActivity() {
private var splitMode = 1 private var splitMode = 1
private lateinit var splitVideoViewAdapter: VideoViewAdapter
private var videos: List<List<Device>>? = null private var videos: List<List<Device>>? = null
private var currentPage = 0 private var videoPageList: ArrayList<SplitViewFragment> = arrayListOf()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -43,35 +49,36 @@ class MainActivity : AppCompatActivity() {
initObservers() initObservers()
} }
override fun onBackPressed() {
super.onBackPressed()
finish()
}
private fun initContentView() = binding.apply { private fun initContentView() = binding.apply {
//region Content area //region Content area
splitVideoViewAdapter = VideoViewAdapter(supportFragmentManager, lifecycle)
viewPager.apply {
adapter = splitVideoViewAdapter
offscreenPageLimit = 1
registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentPage = position
}
})
}
button.setOnClickListener { button.setOnClickListener {
viewModel.setSplitMode(MainViewModel.PAGE_MODE_ONE) if (splitMode == Constants.SPLIT_MODE_SINGLE) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_SINGLE)
Log.e(TAG, "+++ split style: 1") Log.e(TAG, "+++ split style: 1")
} }
button2.setOnClickListener { button2.setOnClickListener {
viewModel.setSplitMode(MainViewModel.PAGE_MODE_FOUR) if (splitMode == Constants.SPLIT_MODE_FOUR) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_FOUR)
Log.e(TAG, "+++ split style: 4") Log.e(TAG, "+++ split style: 4")
} }
button3.setOnClickListener { button3.setOnClickListener {
viewModel.setSplitMode(MainViewModel.PAGE_MODE_NINE) if (splitMode == Constants.SPLIT_MODE_NINE) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_NINE)
Log.e(TAG, "+++ split style: 9") Log.e(TAG, "+++ split style: 9")
} }
button4.setOnClickListener {
if (splitMode == Constants.SPLIT_MODE_SIXTEEN) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_SIXTEEN)
Log.e(TAG, "+++ split style: 16")
}
//endregion //endregion
} }
@@ -84,13 +91,18 @@ class MainActivity : AppCompatActivity() {
viewModel.cameraList.observe(this) { list -> viewModel.cameraList.observe(this) { list ->
this.videos = list this.videos = list
reloadVideoViews(this.videos) reloadVideoViews(this.videos)
currentPage = 0
} }
} }
private fun reloadVideoViews(list: List<List<Device>>?) = MainScope().launch { private fun reloadVideoViews(list: List<List<Device>>?) {
binding.viewPager.setCurrentItem(0, false) for (videoPage in videoPageList) {
splitVideoViewAdapter.clear() supportFragmentManager.commit {
remove(videoPage)
}
}
binding.viewPager.removeAllViews()
videoPageList.clear()
// 如果群組內沒有裝置,則顯示底圖 // 如果群組內沒有裝置,則顯示底圖
if (list.isNullOrEmpty()) { if (list.isNullOrEmpty()) {
binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing) binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing)
@@ -102,7 +114,15 @@ class MainActivity : AppCompatActivity() {
splitMode = splitMode, splitMode = splitMode,
pageData = ArrayList(list[i]) pageData = ArrayList(list[i])
) )
splitVideoViewAdapter.add(i, splitFragment) videoPageList.add(i, splitFragment)
val frameLayout = FrameLayout(mContext).apply {
id = View.generateViewId()
}
binding.viewPager.addView(frameLayout)
supportFragmentManager.commit {
add(frameLayout.id, splitFragment, "$i")
}
} }
} }
} }
@@ -0,0 +1,140 @@
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.ray650128.gstreamer_demo_app.PreferenceUtil
import com.ray650128.gstreamer_demo_app.model.Device
class MainViewModel: ViewModel() {
private val uriList: List<Device> by lazy {
listOf(
Device(
deviceName = "192.168.0.77",
ip = "192.168.0.77",
rtspPort = "554",
account = "admin",
password = "admin",
stream1 = "/media/video1",
stream2 = "/media/video2",
),
/*Device(
deviceName = "192.168.0.73",
ip = "192.168.0.73",
rtspPort = "554",
account = "admin",
password = "hs22601576",
stream1 = "/media/video1",
stream2 = "/media/video2",
),*/
/*Device(
deviceName = "192.168.0.79",
ip = "192.168.0.79",
rtspPort = "554",
account = "admin",
password = "1q2w3e4r!",
stream1 = "/media/video1",
stream2 = "/media/video2",
),*/
Device(
deviceName = "192.168.0.88",
ip = "211.23.78.226",
rtspPort = "8588",
account = "admin",
password = "1q2w3e4r~",
stream1 = "/media/video1",
stream2 = "/media/video2",
),
Device(
deviceName = "192.168.0.74",
ip = "211.23.78.226",
rtspPort = "8574",
account = "admin",
password = "admin",
stream1 = "/v01",
stream2 = "/v02",
),
Device(
deviceName = "192.168.0.75",
ip = "211.23.78.226",
rtspPort = "8575",
account = "admin",
password = "admin",
stream1 = "/v01",
stream2 = "/v02",
),
/*Device(
deviceName = "192.168.0.76",
ip = "211.23.78.226",
rtspPort = "8576",
account = "admin",
password = "123456",
stream1 = "/profile1",
stream2 = "/profile2",
),*/
Device(
deviceName = "192.168.0.82",
ip = "192.168.0.82",
rtspPort = "554",
account = "admin",
password = "123456",
stream1 = "/profile1",
stream2 = "/profile2",
),
Device(
deviceName = "192.168.0.84",
ip = "192.168.0.84",
rtspPort = "554",
account = "admin",
password = "123456",
stream1 = "/profile1",
stream2 = "/profile2",
),
/*Device(
deviceName = "192.168.0.95",
ip = "192.168.0.95",
rtspPort = "554",
account = "admin",
password = "123456",
stream1 = "/profile1",
stream2 = "/profile2",
)*/
)
}
val splitMode: MutableLiveData<Int> by lazy { MutableLiveData<Int>() }
val cameraList: MediatorLiveData<List<List<Device>>> by lazy {
MediatorLiveData<List<List<Device>>>().apply {
addSource(splitMode) {
postValue(updateCameraList(uriList))
}
}
}
init {
splitMode.postValue(PreferenceUtil.lastSplitMode)
}
fun setSplitMode(mode: Int) {
PreferenceUtil.lastSplitMode = mode
splitMode.postValue(PreferenceUtil.lastSplitMode)
}
private fun updateCameraList(dbData: List<Device>?): List<List<Device>>? {
if (dbData.isNullOrEmpty()) return null
val tmpData = ArrayList<List<Device>>()
for (index in uriList.indices step (PreferenceUtil.lastSplitMode)) {
if (index == uriList.size) break
val tmpSubData = ArrayList<Device>()
for (subIndex in 0 until (PreferenceUtil.lastSplitMode)) {
val dataIndex = index + subIndex
if (dataIndex !in uriList.indices) break
tmpSubData.add(uriList[dataIndex])
}
tmpData.add(tmpSubData)
}
return tmpData
}
}
@@ -0,0 +1,256 @@
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import com.ray650128.gstreamer_demo_app.Constants
import com.ray650128.gstreamer_demo_app.DisplayUtils
import com.ray650128.gstreamer_demo_app.R
import com.ray650128.gstreamer_demo_app.databinding.FragmentSplitViewBinding
import com.ray650128.gstreamer_demo_app.dp
import com.ray650128.gstreamer_demo_app.model.Device
import com.ray650128.gstreamer_demo_app.ui.monitoringScreen.MonitoringActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.sqrt
class SplitViewFragment : Fragment() {
private var mPageNum: Int = 0
private var splitMode = Constants.SPLIT_MODE_SINGLE
private var streamType = VideoView.SUB_STREAM
private var isClickable = true
private lateinit var binding: FragmentSplitViewBinding
private var data: ArrayList<Device> = ArrayList()
private var videoViews: ArrayList<VideoView> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
mPageNum = requireArguments().getInt(ARG_PAGE_NUM)
splitMode = requireArguments().getInt(ARG_SPLIT_MODE)
isClickable = requireArguments().getBoolean(ARG_CLICKABLE)
streamType = requireArguments().getInt(ARG_STREAM_TYPE)
data = requireArguments().getParcelableArrayList(ARG_PAGE_DATA) ?: arrayListOf(Device())
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentSplitViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
override fun onPause() {
stopAll()
super.onPause()
Log.d("${TAG}_$mPageNum", "onPause()")
}
override fun onResume() {
super.onResume()
playAll()
Log.d("${TAG}_$mPageNum", "onResume()")
}
override fun onDestroy() {
//destroyAll()
stopAll()
Log.d("${TAG}_$mPageNum", "onDestroy()")
super.onDestroy()
}
private fun initView() {
val displayUtil = DisplayUtils(requireContext())
// 生成 VideoView 分割畫面
binding.apply {
val maxRow = sqrt(splitMode.toFloat()).toInt()
val maxCol = sqrt(splitMode.toFloat()).toInt()
//Log.d(TAG, "maxRow: $maxRow, maxCol: $maxCol")
baseView.rowCount = maxRow
baseView.columnCount = maxCol
val cellWidth: Int
val cellHeight: Int
when (splitMode) {
Constants.SPLIT_MODE_SINGLE -> {
cellWidth = (displayUtil.getScreenWidth() / maxRow)
cellHeight = (cellWidth * 0.5625).toInt()
}
else -> {
cellWidth = (displayUtil.getScreenWidth() / maxRow) - maxRow.dp
cellHeight = (cellWidth * 0.5625).toInt() - maxCol.dp
}
}
for (col in 0 until maxCol) {
for (row in 0 until maxRow) {
val videoView = VideoView(requireContext())
val layoutParam = GridLayout.LayoutParams().apply {
topMargin = 0.dp
bottomMargin = 0.dp
marginEnd = 0.dp
marginStart = 0.dp
width = cellWidth
height = cellHeight
// 調整間距
when (splitMode) {
Constants.SPLIT_MODE_FOUR -> {
when (col) {
0 -> bottomMargin = 2.dp
1 -> topMargin = 2.dp
}
when (row) {
0 -> marginEnd = 2.dp
1 -> marginStart = 2.dp
}
}
Constants.SPLIT_MODE_NINE -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginEnd = 4.dp
marginStart = 4.dp
}
}
Constants.SPLIT_MODE_SIXTEEN -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 2.dp
}
if (col == 2) {
topMargin = 2.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginStart = 4.dp
marginEnd = 2.dp
}
if (row == 2) {
marginStart = 2.dp
marginEnd = 4.dp
}
}
}
}
baseView.addView(videoView, layoutParam)
videoViews.add(videoView)
}
}
}
if (isClickable) {
videoViews.forEach { videoView ->
videoView.setOnClickListener {
if (!videoView.isPlaying) {
return@setOnClickListener
}
stopAll()
val item = videoView.data
val bundle = Bundle().apply {
//putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
//putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
putParcelable(MonitoringActivity.BUNDLE_DEVICE, item)
}
val intent = Intent(requireContext(), MonitoringActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
//gotoActivity(MonitoringActivity::class.java, bundle)*/
//Log.d("${TAG}_$mPageNum", "check: $item")
}
}
}
setAllUrl()
}
private fun setAllUrl() {
for (index in data.indices) {
videoViews[index].data = data[index]
videoViews[index].setTextVisible(
(splitMode != Constants.SPLIT_MODE_NINE && splitMode != Constants.SPLIT_MODE_SIXTEEN)
)
}
}
fun playAll() {
videoViews.forEach { videoView ->
if (videoView.isReady) {
videoView.resetRetryCount()
videoView.play()
}
}
}
fun stopAll() {
videoViews.forEach { videoView ->
videoView.stopRetryCount()
if (videoView.isPlaying || !videoView.isLoading) {
videoView.pause()
}
}
}
fun destroyAll() {
videoViews.forEach { videoView ->
//videoView.destroy()
videoView.destroySurface()
}
}
companion object {
private val TAG = SplitViewFragment::class.java.simpleName
private const val ARG_PAGE_NUM = "page_number"
private const val ARG_SPLIT_MODE = "split_mode"
private const val ARG_CLICKABLE = "clickable"
private const val ARG_PAGE_DATA = "page_data"
private const val ARG_STREAM_TYPE = "stream_type"
/**
* 透過傳入的參數,生成新的 Fragment 實例
*
* @param pageNumber 該 Fragment 頁碼
* @param splitMode 畫面分割模式(9/4/1分割)
* @return 透過傳入的參數,生成新的 Fragment 實例
*/
fun newInstance(
pageNumber: Int,
splitMode: Int,
isClickable: Boolean = true,
pageData: ArrayList<Device>,
streamType: Int = VideoView.SUB_STREAM
): SplitViewFragment {
val fragment = SplitViewFragment()
val args = Bundle()
args.putInt(ARG_PAGE_NUM, pageNumber)
args.putInt(ARG_SPLIT_MODE, splitMode)
args.putBoolean(ARG_CLICKABLE, isClickable)
args.putParcelableArrayList(ARG_PAGE_DATA, pageData)
args.putInt(ARG_STREAM_TYPE, streamType)
fragment.arguments = args
return fragment
}
}
}
@@ -0,0 +1,10 @@
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SplitViewModel : ViewModel() {
val activePage: MutableLiveData<Int> by lazy {
MutableLiveData()
}
}
@@ -0,0 +1,219 @@
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.hisharp.gstreamer_player.GstCallback
import com.hisharp.gstreamer_player.GstLibrary
import com.hisharp.gstreamer_player.GstStatus
import com.ray650128.gstreamer_demo_app.databinding.ItemVideoViewBinding
import com.ray650128.gstreamer_demo_app.extensions.getStreamPath
import com.ray650128.gstreamer_demo_app.model.Device
class VideoView : ConstraintLayout, GstCallback {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet, intRes: Int) : super(context, attributeSet, intRes) {
initView(context)
}
private lateinit var view: ItemVideoViewBinding
var streamType: Int = MAIN_STREAM
var data: Device? = null
set(value) {
field = value
if (field == null) {
view.textDeviceName.isVisible = false
isPlaying = false
isLoading = false
return
}
this.tag = field?.deviceName
view.textDeviceName.text = field?.deviceName
view.textDeviceName.isVisible = true
val rtspUrl = this.data?.getStreamPath(streamType) ?: return // 如果 null 就不指派給 Gstreamer 了
gstLibrary.setTag(this.data!!.deviceName)
gstLibrary.setRtspUrl(rtspUrl)
Log.d("${TAG}_$tag", "Set device to: $field, rtspUrl = $rtspUrl")
}
var isReady: Boolean = false
var isLoading: Boolean = false
set(value) {
view.pbLoading.isVisible = if (retryCount in 1..5) {
true
} else {
value
}
field = value
}
var isPlaying: Boolean = false
set(value) {
view.imgPause.isVisible = !value
field = value
}
private var retryCount = 0
private var retryRunnable = object : Runnable {
override fun run() {
play()
//Log.e("${TAG}_$tag", "Retry count: $retryCount")
}
}
private val videoView: TextureView by lazy { view.videoView }
private lateinit var gstLibrary: GstLibrary
private val mHandler: MyHandler by lazy {
MyHandler(Looper.getMainLooper())
}
private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context)
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true
gstLibrary = GstLibrary(context)
gstLibrary.setTextureView(videoView)
gstLibrary.setOnStatusChangeListener(this)
//videoView.holder.addCallback(this)
//gstLibrary.setSurfaceHolder(videoView.holder)
// View 預設狀態
view.textDeviceName.isVisible = false
isPlaying = false
isLoading = false
view.btnRetry.setOnClickListener {
it.isVisible = false
resetRetryCount()
play()
}
}
fun setTextVisible(isVisible: Boolean) {
view.textDeviceName.isVisible = isVisible
}
fun play() {
if (data == null) return
if (this::gstLibrary.isInitialized) {
gstLibrary.play()
}
}
fun pause() {
if (data == null) return
isPlaying = false
if (this::gstLibrary.isInitialized) {
gstLibrary.pause()
}
}
fun destroySurface() {
if (this::gstLibrary.isInitialized) {
gstLibrary.releaseSurface()
}
}
fun destroy() {
if (this::gstLibrary.isInitialized) {
gstLibrary.close()
}
}
fun resetRetryCount() {
retryCount = 0
}
fun stopRetryCount() {
retryCount = RETRY_OFF
mHandler.removeCallbacks(retryRunnable)
}
override fun onStatus(gstStatus: GstStatus?) { // onStatus 不是在主執行緒,因此透過 Handler 發訊息到主執行緒去執行
when (gstStatus) {
GstStatus.READY -> isReady = true
GstStatus.PLAYING -> mHandler.sendMessage(Message().apply { what = MSG_PLAY })
GstStatus.PAUSE -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
//GstStatus.ERROR_WHEN_OPENING -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
GstStatus.BUFFERING -> mHandler.sendMessage(Message().apply { what = MSG_BUFFERING })
GstStatus.ERROR_WHEN_OPENING -> mHandler.sendMessage(Message().apply { what = MSG_ERROR })
else -> {}
}
//Log.e("${TAG}_$tag", "onStatus: $gstStatus")
}
override fun onMessage(message: String?) {
//Log.e("${TAG}_$tag", "onMessage: $message")
}
inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
when(msg.what) {
MSG_PAUSE -> {
isPlaying = false
isLoading = false
}
MSG_PLAY -> {
isLoading = false
isPlaying = true
resetRetryCount()
}
MSG_BUFFERING -> {
isLoading = true
isPlaying = true
}
MSG_ERROR -> {
if (retryCount != RETRY_OFF && retryCount in 0 until 5) {
mHandler.post(retryRunnable)
retryCount++
isLoading = true
view.btnRetry.isVisible = false
} else {
stopRetryCount()
view.btnRetry.isVisible = true
isLoading = false
Log.e("${TAG}_$tag", "Retry count = 5, stopped retry...")
}
}
}
}
}
companion object {
private val TAG = VideoView::class.java.simpleName
const val MAIN_STREAM = 1
const val SUB_STREAM = 2
private const val MSG_PAUSE = 1
private const val MSG_PLAY = 2
private const val MSG_BUFFERING = 3
private const val MSG_ERROR = 4
private const val RETRY_OFF = 999
}
}
@@ -1,4 +1,4 @@
package com.ray650128.gstreamer_demo_app package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@@ -41,21 +41,41 @@ class VideoViewAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
fun stop(index: Int) { fun pause() {
stop(fragments[index]) for (i in fragments.indices) {
pause(i)
}
} }
private fun stop(fragment: SplitViewFragment) { fun pause(index: Int) {
pause(fragments[index])
}
private fun pause(fragment: SplitViewFragment) {
fragment.stopAll() fragment.stopAll()
} }
/*fun play(index: Int) { fun play(index: Int) {
play(fragments[index]) play(fragments[index])
} }
private fun play(fragment: SplitViewFragment) { private fun play(fragment: SplitViewFragment) {
fragment.playAll() fragment.playAll()
}*/ }
fun destroy() {
for (i in fragments.indices) {
destroy(i)
}
}
fun destroy(index: Int) {
destroy(fragments[index])
}
private fun destroy(fragment: SplitViewFragment) {
fragment.destroyAll()
}
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return fragments[position].hashCode().toLong() return fragments[position].hashCode().toLong()
@@ -0,0 +1,39 @@
package com.ray650128.gstreamer_demo_app.ui.monitoringScreen
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.WindowManager
import com.ray650128.gstreamer_demo_app.R
import com.ray650128.gstreamer_demo_app.databinding.ActivityMonitoringBinding
class MonitoringActivity : AppCompatActivity() {
private lateinit var binding: ActivityMonitoringBinding
override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState)
binding = ActivityMonitoringBinding.inflate(layoutInflater)
setContentView(binding.root)
initToolbar()
}
private fun initToolbar() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_back_arrow)
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
companion object {
const val BUNDLE_DEVICE_ID = "BUNDLE_DEVICE_ID"
const val BUNDLE_CHANNEL_ID = "BUNDLE_CHANNEL_ID"
const val BUNDLE_DEVICE = "BUNDLE_DEVICE"
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="#9b9b9b" />
<item android:state_enabled="true" android:color="#FF000000" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Some files were not shown because too many files have changed in this diff Show More