| | |
| | | void *task_pushFrames(void *args){ |
| | | int64_t time = getCurrentTimestamp(); |
| | | while (TRUE){ |
| | | YUVData *mainYuvData; |
| | | int result = pushFrames.pop(mainYuvData); |
| | | if(!result){ |
| | | continue; |
| | |
| | | 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); |
| | |
| | | if(!nativeMainWindow){ |
| | | LOGE("nativeWindow 回收"); |
| | | } |
| | | if(!jvmMainCall){ |
| | | jvmMainCall = new JavaMainCall(env,clazz); |
| | | } |
| | | |
| | | } |
| | | extern "C" |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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() { |
| | |
| | | * @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"); |
| | |
| | | |
| | | 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 ) { |
| | |
| | | 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); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | |
| | | 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; |
| | |
| | | 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); |
| | |
| | | */ |
| | | 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) |
| | | } |
| | |
| | | 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 |
| | |
| | | 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(); |
| | | } |
| | |
| | | //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{ |
| | |
| | | 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 |
| | |
| | | } |
| | | //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 |
| | | |
| | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | 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) |
| | |
| | | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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,{ |
| | |
| | | } |
| | | StreamType.AUDIO,StreamType.VIDEO->{ |
| | | mViewModel!!.closePlayer(it.id) |
| | | } |
| | | StreamType.TEXT->{ |
| | | |
| | | } |
| | | StreamType.SYSTEM->{ |
| | | if(sysRecorderService != null){ |
| | |
| | | }) |
| | | } |
| | | |
| | | 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 选择文件授权 |
| | |
| | | // 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" |
| | |
| | | } |
| | | uvcHelper?.unRegister(); |
| | | } |
| | | |
| | | fun getMemoryInfo(context : Context):String? { |
| | | val memoryInfo = Debug.MemoryInfo() |
| | | Debug.getMemoryInfo(memoryInfo) // 以 KB 为单位 |
| | |
| | | |
| | | 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 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 |
| | |
| | | |
| | | var liveAdressesState = mutableStateListOf<LiveAddress>() |
| | | |
| | | var subStreamsState = mutableStateOf(listOf<StreamWindow>()) |
| | | var liveStreamsState = mutableStateOf(LiveStreams(subStreamsState)) |
| | | |
| | | var showMainBorder = mutableStateOf(false); |
| | | var mainStreamState = mutableStateOf(0) |
| | |
| | | |
| | | //新窗口 |
| | | 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){ |
| | |
| | | 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) |
| | | } |
| | | |
| | |
| | | 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), |
| | |
| | | } |
| | | } |
| | | } |
| | | 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()){ |
| | |
| | | } |
| | | } |
| | | Button(onClick = { |
| | | openInputDialog.value = true |
| | | rtmpInputDialog.value = true |
| | | }, modifier = Modifier.padding(start = 10.dp), contentPadding = PaddingValues(0.dp)) { |
| | | Text(text = "RTMP") |
| | | } |
| | |
| | | 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 = { |
| | |
| | | } |
| | | } |
| | | 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) |
| | | }) |
| | |
| | | } |
| | | |
| | | @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 = "确认") |
| | |
| | | } |
| | | }, |
| | | 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)) |
| | | }) |
| | |
| | | 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 |
| | | |
| | | |
| | | /** |
| | |
| | | players.put(streamWindow.id,player) |
| | | player.load(filePath) |
| | | } |
| | | |
| | | |
| | | |
| | | fun closePlayer(streamCode : Int){ |
| | | players.get(streamCode)?.let { |
| | |
| | | } |
| | | pushers.remove(code); |
| | | } |
| | | |
| | | fun closePusher(url : String){ |
| | | var code = getPusherCode(url); |
| | | if(code > -1){ |
| | |
| | | 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() |
| | | } |
| | | |
| | | /** |
| | | * 拉流 |
| | | */ |
| | |
| | | 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 |
| | |
| | | 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 |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | <color name="teal_700">#FF018786</color> |
| | | <color name="black">#FF000000</color> |
| | | <color name="white">#FFFFFFFF</color> |
| | | <color name="transparent">#00000000</color> |
| | | </resources> |