Compare commits
32 Commits
0a65a9615b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e3c958c632 | |||
| 48db07290c | |||
| 1b80ae3349 | |||
| ed8f2a7c0f | |||
| d8d351831c | |||
| 77df0eb8a5 | |||
| 0dca3ce9e8 | |||
| 363bfdbfbd | |||
| dc1da2449b | |||
| 53763bfa34 | |||
| 4cf5ae5094 | |||
| ced178513b | |||
| 88954a6599 | |||
| df105dd518 | |||
| 2f4e3659f9 | |||
| 2e8d8a6381 | |||
| 87f731168f | |||
| c7d0785cef | |||
| a37e79d841 | |||
| 4e09a449ca | |||
| 85218c443f | |||
| b4346f142b | |||
| 9fc8673715 | |||
| de152da749 | |||
| db673d3553 | |||
| 792ebc70c2 | |||
| 90344d14b0 | |||
| c3f8366475 | |||
| 15475923cb | |||
| a29f0e7bbf | |||
| ac15111ab3 | |||
| cdd2c3775d |
@@ -48,6 +48,7 @@ dependencies {
|
||||
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.core:core-ktx:+'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.ray650128.gstreamer_demo_app">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -10,14 +10,19 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Gstreamer">
|
||||
<activity
|
||||
android:name="com.ray650128.gstreamer_demo_app.MainActivity"
|
||||
android:exported="true">
|
||||
android:name="com.ray650128.gstreamer_demo_app.ui.mainScreen.MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="nosensor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.monitoringScreen.MonitoringActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="nosensor" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.ray650128.gstreamer_demo_app
|
||||
|
||||
object Constants {
|
||||
const val CONF_DELAY_BASE_MILLIS = 1000L
|
||||
}
|
||||
@@ -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, MainViewModel.PAGE_MODE_ONE)
|
||||
set(value) = sharedPreferences.edit().putInt(LAST_SPLIT_MODE, value).apply()
|
||||
}
|
||||
@@ -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
|
||||
data class Device(
|
||||
val name: String = "",
|
||||
val rtspUrl: String = "",
|
||||
var ip: 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
|
||||
): Parcelable
|
||||
|
||||
+54
-4
@@ -1,19 +1,20 @@
|
||||
package com.ray650128.gstreamer_demo_app
|
||||
package com.ray650128.gstreamer_demo_app.ui.mainScreen
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.model.Device
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.freedesktop.gstreamer.GStreamer
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -21,9 +22,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private val splitViewModel: SplitViewModel by viewModels()
|
||||
|
||||
private val mContext: Context by lazy { this }
|
||||
|
||||
private var splitMode = 1
|
||||
private var oldSplitMode = 1
|
||||
|
||||
private lateinit var splitVideoViewAdapter: VideoViewAdapter
|
||||
|
||||
@@ -43,32 +47,70 @@ class MainActivity : AppCompatActivity() {
|
||||
initObservers()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
super.onBackPressed()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
for (i in 0 until splitVideoViewAdapter.itemCount) {
|
||||
splitVideoViewAdapter.destroy(i)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun initContentView() = binding.apply {
|
||||
//region Content area
|
||||
splitVideoViewAdapter = VideoViewAdapter(supportFragmentManager, lifecycle)
|
||||
|
||||
viewPager.apply {
|
||||
adapter = splitVideoViewAdapter
|
||||
offscreenPageLimit = 1
|
||||
offscreenPageLimit = 100
|
||||
setPageTransformer(null)
|
||||
registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
|
||||
private var oldPage = 0
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
super.onPageScrollStateChanged(state)
|
||||
/*if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
|
||||
oldPage = currentPage
|
||||
splitVideoViewAdapter.stop(currentPage)
|
||||
}
|
||||
if (state == ViewPager2.SCROLL_STATE_IDLE && currentPage == oldPage) {
|
||||
splitVideoViewAdapter.play(currentPage)
|
||||
}*/
|
||||
//Log.d("Split", "oldPage: $oldPage, currentPage: $currentPage")
|
||||
}
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) {
|
||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||
//Log.d("Split", "onPageScrolled: $currentPage")
|
||||
}
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
currentPage = position
|
||||
splitViewModel.activePage.postValue(position)
|
||||
//Log.d("Split", "currentPage: $currentPage")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
button.setOnClickListener {
|
||||
if (splitMode == MainViewModel.PAGE_MODE_ONE) return@setOnClickListener
|
||||
viewModel.setSplitMode(MainViewModel.PAGE_MODE_ONE)
|
||||
Log.e(TAG, "+++ split style: 1")
|
||||
}
|
||||
|
||||
button2.setOnClickListener {
|
||||
if (splitMode == MainViewModel.PAGE_MODE_FOUR) return@setOnClickListener
|
||||
viewModel.setSplitMode(MainViewModel.PAGE_MODE_FOUR)
|
||||
Log.e(TAG, "+++ split style: 4")
|
||||
}
|
||||
|
||||
button3.setOnClickListener {
|
||||
if (splitMode == MainViewModel.PAGE_MODE_NINE) return@setOnClickListener
|
||||
viewModel.setSplitMode(MainViewModel.PAGE_MODE_NINE)
|
||||
Log.e(TAG, "+++ split style: 9")
|
||||
}
|
||||
@@ -90,6 +132,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun reloadVideoViews(list: List<List<Device>>?) = MainScope().launch {
|
||||
binding.viewPager.setCurrentItem(0, false)
|
||||
for (i in 0 until splitVideoViewAdapter.itemCount) {
|
||||
splitVideoViewAdapter.stop(i)
|
||||
}
|
||||
//delay(oldSplitMode * Constants.CONF_DELAY_BASE_MILLIS)
|
||||
delay((oldSplitMode * 100) + Constants.CONF_DELAY_BASE_MILLIS)
|
||||
oldSplitMode = splitMode
|
||||
splitVideoViewAdapter.clear()
|
||||
// 如果群組內沒有裝置,則顯示底圖
|
||||
if (list.isNullOrEmpty()) {
|
||||
@@ -104,6 +152,8 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
splitVideoViewAdapter.add(i, splitFragment)
|
||||
}
|
||||
currentPage = 0
|
||||
//binding.viewPager.currentItem = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PAGE_MODE_ONE = 1
|
||||
const val PAGE_MODE_FOUR = 4
|
||||
const val PAGE_MODE_NINE = 9
|
||||
}
|
||||
}
|
||||
+61
-12
@@ -1,14 +1,19 @@
|
||||
package com.ray650128.gstreamer_demo_app
|
||||
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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import com.ray650128.gstreamer_demo_app.Constants
|
||||
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
|
||||
@@ -17,6 +22,8 @@ import kotlin.math.sqrt
|
||||
|
||||
class SplitViewFragment : Fragment() {
|
||||
|
||||
val viewModel: SplitViewModel by activityViewModels()
|
||||
|
||||
private var mPageNum: Int = 0
|
||||
private var splitMode = MainViewModel.PAGE_MODE_ONE
|
||||
private var streamType = VideoView.SUB_STREAM
|
||||
@@ -49,11 +56,23 @@ class SplitViewFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initView()
|
||||
|
||||
/*viewModel.activePage.observe(viewLifecycleOwner) {
|
||||
if (it == null) return@observe
|
||||
if (it == this.mPageNum) {
|
||||
MainScope().launch {
|
||||
//delay(1000)
|
||||
//playAll()
|
||||
}
|
||||
} else {
|
||||
//stopAll()
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
stopAll()
|
||||
super.onPause()
|
||||
Log.d("${TAG}_$mPageNum", "onPause()")
|
||||
}
|
||||
|
||||
@@ -63,6 +82,13 @@ class SplitViewFragment : Fragment() {
|
||||
Log.d("${TAG}_$mPageNum", "onResume()")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
//destroyAll()
|
||||
stopAll()
|
||||
Log.d("${TAG}_$mPageNum", "onDestroy()")
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
// 生成 VideoView 分割畫面
|
||||
binding.apply {
|
||||
@@ -122,31 +148,40 @@ class SplitViewFragment : Fragment() {
|
||||
}
|
||||
|
||||
setAllUrl()
|
||||
}
|
||||
}
|
||||
|
||||
if (isClickable) {
|
||||
for (position in videoViews.indices) {
|
||||
videoViews[position].setOnClickListener {
|
||||
if (position >= data.size) return@setOnClickListener
|
||||
/*if (!videoViews[position].isPlaying) {
|
||||
if (!videoViews[position].isPlaying) {
|
||||
if (!videoViews[position].isLoading) {
|
||||
videoViews[position].resetRetryCount()
|
||||
videoViews[position].play()
|
||||
}
|
||||
return@setSafeOnClickListener
|
||||
return@setOnClickListener
|
||||
}
|
||||
MainScope().launch {
|
||||
stopAll()
|
||||
delay(splitMode * Constants.CONF_DELAY_BASE_MILLIS)
|
||||
//delay((splitMode * 100) + Constants.CONF_DELAY_BASE_MILLIS)
|
||||
val item = data[position]
|
||||
val bundle = Bundle().apply {
|
||||
putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
|
||||
putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
|
||||
//putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
|
||||
//putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
|
||||
putParcelable(MonitoringActivity.BUNDLE_DEVICE, item)
|
||||
}
|
||||
gotoActivity(MonitoringActivity::class.java, bundle)*/
|
||||
val intent = Intent(requireContext(), MonitoringActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
//gotoActivity(MonitoringActivity::class.java, bundle)*/
|
||||
//Log.d("${TAG}_$mPageNum", "check: $item")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAllUrl() {
|
||||
for (index in data.indices) {
|
||||
@@ -157,18 +192,32 @@ class SplitViewFragment : Fragment() {
|
||||
|
||||
fun playAll() = MainScope().launch(Dispatchers.Main) {
|
||||
if (videoViews.isEmpty()) return@launch
|
||||
delay(splitMode * Constants.CONF_DELAY_BASE_MILLIS)
|
||||
for (index in data.indices) {
|
||||
if (!videoViews[index].isReady) continue
|
||||
videoViews[index].resetRetryCount()
|
||||
videoViews[index].play()
|
||||
delay(300)
|
||||
}
|
||||
//delay(300)
|
||||
}
|
||||
}.start()
|
||||
|
||||
fun stopAll() = MainScope().launch(Dispatchers.Main) {
|
||||
if (videoViews.isEmpty()) return@launch
|
||||
for (index in data.indices) {
|
||||
videoViews[index].stopRetryCount()
|
||||
if (!videoViews[index].isPlaying || videoViews[index].isLoading) continue
|
||||
videoViews[index].stop()
|
||||
delay(300)
|
||||
//delay(300)
|
||||
}
|
||||
}.start()
|
||||
|
||||
fun destroyAll() /*= MainScope().launch(Dispatchers.Main)*/ {
|
||||
if (videoViews.isEmpty()) return
|
||||
for (index in data.indices) {
|
||||
videoViews[index].destroy()
|
||||
//delay(100)
|
||||
}
|
||||
}//.start()
|
||||
|
||||
companion object {
|
||||
private val TAG = SplitViewFragment::class.java.simpleName
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+62
-30
@@ -1,4 +1,4 @@
|
||||
package com.ray650128.gstreamer_demo_app
|
||||
package com.ray650128.gstreamer_demo_app.ui.mainScreen
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
@@ -15,9 +15,10 @@ 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, SurfaceHolder.Callback, GstCallback {
|
||||
class VideoView : ConstraintLayout, GstCallback {
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
initView(context)
|
||||
@@ -35,6 +36,8 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
|
||||
private var data: Device? = null
|
||||
|
||||
var isReady: Boolean = false
|
||||
|
||||
var isLoading: Boolean = false
|
||||
set(value) {
|
||||
view.pbLoading.isVisible = value
|
||||
@@ -47,14 +50,18 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
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: 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())
|
||||
}
|
||||
@@ -64,23 +71,32 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
|
||||
view.baseView.clipToOutline = true
|
||||
|
||||
videoView.holder.addCallback(this)
|
||||
//videoView.holder.addCallback(this)
|
||||
gstLibrary = GstLibrary(context)
|
||||
gstLibrary.setSurfaceHolder(videoView.holder)
|
||||
gstLibrary.setOnStatusChangeListener(this)
|
||||
|
||||
// View 預設狀態
|
||||
view.textDeviceName.isVisible = false
|
||||
isPlaying = false
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
fun setData(device: Device?) {
|
||||
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.name
|
||||
view.textDeviceName.text = device.name
|
||||
this.tag = device.deviceName
|
||||
view.textDeviceName.text = device.deviceName
|
||||
view.textDeviceName.isVisible = true
|
||||
gstLibrary.setRtspUrl(this.data?.rtspUrl)
|
||||
Log.d("${TAG}_$tag", "Set device to: $device")
|
||||
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) {
|
||||
@@ -88,50 +104,49 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
}
|
||||
|
||||
fun play() {
|
||||
if (data == null) return
|
||||
videoView.postInvalidate()
|
||||
gstLibrary.play()
|
||||
retryCount = 0
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (data == null) return
|
||||
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")
|
||||
fun destroy() {
|
||||
if (this::gstLibrary.isInitialized) {
|
||||
gstLibrary.setSurfaceHolder(holder)
|
||||
gstLibrary.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(p0: SurfaceHolder) {
|
||||
Log.d("${TAG}_$tag", "Surface destroyed")
|
||||
// TODO: 如果呼叫 releaseSurface(),會閃退
|
||||
/*if (this::gstLibrary.isInitialized) {
|
||||
gstLibrary.releaseSurface()
|
||||
}*/
|
||||
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.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")
|
||||
//Log.e("${TAG}_$tag", "onStatus: $gstStatus")
|
||||
}
|
||||
|
||||
override fun onMessage(message: String?) {
|
||||
Log.e("${TAG}_$tag", "onMessage: $message")
|
||||
//Log.e("${TAG}_$tag", "onMessage: $message")
|
||||
}
|
||||
|
||||
inner class MyHandler(looper: Looper): Handler(looper) {
|
||||
@@ -144,6 +159,20 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
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++
|
||||
} else {
|
||||
stopRetryCount()
|
||||
Log.e("${TAG}_$tag", "Retry count = 5, stopped retry...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,6 +186,9 @@ class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -1,4 +1,4 @@
|
||||
package com.ray650128.gstreamer_demo_app
|
||||
package com.ray650128.gstreamer_demo_app.ui.mainScreen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.fragment.app.FragmentManager
|
||||
@@ -49,13 +49,21 @@ class VideoViewAdapter(
|
||||
fragment.stopAll()
|
||||
}
|
||||
|
||||
/*fun play(index: Int) {
|
||||
fun play(index: Int) {
|
||||
play(fragments[index])
|
||||
}
|
||||
|
||||
private fun play(fragment: SplitViewFragment) {
|
||||
fragment.playAll()
|
||||
}*/
|
||||
}
|
||||
|
||||
fun destroy(index: Int) {
|
||||
destroy(fragments[index])
|
||||
}
|
||||
|
||||
private fun destroy(fragment: SplitViewFragment) {
|
||||
fragment.destroyAll()
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return fragments[position].hashCode().toLong()
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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.databinding.ActivityMainBinding
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
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,9 @@
|
||||
<?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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.monitoringScreen.MonitoringActivity">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -9,4 +9,4 @@
|
||||
app:columnCount="3"
|
||||
app:rowCount="3"
|
||||
app:useDefaultMargins="false"
|
||||
tools:context=".SplitViewFragment" />
|
||||
tools:context=".ui.mainScreen.SplitViewFragment" />
|
||||
+3
-3
@@ -7,9 +7,9 @@ buildscript {
|
||||
}
|
||||
|
||||
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 'com.android.application' version '7.4.2' apply false
|
||||
id 'com.android.library' version '7.4.2' 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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Thu Mar 24 14:41:01 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -8,8 +8,6 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
|
||||
@@ -35,6 +35,7 @@ typedef struct _CustomData {
|
||||
GstState state; /* Current pipeline state */
|
||||
GstState target_state; /* Desired pipeline state, to be set once buffering is complete */
|
||||
gboolean is_live; /* Live streams do not use buffering */
|
||||
gchar * tag;
|
||||
} CustomData;
|
||||
|
||||
/* playbin2 flags */
|
||||
@@ -94,7 +95,7 @@ static JNIEnv *get_jni_env(void) {
|
||||
/* Change the content of the UI's TextView */
|
||||
static void set_ui_message(const gchar *message, CustomData *data) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
GST_DEBUG ("Setting message to: %s", message);
|
||||
//GST_DEBUG ("Setting message to: %s", message);
|
||||
jstring jmessage = (*env)->NewStringUTF(env, message);
|
||||
(*env)->CallVoidMethod(env, data->app, set_message_method_id, jmessage);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
@@ -125,8 +126,9 @@ static void error_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
|
||||
/* Called when the End Of the Stream is reached. Just move to the beginning of the media and pause. */
|
||||
static void eos_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
|
||||
data->target_state = GST_STATE_PAUSED;
|
||||
data->is_live |= (gst_element_set_state(data->pipeline,
|
||||
GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GstStateChangeReturn status = gst_element_set_state(data->pipeline,GST_STATE_PAUSED);
|
||||
data->is_live |= (status == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GST_DEBUG ("eos_cb, %s", data->is_live ? "true" : "false");
|
||||
}
|
||||
|
||||
/* Called when buffering messages are received. We inform the UI about the current buffering level and
|
||||
@@ -196,8 +198,7 @@ static void state_changed_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
|
||||
/* Only pay attention to messages coming from the pipeline, not its children */
|
||||
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->pipeline)) {
|
||||
data->state = new_state;
|
||||
gchar *message = g_strdup_printf("State changed to %s",
|
||||
gst_element_state_get_name(new_state));
|
||||
gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
|
||||
set_ui_message(message, data);
|
||||
g_free(message);
|
||||
|
||||
@@ -217,9 +218,7 @@ static void state_changed_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
|
||||
static void check_initialization_complete(CustomData *data) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
if (!data->initialized && data->native_window && data->main_loop) {
|
||||
GST_DEBUG
|
||||
("Initialization complete, notifying application. native_window:%p main_loop:%p",
|
||||
data->native_window, data->main_loop);
|
||||
GST_DEBUG("Initialization complete, notifying application. native_window:%p main_loop:%p", data->native_window, data->main_loop);
|
||||
|
||||
/* The main loop is running and we received a native window, inform the sink about it */
|
||||
gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY (data->pipeline),
|
||||
@@ -311,8 +310,7 @@ static void *app_function(void *userdata) {
|
||||
static void gst_native_init(JNIEnv *env, jobject thiz) {
|
||||
CustomData *data = g_new0 (CustomData, 1);
|
||||
SET_CUSTOM_DATA (env, thiz, custom_data_field_id, data);
|
||||
GST_DEBUG_CATEGORY_INIT (debug_category, "player", 0,
|
||||
"Android tutorial 5");
|
||||
GST_DEBUG_CATEGORY_INIT (debug_category, "player", 0, "HI SHARP DX RTSP Player");
|
||||
gst_debug_set_threshold_for_name("player", GST_LEVEL_DEBUG);
|
||||
GST_DEBUG ("Created CustomData at %p", data);
|
||||
data->app = (*env)->NewGlobalRef(env, thiz);
|
||||
@@ -325,16 +323,16 @@ static void gst_native_finalize(JNIEnv *env, jobject thiz) {
|
||||
CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
|
||||
if (!data)
|
||||
return;
|
||||
GST_DEBUG ("Quitting main loop...");
|
||||
GST_DEBUG ("[%s]Quitting main loop...", data->tag);
|
||||
g_main_loop_quit(data->main_loop);
|
||||
GST_DEBUG ("Waiting for thread to finish...");
|
||||
GST_DEBUG ("[%s]Waiting for thread to finish...", data->tag);
|
||||
pthread_join(gst_app_thread, NULL);
|
||||
GST_DEBUG ("Deleting GlobalRef for app object at %p", data->app);
|
||||
GST_DEBUG ("[%s]Deleting GlobalRef for app object at %p", data->tag, data->app);
|
||||
(*env)->DeleteGlobalRef(env, data->app);
|
||||
GST_DEBUG ("Freeing CustomData at %p", data);
|
||||
GST_DEBUG ("[%s]Freeing CustomData at %p", data->tag, data);
|
||||
g_free(data);
|
||||
SET_CUSTOM_DATA (env, thiz, custom_data_field_id, NULL);
|
||||
GST_DEBUG ("Done finalizing");
|
||||
GST_DEBUG ("[%s]Done finalizing", data->tag);
|
||||
}
|
||||
|
||||
/* Set playbin2's URI */
|
||||
@@ -349,7 +347,21 @@ void gst_native_set_uri(JNIEnv *env, jobject thiz, jstring uri) {
|
||||
g_object_set(data->pipeline, "uri", char_uri, NULL);
|
||||
(*env)->ReleaseStringUTFChars(env, uri, char_uri);
|
||||
g_object_set(data->pipeline, "latency", 250, NULL);
|
||||
data->is_live = (gst_element_set_state(data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GstStateChangeReturn status = gst_element_set_state(data->pipeline, data->target_state);
|
||||
data->is_live = (status == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GST_DEBUG ("[%s]gst_native_set_uri, %s", data->tag, data->is_live ? "true" : "false");
|
||||
}
|
||||
|
||||
|
||||
/* Set name */
|
||||
void gst_native_set_tag(JNIEnv *env, jobject thiz, jstring tag) {
|
||||
CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
|
||||
if (!data || !data->pipeline)
|
||||
return;
|
||||
const gchar *char_tag = (*env)->GetStringUTFChars(env, tag, NULL);
|
||||
GST_DEBUG ("Setting TAG to %s", char_tag);
|
||||
data->tag = g_strdup (char_tag);
|
||||
(*env)->ReleaseStringUTFChars(env, tag, char_tag);
|
||||
}
|
||||
|
||||
/* Set pipeline to PLAYING state */
|
||||
@@ -357,9 +369,12 @@ static void gst_native_play(JNIEnv *env, jobject thiz) {
|
||||
CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
|
||||
if (!data)
|
||||
return;
|
||||
GST_DEBUG ("Setting state to PLAYING");
|
||||
//GST_DEBUG ("Setting state to PLAYING");
|
||||
|
||||
GstStateChangeReturn status = gst_element_set_state(data->pipeline, GST_STATE_PLAYING);
|
||||
data->is_live = (status == GST_STATE_CHANGE_ASYNC);
|
||||
data->target_state = GST_STATE_PLAYING;
|
||||
data->is_live = (gst_element_set_state(data->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GST_DEBUG ("[%s]Setting state to PLAYING, %s", data->tag, data->is_live ? "true" : "false");
|
||||
}
|
||||
|
||||
/* Set pipeline to PAUSED state */
|
||||
@@ -367,9 +382,11 @@ static void gst_native_pause(JNIEnv *env, jobject thiz) {
|
||||
CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
|
||||
if (!data)
|
||||
return;
|
||||
GST_DEBUG ("Setting state to PAUSED");
|
||||
//GST_DEBUG ("Setting state to PAUSED");
|
||||
GstStateChangeReturn status = gst_element_set_state(data->pipeline, GST_STATE_PAUSED);
|
||||
data->is_live = (status == GST_STATE_CHANGE_ASYNC);
|
||||
data->target_state = GST_STATE_PAUSED;
|
||||
data->is_live = (gst_element_set_state(data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
|
||||
GST_DEBUG ("[%s]Setting state to PAUSED, %s", data->tag, data->is_live ? "true" : "false");
|
||||
}
|
||||
|
||||
/* Static class initializer: retrieve method and field IDs */
|
||||
@@ -396,20 +413,18 @@ static void gst_native_surface_init(JNIEnv *env, jobject thiz, jobject surface)
|
||||
if (!data)
|
||||
return;
|
||||
ANativeWindow *new_native_window = ANativeWindow_fromSurface(env, surface);
|
||||
GST_DEBUG ("Received surface %p (native window %p)", surface,
|
||||
new_native_window);
|
||||
GST_DEBUG ("[%s]Received surface %p (native window %p)", data->tag, surface, new_native_window);
|
||||
|
||||
if (data->native_window) {
|
||||
ANativeWindow_release(data->native_window);
|
||||
if (data->native_window == new_native_window) {
|
||||
GST_DEBUG ("New native window is the same as the previous one %p",
|
||||
data->native_window);
|
||||
GST_DEBUG ("[%s]New native window is the same as the previous one %p", data->tag, data->native_window);
|
||||
if (data->pipeline) {
|
||||
gst_video_overlay_expose(GST_VIDEO_OVERLAY (data->pipeline));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
GST_DEBUG ("Released previous native window %p", data->native_window);
|
||||
GST_DEBUG ("[%s]Released previous native window %p", data->tag, data->native_window);
|
||||
data->initialized = FALSE;
|
||||
}
|
||||
}
|
||||
@@ -422,7 +437,7 @@ static void gst_native_surface_finalize(JNIEnv *env, jobject thiz) {
|
||||
CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
|
||||
if (!data)
|
||||
return;
|
||||
GST_DEBUG ("Releasing Native Window %p", data->native_window);
|
||||
GST_DEBUG ("[%s]Releasing Native Window %p", data->tag, data->native_window);
|
||||
|
||||
if (data->pipeline) {
|
||||
gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY (data->pipeline),
|
||||
@@ -440,6 +455,7 @@ static JNINativeMethod native_methods[] = {
|
||||
{"nativeInit", "()V", (void *) gst_native_init},
|
||||
{"nativeFinalize", "()V", (void *) gst_native_finalize},
|
||||
{"nativeSetUri", "(Ljava/lang/String;)V", (void *) gst_native_set_uri},
|
||||
{"nativeSetTag", "(Ljava/lang/String;)V", (void *) gst_native_set_tag},
|
||||
{"nativePlay", "()V", (void *) gst_native_play},
|
||||
{"nativePause", "()V", (void *) gst_native_pause},
|
||||
{"nativeSurfaceInit", "(Ljava/lang/Object;)V",
|
||||
@@ -455,8 +471,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
java_vm = vm;
|
||||
|
||||
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "player",
|
||||
"Could not retrieve JNIEnv");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "player", "Could not retrieve JNIEnv");
|
||||
return 0;
|
||||
}
|
||||
jclass klass = (*env)->FindClass(env, "com/hisharp/gstreamer_player/GstLibrary");
|
||||
|
||||
@@ -13,7 +13,9 @@ import org.freedesktop.gstreamer.GStreamer;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public class GstLibrary implements Closeable {
|
||||
public class GstLibrary implements Closeable, SurfaceHolder.Callback {
|
||||
|
||||
private static final String TAG = GstLibrary.class.getSimpleName();
|
||||
|
||||
final Context mAppContext;
|
||||
|
||||
@@ -21,6 +23,10 @@ public class GstLibrary implements Closeable {
|
||||
|
||||
private String rtspUrl;
|
||||
|
||||
private String tag = "";
|
||||
|
||||
private Boolean isInit = false;
|
||||
|
||||
public GstLibrary(Context context) {
|
||||
this.mAppContext = context.getApplicationContext();
|
||||
|
||||
@@ -38,6 +44,7 @@ public class GstLibrary implements Closeable {
|
||||
private native void nativeInit(); // Initialize native code, build pipeline, etc
|
||||
private native void nativeFinalize(); // Destroy pipeline and shutdown native code
|
||||
private native void nativeSetUri(String uri); // Set the URI of the media to play
|
||||
private native void nativeSetTag(String tag); // Set the URI of the media to play
|
||||
private native void nativePlay(); // Set pipeline to PLAYING
|
||||
private native void nativePause(); // Set pipeline to PAUSED
|
||||
private static native boolean nativeClassInit(); // Initialize native class: cache Method IDs for callbacks
|
||||
@@ -48,10 +55,12 @@ public class GstLibrary implements Closeable {
|
||||
//private final String defaultMediaUri = "rtsp://admin:admin@192.168.0.77:554/media/video2";
|
||||
|
||||
public void play() {
|
||||
if (!isInit) return;
|
||||
nativePlay();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!isInit) return;
|
||||
nativePause();
|
||||
}
|
||||
|
||||
@@ -59,7 +68,13 @@ public class GstLibrary implements Closeable {
|
||||
this.rtspUrl = rtspUrl;
|
||||
}
|
||||
|
||||
public void setTag(String tag) {
|
||||
this.tag = tag;
|
||||
nativeSetTag(tag);
|
||||
}
|
||||
|
||||
public void releaseSurface() {
|
||||
isInit = false;
|
||||
nativeSurfaceFinalize();
|
||||
}
|
||||
|
||||
@@ -68,7 +83,7 @@ public class GstLibrary implements Closeable {
|
||||
}
|
||||
|
||||
public void setSurfaceHolder(SurfaceHolder holder) {
|
||||
nativeSurfaceInit(holder.getSurface());
|
||||
holder.addCallback(this);
|
||||
}
|
||||
|
||||
// Called from native code. This sets the content of the TextView from the UI thread.
|
||||
@@ -76,7 +91,7 @@ public class GstLibrary implements Closeable {
|
||||
if (gstCallback == null) return;
|
||||
if (message.contains("State changed to PAUSED")) {
|
||||
gstCallback.onStatus(GstStatus.PAUSE);
|
||||
} else if (message.contains("State changed to PLAYING")) {
|
||||
} else if (message.contains("State changed to PLAYING") || message.contains("Buffering complete")) {
|
||||
gstCallback.onStatus(GstStatus.PLAYING);
|
||||
} else if (message.contains("Could not open resource for reading and writing")) {
|
||||
gstCallback.onStatus(GstStatus.ERROR_WHEN_OPENING);
|
||||
@@ -84,6 +99,8 @@ public class GstLibrary implements Closeable {
|
||||
gstCallback.onStatus(GstStatus.ERROR);
|
||||
} else if (message.contains("Unhandled error")) {
|
||||
gstCallback.onStatus(GstStatus.ERROR);
|
||||
} else if (message.contains("Buffering")) {
|
||||
gstCallback.onStatus(GstStatus.BUFFERING);
|
||||
}
|
||||
gstCallback.onMessage(message);
|
||||
}
|
||||
@@ -91,7 +108,7 @@ public class GstLibrary implements Closeable {
|
||||
// Called from native code. Native code calls this once it has created its pipeline and
|
||||
// the main loop is running, so it is ready to accept commands.
|
||||
private void onGStreamerInitialized () {
|
||||
Log.i ("GStreamer", "GStreamer initialized:");
|
||||
Log.i (TAG + "+" + tag, "GStreamer initialized:");
|
||||
|
||||
if (gstCallback != null) {
|
||||
gstCallback.onStatus(GstStatus.READY);
|
||||
@@ -101,12 +118,14 @@ public class GstLibrary implements Closeable {
|
||||
if (rtspUrl != null) {
|
||||
nativeSetUri(rtspUrl);
|
||||
}
|
||||
|
||||
isInit = true;
|
||||
}
|
||||
|
||||
// Called from native code when the size of the media changes or is first detected.
|
||||
// Inform the video surface about the new size and recalculate the layout.
|
||||
private void onMediaSizeChanged (int width, int height) {
|
||||
Log.i ("GStreamer", "Media size changed to " + width + "x" + height);
|
||||
Log.i (TAG + "+" + tag, String.format("Media size changed to %d x %d", width, height));
|
||||
/*mainHandler.post(() -> {
|
||||
surfaceView.media_width = width;
|
||||
surfaceView.media_height = height;
|
||||
@@ -122,10 +141,32 @@ public class GstLibrary implements Closeable {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
isInit = false;
|
||||
try {
|
||||
nativeFinalize();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.d(TAG + "+" + tag, "Surface created: " + holder.getSurface());
|
||||
Log.i (TAG + "+" + tag, "GStreamer surfaceCreated");
|
||||
isInit = true;
|
||||
nativeSurfaceInit(holder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
Log.d(TAG + "+" + tag, String.format("Surface changed, format: %d, width: %d, height: %d", format, width, height));
|
||||
/*setSurfaceHolder(holder);*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.d(TAG + "+" + tag, "Surface destroyed");
|
||||
releaseSurface();
|
||||
isInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ public enum GstStatus {
|
||||
PAUSE,
|
||||
PLAYING,
|
||||
ERROR,
|
||||
ERROR_WHEN_OPENING
|
||||
ERROR_WHEN_OPENING,
|
||||
BUFFERING
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user