Compare commits

..

58 Commits

Author SHA1 Message Date
Raymond Yang 4a8fb55fc8 拿掉LOG 2023-05-22 13:37:27 +08:00
Raymond Yang fdb88fc47a 更新Gradle Plugin 2023-05-22 13:36:56 +08:00
Raymond Yang ba12fd3856 1.優化for loop
2.優化VideoView get/set
2023-05-22 10:56:24 +08:00
Raymond Yang 8449706b64 1.加入TextureView支援
2.將取得畫面寬高的計算方式改成不使用baseView.post
2023-05-22 09:43:53 +08:00
Raymond Yang 02a26445be 同步HiSharpDX的程式碼 2023-05-19 14:02:47 +08:00
Raymond Yang 0021b30842 將viewpager改成scrollview 2023-03-27 16:52:08 +08:00
Raymond Yang 992dd860d9 補上UI文件 2023-03-09 18:00:13 +08:00
Raymond Yang 6378f4d80f 1.將重試功能改成VideoView內部呼叫
2.加上16分割
2023-03-09 17:52:39 +08:00
Raymond Yang f7aa9f74d2 VideoViewAdapter.kt加上對所有的SplitViewFragment.kt下指令的功能 2023-03-09 15:02:27 +08:00
Raymond Yang 0ffafcbaf7 VideoView.kt加上重試時的狀態 2023-03-09 14:59:52 +08:00
Raymond Yang 07afc01354 releaseSurface、pause加上isInit判斷 2023-03-09 14:48:25 +08:00
Raymond Yang d77fc98280 調整releaseSurface的呼叫時機 2023-03-09 09:06:25 +08:00
Raymond Yang 2897cdca0d 加入獨立監控假畫面 2023-03-08 17:57:29 +08:00
Raymond Yang c9ca4156aa 在surfaceDestroyed裡面呼叫pause 2023-03-08 13:37:26 +08:00
Raymond Yang 975e2eba79 GstLibrary改成kotlin 2023-03-08 11:36:48 +08:00
Raymond Yang 20949aa24d Rename .java to .kt 2023-03-08 11:36:47 +08:00
Raymond Yang e3c958c632 加入初始化旗標 2023-03-07 11:55:54 +08:00
Raymond Yang 48db07290c 調整onResume 2023-03-06 17:08:20 +08:00
Raymond Yang 1b80ae3349 更新Gradle Plugin 2023-03-06 10:11:18 +08:00
Raymond Yang ed8f2a7c0f 使用暴力方式解決閃退問題:加大ViewPager可以容許的page數量 2023-03-06 09:31:20 +08:00
Raymond Yang d8d351831c 加上exitProcess 2023-02-10 10:36:51 +08:00
Raymond Yang 77df0eb8a5 修正GstLibrary的LOG TAG名稱 2023-02-10 09:23:00 +08:00
Raymond Yang 0dca3ce9e8 C lib加上tag區分 2023-02-09 16:16:49 +08:00
Raymond Yang 363bfdbfbd 調整delay時間算法 2023-02-09 11:27:55 +08:00
Raymond Yang dc1da2449b 當MainActivity退出時,呼叫GStreamer的finalize 2023-02-08 14:41:33 +08:00
Raymond Yang 53763bfa34 將延遲參數常數化 2023-02-08 10:28:40 +08:00
Raymond Yang 4cf5ae5094 補上VideoViewAdapter 2023-02-07 17:01:44 +08:00
Raymond Yang ced178513b 將滑動控制加上播放/暫停 2023-02-07 17:01:07 +08:00
Raymond Yang 88954a6599 GStreamer更新到1.22.0 2023-02-07 16:02:56 +08:00
Raymond Yang df105dd518 將stopRetryCount函數加上removeCallbacks,停止重連功能 2023-02-07 13:45:41 +08:00
Raymond Yang 2f4e3659f9 更新Kotlin plugin 2023-02-06 14:03:39 +08:00
Raymond Yang 2e8d8a6381 將分割設定值存入Shared Preference內 2023-02-06 10:55:45 +08:00
Raymond Yang 87f731168f 如果目前的分割模式是點選的類型,則不刷新分割模式 2023-02-03 17:46:27 +08:00
Raymond Yang c7d0785cef 將延遲時間改成500毫秒,再繼續觀察 2023-02-03 17:40:22 +08:00
Raymond Yang a37e79d841 加上註解 2023-02-03 12:24:43 +08:00
Raymond Yang 4e09a449ca 將Device修改成HiSharpDX的資料結構 2023-02-03 12:07:06 +08:00
Raymond Yang 85218c443f 將跳轉加上延遲 2023-02-02 17:13:20 +08:00
Raymond Yang b4346f142b 將跳轉加上coroutine 2023-02-02 12:20:35 +08:00
Raymond Yang 9fc8673715 將停止播放函數改成手動呼叫,測試看看是否還會容易閃退 2023-02-02 10:20:00 +08:00
Raymond Yang de152da749 在surface create時,呼叫gstLibrary.setSurfaceHolder(holder) 2023-02-01 14:48:37 +08:00
Raymond Yang db673d3553 修正log內容 2023-02-01 13:35:46 +08:00
Raymond Yang 792ebc70c2 將VideoView的retryCount封裝 2023-02-01 10:38:22 +08:00
Raymond Yang 90344d14b0 VideoView加上預設狀態 2023-02-01 09:13:01 +08:00
Raymond Yang c3f8366475 加入點擊 2023-01-31 11:56:05 +08:00
Raymond Yang 15475923cb 整理專案結構 2023-01-31 09:49:26 +08:00
Raymond Yang a29f0e7bbf 加入重連機制 2023-01-30 16:16:20 +08:00
Raymond Yang ac15111ab3 加入重連機制 2023-01-30 16:15:34 +08:00
Raymond Yang cdd2c3775d 狀態加上緩衝中 2023-01-30 11:56:20 +08:00
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
226 changed files with 3673 additions and 677 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="16" />
<bytecodeTargetLevel target="17" />
</component>
</project>
+1 -1
View File
@@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="semeru-16" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
+1 -2
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
@@ -16,5 +15,5 @@
</option>
</component>
<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_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK" />
</project>
+30 -17
View File
@@ -1,15 +1,19 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.parcelize'
id 'org.jetbrains.kotlin.kapt'
}
apply plugin: 'kotlin-android'
android {
compileSdk 32
namespace 'com.ray650128.gstreamer_demo_app'
ndkVersion "21.3.6528147"
compileSdk 33
defaultConfig {
applicationId "com.ray650128.gstreamer_demo_app"
minSdk 26
targetSdk 32
targetSdk 33
versionCode 1
versionName "1.0"
@@ -23,26 +27,35 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
lint {
abortOnError false
checkReleaseBuilds false
}
viewBinding.enabled = true
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(path: ':gstreamer_player')
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'androidx.core:core-ktx:1.10.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation project(path: ':gstreamer_player')
// Android Jetpack lib
implementation("androidx.fragment:fragment-ktx:1.5.7")
implementation("androidx.activity:activity-ktx:1.7.1")
}
repositories {
mavenCentral()
}
+9 -4
View File
@@ -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,12 @@
package com.ray650128.gstreamer_demo_app
object Constants {
//region Split mode
const val SPLIT_MODE_SIXTEEN = 16
const val SPLIT_MODE_NINE = 9
const val SPLIT_MODE_FOUR = 4
const val SPLIT_MODE_SINGLE = 1
//endregion
const val CONF_DELAY_BASE_MILLIS = 1000L
}
@@ -0,0 +1,94 @@
package com.ray650128.gstreamer_demo_app
import android.content.Context
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowManager
/**
* 螢幕參數工具類別
* @author Raymond Yang
*/
class DisplayUtils(private var context: Context) {
private val TAG = DisplayUtils::class.java.simpleName
/**
* 取得螢幕寬度
* @return 螢幕寬度值
*/
fun getScreenWidth(): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
windowMetrics.bounds.width()
} else {
val metric = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metric)
metric.widthPixels
}
}
/**
* 取得螢幕高度
* @return 螢幕高度值
*/
fun getScreenHeight(): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
windowMetrics.bounds.height()
} else {
val metric = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metric)
metric.heightPixels
}
}
/**
* 取得狀態列高度
* @return 狀態列高度值
*/
fun getStatusBarHeight(): Int {
var result = 0
val resourceId: Int = context.resources.getIdentifier(
"status_bar_height",
"dimen",
"android"
)
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
return result
}
/**
* 取得導航列高度
* @return 導航列高度值
*/
fun getNavigationBarHeight(): Int {
val resources = context.resources
val resourceId: Int = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else 0
}
/**
* 取得視窗範圍高度
* @return 視窗範圍高度值
*/
fun getWindowHeight(): Int {
val screen = getScreenHeight()
val statusBar = getStatusBarHeight()
val navBar = getNavigationBarHeight()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
screen - statusBar - navBar
} else {
screen
}
}
}
@@ -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
}
}
}
@@ -1,80 +0,0 @@
package com.ray650128.gstreamer_demo_app
import android.annotation.SuppressLint
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import com.hisharp.gstreamer_player.GstCallback
import com.ray650128.gstreamer_demo_app.MainActivity
import com.hisharp.gstreamer_player.GstLibrary
import com.hisharp.gstreamer_player.GStreamerSurfaceView
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.WindowManager
import com.hisharp.gstreamer_player.GstStatus
import com.ray650128.gstreamer_demo_app.databinding.ActivityMainBinding
import java.util.ArrayList
import java.util.function.Consumer
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var splitVideoViewAdapter: ViewPager2Adapter
@SuppressLint("AuthLeak")
private val defaultMediaUris = arrayListOf(
arrayListOf(
"rtsp://admin:admin@211.23.78.226:8574/v02",
"rtsp://admin:admin@211.23.78.226:8575/v02",
"rtsp://admin:admin@192.168.0.77:554/media/video2",
"rtsp://admin:123456@192.168.0.80:554/profile2",
"rtsp://admin:123456@192.168.0.83:554/profile2",
"rtsp://admin:123456@192.168.0.84:554/profile2",
"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?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
splitVideoViewAdapter = ViewPager2Adapter(supportFragmentManager, lifecycle)
binding.viewPager.apply {
adapter = splitVideoViewAdapter
offscreenPageLimit = 1
}
reloadVideoViews()
}
private fun reloadVideoViews() {
splitVideoViewAdapter.clear()
for (index in defaultMediaUris.indices) {
val gridFragment = GridVideoFragment.newInstance(index, 9, defaultMediaUris[index])
splitVideoViewAdapter.add(index, gridFragment)
}
}
companion object {
private val TAG = MainActivity::class.java.simpleName
}
}
@@ -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, Constants.SPLIT_MODE_SINGLE)
set(value) = sharedPreferences.edit().putInt(LAST_SPLIT_MODE, value).apply()
}
@@ -1,58 +0,0 @@
package com.ray650128.gstreamer_demo_app
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.hisharp.gstreamer_player.GStreamerSurfaceView
import com.ray650128.gstreamer_demo_app.databinding.ItemVideoViewBinding
class VideoView : ConstraintLayout {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet, intRes: Int) : super(context, attributeSet, intRes) {
initView(context)
}
private lateinit var view: ItemVideoViewBinding
//private var data: Device? = null
var isPlaying: Boolean = false
set(value) {
view.imgPause.isVisible = !value
field = value
}
val videoView: GStreamerSurfaceView by lazy { view.videoView }
private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context)
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true
}
/*fun setData(device: Device?) {
if (device == null) {
view.textDeviceName.isVisible = false
return
}
this.data = device
view.textDeviceName.text = if (device.channelId == -1) device.deviceName else device.channelName
view.textDeviceName.isVisible = true
Log.d(TAG, "Set device to: $device")
}*/
companion object {
private val TAG = VideoView::class.java.simpleName
}
}
@@ -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
}
@@ -0,0 +1,17 @@
package com.ray650128.gstreamer_demo_app.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Device(
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
@@ -0,0 +1,484 @@
package com.ray650128.gstreamer_demo_app.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/**
* A view group that allows users to switch between multiple screens (layouts) in the same way as
* the Android home screen (Launcher application).
* <p>
* You can add and remove views using the normal methods {@link ViewGroup#addView(View)},
* {@link ViewGroup#removeView(View)} etc. You may want to listen for updates by calling
* {@link HorizontalPager#setOnScreenSwitchListener(OnScreenSwitchListener)} in order to perform
* operations once a new screen has been selected.
*
* Modifications from original version (ysamlan): Animate argument in setCurrentScreen and duration
* in snapToScreen; onInterceptTouchEvent handling to support nesting a vertical Scrollview inside
* the RealViewSwitcher; allowing snapping to a view even during an ongoing scroll; snap to
* next/prev view on 25% scroll change; density-independent swipe sensitivity; width-independent
* pager animation durations on scrolling to properly handle large screens without excessively
* long animations.
*
* Other modifications:
* (aveyD) Handle orientation changes properly and fully snap to the right position.
*
* @author Marc Reichelt, <a href="http://www.marcreichelt.de/">http://www.marcreichelt.de/</a>
* @version 0.1.0
*/
public final class HorizontalPager extends ViewGroup {
/*
* How long to animate between screens when programmatically setting with setCurrentScreen using
* the animate parameter
*/
private static final int ANIMATION_SCREEN_SET_DURATION_MILLIS = 500;
// What fraction (1/x) of the screen the user must swipe to indicate a page change
private static final int FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE = 4;
private static final int INVALID_SCREEN = -1;
/*
* Velocity of a swipe (in density-independent pixels per second) to force a swipe to the
* next/previous screen. Adjusted into mDensityAdjustedSnapVelocity on init.
*/
private static final int SNAP_VELOCITY_DIP_PER_SECOND = 600;
// Argument to getVelocity for units to give pixels per second (1 = pixels per millisecond).
private static final int VELOCITY_UNIT_PIXELS_PER_SECOND = 1000;
private static final int TOUCH_STATE_REST = 0;
private static final int TOUCH_STATE_HORIZONTAL_SCROLLING = 1;
private static final int TOUCH_STATE_VERTICAL_SCROLLING = -1;
private int mCurrentScreen;
private int mDensityAdjustedSnapVelocity;
private boolean mFirstLayout = true;
private float mLastMotionX;
private float mLastMotionY;
private OnScreenSwitchListener mOnScreenSwitchListener;
private int mMaximumVelocity;
private int mNextScreen = INVALID_SCREEN;
private Scroller mScroller;
private int mTouchSlop;
private int mTouchState = TOUCH_STATE_REST;
private VelocityTracker mVelocityTracker;
private int mLastSeenLayoutWidth = -1;
/**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public HorizontalPager(final Context context) {
super(context);
init();
}
/**
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file, supplying attributes
* that were specified in the XML file. This version uses a default style of
* 0, so the only attribute values applied are those in the Context's Theme
* and the given AttributeSet.
*
* <p>
* The method onFinishInflate() will be called after all children have been
* added.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @see #View(Context, AttributeSet, int)
*/
public HorizontalPager(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Sets up the scroller and touch/fling sensitivity parameters for the pager.
*/
private void init() {
mScroller = new Scroller(getContext());
// Calculate the density-dependent snap velocity in pixels
DisplayMetrics displayMetrics = new DisplayMetrics();
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
.getMetrics(displayMetrics);
mDensityAdjustedSnapVelocity =
(int) (displayMetrics.density * SNAP_VELOCITY_DIP_PER_SECOND);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("ViewSwitcher can only be used in EXACTLY mode.");
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("ViewSwitcher can only be used in EXACTLY mode.");
}
// The children are given the same width and height as the workspace
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
if (mFirstLayout) {
scrollTo(mCurrentScreen * width, 0);
mFirstLayout = false;
}
else if (width != mLastSeenLayoutWidth) { // Width has changed
/*
* Recalculate the width and scroll to the right position to be sure we're in the right
* place in the event that we had a rotation that didn't result in an activity restart
* (code by aveyD). Without this you can end up between two pages after a rotation.
*/
Display display =
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
int displayWidth = display.getWidth();
mNextScreen = Math.max(0, Math.min(getCurrentScreen(), getChildCount() - 1));
final int newX = mNextScreen * displayWidth;
final int delta = newX - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0, 0);
}
mLastSeenLayoutWidth = width;
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r,
final int b) {
int childLeft = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
final int childWidth = child.getMeasuredWidth();
child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
childLeft += childWidth;
}
}
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
/*
* By Yoni Samlan: Modified onInterceptTouchEvent based on standard ScrollView's
* onIntercept. The logic is designed to support a nested vertically scrolling view inside
* this one; once a scroll registers for X-wise scrolling, handle it in this view and don't
* let the children, but once a scroll registers for y-wise scrolling, let the children
* handle it exclusively.
*/
final int action = ev.getAction();
boolean intercept = false;
switch (action) {
case MotionEvent.ACTION_MOVE:
/*
* If we're in a horizontal scroll event, take it (intercept further events). But if
* we're mid-vertical-scroll, don't even try; let the children deal with it. If we
* haven't found a scroll event yet, check for one.
*/
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
/*
* We've already started a horizontal scroll; set intercept to true so we can
* take the remainder of all touch events in onTouchEvent.
*/
intercept = true;
} else if (mTouchState == TOUCH_STATE_VERTICAL_SCROLLING) {
// Let children handle the events for the duration of the scroll event.
intercept = false;
} else { // We haven't picked up a scroll event yet; check for one.
/*
* If we detected a horizontal scroll event, start stealing touch events (mark
* as scrolling). Otherwise, see if we had a vertical scroll event -- if so, let
* the children handle it and don't look to intercept again until the motion is
* done.
*/
final float x = ev.getX();
final int xDiff = (int) Math.abs(x - mLastMotionX);
boolean xMoved = xDiff > mTouchSlop;
if (xMoved) {
// Scroll if the user moved far enough along the X axis
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
mLastMotionX = x;
}
final float y = ev.getY();
final int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > mTouchSlop;
if (yMoved) {
mTouchState = TOUCH_STATE_VERTICAL_SCROLLING;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Release the drag.
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_DOWN:
/*
* No motion yet, but register the coordinates so we can check for intercept at the
* next MOVE event.
*/
mLastMotionY = ev.getY();
mLastMotionX = ev.getX();
break;
default:
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(final MotionEvent ev) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished will be false if
* being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionX = x;
if (mScroller.isFinished()) {
mTouchState = TOUCH_STATE_REST;
} else {
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
}
break;
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(x - mLastMotionX);
boolean xMoved = xDiff > mTouchSlop;
if (xMoved) {
// Scroll if the user moved far enough along the X axis
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
}
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
// Scroll to follow the motion event
final int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
final int scrollX = getScrollX();
if (deltaX < 0) {
if (scrollX > 0) {
scrollBy(Math.max(-scrollX, deltaX), 0);
}
} else if (deltaX > 0) {
final int availableToScroll =
getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX), 0);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(VELOCITY_UNIT_PIXELS_PER_SECOND,
mMaximumVelocity);
int velocityX = (int) velocityTracker.getXVelocity();
if (velocityX > mDensityAdjustedSnapVelocity && mCurrentScreen > 0) {
// Fling hard enough to move left
snapToScreen(mCurrentScreen - 1);
} else if (velocityX < -mDensityAdjustedSnapVelocity
&& mCurrentScreen < getChildCount() - 1) {
// Fling hard enough to move right
snapToScreen(mCurrentScreen + 1);
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
default:
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
} else if (mNextScreen != INVALID_SCREEN) {
mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
// Notify observer about screen change
if (mOnScreenSwitchListener != null) {
mOnScreenSwitchListener.onScreenSwitched(mCurrentScreen);
}
mNextScreen = INVALID_SCREEN;
}
}
/**
* Returns the index of the currently displayed screen.
*
* @return The index of the currently displayed screen.
*/
public int getCurrentScreen() {
return mCurrentScreen;
}
/**
* Sets the current screen.
*
* @param currentScreen The new screen.
* @param animate True to smoothly scroll to the screen, false to snap instantly
*/
public void setCurrentScreen(final int currentScreen, final boolean animate) {
mCurrentScreen = Math.max(0, Math.min(currentScreen, getChildCount() - 1));
if (animate) {
snapToScreen(currentScreen, ANIMATION_SCREEN_SET_DURATION_MILLIS);
} else {
scrollTo(mCurrentScreen * getWidth(), 0);
}
invalidate();
}
/**
* Sets the {@link OnScreenSwitchListener}.
*
* @param onScreenSwitchListener The listener for switch events.
*/
public void setOnScreenSwitchListener(final OnScreenSwitchListener onScreenSwitchListener) {
mOnScreenSwitchListener = onScreenSwitchListener;
}
/**
* Snaps to the screen we think the user wants (the current screen for very small movements; the
* next/prev screen for bigger movements).
*/
private void snapToDestination() {
final int screenWidth = getWidth();
int scrollX = getScrollX();
int whichScreen = mCurrentScreen;
int deltaX = scrollX - (screenWidth * mCurrentScreen);
// Check if they want to go to the prev. screen
if ((deltaX < 0) && mCurrentScreen != 0
&& ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < -deltaX)) {
whichScreen--;
// Check if they want to go to the next screen
} else if ((deltaX > 0) && (mCurrentScreen + 1 != getChildCount())
&& ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < deltaX)) {
whichScreen++;
}
snapToScreen(whichScreen);
}
/**
* Snap to a specific screen, animating automatically for a duration proportional to the
* distance left to scroll.
*
* @param whichScreen Screen to snap to
*/
private void snapToScreen(final int whichScreen) {
snapToScreen(whichScreen, -1);
}
/**
* Snaps to a specific screen, animating for a specific amount of time to get there.
*
* @param whichScreen Screen to snap to
* @param duration -1 to automatically time it based on scroll distance; a positive number to
* make the scroll take an exact duration.
*/
private void snapToScreen(final int whichScreen, final int duration) {
/*
* Modified by Yoni Samlan: Allow new snapping even during an ongoing scroll animation. This
* is intended to make HorizontalPager work as expected when used in conjunction with a
* RadioGroup used as "tabbed" controls. Also, make the animation take a percentage of our
* normal animation time, depending how far they've already scrolled.
*/
mNextScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
final int newX = mNextScreen * getWidth();
final int delta = newX - getScrollX();
if (duration < 0) {
// E.g. if they've scrolled 80% of the way, only animation for 20% of the duration
mScroller.startScroll(getScrollX(), 0, delta, 0, (int) (Math.abs(delta)
/ (float) getWidth() * ANIMATION_SCREEN_SET_DURATION_MILLIS));
} else {
mScroller.startScroll(getScrollX(), 0, delta, 0, duration);
}
invalidate();
}
/**
* Listener for the event that the HorizontalPager switches to a new view.
*/
public static interface OnScreenSwitchListener {
/**
* Notifies listeners about the new screen. Runs after the animation completed.
*
* @param screen The new screen index.
*/
void onScreenSwitched(int screen);
}
}
@@ -0,0 +1,134 @@
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.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
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
/**
* MainActivity.kt
* 應用程式主畫面
*/
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
private val mContext: Context by lazy { this }
private var splitMode = 1
private var videos: List<List<Device>>? = null
private var videoPageList: ArrayList<SplitViewFragment> = arrayListOf()
override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initContentView()
initObservers()
}
override fun onBackPressed() {
super.onBackPressed()
finish()
}
private fun initContentView() = binding.apply {
//region Content area
button.setOnClickListener {
if (splitMode == Constants.SPLIT_MODE_SINGLE) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_SINGLE)
Log.e(TAG, "+++ split style: 1")
}
button2.setOnClickListener {
if (splitMode == Constants.SPLIT_MODE_FOUR) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_FOUR)
Log.e(TAG, "+++ split style: 4")
}
button3.setOnClickListener {
if (splitMode == Constants.SPLIT_MODE_NINE) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_NINE)
Log.e(TAG, "+++ split style: 9")
}
button4.setOnClickListener {
if (splitMode == Constants.SPLIT_MODE_SIXTEEN) return@setOnClickListener
viewModel.setSplitMode(Constants.SPLIT_MODE_SIXTEEN)
Log.e(TAG, "+++ split style: 16")
}
//endregion
}
private fun initObservers() {
viewModel.splitMode.observe(this) {
splitMode = it
Log.e(TAG, "Split count: $it")
}
viewModel.cameraList.observe(this) { list ->
this.videos = list
reloadVideoViews(this.videos)
}
}
private fun reloadVideoViews(list: List<List<Device>>?) {
for (videoPage in videoPageList) {
supportFragmentManager.commit {
remove(videoPage)
}
}
binding.viewPager.removeAllViews()
videoPageList.clear()
// 如果群組內沒有裝置,則顯示底圖
if (list.isNullOrEmpty()) {
binding.viewPager.setBackgroundResource(R.drawable.bg_not_in_playing)
} else {
binding.viewPager.setBackgroundColor(Color.TRANSPARENT)
for (i in list.indices) {
val splitFragment = SplitViewFragment.newInstance(
pageNumber = i,
splitMode = splitMode,
pageData = ArrayList(list[i])
)
videoPageList.add(i, splitFragment)
val frameLayout = FrameLayout(mContext).apply {
id = View.generateViewId()
}
binding.viewPager.addView(frameLayout)
supportFragmentManager.commit {
add(frameLayout.id, splitFragment, "$i")
}
}
}
}
companion object {
private val TAG = MainActivity::class.java.simpleName
}
}
@@ -0,0 +1,140 @@
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
}
}
@@ -0,0 +1,256 @@
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 android.widget.ImageView
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import com.ray650128.gstreamer_demo_app.Constants
import com.ray650128.gstreamer_demo_app.DisplayUtils
import com.ray650128.gstreamer_demo_app.R
import com.ray650128.gstreamer_demo_app.databinding.FragmentSplitViewBinding
import com.ray650128.gstreamer_demo_app.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
import kotlinx.coroutines.launch
import kotlin.math.sqrt
class SplitViewFragment : Fragment() {
private var mPageNum: Int = 0
private var splitMode = Constants.SPLIT_MODE_SINGLE
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() {
stopAll()
super.onPause()
Log.d("${TAG}_$mPageNum", "onPause()")
}
override fun onResume() {
super.onResume()
playAll()
Log.d("${TAG}_$mPageNum", "onResume()")
}
override fun onDestroy() {
//destroyAll()
stopAll()
Log.d("${TAG}_$mPageNum", "onDestroy()")
super.onDestroy()
}
private fun initView() {
val displayUtil = DisplayUtils(requireContext())
// 生成 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
val cellWidth: Int
val cellHeight: Int
when (splitMode) {
Constants.SPLIT_MODE_SINGLE -> {
cellWidth = (displayUtil.getScreenWidth() / maxRow)
cellHeight = (cellWidth * 0.5625).toInt()
}
else -> {
cellWidth = (displayUtil.getScreenWidth() / maxRow) - maxRow.dp
cellHeight = (cellWidth * 0.5625).toInt() - maxCol.dp
}
}
for (col in 0 until maxCol) {
for (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
width = cellWidth
height = cellHeight
// 調整間距
when (splitMode) {
Constants.SPLIT_MODE_FOUR -> {
when (col) {
0 -> bottomMargin = 2.dp
1 -> topMargin = 2.dp
}
when (row) {
0 -> marginEnd = 2.dp
1 -> marginStart = 2.dp
}
}
Constants.SPLIT_MODE_NINE -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginEnd = 4.dp
marginStart = 4.dp
}
}
Constants.SPLIT_MODE_SIXTEEN -> {
if (col == 1) {
topMargin = 4.dp
bottomMargin = 2.dp
}
if (col == 2) {
topMargin = 2.dp
bottomMargin = 4.dp
}
if (row == 1) {
marginStart = 4.dp
marginEnd = 2.dp
}
if (row == 2) {
marginStart = 2.dp
marginEnd = 4.dp
}
}
}
}
baseView.addView(videoView, layoutParam)
videoViews.add(videoView)
}
}
}
if (isClickable) {
videoViews.forEach { videoView ->
videoView.setOnClickListener {
if (!videoView.isPlaying) {
return@setOnClickListener
}
stopAll()
val item = videoView.data
val bundle = Bundle().apply {
//putInt(MonitoringActivity.BUNDLE_DEVICE_ID, item.id)
//putInt(MonitoringActivity.BUNDLE_CHANNEL_ID, item.channelId)
putParcelable(MonitoringActivity.BUNDLE_DEVICE, item)
}
val intent = Intent(requireContext(), MonitoringActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
//gotoActivity(MonitoringActivity::class.java, bundle)*/
//Log.d("${TAG}_$mPageNum", "check: $item")
}
}
}
setAllUrl()
}
private fun setAllUrl() {
for (index in data.indices) {
videoViews[index].data = data[index]
videoViews[index].setTextVisible(
(splitMode != Constants.SPLIT_MODE_NINE && splitMode != Constants.SPLIT_MODE_SIXTEEN)
)
}
}
fun playAll() {
videoViews.forEach { videoView ->
if (videoView.isReady) {
videoView.resetRetryCount()
videoView.play()
}
}
}
fun stopAll() {
videoViews.forEach { videoView ->
videoView.stopRetryCount()
if (videoView.isPlaying || !videoView.isLoading) {
videoView.pause()
}
}
}
fun destroyAll() {
videoViews.forEach { videoView ->
//videoView.destroy()
videoView.destroySurface()
}
}
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
}
}
}
@@ -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()
}
}
@@ -0,0 +1,219 @@
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
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, GstCallback {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initView(context)
}
constructor(context: Context, attributeSet: AttributeSet, intRes: Int) : super(context, attributeSet, intRes) {
initView(context)
}
private lateinit var view: ItemVideoViewBinding
var streamType: Int = MAIN_STREAM
var data: Device? = null
set(value) {
field = value
if (field == null) {
view.textDeviceName.isVisible = false
isPlaying = false
isLoading = false
return
}
this.tag = field?.deviceName
view.textDeviceName.text = field?.deviceName
view.textDeviceName.isVisible = true
val rtspUrl = this.data?.getStreamPath(streamType) ?: return // 如果 null 就不指派給 Gstreamer 了
gstLibrary.setTag(this.data!!.deviceName)
gstLibrary.setRtspUrl(rtspUrl)
Log.d("${TAG}_$tag", "Set device to: $field, rtspUrl = $rtspUrl")
}
var isReady: Boolean = false
var isLoading: Boolean = false
set(value) {
view.pbLoading.isVisible = if (retryCount in 1..5) {
true
} else {
value
}
field = value
}
var isPlaying: Boolean = false
set(value) {
view.imgPause.isVisible = !value
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: TextureView by lazy { view.videoView }
private lateinit var gstLibrary: GstLibrary
private val mHandler: MyHandler by lazy {
MyHandler(Looper.getMainLooper())
}
private fun initView(context: Context) {
val layoutInflater = LayoutInflater.from(context)
view = ItemVideoViewBinding.inflate(layoutInflater, this, true)
view.baseView.clipToOutline = true
gstLibrary = GstLibrary(context)
gstLibrary.setTextureView(videoView)
gstLibrary.setOnStatusChangeListener(this)
//videoView.holder.addCallback(this)
//gstLibrary.setSurfaceHolder(videoView.holder)
// View 預設狀態
view.textDeviceName.isVisible = false
isPlaying = false
isLoading = false
view.btnRetry.setOnClickListener {
it.isVisible = false
resetRetryCount()
play()
}
}
fun setTextVisible(isVisible: Boolean) {
view.textDeviceName.isVisible = isVisible
}
fun play() {
if (data == null) return
if (this::gstLibrary.isInitialized) {
gstLibrary.play()
}
}
fun pause() {
if (data == null) return
isPlaying = false
if (this::gstLibrary.isInitialized) {
gstLibrary.pause()
}
}
fun destroySurface() {
if (this::gstLibrary.isInitialized) {
gstLibrary.releaseSurface()
}
}
fun destroy() {
if (this::gstLibrary.isInitialized) {
gstLibrary.close()
}
}
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.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")
}
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
resetRetryCount()
}
MSG_BUFFERING -> {
isLoading = true
isPlaying = true
}
MSG_ERROR -> {
if (retryCount != RETRY_OFF && retryCount in 0 until 5) {
mHandler.post(retryRunnable)
retryCount++
isLoading = true
view.btnRetry.isVisible = false
} else {
stopRetryCount()
view.btnRetry.isVisible = true
isLoading = false
Log.e("${TAG}_$tag", "Retry count = 5, stopped retry...")
}
}
}
}
}
companion object {
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
private const val MSG_BUFFERING = 3
private const val MSG_ERROR = 4
private const val RETRY_OFF = 999
}
}
@@ -1,31 +1,31 @@
package com.ray650128.gstreamer_demo_app
package com.ray650128.gstreamer_demo_app.ui.mainScreen
import android.annotation.SuppressLint
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
class ViewPager2Adapter(
class VideoViewAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {
private var fragments: MutableList<Fragment> = arrayListOf()
private var fragments: MutableList<SplitViewFragment> = arrayListOf()
override fun getItemCount(): Int {
return fragments.size
}
override fun createFragment(position: Int): Fragment {
override fun createFragment(position: Int): SplitViewFragment {
return fragments[position]
}
fun add(index: Int, fragment: Fragment) {
fun add(index: Int, fragment: SplitViewFragment) {
fragments.add(index, fragment)
notifyItemChanged(index)
}
fun refreshFragment(index: Int, fragment: Fragment) {
fun refreshFragment(index: Int, fragment: SplitViewFragment) {
fragments[index] = fragment
notifyItemChanged(index)
}
@@ -41,6 +41,42 @@ class ViewPager2Adapter(
notifyDataSetChanged()
}
fun pause() {
for (i in fragments.indices) {
pause(i)
}
}
fun pause(index: Int) {
pause(fragments[index])
}
private fun pause(fragment: SplitViewFragment) {
fragment.stopAll()
}
fun play(index: Int) {
play(fragments[index])
}
private fun play(fragment: SplitViewFragment) {
fragment.playAll()
}
fun destroy() {
for (i in fragments.indices) {
destroy(i)
}
}
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()
}
@@ -0,0 +1,39 @@
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.R
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)
initToolbar()
}
private fun initToolbar() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_back_arrow)
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
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,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="#9b9b9b" />
<item android:state_enabled="true" android:color="#FF000000" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Some files were not shown because too many files have changed in this diff Show More