视频播放

This commit is contained in:
2026-05-06 17:05:35 +08:00
parent a3115b6b86
commit 3558f053e6
10 changed files with 353 additions and 135 deletions

View File

@@ -4,6 +4,13 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-06T08:15:17.445018500Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=80bece4" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>

View File

@@ -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")
}

View File

@@ -25,7 +25,13 @@
android:theme="@style/Theme.DefenseApplication">
<activity
android:name=".liveVideo.LiveVideoActivity"
android:exported="false" />
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".home.ScanActivity"
android:exported="false" />
@@ -46,13 +52,7 @@
android:exported="false" />
<activity
android:name=".home.HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:exported="false"/>
<activity
android:name=".login.RegisterSuccessActivity"
android:exported="false" />

View File

@@ -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()
}
}

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: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

View File

@@ -8,6 +8,7 @@
<!-- 顶部导航栏 -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#FFFFFF"
@@ -16,12 +17,11 @@
android:paddingLeft="16dp"
android:paddingRight="16dp">
<Button
<ImageView
android:id="@+id/btnBack"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/back"
android:contentDescription="返回" />
android:background="@drawable/back"/>
<TextView
android:id="@+id/tvTitle"
@@ -30,8 +30,9 @@
android:layout_weight="1"
android:gravity="center"
android:text="智能防御机A1"
android:textColor="#000000"
android:textSize="18sp" />
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/ivMenu"
@@ -43,6 +44,7 @@
<!-- 视频流信息 -->
<LinearLayout
android:id="@+id/bitRateBar"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="#FFFFFF"
@@ -57,38 +59,55 @@
android:layout_height="wrap_content"
android:gravity="center"
android:text="120 B/S"
android:textColor="#999999"
android:textSize="14sp" />
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 视频显示区域 -->
<RelativeLayout
<!-- 视频播放区域 -->
<FrameLayout
android:id="@+id/videoContainer"
android:layout_width="match_parent"
android:layout_height="280dp"
android:layout_height="220dp"
android:background="#000000">
<ImageView
android:id="@+id/ivVideo"
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/img" />
android:layout_gravity="center" />
<!-- 全屏按钮 -->
<ImageView
android:id="@+id/ivFullscreen"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/go_up" />
android:src="@drawable/full_screen"/>
</RelativeLayout>
<!-- 播放控制按钮 -->
<LinearLayout
android:id="@+id/playControl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone">
<ImageView
android:id="@+id/ivPlay"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/full_screen" />
</LinearLayout>
</FrameLayout>
<!-- 功能按钮区域 -->
<LinearLayout
android:id="@+id/functionBar"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="#FFFFFF"
@@ -101,49 +120,69 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
android:orientation="vertical">
<ImageView
android:id="@+id/ivVoice"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerInParent="true"
android:layout_width="42dp"
android:layout_height="42dp"
android:src="@drawable/voice" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="语音"
android:textSize="14sp"
android:textColor="@color/ic_normal"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llLight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivLight"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerInParent="true"
android:layout_width="42dp"
android:layout_height="42dp"
android:src="@drawable/light_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="灯光"
android:textSize="14sp"
android:textColor="@color/ic_normal"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llRecord"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivRecord"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerInParent="true"
android:src="@drawable/copy" />
</LinearLayout>
android:layout_width="42dp"
android:layout_height="42dp"
android:src="@drawable/copy"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="控制"
android:textSize="14sp"
android:textColor="@color/ic_normal"/>
</LinearLayout>
@@ -153,10 +192,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:background="#FFFFFF">
<LinearLayout
@@ -169,23 +205,18 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="告警日志"
android:textColor="#000000"
android:textSize="16sp"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/go_up" />
</LinearLayout>
<!-- 告警图片列表 -->
@@ -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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="03-11"
android:textColor="#999999"
android:textSize="10sp" />
android:textColor="@color/ic_normal"
android:textSize="12sp" />
</LinearLayout>
@@ -265,7 +297,7 @@
android:layout_height="20dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:src="@drawable/copy" />
android:src="@drawable/album" />
</RelativeLayout>
@@ -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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="03-11"
android:textColor="#999999"
android:textSize="10sp" />
android:textColor="@color/ic_normal"
android:textSize="12sp" />
</LinearLayout>
@@ -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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="03-11"
android:textColor="#999999"
android:textSize="10sp" />
android:textColor="@color/ic_normal"
android:textSize="12sp" />
</LinearLayout>

View File

@@ -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" }