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