diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8..659bf43 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index f3e97d6..823f760 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,7 +7,7 @@
-
+
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 5b37e5f..ab6244d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,9 +1,12 @@
plugins {
id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.plugin.parcelize'
+ id 'org.jetbrains.kotlin.kapt'
}
-apply plugin: 'kotlin-android'
android {
+ ndkVersion "21.3.6528147"
compileSdk 33
defaultConfig {
@@ -26,23 +29,32 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
-
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ lint {
+ abortOnError false
+ checkReleaseBuilds false
+ }
viewBinding.enabled = true
+ namespace 'com.ray650128.gstreamer_demo_app'
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.0.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation project(path: ':gstreamer_player')
+ implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'com.google.android.material:material:1.7.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
- implementation 'androidx.viewpager2:viewpager2:1.0.0'
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
- implementation "androidx.core:core-ktx:+"
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+
+ implementation project(path: ':gstreamer_player')
+
+ // Android Jetpack lib
+ implementation("androidx.fragment:fragment-ktx:1.5.5")
+ implementation("androidx.activity:activity-ktx:1.6.1")
}
-repositories {
- mavenCentral()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/GridVideoFragment.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/GridVideoFragment.kt
deleted file mode 100644
index 87ed4a1..0000000
--- a/app/src/main/java/com/ray650128/gstreamer_demo_app/GridVideoFragment.kt
+++ /dev/null
@@ -1,244 +0,0 @@
-package com.ray650128.gstreamer_demo_app
-
-import android.os.Bundle
-import android.util.DisplayMetrics
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.SurfaceHolder
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.gridlayout.widget.GridLayout
-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.FragmentGridVideoBinding
-import kotlin.math.sqrt
-
-
-class GridVideoFragment : Fragment(), GstCallback, SurfaceHolder.Callback {
-
- private var mPageNum: Int = 0
- private var splitMode = 0
- private var streamType = STREAM_SUB
- private var isClickable = true
-
- private var data: ArrayList = ArrayList()
-
- private lateinit var binding: FragmentGridVideoBinding
-
- private var gstPlayers: ArrayList = ArrayList()
-
- private var videoViews: ArrayList = ArrayList()
-
- private var surfaceHolders: ArrayList = 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().getStringArrayList(ARG_STREAM_URLS) ?: arrayListOf()
- }
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- binding = FragmentGridVideoBinding.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 onStop() {
- super.onStop()
- Log.d("${TAG}_$mPageNum", "onStop()")
- }
-
- override fun onStart() {
- super.onStart()
- Log.d("${TAG}_$mPageNum", "onStart()")
- }
-
- override fun onDestroyView() {
- for (i in gstPlayers.indices) {
- gstPlayers[i].close()
- }
- Log.d("${TAG}_$mPageNum", "onDestroyView()")
- super.onDestroyView()
- }
-
- override fun onDestroy() {
- Log.d("${TAG}_$mPageNum", "onDestroy()")
- super.onDestroy()
- }
-
- private fun initView() {
- val maxRow = sqrt(splitMode.toFloat()).toInt()
- val maxCol = sqrt(splitMode.toFloat()).toInt()
-
- val displayMetrics = DisplayMetrics()
- requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
- val screenWidth = displayMetrics.widthPixels
-
- val cellWidth = when (splitMode) {
- 1 -> (screenWidth / maxRow)
- else -> (screenWidth / maxRow) - maxRow.dp
- }
-
- val cellHeight = (cellWidth * (0.62)).toInt()
- Log.e(TAG, "cellWidth: $cellWidth, cellHeight: $cellHeight")
-
- // 生成 VideoView 分割畫面
- binding.apply {
- baseView.rowCount = maxRow
- baseView.columnCount = maxCol
-
- for (col in 0 until maxCol) {
- for (row in 0 until maxRow) {
- val videoView = VideoView(requireContext())
- val layoutParams = GridLayout.LayoutParams().apply {
- height = cellHeight
- width = cellWidth
-
- topMargin = 0.dp
- bottomMargin = 0.dp
- marginEnd = 0.dp
- marginStart = 0.dp
-
- // 調整間距
- when (splitMode) {
- 4 -> {
- when (col) {
- 0 -> bottomMargin = 2.dp
- 1 -> topMargin = 2.dp
- }
- when (row) {
- 0 -> marginEnd = 2.dp
- 1 -> marginStart = 2.dp
- }
- }
- 9 -> {
- if (col == 1) {
- topMargin = 4.dp
- bottomMargin = 4.dp
- }
- if (row == 1) {
- marginEnd = 4.dp
- marginStart = 4.dp
- }
- }
- }
- }
-
- baseView.addView(videoView, layoutParams)
- videoViews.add(videoView)
-
- videoView.videoView.holder.addCallback(this@GridVideoFragment)
- surfaceHolders.add(videoView.videoView.holder)
- }
- }
- }
-
- for (index in videoViews.indices) {
- gstPlayers.add(GstLibrary(requireContext(), data[index]))
- gstPlayers[index].setOnStatusChangeListener(this)
- }
- }
-
- private fun playAll() {
- for (index in data.indices) {
- gstPlayers[index].play()
- }
- }
-
- private fun stopAll() {
- for (index in data.indices) {
- gstPlayers[index].stop()
- gstPlayers[index].close()
- }
- }
-
- override fun onStatus(gstInstance: GstLibrary, gstStatus: GstStatus?) {
- val index = gstPlayers.indexOf(gstInstance)
- when (gstStatus) {
- GstStatus.PAUSE -> videoViews[index].isPlaying = false
- GstStatus.PLAYING -> videoViews[index].isPlaying = true
- GstStatus.ERROR_WHEN_OPENING -> videoViews[index].isPlaying = false
- GstStatus.ERROR -> videoViews[index].isPlaying = false
- else -> {}
- }
- Log.d("${TAG}_$mPageNum", "GstPlayer #$index status: $gstStatus")
- }
-
- override fun onMessage(gstInstance: GstLibrary, message: String?) {
- val index = gstPlayers.indexOf(gstInstance)
- Log.d("${TAG}_$mPageNum", "GstPlayer #$index: $message")
- }
-
- override fun surfaceCreated(holder: SurfaceHolder) {
- val index = surfaceHolders.indexOf(holder)
- gstPlayers[index].setSurfaceHolder(holder)
- Log.d("GStreamer", "Surface created: " + holder.surface)
- }
-
- override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
- //val index = surfaceHolders.indexOf(holder)
- Log.d("GStreamer", "Surface changed to format $format width $width height $height")
- }
-
- override fun surfaceDestroyed(holder: SurfaceHolder) {
- val index = surfaceHolders.indexOf(holder)
- gstPlayers[index].releaseSurface()
- Log.d("GStreamer", "Surface destroyed")
- }
-
- companion object {
- private val TAG = GridVideoFragment::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_STREAM_TYPE = "stream_type"
- private const val ARG_STREAM_URLS = "stream_urls"
-
- const val STREAM_MAIN = 1
- const val STREAM_SUB = 2
-
- /**
- * 透過傳入的參數,生成新的 Fragment 實例
- *
- * @param pageNumber 該 Fragment 頁碼
- * @param splitMode 畫面分割模式(9/4/1分割)
- * @return 透過傳入的參數,生成新的 Fragment 實例
- */
- fun newInstance(pageNumber: Int, splitMode: Int, streamUrls: ArrayList, isClickable: Boolean = true, streamType: Int = STREAM_SUB): GridVideoFragment {
- val fragment = GridVideoFragment()
- val args = Bundle()
- args.putInt(ARG_PAGE_NUM, pageNumber)
- args.putInt(ARG_SPLIT_MODE, splitMode)
- args.putBoolean(ARG_CLICKABLE, isClickable)
- args.putInt(ARG_STREAM_TYPE, streamType)
- args.putStringArrayList(ARG_STREAM_URLS, streamUrls)
- fragment.arguments = args
- return fragment
- }
- }
-}
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/MainActivity.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/MainActivity.kt
index a70960c..84ab0e3 100644
--- a/app/src/main/java/com/ray650128/gstreamer_demo_app/MainActivity.kt
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/MainActivity.kt
@@ -1,113 +1,107 @@
package com.ray650128.gstreamer_demo_app
-import android.annotation.SuppressLint
+import android.content.Context
import android.graphics.Color
-import androidx.appcompat.app.AppCompatActivity
-import com.hisharp.gstreamer_player.GstCallback
-import com.ray650128.gstreamer_demo_app.MainActivity
-import com.hisharp.gstreamer_player.GstLibrary
-import com.hisharp.gstreamer_player.GStreamerSurfaceView
import android.os.Bundle
import android.util.Log
-import android.view.View
import android.view.WindowManager
-import com.hisharp.gstreamer_player.GstStatus
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.viewpager2.widget.ViewPager2
import com.ray650128.gstreamer_demo_app.databinding.ActivityMainBinding
-import java.util.ArrayList
-import java.util.function.Consumer
+import com.ray650128.gstreamer_demo_app.model.Device
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import org.freedesktop.gstreamer.GStreamer
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
- private lateinit var splitVideoViewAdapter: ViewPager2Adapter
+ private val viewModel: MainViewModel by viewModels()
- @SuppressLint("AuthLeak")
- private val defaultMediaUris = arrayListOf(
- arrayListOf(
- "rtsp://admin:admin@192.168.0.77:554/media/video2",
- "rtsp://admin:1q2w3e4r!@192.168.0.79:554/media/video2",
- "rtsp://admin:1q2w3e4r~@211.23.78.226:8588/media/video2",
- "rtsp://admin:admin@211.23.78.226:8574/v02",
- "rtsp://admin:admin@211.23.78.226:8575/v02",
- "rtsp://admin:admin@192.168.0.77:554/media/video2",
- "rtsp://admin:1q2w3e4r!@192.168.0.79:554/media/video2",
- "rtsp://admin:1q2w3e4r~@211.23.78.226:8588/media/video2",
- "rtsp://admin:admin@211.23.78.226:8574/v02",
- ),
- arrayListOf(
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- ),
- /*arrayListOf(
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c1/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c2/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c3/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c4/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c5/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c6/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c7/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c8/s1/live",
- "rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c9/s1/live"
- ),*/
- arrayListOf(
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- "",
- )
- )
+ private val mContext: Context by lazy { this }
- /*@SuppressLint("AuthLeak")
- private val defaultMediaUris = arrayListOf(
- arrayListOf(
- "rtsp://admin:admin@211.23.78.226:8574/v02"
- ),
- arrayListOf(
- "rtsp://admin:admin@211.23.78.226:8575/v02"
- ),
- arrayListOf(
- "rtsp://admin:admin@192.168.0.77:554/media/video2"
- ),
- arrayListOf(
- "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4"
- )
- )*/
+ private var splitMode = 1
+
+ private lateinit var splitVideoViewAdapter: VideoViewAdapter
+
+ private var videos: List>? = null
+
+ private var currentPage = 0
override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState)
+
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
- splitVideoViewAdapter = ViewPager2Adapter(supportFragmentManager, lifecycle)
+ initContentView()
- binding.viewPager.apply {
- adapter = splitVideoViewAdapter
- offscreenPageLimit = 1
- }
-
- reloadVideoViews()
+ initObservers()
}
- private fun reloadVideoViews() {
- splitVideoViewAdapter.clear()
+ private fun initContentView() = binding.apply {
+ //region Content area
+ splitVideoViewAdapter = VideoViewAdapter(supportFragmentManager, lifecycle)
- for (index in defaultMediaUris.indices) {
- val gridFragment = GridVideoFragment.newInstance(index, defaultMediaUris[index].size, defaultMediaUris[index])
- splitVideoViewAdapter.add(index, gridFragment)
+ viewPager.apply {
+ adapter = splitVideoViewAdapter
+ offscreenPageLimit = 1
+ registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+ currentPage = position
+ }
+ })
+ }
+
+ button.setOnClickListener {
+ viewModel.setSplitMode(MainViewModel.PAGE_MODE_ONE)
+ Log.e(TAG, "+++ split style: 1")
+ }
+
+ button2.setOnClickListener {
+ viewModel.setSplitMode(MainViewModel.PAGE_MODE_FOUR)
+ Log.e(TAG, "+++ split style: 4")
+ }
+
+ button3.setOnClickListener {
+ viewModel.setSplitMode(MainViewModel.PAGE_MODE_NINE)
+ Log.e(TAG, "+++ split style: 9")
+ }
+ //endregion
+ }
+
+ private fun initObservers() {
+ viewModel.splitMode.observe(this) {
+ splitMode = it
+ Log.e(TAG, "Split count: $it")
+ }
+
+ viewModel.cameraList.observe(this) { list ->
+ this.videos = list
+ reloadVideoViews(this.videos)
+ }
+ }
+
+ private fun reloadVideoViews(list: List>?) = MainScope().launch {
+ splitVideoViewAdapter.clear()
+ // 如果群組內沒有裝置,則顯示底圖
+ if (list.isNullOrEmpty()) {
+ binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing)
+ } else {
+ binding.viewPager.setBackgroundColor(Color.TRANSPARENT)
+ for (i in list.indices) {
+ val splitFragment = SplitViewFragment.newInstance(
+ pageNumber = i,
+ splitMode = splitMode,
+ pageData = ArrayList(list[i])
+ )
+ splitVideoViewAdapter.add(i, splitFragment)
+ }
}
}
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/MainViewModel.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/MainViewModel.kt
new file mode 100644
index 0000000..7e269ec
--- /dev/null
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/MainViewModel.kt
@@ -0,0 +1,97 @@
+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 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 by lazy { MutableLiveData() }
+
+ val cameraList: MediatorLiveData>> by lazy {
+ MediatorLiveData>>().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?): List>? {
+ if (dbData.isNullOrEmpty()) return null
+ val tmpData = ArrayList>()
+ for (index in uriList.indices step (splitModeInt)) {
+ if (index == uriList.size) break
+ val tmpSubData = ArrayList()
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/SplitViewFragment.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/SplitViewFragment.kt
new file mode 100644
index 0000000..ac44b98
--- /dev/null
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/SplitViewFragment.kt
@@ -0,0 +1,210 @@
+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 = ArrayList()
+
+ private var videoViews: ArrayList = 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
+ videoViews[position].play()
+ /*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() {
+ for (index in data.indices) {
+ try {
+ videoViews[index].stop()
+ } catch (e: IllegalStateException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ 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,
+ 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
+ }
+ }
+}
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoView.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoView.kt
index 1d61683..1ea8471 100644
--- a/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoView.kt
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoView.kt
@@ -4,12 +4,18 @@ import android.content.Context
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.GStreamerSurfaceView
+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 {
+class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
constructor(context: Context) : super(context) {
initView(context)
@@ -25,7 +31,13 @@ class VideoView : ConstraintLayout {
private lateinit var view: ItemVideoViewBinding
- //private var data: Device? = null
+ private var data: Device? = null
+
+ var isLoading: Boolean = false
+ set(value) {
+ view.pbLoading.isVisible = value
+ field = value
+ }
var isPlaying: Boolean = false
set(value) {
@@ -33,26 +45,103 @@ class VideoView : ConstraintLayout {
field = value
}
- val videoView: GStreamerSurfaceView by lazy { view.videoView }
+ private val videoView: SurfaceView by lazy { view.videoView }
+
+ private lateinit var gstLibrary: GstLibrary
+
+ private var streamType = MAIN_STREAM
+
+ private var retryCount = 0
private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context)
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true
+
+ videoView.holder.addCallback(this)
}
- /*fun setData(device: Device?) {
+ fun setData(device: Device?) {
if (device == null) {
view.textDeviceName.isVisible = false
return
}
this.data = device
- view.textDeviceName.text = if (device.channelId == -1) device.deviceName else device.channelName
+ this.tag = device.name
+ view.textDeviceName.text = device.name
view.textDeviceName.isVisible = true
Log.d(TAG, "Set device to: $device")
- }*/
+ }
+
+ fun setTextVisible(isVisible: Boolean) {
+ view.textDeviceName.isVisible = isVisible
+ }
+
+ fun setViewSize(width: Int, height: Int) {
+ layoutParams?.width = width
+ layoutParams?.height = height
+ }
+
+ fun play() {
+ videoView.isVisible = true
+ gstLibrary = GstLibrary(context)
+ gstLibrary.setRtspUrl(this.data?.rtspUrl)
+ gstLibrary.setOnStatusChangeListener(this)
+ retryCount = 0
+ }
+
+ fun stop() {
+ videoView.isVisible = false
+ isPlaying = false
+ if (this::gstLibrary.isInitialized) {
+ gstLibrary.stop()
+ gstLibrary.setOnStatusChangeListener(null)
+ }
+ retryCount = 999
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ Log.d(TAG, "Surface created: " + holder.surface)
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+ Log.d(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, "Surface destroyed")
+ if (this::gstLibrary.isInitialized) {
+ //gstLibrary.releaseSurface()
+ }
+ }
+
+ override fun onStatus(gstInstance: GstLibrary?, gstStatus: GstStatus?) {
+ when (gstStatus) {
+ GstStatus.PLAYING -> {
+ isLoading = false
+ isPlaying = true
+ }
+ GstStatus.PAUSE -> {
+ isPlaying = false
+ isLoading = false
+ }
+ else -> {}
+ }
+ Log.e(TAG, "onStatus: $gstStatus")
+ }
+
+ override fun onMessage(gstInstance: GstLibrary?, message: String?) {
+ gstLibrary.play()
+ Log.e(TAG, "onMessage: $message")
+ }
companion object {
private val TAG = VideoView::class.java.simpleName
+
+ const val MAIN_STREAM = 1
+ const val SUB_STREAM = 2
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/ViewPager2Adapter.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoViewAdapter.kt
similarity index 64%
rename from app/src/main/java/com/ray650128/gstreamer_demo_app/ViewPager2Adapter.kt
rename to app/src/main/java/com/ray650128/gstreamer_demo_app/VideoViewAdapter.kt
index 7c21c90..53741d3 100644
--- a/app/src/main/java/com/ray650128/gstreamer_demo_app/ViewPager2Adapter.kt
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/VideoViewAdapter.kt
@@ -1,31 +1,31 @@
package com.ray650128.gstreamer_demo_app
import android.annotation.SuppressLint
-import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
-class ViewPager2Adapter(
+class VideoViewAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {
- private var fragments: MutableList = arrayListOf()
+ private var fragments: MutableList = arrayListOf()
override fun getItemCount(): Int {
return fragments.size
}
- override fun createFragment(position: Int): Fragment {
+
+ override fun createFragment(position: Int): SplitViewFragment {
return fragments[position]
}
- fun add(index: Int, fragment: Fragment) {
+ fun add(index: Int, fragment: SplitViewFragment) {
fragments.add(index, fragment)
notifyItemChanged(index)
}
- fun refreshFragment(index: Int, fragment: Fragment) {
+ fun refreshFragment(index: Int, fragment: SplitViewFragment) {
fragments[index] = fragment
notifyItemChanged(index)
}
@@ -41,6 +41,22 @@ class ViewPager2Adapter(
notifyDataSetChanged()
}
+ fun stop(index: Int) {
+ stop(fragments[index])
+ }
+
+ private fun stop(fragment: SplitViewFragment) {
+ fragment.stopAll()
+ }
+
+ /*fun play(index: Int) {
+ play(fragments[index])
+ }
+
+ private fun play(fragment: SplitViewFragment) {
+ fragment.playAll()
+ }*/
+
override fun getItemId(position: Int): Long {
return fragments[position].hashCode().toLong()
}
diff --git a/app/src/main/java/com/ray650128/gstreamer_demo_app/model/Device.kt b/app/src/main/java/com/ray650128/gstreamer_demo_app/model/Device.kt
new file mode 100644
index 0000000..4cb4a70
--- /dev/null
+++ b/app/src/main/java/com/ray650128/gstreamer_demo_app/model/Device.kt
@@ -0,0 +1,11 @@
+package com.ray650128.gstreamer_demo_app.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class Device(
+ val name: String = "",
+ val rtspUrl: String = "",
+ var isPlaying: Boolean = false
+): Parcelable
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 2302290..90f1fab 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -19,5 +19,40 @@
tools:layout_conversion_absoluteHeight="0dp"
tools:layout_conversion_absoluteWidth="411dp" />
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_split_view.xml b/app/src/main/res/layout/fragment_split_view.xml
new file mode 100644
index 0000000..8b71ef6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_split_view.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_video_view.xml b/app/src/main/res/layout/item_video_view.xml
index 6484d04..a79da4e 100644
--- a/app/src/main/res/layout/item_video_view.xml
+++ b/app/src/main/res/layout/item_video_view.xml
@@ -1,14 +1,13 @@
-
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:drawableStartCompat="@drawable/ic_ip_cam_name" />