Runt
2025-04-13 8c0f96b20f4cf44d230b373f51261566af02de8e
app/src/main/java/com/runt/live/ui/stream/LiveLayoutView.kt
@@ -2,6 +2,7 @@
import android.content.Context
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.graphics.Point
import android.graphics.PointF
import android.hardware.usb.UsbDevice
@@ -88,6 +89,7 @@
import androidx.compose.ui.window.Dialog
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.lifecycle.viewModelScope
import com.runt.live.R
import com.runt.live.cpp.LiveMiniView
import com.runt.live.data.LiveAddress
import com.runt.live.data.LiveStreams
@@ -121,8 +123,6 @@
    var liveAdressesState = mutableStateListOf<LiveAddress>()
    var subStreamsState = mutableStateOf(listOf<StreamWindow>())
    var liveStreamsState = mutableStateOf(LiveStreams(subStreamsState))
    var showMainBorder  = mutableStateOf(false);
    var mainStreamState = mutableStateOf(0)
@@ -142,14 +142,22 @@
    //新窗口
    var openSurfaceClick = MutableSharedFlow<StreamWindow>();
    fun pxToDp(pix:Int):Dp{
        var dpV = pix/mViewModel.getActivity().resources.displayMetrics.density;
        return dpV.dp;
    }
    fun sp2px(spValue: Float): Int {
        val fontScale = mViewModel.getActivity().resources.displayMetrics.density
        return (spValue * fontScale + 0.5f).toInt()
    }
    companion object {
        var uvcList = mutableStateOf(listOf<UsbDevice>())
        var usbDevices = mutableStateOf(listOf<UsbDevice>())
        var subStreamsState = mutableStateOf(listOf<StreamWindow>())
        var liveStreamsState = mutableStateOf(LiveStreams(subStreamsState))
    }
    fun onCloseClick(streamWindow : StreamWindow){
@@ -417,6 +425,10 @@
                    streamWindow.surfaceView = this;
                    streamWindow.surfaceHolder = holder;
                    streamWindow.listener?.onSurfaceUpdate?.let { it(holder.surface) }
                    if(streamWindow.streamType == StreamType.TEXT){
                        holder.setFormat(PixelFormat.TRANSLUCENT) // 透明像素格式
                        setBackgroundColor(context.resources.getColor(R.color.transparent))     // 设置背景透明
                    }
                    holder.addCallback(streamWindow.surfaceCallBack)
                }
@@ -452,7 +464,7 @@
                    streamWindow.surfaceView?.setZOrderMediaOverlay(true)
                    subStreamsState.value = value;
                    for (stream in subStreamsState.value) {
                        mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
                        mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow), streamWindow)
                    }
                }
                .align(alignment = Alignment.TopStart),
@@ -987,7 +999,8 @@
                    }
                }
            }
            val openInputDialog = remember { mutableStateOf(false) }
            val rtmpInputDialog = remember { mutableStateOf(false) }
            val textInputDialog = remember { mutableStateOf(false) }
            val openScreenDialog = remember { mutableStateOf(false) }
            Row {
                if(!liveStreamsState.value.hasCamera()){
@@ -999,7 +1012,7 @@
                    }
                }
                Button(onClick = {
                    openInputDialog.value = true
                    rtmpInputDialog.value = true
                }, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
                    Text(text = "RTMP")
                }
