如何将Android(Kotlin)相机预览转为视频流并传输至C#程序?
安卓相机预览流转C#程序解决方案
一、相机预览帧捕获与编码
你当前基于CameraX实现了预览和拍照功能,要获取可传输的流,需要新增ImageAnalysis用例来捕获实时预览帧,再通过硬件编码将原始帧转为适合网络传输的格式(推荐H.264,压缩率高、兼容性好)。以下是修改后的CameraManager核心代码:
修改CameraManager添加帧捕获逻辑
// 新增ImageAnalysis成员变量 private lateinit var imageAnalysis: ImageAnalysis // 用于网络传输的输出流(后续初始化) private var outputStream: OutputStream? = null private fun bindCameraUseCases() { cameraProvider?.let { cameraProvider -> val cameraSelector = getCameraSelector() val previewView = getPreviewUseCase() imageCapture = getImageCapture() // 初始化ImageAnalysis,设置只保留最新帧避免阻塞 imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() // 设置帧分析器,在后台线程处理帧编码与传输 imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> processAndSendFrame(imageProxy) imageProxy.close() // 必须关闭,否则相机资源泄漏 } cameraProvider.unbindAll() try { camera = cameraProvider.bindToLifecycle( owner, cameraSelector, previewView, imageCapture, imageAnalysis // 绑定新增的帧捕获用例 ) camera?.cameraControl?.setLinearZoom(0.0f) previewView.setSurfaceProvider(viewPreview.surfaceProvider) } catch (exc: Exception) { Log.e(TAG, "Use case binding failed $exc") } } ?: throw IllegalStateException("Camera initialization failed.") } // 处理帧并编码为H.264后发送 private fun processAndSendFrame(imageProxy: ImageProxy) { val image = imageProxy.image ?: return // 提取YUV格式帧数据(CameraX默认输出YUV_420_888) val yPlane = image.planes[0] val uPlane = image.planes[1] val vPlane = image.planes[2] // 将YUV数据转为字节数组(简化示例,实际可直接传入MediaCodec编码) val yBuffer = yPlane.buffer val uBuffer = uPlane.buffer val vBuffer = vPlane.buffer val yData = ByteArray(yBuffer.remaining()) val uData = ByteArray(uBuffer.remaining()) val vData = ByteArray(vBuffer.remaining()) yBuffer.get(yData) uBuffer.get(uData) vBuffer.get(vData) // 这里省略MediaCodec硬件编码H.264的逻辑(需提前初始化MediaCodec) // 编码后得到h264Bytes,调用sendFrame发送 // sendFrame(h264Bytes) } // 发送帧数据到C#端 private fun sendFrame(data: ByteArray) { outputStream?.run { try { // 先发送帧长度,方便C#端解析 val lengthBytes = ByteBuffer.allocate(4).putInt(data.size).array() write(lengthBytes) write(data) flush() } catch (e: IOException) { Log.e(TAG, "发送失败: ${e.message}") } } }
二、网络协议选择
推荐两种适配场景的方案:
- TCP协议:传输可靠、延迟适中,适合需要稳定画面的场景(如远程协助)。安卓端用
Socket,C#端用TcpListener/TcpClient。 - UDP协议:延迟极低但可能丢包,适合实时性要求极高的场景(如实时监控)。安卓端用
DatagramSocket,C#端用UdpClient。
安卓端TCP连接初始化示例
// 在Activity或Manager中初始化连接(建议用协程避免阻塞UI) fun connectToWindowsServer(ip: String, port: Int) { CoroutineScope(Dispatchers.IO).launch { try { val socket = Socket(ip, port) outputStream = socket.getOutputStream() Log.d(TAG, "已连接到Windows服务器") } catch (e: IOException) { Log.e(TAG, "连接失败: ${e.message}") } } }
三、C#端接收与解码
以TCP接收为例,C#端需要接收字节流,解码H.264并显示画面(可借助OpenCVSharp或FFmpeg实现解码):
C# TCP接收与显示示例
using System; using System.Net; using System.Net.Sockets; using System.Threading; using OpenCvSharp; class AndroidCameraStreamReceiver { private TcpListener _listener; private TcpClient _client; private NetworkStream _stream; public void StartListening(int port) { _listener = new TcpListener(IPAddress.Any, port); _listener.Start(); Console.WriteLine("等待安卓设备连接..."); _client = _listener.AcceptTcpClient(); _stream = _client.GetStream(); var receiveThread = new Thread(ReceiveAndDisplayFrames); receiveThread.Start(); } private void ReceiveAndDisplayFrames() { var lengthBuffer = new byte[4]; while (true) { // 读取帧长度 var bytesRead = _stream.Read(lengthBuffer, 0, 4); if (bytesRead != 4) break; var frameLength = BitConverter.ToInt32(lengthBuffer, 0); // 读取帧数据 var frameData = new byte[frameLength]; bytesRead = _stream.Read(frameData, 0, frameLength); if (bytesRead != frameLength) break; // 解码H.264帧并显示(简化示例,实际需用解码器处理) using var mat = new Mat(); // 此处省略H.264解码逻辑,可借助OpenCV的VideoCapture或FFmpeg库 Cv2.ImShow("安卓相机预览", mat); Cv2.WaitKey(1); } } }
关键注意事项
- 线程隔离:编码、网络传输必须放在后台线程,安卓用协程,C#用线程池,避免阻塞UI或主线程。
- 权限申请:安卓端需动态申请
CAMERA和INTERNET权限,否则无法正常捕获帧和传输数据。 - 资源释放:安卓退出时要关闭Socket、MediaCodec和CameraX绑定的用例;C#端关闭TcpClient和Listener,避免资源泄漏。
- 编码优化:优先使用MediaCodec硬件编码,比软件编码性能提升数倍,适配中高端安卓设备。
内容的提问来源于stack exchange,提问作者Pantastix
相关产品推荐
相关产品推荐

