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