@@ -1014,16 +1027,21 @@
                    Text(text = "音频")
                }*/
                // USB Video Class (UVC)  视频采集卡
                if(uvcList.value.size > 0){
                if(usbDevices.value.size > 0){
                    Button(onClick = {
                        var streamWindow = StreamWindow(streamType = StreamType.UVC);
                        //默认没有音频
                        streamWindow.hasAudioState.value = false;
                        streamWindow.remark = uvcList.value[0].deviceName
                        streamWindow.remark = usbDevices.value[0].deviceName
                        newSurfaceClick(streamWindow)
                    }, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
                        Text(text = "UVC")
                    }
                }
                Button(onClick = {
                    textInputDialog.value = true
                }, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
                    Text(text = "文本")
                }
                if(!liveStreamsState.value.hasSystem()) {
                    Button(onClick = {
@@ -1034,12 +1052,24 @@
                }
            }
            when{
                openInputDialog.value->{
                    InputDialog(onDismissRequest = { openInputDialog.value = false } , onConfirmRequest = {
                        openInputDialog.value = false
                rtmpInputDialog.value->{
                    var url =  (mViewModel.getActivity() as LiveActivity ).getStringProjectPrefrence("pullAddr","rtmp://192.168.28.79/live");
                    InputDialog(url,StreamType.RTMP,onDismissRequest = { rtmpInputDialog.value = false } , onConfirmRequest = {
                        rtmpInputDialog.value = false
                        (mViewModel.getActivity() as LiveActivity ) .putStringProjectPrefrence("pullAddr",it)
                        var streamWindow = StreamWindow(streamType = StreamType.RTMP, remark = it);
                        streamWindow.hasVideoState.value = false;
                        streamWindow.hasAudioState.value = false;
                        newSurfaceClick(streamWindow)
                    })
                }
                textInputDialog.value->{
                    var url =  (mViewModel.getActivity() as LiveActivity ).getStringProjectPrefrence("live_text","");
                    InputDialog(url,StreamType.TEXT,onDismissRequest = { textInputDialog.value = false } , onConfirmRequest = {
                        textInputDialog.value = false
                        (mViewModel.getActivity() as LiveActivity ) .putStringProjectPrefrence("live_text",it)
                        var streamWindow = StreamWindow(streamType = StreamType.TEXT, remark = it);
                        streamWindow.hasVideoState.value = true;
                        streamWindow.hasAudioState.value = false;
                        newSurfaceClick(streamWindow)
                    })
@@ -1164,19 +1194,36 @@
    }
    @Composable
    fun InputDialog(onDismissRequest:()->Unit,onConfirmRequest:(String)->Unit){
        var url =  (mViewModel.getActivity() as LiveActivity ).getStringProjectPrefrence("pullAddr","rtmp://192.168.28.79/live");
    fun InputDialog(url:String,streamType : StreamType,onDismissRequest:()->Unit,onConfirmRequest:(String)->Unit){
        var text by remember {
            mutableStateOf(url)
        }
        var modifier : Modifier;
        var keyboardType : KeyboardType;
        if(streamType == StreamType.RTMP){
            modifier = Modifier
                .width(260.dp)
                .height(50.dp)
                .padding(0.dp)
            keyboardType = KeyboardType.Uri;
        }else{
            modifier = Modifier
                .width(260.dp)
                .padding(0.dp)
            keyboardType = KeyboardType.Text;
        }
        AlertDialog(onDismissRequest = { onDismissRequest() } ,
            confirmButton = {
                TextButton(onClick = {
                    val pattern = """rtmp://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]""".toRegex()
                    if(text.length > 7 && pattern.matches(text)){
                        onConfirmRequest(text)
                    if(streamType == StreamType.RTMP){
                        val pattern = """rtmp://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]""".toRegex()
                        if(text.length > 7 && pattern.matches(text)){
                            onConfirmRequest(text)
                        }else{
                            Toast.makeText(mViewModel.getActivity(),"地址格式错误",Toast.LENGTH_SHORT).show();
                        }
                    }else{
                        Toast.makeText(mViewModel.getActivity(),"地址格式错误",Toast.LENGTH_SHORT).show();
                        onConfirmRequest(text)
                    }
                }) {
                    Text(text = "确认")
@@ -1187,22 +1234,27 @@
                }
            },
            title = {
                Text(text = "请输入直播地址", fontSize = 16.sp)
                if(streamType == StreamType.RTMP) {
                    Text(text = "请输入直播地址" , fontSize = 16.sp)
                }else{
                    Text(text = "请输入要展示的文本" , fontSize = 16.sp)
                }
            },
            text = {
                TextField(value = text , onValueChange = {
                    //val pattern = """(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]""".toRegex()
                    text = it;
                },singleLine = true,
                    modifier = Modifier
                        .width(260.dp)
                        .height(50.dp)
                        .padding(0.dp),
                },singleLine = streamType == StreamType.RTMP,
                    modifier = modifier,
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Uri
                        keyboardType = keyboardType
                    ) ,
                    placeholder = {
                        Text(text = "请输入直播地址", fontSize = 14.sp)
                        if(streamType == StreamType.RTMP) {
                            Text(text = "请输入直播地址" , fontSize = 14.sp)
                        }else{
                            Text(text = "请输入要展示的文本" , fontSize = 14.sp)
                        }
                    }
                    , textStyle = TextStyle(fontSize = 14.sp, color = Color.Black))
            })