From 8c0f96b20f4cf44d230b373f51261566af02de8e Mon Sep 17 00:00:00 2001 From: Runt <qingingrunt2010@qq.com> Date: Sun, 13 Apr 2025 14:28:37 +0000 Subject: [PATCH] 绘制文本功能 --- app/src/main/java/com/runt/live/ui/stream/LiveLayoutView.kt | 102 ++++++++-- app/src/main/java/com/runt/live/enum/StreamType.kt | 2 app/src/main/cpp/native-lib.cpp | 101 ++++++++-- app/src/main/java/com/runt/live/media/UVCHelper.kt | 17 + app/src/main/res/values/colors.xml | 1 app/src/main/java/com/runt/live/media/CameraHelper.kt | 23 -- app/src/main/cpp/server_global.h | 3 app/src/main/java/com/runt/live/util/BitmapUtils.kt | 94 ++++++++++ app/src/main/java/com/runt/live/ui/stream/LiveViewModel.kt | 96 ++++++++++ app/src/main/java/com/runt/live/ui/stream/LiveActivity.kt | 15 + app/src/main/java/com/runt/live/media/ScreenHelper.kt | 11 - app/src/main/cpp/server_global.cpp | 24 +- app/src/main/cpp/pusher/live_pusher.cpp | 31 +-- 13 files changed, 396 insertions(+), 124 deletions(-) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 7e158a1..7de54db 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -115,7 +115,6 @@ void *task_pushFrames(void *args){ int64_t time = getCurrentTimestamp(); while (TRUE){ - YUVData *mainYuvData; int result = pushFrames.pop(mainYuvData); if(!result){ continue; @@ -186,37 +185,32 @@ if(pushers.size() == 0){ break; } + if(width != FRAME_WIDTH){ //缩放至1080 uint8_t *dstData[3]; + copyYUV(mainYuvData->yuvData,width,height,dstData); + delete mainYuvData->yuvData[0]; + delete mainYuvData->yuvData[1]; + delete mainYuvData->yuvData[2]; //LOGI("缩放至1080"); int64_t t1 = getCurrentTimestamp(); - scaleYUV(mainYuvData->yuvData,width,height,FRAME_WIDTH,FRAME_HEIGHT,dstData); + scaleYUV(dstData,width,height,FRAME_WIDTH,FRAME_HEIGHT,mainYuvData->yuvData); //LOGI("缩放至1080 耗时:%d",getCurrentTimestamp() - t1); - //加水印 - int64_t t2 = getCurrentTimestamp(); - waterYUV(dstData); - //LOGI("加水印 耗时:%d",getCurrentTimestamp() - t2); - //主画面 - int64_t t3 = getCurrentTimestamp(); - videoChannel->pushYUV(dstData); - //LOGI("pushYUV 编码 耗时:%d", getCurrentTimestamp() - t3); delete dstData[0]; delete dstData[1]; delete dstData[2]; - }else{ - //加水印 - //LOGI("加水印"); - int64_t t2 = getCurrentTimestamp(); - waterYUV(mainYuvData->yuvData); - //LOGI("加水印 耗时:%d",getCurrentTimestamp() - t2); - //主画面 - //LOGI("pushYUV pushYUV %d",pushers.size()); - int64_t t1 = getCurrentTimestamp(); - videoChannel->pushYUV(mainYuvData->yuvData); - //LOGI("pushYUV 编码 耗时:%d", getCurrentTimestamp() - t1); - } + //加水印 + //LOGI("加水印"); + int64_t t2 = getCurrentTimestamp(); + waterYUV(mainYuvData->yuvData); + //LOGI("加水印 耗时:%d",getCurrentTimestamp() - t2); + //主画面 + //LOGI("pushYUV pushYUV %d",pushers.size()); + int64_t t1 = getCurrentTimestamp(); + videoChannel->pushYUV(mainYuvData->yuvData); + //LOGI("pushYUV 编码 耗时:%d", getCurrentTimestamp() - t1); //LOGI("pushYUV 耗时:%d", getCurrentTimestamp() - t); } while (0); releaseYuvFrameData(mainYuvData); @@ -369,6 +363,9 @@ if(!nativeMainWindow){ LOGE("nativeWindow 回收"); } + if(!jvmMainCall){ + jvmMainCall = new JavaMainCall(env,clazz); + } } extern "C" @@ -443,4 +440,64 @@ yuv[2] = reinterpret_cast<uint8_t *>(data + ySize + uSize); // V 平面 pushYUV(stream_code,yuv); env->ReleaseByteArrayElements(bytes,data,0); +} +extern "C" JNIEXPORT jbyteArray JNICALL +Java_com_runt_live_cpp_LiveMiniView_native_1get_1cut_1frame(JNIEnv *env, jclass clazz, jint index, jint stream_code) { + uint8_t *yuvData[3]; + if(mainYuvData){ + pthread_mutex_lock(&pushVideoMutex); + copyYUV(mainYuvData->yuvData,FRAME_WIDTH,FRAME_HEIGHT,yuvData); + pthread_mutex_unlock(&pushVideoMutex); + }else{ + copyYUV(blackYUV,FRAME_WIDTH,FRAME_HEIGHT,yuvData); + } + waterYUV(index,yuvData); + //*****裁切画面 + MiniViewData *miniView = getMiniView(stream_code); + int scaleWidth = (FRAME_WIDTH * miniView->viewRate); + int scaleHeight = (FRAME_WIDTH / (miniView->width * 1.0 / miniView->height) * miniView->viewRate); + //涉及到 YUV 4:2:0 格式的图像时,宽度和高度通常需要是 2 的倍数 + if(scaleWidth % 2 == 1){ + scaleWidth+=1; + } + if(scaleHeight % 2 == 1){ + scaleHeight+=1; + } + if(scaleWidth > FRAME_WIDTH){ + scaleWidth = FRAME_WIDTH; + } + if(scaleHeight > FRAME_HEIGHT){ + scaleHeight = FRAME_HEIGHT; + } + if(!miniView->scaledYuvFrame){ + miniView->scaledYuvFrame = new YUVData; + } + miniView->scaledYuvFrame->width = scaleWidth; + miniView->scaledYuvFrame->height = scaleHeight; + miniView->scaledYuvFrame->pYrate = miniView->pYrate; + miniView->scaledYuvFrame->pXrate = miniView->pXrate; + uint8_t *dstData[3]; + //位置 + int offsetY = (FRAME_HEIGHT - scaleHeight) * miniView->pYrate; + int offsetX = (FRAME_WIDTH - scaleWidth) * miniView->pXrate; + cutYUV(yuvData,FRAME_WIDTH,FRAME_HEIGHT,offsetX,offsetY,dstData,scaleWidth,scaleHeight); + LOGE("index:%d x:%d y:%d %dx%d view:%dx%d",index,offsetX,offsetY,scaleWidth,scaleHeight,miniView->width,miniView->height); + copyYUV(dstData,scaleWidth,scaleHeight,miniView->scaledYuvFrame->yuvData); + addBlackBorder(miniView->scaledYuvFrame->yuvData,scaleWidth,scaleHeight,3); + //源数据没用了 + delete yuvData[0]; + delete yuvData[1]; + delete yuvData[2]; + //******转为rgba + int size = scaleWidth * scaleHeight * 4; + uint8_t *rgba_data = yuvToRGBA(dstData,scaleWidth,scaleHeight); + //yuv数据没用了 + delete dstData[0]; + delete dstData[1]; + delete dstData[2]; + //转为java字节数组 + jbyteArray java_array = env->NewByteArray(size); + env->SetByteArrayRegion(java_array, 0, size, (jbyte *)rgba_data); + delete[] rgba_data; + return java_array; } \ No newline at end of file diff --git a/app/src/main/cpp/pusher/live_pusher.cpp b/app/src/main/cpp/pusher/live_pusher.cpp index e30b65e..1fa7630 100644 --- a/app/src/main/cpp/pusher/live_pusher.cpp +++ b/app/src/main/cpp/pusher/live_pusher.cpp @@ -110,6 +110,11 @@ } pusher->pushFailedCount++; } + if(!RTMP_IsConnected(pusher->pRtmp)){ + LOGE("RTMP pusher 网络断连"); + pusher->isLive = -1; + pusher->pusherCall->onConnectFailed(1001); + } }else{ //LOGI("RTMP 发给 m_nBodySize:%d 耗时:%d",packet->m_nBodySize,getCurrentTimestamp() - t); pusher->pushFailedCount = 0; @@ -172,16 +177,10 @@ if(isLive != 1){ return; } - if(!RTMP_IsConnected(pRtmp)){ - LOGE("RTMP pusher 网络断连"); - isLive = -1; - reConnect(); - }else{ - RTMPPacket *dstPacket = new RTMPPacket ; - int result = CopyRTMPPacket(dstPacket,packet); - //LOGE("result:%d",result); - streamPackets.push(dstPacket); - } + RTMPPacket *dstPacket = new RTMPPacket ; + int result = CopyRTMPPacket(dstPacket,packet); + //LOGE("result:%d",result); + streamPackets.push(dstPacket); //LOGI("RTMP 发给x264解码 耗时:%d",getCurrentTimestamp() - t); } @@ -189,15 +188,9 @@ if( isLive != 1){ return; } - if(!RTMP_IsConnected(pRtmp)){ - LOGE("RTMP pusher 网络断连"); - isLive = -1; - reConnect(); - }else{ - RTMPPacket *dstPacket= new RTMPPacket ; - CopyRTMPPacket(dstPacket,packet); - streamPackets.push(dstPacket); - } + RTMPPacket *dstPacket= new RTMPPacket ; + CopyRTMPPacket(dstPacket,packet); + streamPackets.push(dstPacket); } void LivePusher::releaseLive() { diff --git a/app/src/main/cpp/server_global.cpp b/app/src/main/cpp/server_global.cpp index e15a9b4..803a36b 100644 --- a/app/src/main/cpp/server_global.cpp +++ b/app/src/main/cpp/server_global.cpp @@ -395,15 +395,20 @@ * @param mainData */ void waterYUV(uint8_t *mainData[3]) { + waterYUV(pushMiniDatas.size(),mainData); +} +void waterYUV(int index,uint8_t *mainData[3]) { //LOGI("waterYUV 加水印(画中画)"); - int size = pushMiniDatas.size(); - for (int i = 0; i < pushMiniDatas.size(); ++i) { + for (int i = 0; i < index; ++i) { MiniViewData *pData = pushMiniDatas[i]; if(pData->streamCode == mainStreamCode){ continue; } if(!pData->videoOn){ continue; + } + if(pData->streamType == 6){ + jvmMainCall->drawText(pData->streamCode,mainData); } if(!pData->scaledYuvFrame && pData->scaledYuvFrames.size() == 0 ){ //LOGE("小窗丢帧1"); @@ -428,10 +433,10 @@ YUVData *scaledYuvFrame = pData->scaledYuvFrame; //位置 - int offsetY = (mainHeight - scaledYuvFrame->height) * scaledYuvFrame->pYrate; - int offsetX = (mainWidth - scaledYuvFrame->width) * scaledYuvFrame->pXrate; + int offsetY = (mainHeight - scaledYuvFrame->height) * (pData->streamType == 6 ? pData->pYrate : scaledYuvFrame->pYrate); + int offsetX = (mainWidth - scaledYuvFrame->width) * (pData->streamType == 6 ? pData->pXrate : scaledYuvFrame->pXrate); - //LOGI("waterYUV %dx%d x:%d,y:%d x:%f,y:%f [0]:%d",pData->scaleWidth,miniHeight,pData->pYrate,pData->pXrate,offsetX,offsetY,pData->scaledYUVData[0]); + LOGI("waterYUV %dx%d x:%d,y:%d x:%d,y:%d ",scaledYuvFrame->width,scaledYuvFrame->height,pData->pYrate,pData->pXrate,offsetX,offsetY); // 边界检查,确保不会越界 if (offsetX + scaledYuvFrame->width > mainWidth ) { @@ -456,17 +461,14 @@ for (int y = 0; y < scaledYuvFrame->height / 2; y++) { uint8_t* main_u_row = mainData[1] + ((offsetY / 2) + y) * (mainWidth / 2) + (offsetX / 2); uint8_t* mini_u_row = scaledYuvFrame->yuvData[1] + y * (scaledYuvFrame->width / 2); - memcpy(main_u_row, mini_u_row, scaledYuvFrame->width / 2); - } - // 处理 V 平面 - for (int y = 0; y < scaledYuvFrame->height / 2; y++) { uint8_t* main_v_row = mainData[2] + ((offsetY / 2) + y) * (mainWidth / 2) + (offsetX / 2); uint8_t* mini_v_row = scaledYuvFrame->yuvData[2] + y * (scaledYuvFrame->width / 2); - memcpy(main_v_row, mini_v_row, scaledYuvFrame->width / 2); + } + } } @@ -529,7 +531,7 @@ if(!miniView){ return; } - if(miniView->videoOn || miniView->streamCode == mainStreamCode){ + if(miniView->videoOn || miniView->streamCode == mainStreamCode || miniView->streamType == 6){ pthread_mutex_lock(&pushVideoMutex); YUVData *yuvFrame = new YUVData(); yuvFrame->width = miniView->width; diff --git a/app/src/main/cpp/server_global.h b/app/src/main/cpp/server_global.h index e4d9fef..36cc348 100644 --- a/app/src/main/cpp/server_global.h +++ b/app/src/main/cpp/server_global.h @@ -45,10 +45,13 @@ void scaleYUV(uint8_t *yuvData[3],int width,int height,int dstWidth,int dstHeight,uint8_t *dstData[3]); void rotate_yuv(uint8_t* srcData[3], int width, int height,int angle,uint8_t* dstData[3],int dstWidth,int dstHeight); void cutYUV(uint8_t *yuvData[3],int width,int height,float positionRate,uint8_t *dstData[]); +void cutYUV(uint8_t *yuvData[3],int width,int height,int cropX,int cropY,uint8_t *dstData[],int dstWidth,int dstHeight); void copyYUV(uint8_t *yuvData[3],int width,int height, uint8_t *dstData[3]); void pushYUV(int streamCode, uint8_t *yuvData[3]); void pushYUYV(int streamCode,jbyte *data); void waterYUV(uint8_t *mainData[3]); +void waterYUV(int index,uint8_t *mainData[3]); +void getPushYUV(int index,uint8_t *mainData[3]); uint8_t* yuvToRGBA(uint8_t *yuvData[3],int width,int height); void pushRGBA(int streamCode,uint8_t *rgbaData); void pushNV21(int streamCode, jbyte *data); diff --git a/app/src/main/java/com/runt/live/enum/StreamType.kt b/app/src/main/java/com/runt/live/enum/StreamType.kt index cc96c0e..a9b9f24 100644 --- a/app/src/main/java/com/runt/live/enum/StreamType.kt +++ b/app/src/main/java/com/runt/live/enum/StreamType.kt @@ -7,5 +7,5 @@ */ enum class StreamType(var value : Int) { NONE(-1),CAMERA(0),RTMP(1),VIDEO(2) ,AUDIO(3) ,SYSTEM(4) //视频文件、相机、音频文件,RTMP推流,本地系统 - ,UVC(5) + ,UVC(5),TEXT(6) } \ No newline at end of file diff --git a/app/src/main/java/com/runt/live/media/CameraHelper.kt b/app/src/main/java/com/runt/live/media/CameraHelper.kt index 9fddb41..763a821 100644 --- a/app/src/main/java/com/runt/live/media/CameraHelper.kt +++ b/app/src/main/java/com/runt/live/media/CameraHelper.kt @@ -3,15 +3,12 @@ import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.ImageFormat import android.graphics.Point -import android.graphics.Rect import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager import android.util.Log import android.util.Size -import android.view.SurfaceHolder import androidx.annotation.OptIn import androidx.camera.camera2.interop.ExperimentalCamera2Interop import androidx.camera.core.Camera @@ -170,7 +167,7 @@ if(OpenApplication.getApplication().isInfront()){ mStreamWindow?.surfaceHolder?.let { //Log.i(TAG , "渲染: ${it.surfaceFrame.width()} ${it.surfaceFrame.height()} ${it.hashCode()}") - cavansSurface(it,imageData.bitmap); + BitmapUtils.instance!!.cavansSurface(it,imageData.bitmap); } imageData.bitmap.recycle(); } @@ -210,24 +207,6 @@ //preview.setSurfaceProvider(mStreamWindow.surfaceHolder!!) camera = cameraProvider.bindToLifecycle(this , selector , preview , imageCapture , imageAnalysis) mLifecycle.currentState = Lifecycle.State.RESUMED - } - - fun cavansSurface(surfaceHolder : SurfaceHolder,bitmap : Bitmap){ - var canvas : Canvas? = null; - try { - canvas = surfaceHolder.lockCanvas() - if (canvas != null) { - var src = Rect(0,0,bitmap.width,bitmap.height) - var des = Rect(0,0,surfaceHolder.surfaceFrame.width(),surfaceHolder.surfaceFrame.height()) - canvas.drawBitmap(bitmap,src,des,null) - } - } catch (e : Exception) { - Log.e("Surface" , "绘制失败:" + e.message) - } finally { - if (canvas != null) { - surfaceHolder.unlockCanvasAndPost(canvas) - } - } } interface OnFrameUpdateListener{ diff --git a/app/src/main/java/com/runt/live/media/ScreenHelper.kt b/app/src/main/java/com/runt/live/media/ScreenHelper.kt index 3099e47..7aaae21 100644 --- a/app/src/main/java/com/runt/live/media/ScreenHelper.kt +++ b/app/src/main/java/com/runt/live/media/ScreenHelper.kt @@ -12,7 +12,6 @@ import android.os.Handler import android.os.Looper import android.util.Log -import android.view.SurfaceHolder import com.runt.live.cpp.LiveMiniView import com.runt.live.cpp.LiveMiniView.mainStreamCode import com.runt.live.data.StreamWindow @@ -164,15 +163,7 @@ } //Log.i("22222" , "swapRBMultiThread: ${numThreads}") } - private fun cavansSurface(surfaceHolder : SurfaceHolder ,bitmap : Bitmap){ - var c = surfaceHolder.lockCanvas() - if(c != null){ - var src = Rect(0,0,bitmap.width,bitmap.height) - var des = Rect(0,0,surfaceHolder.surfaceFrame.width(),surfaceHolder.surfaceFrame.height()) - c.drawBitmap(bitmap,src,des,null) - surfaceHolder.unlockCanvasAndPost(c) - } - } + private fun printFirstRowRGBA(rgbaData : ByteArray , width : Int) { // 每行的字节数(宽度 × 每像素字节数) val rowBytes = width * 4 diff --git a/app/src/main/java/com/runt/live/media/UVCHelper.kt b/app/src/main/java/com/runt/live/media/UVCHelper.kt index cc2338d..b3535ea 100644 --- a/app/src/main/java/com/runt/live/media/UVCHelper.kt +++ b/app/src/main/java/com/runt/live/media/UVCHelper.kt @@ -16,14 +16,14 @@ import com.runt.live.data.UVCMonitor import com.runt.live.enum.ErrorCode import com.runt.live.enum.LiveState -import com.runt.live.ui.stream.LiveLayoutView.Companion.uvcList +import com.runt.live.ui.stream.LiveLayoutView import java.util.Date import kotlin.concurrent.thread /** * @author Runt(qingingrunt2010@qq.com) - * @purpose + * @purpose usb相机,视频采集卡等 * @date 3/6/25 */ class UVCHelper : USBHelper { @@ -70,6 +70,7 @@ } } monitor.streamWindow.sizeState.value = Point(camera.previewSize.width,camera.previewSize.height ) + monitor.streamWindow.hasVideoState.value = true; monitor.streamWindow.listener?.onStarted?.invoke() if (monitor.streamWindow.surfaceHolder != null) { camera.setPreviewDisplay(monitor.streamWindow.surfaceHolder!!.surface) @@ -140,16 +141,16 @@ override fun onUsbRgister(intent : Intent) { for ( device in usbManager!!.deviceList.values){ - if(hasCamera(device) && !uvcList.value.contains(device)){ - uvcList.value+=device; + if(hasCamera(device) && ! LiveLayoutView.usbDevices.value.contains(device)){ + LiveLayoutView.usbDevices.value+=device; } } } override fun onDeviceOut(device : UsbDevice) { Log.i(TAG , "onReceive: 外设已经移除") - if(uvcList.value.contains(device)){ - uvcList.value-=device; + if(LiveLayoutView.usbDevices.value.contains(device)){ + LiveLayoutView.usbDevices.value-=device; } } @@ -162,8 +163,8 @@ Log.d(TAG , "This device is a USB Camera.") //判断外设 Log.i(TAG , "onReceive: 外设连接") - if(!uvcList.value.contains(device)){ - uvcList.value+=device; + if(! LiveLayoutView.usbDevices.value.contains(device)){ + LiveLayoutView.usbDevices.value+=device; } } } diff --git a/app/src/main/java/com/runt/live/ui/stream/LiveActivity.kt b/app/src/main/java/com/runt/live/ui/stream/LiveActivity.kt index a0cf078..79db6fa 100644 --- a/app/src/main/java/com/runt/live/ui/stream/LiveActivity.kt +++ b/app/src/main/java/com/runt/live/ui/stream/LiveActivity.kt @@ -156,10 +156,13 @@ startCamera() } StreamType.VIDEO ->{ - chooseFile() + chooseFile(it.streamType) } StreamType.UVC ->{ openUVC(it,it.remark!!) + } + StreamType.TEXT->{ + mViewModel!!.openText(it); } StreamType.SYSTEM ->{ requestPermissions(Manifest.permission.POST_NOTIFICATIONS,{ @@ -196,6 +199,9 @@ } StreamType.AUDIO,StreamType.VIDEO->{ mViewModel!!.closePlayer(it.id) + } + StreamType.TEXT->{ + } StreamType.SYSTEM->{ if(sysRecorderService != null){ @@ -309,11 +315,11 @@ }) } - fun chooseFile(){ + fun chooseFile(type:StreamType){ var permissions = ""; //android 13 权限申请细化类型 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - when(mStreamWindow!!.streamType){ + when(type){ StreamType.VIDEO ->{ permissions = Manifest.permission.READ_MEDIA_VIDEO ; //android 14 选择文件授权 @@ -337,7 +343,7 @@ // intent.action = Intent.ACTION_GET_CONTENT //intent.setType("image/*") - when(mStreamWindow!!.streamType){ + when(type){ StreamType.VIDEO ->{ intent.action = MediaStore.ACTION_PICK_IMAGES intent.setType("video/*") // 设置文件类型,可以更具体如"application/pdf" @@ -426,6 +432,7 @@ } uvcHelper?.unRegister(); } + fun getMemoryInfo(context : Context):String? { val memoryInfo = Debug.MemoryInfo() Debug.getMemoryInfo(memoryInfo) // 以 KB 为单位 diff --git a/app/src/main/java/com/runt/live/ui/stream/LiveLayoutView.kt b/app/src/main/java/com/runt/live/ui/stream/LiveLayoutView.kt index 589cbbb..43a29ab 100644 --- a/app/src/main/java/com/runt/live/ui/stream/LiveLayoutView.kt +++ b/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)) }) diff --git a/app/src/main/java/com/runt/live/ui/stream/LiveViewModel.kt b/app/src/main/java/com/runt/live/ui/stream/LiveViewModel.kt index 34a5083..b9c2ac8 100644 --- a/app/src/main/java/com/runt/live/ui/stream/LiveViewModel.kt +++ b/app/src/main/java/com/runt/live/ui/stream/LiveViewModel.kt @@ -1,13 +1,27 @@ package com.runt.live.ui.stream +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Point +import android.graphics.Rect +import android.util.Log +import android.view.SurfaceHolder +import com.runt.live.R import com.runt.live.cpp.LiveMiniView +import com.runt.live.cpp.LiveMiniView.FRAME_HEIGHT +import com.runt.live.cpp.LiveMiniView.FRAME_WIDTH import com.runt.live.data.StreamWindow import com.runt.live.enum.LiveState +import com.runt.live.enum.StreamType import com.runt.live.native.LivePuller import com.runt.live.native.LivePusher import com.runt.live.native.MediaPlayer +import com.runt.live.ui.stream.LiveLayoutView.Companion.subStreamsState +import com.runt.live.util.BitmapUtils import com.runt.open.mvi.base.model.BaseViewModel +import java.nio.ByteBuffer import java.util.concurrent.ConcurrentSkipListMap +import kotlin.concurrent.thread /** @@ -37,8 +51,6 @@ players.put(streamWindow.id,player) player.load(filePath) } - - fun closePlayer(streamCode : Int){ players.get(streamCode)?.let { @@ -94,6 +106,7 @@ } pushers.remove(code); } + fun closePusher(url : String){ var code = getPusherCode(url); if(code > -1){ @@ -138,6 +151,85 @@ LiveMiniView.native_remove_mini_view(streamCode) } + fun updateText(streamWindow : StreamWindow){ + thread { + var bytes = LiveMiniView.native_get_cut_frame(subStreamsState.value.indexOf(streamWindow),streamWindow.id) + var bitmap = BitmapUtils.instance!!.textToBitmap(streamWindow.remark!!,130f,getActivity().resources.getColor(R.color.white)!!,getActivity().resources.getColor(R.color.transparent)!!) + + var cutwidth : Int = (FRAME_WIDTH * streamWindow.viewRateState.value).toInt() + var cutHeight : Int = (FRAME_WIDTH / ( streamWindow.sizeState.value.x * 1.0 / streamWindow.sizeState.value.y ) * streamWindow.viewRateState.value).toInt() + + //涉及到 YUV 4:2:0 格式的图像时,宽度和高度通常需要是 2 的倍数 + if (cutwidth % 2 == 1) { + cutwidth += 1 + } + if (cutHeight % 2 == 1) { + cutHeight += 1 + } + if (cutwidth > FRAME_WIDTH) { + cutwidth = FRAME_WIDTH + } + if (cutHeight > FRAME_HEIGHT) { + cutHeight = FRAME_HEIGHT + } + val cutBitmap = Bitmap.createBitmap(cutwidth, cutHeight, Bitmap.Config.ARGB_8888) + Log.e(TAG , "updateText: ${cutwidth}x${cutHeight} ${streamWindow.sizeState.value} ${( streamWindow.sizeState.value.x * 1.0 / streamWindow.sizeState.value.y )}", ) + val buffer = ByteBuffer.wrap(bytes) + cutBitmap.copyPixelsFromBuffer(buffer) + + val dstBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + + val canvas = Canvas(dstBitmap) + + // 构建目标区域 + val destRect = Rect(0,0,dstBitmap.width,dstBitmap.height) + + // 绘制小图到大图 + canvas.drawBitmap(cutBitmap, null, destRect, null) + //canvas.drawBitmap(bitmap, null, destRect, null) + + val width : Int = bitmap.getWidth() + val height : Int = bitmap.getHeight() + val argb = IntArray(width * height) + dstBitmap.getPixels(argb , 0 , width , 0 , 0 , width , height) + //LiveMiniView.native_push_nv21(streamWindow.id,BitmapUtils.instance!!.argbToNV21(argb,width,height)); + } + + } + fun openText(streamWindow : StreamWindow){ + + var bitmap = BitmapUtils.instance!!.textToBitmap(streamWindow.remark!!,130f,getActivity().resources.getColor(R.color.white)!!,getActivity().resources.getColor(R.color.transparent)!!) + streamWindow.hasAudioState.value = false; + streamWindow.hasVideoState.value = true; + streamWindow.sizeState.value = Point(bitmap.width,bitmap.height); + streamWindow.surfaceCallBack = 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) { + streamWindow.surfaceHolder = holder; + streamWindow.listener?.onSurfaceUpdate?.let { it(holder.surface) } + thread { + BitmapUtils.instance!!.cavansSurface(holder,bitmap); + val width : Int = bitmap.getWidth() + val height : Int = bitmap.getHeight() + val argb = IntArray(width * height) + bitmap.getPixels(argb , 0 , width , 0 , 0 , width , height) + LiveMiniView.native_push_nv21(streamWindow.id,BitmapUtils.instance!!.argbToNV21(argb,width,height)); + } + //Log.w(TAG , "surfaceChanged: ${holder.hashCode()}" , ) + } + + override fun surfaceDestroyed(holder : SurfaceHolder) { + streamWindow.surfaceHolder = null; + streamWindow.listener?.onSurfaceUpdate?.let { it(null) } + //Log.w(TAG , "surfaceDestroyed: ${holder.hashCode()} ${id}" , ) + } + } + streamWindow.listener?.onStarted?.invoke() + } + /** * 拉流 */ diff --git a/app/src/main/java/com/runt/live/util/BitmapUtils.kt b/app/src/main/java/com/runt/live/util/BitmapUtils.kt index 55eada5..cac5acc 100644 --- a/app/src/main/java/com/runt/live/util/BitmapUtils.kt +++ b/app/src/main/java/com/runt/live/util/BitmapUtils.kt @@ -3,9 +3,11 @@ import android.content.ContentResolver import android.content.Context import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.Color import android.graphics.ImageFormat import android.graphics.Matrix +import android.graphics.Paint import android.graphics.Rect import android.media.ExifInterface import android.media.Image @@ -18,11 +20,13 @@ import android.renderscript.ScriptIntrinsicYuvToRGB import android.renderscript.Type import android.util.Log +import android.view.SurfaceHolder import androidx.annotation.OptIn import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageProxy import java.io.IOException import java.nio.ByteBuffer +import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -683,5 +687,95 @@ } } + fun textToBitmap(text : String , textSize : Float , textColor : Int , bgColor : Int) : Bitmap { // 创建 Paint 对象设置文本参数 + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + paint.textSize = textSize.toFloat() + paint.color = textColor + + val fontMetrics = paint.fontMetrics + val lineHeight : Float = fontMetrics.bottom - fontMetrics.top + 30 + + // 按换行符分割文本 + val lines = text.split("\n") + + + // 计算最大宽度 + var maxWidth = 0f + for (line in lines) { + val width = paint.measureText(line) + if (width > maxWidth) maxWidth = width + } + + + // 创建 Bitmap + val bmpWidth = ceil(maxWidth.toDouble()).toInt() + val bmpHeight = ceil((lineHeight * lines.size).toDouble()).toInt() + + val bitmap = Bitmap.createBitmap(bmpWidth , bmpHeight , Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(bgColor) // 背景色 + + + // 逐行绘制文本 + var y = - fontMetrics.top + for (line in lines) { + canvas.drawText(line !! , 0f , y , paint) + y += lineHeight + } + + return bitmap + } + + fun argbToNV21(argb : IntArray , width : Int , height : Int) : ByteArray { + val yuv = ByteArray(width * height * 3 / 2) + val frameSize = width * height + + var yIndex = 0 + var uvIndex = frameSize + + for (j in 0 until height) { + for (i in 0 until width) { + val argbIndex = j * width + i + val color = argb[argbIndex] + + val r = (color shr 16) and 0xFF + val g = (color shr 8) and 0xFF + val b = color and 0xFF + + // RGB to YUV conversion + val y = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16 + val u = ((- 38 * r - 74 * g + 112 * b + 128) shr 8) + 128 + val v = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128 + + yuv[yIndex ++] = max(0.0 , min(255.0 , y.toDouble())).toInt().toByte() + + // 每 2x2 像素写一次 UV + if ((j % 2 == 0) && (i % 2 == 0)) { + yuv[uvIndex ++] = max(0.0 , min(255.0 , v.toDouble())).toInt().toByte() + yuv[uvIndex ++] = max(0.0 , min(255.0 , u.toDouble())).toInt().toByte() + } + } + } + return yuv + } + + + fun cavansSurface(surfaceHolder : SurfaceHolder ,bitmap : Bitmap){ + var canvas : Canvas? = null; + try { + canvas = surfaceHolder.lockCanvas() + if (canvas != null) { + var src = Rect(0,0,bitmap.width,bitmap.height) + var des = Rect(0,0,surfaceHolder.surfaceFrame.width(),surfaceHolder.surfaceFrame.height()) + canvas.drawBitmap(bitmap,src,des,null) + } + } catch (e : Exception) { + Log.e("Surface" , "绘制失败:" + e.message) + } finally { + if (canvas != null) { + surfaceHolder.unlockCanvasAndPost(canvas) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..d29adb9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,5 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="transparent">#00000000</color> </resources> \ No newline at end of file -- Gitblit v1.9.1