Compare commits

...

4 Commits

Author SHA1 Message Date
5cafd2ac1d 登录启动页优化 2026-06-08 10:39:30 +08:00
c2b947c630 实时视频组件优化 2026-06-05 17:17:33 +08:00
d60322cbc2 视频组件 2026-06-05 09:33:30 +08:00
88c791a520 视频组件更换StandardGSYVideoPlayer 2026-06-04 17:51:19 +08:00
15 changed files with 428 additions and 792 deletions

View File

@@ -101,8 +101,8 @@ dependencies {
implementation("com.yanzhenjie.recyclerview:x:1.3.2")
//Android-PickerView-时间弹窗
implementation("com.contrarywind:Android-PickerView:4.1.9")
//基于exoPlayer 自定义播放器 JPlayer支持功能
implementation("com.ycjiang:VideoPlayModule-Lite:2.3.61")
// GSYVideoPlayer
implementation("io.github.carguo:gsyvideoplayer:13.0.0")
}

View File

@@ -27,6 +27,15 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DefenseApplication">
<activity
android:name=".home.StartActivity"
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=".personal.LoginPasswordActivity"
android:exported="false" />
@@ -90,6 +99,7 @@
android:exported="false" />
<activity
android:name=".liveVideo.LiveVideoActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false" />
<activity
android:name=".home.SetNameActivity"
@@ -108,7 +118,8 @@
android:exported="false" />
<activity
android:name=".home.HomeActivity"
android:exported="false" />
android:exported="false">
</activity>
<activity
android:name=".login.RegisterSuccessActivity"
android:exported="false" />
@@ -126,13 +137,7 @@
android:exported="false" />
<activity
android:name=".login.LoginActivity"
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" />
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -15,11 +15,6 @@ class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Utils.init(this) // 初始化 AndroidUtilCode
var token = UserinfoUtil.getUserInfo()?.access_token;
if (token != null) {
ApiConfig.authToken = token
};
LanguageUtil.init(this)
}

View File

@@ -0,0 +1,36 @@
package com.example.defenseapplication.home
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.example.defenseapplication.databinding.ActivityStartBinding
import com.example.defenseapplication.login.LoginActivity
import com.example.defenseapplication.utils.ApiConfig
import com.example.defenseapplication.utils.UserinfoUtil
class StartActivity : AppCompatActivity() {
private lateinit var binding: ActivityStartBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStartBinding.inflate(layoutInflater)
setContentView(binding.root)
// 延迟2秒后再判断登录状态
Handler(Looper.getMainLooper()).postDelayed({
val userInfo = UserinfoUtil.getUserInfo()
if (userInfo != null) {
// 设置token
ApiConfig.authToken = userInfo.access_token
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
} else {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
finish()
}, 2000) // 2秒延迟
}
}

View File

