diff --git a/example/libs/sellycloudsdk-1.0.0.aar b/example/libs/sellycloudsdk-1.0.0.aar index 0a10475..2cf5902 100644 Binary files a/example/libs/sellycloudsdk-1.0.0.aar and b/example/libs/sellycloudsdk-1.0.0.aar differ diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 9f2d07e..dde2f3d 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -64,6 +64,14 @@ android:supportsPictureInPicture="true" android:parentActivityName=".FeatureHubActivity" /> + + setPlayingUi(false) SellyPlayerState.Playing -> setPlayingUi(true) SellyPlayerState.Paused -> setPlayingUi(false) - SellyPlayerState.Stopped -> setPlayingUi(false) + SellyPlayerState.StoppedOrEnded -> setPlayingUi(false) SellyPlayerState.Failed -> setPlayingUi(false) SellyPlayerState.Idle -> Unit } @@ -722,7 +722,7 @@ class LivePlayActivity : AppCompatActivity() { SellyPlayerState.Reconnecting -> "重连中" SellyPlayerState.Playing -> "播放中" SellyPlayerState.Paused -> "已暂停" - SellyPlayerState.Stopped -> "已停止" + SellyPlayerState.StoppedOrEnded -> "已停止" SellyPlayerState.Failed -> "失败" SellyPlayerState.Idle -> "空闲" } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/vod/VodPlayActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/vod/VodPlayActivity.kt new file mode 100644 index 0000000..15f7a3a --- /dev/null +++ b/example/src/main/java/com/demo/SellyCloudSDK/vod/VodPlayActivity.kt @@ -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 = 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) + } + } + } +} diff --git a/example/src/main/res/drawable/ic_av_pause.xml b/example/src/main/res/drawable/ic_av_pause.xml new file mode 100644 index 0000000..be64c25 --- /dev/null +++ b/example/src/main/res/drawable/ic_av_pause.xml @@ -0,0 +1,10 @@ + + + + diff --git a/example/src/main/res/layout/activity_feature_hub.xml b/example/src/main/res/layout/activity_feature_hub.xml index b0aab96..a4141f2 100644 --- a/example/src/main/res/layout/activity_feature_hub.xml +++ b/example/src/main/res/layout/activity_feature_hub.xml @@ -110,6 +110,40 @@ android:textSize="15sp" android:textStyle="bold" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/src/main/res/layout/dialog_vod_input.xml b/example/src/main/res/layout/dialog_vod_input.xml new file mode 100644 index 0000000..968ac32 --- /dev/null +++ b/example/src/main/res/layout/dialog_vod_input.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + +