package com.runt.live.ui.stream
|
|
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
|
import android.util.Log
|
import android.view.SurfaceHolder
|
import android.view.SurfaceView
|
import android.view.ViewGroup
|
import android.widget.Toast
|
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.background
|
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.filled.ArrowForward
|
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.Button
|
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.Card
|
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Icon
|
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.Slider
|
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Text
|
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextField
|
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.key
|
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.setValue
|
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.layout.SubcomposeLayout
|
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.viewinterop.AndroidView
|
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
|
import com.runt.live.data.StreamWindow
|
import com.runt.live.enum.CameraClickType
|
import com.runt.live.enum.LiveState
|
import com.runt.live.enum.MuteState
|
import com.runt.live.enum.StreamType
|
import com.runt.live.icon.Image
|
import com.runt.live.icon.Mic
|
import com.runt.live.icon.MuteImage
|
import com.runt.live.icon.MuteMic
|
import com.runt.live.icon.MuteSpeaker
|
import com.runt.live.icon.PauseArrow
|
import com.runt.live.icon.Speaker
|
import com.runt.live.listener.MiniWindowListener
|
import com.runt.live.native.LivePuller
|
import com.runt.live.native.MediaPlayer
|
import com.runt.myapplication.ui.theme.MyApplicationTheme
|
import com.runt.myapplication.ui.theme.TransBlack
|
import com.runt.open.mvi.base.LayoutView
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.launch
|
|
/**
|
* @author Runt(qingingrunt2010@qq.com)
|
* @purpose 直播界面
|
* @date 9/17/24
|
*/
|
class LiveLayoutView(mViewModel : LiveViewModel) : LayoutView<LiveViewModel>(mViewModel) {
|
|
var liveAdressesState = mutableStateListOf<LiveAddress>()
|
|
|
var showMainBorder = mutableStateOf(false);
|
var mainStreamState = mutableStateOf(0)
|
var memoryState by mutableStateOf("Hello")
|
var mainWindowSize = mutableStateOf(Point(0,0))
|
@Composable
|
fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
|
val STREAM_RATE = 9f/16;
|
var liveAddressClick = MutableSharedFlow<LiveAddress?>();
|
var liveAddressConnectClick = MutableSharedFlow<LiveAddress>();
|
|
var cameraClick = MutableSharedFlow<CameraClickType>();
|
var closeSurfaceClick = MutableSharedFlow<StreamWindow>();
|
|
val moreDialogState = mutableStateOf(false)
|
var controlStream :StreamWindow ? = null;
|
|
//新窗口
|
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 usbDevices = mutableStateOf(listOf<UsbDevice>())
|
var subStreamsState = mutableStateOf(listOf<StreamWindow>())
|
var liveStreamsState = mutableStateOf(LiveStreams(subStreamsState))
|
}
|
|
fun onCloseClick(streamWindow : StreamWindow){
|
mViewModel.viewModelScope.launch {
|
closeSurfaceClick.emit(streamWindow);
|
}
|
mViewModel.removeMiniView(streamWindow.id);
|
LiveMiniView.removePcm(streamWindow.id)
|
subStreamsState.value = subStreamsState.value.filter { it.id != streamWindow.id }
|
}
|
|
@Composable
|
override fun layout() {
|
MyApplicationTheme {
|
Surface(modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background) {
|
var margin by remember {
|
mutableStateOf(Point(0,0))
|
}
|
ConstraintLayout(modifier = Modifier
|
.fillMaxSize()
|
.onSizeChanged { //主画面
|
Log.w(TAG , "主画面 onSizeChanged: ${it.width}x${it.height}" , )
|
updateWindowSize({ x , y -> margin = Point(x , y) } , it.width , it.height)
|
}) {
|
val (mainSurface) = createRefs()
|
//主窗口
|
Box(modifier = Modifier
|
.constrainAs(mainSurface) {
|
top.linkTo(parent.top , pxToDp(margin.y))
|
start.linkTo(parent.start , pxToDp(margin.x))
|
}
|
.size(pxToDp(mainWindowSize.value.x) , pxToDp(mainWindowSize.value.y))){
|
AndroidView(factory = { context: Context ->
|
updateWindowSize({x,y-> margin = Point(x,y) },
|
context.resources.displayMetrics.widthPixels,context.resources.displayMetrics.heightPixels)
|
SurfaceView(context).apply {
|
layoutParams = ViewGroup.LayoutParams(
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT
|
)
|
LiveMiniView.native_set_main_surface(holder.surface);
|
holder.addCallback(object :SurfaceHolder.Callback{
|
|
override fun surfaceCreated(holder : SurfaceHolder) {
|
//Log.w(TAG , "surfaceCreated: ${holder.hashCode()}" , )
|
}
|
|
override fun surfaceChanged(holder : SurfaceHolder , format : Int , width : Int , height : Int) {
|
LiveMiniView.native_set_main_surface(holder.surface);
|
//Log.w(TAG , "surfaceChanged: ${holder.hashCode()}" , )
|
}
|
|
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
LiveMiniView.native_release_main_surface();
|
Log.w(TAG , "主画面 surfaceDestroyed: ${holder.hashCode()} ${id}" , )
|
}
|
})
|
|
}
|
|
})
|
}
|
val (miniSurfaces) = createRefs()
|
ConstraintLayout (modifier = Modifier
|
.fillMaxSize()
|
.constrainAs(miniSurfaces) {
|
top.linkTo(parent.top , pxToDp(margin.y))
|
start.linkTo(parent.start , pxToDp(margin.x))
|
}){
|
//小窗
|
for (stream in subStreamsState.value){
|
key(stream.id) {
|
val (suface) = createRefs()
|
MiniSurfaceWindow(streamWindow = stream, modifier = Modifier.constrainAs(suface){
|
var marginTop = pxToDp((stream.positionRateState.value.y * (mainWindowSize.value.y - (mainWindowSize.value.x / (stream.sizeState.value.x * 1.0 / stream.sizeState.value.y)) * stream.viewRateState.value)).toInt());
|
var marginStart = pxToDp((stream.positionRateState.value.x * (mainWindowSize.value.x - mainWindowSize.value.x * stream.viewRateState.value)).toInt());
|
top.linkTo(parent.top,marginTop)
|
start.linkTo(parent.start,marginStart)
|
})
|
}
|
}
|
}
|
}
|
//底部按钮
|
Box(modifier = Modifier.fillMaxSize()){
|
BottomBtns(modifier = Modifier.align(alignment = Alignment.BottomCenter))
|
}
|
SubcomposeLayout { constraints ->
|
val textPlaceable = subcompose("text") {
|
Box(modifier = Modifier.wrapContentSize(align = Alignment.BottomEnd).size(100.dp , 70.dp).padding(10.dp).background(color = Color.Gray)){
|
Text(text = "${memoryState}", fontSize = 11.sp, color = Color.White, style = TextStyle(lineHeight = 12.sp, textAlign = TextAlign.Center), modifier = Modifier.size(100.dp,70.dp) )
|
}
|
}.first().measure(constraints)
|
|
layout(textPlaceable.width, textPlaceable.height) {
|
textPlaceable.place(0, 0)
|
}
|
}
|
when{
|
moreDialogState.value->{
|
MiniMoreControlDialog(audioMS = controlStream!!.audioDelay, videoMS = controlStream!!.videoDelay, volume = controlStream!!.volume,
|
hasVideo = controlStream!!.hasVideoState.value,hasAudio = controlStream!!.hasAudioState.value ,
|
onDismissRequest = { moreDialogState.value = false } , onConfirmRequest = { audioms,videoms,volume ->
|
moreDialogState.value = false
|
controlStream!!.audioDelay = audioms;
|
controlStream!!.videoDelay = videoms;
|
controlStream!!.volume = volume;
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(controlStream),controlStream!!)
|
})
|
}
|
}
|
}
|
}
|
}
|
|
fun updateWindowSize(onMarginChange:(Int,Int)->Unit,width:Int,height:Int){
|
var rate = width * 1f/height;
|
if(rate != STREAM_RATE) {
|
var mainWidth = 0;
|
var mainHeight = 0;
|
if(rate < STREAM_RATE){
|
mainWidth = width;
|
mainHeight = (mainWidth / 9 * 16);
|
}else{
|
mainHeight = height;
|
mainWidth = (mainHeight / 16 * 9);
|
}
|
if(mainWindowSize.value.x != mainWidth || mainWindowSize.value.y != mainHeight){
|
mainWindowSize.value = Point(mainWidth , mainHeight);
|
onMarginChange((width - mainWidth)/2,0)
|
}
|
}else{
|
if(mainWindowSize.value.x != width || mainWindowSize.value.y != height){
|
mainWindowSize.value = Point(width , height);
|
onMarginChange(0,0)
|
}
|
}
|
//Log.w(TAG , "onSizeChanged: ${mainWindowSize.value}" , )
|
}
|
|
fun newSurfaceClick(streamType : StreamType){
|
var streamWindow = StreamWindow(streamType = streamType);
|
newSurfaceClick(streamWindow)
|
}
|
fun newSurfaceClick(streamWindow : StreamWindow){
|
Log.i(TAG,"newSurfaceClick stream:${streamWindow.id}");
|
streamWindow.id = System.identityHashCode(streamWindow)
|
subStreamsState.value+=streamWindow
|
if(streamWindow.streamType == StreamType.VIDEO || streamWindow.streamType == StreamType.AUDIO){
|
var listener = MediaPlayer.MediaPlayerListener();
|
streamWindow.listener = listener
|
listener.setMediaInfo = { width,height,duration ->
|
Log.i(TAG , "mediaInfo ${width}x${height} duration:${duration}")
|
listener.durationState.value = duration;
|
if (width > height) {
|
streamWindow.viewRateState.value = 0.4f
|
}
|
streamWindow.sizeState.value = Point(width,height);
|
}
|
}else if(streamWindow.streamType == StreamType.RTMP){
|
streamWindow.listener = LivePuller.ReadStreamListener()
|
}else{
|
streamWindow.listener = MiniWindowListener();
|
}
|
streamWindow.listener?.onFailed = { errorCode ->
|
onCloseClick(streamWindow)
|
}
|
mViewModel.viewModelScope.launch {
|
openSurfaceClick.emit(streamWindow)
|
}
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow),streamWindow)
|
}
|
|
/**
|
* 小窗口
|
*/
|
@OptIn(ExperimentalFoundationApi::class , ExperimentalComposeUiApi::class)
|
@Composable
|
fun MiniSurfaceWindow(streamWindow : StreamWindow,modifier : Modifier){
|
var viewRate = streamWindow.viewRateState;
|
var streamSize = streamWindow.sizeState;
|
if(streamSize.value.x == 0){
|
streamSize.value.x = mainWindowSize.value.x;
|
streamSize.value.y = mainWindowSize.value.y;
|
}
|
var width = (mainWindowSize.value.x * viewRate.value).toInt();
|
var height = (mainWindowSize.value.x /(streamSize.value.x*1.0/streamSize.value.y) * viewRate.value).toInt();
|
//var offset:Offset? = null;
|
Box(modifier = modifier
|
.wrapContentSize()
|
.combinedClickable(onDoubleClick = {
|
showMainBorder.value = true;
|
} , onClick = {})
|
.background(color = Color.Black)/*.pointerInteropFilter {
|
Log.i(TAG , "SurfaceWindow: action: ${it.action} ")
|
if(it.action == MotionEvent.ACTION_DOWN){
|
offset = null
|
Log.e(TAG , "SurfaceWindow: viewRate: ${stream.viewRate} viewRate: ${viewRate} position: ${stream.positionState.value} position:${stream.position}")
|
}
|
true;
|
}*/
|
.pointerInput(Unit) {
|
detectTransformGestures { centroid , pan , zoom , rotation -> //Log.i(TAG , "SurfaceWindow: centroid: ${centroid} pan: ${pan} zoom: ${zoom} rotation:${rotation}")
|
val widthL = mainWindowSize.value.x - (mainWindowSize.value.x * viewRate.value);
|
val heightH = mainWindowSize.value.y - (mainWindowSize.value.x / (streamSize.value.x * 1.0 / streamSize.value.y) * viewRate.value).toFloat(); //Log.i(TAG , "SurfaceWindow: streamSize: ${streamSize.value} widthL:${widthL} heightH:${heightH} :${streamWindow.positionRateState.value}")
|
var zoomZ = ((zoom - 1) * 1);
|
var minRate = 0.3f;
|
if (streamSize.value.x > streamSize.value.y) {
|
minRate = 0.4f
|
}
|
var maxRate = 1.0f;
|
var rate = streamWindow.sizeState.value.x * 1f / streamWindow.sizeState.value.y;
|
if (rate < STREAM_RATE) {
|
maxRate = (mainWindowSize.value.y * rate) / mainWindowSize.value.x
|
}
|
if (viewRate.value + zoomZ < minRate) {
|
viewRate.value = minRate
|
} else if (viewRate.value + zoomZ > maxRate) {
|
viewRate.value = maxRate
|
} else {
|
viewRate.value += zoomZ
|
var zoomX = widthL * zoomZ
|
var zoomY = heightH * zoomZ;
|
var x1 = streamWindow.positionRateState.value.x * widthL - zoomX / 2;
|
var y2 = streamWindow.positionRateState.value.y * heightH - zoomY / 2;
|
if (x1 < 0) {
|
x1 = 0f;
|
} else if (x1 > widthL) {
|
x1 = widthL
|
}
|
if (y2 < 0) {
|
y2 = 0f;
|
} else if (y2 > heightH) {
|
y2 = heightH
|
}
|
var rateX = if (widthL == 0f) 0f else x1 / widthL;
|
var rateY = if (heightH == 0f) 0f else y2 / heightH;
|
streamWindow.positionRateState.value = PointF(rateX , rateY)
|
} //Log.i(TAG , "SurfaceWindow: zoom: ${zoom} viewRate:${viewRate} ")
|
|
var x = streamWindow.positionRateState.value.x * widthL + pan.x;
|
var y = streamWindow.positionRateState.value.y * heightH + pan.y; //Log.e(TAG , "SurfaceWindow: x:${x} y:${streamWindow.positionRateState.value.y} ${streamWindow.positionRateState.value.y * heightH} ${y} ")
|
if (x < 0) {
|
x = 0f;
|
} else if (x > widthL) {
|
x = widthL
|
}
|
if (y < 0) {
|
y = 0f;
|
} else if (y > heightH) {
|
y = heightH
|
} //offset = pan;
|
var rateX = if (widthL == 0f) 0f else x / widthL;
|
var rateY = if (heightH == 0f) 0f else y / heightH;
|
streamWindow.positionRateState.value = PointF(rateX , rateY) //Log.e(TAG , "SurfaceWindow: x:${x} y:${y} widthL:${widthL} heightH:${heightH}")
|
//Log.e(TAG , "SurfaceWindow: viewRate: ${streamWindow.viewRateState} position: ${streamWindow.positionRateState.value} ")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
|
}
|
}
|
.padding()
|
.shadow(elevation = 2.dp , spotColor = Color.White , ambientColor = Color.White , shape = RoundedCornerShape(2.dp) , clip = true)){
|
AndroidView(factory = { context: Context ->
|
SurfaceView(context).apply {
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)
|
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)
|
}
|
|
}, modifier = Modifier
|
.padding(1.dp)
|
.size(width = pxToDp(width) - 2.dp , height = pxToDp(height) - 2.dp))
|
if(mainStreamState.value == streamWindow.id && showMainBorder.value){
|
//主画面选取框
|
MiniMainBorder(width,height,streamWindow)
|
}
|
//关闭
|
IconButton(onClick = {
|
streamWindow.videoState.value = LiveState.OUT_LIVE;
|
streamWindow.audioState.value = LiveState.OUT_LIVE;
|
onCloseClick(streamWindow)
|
}, modifier = Modifier
|
.size(20.dp , 20.dp)
|
.align(alignment = Alignment.TopEnd)) {
|
Icon(Icons.Default.Close, contentDescription = null, tint = Color.White)
|
}
|
//序号
|
Row ( modifier = Modifier
|
.background(color = Color.LightGray)
|
.clickable {
|
for (stream in subStreamsState.value) {
|
stream.surfaceView?.setZOrderOnTop(false)
|
stream.surfaceView?.setZOrderMediaOverlay(false)
|
}
|
var value = subStreamsState.value
|
value -= streamWindow
|
value += streamWindow;
|
streamWindow.surfaceView?.setZOrderOnTop(true)
|
streamWindow.surfaceView?.setZOrderMediaOverlay(true)
|
subStreamsState.value = value;
|
for (stream in subStreamsState.value) {
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow), streamWindow)
|
}
|
}
|
.align(alignment = Alignment.TopStart),
|
horizontalArrangement = Arrangement.Center){
|
Icon(Icons.Default.ArrowForward,modifier = Modifier.size(20.dp , 20.dp), contentDescription = null, tint = Color.White)
|
Text(text = "${subStreamsState.value.indexOf(streamWindow)}", fontSize = 16.sp )
|
}
|
var loadState by remember {
|
mutableStateOf(false)
|
}
|
streamWindow.listener?.onStarted = {
|
if(!loadState){
|
loadState = true;
|
}
|
}
|
if(!loadState){
|
return
|
}
|
when(streamWindow.streamType){
|
StreamType.VIDEO,StreamType.AUDIO ->{
|
MiniPlayerBottomBtns(streamWindow, Modifier
|
.width(pxToDp(width) - 2.dp)
|
.align(alignment = Alignment.BottomCenter))
|
}
|
else ->{
|
MiniBottomBtns(streamWindow = streamWindow , modifier = Modifier
|
.width(pxToDp(width))
|
.align(alignment = Alignment.BottomCenter));
|
}
|
}
|
}
|
}
|
|
@Composable
|
fun MiniBottomBtns(streamWindow : StreamWindow,modifier : Modifier){
|
Row (modifier = modifier.background(color = TransBlack)){
|
Spacer(modifier = Modifier.width(10.dp))
|
//切换摄像头
|
if(streamWindow.streamType == StreamType.CAMERA){
|
IconButton(onClick = {
|
mViewModel.viewModelScope.launch {
|
cameraClick.emit(CameraClickType.SWITCH)
|
}
|
}, modifier = Modifier
|
.size(20.dp , 20.dp)
|
.align(alignment = Alignment.CenterVertically)) {
|
Icon(Icons.Default.Refresh, contentDescription = null, tint = Color.White)
|
}
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
if(streamWindow.hasVideoState.value){
|
//画面加入直播
|
MiniImageButton(streamWindow = streamWindow, modifier = Modifier.align(alignment = Alignment.CenterVertically))
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
if(streamWindow.hasAudioState.value){
|
//声音加入直播
|
MiniAudioButton(streamWindow = streamWindow, modifier = Modifier.align(alignment = Alignment.CenterVertically))
|
if(streamWindow.streamType != StreamType.CAMERA && streamWindow.streamType != StreamType.SYSTEM){
|
Spacer(modifier = Modifier.weight(1f))
|
MiniSpeakerButton(streamWindow = streamWindow, modifier = Modifier.align(alignment = Alignment.CenterVertically))
|
}
|
Spacer(modifier = Modifier.weight(1f)) //更多
|
}
|
IconButton(onClick = {
|
controlStream = streamWindow;
|
moreDialogState.value = true;
|
} , modifier = Modifier.align(alignment = Alignment.CenterVertically).size(20.dp , 20.dp)) {
|
Icon(Icons.Default.MoreVert , contentDescription = null , tint = Color.White)
|
}
|
Spacer(modifier = Modifier.width(10.dp))
|
}
|
}
|
|
@Composable
|
fun MiniMoreControlDialog(audioMS: Long = 0L,videoMS: Long = 0L,volume : Double = 0.0,hasVideo:Boolean = true, hasAudio : Boolean = true, onDismissRequest:()->Unit,onConfirmRequest:(Long,Long,Double)->Unit){
|
Log.i(TAG , "MiniMoreControlDialog: ${audioMS} vo:${volume}")
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
Card(
|
modifier = Modifier
|
.fillMaxWidth()
|
.wrapContentHeight()
|
.padding(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
) {
|
var audioState by remember { mutableStateOf(audioMS) }
|
var videoState by remember { mutableStateOf(videoMS) }
|
var volumeState by remember { mutableStateOf(volume) }
|
Column(modifier = Modifier.wrapContentHeight()){
|
Text(text = "其他功能设置:", fontSize = 16.sp,
|
modifier = Modifier
|
.padding(top = 20.dp , start = 20.dp)
|
.wrapContentSize(Alignment.Center),
|
textAlign = TextAlign.Center)
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp,),
|
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Slider(value = audioState / 5000f , onValueChange = {
|
audioState = (it*5000).toLong();
|
}, colors = SliderDefaults.colors(inactiveTrackColor = Color.LightGray), enabled = hasAudio)
|
}
|
Row ( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Spacer(modifier = Modifier.weight(1f))
|
Text(text = "声音延时:"+audioState+"ms")
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp,),
|
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Slider(value = videoState / 5000f , onValueChange = {
|
videoState = (it*5000).toLong();
|
}, colors = SliderDefaults.colors(inactiveTrackColor = Color.LightGray), enabled = hasVideo)
|
}
|
Row ( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Spacer(modifier = Modifier.weight(1f))
|
Text(text = "画面延时:"+videoState+"ms")
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp,),
|
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Slider(value = volumeState.toFloat() , onValueChange = {
|
volumeState = it.toDouble();
|
}, colors = SliderDefaults.colors(inactiveTrackColor = Color.LightGray), enabled = hasAudio)
|
}
|
Row ( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Spacer(modifier = Modifier.weight(1f))
|
Text(text = "音量:"+(volumeState*100).toInt()+"%")
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp, bottom = 20.dp)){
|
Spacer(modifier = Modifier.weight(1f))
|
Button(onClick = { onConfirmRequest(audioState,videoState,volumeState)}) {
|
Text(text = "确认")
|
}
|
}
|
}
|
}
|
}
|
}
|
/**
|
* 小窗主画面选取框
|
*/
|
@OptIn(ExperimentalFoundationApi::class)
|
@Composable
|
fun MiniMainBorder(width:Int,height:Int,streamWindow : StreamWindow){
|
//主画面选取框
|
var rate = streamWindow.sizeState.value.x * 1f/streamWindow.sizeState.value.y;
|
if(rate != STREAM_RATE && rate != 0f && rate != mainWindowSize.value.x * 1f/mainWindowSize.value.y) {
|
var miniHeight = 0;
|
var miniWidth = 0;
|
if (rate < STREAM_RATE){
|
miniWidth = width;
|
miniHeight = (miniWidth / 9 * 16);
|
}else{
|
miniHeight = height;
|
miniWidth = (miniHeight / 16 * 9)
|
}
|
ConstraintLayout(modifier = Modifier
|
.size(width.pxToDp() - 2.dp , height.pxToDp() - 2.dp)
|
.combinedClickable(onDoubleClick = {
|
showMainBorder.value = false;
|
} , onClick = {}) ) {
|
val (canvas) = createRefs()
|
Canvas(modifier = Modifier
|
.size(miniWidth.pxToDp() - 2.dp , miniHeight.pxToDp() - 2.dp)
|
.constrainAs(canvas) {
|
top.linkTo(parent.top , if (miniWidth == width) pxToDp((height * streamWindow.mainPositionRateState.value).toInt()) else 0.dp)
|
start.linkTo(parent.start , if (miniHeight == height) (pxToDp((width * streamWindow.mainPositionRateState.value).toInt())) else 0.dp)
|
}
|
.pointerInput(Unit) {
|
detectTransformGestures { centroid , pan , zoom , rotation -> //Log.i(TAG , "SurfaceWindow: zoom: ${zoom} viewRate:${viewRate} ")
|
var x = width * streamWindow.mainPositionRateState.value + pan.x;
|
var y = height * streamWindow.mainPositionRateState.value + pan.y;
|
if (miniHeight == height) {
|
if (x < 0) {
|
x = 0f;
|
} else if (x > width - miniWidth) {
|
x = (width - miniWidth).toFloat()
|
}
|
streamWindow.mainPositionRateState.value = x / width;
|
} else {
|
if (y < 0) {
|
y = 0f;
|
} else if (y > height - miniHeight) {
|
y = (height - miniHeight).toFloat()
|
} //offset = pan;
|
streamWindow.mainPositionRateState.value = y / height;
|
} //Log.e(TAG , "SurfaceWindow: x: ${x} y: ${y} ${streamWindow.mainPositionRateState.value}")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
|
}
|
}) {
|
drawRect(color = Color.Blue , topLeft = Offset.Zero , size = Size(miniWidth.toFloat() , miniHeight.toFloat()) , alpha = 1f , style = Stroke(width = 2.dp.toPx()))
|
}
|
}
|
}
|
}
|
|
@OptIn(ExperimentalFoundationApi::class)
|
@Composable
|
fun MiniIconButton(streamWindow : StreamWindow,state : MutableState<LiveState>,modifier : Modifier,icon:ImageVector,tint : Color,onLongClick : () -> Unit ){
|
Icon(icon , contentDescription = null, tint = tint,
|
modifier = modifier
|
.size(20.dp , 20.dp)
|
.combinedClickable(onLongClick = onLongClick , onClick = {
|
if (mainStreamState.value == streamWindow.id) {
|
return@combinedClickable;
|
}
|
if (state.value == LiveState.IN_LIVE) {
|
state.value = LiveState.OUT_LIVE
|
LiveMiniView.removePcm(streamWindow.id)
|
} else {
|
state.value = LiveState.IN_LIVE
|
}
|
Log.i(TAG , "加入直播 = ${streamWindow.audioState} ${streamWindow.videoState} ")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
|
Log.i(TAG , "MiniBottomBtns: onClick")
|
}))
|
}
|
|
@OptIn(ExperimentalFoundationApi::class)
|
@Composable
|
fun MiniAudioButton(streamWindow : StreamWindow,modifier : Modifier) {
|
var b = streamWindow.audioState.value == LiveState.IN_LIVE ;
|
var tint = if(b) Color.White else Color.LightGray
|
var icon = if(b) Icons.Default.Mic else Icons.Default.MuteMic;
|
Icon(icon , contentDescription = null, tint = tint,
|
modifier = modifier.size(20.dp , 20.dp).combinedClickable(onLongClick = {}, onClick = {
|
if (streamWindow.audioState.value == LiveState.IN_LIVE) {
|
streamWindow.audioState.value = LiveState.OUT_LIVE
|
LiveMiniView.removePcm(streamWindow.id)
|
} else {
|
streamWindow.audioState.value = LiveState.IN_LIVE
|
}
|
Log.i(TAG , "加入直播 = ${streamWindow.audioState} ${streamWindow.videoState} ")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
|
Log.i(TAG , "MiniBottomBtns: onClick")
|
}))
|
}
|
|
@OptIn(ExperimentalFoundationApi::class)
|
@Composable
|
fun MiniImageButton(streamWindow : StreamWindow,modifier : Modifier) {
|
var b = streamWindow.videoState.value == LiveState.IN_LIVE || mainStreamState.value == streamWindow.id;
|
var tint = if(mainStreamState.value == streamWindow.id) Color.Red else if(b) Color.White else Color.LightGray
|
var icon = if(b) Icons.Default.Image else Icons.Default.MuteImage;
|
Icon(icon , contentDescription = null, tint = tint,
|
modifier = modifier.size(20.dp , 20.dp).combinedClickable(onLongClick = {
|
LiveMiniView.mainStreamCode = streamWindow.id;
|
LiveMiniView.native_set_main_stream_code(LiveMiniView.mainStreamCode)
|
mainStreamState.value = LiveMiniView.mainStreamCode
|
showMainBorder.value = true;
|
streamWindow.videoState.value = LiveState.IN_LIVE
|
Log.i(TAG,"作为主画面 = ${mainStreamState.value} ${streamWindow.id}")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow),streamWindow)
|
} , onClick = {
|
if (mainStreamState.value == streamWindow.id) {
|
return@combinedClickable;
|
}
|
if (streamWindow.videoState.value == LiveState.IN_LIVE) {
|
streamWindow.videoState.value = LiveState.OUT_LIVE
|
} else {
|
streamWindow.videoState.value = LiveState.IN_LIVE
|
}
|
Log.i(TAG , "加入直播 = ${streamWindow.audioState} ${streamWindow.videoState} ")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow) , streamWindow)
|
Log.i(TAG , "MiniBottomBtns: onClick")
|
}))
|
}
|
|
@Composable
|
fun MiniSpeakerButton(streamWindow : StreamWindow,modifier : Modifier) {
|
IconButton(onClick = {
|
if (streamWindow.speakerState.value == MuteState.MUTE){
|
streamWindow.speakerState.value = MuteState.UN_MUTE
|
} else {
|
streamWindow.speakerState.value = MuteState.MUTE
|
}
|
Log.i(TAG,"加入直播 = ${streamWindow.speakerState.value} ")
|
mViewModel.updateMiniView(subStreamsState.value.indexOf(streamWindow),streamWindow)
|
}, modifier = modifier.size(20.dp , 20.dp)) {
|
var b = streamWindow.speakerState.value == MuteState.UN_MUTE;
|
Icon(if(b) Icons.Default.Speaker else Icons.Default.MuteSpeaker, contentDescription = null, tint = if(b) Color.White else Color.LightGray)
|
}
|
}
|
|
//视频播放器
|
@OptIn(ExperimentalComposeUiApi::class)
|
@Composable
|
fun MiniPlayerBottomBtns(streamWindow : StreamWindow,modifier : Modifier){
|
Column ( modifier = modifier){
|
MiniBottomBtns(streamWindow = streamWindow , modifier = Modifier
|
.fillMaxWidth()
|
.align(alignment = Alignment.CenterHorizontally));
|
Row (modifier = Modifier
|
.height((30).dp)
|
.background(color = TransBlack)
|
.padding(bottom = (5 * streamWindow.viewRateState.value).dp)
|
.wrapContentSize(align = Alignment.Center)) {
|
Spacer(modifier = Modifier.width((10* streamWindow.viewRateState.value).dp))
|
var sliderPosition by remember { mutableFloatStateOf(0f) }
|
var playState by remember {
|
mutableStateOf(false)
|
}
|
var touchState by remember {
|
mutableStateOf(false)
|
}
|
streamWindow.listener?.let {
|
var listener = it as MediaPlayer.MediaPlayerListener
|
listener.updateProgress = { progress ->
|
//Log.i(TAG , "updateProgress: ${progress}")
|
mViewModel.getActivity().runOnUiThread {
|
if(!touchState){
|
sliderPosition = progress*1f/listener.durationState.value;
|
if(listener.durationState.value == progress){
|
playState = false;
|
}
|
}
|
//Log.i(TAG , "updateProgress: ${progress} ${sliderPosition} ${listener.durationState.value}")
|
}
|
}
|
listener.onFailed = { errorCode ->
|
playState = false;
|
onCloseClick(streamWindow)
|
}
|
listener.onStop = {
|
if(playState){
|
playState = false;
|
}
|
}
|
}
|
|
IconButton(onClick = {
|
if(playState){
|
playState = false;
|
mViewModel.pausePlayer(streamWindow.id);
|
}else{
|
mViewModel.playPlayer(streamWindow.id);
|
playState = true;
|
}
|
}, modifier = Modifier
|
.size((40 * streamWindow.viewRateState.value).dp , (40 * streamWindow.viewRateState.value).dp)
|
.align(alignment = Alignment.CenterVertically)) {
|
Icon(if(playState) Icons.Filled.PauseArrow else Icons.Filled.PlayArrow, contentDescription = null, tint = Color.White)
|
}
|
Spacer(modifier = Modifier.width((15 * streamWindow.viewRateState.value).dp))
|
|
var sliderWidth by remember { mutableFloatStateOf(0f) } ;
|
Box (
|
modifier = Modifier
|
.align(alignment = Alignment.CenterVertically)
|
.height((20 * streamWindow.viewRateState.value).dp)
|
.wrapContentHeight(Alignment.CenterVertically)
|
.onSizeChanged {
|
sliderWidth = it.width.toFloat();
|
}
|
.weight(1f)
|
.pointerInput(Unit) {
|
awaitPointerEventScope {
|
while (true) {
|
val event = awaitPointerEvent()
|
if (event.type == PointerEventType.Press || event.type == PointerEventType.Move) {
|
touchState = true
|
if (event.changes.first().position.x > 0 && event.changes.first().position.x < sliderWidth) {
|
sliderPosition = event.changes.first().position.x / sliderWidth;
|
} else if (event.changes.first().position.x <= 0) {
|
sliderPosition = 0f;
|
} else {
|
sliderPosition = 1f;
|
}
|
} else if (event.type == PointerEventType.Release) {
|
touchState = false;
|
streamWindow.listener?.let {
|
var listener = it as MediaPlayer.MediaPlayerListener
|
mViewModel.setPlayerProgress(streamCode = streamWindow.id , (listener.durationState.value * sliderPosition).toLong())
|
}
|
} //阻止事件传递给父控件
|
event.changes.forEach { it.consume() }
|
}
|
}
|
}){
|
|
// 自定义轨道
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
sliderWidth = size.width
|
val trackOffsetY = size.height / 2
|
|
// 画活动部分轨道
|
drawLine(
|
color = Color.White,
|
strokeWidth = 10f * streamWindow.viewRateState.value,
|
start = Offset(10f, trackOffsetY),
|
end = Offset( sliderPosition * sliderWidth, trackOffsetY)
|
)
|
|
// 画非活动部分轨道
|
drawLine(
|
color = Color.Gray,
|
strokeWidth = 10f * streamWindow.viewRateState.value,
|
start = Offset(sliderPosition * sliderWidth, trackOffsetY),
|
end = Offset(sliderWidth, trackOffsetY)
|
)
|
|
drawCircle(Color.White, radius = 30f * streamWindow.viewRateState.value,center = Offset(x = sliderPosition * sliderWidth,trackOffsetY))
|
}
|
}
|
Spacer(modifier = Modifier.width((10 * streamWindow.viewRateState.value).dp))
|
}
|
}
|
}
|
|
@Composable
|
fun SliderView(modifier : Modifier,viewRateState : MutableState<Float>){
|
var sliderWidth by remember { mutableFloatStateOf(0f) } ;
|
var touchState by remember {
|
mutableStateOf(false)
|
}
|
var sliderPosition by remember { mutableFloatStateOf(0f) }
|
Box (
|
modifier = modifier
|
.height((20 * viewRateState.value).dp)
|
.wrapContentHeight(Alignment.CenterVertically)
|
.onSizeChanged {
|
sliderWidth = it.width.toFloat();
|
}
|
.pointerInput(Unit) {
|
awaitPointerEventScope {
|
while (true) {
|
val event = awaitPointerEvent()
|
if (event.type == PointerEventType.Press || event.type == PointerEventType.Move) {
|
touchState = true
|
if (event.changes.first().position.x > 0 && event.changes.first().position.x < sliderWidth) {
|
sliderPosition = event.changes.first().position.x / sliderWidth;
|
} else if (event.changes.first().position.x <= 0) {
|
sliderPosition = 0f;
|
} else {
|
sliderPosition = 1f;
|
}
|
} else if (event.type == PointerEventType.Release) {
|
touchState = false;
|
} //阻止事件传递给父控件
|
event.changes.forEach { it.consume() }
|
}
|
}
|
}){
|
|
// 自定义轨道
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
sliderWidth = size.width
|
val trackOffsetY = size.height / 2
|
|
// 画活动部分轨道
|
drawLine(
|
color = Color.White,
|
strokeWidth = 10f * viewRateState.value,
|
start = Offset(10f, trackOffsetY),
|
end = Offset( sliderPosition * sliderWidth, trackOffsetY)
|
)
|
|
// 画非活动部分轨道
|
drawLine(
|
color = Color.Gray,
|
strokeWidth = 10f * viewRateState.value,
|
start = Offset(sliderPosition * sliderWidth, trackOffsetY),
|
end = Offset(sliderWidth, trackOffsetY)
|
)
|
|
drawCircle(Color.White, radius = 30f * viewRateState.value,center = Offset(x = sliderPosition * sliderWidth,trackOffsetY))
|
}
|
}
|
}
|
|
@Composable
|
fun BottomBtns(modifier : Modifier){
|
Column(modifier = modifier.padding(bottom = 30.dp) ) {
|
Row {
|
//推流按钮
|
LazyHorizontalGrid(rows = GridCells.Adaptive(minSize = (30 + 5).dp), modifier = Modifier.height(40.dp),
|
contentPadding = PaddingValues(end = 5.dp, bottom = 5.dp)) {
|
items(liveAdressesState.size + 1) { index ->
|
if(liveAdressesState.size <= index){
|
Button(onClick = {
|
mViewModel.viewModelScope.launch {
|
liveAddressClick.emit(null);
|
}
|
}, modifier = Modifier
|
.wrapContentSize(Alignment.Center)
|
.padding(start = 5.dp , top = 5.dp),
|
contentPadding = PaddingValues(0.dp),
|
colors = ButtonDefaults.buttonColors(contentColor = Color.White, containerColor = Color.LightGray)) {
|
Text(text = " + ", modifier = Modifier
|
.fillMaxSize()
|
.wrapContentSize(Alignment.Center)
|
, color = Color.White, fontSize = (12).sp
|
)
|
}
|
}else{
|
var adress = liveAdressesState[index]
|
var openLiveDialog by remember { mutableStateOf(false) }
|
//Log.i(TAG , "BottomBtns: ${adress.liveState.value}")
|
LiveButton(onclick = {
|
openLiveDialog = !openLiveDialog;
|
} , text = adress.name , color = if (adress.liveState.value == LiveState.IN_LIVE) Color.Red else Color.LightGray)
|
when{
|
openLiveDialog->{
|
LiveDialog(onButtonClick = {
|
openLiveDialog = false;
|
when(it){
|
//连接
|
0->{
|
mViewModel.viewModelScope.launch {
|
liveAddressConnectClick.emit(adress);
|
}
|
//adress.liveState.value = if(adress.liveState.value == LiveState.IN_LIVE) LiveState.OUT_LIVE else LiveState.IN_LIVE
|
}
|
/*//编辑
|
1->{
|
mViewModel.viewModelScope.launch {
|
liveAddressClick.emit(adress);
|
}
|
}*/
|
//删除
|
-1->{
|
liveAdressesState.remove(adress)
|
}
|
}
|
} , name = adress.name, url = adress.url, state = adress.liveState.value)
|
}
|
}
|
}
|
}
|
}
|
}
|
val rtmpInputDialog = remember { mutableStateOf(false) }
|
val textInputDialog = remember { mutableStateOf(false) }
|
val openScreenDialog = remember { mutableStateOf(false) }
|
Row {
|
if(!liveStreamsState.value.hasCamera()){
|
Button(onClick = {
|
newSurfaceClick(StreamType.CAMERA)
|
Log.i(TAG,"打开相机 = ${liveStreamsState.value.subStreamWindows.value.size} ${subStreamsState.value.size}")
|
}, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
|
Text(text = "相机")
|
}
|
}
|
Button(onClick = {
|
rtmpInputDialog.value = true
|
}, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
|
Text(text = "RTMP")
|
}
|
/*Button(onClick = {
|
newSurfaceClick(StreamType.VIDEO)
|
}, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
|
Text(text = "视频")
|
}*/
|
/*Button(onClick = {
|
newSurfaceClick(StreamType.AUDIO)
|
}, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) {
|
Text(text = "音频")
|
}*/
|
// USB Video Class (UVC) 视频采集卡
|
if(usbDevices.value.size > 0){
|
Button(onClick = {
|
var streamWindow = StreamWindow(streamType = StreamType.UVC);
|
//默认没有音频
|
streamWindow.hasAudioState.value = false;
|
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 = {
|
openScreenDialog.value = true;
|
} , modifier = Modifier.padding(start = 10.dp) , contentPadding = PaddingValues(0.dp)) {
|
Text(text = "系统")
|
}
|
}
|
}
|
when{
|
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)
|
})
|
}
|
openScreenDialog.value->{
|
ScreenDialog(onDismissRequest = { openScreenDialog.value = false } ) { orientation,screen,voice ->
|
openScreenDialog.value = false;
|
if(screen || voice){
|
var streamWindow = StreamWindow(streamType = StreamType.SYSTEM, remark = orientation.toString(), screenOn = screen, voiceOn = voice);
|
streamWindow.hasAudioState.value = voice;
|
streamWindow.hasVideoState.value = screen;
|
newSurfaceClick(streamWindow);
|
}
|
}
|
}
|
}
|
}
|
}
|
|
@Composable
|
fun LiveButton(onclick:() ->Unit,text:String,color:Color){
|
Button(onClick = onclick, modifier = Modifier
|
.wrapContentSize(Alignment.Center)
|
.padding(start = 5.dp , top = 5.dp),
|
contentPadding = PaddingValues(0.dp),
|
colors = ButtonDefaults.buttonColors(contentColor = Color.White, containerColor = color)) {
|
Text(text = text, modifier = Modifier
|
.fillMaxSize()
|
.wrapContentSize(Alignment.Center)
|
, color = Color.White, fontSize = (12 ).sp
|
)
|
}
|
}
|
|
@Composable
|
fun LiveDialog(onButtonClick:(code:Int)->Unit,name : String,url:String,state:LiveState){
|
Dialog(onDismissRequest = { onButtonClick(-2) }) {
|
Card(
|
modifier = Modifier
|
.fillMaxWidth()
|
.wrapContentHeight()
|
.padding(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
) {
|
Column(modifier = Modifier.wrapContentHeight()){
|
Text(text = "直播名称:"+name, fontSize = 16.sp,
|
modifier = Modifier
|
.padding(top = 20.dp , start = 20.dp)
|
.wrapContentSize(Alignment.Center),
|
textAlign = TextAlign.Center)
|
Text(
|
text = "当前直播地址:\n" +url+"\n直播状态:"+(if(state == LiveState.IN_LIVE) "良好" else "断开"),
|
modifier = Modifier
|
.fillMaxWidth()
|
.wrapContentSize()
|
.padding(top = 30.dp , bottom = 20.dp)
|
.wrapContentSize(Alignment.Center),
|
textAlign = TextAlign.Center,
|
)
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp, bottom = 20.dp)){
|
if(state == LiveState.OUT_LIVE){
|
/*Button(onClick = { onButtonClick(1)}) {
|
Text(text = "编辑")
|
}*/
|
Spacer(modifier = Modifier.weight(1f))
|
Button(onClick = { onButtonClick(-1)}) {
|
Text(text = "删除")
|
}
|
}
|
Spacer(modifier = Modifier.weight(1f))
|
Button(onClick = { onButtonClick(0)}) {
|
Text(text = if(state == LiveState.IN_LIVE) "断开" else "重连")
|
}
|
}
|
}
|
}
|
}
|
}
|
|
@Composable
|
fun ScreenDialog(onDismissRequest:()->Unit,onConfirmRequest:(Int,Boolean,Boolean)->Unit){
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
Card(
|
modifier = Modifier
|
.fillMaxWidth()
|
.wrapContentHeight()
|
.padding(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
) {
|
var screenChecked by remember { mutableStateOf(false) }
|
var voiceChecked by remember { mutableStateOf(true) }
|
Column(modifier = Modifier.wrapContentHeight()){
|
Text(text = "选择画面样式:", fontSize = 16.sp,
|
modifier = Modifier
|
.padding(top = 20.dp , start = 20.dp)
|
.wrapContentSize(Alignment.Center),
|
textAlign = TextAlign.Center)
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp, bottom = 20.dp),
|
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically){
|
Spacer(modifier = Modifier.weight(1f))
|
Checkbox(checked = screenChecked , onCheckedChange = {screenChecked = it},enabled = false)
|
Text(text = "屏幕")
|
Spacer(modifier = Modifier.weight(1f))
|
Checkbox(checked = voiceChecked , onCheckedChange = {voiceChecked = it})
|
Text(text = "声音")
|
Spacer(modifier = Modifier.weight(1f))
|
}
|
Spacer(modifier = Modifier.height(20.dp))
|
Row (modifier = Modifier.padding(start = 15.dp, end = 15.dp, bottom = 20.dp)){
|
Button(onClick = { onConfirmRequest(Configuration.ORIENTATION_PORTRAIT,screenChecked,voiceChecked)}) {
|
Text(text = "竖屏")
|
}
|
Spacer(modifier = Modifier.weight(1f))
|
Button(onClick = { onConfirmRequest(Configuration.ORIENTATION_LANDSCAPE,screenChecked,voiceChecked)}) {
|
Text(text = "横屏")
|
}
|
}
|
}
|
}
|
}
|
}
|
|
@Composable
|
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 = {
|
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{
|
onConfirmRequest(text)
|
}
|
}) {
|
Text(text = "确认")
|
}
|
}, dismissButton = {
|
TextButton(onClick = {onDismissRequest()}) {
|
Text(text = "取消")
|
}
|
},
|
title = {
|
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 = streamType == StreamType.RTMP,
|
modifier = modifier,
|
keyboardOptions = KeyboardOptions(
|
keyboardType = keyboardType
|
) ,
|
placeholder = {
|
if(streamType == StreamType.RTMP) {
|
Text(text = "请输入直播地址" , fontSize = 14.sp)
|
}else{
|
Text(text = "请输入要展示的文本" , fontSize = 14.sp)
|
}
|
}
|
, textStyle = TextStyle(fontSize = 14.sp, color = Color.Black))
|
})
|
}
|
|
}
|
|
@Preview
|
@Composable
|
fun preview_mini_bottom_btns(){
|
Box{
|
LiveLayoutView(LiveViewModel()).MiniBottomBtns(StreamWindow(), Modifier
|
.width(220.dp)
|
.align(alignment = Alignment.BottomCenter))
|
}
|
}
|
|
@Preview
|
@Composable
|
fun preview_dialog(){
|
LiveLayoutView(LiveViewModel()).LiveDialog(onButtonClick = {} , name = "sss" , url = "lllll" , state = LiveState.IN_LIVE)
|
}
|
@Preview
|
@Composable
|
fun preview_screen_dialog(){
|
LiveLayoutView(LiveViewModel()).ScreenDialog(onDismissRequest = { /*TODO*/ }) {orientation,screen,voice ->
|
}
|
}
|
|
@Preview
|
@Composable
|
fun preview_more_dialog(){
|
LiveLayoutView(LiveViewModel()).MiniMoreControlDialog(audioMS = 500, onDismissRequest = { /*TODO*/ }) { ms,s,volume ->
|
}
|
}
|
|
@Preview
|
@Composable
|
fun preview_playerWindow(){
|
Box{
|
LiveLayoutView(LiveViewModel()).MiniPlayerBottomBtns(StreamWindow(), Modifier
|
.width(220.dp)
|
.align(alignment = Alignment.BottomCenter))
|
}
|
}
|
|
@Preview
|
@Composable
|
fun preview_speakerIcon(){
|
Row(modifier = Modifier
|
.wrapContentSize(align = Alignment.BottomEnd)
|
.padding(10.dp)
|
.background(color = Color.Gray)){
|
Text(text = "111111\n111111", fontSize = 11.sp, color = Color.White, style = TextStyle(lineHeight = 12.sp) )
|
Box{IconButton(onClick = {
|
}, modifier = Modifier
|
.size(20.dp , 20.dp)
|
.background(color = Color.Black)) {
|
Icon(Icons.Default.MuteImage, contentDescription = null, tint = Color.White)
|
}
|
}
|
}
|
}
|