Compare commits

...

10 Commits

Author SHA1 Message Date
Raymond Yang
0a65a9615b 刪除無用的資源 2023-01-19 17:07:30 +08:00
Raymond Yang
642d0e7525 清理程式碼,並加上註解 2023-01-19 17:04:57 +08:00
Raymond Yang
eb4f643e99 初步避開閃退問題 2023-01-19 16:07:10 +08:00
Raymond Yang
e48ebcbb02 調整架構 2023-01-19 12:12:41 +08:00
Raymond Yang
8288bc57c5 修正gradle文件 2023-01-18 14:47:46 +08:00
Barney
ca3744f8be 整理排版 2022-08-09 14:18:09 +08:00
Barney
969c91e436 拿掉無用的try catch 2022-08-09 14:17:08 +08:00
Barney
38b39c6d1d 將nativeSurfaceInit改為在surfaceChanged呼叫 2022-08-09 12:29:09 +08:00
Barney
2eca9c4860 測試單分割狀態 2022-08-09 12:23:14 +08:00
Barney
81c48f7cf2 新增錯誤狀態 2022-08-08 14:35:29 +08:00
30 changed files with 719 additions and 456 deletions

View File

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

View File

@ -1,15 +1,18 @@
plugins { plugins {
id 'com.android.application' 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 { android {
compileSdk 32 ndkVersion "21.3.6528147"
compileSdk 33
defaultConfig { defaultConfig {
applicationId "com.ray650128.gstreamer_demo_app" applicationId "com.ray650128.gstreamer_demo_app"
minSdk 26 minSdk 26
targetSdk 32 targetSdk 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -26,23 +29,32 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = '1.8'
}
lint {
abortOnError false
checkReleaseBuilds false
}
viewBinding.enabled = true viewBinding.enabled = true
namespace 'com.ray650128.gstreamer_demo_app'
} }
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.appcompat:appcompat:1.5.1'
implementation project(path: ':gstreamer_player') 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.gridlayout:gridlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation project(path: ':gstreamer_player')
}
repositories { // Android Jetpack lib
mavenCentral() implementation("androidx.fragment:fragment-ktx:1.5.5")
implementation("androidx.activity:activity-ktx:1.6.1")
} }

View File

@ -1,227 +0,0 @@
package com.ray650128.gstreamer_demo_app
import android.graphics.Color
import android.os.Bundle
import android.util.DisplayMetrics
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.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 {
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()
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() {
Log.d("${TAG}_$mPageNum", "onDestroyView()")
super.onDestroyView()
}
override fun onDestroy() {
for (i in gstPlayers.indices) {
gstPlayers[i]?.release()
gstPlayers[i] = null
}
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()).apply {
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)
videoViews.add(videoView)
}
}
}
for (index in videoViews.indices) {
gstPlayers.add(GstLibrary(requireContext(), data[index]))
gstPlayers[index]?.setSurfaceView(videoViews[index].videoView)
gstPlayers[index]?.setOnStatusChangeListener(this)
}
}
private fun playAll() {
for (index in data.indices) {
gstPlayers[index]?.apply {
play()
}
}
}
private fun stopAll() {
for (index in data.indices) {
try {
gstPlayers[index]?.stop()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
}
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
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")
}
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,75 +1,109 @@
package com.ray650128.gstreamer_demo_app package com.ray650128.gstreamer_demo_app
import android.annotation.SuppressLint import android.content.Context
import android.graphics.Color 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.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.view.WindowManager 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 com.ray650128.gstreamer_demo_app.databinding.ActivityMainBinding
import java.util.ArrayList import com.ray650128.gstreamer_demo_app.model.Device
import java.util.function.Consumer import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.freedesktop.gstreamer.GStreamer
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var splitVideoViewAdapter: ViewPager2Adapter private val viewModel: MainViewModel by viewModels()
@SuppressLint("AuthLeak") private val mContext: Context by lazy { this }
private val defaultMediaUris = arrayListOf(
arrayListOf( private var splitMode = 1
"rtsp://admin:admin@211.23.78.226:8574/v02",
"rtsp://admin:admin@211.23.78.226:8575/v02", private lateinit var splitVideoViewAdapter: VideoViewAdapter
"rtsp://admin:admin@192.168.0.77:554/media/video2",
"rtsp://admin:123456@192.168.0.80:554/profile2", private var videos: List<List<Device>>? = null
"rtsp://admin:123456@192.168.0.83:554/profile2",
"rtsp://admin:123456@192.168.0.84:554/profile2", private var currentPage = 0
"rtsp://admin:admin@192.168.0.86:554/v2",
"rtsp://admin:admin@192.168.0.89:554/v02",
"rtsp://admin:1q2w3e4r!@60.249.32.50:554/unicast/c8/s1/live",
),
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"
)
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
splitVideoViewAdapter = ViewPager2Adapter(supportFragmentManager, lifecycle) initContentView()
binding.viewPager.apply { initObservers()
}
private fun initContentView() = binding.apply {
//region Content area
splitVideoViewAdapter = VideoViewAdapter(supportFragmentManager, lifecycle)
viewPager.apply {
adapter = splitVideoViewAdapter adapter = splitVideoViewAdapter
offscreenPageLimit = 1 offscreenPageLimit = 1
registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentPage = position
}
})
} }
reloadVideoViews() button.setOnClickListener {
viewModel.setSplitMode(MainViewModel.PAGE_MODE_ONE)
Log.e(TAG, "+++ split style: 1")
} }
private fun reloadVideoViews() { 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)
currentPage = 0
}
}
private fun reloadVideoViews(list: List<List<Device>>?) = MainScope().launch {
binding.viewPager.setCurrentItem(0, false)
splitVideoViewAdapter.clear() splitVideoViewAdapter.clear()
// 如果群組內沒有裝置,則顯示底圖
for (index in defaultMediaUris.indices) { if (list.isNullOrEmpty()) {
val gridFragment = GridVideoFragment.newInstance(index, 9, defaultMediaUris[index]) binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing)
splitVideoViewAdapter.add(index, gridFragment) } 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,206 @@
package com.ray650128.gstreamer_demo_app
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import com.ray650128.gstreamer_demo_app.databinding.FragmentSplitViewBinding
import com.ray650128.gstreamer_demo_app.model.Device
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.sqrt
class SplitViewFragment : Fragment() {
private var mPageNum: Int = 0
private var splitMode = MainViewModel.PAGE_MODE_ONE
private var streamType = VideoView.SUB_STREAM
private var isClickable = true
private lateinit var binding: FragmentSplitViewBinding
private var data: ArrayList<Device> = ArrayList()
private var videoViews: ArrayList<VideoView> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
mPageNum = requireArguments().getInt(ARG_PAGE_NUM)
splitMode = requireArguments().getInt(ARG_SPLIT_MODE)
isClickable = requireArguments().getBoolean(ARG_CLICKABLE)
streamType = requireArguments().getInt(ARG_STREAM_TYPE)
data = requireArguments().getParcelableArrayList(ARG_PAGE_DATA) ?: arrayListOf(Device())
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentSplitViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
override fun onPause() {
super.onPause()
stopAll()
Log.d("${TAG}_$mPageNum", "onPause()")
}
override fun onResume() {
super.onResume()
playAll()
Log.d("${TAG}_$mPageNum", "onResume()")
}
private fun initView() {
// 生成 VideoView 分割畫面
binding.apply {
val maxRow = sqrt(splitMode.toFloat()).toInt()
val maxCol = sqrt(splitMode.toFloat()).toInt()
//Log.d(TAG, "maxRow: $maxRow, maxCol: $maxCol")
baseView.rowCount = maxRow
baseView.columnCount = maxCol
Log.e("${TAG}_$mPageNum", "baseView.rowCount: ${baseView.rowCount}, baseView.columnCount: ${baseView.columnCount}")
baseView.post {
for (col in 0 until maxCol) {
for (row in 0 until maxRow) {
val videoView = VideoView(requireContext())
val layoutParam = GridLayout.LayoutParams().apply {
topMargin = 0.dp
bottomMargin = 0.dp
marginEnd = 0.dp
marginStart = 0.dp
// 調整間距
when (splitMode) {
MainViewModel.PAGE_MODE_FOUR -> {
when (col) {
0 -> bottomMargin = 2.dp
1 -> topMargin = 2.dp
}
when (row) {
0 -> marginEnd = 2.dp
1 -> marginStart = 2.dp
}
width = (baseView.width / maxRow) - maxRow.dp
height = (baseView.height / maxCol) - maxCol.dp
}
MainViewModel.PAGE_MODE_NINE -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginEnd = 4.dp
marginStart = 4.dp
}
width = (baseView.width / maxRow) - maxRow.dp
height = (baseView.height / maxCol) - maxCol.dp
}
MainViewModel.PAGE_MODE_ONE -> {
width = (baseView.width / maxRow)
height = (baseView.height / maxCol)
}
}
}
baseView.addView(videoView, layoutParam)
videoViews.add(videoView)
}
}
setAllUrl()
}
}
if (isClickable) {
for (position in videoViews.indices) {
videoViews[position].setOnClickListener {
if (position >= data.size) return@setOnClickListener
/*if (!videoViews[position].isPlaying) {
if (!videoViews[position].isLoading) {
videoViews[position].play()
}
return@setSafeOnClickListener
}
stopAll()
val item = data[position]
val bundle = Bundle().apply {
putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
}
gotoActivity(MonitoringActivity::class.java, bundle)*/
//Log.d("${TAG}_$mPageNum", "check: $item")
}
}
}
}
private fun setAllUrl() {
for (index in data.indices) {
videoViews[index].setData(data[index])
videoViews[index].setTextVisible((splitMode != MainViewModel.PAGE_MODE_NINE))
}
}
fun playAll() = MainScope().launch(Dispatchers.Main) {
if (videoViews.isEmpty()) return@launch
for (index in data.indices) {
videoViews[index].play()
delay(300)
}
}
fun stopAll() = MainScope().launch(Dispatchers.Main) {
for (index in data.indices) {
videoViews[index].stop()
delay(300)
}
}
companion object {
private val TAG = SplitViewFragment::class.java.simpleName
private const val ARG_PAGE_NUM = "page_number"
private const val ARG_SPLIT_MODE = "split_mode"
private const val ARG_CLICKABLE = "clickable"
private const val ARG_PAGE_DATA = "page_data"
private const val ARG_STREAM_TYPE = "stream_type"
/**
* 透過傳入的參數生成新的 Fragment 實例
*
* @param pageNumber Fragment 頁碼
* @param splitMode 畫面分割模式(9/4/1分割)
* @return 透過傳入的參數生成新的 Fragment 實例
*/
fun newInstance(
pageNumber: Int,
splitMode: Int,
isClickable: Boolean = true,
pageData: ArrayList<Device>,
streamType: Int = VideoView.SUB_STREAM
): SplitViewFragment {
val fragment = SplitViewFragment()
val args = Bundle()
args.putInt(ARG_PAGE_NUM, pageNumber)
args.putInt(ARG_SPLIT_MODE, splitMode)
args.putBoolean(ARG_CLICKABLE, isClickable)
args.putParcelableArrayList(ARG_PAGE_DATA, pageData)
args.putInt(ARG_STREAM_TYPE, streamType)
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,15 +1,23 @@
package com.ray650128.gstreamer_demo_app package com.ray650128.gstreamer_demo_app
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible 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.databinding.ItemVideoViewBinding
import com.ray650128.gstreamer_demo_app.model.Device
class VideoView : ConstraintLayout { class VideoView : ConstraintLayout, SurfaceHolder.Callback, GstCallback {
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
initView(context) initView(context)
@ -25,7 +33,13 @@ class VideoView : ConstraintLayout {
private lateinit var view: ItemVideoViewBinding 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 var isPlaying: Boolean = false
set(value) { set(value) {
@ -33,26 +47,116 @@ class VideoView : ConstraintLayout {
field = value 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 val mHandler: MyHandler by lazy {
MyHandler(Looper.getMainLooper())
}
private fun initView(context: Context) { private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context) val layoutInflater = LayoutInflater.from(context)
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.setOnStatusChangeListener(this)
} }
/*fun setData(device: Device?) { fun setData(device: Device?) {
if (device == null) { if (device == null) {
view.textDeviceName.isVisible = false view.textDeviceName.isVisible = false
isPlaying = false
return return
} }
this.data = device 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 view.textDeviceName.isVisible = true
Log.d(TAG, "Set device to: $device") gstLibrary.setRtspUrl(this.data?.rtspUrl)
Log.d("${TAG}_$tag", "Set device to: $device")
}
fun setTextVisible(isVisible: Boolean) {
view.textDeviceName.isVisible = isVisible
}
fun play() {
videoView.postInvalidate()
gstLibrary.play()
retryCount = 0
}
fun stop() {
isPlaying = false
if (this::gstLibrary.isInitialized) {
gstLibrary.stop()
}
retryCount = 999
}
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d("${TAG}_$tag", "Surface created: " + holder.surface)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d("${TAG}_$tag", "Surface changed to format: $format, width: $width, height: $height")
if (this::gstLibrary.isInitialized) {
gstLibrary.setSurfaceHolder(holder)
}
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
Log.d("${TAG}_$tag", "Surface destroyed")
// TODO: 如果呼叫 releaseSurface(),會閃退
/*if (this::gstLibrary.isInitialized) {
gstLibrary.releaseSurface()
}*/ }*/
}
override fun onStatus(gstStatus: GstStatus?) { // onStatus 不是在主執行緒,因此透過 Handler 發訊息到主執行緒去執行
when (gstStatus) {
GstStatus.PLAYING -> mHandler.sendMessage(Message().apply { what = MSG_PLAY })
GstStatus.PAUSE -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
GstStatus.ERROR_WHEN_OPENING -> mHandler.sendMessage(Message().apply { what = MSG_PAUSE })
else -> {}
}
Log.e("${TAG}_$tag", "onStatus: $gstStatus")
}
override fun onMessage(message: String?) {
Log.e("${TAG}_$tag", "onMessage: $message")
}
inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
when(msg.what) {
MSG_PAUSE -> {
isPlaying = false
isLoading = false
}
MSG_PLAY -> {
isLoading = false
isPlaying = true
}
}
}
}
companion object { companion object {
private val TAG = VideoView::class.java.simpleName private val TAG = VideoView::class.java.simpleName
const val MAIN_STREAM = 1
const val SUB_STREAM = 2
private const val MSG_PAUSE = 1
private const val MSG_PLAY = 2
} }
} }

