视频播放1

This commit is contained in:
2026-05-27 11:58:26 +08:00
parent ac906c901b
commit 97a0cdc14f
4 changed files with 75 additions and 288 deletions

View File

@@ -81,12 +81,6 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.16.0") implementation("com.github.bumptech.glide:glide:4.16.0")
// ZXing 二维码扫描库 // ZXing 二维码扫描库
implementation("com.google.zxing:core:3.5.2") 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")
// 基础依赖包,必须要依赖 // 基础依赖包,必须要依赖
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2") implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
// kotlin扩展可选 // kotlin扩展可选
@@ -107,5 +101,7 @@ dependencies {
implementation("com.yanzhenjie.recyclerview:x:1.3.2") implementation("com.yanzhenjie.recyclerview:x:1.3.2")
//Android-PickerView-时间弹窗 //Android-PickerView-时间弹窗
implementation("com.contrarywind:Android-PickerView:4.1.9") implementation("com.contrarywind:Android-PickerView:4.1.9")
//基于exoPlayer 自定义播放器 JPlayer支持功能
implementation("com.ycjiang:VideoPlayModule-Lite:2.3.61")
} }

View File

@@ -2,55 +2,40 @@ package com.example.defenseapplication.liveVideo
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.MediaRecorder import android.media.MediaRecorder
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.util.Log
import android.view.SurfaceHolder
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.ScaleAnimation import android.view.animation.ScaleAnimation
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import chuangyuan.ycj.videolibrary.video.ExoUserPlayer
import com.example.defenseapplication.R import com.example.defenseapplication.R
import com.example.defenseapplication.base.BaseActivity import com.example.defenseapplication.base.BaseActivity
import com.example.defenseapplication.databinding.LiveVideoActivityBinding import com.example.defenseapplication.databinding.LiveVideoActivityBinding
import com.example.defenseapplication.utils.ActivityManager import com.example.defenseapplication.utils.ActivityManager
import com.example.defenseapplication.utils.DeviceIdEvent
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import com.gyf.immersionbar.ktx.destroyImmersionBar
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import android.content.res.Configuration
import androidx.activity.OnBackPressedCallback
import chuangyuan.ycj.videolibrary.listener.VideoInfoListener
import chuangyuan.ycj.videolibrary.video.VideoPlayerManager
private const val TAG = "LiveVideoActivity" private const val TAG = "LiveVideoActivity"
//智能防御机A1 //智能防御机A1
class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback { class LiveVideoActivity : BaseActivity() {
private lateinit var binding: LiveVideoActivityBinding private lateinit var binding: LiveVideoActivityBinding
private var mediaPlayer: MediaPlayer? = null private lateinit var exoPlayerManager: ExoUserPlayer
private var isFullscreen = false
private var isPlaying = false
private var isVoiceEnabled = false private var isVoiceEnabled = false
private var isLightOn = false private var isLightOn = false
private var isRecording = false private var isRecording = false
private var mediaRecorder: MediaRecorder? = null private var mediaRecorder: MediaRecorder? = null
private var micPulseAnimation: ScaleAnimation? = null private var micPulseAnimation: ScaleAnimation? = null
private var videoWidth = 0
private var videoHeight = 0
private val testVideoUrl = "https://vjs.zencdn.net/v/oceans.mp4"
private val requestPermissionLauncher = registerForActivityResult( private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
@@ -78,93 +63,55 @@ class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback {
binding.tvTitle.text = deviceName binding.tvTitle.text = deviceName
} }
initSurfaceView() // 初始化播放器
initVideoPlayer()
initListener() initListener()
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (exoPlayerManager.onBackPressed()) {
return
}
// 无全屏时执行默认返回逻辑(如 finish
finish()
}
})
}
private fun initVideoPlayer() {
val videoPlayerView = binding.exoPlayContextId
exoPlayerManager = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_GESTURE, videoPlayerView)
.setPlayUri("https://media.w3.org/2010/05/sintel/trailer.mp4")//真实地址
.setPosition(0)
.addVideoInfoListener(object : VideoInfoListener {
// 注意:此处的 p0: Long 是视频开始播放时的位置信息,可以按需使用
override fun onPlayStart(p0: Long) {
Log.d(TAG, "视频开始播放")
} }
private fun initSurfaceView() { override fun onLoadingChanged() {
binding.surfaceView.holder.addCallback(this) // 视频加载状态变化
} }
override fun surfaceCreated(holder: SurfaceHolder) { override fun onPlayerError(p0: com.google.android.exoplayer2.ExoPlaybackException?) {
initMediaPlayer(holder) Toast.makeText(this@LiveVideoActivity, "播放失败", Toast.LENGTH_SHORT).show()
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun onPlayEnd() {
adjustSurfaceViewSize() Log.d(TAG, "播放结束")
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { // 修正点:库的接口要求必须实现 isPlaying 方法
releaseMediaPlayer() override fun isPlaying(p0: Boolean) {
// p0 表示当前是否正在播放
} }
private fun initMediaPlayer(holder: SurfaceHolder) { fun onRepeatModeChanged(p0: Int) {
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, R.string.video_ready, Toast.LENGTH_SHORT).show()
} }
})
.create()
setOnCompletionListener { exoPlayerManager.startPlayer()
this@LiveVideoActivity.isPlaying = false
Toast.makeText(this@LiveVideoActivity, R.string.playback_complete, Toast.LENGTH_SHORT).show()
}
setOnErrorListener { _, what, extra ->
Toast.makeText(this@LiveVideoActivity, getString(R.string.video_error_format, what, extra), Toast.LENGTH_SHORT).show()
releaseMediaPlayer()
true
}
setOnBufferingUpdateListener { _, percent ->
if (percent < 100) {
Toast.makeText(this@LiveVideoActivity, getString(R.string.buffering_format, percent), Toast.LENGTH_SHORT).show()
}
}
} catch (e: IOException) {
e.printStackTrace()
Toast.makeText(this@LiveVideoActivity, R.string.cannot_load_video, 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() { private fun initListener() {
@@ -180,15 +127,6 @@ class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback {
val intent = Intent(this, SettingsActivity::class.java) val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent) startActivity(intent)
} }
binding.ivFullscreen.setOnClickListener {
toggleFullscreen()
}
binding.surfaceView.setOnClickListener {
togglePlayPause()
}
binding.llVoice.setOnClickListener { binding.llVoice.setOnClickListener {
toggleVoice() toggleVoice()
} }
@@ -212,90 +150,14 @@ class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback {
} }
} }
private fun togglePlayPause() {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
mp.pause()
isPlaying = false
Toast.makeText(this, R.string.paused, Toast.LENGTH_SHORT).show()
} else {
mp.start()
isPlaying = true
Toast.makeText(this, R.string.resume_playback, 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, R.string.entered_fullscreen, 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, R.string.exited_fullscreen, Toast.LENGTH_SHORT).show()
}
private fun toggleVoice() { private fun toggleVoice() {
isVoiceEnabled = !isVoiceEnabled isVoiceEnabled = !isVoiceEnabled
if (isVoiceEnabled) { if (isVoiceEnabled) {
mediaPlayer?.setVolume(1.0f, 1.0f)
binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice_on_open) binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice_on_open)
Toast.makeText(this, R.string.sound_on, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.sound_on, Toast.LENGTH_SHORT).show()
} else { } else {
mediaPlayer?.setVolume(0.0f, 0.0f)
binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice) binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice)
Toast.makeText(this, R.string.sound_off, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.sound_off, Toast.LENGTH_SHORT).show()
} }
@@ -378,20 +240,6 @@ class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback {
Toast.makeText(this, R.string.stop_control_mode, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.stop_control_mode, Toast.LENGTH_SHORT).show()
} }
// private fun startMicPulseAnimation() {
// micPulseAnimation = ScaleAnimation(
// 1.0f, 1.3f,
// 1.0f, 1.3f,
// Animation.RELATIVE_TO_SELF, 0.5f,
// Animation.RELATIVE_TO_SELF, 0.5f
// ).apply {
// duration = 500
// repeatCount = Animation.INFINITE
// repeatMode = Animation.REVERSE
// interpolator = LinearInterpolator()
// }
// binding.ivVoice.startAnimation(micPulseAnimation)
// }
private fun stopMicPulseAnimation() { private fun stopMicPulseAnimation() {
micPulseAnimation?.let { micPulseAnimation?.let {
@@ -434,32 +282,26 @@ class LiveVideoActivity : BaseActivity(), SurfaceHolder.Callback {
mediaRecorder = null mediaRecorder = null
} }
} }
override fun onResume() {
private fun releaseMediaPlayer() { super.onResume()
mediaPlayer?.let { exoPlayerManager.onResume()
if (it.isPlaying) {
it.stop()
}
it.release()
mediaPlayer = null
}
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
mediaPlayer?.pause() exoPlayerManager.onPause()
}
override fun onResume() {
super.onResume()
mediaPlayer?.start()
} }
override fun onDestroy() { override fun onDestroy() {
releaseMediaPlayer()
super.onDestroy() super.onDestroy()
exoPlayerManager.onDestroy()
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 横竖屏切换时通知播放器
exoPlayerManager.onConfigurationChanged(newConfig)
}
} }

View File

@@ -4,6 +4,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F5F6F6" android:background="#F5F6F6"
android:name=".liveVideo.LiveVideoActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait"
android:orientation="vertical"> android:orientation="vertical">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
@@ -41,68 +44,12 @@
</LinearLayout> </LinearLayout>
<!-- 视频流信息 -->
<LinearLayout
android:id="@+id/bitRateBar"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="#FFFFFF"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/tvBitRate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="120 B/S"
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 视频播放区域 --> <!-- 视频播放区域 -->
<FrameLayout <chuangyuan.ycj.videolibrary.widget.VideoPlayerView
android:id="@+id/videoContainer" android:id="@+id/exoPlayContextId"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="220dp" android:layout_height="250dp"
android:background="#000000"> android:background="@android:color/transparent"/>
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<!-- 全屏按钮 -->
<ImageView
android:id="@+id/ivFullscreen"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/full_screen"/>
<!-- 播放控制按钮 -->
<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 <LinearLayout

View File

@@ -13,3 +13,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# org.gradle.parallel=true # org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.useAndroidX=true
android.enableJetifier=true