在android中,compose作为声明式UI框架有着广泛的应用。其典型范式包括诸如声明式UI描述页面结构、响应式状态作为UI唯一驱动、带参remember实现高开销实例的懒加载与重建、LaunchedEffect/DisposableEffect处理副作用函数用于处理非UI逻辑和资源管理、AndroidView实现与原生视图的无缝集成、组合式组件实现代码复用与扩展。
// 在build.gradle中的dependencies中添加依赖
implementation 'org.videolan.android:libvlc-all:3.6.0'
package com.eric.video
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.TextureView
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.eric.video.EricActivity.Companion.TAG
import com.eric.video.ui.theme.VideoTheme
import kotlinx.coroutines.delay
import org.videolan.libvlc.LibVLC
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.util.VLCVideoLayout
class EricActivity : ComponentActivity() {
companion object {
// RTSP(Real-Time Streaming Protocol,实时流传输协议)的地址属于专用 URL,核心用于音视频实时流的拉取 / 推送,其通用基础格式为:
// rtsp://[用户名:密码@]设备/服务器IP[:端口号]/流媒体资源路径
// 554:RTSP 协议的国际标准默认端口号(由 IANA 分配)
const val VIDEO_RTSP_URL = "rtsp://eirc:eric666@192.168.0.190:554/LiveMedia/ch1/Media1"
const val PARAM_KEY = "param_key"
const val TAG = "Eric-RTSP"
}
var playedRtspUrl = VIDEO_RTSP_URL
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val recvRtspUrl = intent?.getStringExtra(PARAM_KEY)
Log.d(TAG, "onCreate,recvRtspUrl:$recvRtspUrl")
playedRtspUrl = if (!recvRtspUrl.isNullOrEmpty() && recvRtspUrl.startsWith("rtsp://")) {
recvRtspUrl
} else {
VIDEO_RTSP_URL
}
Log.d(TAG, "onCreate,playedRtspUrl:$playedRtspUrl")
// Compose 摒弃了传统 Android 的 “视图继承体系”,采用组合式组件开发:将复杂 UI 拆分为多个独立的、可复用的小组件,通过组合的方式搭建复杂 UI,而非通过继承扩展组件功能。
// 整个页面由Scaffold(页面骨架)、TopAppBar(顶部栏)、EricRtspPlayer(核心播放器)、OutlinedTextField(输入框)等组件组合而成,每个组件职责单一.
// 在onCreate中通过 setContent 初始化 Compose 根布局,使用 Material3 的Scaffold+TopAppBar搭建基础页面结构;
setContent {
VideoTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Video surface") },
navigationIcon = {
IconButton(onClick = {
finish()
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "back",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF027EFB),
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) {
innerPadding ->
EricRtspPlayer(
modifier = Modifier.fillMaxSize().padding(innerPadding),
rtspUrl = playedRtspUrl
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun RtspPlayerPreview() {
VideoTheme {
EricRtspPlayer(rtspUrl = EricActivity.Companion.VIDEO_RTSP_URL)
}
}
// EricRtspPlayer 是独立的可组合函数,可在任意 Compose 布局中复用,只需传递rtspUrl和modifier参数;可以复用组件。
@Composable
fun EricRtspPlayer(modifier: Modifier = Modifier, rtspUrl: String) {
val context = LocalContext.current
// remember是 Compose 的状态缓存函数,用于在组件重组时保留数据/实例,避免重复创建(如复杂对象、原生实例、网络请求结果)。
// 无参remember:仅在组件首次创建时执行,后续重组复用结果(如remember { mutableStateOf(rtspUrl) });
// 带参remember:仅在依赖的参数变化时执行,否则复用结果(如remember(inputRtspUrl) { LibVLC(...) });
// remember 保留状态在组件重组时不被重新初始化,保证状态的连续性。
// 作用:该状态是整个播放器的唯一数据源,鸿蒙传递的初始地址、手动编辑的地址都通过该状态管理,实现 “一处修改,全局更新”。
// Compose 的核心设计:UI = f (状态),UI 是状态的函数,状态变化自动触发 UI 重组,更新为对应状态的 UI,无需手动调用notifyDataSetChanged、invalidate等方法。
var inputRtspUrl by remember {
// mutableStateOf 创建 Compose可观察状态,状态变化会自动触发 Compose 重组,更新关联的 UI 和业务逻辑.
mutableStateOf(rtspUrl)
}
// 仅当inputRtspUrl变化时,重新创建LibVLC实例.
// remember(inputRtspUrl):带依赖参数的 remember,只有依赖的状态发生变化时,才会重新执行 Lambda 创建新实例,否则复用原有实例,避免不必要的性能开销.
// 将 VLC 实例的创建与 RTSP 地址状态绑定,地址变化自动重建播放器实例,实现 “输入地址即实时刷新播放” 的核心需求.
val libVlc = remember(inputRtspUrl) {
val vlcOptions = ArrayList<String>().apply {
add("--rtsp-tcp")
add("--avcodec-codec=h264")
add("--network-caching=200")
add("--no-drop-late-frames")
add("--no-skip-frames")
}
LibVLC(context, vlcOptions).also {
Log.d(TAG, "LibVLC init success")
}
}
// 仅当inputRtspUrl/libVlc变化时,重新创建MediaPlayer实例
val mediaPlayer = remember(inputRtspUrl,libVlc) {
MediaPlayer(libVlc).also {
player ->
try {
val media = Media(libVlc, Uri.parse(inputRtspUrl,))
player.media = media
media.release()
Log.d(TAG, "MediaPlayer init success and rtsp url is $inputRtspUrl,")
} catch (e: Exception) {
Log.e(TAG, "Media init failed, url is $inputRtspUrl, error ", e)
}
}
}
// Compose 作为新的 UI 框架,无法完全替代现有 Android 的原生视图(如自定义 View、第三方原生控件、VLC 播放器),
// 因此提供了 AndroidView 和 AndroidViewBinding 两个核心 API,实现Compose 与原生视图的无缝集成。
// 通过AndroidView嵌入 VLC 的原生渲染视图VLCVideoLayout,实现 Compose 中无原生组件的视频渲染需求.
// 核心作用:解决了 Compose 生态暂未覆盖的原生视图需求(如 VLC 播放器、自定义原生控件),是 Compose 与现有 Android 原生代码兼容的关键。
AndroidView(
modifier = modifier.fillMaxSize().padding(bottom = 450.dp),
// factory:仅首次创建原生视图时执行,负责原生视图的初始化,避免重复创建.
factory = {
ctx ->
// 仅首次创建时执行:创建原生VLCVideoLayout
VLCVideoLayout(ctx).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
Log.d(TAG, "VLCVideoLayout create success.")
}
},
// update:Compose 重组/状态变化时执行,负责原生视图的更新(如绑定视频输出、修改属性),实现 Compose 状态与原生视图的联动.
update = {
// 组件重组/状态变化时执行:更新视图绑定
videoLayout ->
try {
val vlcVout = mediaPlayer.vlcVout
vlcVout.detachViews()
videoLayout.removeAllViews()
// 绑定TextureView到VLC的视频输出,完成渲染视图初始化
val textureView = TextureView(context)
val textureParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
textureView.layoutParams = textureParams
videoLayout.addView(textureView)
vlcVout.setVideoView(textureView)
vlcVout.attachViews()
mediaPlayer.scale = 0f
mediaPlayer.aspectRatio = "16:9"
} catch (e: Exception) {
Log.e(TAG, "draw view failed:", e)
}
}
)
Column(
modifier = modifier.padding(top = 300.dp).background(Color.White),
verticalArrangement = Arrangement.Top
) {
inputRtspUrl?.let {
// OutlinedTextField是Compose 官方可编辑输入框.
OutlinedTextField(
value = it, // 绑定输入状态,即绑定可观察状态:UI展示状态值
onValueChange = {
newUrl ->
if (!newUrl.isNullOrEmpty() && newUrl.startsWith("rtsp://")) {
inputRtspUrl = newUrl
} else {
Toast.makeText(context, "please input right RTSP address", Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(Color.White, RoundedCornerShape(8.dp)),
label = { Text("RTSP url") },
placeholder = {
if (rtspUrl != null) {
Text(rtspUrl)
}
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { }
),
singleLine = true,
maxLines = 1
)
}
}
// Compose 是 Android 官方的声明式 UI 框架. Compose 中禁止在可组合函数中直接执行非 UI 逻辑(如延迟播放、资源释放、网络请求),需通过副作用函数处理.
// Compose 提供了副作用函数,用于处理非 UI 逻辑,确保逻辑的执行时机可控、次数确定,本代码使用的LaunchedEffect、DisposableEffect是最常用的两个副作用函数.
// 代码中使用了 2 个核心副作用函数LaunchedEffect和DisposableEffect。
// LaunchedEffect:处理挂起函数与延迟逻辑。
// LaunchedEffect内置 Kotlin 协程作用域,直接执行挂起函数,无需手动创建协程。
LaunchedEffect(mediaPlayer) {
try {
delay(300) // 挂起函数,需在协程中执行
mediaPlayer.play()
Log.d(TAG, "begin to play RTSP stream...")
} catch (e: Exception) {
Log.e(TAG, "play failed,url is $inputRtspUrl,error:", e)
}
}
// DisposableEffect:处理资源释放
DisposableEffect(libVlc, mediaPlayer) {
onDispose {
Log.d(TAG, "release VLC resource...")
mediaPlayer.vlcVout.detachViews()
mediaPlayer.stop()
mediaPlayer.release()
libVlc.release()
}
}
}