添加点播播放功能,创建VodPlayActivity及相关UI,更新FeatureHubActivity以支持点播配置
This commit is contained in:
Binary file not shown.
@@ -64,6 +64,14 @@
|
|||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:parentActivityName=".FeatureHubActivity" />
|
android:parentActivityName=".FeatureHubActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".vod.VodPlayActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
android:theme="@style/Theme.AVDemo.NoActionBar"
|
||||||
|
android:parentActivityName=".FeatureHubActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".interactive.InteractiveLiveActivity"
|
android:name=".interactive.InteractiveLiveActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.demo.SellyCloudSDK.avdemo.AvDemoSettingsStore
|
|||||||
import com.demo.SellyCloudSDK.databinding.ActivityFeatureHubBinding
|
import com.demo.SellyCloudSDK.databinding.ActivityFeatureHubBinding
|
||||||
import com.demo.SellyCloudSDK.databinding.DialogLivePresetSettingsBinding
|
import com.demo.SellyCloudSDK.databinding.DialogLivePresetSettingsBinding
|
||||||
import com.demo.SellyCloudSDK.databinding.DialogPlayConfigOverlayBinding
|
import com.demo.SellyCloudSDK.databinding.DialogPlayConfigOverlayBinding
|
||||||
|
import com.demo.SellyCloudSDK.databinding.DialogVodInputBinding
|
||||||
import com.demo.SellyCloudSDK.databinding.DialogPushProtocolSheetBinding
|
import com.demo.SellyCloudSDK.databinding.DialogPushProtocolSheetBinding
|
||||||
import com.demo.SellyCloudSDK.interactive.InteractiveLiveActivity
|
import com.demo.SellyCloudSDK.interactive.InteractiveLiveActivity
|
||||||
import com.demo.SellyCloudSDK.live.LivePlayActivity
|
import com.demo.SellyCloudSDK.live.LivePlayActivity
|
||||||
@@ -30,6 +31,7 @@ import com.demo.SellyCloudSDK.live.square.AliveStreamAdapter
|
|||||||
import com.demo.SellyCloudSDK.live.square.AliveStreamItem
|
import com.demo.SellyCloudSDK.live.square.AliveStreamItem
|
||||||
import com.demo.SellyCloudSDK.login.DemoLoginStore
|
import com.demo.SellyCloudSDK.login.DemoLoginStore
|
||||||
import com.demo.SellyCloudSDK.login.LoginActivity
|
import com.demo.SellyCloudSDK.login.LoginActivity
|
||||||
|
import com.demo.SellyCloudSDK.vod.VodPlayActivity
|
||||||
import com.sellycloud.sellycloudsdk.SellyLiveMode
|
import com.sellycloud.sellycloudsdk.SellyLiveMode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -118,6 +120,9 @@ class FeatureHubActivity : AppCompatActivity() {
|
|||||||
binding.btnHomeLivePull.setOnClickListener {
|
binding.btnHomeLivePull.setOnClickListener {
|
||||||
showPlayConfigDialog()
|
showPlayConfigDialog()
|
||||||
}
|
}
|
||||||
|
binding.btnHomeVod.setOnClickListener {
|
||||||
|
showVodConfigDialog()
|
||||||
|
}
|
||||||
binding.btnCallSingleChat.setOnClickListener {
|
binding.btnCallSingleChat.setOnClickListener {
|
||||||
startInteractive(InteractiveLiveActivity.DEFAULT_CALL_TYPE_P2P)
|
startInteractive(InteractiveLiveActivity.DEFAULT_CALL_TYPE_P2P)
|
||||||
}
|
}
|
||||||
@@ -390,6 +395,27 @@ class FeatureHubActivity : AppCompatActivity() {
|
|||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showVodConfigDialog() {
|
||||||
|
val dialog = Dialog(this, R.style.Theme_AVDemo_Dialog_FullscreenOverlay)
|
||||||
|
val dialogBinding = DialogVodInputBinding.inflate(layoutInflater)
|
||||||
|
dialog.setContentView(dialogBinding.root)
|
||||||
|
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
|
dialog.setCancelable(true)
|
||||||
|
|
||||||
|
dialogBinding.btnClose.setOnClickListener { dialog.dismiss() }
|
||||||
|
dialogBinding.btnStartVod.setOnClickListener {
|
||||||
|
val input = dialogBinding.etVodUrl.text?.toString()?.trim().orEmpty()
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
Toast.makeText(this, getString(R.string.vod_config_hint), Toast.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
startActivity(VodPlayActivity.createIntent(this, input))
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupSettingsSave() {
|
private fun setupSettingsSave() {
|
||||||
binding.btnSaveSettings.setOnClickListener {
|
binding.btnSaveSettings.setOnClickListener {
|
||||||
val settings = uiToSettingsOrNull() ?: return@setOnClickListener
|
val settings = uiToSettingsOrNull() ?: return@setOnClickListener
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
SellyPlayerState.Reconnecting -> setPlayingUi(false)
|
SellyPlayerState.Reconnecting -> setPlayingUi(false)
|
||||||
SellyPlayerState.Playing -> setPlayingUi(true)
|
SellyPlayerState.Playing -> setPlayingUi(true)
|
||||||
SellyPlayerState.Paused -> setPlayingUi(false)
|
SellyPlayerState.Paused -> setPlayingUi(false)
|
||||||
SellyPlayerState.Stopped -> setPlayingUi(false)
|
SellyPlayerState.StoppedOrEnded -> setPlayingUi(false)
|
||||||
SellyPlayerState.Failed -> setPlayingUi(false)
|
SellyPlayerState.Failed -> setPlayingUi(false)
|
||||||
SellyPlayerState.Idle -> Unit
|
SellyPlayerState.Idle -> Unit
|
||||||
}
|
}
|
||||||
@@ -722,7 +722,7 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
SellyPlayerState.Reconnecting -> "重连中"
|
SellyPlayerState.Reconnecting -> "重连中"
|
||||||
SellyPlayerState.Playing -> "播放中"
|
SellyPlayerState.Playing -> "播放中"
|
||||||
SellyPlayerState.Paused -> "已暂停"
|
SellyPlayerState.Paused -> "已暂停"
|
||||||
SellyPlayerState.Stopped -> "已停止"
|
SellyPlayerState.StoppedOrEnded -> "已停止"
|
||||||
SellyPlayerState.Failed -> "失败"
|
SellyPlayerState.Failed -> "失败"
|
||||||
SellyPlayerState.Idle -> "空闲"
|
SellyPlayerState.Idle -> "空闲"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,645 @@
|
|||||||
|
package com.demo.SellyCloudSDK.vod
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.demo.SellyCloudSDK.R
|
||||||
|
import com.demo.SellyCloudSDK.databinding.ActivityVodPlayBinding
|
||||||
|
import com.demo.SellyCloudSDK.live.util.GalleryImageSaver
|
||||||
|
import com.sellycloud.sellycloudsdk.SellyLiveError
|
||||||
|
import com.sellycloud.sellycloudsdk.SellyPlayerState
|
||||||
|
import com.sellycloud.sellycloudsdk.SellyVodPlayer
|
||||||
|
import com.sellycloud.sellycloudsdk.SellyVodPlayerDelegate
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class VodPlayActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityVodPlayBinding
|
||||||
|
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
|
||||||
|
private var player: SellyVodPlayer? = null
|
||||||
|
private var renderView: View? = null
|
||||||
|
|
||||||
|
private var isPlaying = false
|
||||||
|
private var isMuted = false
|
||||||
|
private var currentState: SellyPlayerState = SellyPlayerState.Idle
|
||||||
|
private var durationMs: Long = 0
|
||||||
|
private var isUserSeeking = false
|
||||||
|
|
||||||
|
private var playAttemptStartElapsedMs: Long? = null
|
||||||
|
private var firstVideoFrameElapsedMs: Long? = null
|
||||||
|
private var firstVideoFrameCostMs: Long? = null
|
||||||
|
private var firstAudioFrameElapsedMs: Long? = null
|
||||||
|
private var firstAudioFrameCostMs: Long? = null
|
||||||
|
private var bufferingActive = false
|
||||||
|
|
||||||
|
private var progressJob: Job? = null
|
||||||
|
|
||||||
|
private val logLines: ArrayDeque<String> = ArrayDeque()
|
||||||
|
private val logTimeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
private var logDialog: AlertDialog? = null
|
||||||
|
private var logSummaryView: TextView? = null
|
||||||
|
private var logContentView: TextView? = null
|
||||||
|
private var logFloatingButton: View? = null
|
||||||
|
|
||||||
|
private val storagePermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
|
if (!granted) {
|
||||||
|
Toast.makeText(this, "需要存储权限才能保存截图", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityVodPlayBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
supportActionBar?.hide()
|
||||||
|
addLogFloatingButton()
|
||||||
|
|
||||||
|
binding.btnClose.setOnClickListener { finish() }
|
||||||
|
binding.actionPlay.setOnClickListener { togglePlay() }
|
||||||
|
binding.actionMute.setOnClickListener { toggleMute() }
|
||||||
|
binding.actionScreenshot.setOnClickListener { captureCurrentFrame() }
|
||||||
|
binding.actionForward.setOnClickListener { seekForward() }
|
||||||
|
|
||||||
|
binding.seekBar.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: android.widget.SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
|
if (fromUser) {
|
||||||
|
binding.tvCurrentTime.text = formatTime(progress.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar) {
|
||||||
|
isUserSeeking = true
|
||||||
|
logEvent("用户操作: 开始拖动进度条")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar) {
|
||||||
|
val position = seekBar.progress.toLong()
|
||||||
|
logEvent("用户操作: 跳转到 ${formatTime(position)}")
|
||||||
|
player?.seekTo(position)
|
||||||
|
isUserSeeking = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val url = intent.getStringExtra(EXTRA_VOD_URL)?.trim().orEmpty()
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
Toast.makeText(this, "请输入有效的播放地址", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initPlayer(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
logEvent("释放播放器")
|
||||||
|
logDialog?.dismiss()
|
||||||
|
progressJob?.cancel()
|
||||||
|
player?.release()
|
||||||
|
player = null
|
||||||
|
uiScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (isPlaying) {
|
||||||
|
player?.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPlayer(url: String) {
|
||||||
|
val vodPlayer = SellyVodPlayer.initWithUrl(this, url).also { client ->
|
||||||
|
client.autoPlay = true
|
||||||
|
client.delegate = object : SellyVodPlayerDelegate {
|
||||||
|
override fun playbackStateChanged(state: SellyPlayerState) {
|
||||||
|
runOnUiThread {
|
||||||
|
currentState = state
|
||||||
|
when (state) {
|
||||||
|
SellyPlayerState.Playing -> setPlayingUi(true)
|
||||||
|
SellyPlayerState.Paused,
|
||||||
|
SellyPlayerState.StoppedOrEnded,
|
||||||
|
SellyPlayerState.Failed,
|
||||||
|
SellyPlayerState.Connecting,
|
||||||
|
SellyPlayerState.Reconnecting,
|
||||||
|
SellyPlayerState.Idle -> setPlayingUi(false)
|
||||||
|
}
|
||||||
|
logEvent("状态变更: ${formatState(state)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepared(durationMs: Long) {
|
||||||
|
runOnUiThread {
|
||||||
|
this@VodPlayActivity.durationMs = durationMs
|
||||||
|
binding.seekBar.max = durationMs.toInt()
|
||||||
|
binding.tvTotalTime.text = formatTime(durationMs)
|
||||||
|
logEvent("准备完成: 总时长=${formatTime(durationMs)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFirstVideoFrameRendered() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (firstVideoFrameElapsedMs != null) return@runOnUiThread
|
||||||
|
val startMs = playAttemptStartElapsedMs ?: return@runOnUiThread
|
||||||
|
firstVideoFrameElapsedMs = SystemClock.elapsedRealtime()
|
||||||
|
firstVideoFrameCostMs = firstVideoFrameElapsedMs!! - startMs
|
||||||
|
logEvent("首帧视频耗时=${firstVideoFrameCostMs}ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFirstAudioFrameRendered() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (firstAudioFrameElapsedMs != null) return@runOnUiThread
|
||||||
|
val startMs = playAttemptStartElapsedMs ?: return@runOnUiThread
|
||||||
|
firstAudioFrameElapsedMs = SystemClock.elapsedRealtime()
|
||||||
|
firstAudioFrameCostMs = firstAudioFrameElapsedMs!! - startMs
|
||||||
|
logEvent("首帧音频耗时=${firstAudioFrameCostMs}ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSeekComplete() {
|
||||||
|
runOnUiThread {
|
||||||
|
isUserSeeking = false
|
||||||
|
logEvent("Seek 完成")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompletion() {
|
||||||
|
runOnUiThread {
|
||||||
|
logEvent("播放完成")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBufferingUpdate(percent: Int) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (percent <= 0) {
|
||||||
|
if (!bufferingActive) {
|
||||||
|
bufferingActive = true
|
||||||
|
logEvent("缓冲中")
|
||||||
|
}
|
||||||
|
return@runOnUiThread
|
||||||
|
}
|
||||||
|
if (bufferingActive) {
|
||||||
|
bufferingActive = false
|
||||||
|
logEvent("缓冲恢复")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: SellyLiveError) {
|
||||||
|
runOnUiThread {
|
||||||
|
logEvent("错误: ${error.message}")
|
||||||
|
Toast.makeText(this@VodPlayActivity, error.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.setMuted(isMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderView = vodPlayer.attachRenderView(binding.renderContainer)
|
||||||
|
player = vodPlayer
|
||||||
|
startPlayAttempt()
|
||||||
|
vodPlayer.prepareAsync()
|
||||||
|
startProgressUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun togglePlay() {
|
||||||
|
if (isPlaying) {
|
||||||
|
logEvent("用户操作: 暂停")
|
||||||
|
player?.pause()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logEvent("用户操作: 播放")
|
||||||
|
if (currentState == SellyPlayerState.Paused) {
|
||||||
|
player?.play()
|
||||||
|
} else {
|
||||||
|
startPlayAttempt()
|
||||||
|
player?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleMute() {
|
||||||
|
isMuted = !isMuted
|
||||||
|
player?.setMuted(isMuted)
|
||||||
|
binding.tvMuteLabel.setText(if (isMuted) R.string.play_ctrl_unmute else R.string.play_ctrl_mute)
|
||||||
|
logEvent(if (isMuted) "用户操作: 静音" else "用户操作: 取消静音")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seekForward() {
|
||||||
|
logEvent("用户操作: 快进10秒")
|
||||||
|
player?.seekBy(SEEK_FORWARD_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startProgressUpdates() {
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = uiScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
if (!isUserSeeking) {
|
||||||
|
val current = player?.getCurrentPositionMs() ?: 0L
|
||||||
|
val duration = player?.getDurationMs() ?: durationMs
|
||||||
|
if (duration > 0) {
|
||||||
|
if (duration != durationMs) {
|
||||||
|
durationMs = duration
|
||||||
|
binding.seekBar.max = duration.toInt()
|
||||||
|
binding.tvTotalTime.text = formatTime(duration)
|
||||||
|
}
|
||||||
|
binding.seekBar.progress = current.coerceAtMost(duration).toInt()
|
||||||
|
binding.tvCurrentTime.text = formatTime(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPlayingUi(playing: Boolean) {
|
||||||
|
isPlaying = playing
|
||||||
|
binding.tvPlayLabel.setText(if (playing) R.string.play_ctrl_pause else R.string.play_ctrl_play)
|
||||||
|
binding.ivPlayIcon.setImageResource(if (playing) R.drawable.ic_av_pause else R.drawable.ic_av_play)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun captureCurrentFrame() {
|
||||||
|
logEvent("用户操作: 截图")
|
||||||
|
val view = renderView
|
||||||
|
if (view == null) {
|
||||||
|
Toast.makeText(this, "当前无可截图的播放画面", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!hasScreenshotWritePermission()) {
|
||||||
|
requestScreenshotWritePermission()
|
||||||
|
Toast.makeText(this, "请授权后重试截图", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
captureSurfaceViewAndSave(view, prefix = "vod")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun captureSurfaceViewAndSave(view: View, prefix: String) {
|
||||||
|
if (view.width <= 0 || view.height <= 0) {
|
||||||
|
Toast.makeText(this, "视图尚未布局完成,稍后再试", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (view !is android.view.SurfaceView) {
|
||||||
|
Toast.makeText(this, "当前视图不支持截图", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val bmp = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
||||||
|
try {
|
||||||
|
val handler = android.os.Handler(mainLooper)
|
||||||
|
android.view.PixelCopy.request(view, bmp, { result ->
|
||||||
|
if (result == android.view.PixelCopy.SUCCESS) {
|
||||||
|
uiScope.launch(Dispatchers.IO) {
|
||||||
|
val ok = saveBitmapToGallery(bmp, prefix)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
Toast.makeText(this@VodPlayActivity, if (ok) "截图已保存到相册" else "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "截图失败,错误码: $result", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}, handler)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, "截图异常: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveBitmapToGallery(bitmap: Bitmap, prefix: String): Boolean {
|
||||||
|
val filename = "${prefix}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())}.png"
|
||||||
|
return GalleryImageSaver.savePng(this, bitmap, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasScreenshotWritePermission(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return true
|
||||||
|
return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestScreenshotWritePermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return
|
||||||
|
storagePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLogFloatingButton() {
|
||||||
|
val sizePx = dpToPx(44)
|
||||||
|
val marginEndPx = dpToPx(18)
|
||||||
|
val controlBarHeight = resources.getDimensionPixelSize(R.dimen.av_control_bar_height)
|
||||||
|
val marginBottomPx = controlBarHeight + dpToPx(80)
|
||||||
|
val bgDrawable = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(
|
||||||
|
Color.parseColor("#B31F2937"),
|
||||||
|
Color.parseColor("#80242E3A")
|
||||||
|
)).apply {
|
||||||
|
shape = GradientDrawable.OVAL
|
||||||
|
setStroke(dpToPx(1), Color.parseColor("#55FFFFFF"))
|
||||||
|
}
|
||||||
|
val button = AppCompatTextView(this).apply {
|
||||||
|
text = "日志"
|
||||||
|
setTextColor(Color.parseColor("#F8FAFC"))
|
||||||
|
textSize = 11f
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
background = bgDrawable
|
||||||
|
elevation = dpToPx(4).toFloat()
|
||||||
|
setShadowLayer(2f, 0f, 1f, Color.parseColor("#66000000"))
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = true
|
||||||
|
contentDescription = "播放日志"
|
||||||
|
setOnClickListener { showLogDialog() }
|
||||||
|
setOnLongClickListener {
|
||||||
|
clearLogs(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val params = FrameLayout.LayoutParams(sizePx, sizePx).apply {
|
||||||
|
gravity = Gravity.END or Gravity.BOTTOM
|
||||||
|
marginEnd = marginEndPx
|
||||||
|
bottomMargin = marginBottomPx
|
||||||
|
}
|
||||||
|
addContentView(button, params)
|
||||||
|
logFloatingButton = button
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLogDialog() {
|
||||||
|
if (logDialog?.isShowing == true) {
|
||||||
|
refreshLogDialogContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val dialogBackground = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(
|
||||||
|
Color.parseColor("#CC0F172A"),
|
||||||
|
Color.parseColor("#A60F172A")
|
||||||
|
)).apply {
|
||||||
|
cornerRadius = dpToPx(16).toFloat()
|
||||||
|
setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"))
|
||||||
|
}
|
||||||
|
val container = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16))
|
||||||
|
}
|
||||||
|
val dialogTitleView = TextView(this).apply {
|
||||||
|
text = "点播调试日志"
|
||||||
|
setTextColor(Color.parseColor("#F8FAFC"))
|
||||||
|
textSize = 16f
|
||||||
|
setTypeface(typeface, Typeface.BOLD)
|
||||||
|
}
|
||||||
|
val summaryTitle = TextView(this).apply {
|
||||||
|
text = "摘要"
|
||||||
|
setTextColor(Color.parseColor("#E2E8F0"))
|
||||||
|
textSize = 12f
|
||||||
|
}
|
||||||
|
val summaryView = TextView(this).apply {
|
||||||
|
text = buildLogSummary()
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
setTextColor(Color.parseColor("#E2E8F0"))
|
||||||
|
textSize = 11f
|
||||||
|
setLineSpacing(dpToPx(1).toFloat(), 1.05f)
|
||||||
|
}
|
||||||
|
val summaryContainer = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
background = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(
|
||||||
|
Color.parseColor("#26FFFFFF"),
|
||||||
|
Color.parseColor("#14FFFFFF")
|
||||||
|
)).apply {
|
||||||
|
cornerRadius = dpToPx(10).toFloat()
|
||||||
|
setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"))
|
||||||
|
}
|
||||||
|
setPadding(dpToPx(8), dpToPx(6), dpToPx(8), dpToPx(6))
|
||||||
|
addView(summaryView)
|
||||||
|
}
|
||||||
|
val logTitle = TextView(this).apply {
|
||||||
|
text = "事件日志"
|
||||||
|
setTextColor(Color.parseColor("#E2E8F0"))
|
||||||
|
textSize = 12f
|
||||||
|
}
|
||||||
|
val logView = TextView(this).apply {
|
||||||
|
text = buildLogContent()
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
setTextColor(Color.parseColor("#E2E8F0"))
|
||||||
|
textSize = 11f
|
||||||
|
setLineSpacing(dpToPx(1).toFloat(), 1.05f)
|
||||||
|
setTextIsSelectable(true)
|
||||||
|
}
|
||||||
|
val scrollView = ScrollView(this).apply {
|
||||||
|
isFillViewport = true
|
||||||
|
addView(
|
||||||
|
logView,
|
||||||
|
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val logContainer = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
background = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(
|
||||||
|
Color.parseColor("#26FFFFFF"),
|
||||||
|
Color.parseColor("#14FFFFFF")
|
||||||
|
)).apply {
|
||||||
|
cornerRadius = dpToPx(10).toFloat()
|
||||||
|
setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"))
|
||||||
|
}
|
||||||
|
setPadding(dpToPx(8), dpToPx(6), dpToPx(8), dpToPx(6))
|
||||||
|
}
|
||||||
|
val logHeight = (resources.displayMetrics.heightPixels * 0.35f).toInt()
|
||||||
|
logContainer.addView(
|
||||||
|
scrollView,
|
||||||
|
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, logHeight)
|
||||||
|
)
|
||||||
|
container.addView(dialogTitleView)
|
||||||
|
container.addView(spaceView(dpToPx(6)))
|
||||||
|
container.addView(summaryTitle)
|
||||||
|
container.addView(summaryContainer)
|
||||||
|
container.addView(spaceView(dpToPx(8)))
|
||||||
|
container.addView(logTitle)
|
||||||
|
container.addView(logContainer)
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(container)
|
||||||
|
.setPositiveButton("复制", null)
|
||||||
|
.setNeutralButton("清空", null)
|
||||||
|
.setNegativeButton("关闭", null)
|
||||||
|
.create()
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
dialog.window?.setBackgroundDrawable(dialogBackground)
|
||||||
|
dialog.window?.setLayout(
|
||||||
|
(resources.displayMetrics.widthPixels * 0.88f).toInt(),
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
copyLogsToClipboard()
|
||||||
|
}
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||||
|
clearLogs(false)
|
||||||
|
}
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
logDialog = null
|
||||||
|
logSummaryView = null
|
||||||
|
logContentView = null
|
||||||
|
}
|
||||||
|
logDialog = dialog
|
||||||
|
logSummaryView = summaryView
|
||||||
|
logContentView = logView
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLogSummary(): String {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
builder.append("状态: ").append(formatState(currentState)).append('\n')
|
||||||
|
builder.append("是否播放中: ").append(if (isPlaying) "是" else "否").append('\n')
|
||||||
|
builder.append("是否静音: ").append(if (isMuted) "是" else "否").append('\n')
|
||||||
|
builder.append("总时长: ").append(if (durationMs > 0) formatTime(durationMs) else "--").append('\n')
|
||||||
|
builder.append("当前进度: ").append(formatTime(player?.getCurrentPositionMs() ?: 0L)).append('\n')
|
||||||
|
builder.append("首帧视频耗时(ms): ").append(firstVideoFrameCostMs ?: "未统计").append('\n')
|
||||||
|
builder.append("首帧音频耗时(ms): ").append(firstAudioFrameCostMs ?: "未统计").append('\n')
|
||||||
|
val attemptElapsed = playAttemptStartElapsedMs?.let { SystemClock.elapsedRealtime() - it }
|
||||||
|
if (attemptElapsed == null) {
|
||||||
|
builder.append("本次播放已耗时(ms): 未开始").append('\n')
|
||||||
|
} else {
|
||||||
|
builder.append("本次播放已耗时(ms): ").append(attemptElapsed).append('\n')
|
||||||
|
}
|
||||||
|
builder.append("日志行数: ").append(logLines.size)
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLogContent(): String {
|
||||||
|
if (logLines.isEmpty()) return "暂无日志"
|
||||||
|
return logLines.joinToString(separator = "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logEvent(message: String) {
|
||||||
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
appendLogLine(message)
|
||||||
|
} else {
|
||||||
|
runOnUiThread { appendLogLine(message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLogLine(message: String) {
|
||||||
|
val timestamp = logTimeFormat.format(Date())
|
||||||
|
if (logLines.size >= MAX_LOG_LINES) {
|
||||||
|
logLines.removeFirst()
|
||||||
|
}
|
||||||
|
logLines.addLast("$timestamp $message")
|
||||||
|
refreshLogDialogContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshLogDialogContent() {
|
||||||
|
val dialog = logDialog ?: return
|
||||||
|
if (!dialog.isShowing) return
|
||||||
|
logSummaryView?.text = buildLogSummary()
|
||||||
|
logContentView?.text = buildLogContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyLogsToClipboard() {
|
||||||
|
val content = buildString {
|
||||||
|
append("摘要\n")
|
||||||
|
append(buildLogSummary())
|
||||||
|
append("\n\n事件日志\n")
|
||||||
|
append(buildLogContent())
|
||||||
|
}
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("playback_logs", content))
|
||||||
|
Toast.makeText(this, "已复制日志", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearLogs(showToast: Boolean) {
|
||||||
|
logLines.clear()
|
||||||
|
refreshLogDialogContent()
|
||||||
|
if (showToast) {
|
||||||
|
Toast.makeText(this, "日志已清空", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPlayAttempt() {
|
||||||
|
playAttemptStartElapsedMs = SystemClock.elapsedRealtime()
|
||||||
|
firstVideoFrameElapsedMs = null
|
||||||
|
firstVideoFrameCostMs = null
|
||||||
|
firstAudioFrameElapsedMs = null
|
||||||
|
firstAudioFrameCostMs = null
|
||||||
|
bufferingActive = false
|
||||||
|
logEvent("播放尝试开始")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatState(state: SellyPlayerState): String {
|
||||||
|
return "${stateLabel(state)}(${state.name})"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stateLabel(state: SellyPlayerState): String {
|
||||||
|
return when (state) {
|
||||||
|
SellyPlayerState.Connecting -> "连接中"
|
||||||
|
SellyPlayerState.Reconnecting -> "重连中"
|
||||||
|
SellyPlayerState.Playing -> "播放中"
|
||||||
|
SellyPlayerState.Paused -> "已暂停"
|
||||||
|
SellyPlayerState.StoppedOrEnded -> "已停止"
|
||||||
|
SellyPlayerState.Failed -> "失败"
|
||||||
|
SellyPlayerState.Idle -> "空闲"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatTime(ms: Long): String {
|
||||||
|
val totalSeconds = (ms / 1000).coerceAtLeast(0)
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
val minutes = (totalSeconds / 60) % 60
|
||||||
|
val hours = totalSeconds / 3600
|
||||||
|
return if (hours > 0) {
|
||||||
|
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dpToPx(dp: Int): Int {
|
||||||
|
return (dp * resources.displayMetrics.density + 0.5f).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun spaceView(heightPx: Int): View {
|
||||||
|
return View(this).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
heightPx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_VOD_URL = "extra_vod_url"
|
||||||
|
private const val MAX_LOG_LINES = 200
|
||||||
|
private const val SEEK_FORWARD_MS = 10_000L
|
||||||
|
|
||||||
|
fun createIntent(context: Context, url: String): Intent {
|
||||||
|
return Intent(context, VodPlayActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_VOD_URL, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
example/src/main/res/drawable/ic_av_pause.xml
Normal file
10
example/src/main/res/drawable/ic_av_pause.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M6,5h4v14H6zM14,5h4v14h-4z" />
|
||||||
|
</vector>
|
||||||
@@ -110,6 +110,40 @@
|
|||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/btnHomeVod"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_av_home_button"
|
||||||
|
android:clickable="true"
|
||||||
|
android:clipToOutline="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:contentDescription="@string/home_vod"
|
||||||
|
android:src="@drawable/ic_av_play"
|
||||||
|
app:tint="@color/brand_primary_text_on" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/home_vod"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
201
example/src/main/res/layout/activity_vod_play.xml
Normal file
201
example/src/main/res/layout/activity_vod_play.xml
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<?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"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/av_surface_black">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/renderContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/progressRow"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnClose"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@drawable/bg_av_icon_circle"
|
||||||
|
android:contentDescription="@string/close"
|
||||||
|
android:src="@drawable/ic_av_close"
|
||||||
|
app:tint="@color/av_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/progressRow"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/controlBar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCurrentTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="00:00"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTotalTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="00:00"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controlBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/av_control_bar_height"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:background="@drawable/bg_av_control_bar"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/actionPlay"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivPlayIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/play_ctrl_play"
|
||||||
|
android:src="@drawable/ic_av_play"
|
||||||
|
app:tint="@color/brand_primary_text_on" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPlayLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/play_ctrl_play"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/actionMute"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/play_ctrl_mute"
|
||||||
|
android:src="@drawable/ic_av_volume"
|
||||||
|
app:tint="@color/brand_primary_text_on" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMuteLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/play_ctrl_mute"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/actionScreenshot"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/play_ctrl_screenshot"
|
||||||
|
android:src="@drawable/ic_av_camera"
|
||||||
|
app:tint="@color/brand_primary_text_on" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/play_ctrl_screenshot"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/actionForward"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/play_ctrl_forward_10"
|
||||||
|
android:src="@drawable/ic_av_replay_10"
|
||||||
|
app:tint="@color/brand_primary_text_on" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/play_ctrl_forward_10"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
74
example/src/main/res/layout/dialog_vod_input.xml
Normal file
74
example/src/main/res/layout/dialog_vod_input.xml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/av_overlay_dim">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnClose"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@drawable/bg_av_icon_circle"
|
||||||
|
android:contentDescription="@string/close"
|
||||||
|
android:src="@drawable/ic_av_close"
|
||||||
|
app:tint="@color/av_text_primary" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/card"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_av_dialog_card_gray"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="18dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingEnd="18dp"
|
||||||
|
android:paddingBottom="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/vod_config_title"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:text="@string/vod_config_hint"
|
||||||
|
android:textColor="@color/av_text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etVodUrl"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/av_field_height"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/bg_av_input_field"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textColorHint="@color/av_text_hint"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStartVod"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/av_primary_button_height"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:background="@drawable/selector_av_primary_button"
|
||||||
|
android:text="@string/play_start"
|
||||||
|
android:textColor="@color/brand_primary_text_on"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<string name="home_live_push">开始直播</string>
|
<string name="home_live_push">开始直播</string>
|
||||||
<string name="home_live_pull">自定义播放</string>
|
<string name="home_live_pull">自定义播放</string>
|
||||||
|
<string name="home_vod">点播播放</string>
|
||||||
<string name="home_single_chat">音视频单聊</string>
|
<string name="home_single_chat">音视频单聊</string>
|
||||||
<string name="home_conference">音视频会议</string>
|
<string name="home_conference">音视频会议</string>
|
||||||
<string name="multi_play">多路播放</string>
|
<string name="multi_play">多路播放</string>
|
||||||
@@ -81,6 +82,8 @@
|
|||||||
<string name="play_config_stream_hint">Stream ID / URL。请输入 Stream ID 或完整 URL</string>
|
<string name="play_config_stream_hint">Stream ID / URL。请输入 Stream ID 或完整 URL</string>
|
||||||
<string name="play_start">开始播放</string>
|
<string name="play_start">开始播放</string>
|
||||||
<string name="close">关闭</string>
|
<string name="close">关闭</string>
|
||||||
|
<string name="vod_config_title">点播播放</string>
|
||||||
|
<string name="vod_config_hint">请输入 MP4 / HLS URL</string>
|
||||||
|
|
||||||
<string name="protocol_rtmp">RTMP</string>
|
<string name="protocol_rtmp">RTMP</string>
|
||||||
<string name="protocol_rtc">RTC</string>
|
<string name="protocol_rtc">RTC</string>
|
||||||
@@ -97,6 +100,7 @@
|
|||||||
<string name="play_ctrl_unmute">取消静音</string>
|
<string name="play_ctrl_unmute">取消静音</string>
|
||||||
<string name="play_ctrl_screenshot">截图</string>
|
<string name="play_ctrl_screenshot">截图</string>
|
||||||
<string name="play_ctrl_pip">画中画</string>
|
<string name="play_ctrl_pip">画中画</string>
|
||||||
|
<string name="play_ctrl_forward_10">快进10秒</string>
|
||||||
<string name="live_play_foreground_title">正在播放</string>
|
<string name="live_play_foreground_title">正在播放</string>
|
||||||
<string name="live_play_foreground_text">直播播放保持中</string>
|
<string name="live_play_foreground_text">直播播放保持中</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user