Compare commits

..

4 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
10 changed files with 194 additions and 105 deletions
+9 -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,29 +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.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.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.9.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'
@@ -56,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")
} }
@@ -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
}
}
}
@@ -94,8 +94,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun reloadVideoViews(list: List<List<Device>>?) = MainScope().launch { private fun reloadVideoViews(list: List<List<Device>>?) {
val oldListCount = videoPageList.size
for (videoPage in videoPageList) { for (videoPage in videoPageList) {
supportFragmentManager.commit { supportFragmentManager.commit {
remove(videoPage) remove(videoPage)
@@ -104,9 +103,6 @@ class MainActivity : AppCompatActivity() {
binding.viewPager.removeAllViews() binding.viewPager.removeAllViews()
videoPageList.clear() videoPageList.clear()
if (oldListCount > 0) {
delay(500L)
}
// 如果群組內沒有裝置,則顯示底圖 // 如果群組內沒有裝置,則顯示底圖
if (list.isNullOrEmpty()) { if (list.isNullOrEmpty()) {
binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing) binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing)
@@ -6,10 +6,12 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.gridlayout.widget.GridLayout import androidx.gridlayout.widget.GridLayout
import com.ray650128.gstreamer_demo_app.Constants 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.databinding.FragmentSplitViewBinding
import com.ray650128.gstreamer_demo_app.dp import com.ray650128.gstreamer_demo_app.dp
import com.ray650128.gstreamer_demo_app.model.Device import com.ray650128.gstreamer_demo_app.model.Device
@@ -54,18 +56,6 @@ class SplitViewFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initView() initView()
/*viewModel.activePage.observe(viewLifecycleOwner) {
if (it == null) return@observe
if (it == this.mPageNum) {
MainScope().launch {
//delay(1000)
//playAll()
}
} else {
//stopAll()
}
}*/
} }
override fun onPause() { override fun onPause() {
@@ -88,6 +78,7 @@ class SplitViewFragment : Fragment() {
} }
private fun initView() { private fun initView() {
val displayUtil = DisplayUtils(requireContext())
// 生成 VideoView 分割畫面 // 生成 VideoView 分割畫面
binding.apply { binding.apply {
val maxRow = sqrt(splitMode.toFloat()).toInt() val maxRow = sqrt(splitMode.toFloat()).toInt()
@@ -97,6 +88,19 @@ class SplitViewFragment : Fragment() {
baseView.rowCount = maxRow baseView.rowCount = maxRow
baseView.columnCount = maxCol 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 (col in 0 until maxCol) {
for (row in 0 until maxRow) { for (row in 0 until maxRow) {
val videoView = VideoView(requireContext()) val videoView = VideoView(requireContext())
@@ -106,6 +110,9 @@ class SplitViewFragment : Fragment() {
marginEnd = 0.dp marginEnd = 0.dp
marginStart = 0.dp marginStart = 0.dp
width = cellWidth
height = cellHeight
// 調整間距 // 調整間距
when (splitMode) { when (splitMode) {
Constants.SPLIT_MODE_FOUR -> { Constants.SPLIT_MODE_FOUR -> {
@@ -152,37 +159,16 @@ class SplitViewFragment : Fragment() {
videoViews.add(videoView) videoViews.add(videoView)
} }
} }
baseView.post {
// 根據分割數量決定子項目的寬度
val cellWidth: Int
val cellHeight: Int
when (splitMode) {
Constants.SPLIT_MODE_SINGLE -> {
cellWidth = (baseView.width / maxRow)
cellHeight = (baseView.height / maxCol)
}
else -> {
cellWidth = (baseView.width / maxRow) - maxRow.dp
cellHeight = (baseView.height / maxCol) - maxCol.dp
}
}
videoViews.forEach { videoView ->
videoView.layoutParams?.width = cellWidth
videoView.layoutParams?.height = cellHeight
}
}
} }
if (isClickable) { if (isClickable) {
for (position in videoViews.indices) { videoViews.forEach { videoView ->
videoViews[position].setOnClickListener { videoView.setOnClickListener {
if (position >= data.size) return@setOnClickListener if (!videoView.isPlaying) {
if (!videoViews[position].isPlaying) {
return@setOnClickListener return@setOnClickListener
} }
stopAll() stopAll()
val item = data[position] val item = videoView.data
val bundle = Bundle().apply { val bundle = Bundle().apply {
//putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id) //putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
//putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId) //putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
@@ -202,38 +188,35 @@ class SplitViewFragment : Fragment() {
private fun setAllUrl() { private fun setAllUrl() {
for (index in data.indices) { for (index in data.indices) {
videoViews[index].setData(data[index]) videoViews[index].data = data[index]
videoViews[index].setTextVisible( videoViews[index].setTextVisible(
(splitMode != Constants.SPLIT_MODE_NINE && splitMode != Constants.SPLIT_MODE_SIXTEEN) (splitMode != Constants.SPLIT_MODE_NINE && splitMode != Constants.SPLIT_MODE_SIXTEEN)
) )
} }
} }
fun playAll() = MainScope().launch(Dispatchers.Main) { fun playAll() {
if (videoViews.isEmpty()) return@launch videoViews.forEach { videoView ->
//delay(splitMode * Constants.CONF_DELAY_BASE_MILLIS) if (videoView.isReady) {
for (index in data.indices) { videoView.resetRetryCount()
if (!videoViews[index].isReady) continue videoView.play()
videoViews[index].resetRetryCount() }
videoViews[index].play()
delay(300)
} }
}.start() }
fun stopAll() { fun stopAll() {
if (videoViews.isEmpty()) return videoViews.forEach { videoView ->
for (index in data.indices) { videoView.stopRetryCount()
videoViews[index].stopRetryCount() if (videoView.isPlaying || !videoView.isLoading) {
if (!videoViews[index].isPlaying || videoViews[index].isLoading) continue videoView.pause()
videoViews[index].pause() }
} }
} }
fun destroyAll() { fun destroyAll() {
if (videoViews.isEmpty()) return videoViews.forEach { videoView ->
for (index in data.indices) { //videoView.destroy()
//videoViews[index].destroy() videoView.destroySurface()
videoViews[index].destroySurface()
} }
} }
@@ -35,7 +35,25 @@ class VideoView : ConstraintLayout, GstCallback {
private lateinit var view: ItemVideoViewBinding private lateinit var view: ItemVideoViewBinding
private var data: Device? = null 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 isReady: Boolean = false
@@ -76,12 +94,13 @@ class VideoView : ConstraintLayout, GstCallback {
view = ItemVideoViewBinding.inflate(layoutInflater, this, true) view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true view.baseView.clipToOutline = true
//videoView.holder.addCallback(this)
gstLibrary = GstLibrary(context) gstLibrary = GstLibrary(context)
//gstLibrary.setSurfaceHolder(videoView.holder)
gstLibrary.setTextureView(videoView) gstLibrary.setTextureView(videoView)
gstLibrary.setOnStatusChangeListener(this) gstLibrary.setOnStatusChangeListener(this)
//videoView.holder.addCallback(this)
//gstLibrary.setSurfaceHolder(videoView.holder)
// View 預設狀態 // View 預設狀態
view.textDeviceName.isVisible = false view.textDeviceName.isVisible = false
isPlaying = false isPlaying = false
@@ -94,23 +113,6 @@ class VideoView : ConstraintLayout, GstCallback {
} }
} }
fun setData(device: Device?, streamType: Int = SUB_STREAM) {
if (device == null) {
view.textDeviceName.isVisible = false
isPlaying = false
isLoading = false
return
}
this.data = device
this.tag = device.deviceName
view.textDeviceName.text = device.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: $device, rtspUrl = $rtspUrl")
}
fun setTextVisible(isVisible: Boolean) { fun setTextVisible(isVisible: Boolean) {
view.textDeviceName.isVisible = isVisible view.textDeviceName.isVisible = isVisible
} }
+2 -2
View File
@@ -7,8 +7,8 @@ buildscript {
} }
plugins { plugins {
id 'com.android.application' version '7.4.2' apply false id 'com.android.application' version '8.0.1' apply false
id 'com.android.library' version '7.4.2' apply false id 'com.android.library' version '8.0.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id 'org.jetbrains.kotlin.plugin.parcelize' version '1.7.0' apply false id 'org.jetbrains.kotlin.plugin.parcelize' version '1.7.0' apply false
} }
+3
View File
@@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
+1 -1
View File
@@ -1,6 +1,6 @@
#Thu Mar 24 14:41:01 CST 2022 #Thu Mar 24 14:41:01 CST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+14 -2
View File
@@ -2,9 +2,10 @@ apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.android'
android { android {
namespace 'com.hisharp.gstreamer_player'
ndkVersion "21.3.6528147" ndkVersion "21.3.6528147"
compileSdkVersion 33 compileSdk 33
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
@@ -58,6 +59,17 @@ android {
path 'jni/Android.mk' path 'jni/Android.mk'
} }
} }
buildFeatures {
renderScript true
aidl true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
} }
afterEvaluate { afterEvaluate {
@@ -69,7 +81,7 @@ afterEvaluate {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.10.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
//implementation 'androidx.appcompat:appcompat:1.6.0' //implementation 'androidx.appcompat:appcompat:1.6.0'
} }
@@ -142,37 +142,36 @@ class GstLibrary(context: Context) : Closeable, SurfaceHolder.Callback, TextureV
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d("$TAG+$tag", String.format("Surface changed, format: %d, width: %d, height: %d", format, width, height)) //Log.d("$TAG+$tag", String.format("Surface changed, format: %d, width: %d, height: %d", format, width, height))
/*setSurfaceHolder(holder);*/ /*setSurfaceHolder(holder);*/
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d("$TAG+$tag", "Surface destroyed") //Log.d("$TAG+$tag", "Surface destroyed")
this.isInit = false this.isInit = false
this.surface = null this.surface = null
//pause() releaseSurface()
//releaseSurface()
} }
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
this.surface = Surface(surface) this.surface = Surface(surface)
this.surface?.let { nativeSurfaceInit(it) } this.surface?.let { nativeSurfaceInit(it) }
Log.d("$TAG+$tag", "Surface onSurfaceTextureAvailable: $surface") //Log.d("$TAG+$tag", "Surface onSurfaceTextureAvailable: $surface")
} }
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.d("$TAG+$tag", "Surface onSurfaceTextureSizeChanged: $surface") //Log.d("$TAG+$tag", "Surface onSurfaceTextureSizeChanged: $surface")
} }
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.d("$TAG+$tag", "Surface onSurfaceTextureDestroyed: $surface") //Log.d("$TAG+$tag", "Surface onSurfaceTextureDestroyed: $surface")
this.isInit = false this.isInit = false
this.surface = null this.surface = null
return isInit return isInit
} }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
Log.d("$TAG+$tag", "Surface onSurfaceTextureUpdated: $surface") //Log.d("$TAG+$tag", "Surface onSurfaceTextureUpdated: $surface")
} }
companion object { companion object {