diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index ca16a99..ea2981d 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,13 @@
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 751c58f..978e094 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -47,6 +47,7 @@ dependencies {
implementation(libs.androidx.constraintlayout)
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation(libs.firebase.crashlytics.buildtools)
+ implementation(libs.androidx.ui.test)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -76,4 +77,10 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.16.0")
// ZXing 二维码扫描库
implementation("com.google.zxing:core:3.5.2")
+ // ExoPlayer 核心库
+ implementation("androidx.media3:media3-exoplayer:1.9.0")
+ // ExoPlayer UI组件库,提供PlayerView等
+ implementation("androidx.media3:media3-ui:1.9.0")
+ // ExoPlayer 通用库
+ implementation("androidx.media3:media3-common:1.9.0")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a0a16f1..a119749 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,7 +25,13 @@
android:theme="@style/Theme.DefenseApplication">
+ android:exported="true">
+
+
+
+
+
+
@@ -46,13 +52,7 @@
android:exported="false" />
-
-
-
-
-
-
+ android:exported="false"/>
diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/LiveVideoActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/LiveVideoActivity.kt
index 958b1b7..f1aa315 100644
--- a/app/src/main/java/com/example/defenseapplication/liveVideo/LiveVideoActivity.kt
+++ b/app/src/main/java/com/example/defenseapplication/liveVideo/LiveVideoActivity.kt
@@ -1,80 +1,251 @@
package com.example.defenseapplication.liveVideo
-import android.content.Context
+import android.content.pm.ActivityInfo
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.net.Uri
import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
+import android.view.SurfaceHolder
import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
import android.widget.Toast
+import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.ui.test.isEnabled
import com.example.defenseapplication.databinding.LiveVideoActivityBinding
+import java.io.IOException
-class LiveVideoActivity : AppCompatActivity() {
+class LiveVideoActivity : AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var binding: LiveVideoActivityBinding
+ private var mediaPlayer: MediaPlayer? = null
+ private var isFullscreen = false
+ private var isPlaying = false
private var isVoiceEnabled = false
private var isLightOn = false
private var isRecording = false
- private var recordTime = 0
- private var recordHandler: Handler? = null
- private var recordRunnable: Runnable? = null
+
+ private var videoWidth = 0
+ private var videoHeight = 0
+
+ private val testVideoUrl = "https://vjs.zencdn.net/v/oceans.mp4"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LiveVideoActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
-
+
+
+ initSurfaceView()
initListener()
}
+ private fun initSurfaceView() {
+ binding.surfaceView.holder.addCallback(this)
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ initMediaPlayer(holder)
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+ adjustSurfaceViewSize()
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ releaseMediaPlayer()
+ }
+
+ private fun initMediaPlayer(holder: SurfaceHolder) {
+ mediaPlayer = MediaPlayer().apply {
+ setDisplay(holder)
+ setAudioStreamType(AudioManager.STREAM_MUSIC)
+
+ try {
+ setDataSource(this@LiveVideoActivity, Uri.parse(testVideoUrl))
+ prepareAsync()
+
+ setOnPreparedListener { mp ->
+ this@LiveVideoActivity.videoWidth = mp.videoWidth
+ this@LiveVideoActivity.videoHeight = mp.videoHeight
+ adjustSurfaceViewSize()
+ mp.isLooping = true
+ mp.start()
+ this@LiveVideoActivity.isPlaying = true
+ Toast.makeText(this@LiveVideoActivity, "视频准备完成", Toast.LENGTH_SHORT).show()
+ }
+
+ setOnCompletionListener {
+ this@LiveVideoActivity.isPlaying = false
+ Toast.makeText(this@LiveVideoActivity, "播放完成", Toast.LENGTH_SHORT).show()
+ }
+
+ setOnErrorListener { _, what, extra ->
+ Toast.makeText(this@LiveVideoActivity, "播放错误: $what, $extra", Toast.LENGTH_SHORT).show()
+ releaseMediaPlayer()
+ true
+ }
+
+ setOnBufferingUpdateListener { _, percent ->
+ if (percent < 100) {
+ Toast.makeText(this@LiveVideoActivity, "缓冲中: $percent%", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ } catch (e: IOException) {
+ e.printStackTrace()
+ Toast.makeText(this@LiveVideoActivity, "无法加载视频", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun adjustSurfaceViewSize() {
+ if (videoWidth == 0 || videoHeight == 0) return
+
+ val screenWidth = resources.displayMetrics.widthPixels
+ val screenHeight = resources.displayMetrics.heightPixels
+ val aspectRatio = videoWidth.toFloat() / videoHeight.toFloat()
+
+ val params = binding.surfaceView.layoutParams
+
+ if (isFullscreen) {
+ if (screenWidth.toFloat() / screenHeight.toFloat() > aspectRatio) {
+ params.width = (screenHeight * aspectRatio).toInt()
+ params.height = screenHeight
+ } else {
+ params.width = screenWidth
+ params.height = (screenWidth / aspectRatio).toInt()
+ }
+ } else {
+ val containerHeight = binding.videoContainer.height
+ params.width = (containerHeight * aspectRatio).toInt()
+ params.height = containerHeight
+ }
+
+ binding.surfaceView.layoutParams = params
+ }
+
private fun initListener() {
- // 返回按钮
binding.btnBack.setOnClickListener {
finish()
}
- // 菜单按钮
binding.ivMenu.setOnClickListener {
Toast.makeText(this, "菜单", Toast.LENGTH_SHORT).show()
}
- // 全屏按钮
binding.ivFullscreen.setOnClickListener {
- Toast.makeText(this, "全屏", Toast.LENGTH_SHORT).show()
+ toggleFullscreen()
+ }
+
+ binding.surfaceView.setOnClickListener {
+ togglePlayPause()
}
- // 语音按钮
binding.llVoice.setOnClickListener {
toggleVoice()
}
- // 灯光按钮
binding.llLight.setOnClickListener {
toggleLight()
}
- // 录像按钮
binding.llRecord.setOnClickListener {
toggleRecording()
}
- // 告警日志区域
binding.llAlarmLog.setOnClickListener {
Toast.makeText(this, "查看告警日志", Toast.LENGTH_SHORT).show()
}
}
+ private fun togglePlayPause() {
+ mediaPlayer?.let { mp ->
+ if (mp.isPlaying) {
+ mp.pause()
+ isPlaying = false
+ Toast.makeText(this, "已暂停", Toast.LENGTH_SHORT).show()
+ } else {
+ mp.start()
+ isPlaying = true
+ Toast.makeText(this, "继续播放", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun toggleFullscreen() {
+ isFullscreen = !isFullscreen
+
+ if (isFullscreen) {
+ enterFullscreen()
+ } else {
+ exitFullscreen()
+ }
+ }
+
+ private fun enterFullscreen() {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+
+ binding.topBar.visibility = View.GONE
+ binding.bitRateBar.visibility = View.GONE
+ binding.functionBar.visibility = View.GONE
+
+ val alarmLogParent = binding.llAlarmLog.parent.parent as View
+ alarmLogParent.visibility = View.GONE
+
+ val containerParams = binding.videoContainer.layoutParams
+ containerParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+ containerParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.videoContainer.layoutParams = containerParams
+
+ val surfaceParams = binding.surfaceView.layoutParams
+ surfaceParams.width = resources.displayMetrics.widthPixels
+ surfaceParams.height = resources.displayMetrics.heightPixels
+ binding.surfaceView.layoutParams = surfaceParams
+
+ binding.ivFullscreen.setImageResource(com.example.defenseapplication.R.drawable.back)
+
+ Toast.makeText(this, "已进入全屏", Toast.LENGTH_SHORT).show()
+ }
+
+ private fun exitFullscreen() {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+
+ binding.topBar.visibility = View.VISIBLE
+ binding.bitRateBar.visibility = View.VISIBLE
+ binding.functionBar.visibility = View.VISIBLE
+
+ val alarmLogParent = binding.llAlarmLog.parent.parent as View
+ alarmLogParent.visibility = View.VISIBLE
+
+ val containerParams = binding.videoContainer.layoutParams
+ containerParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+ containerParams.height = (220 * resources.displayMetrics.density).toInt()
+ binding.videoContainer.layoutParams = containerParams
+
+ adjustSurfaceViewSize()
+
+ binding.ivFullscreen.setImageResource(com.example.defenseapplication.R.drawable.full_screen)
+
+ Toast.makeText(this, "已退出全屏", Toast.LENGTH_SHORT).show()
+ }
+
private fun toggleVoice() {
isVoiceEnabled = !isVoiceEnabled
if (isVoiceEnabled) {
- binding.vVoiceActive.visibility = View.VISIBLE
- binding.ivVoice.setColorFilter(resources.getColor(com.example.defenseapplication.R.color.green))
- Toast.makeText(this, "语音已开启", Toast.LENGTH_SHORT).show()
+ mediaPlayer?.setVolume(1.0f, 1.0f)
+ binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice_open)
+ Toast.makeText(this, "声音已开启", Toast.LENGTH_SHORT).show()
} else {
- binding.vVoiceActive.visibility = View.INVISIBLE
- binding.ivVoice.clearColorFilter()
- Toast.makeText(this, "语音已关闭", Toast.LENGTH_SHORT).show()
+ mediaPlayer?.setVolume(0.0f, 0.0f)
+ binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice)
+ Toast.makeText(this, "声音已关闭", Toast.LENGTH_SHORT).show()
}
}
@@ -82,12 +253,10 @@ class LiveVideoActivity : AppCompatActivity() {
isLightOn = !isLightOn
if (isLightOn) {
- binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on)
- binding.ivLight.setColorFilter(resources.getColor(com.example.defenseapplication.R.color.green))
+ binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on_open)
Toast.makeText(this, "灯光已开启", Toast.LENGTH_SHORT).show()
} else {
- binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.low_light)
- binding.ivLight.clearColorFilter()
+ binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on)
Toast.makeText(this, "灯光已关闭", Toast.LENGTH_SHORT).show()
}
}
@@ -96,38 +265,37 @@ class LiveVideoActivity : AppCompatActivity() {
isRecording = !isRecording
if (isRecording) {
- recordTime = 0
- binding.tvRecordTime.text = "00"
- startRecordTimer()
- binding.ivRecord.setColorFilter(resources.getColor(com.example.defenseapplication.R.color.red))
+ binding.ivRecord.setImageResource(com.example.defenseapplication.R.drawable.copy_open)
Toast.makeText(this, "开始录像", Toast.LENGTH_SHORT).show()
} else {
- stopRecordTimer()
- binding.ivRecord.clearColorFilter()
- Toast.makeText(this, "停止录像,共录制 ${recordTime} 秒", Toast.LENGTH_SHORT).show()
+ binding.ivRecord.setImageResource(com.example.defenseapplication.R.drawable.copy)
+ Toast.makeText(this, "停止录像", Toast.LENGTH_SHORT).show()
}
}
- private fun startRecordTimer() {
- recordHandler = Handler(Looper.getMainLooper())
- recordRunnable = object : Runnable {
- override fun run() {
- recordTime++
- binding.tvRecordTime.text = String.format("%02d", recordTime)
- recordHandler?.postDelayed(this, 1000)
+ private fun releaseMediaPlayer() {
+ mediaPlayer?.let {
+ if (it.isPlaying) {
+ it.stop()
}
+ it.release()
+ mediaPlayer = null
}
- recordHandler?.post(recordRunnable!!)
}
- private fun stopRecordTimer() {
- recordRunnable?.let { recordHandler?.removeCallbacks(it) }
- recordHandler = null
- recordRunnable = null
+ override fun onPause() {
+ super.onPause()
+ mediaPlayer?.pause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mediaPlayer?.start()
}
override fun onDestroy() {
+ releaseMediaPlayer()
super.onDestroy()
- stopRecordTimer()
}
-}
\ No newline at end of file
+}
+
diff --git a/app/src/main/res/drawable/copy_open.png b/app/src/main/res/drawable/copy_open.png
new file mode 100644
index 0000000..17a346b
Binary files /dev/null and b/app/src/main/res/drawable/copy_open.png differ
diff --git a/app/src/main/res/drawable/full_screen.png b/app/src/main/res/drawable/full_screen.png
new file mode 100644
index 0000000..cb91da8
Binary files /dev/null and b/app/src/main/res/drawable/full_screen.png differ
diff --git a/app/src/main/res/drawable/light_on_open.png b/app/src/main/res/drawable/light_on_open.png
new file mode 100644
index 0000000..6f1bacb
Binary files /dev/null and b/app/src/main/res/drawable/light_on_open.png differ
diff --git a/app/src/main/res/drawable/voice_open.png b/app/src/main/res/drawable/voice_open.png
new file mode 100644
index 0000000..ea45370
Binary files /dev/null and b/app/src/main/res/drawable/voice_open.png differ
diff --git a/app/src/main/res/layout/live_video_activity.xml b/app/src/main/res/layout/live_video_activity.xml
index 60ab653..19f4d0c 100644
--- a/app/src/main/res/layout/live_video_activity.xml
+++ b/app/src/main/res/layout/live_video_activity.xml
@@ -8,6 +8,7 @@
-
+ android:background="@drawable/back"/>
+ android:textColor="@color/black"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ android:textStyle="bold" />
-
-
+
-
+ android:layout_gravity="center" />
+ android:src="@drawable/full_screen"/>
-
+
+
+
+
+
+
+
+
+ android:orientation="vertical">
-
+
+
+ android:layout_marginTop="8dp"
+ android:text="语音"
+ android:textSize="14sp"
+ android:textColor="@color/ic_normal"/>
-
-
+
-
+
+
+
+
+ android:layout_marginTop="8dp"
+ android:text="灯光"
+ android:textSize="14sp"
+ android:textColor="@color/ic_normal"/>
-
-
+
-
+
+
+
+
-
-
-
+ android:layout_marginTop="8dp"
+ android:text="控制"
+ android:textSize="14sp"
+ android:textColor="@color/ic_normal"/>
@@ -153,10 +192,7 @@
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
-
-
@@ -230,15 +261,16 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="入侵告警"
- android:textColor="#666666"
- android:textSize="12sp" />
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+ android:textColor="@color/ic_normal"
+ android:textSize="12sp" />
@@ -265,7 +297,7 @@
android:layout_height="20dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
- android:src="@drawable/copy" />
+ android:src="@drawable/album" />
@@ -274,15 +306,16 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="入侵告警"
- android:textColor="#666666"
- android:textSize="12sp" />
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+ android:textColor="@color/ic_normal"
+ android:textSize="12sp" />
@@ -321,15 +354,16 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="入侵告警"
- android:textColor="#666666"
- android:textSize="12sp" />
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+ android:textColor="@color/ic_normal"
+ android:textSize="12sp" />
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index deeecec..8ff0d88 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,6 +9,7 @@ material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
firebaseCrashlyticsBuildtools = "3.0.7"
+uiTest = "1.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -20,6 +21,7 @@ material = { group = "com.google.android.material", name = "material", version.r
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
+androidx-ui-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "uiTest" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }