調整架構

This commit is contained in:
Raymond Yang 2023-01-19 12:12:41 +08:00
parent 8288bc57c5
commit e48ebcbb02
20 changed files with 655 additions and 389 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="16" />
</component>
</project>

View File

@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="gradleJvm" value="semeru-16" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -16,5 +16,5 @@
</option>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_16_PREVIEW" project-jdk-name="11" project-jdk-type="JavaSDK" />
</project>

View File

@ -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()
}

View File

@ -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<String> = ArrayList()
private lateinit var binding: FragmentGridVideoBinding
private var gstPlayers: ArrayList<GstLibrary> = ArrayList()
private var videoViews: ArrayList<VideoView> = ArrayList()
private var surfaceHolders: ArrayList<SurfaceHolder> = 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<String>, 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
}
}
}

View File

@ -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<List<Device>>? = 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<List<Device>>?) = 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)
}
}
}

View File

@ -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<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
}
}

View File

@ -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<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
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<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
}
}
}

View File

@ -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
}
}

View File

@ -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<Fragment> = arrayListOf()
private var fragments: MutableList<SplitViewFragment> = 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()
}

View File

@ -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

View File

@ -19,5 +19,40 @@
tools:layout_conversion_absoluteHeight="0dp"
tools:layout_conversion_absoluteWidth="411dp" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="1"
app:layout_constraintEnd_toStartOf="@+id/button2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/viewPager" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="4"
app:layout_constraintEnd_toStartOf="@+id/button3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/button"
app:layout_constraintTop_toBottomOf="@+id/viewPager" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/button2"
app:layout_constraintTop_toBottomOf="@+id/viewPager" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.gridlayout.widget.GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/baseView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:columnCount="3"
app:rowCount="3"
app:useDefaultMargins="false"
tools:context=".SplitViewFragment" />

View File

@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/baseView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_video_view"
android:outlineProvider="background">
<com.hisharp.gstreamer_player.GStreamerSurfaceView
<SurfaceView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -34,16 +33,16 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="4dp"
android:drawableLeft="@drawable/ic_ip_cam_name"
android:drawablePadding="6dp"
android:ellipsize="end"
android:maxLines="1"
android:text="TextView"
android:text=""
android:textColor="@android:color/white"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/videoView"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:drawableStartCompat="@drawable/ic_ip_cam_name" />
<ProgressBar
android:id="@+id/pbLoading"
@ -51,7 +50,7 @@
android:layout_width="64dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/videoView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -1,25 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
mavenCentral()
}
}
allprojects {
repositories {
jcenter()
google()
}
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
id 'org.jetbrains.kotlin.plugin.parcelize' version '1.7.0' apply false
}
task clean(type: Delete) {

View File

@ -1,2 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

View File

@ -1,6 +1,6 @@
#Sat Apr 21 19:58:19 WEST 2018
#Thu Mar 24 14:41:01 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.library'
android {
ndkVersion "21.3.6528147"
compileSdkVersion 33
defaultConfig {
@ -21,7 +23,10 @@ android {
if (gstRoot == null)
throw new GradleException('GSTREAMER_ROOT_ANDROID must be set, or "gstAndroidRoot" must be defined in your gradle.properties in the top level directory of the unpacked universal GStreamer Android binaries')
arguments "NDK_APPLICATION_MK=jni/Application.mk", "GSTREAMER_JAVA_SRC_DIR=src", "GSTREAMER_ROOT_ANDROID=$gstRoot", "GSTREAMER_ASSETS_DIR=src/assets"
arguments "NDK_APPLICATION_MK=jni/Application.mk",
"GSTREAMER_JAVA_SRC_DIR=src",
"GSTREAMER_ROOT_ANDROID=$gstRoot",
"GSTREAMER_ASSETS_DIR=src/assets"
targets "gst_player"
@ -65,6 +70,6 @@ afterEvaluate {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
implementation 'androidx.appcompat:appcompat:1.0.0'
testImplementation 'junit:junit:4.13.2'
//implementation 'androidx.appcompat:appcompat:1.6.0'
}

View File

@ -19,11 +19,10 @@ public class GstLibrary implements Closeable {
private GstCallback gstCallback;
private final String rtspUrl;
private String rtspUrl;
public GstLibrary(Context context, String rtspUrl) {
public GstLibrary(Context context) {
this.mAppContext = context.getApplicationContext();
this.rtspUrl = rtspUrl;
// Initialize GStreamer and warn if it fails
try {
@ -56,6 +55,10 @@ public class GstLibrary implements Closeable {
nativePause();
}
public void setRtspUrl(String rtspUrl) {
this.rtspUrl = rtspUrl;
}
public void releaseSurface() {
nativeSurfaceFinalize();
}

View File

@ -1,2 +1,19 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
maven {url "https://jitpack.io"}
}
}
include ':gstreamer_player'
include ':app'