@@ -1,39 +1,32 @@
package com.example.defenseapplication.liveVideo
import android.content.res.Configuration
import android.content.Context
import android.graphics.Color
import android.media.AudioManager
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.lifecycle.lifecycleScope
import chuangyuan.ycj.videolibrary.listener.VideoInfoListener
import chuangyuan.ycj.videolibrary.video.ExoUserPlayer
import chuangyuan.ycj.videolibrary.video.VideoPlayerManager
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.SimpleExoPlayer
import com.example.defenseapplication.R
import com.example.defenseapplication.adapter.AlarmLogAdapter
import com.example.defenseapplication.bean.DeviceAlarmBean
import com.example.defenseapplication.bean.DeviceAlarmResponse
import com.example.defenseapplication.databinding.AlarmLogActivityBinding
import com.example.defenseapplication.utils.RetrofitNetwork
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.launch
import android.os.Handler
import android.os.Looper
import java.text.SimpleDateFormat
import java.util.*
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack
import com.shuyu.gsyvideoplayer.utils.OrientationUtils
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
import com.bigkoo.pickerview.builder.TimePickerBuilder
import com.bigkoo.pickerview.listener.OnTimeSelectListener
import com.example.defenseapplication.view.MyGSYVideoPlayer
import com.shuyu.gsyvideoplayer.GSYVideoManager
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
//告警记录
class AlarmLogActivity : AppCompatActivity() {
@@ -42,151 +35,105 @@ class AlarmLogActivity : AppCompatActivity() {
private var adapter: AlarmLogAdapter? = null
private val alarmList = mutableListOf<DeviceAlarmBean>()
private val originalAlarmList = mutableListOf<DeviceAlarmBean>()
private lateinit var exoPlayerManager: ExoUserPlayer
private var isMuted = false
private var isPlaying = false
private var deviceId: String? = null
private var isLoading = false
private var fromLiveVideo = false
private var isFullScreen = false
private var isControlsVisible = false
private val hideDelay = 5000L // 5秒自动隐藏
private val hideHandler = Handler(Looper.getMainLooper())
private val hideRunnable = Runnable {
hideControls()
}
private var passedVideoUrl: String? = null
private var passedSelectedId: String? = null
private var passedCreateTime: String? = null
private var totalDuration: Long = 0
private val progressHandler = Handler(Looper.getMainLooper())
private val progressRunnable = object : Runnable {
override fun run() {
updateProgress()
progressHandler.postDelayed(this, 1000)
}
}
private var videoUrl: String? = null
private var orientationUtils: OrientationUtils? = null
private var isMute = false
private lateinit var audioManager: AudioManager
private var isRightMenuVisible = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AlarmLogActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
deviceId = intent.getStringExtra("device_id")
fromLiveVideo = intent.getBooleanExtra("from_live_video", false)
passedVideoUrl = intent.getStringExtra("video_url")
passedSelectedId = intent.getStringExtra("selected_id")
passedCreateTime = intent.getStringExtra("create_time")
videoUrl = intent.getStringExtra("video_url")
calendar = Calendar.getInstance()
updateDateDisplay()
initVideoPlayer()
initRecyclerView()
fetchAlarmList()
initListener()
// onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
// if (exoPlayerManager.onBackPressed()) {
// return
// }
// finish()
// }
// })
}
private fun dpToPx(dp: Int): Int {
val density = resources.displayMetrics.density
return (dp * density).toInt()
}
private fun initVideoPlayer() {
val videoPlayerView = binding.exoPlayContextId
val videoUri = if (fromLiveVideo && !passedVideoUrl.isNullOrEmpty()) {
passedVideoUrl!!
} else {
"https://media.w3.org/2010/05/sintel/trailer.mp4"
}
val videoUrl = intent.getStringExtra("video_url") ?: "https://media.w3.org/2010/05/sintel/trailer.mp4"
val progressDrawable = ContextCompat.getDrawable(this, R.drawable.progress_video)
val thumbDrawable = ContextCompat.getDrawable(this, R.drawable.thumb_video)
exoPlayerManager = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_GESTURE, videoPlayerView)
.setPlayUri(videoUri)
.setPosition(0)
.addVideoInfoListener(object : VideoInfoListener {
override fun onPlayStart(p0: Long) {
Log.d("AlarmLogActivity", "视频开始播放")
startProgressUpdate()
showControls()
resetHideTimer()
orientationUtils = OrientationUtils(this, binding.exoPlayContextId)
orientationUtils?.isEnable = true
GSYVideoOptionBuilder()
.setUrl(videoUrl)
.setVideoTitle("")
.setNeedLockFull(true)
.setCacheWithPlay(false)
.setRotateViewAuto(true)
.setLockLand(false)
.setShowFullAnimation(true)
.setNeedShowWifiTip(false)
.setEnlargeImageRes(R.drawable.full_size)
.setShrinkImageRes(R.drawable.full_screen)
.setBottomShowProgressBarDrawable(progressDrawable, thumbDrawable)
.setVideoAllCallBack(object : GSYSampleCallBack() {
override fun onPrepared(url: String?, vararg objects: Any?) {
super.onPrepared(url, *objects)
orientationUtils?.isEnable = true
}
override fun onLoadingChanged() {
}
override fun onPlayerError(p0: com.google.android.exoplayer2.ExoPlaybackException?) {
Toast.makeText(this@AlarmLogActivity, "播放失败", Toast.LENGTH_SHORT).show()
}
override fun onPlayEnd() {
Log.d("AlarmLogActivity", "播放结束")
stopProgressUpdate()
hideControls()
}
override fun isPlaying(p0: Boolean) {
isPlaying = p0
if (p0) {
startProgressUpdate()
// 视频开始播放时,如果控制栏可见,启动自动隐藏计时器
if (isControlsVisible) {
resetHideTimer()
}
} else {
stopProgressUpdate()
// 视频暂停时,取消自动隐藏计时器,保持控制栏显示
hideHandler.removeCallbacks(hideRunnable)
showControls()
}
}
fun onRepeatModeChanged(p0: Int) {
override fun onQuitFullscreen(url: String?, vararg objects: Any?) {
super.onQuitFullscreen(url, *objects)
orientationUtils?.backToProtVideo()
}
})
.create()
.build(binding.exoPlayContextId)
binding.exoPlayContextId.backButton.setOnClickListener {
exoPlayerManager.startPlayer()
setupControllerVisibilityListener()
updateProgress()
}
private fun setupControllerVisibilityListener() {
// 移除原来的控制器监听器,改为自定义控制逻辑
}
private fun findPlayerView(view: View): PlayerView? {
if (view is PlayerView) {
return view
if (orientationUtils?.isLand == 1) {
// 当前全屏,先退出全屏
binding.exoPlayContextId.fullscreenButton.performClick()
} else {
finish()
}
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
val found = findPlayerView(child)
if (found != null) {
return found
binding.exoPlayContextId.onControlVisibilityChanged = { show ->
Log.e("GSY_TEST", "Activity收到回调 show=$show")
val visible = if (show) View.VISIBLE else View.GONE
binding.ivDownload.visibility = visible
binding.ivDelete.visibility = visible
binding.ivMute.visibility = visible
}
binding.exoPlayContextId.startPlayLogic()
binding.exoPlayContextId.fullscreenButton.setOnClickListener {
orientationUtils?.resolveByClick()
binding.exoPlayContextId.startWindowFullscreen(this, true, true)?.let { fullPlayer ->
if (fullPlayer is MyGSYVideoPlayer) {
fullPlayer.onControlVisibilityChanged = { show ->
val visible = if (show) View.VISIBLE else View.GONE
binding.ivDownload.visibility = visible
binding.ivDelete.visibility = visible
binding.ivMute.visibility = visible
}
}
}
}
return null
}
}
private fun initRecyclerView() {
adapter = AlarmLogAdapter(alarmList) { item ->
if (!item.video.isNullOrEmpty()) {
playVideo(item.video!!)
Toast.makeText(this, "该告警记录有视频", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "该告警记录没有视频", Toast.LENGTH_SHORT).show()
}
@@ -195,68 +142,6 @@ class AlarmLogActivity : AppCompatActivity() {
binding.alarmListRecyclerView.adapter = adapter
}
private fun playVideo(videoUrl: String) {
exoPlayerManager.onDestroy()
exoPlayerManager = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_GESTURE, binding.exoPlayContextId)
.setPlayUri(videoUrl)
.setPosition(0)
.addVideoInfoListener(object : VideoInfoListener {
override fun onPlayStart(p0: Long) {
Log.d("AlarmLogActivity", "视频开始播放")
startProgressUpdate()
showControls()
resetHideTimer()
}
override fun onLoadingChanged() {
}
override fun onPlayerError(p0: com.google.android.exoplayer2.ExoPlaybackException?) {
Toast.makeText(this@AlarmLogActivity, "播放失败", Toast.LENGTH_SHORT).show()
}
override fun onPlayEnd() {
Log.d("AlarmLogActivity", "播放结束")
stopProgressUpdate()
hideControls()
}
override fun isPlaying(p0: Boolean) {
isPlaying = p0
if (p0) {
startProgressUpdate()
// 视频开始播放时,如果控制栏可见,启动自动隐藏计时器
if (isControlsVisible) {
resetHideTimer()
}
} else {
stopProgressUpdate()
// 视频暂停时,取消自动隐藏计时器,保持控制栏显示
hideHandler.removeCallbacks(hideRunnable)
showControls()
}
}
fun onRepeatModeChanged(p0: Int) {
}
})
.create()
exoPlayerManager.startPlayer()
// 保持当前的静音状态
if (isMuted) {
try {
val method = exoPlayerManager.javaClass.getMethod("getPlayer")
val player = method.invoke(exoPlayerManager) as? SimpleExoPlayer
player?.volume = 0f
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun fetchAlarmList() {
val currentDeviceId = deviceId ?: return
@@ -328,226 +213,34 @@ class AlarmLogActivity : AppCompatActivity() {
binding.tvDate.text = "${month}${day}"
}
private fun showControls() {
isControlsVisible = true
binding.functionBar.visibility = View.VISIBLE
binding.muteBar.visibility = View.VISIBLE
binding.playBar.visibility = View.VISIBLE
binding.progressBar.visibility = View.VISIBLE
// 只有在视频播放时才启动自动隐藏计时器
if (isPlaying) {
resetHideTimer()
}
}
private fun hideControls() {
isControlsVisible = false
binding.functionBar.visibility = View.GONE
binding.muteBar.visibility = View.GONE
binding.playBar.visibility = View.GONE
binding.progressBar.visibility = View.GONE
}
private fun toggleControls() {
if (isControlsVisible) {
hideControls()
} else {
showControls()
}
}
private fun resetHideTimer() {
hideHandler.removeCallbacks(hideRunnable)
// 只有在视频播放时才启动自动隐藏计时器
if (isPlaying) {
hideHandler.postDelayed(hideRunnable, hideDelay)
}
}
private fun initListener() {
// 点击视频区域切换控制栏显示/隐藏
binding.exoPlayContextId.setOnClickListener {
toggleControls()
}
// 控制栏上的按钮点击时重置隐藏计时器
binding.ivBack.setOnClickListener {
resetHideTimer()
finish()
}
binding.expandImg.setOnClickListener {
showDatePicker()
}
binding.ivMute.setOnClickListener {
resetHideTimer()
toggleMute()
}
binding.ivDownload.setOnClickListener {
resetHideTimer()
downloadVideo()
Toast.makeText(this,"下载视频",Toast.LENGTH_SHORT).show()
}
binding.ivDelete.setOnClickListener {
resetHideTimer()
deleteVideo()
}
binding.ivFullScreen.setOnClickListener {
resetHideTimer()
toggleFullScreen()
}
binding.ivPlay.setOnClickListener {
resetHideTimer()
togglePlay()
}
binding.seekBar.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
resetHideTimer()
exoPlayerManager.seekTo(progress.toLong() * 1000)
}
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {
resetHideTimer()
}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {
}
})
}
private fun toggleMute() {
isMuted = !isMuted
try {
// 通过反射获取底层的 ExoPlayer 实例
val method = exoPlayerManager.javaClass.getMethod("getPlayer")
val player = method.invoke(exoPlayerManager) as? SimpleExoPlayer
if (isMuted) {
binding.ivMute.setImageResource(R.drawable.mute)
player?.volume = 0f
Toast.makeText(this, R.string.sound_off, Toast.LENGTH_SHORT).show()
binding.ivMute.setOnClickListener {
isMute = !isMute
if (isMute) {
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0)
binding.ivMute.setImageResource(
R.drawable.mute
)
} else {
binding.ivMute.setImageResource(R.drawable.un_mute)
player?.volume = 1f
Toast.makeText(this, R.string.sound_on, Toast.LENGTH_SHORT).show()
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 2, 0)
binding.ivMute.setImageResource(
R.drawable.un_mute
)
}
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, "音量控制失败", Toast.LENGTH_SHORT).show()
}
}
private fun downloadVideo() {
Toast.makeText(this, R.string.download, Toast.LENGTH_SHORT).show()
}
private fun deleteVideo() {
Toast.makeText(this, R.string.delete, Toast.LENGTH_SHORT).show()
}
private fun updateProgress() {
val currentPosition = exoPlayerManager.getCurrentPosition()
totalDuration = exoPlayerManager.getDuration()
binding.tvCurrentTime.text = formatTime(currentPosition)
binding.tvTotalTime.text = formatTime(totalDuration)
if (totalDuration > 0) {
val progress = (currentPosition * 100 / totalDuration).toInt()
binding.seekBar.progress = progress
}
}
private fun formatTime(milliseconds: Long): String {
val seconds = (milliseconds / 1000).toInt()
val hours = seconds / 3600
val minutes = (seconds % 3600) / 60
val secs = seconds % 60
return if (hours > 0) {
String.format("%02d:%02d:%02d", hours, minutes, secs)
} else {
String.format("%02d:%02d", minutes, secs)
}
}
private fun startProgressUpdate() {
stopProgressUpdate()
progressHandler.post(progressRunnable)
}
private fun stopProgressUpdate() {
progressHandler.removeCallbacks(progressRunnable)
}
private fun toggleFullScreen() {
isFullScreen = !isFullScreen
if (isFullScreen) {
binding.ivFullScreen.setImageResource(R.drawable.full_screen)
requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
binding.ivFullScreen.setImageResource(R.drawable.full_size)
requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
private fun togglePlay() {
isPlaying = !isPlaying
if (isPlaying) {
binding.ivPlay.setImageResource(R.drawable.zt)
exoPlayerManager.startPlayer()
} else {
binding.ivPlay.setImageResource(R.drawable.start)
exoPlayerManager.onPause()
}
}
override fun onResume() {
super.onResume()
exoPlayerManager.onResume()
}
override fun onPause() {
super.onPause()
exoPlayerManager.onPause()
}
override fun onDestroy() {
super.onDestroy()
exoPlayerManager.onDestroy()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 进入横屏(全屏)模式
binding.titleLayout.visibility = View.GONE
binding.alarmListRecyclerView.visibility = View.GONE
val params = binding.exoPlayContextId.layoutParams
params.width = LinearLayout.LayoutParams.MATCH_PARENT
params.height = LinearLayout.LayoutParams.MATCH_PARENT
binding.exoPlayContextId.layoutParams = params
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
// 进入竖屏(非全屏)模式
binding.titleLayout.visibility = View.VISIBLE
binding.alarmListRecyclerView.visibility = View.VISIBLE
val params = binding.exoPlayContextId.layoutParams
params.width = LinearLayout.LayoutParams.MATCH_PARENT
params.height = dpToPx(280)
binding.exoPlayContextId.layoutParams = params
}
exoPlayerManager.onConfigurationChanged(newConfig)
}
private fun showDatePicker() {
@@ -568,4 +261,21 @@ class AlarmLogActivity : AppCompatActivity() {
.build()
timePicker.show()
}
override fun onPause() {
super.onPause()
binding.exoPlayContextId.onVideoPause()
}
override fun onResume() {
super.onResume()
binding.exoPlayContextId.onVideoResume()
}
override fun onDestroy() {
super.onDestroy()
orientationUtils?.releaseListener()
binding.exoPlayContextId.release()
}
}

View File

@@ -1,47 +1,32 @@
package com.example.defenseapplication.liveVideo
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.animation.ScaleAnimation
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chuangyuan.ycj.videolibrary.video.ExoUserPlayer
import com.example.defenseapplication.R
import com.example.defenseapplication.adapter.AlarmLogHorizontalAdapter
import com.example.defenseapplication.base.BaseActivity
import com.example.defenseapplication.bean.DeviceAlarmBean
import com.example.defenseapplication.bean.DeviceAlarmResponse
import com.example.defenseapplication.databinding.LiveVideoActivityBinding
import com.example.defenseapplication.utils.ActivityManager
import com.example.defenseapplication.utils.RetrofitNetwork
import com.gyf.immersionbar.ImmersionBar
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack
import com.shuyu.gsyvideoplayer.utils.OrientationUtils
import kotlinx.coroutines.launch
import java.io.File
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
import android.view.View
import com.bumptech.glide.Glide
import androidx.lifecycle.lifecycleScope
class LiveVideoActivity : BaseActivity() {
private lateinit var binding: LiveVideoActivityBinding
private lateinit var exoPlayerManager: ExoUserPlayer
private var isVoiceEnabled = false
private var isLightOn = false
private var isRecording = false
private var mediaRecorder: MediaRecorder? = null
private var micPulseAnimation: ScaleAnimation? = null
private var deviceId: String? = null
private var alarmList: MutableList<DeviceAlarmBean> = mutableListOf()
private var alarmAdapter: AlarmLogHorizontalAdapter? = null
@@ -50,21 +35,13 @@ class LiveVideoActivity : BaseActivity() {
private val pageSize = 10
private var hasMore = true
private var isLoading = false
private var isFirstLoad = true
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
startRecordingMode()
} else {
Toast.makeText(this, R.string.recording_permission_needed, Toast.LENGTH_SHORT).show()
isRecording = false
}
}
private var orientationUtils: OrientationUtils? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("onEnterFullscreen", "onCreate")
binding = LiveVideoActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -83,47 +60,86 @@ class LiveVideoActivity : BaseActivity() {
initAlarmRecyclerView()
initListener()
fetchAlarmList()
// onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
// if (exoPlayerManager.onBackPressed()) {
// return
// }
// finish()
// }
// })
}
/*
* 这段代码重写了onConfigurationChanged方法用于监听屏幕方向变化
当屏幕旋转为横屏时隐藏顶部栏topBar
当屏幕为竖屏时,显示顶部栏
目的是在横屏观看视频时提供更沉浸的全屏体验。
* */
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.topBar.visibility = View.GONE
} else {
binding.topBar.visibility = View.VISIBLE
}
}
private fun initVideoPlayer() {
val videoPlayerView = binding.exoPlayContextId
val videoUrl = "https://www.w3schools.com/html/movie.mp4"//"https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
exoPlayerManager = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_GESTURE, videoPlayerView)
.setPlayUri("https://media.w3.org/2010/05/sintel/trailer.mp4")
.setPosition(0)
.addVideoInfoListener(object : VideoInfoListener {
override fun onPlayStart(p0: Long) {
Log.d("LiveVideoActivity", "视频开始播放")
orientationUtils = OrientationUtils(this, binding.exoPlayContextId)
orientationUtils?.isEnable = true
GSYVideoOptionBuilder()
.setUrl(videoUrl)
.setCacheWithPlay(false)
.setCacheWithPlay(false)
.setRotateViewAuto(true)
.setLockLand(false)
.setShowFullAnimation(true)
.setNeedShowWifiTip(false)
.setEnlargeImageRes(R.drawable.full_size)
.setShrinkImageRes(R.drawable.full_screen)
.setVideoAllCallBack(object : GSYSampleCallBack() {
override fun onPrepared(url: String?, vararg objects: Any?) {
super.onPrepared(url, *objects)
orientationUtils?.isEnable = true
}
override fun onLoadingChanged() {
}
override fun onQuitFullscreen(url: String?, vararg objects: Any?) {
super.onQuitFullscreen(url, *objects)
override fun onPlayerError(p0: com.google.android.exoplayer2.ExoPlaybackException?) {
Toast.makeText(this@LiveVideoActivity, "播放失败", Toast.LENGTH_SHORT).show()
}
override fun onPlayEnd() {
Log.d("LiveVideoActivity", "播放结束")
}
override fun isPlaying(p0: Boolean) {
}
fun onRepeatModeChanged(p0: Int) {
orientationUtils?.backToProtVideo()
}
})
.create()
.build(binding.exoPlayContextId)
hidePlayerControls()
binding.exoPlayContextId.startPlayLogic()
binding.exoPlayContextId.fullscreenButton.setOnClickListener {
orientationUtils?.resolveByClick()
binding.exoPlayContextId.startWindowFullscreen(this, true, true)
}
}
private fun hidePlayerControls() {
binding.exoPlayContextId.backButton.visibility = View.GONE
binding.exoPlayContextId.titleTextView.visibility = View.GONE
binding.exoPlayContextId.startButton.visibility = View.GONE
binding.exoPlayContextId.findViewById<View>(
com.shuyu.gsyvideoplayer.R.id.current
)?.visibility = View.GONE
binding.exoPlayContextId.findViewById<View>(
com.shuyu.gsyvideoplayer.R.id.total
)?.visibility = View.GONE
binding.exoPlayContextId.findViewById<View>(
com.shuyu.gsyvideoplayer.R.id.progress
)?.visibility = View.INVISIBLE
binding.exoPlayContextId.findViewById<View>(
com.shuyu.gsyvideoplayer.R.id.start
)?.visibility = View.GONE
binding.exoPlayContextId.findViewById<View>(
com.shuyu.gsyvideoplayer.R.id.layout_bottom
)?.setBackgroundColor(Color.TRANSPARENT)
exoPlayerManager.startPlayer()
}
private fun initAlarmRecyclerView() {
@@ -177,20 +193,15 @@ class LiveVideoActivity : BaseActivity() {
startActivity(intent)
}
binding.llVoice.setOnClickListener {
toggleVoice()
}
binding.llLight.setOnClickListener {
toggleLight()
}
isLightOn = !isLightOn
binding.llRecord.setOnClickListener {
toggleRecording()
}
binding.llAlarmLog.setOnClickListener {
Toast.makeText(this, R.string.view_alarm_log, Toast.LENGTH_SHORT).show()
if (isLightOn) {
binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on_open)
} else {
binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on)
}
}
binding.xq.setOnClickListener {
@@ -199,53 +210,6 @@ class LiveVideoActivity : BaseActivity() {
intent.putExtra("from_live_video", true)
startActivity(intent)
}
// 控制台方向按钮点击事件
binding.ivUp.setOnClickListener {
handleDirectionClick("up")
}
binding.ivDown.setOnClickListener {
handleDirectionClick("down")
}
binding.ivLeft.setOnClickListener {
handleDirectionClick("left")
}
binding.ivRight.setOnClickListener {
handleDirectionClick("right")
}
// 触摸反馈
setTouchFeedback(binding.ivUp)
setTouchFeedback(binding.ivDown)
setTouchFeedback(binding.ivLeft)
setTouchFeedback(binding.ivRight)
}
private fun setTouchFeedback(view: View) {
view.setOnTouchListener { v, event ->
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
v.scaleX = 0.9f
v.scaleY = 0.9f
}
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
v.scaleX = 1.0f
v.scaleY = 1.0f
}
}
false
}
}
private fun handleDirectionClick(direction: String) {
val directionText = when (direction) {
"up" -> getString(R.string.direction_up)
"down" -> getString(R.string.direction_down)
"left" -> getString(R.string.direction_left)
"right" -> getString(R.string.direction_right)
else -> ""
}
}
private fun fetchAlarmList(page: Int = 1) {
@@ -260,20 +224,13 @@ class LiveVideoActivity : BaseActivity() {
)
Log.d("LiveVideoActivity", "总条数: ${response.total}, 当前页数据条数: ${response.rows.size}")
val newData = response.rows
if (newData.isEmpty()) {
} else {
newData.take(3).forEachIndexed { index, item ->
Log.d("LiveVideoActivity", "${index + 1}条 - id: ${item.id}, categoryName: ${item.categoryName}, createTime: ${item.createTime}")
}
}
totalPage = if (response.total % pageSize == 0) {
response.total / pageSize
} else {
response.total / pageSize + 1
}
isFirstLoad = false
if (page == 1) {
alarmList.clear()
hasMore = totalPage > 1
@@ -288,135 +245,19 @@ class LiveVideoActivity : BaseActivity() {
}
}
private fun toggleVoice() {
isVoiceEnabled = !isVoiceEnabled
if (isVoiceEnabled) {
Glide.with(this).asGif().load(com.example.defenseapplication.R.drawable.voice_on_open).into(binding.ivVoice)
} else {
binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice)
}
}
private fun toggleLight() {
isLightOn = !isLightOn
if (isLightOn) {
binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on_open)
} else {
binding.ivLight.setImageResource(com.example.defenseapplication.R.drawable.light_on)
}
}
private fun toggleRecording() {
if (!isRecording) {
if (checkRecordAudioPermission()) {
startRecordingMode()
} else {
requestRecordAudioPermission()
}
} else {
stopRecordingMode()
}
}
private fun checkRecordAudioPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
private fun requestRecordAudioPermission() {
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
private fun startRecordingMode() {
isRecording = true
binding.ivRecord.setImageResource(com.example.defenseapplication.R.drawable.copy_open)
binding.llAlarmLog.visibility = View.GONE
binding.consoleBar.visibility = View.VISIBLE
Glide.with(this).asGif().load(com.example.defenseapplication.R.drawable.voice_on_open).into(binding.ivVoice)
isVoiceEnabled = true
startVoiceRecording()
}
private fun stopRecordingMode() {
isRecording = false
binding.ivRecord.setImageResource(com.example.defenseapplication.R.drawable.copy)
binding.llAlarmLog.visibility = View.VISIBLE
binding.consoleBar.visibility = View.GONE
binding.ivVoice.setImageResource(com.example.defenseapplication.R.drawable.voice)
isVoiceEnabled = false
stopVoiceRecording()
stopMicPulseAnimation()
}
private fun stopMicPulseAnimation() {
micPulseAnimation?.let {
binding.ivVoice.clearAnimation()
micPulseAnimation = null
}
}
private fun startVoiceRecording() {
mediaRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
val outputDir = getExternalFilesDir(null) ?: filesDir
val audioDir = File(outputDir, "audio")
if (!audioDir.exists()) {
audioDir.mkdirs()
}
val outputFile = File(audioDir, "voice_record_${System.currentTimeMillis()}.3gp")
setOutputFile(outputFile.absolutePath)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
try {
prepare()
start()
} catch (e: IOException) {
e.printStackTrace()
Toast.makeText(this@LiveVideoActivity, R.string.recording_start_failed, Toast.LENGTH_SHORT).show()
}
}
}
private fun stopVoiceRecording() {
mediaRecorder?.let {
try {
it.stop()
it.release()
} catch (e: Exception) {
e.printStackTrace()
}
mediaRecorder = null
}
}
override fun onResume() {
super.onResume()
exoPlayerManager.onResume()
binding.exoPlayContextId.onVideoResume()
}
override fun onPause() {
super.onPause()
exoPlayerManager.onPause()
binding.exoPlayContextId.onVideoPause()
}
override fun onDestroy() {
super.onDestroy()
exoPlayerManager.onDestroy()
orientationUtils?.releaseListener()
binding.exoPlayContextId.release()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
exoPlayerManager.onConfigurationChanged(newConfig)
}
}
}

View File

@@ -32,14 +32,6 @@ class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (UserinfoUtil.getUserInfo() != null) {
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
return
}
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)

View File

@@ -0,0 +1,61 @@
package com.example.defenseapplication.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
class MyGSYVideoPlayer @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : StandardGSYVideoPlayer(context, attrs) {
/** 控制栏显示/隐藏监听 */
var onControlVisibilityChanged: ((Boolean) -> Unit)? = null
// 播放中显示控制栏
override fun changeUiToPlayingShow() {
super.changeUiToPlayingShow()
Log.e("GSY_TEST", "通知显示按钮")
onControlVisibilityChanged?.invoke(true)
}
// 暂停中显示控制栏hideAllExceptFullscreen
override fun changeUiToPauseShow() {
super.changeUiToPauseShow()
onControlVisibilityChanged?.invoke(true)
}
// 播放中清除控制栏
override fun changeUiToPlayingClear() {
super.changeUiToPlayingClear()
Log.e("GSY_TEST", "changeUiToPlayingClear")
onControlVisibilityChanged?.invoke(false)
}
// 暂停中清除控制栏
override fun changeUiToPauseClear() {
super.changeUiToPauseClear()
onControlVisibilityChanged?.invoke(false)
}
// 完成播放
override fun changeUiToCompleteClear() {
super.changeUiToCompleteClear()
onControlVisibilityChanged?.invoke(false)
}
override fun hideAllWidget() {
Log.e("GSY_TEST", "通知隐藏按钮")
super.hideAllWidget()
onControlVisibilityChanged?.invoke(false)
}
override fun changeUiToNormal() {
Log.e("GSY_TEST", "changeUiToNormal")
super.changeUiToNormal()
}
override fun changeUiToPreparingShow() {
Log.e("GSY_TEST", "changeUiToPreparingShow")
super.changeUiToPreparingShow()
}
}

View File

@@ -0,0 +1,20 @@
package com.example.defenseapplication.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
class MyGSYVideoPlayerLive @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : StandardGSYVideoPlayer(context, attrs) {
override fun init(context: Context) {
super.init(context)
startButton.visibility = View.GONE
}
override fun updateStartImage() {
startButton.visibility = View.GONE
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,31 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景 -->
<item android:id="@android:id/background">
<shape>
<corners android:radius="2dp"/>
<solid android:color="#333333"/>
</shape>
</item>
<!-- 缓冲 -->
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2dp"/>
<solid android:color="#888888"/>
</shape>
</clip>
</item>
<!-- 播放进度 -->
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="2dp"/>
<solid android:color="#09C2BD"/>
</shape>
</clip>
</item>
</layer-list>

View File

@@ -0,0 +1,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="12dp"
android:height="12dp"/>
<solid android:color="#09C2BD"/>
<corners android:radius="6dp"/>
</shape>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".home.StartActivity">
<!-- Logo 图片:横竖屏均居中 -->
<ImageView
android:id="@+id/logoFrame"
android:layout_width="160dp"
android:layout_height="120dp"
android:src="@mipmap/loge1"
android:padding="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -2,10 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_marginTop="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:orientation="vertical">
<!-- 视频播放区域和悬浮按钮 -->
@@ -14,137 +12,51 @@
android:layout_height="wrap_content">
<!-- 视频播放区域 -->
<chuangyuan.ycj.videolibrary.widget.VideoPlayerView
<com.example.defenseapplication.view.MyGSYVideoPlayer
android:layout_marginTop="40dp"
android:id="@+id/exoPlayContextId"
android:layout_width="match_parent"
android:layout_height="280dp"
android:background="@android:color/transparent"
app:use_controller="false"/>
android:layout_height="280dp"/>
<!-- 自定义顶部栏 - 返回、下载、删除在同一行 -->
<!-- 右上侧按钮层 -->
<LinearLayout
android:id="@+id/functionBar"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_gravity="top"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:visibility="visible">
<ImageView
android:id="@+id/ivBack"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:src="@drawable/back_white"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_marginTop="50dp"
android:id="@+id/rightMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="15dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivDownload"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/download"/>
<ImageView
android:id="@+id/ivDelete"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:padding="8dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="20dp"
android:src="@drawable/delete"/>
</LinearLayout>
<!-- 右下角按钮区域 - 静音、全屏按钮 -->
<!-- 右下侧按钮层 -->
<LinearLayout
android:id="@+id/muteBar"
android:id="@+id/rightDow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="6dp"
android:orientation="vertical"
android:gravity="center">
android:layout_gravity="end|bottom"
android:layout_marginEnd="15dp">
<ImageView
android:id="@+id/ivMute"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginBottom="60dp"
android:src="@drawable/un_mute"/>
<ImageView
android:id="@+id/ivFullScreen"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:layout_marginTop="8dp"
android:src="@drawable/full_size"/>
</LinearLayout>
<!-- 左下角播放按钮 -->
<LinearLayout
android:id="@+id/playBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start">
<ImageView
android:id="@+id/ivPlay"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:src="@drawable/zt"/>
</LinearLayout>
<!-- 底部进度条和时间区域 -->
<LinearLayout
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:paddingLeft="70dp"
android:paddingRight="70dp">
<!-- 当前时间 -->
<TextView
android:id="@+id/tvCurrentTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="#FFFFFF"
android:textSize="14sp"/>
<!-- 进度条 -->
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/custom_seekbar"
android:thumb="@drawable/custom_thumb"/>
<!-- 总时长 -->
<TextView
android:id="@+id/tvTotalTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="#FFFFFF"
android:textSize="14sp"/>
</LinearLayout>
</FrameLayout>

View File

@@ -43,13 +43,12 @@
</LinearLayout>
<!-- 视频播放区域 -->
<chuangyuan.ycj.videolibrary.widget.VideoPlayerView
<com.example.defenseapplication.view.MyGSYVideoPlayerLive
android:id="@+id/exoPlayContextId"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@android:color/transparent"
app:use_controller="false"/>
android:layout_height="280dp"/>
<!-- 功能按钮区域 -->
<LinearLayout