View File

@ -1,31 +1,31 @@
package com.ray650128.gstreamer_demo_app package com.ray650128.gstreamer_demo_app
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
class ViewPager2Adapter( class VideoViewAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
lifecycle: Lifecycle lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) { ) : FragmentStateAdapter(fragmentManager, lifecycle) {
private var fragments: MutableList<Fragment> = arrayListOf() private var fragments: MutableList<SplitViewFragment> = arrayListOf()
override fun getItemCount(): Int { override fun getItemCount(): Int {
return fragments.size return fragments.size
} }
override fun createFragment(position: Int): Fragment {
override fun createFragment(position: Int): SplitViewFragment {
return fragments[position] return fragments[position]
} }
fun add(index: Int, fragment: Fragment) { fun add(index: Int, fragment: SplitViewFragment) {
fragments.add(index, fragment) fragments.add(index, fragment)
notifyItemChanged(index) notifyItemChanged(index)
} }
fun refreshFragment(index: Int, fragment: Fragment) { fun refreshFragment(index: Int, fragment: SplitViewFragment) {
fragments[index] = fragment fragments[index] = fragment
notifyItemChanged(index) notifyItemChanged(index)
} }
@ -41,6 +41,22 @@ class ViewPager2Adapter(
notifyDataSetChanged() 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 { override fun getItemId(position: Int): Long {
return fragments[position].hashCode().toLong() 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_absoluteHeight="0dp"
tools:layout_conversion_absoluteWidth="411dp" /> 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> </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"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/baseView" android:id="@+id/baseView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/bg_video_view" android:background="@drawable/bg_video_view"
android:outlineProvider="background"> android:outlineProvider="background">
<com.hisharp.gstreamer_player.GStreamerSurfaceView <SurfaceView
android:id="@+id/videoView" android:id="@+id/videoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -22,6 +21,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="fitXY" android:scaleType="fitXY"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -34,16 +34,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:drawableLeft="@drawable/ic_ip_cam_name"
android:drawablePadding="6dp" android:drawablePadding="6dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:text="TextView" android:text=""
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/videoView" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent"
app:drawableStartCompat="@drawable/ic_ip_cam_name" />
<ProgressBar <ProgressBar
android:id="@+id/pbLoading" android:id="@+id/pbLoading"
@ -51,7 +51,7 @@
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="0dp" android:layout_height="0dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/videoView" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1" app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.7.10'
repositories { repositories {
jcenter() mavenCentral()
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
} }
} }
allprojects { plugins {
repositories { id 'com.android.application' version '7.3.1' apply false
jcenter() id 'com.android.library' version '7.3.1' apply false
google() 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) { task clean(type: Delete) {

View File

@ -1,2 +1,21 @@
android.enableJetifier=true # 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 android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=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 distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME

View File

@ -1,11 +1,13 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
compileSdkVersion 32 ndkVersion "21.3.6528147"
compileSdkVersion 33
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 32 targetSdkVersion 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -21,7 +23,10 @@ android {
if (gstRoot == null) 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') 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" targets "gst_player"
@ -65,6 +70,6 @@ afterEvaluate {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
implementation 'androidx.appcompat:appcompat:1.0.0' //implementation 'androidx.appcompat:appcompat:1.6.0'
} }

View File

@ -253,8 +253,7 @@ static void *app_function(void *userdata) {
/* Build pipeline */ /* Build pipeline */
data->pipeline = gst_parse_launch("playbin", &error); data->pipeline = gst_parse_launch("playbin", &error);
if (error) { if (error) {
gchar *message = gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);
g_strdup_printf("Unable to build pipeline: %s", error->message);
g_clear_error(&error); g_clear_error(&error);
set_ui_message(message, data); set_ui_message(message, data);
g_free(message); g_free(message);
@ -280,12 +279,9 @@ static void *app_function(void *userdata) {
g_signal_connect (G_OBJECT(bus), "message::error", (GCallback) error_cb, g_signal_connect (G_OBJECT(bus), "message::error", (GCallback) error_cb,
data); data);
g_signal_connect (G_OBJECT(bus), "message::eos", (GCallback) eos_cb, data); g_signal_connect (G_OBJECT(bus), "message::eos", (GCallback) eos_cb, data);
g_signal_connect (G_OBJECT(bus), "message::state-changed", g_signal_connect (G_OBJECT(bus), "message::state-changed", (GCallback) state_changed_cb, data);
(GCallback) state_changed_cb, data); g_signal_connect (G_OBJECT(bus), "message::buffering", (GCallback) buffering_cb, data);
g_signal_connect (G_OBJECT(bus), "message::buffering", g_signal_connect (G_OBJECT(bus), "message::clock-lost", (GCallback) clock_lost_cb, data);
(GCallback) buffering_cb, data);
g_signal_connect (G_OBJECT(bus), "message::clock-lost",
(GCallback) clock_lost_cb, data);
gst_object_unref(bus); gst_object_unref(bus);
/* Create a GLib Main Loop and set it to run */ /* Create a GLib Main Loop and set it to run */
@ -352,8 +348,8 @@ void gst_native_set_uri(JNIEnv *env, jobject thiz, jstring uri) {
gst_element_set_state(data->pipeline, GST_STATE_READY); gst_element_set_state(data->pipeline, GST_STATE_READY);
g_object_set(data->pipeline, "uri", char_uri, NULL); g_object_set(data->pipeline, "uri", char_uri, NULL);
(*env)->ReleaseStringUTFChars(env, uri, char_uri); (*env)->ReleaseStringUTFChars(env, uri, char_uri);
data->is_live |= g_object_set(data->pipeline, "latency", 250, NULL);
(gst_element_set_state(data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL); data->is_live = (gst_element_set_state(data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL);
} }
/* Set pipeline to PLAYING state */ /* Set pipeline to PLAYING state */
@ -363,7 +359,7 @@ static void gst_native_play(JNIEnv *env, jobject thiz) {
return; return;
GST_DEBUG ("Setting state to PLAYING"); GST_DEBUG ("Setting state to PLAYING");
data->target_state = GST_STATE_PLAYING; data->target_state = GST_STATE_PLAYING;
data->is_live |= (gst_element_set_state(data->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL); data->is_live = (gst_element_set_state(data->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
} }
/* Set pipeline to PAUSED state */ /* Set pipeline to PAUSED state */
@ -373,19 +369,15 @@ static void gst_native_pause(JNIEnv *env, jobject thiz) {
return; return;
GST_DEBUG ("Setting state to PAUSED"); GST_DEBUG ("Setting state to PAUSED");
data->target_state = GST_STATE_PAUSED; data->target_state = GST_STATE_PAUSED;
data->is_live |=(gst_element_set_state(data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL); data->is_live = (gst_element_set_state(data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
} }
/* Static class initializer: retrieve method and field IDs */ /* Static class initializer: retrieve method and field IDs */
static jboolean gst_native_class_init(JNIEnv *env, jclass klass) { static jboolean gst_native_class_init(JNIEnv *env, jclass klass) {
custom_data_field_id = custom_data_field_id = (*env)->GetFieldID(env, klass, "native_custom_data", "J");
(*env)->GetFieldID(env, klass, "native_custom_data", "J"); set_message_method_id = (*env)->GetMethodID(env, klass, "setMessage", "(Ljava/lang/String;)V");
set_message_method_id = on_gstreamer_initialized_method_id = (*env)->GetMethodID(env, klass, "onGStreamerInitialized", "()V");
(*env)->GetMethodID(env, klass, "setMessage", "(Ljava/lang/String;)V"); on_media_size_changed_method_id = (*env)->GetMethodID(env, klass, "onMediaSizeChanged", "(II)V");
on_gstreamer_initialized_method_id =
(*env)->GetMethodID(env, klass, "onGStreamerInitialized", "()V");
on_media_size_changed_method_id =
(*env)->GetMethodID(env, klass, "onMediaSizeChanged", "(II)V");
if (!custom_data_field_id || !set_message_method_id if (!custom_data_field_id || !set_message_method_id
|| !on_gstreamer_initialized_method_id || !on_media_size_changed_method_id) { || !on_gstreamer_initialized_method_id || !on_media_size_changed_method_id) {
@ -414,7 +406,6 @@ static void gst_native_surface_init(JNIEnv *env, jobject thiz, jobject surface)
data->native_window); data->native_window);
if (data->pipeline) { if (data->pipeline) {
gst_video_overlay_expose(GST_VIDEO_OVERLAY (data->pipeline)); gst_video_overlay_expose(GST_VIDEO_OVERLAY (data->pipeline));
gst_video_overlay_expose(GST_VIDEO_OVERLAY (data->pipeline));
} }
return; return;
} else { } else {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical" >
<TextView
android:id="@+id/textview_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dip"
android:gravity="center_horizontal" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dip"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<ImageButton
android:id="@+id/button_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_play"
android:src="@android:drawable/ic_media_play"
android:text="@string/button_play" />
<ImageButton
android:id="@+id/button_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_stop"
android:src="@android:drawable/ic_media_pause"
android:text="@string/button_stop" />
</LinearLayout>
<com.hisharp.gstreamer_player.GStreamerSurfaceView
android:id="@+id/surface_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal" />
</LinearLayout>

View File

@ -1,12 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">GStreamer tutorial 5</string>
<string name="button_play">Play</string>
<string name="button_stop">Stop</string>
<string name="button_select">Select</string>
<string name="button_cancel">Cancel</string>
<string name="filechooser_name">Select a file</string>
<string name="location">Location</string>
<string name="cant_read_folder">folder cannot be read</string>
<string name="icon">Icon</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
package com.hisharp.gstreamer_player; package com.hisharp.gstreamer_player;
public interface GstCallback { public interface GstCallback {
void onStatus(GstLibrary gstInstance, GstStatus gstStatus); void onStatus(GstStatus gstStatus);
void onMessage(GstLibrary gstInstance, String message); void onMessage(String message);
} }

View File

@ -10,21 +10,19 @@ import android.view.SurfaceView;
import org.freedesktop.gstreamer.GStreamer; import org.freedesktop.gstreamer.GStreamer;
public class GstLibrary implements SurfaceHolder.Callback { import java.io.Closeable;
import java.io.IOException;
public class GstLibrary implements Closeable {
final Context mAppContext; final Context mAppContext;
private GstCallback gstCallback; private GstCallback gstCallback;
private GStreamerSurfaceView surfaceView; private String rtspUrl;
private final Handler mainHandler = new Handler(Looper.getMainLooper()); public GstLibrary(Context context) {
private final String rtspUrl;
public GstLibrary(Context context, String rtspUrl) {
this.mAppContext = context.getApplicationContext(); this.mAppContext = context.getApplicationContext();
this.rtspUrl = rtspUrl;
// Initialize GStreamer and warn if it fails // Initialize GStreamer and warn if it fails
try { try {
@ -41,7 +39,6 @@ public class GstLibrary implements SurfaceHolder.Callback {
private native void nativeFinalize(); // Destroy pipeline and shutdown native code 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 nativeSetUri(String uri); // Set the URI of the media to play
private native void nativePlay(); // Set pipeline to PLAYING private native void nativePlay(); // Set pipeline to PLAYING
private native void nativeSetPosition(int milliseconds); // Seek to the indicated position, in milliseconds
private native void nativePause(); // Set pipeline to PAUSED private native void nativePause(); // Set pipeline to PAUSED
private static native boolean nativeClassInit(); // Initialize native class: cache Method IDs for callbacks private static native boolean nativeClassInit(); // Initialize native class: cache Method IDs for callbacks
private native void nativeSurfaceInit(Object surface); // A new surface is available private native void nativeSurfaceInit(Object surface); // A new surface is available
@ -58,18 +55,19 @@ public class GstLibrary implements SurfaceHolder.Callback {
nativePause(); nativePause();
} }
public void release() { public void setRtspUrl(String rtspUrl) {
nativeFinalize(); this.rtspUrl = rtspUrl;
}
public void releaseSurface() {
nativeSurfaceFinalize();
} }
public void setOnStatusChangeListener(GstCallback callback) { public void setOnStatusChangeListener(GstCallback callback) {
this.gstCallback = callback; this.gstCallback = callback;
} }
public void setSurfaceView(GStreamerSurfaceView surfaceView) { public void setSurfaceHolder(SurfaceHolder holder) {
this.surfaceView = surfaceView;
SurfaceHolder holder = this.surfaceView.getHolder();
holder.addCallback(this);
nativeSurfaceInit(holder.getSurface()); nativeSurfaceInit(holder.getSurface());
} }
@ -77,13 +75,17 @@ public class GstLibrary implements SurfaceHolder.Callback {
private void setMessage(final String message) { private void setMessage(final String message) {
if (gstCallback == null) return; if (gstCallback == null) return;
if (message.contains("State changed to PAUSED")) { if (message.contains("State changed to PAUSED")) {
gstCallback.onStatus(this, GstStatus.PAUSE); gstCallback.onStatus(GstStatus.PAUSE);
} else if (message.contains("State changed to PLAYING")) { } else if (message.contains("State changed to PLAYING")) {
gstCallback.onStatus(this, GstStatus.PLAYING); gstCallback.onStatus(GstStatus.PLAYING);
} else if (message.contains("Could not open resource for reading and writing")) { } else if (message.contains("Could not open resource for reading and writing")) {
gstCallback.onStatus(this, GstStatus.ERROR); gstCallback.onStatus(GstStatus.ERROR_WHEN_OPENING);
} else if (message.contains("GStreamer encountered a general supporting library error")) {
gstCallback.onStatus(GstStatus.ERROR);
} else if (message.contains("Unhandled error")) {
gstCallback.onStatus(GstStatus.ERROR);
} }
gstCallback.onMessage(this, message); gstCallback.onMessage(message);
} }
// Called from native code. Native code calls this once it has created its pipeline and // Called from native code. Native code calls this once it has created its pipeline and
@ -92,26 +94,24 @@ public class GstLibrary implements SurfaceHolder.Callback {
Log.i ("GStreamer", "GStreamer initialized:"); Log.i ("GStreamer", "GStreamer initialized:");
if (gstCallback != null) { if (gstCallback != null) {
gstCallback.onStatus(this, GstStatus.READY); gstCallback.onStatus(GstStatus.READY);
} }
// Restore previous playing state // Restore previous playing state
nativeSetUri (rtspUrl); if (rtspUrl != null) {
nativeSetPosition (0); nativeSetUri(rtspUrl);
}
} }
// Called from native code
private void setCurrentPosition(final int position, final int duration) {}
// Called from native code when the size of the media changes or is first detected. // 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. // Inform the video surface about the new size and recalculate the layout.
private void onMediaSizeChanged (int width, int height) { private void onMediaSizeChanged (int width, int height) {
Log.i ("GStreamer", "Media size changed to " + width + "x" + height); Log.i ("GStreamer", "Media size changed to " + width + "x" + height);
mainHandler.post(() -> { /*mainHandler.post(() -> {
surfaceView.media_width = width; surfaceView.media_width = width;
surfaceView.media_height = height; surfaceView.media_height = height;
surfaceView.requestLayout(); surfaceView.requestLayout();
}); });*/
} }
static { static {
@ -120,19 +120,12 @@ public class GstLibrary implements SurfaceHolder.Callback {
nativeClassInit(); nativeClassInit();
} }
public void surfaceChanged(SurfaceHolder holder, int format, int width, @Override
int height) { public void close() throws IOException {
Log.d("GStreamer", "Surface changed to format " + format + " width " try {
+ width + " height " + height); nativeFinalize();
nativeSurfaceInit (holder.getSurface()); } catch (Exception e) {
e.printStackTrace();
} }
public void surfaceCreated(SurfaceHolder holder) {
Log.d("GStreamer", "Surface created: " + holder.getSurface());
}
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d("GStreamer", "Surface destroyed");
nativeSurfaceFinalize();
} }
} }

View File

@ -4,5 +4,6 @@ public enum GstStatus {
READY, READY,
PAUSE, PAUSE,
PLAYING, PLAYING,
ERROR ERROR,
ERROR_WHEN_OPENING
} }

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 ':gstreamer_player'
include ':app' include ':app'