安卓手机连接USB摄像头代码

package net.staredu

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.serenegiant.usb.*
import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.PrintWriter
import java.text.SimpleDateFormat
import java.util.*
import java.nio.ByteBuffer
import android.graphics.YuvImage
import java.io.ByteArrayOutputStream


class USBCameraTestActivity : AppCompatActivity() {

    private lateinit var surfaceView: SurfaceView
    private lateinit var tvStatus: TextView
    private lateinit var btnStart: Button
    private lateinit var btnCapture: Button
    private lateinit var ivCaptured: ImageView

    private var usbMonitor: USBMonitor? = null
    private var camera: UVCCamera? = null
    private var mUsbDevice: UsbDevice? = null

    private lateinit var logFile: File
    private var bestResolution = "未找到"
    private var bestFps = 0

    // 状态标志
    private var isConnected = false
    private var isPreviewing = false

    private val deviceConnectListener = object : USBMonitor.OnDeviceConnectListener {
        override fun onAttach(device: UsbDevice?) {
            runOnUiThread {
                tvStatus.text = "设备已连接: ${device?.deviceName}"
                Toast.makeText(this@USBCameraTestActivity, "设备已连接", Toast.LENGTH_SHORT).show()
                log("设备已连接: ${device?.deviceName}")
                device?.let { usbMonitor?.requestPermission(it) }
            }
        }

        override fun onDeviceOpen(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?, createNew: Boolean) {
            if (ctrlBlock == null) {
                log("onDeviceOpen: ctrlBlock is null")
                return
            }
            runOnUiThread {
                tvStatus.text = "设备已打开,正在探测最佳参数..."
            }
            try {
                val param = UVCParam()
                camera = UVCCamera(param)
                val result = camera?.open(ctrlBlock) ?: -1
                log("UVCCamera.open 返回: $result")
                if (result != 0) {
                    runOnUiThread {
                        tvStatus.text = "打开摄像头失败,错误码: $result"
                        Toast.makeText(this@USBCameraTestActivity, "打开摄像头失败,错误码: $result", Toast.LENGTH_LONG).show()
                    }
                    log("打开摄像头失败,错误码: $result")
                    return
                }

                // 标记已连接
                isConnected = true
                log("设备已连接并打开")

                // ========== 按优先级探测最佳分辨率和帧率 ==========
                val resolutionPriority = listOf(
                    Pair(1920, 1080),
                    Pair(1280, 720),
                    Pair(800, 600),
                    Pair(1024, 768),
                    Pair(640, 480),
                    Pair(320, 240)
                )
                val fpsPriority = intArrayOf(60, 50, 40, 30, 25, 20, 15, 10, 5)

                var found = false
                for ((width, height) in resolutionPriority) {
                    for (fps in fpsPriority) {
                        try {
                            camera?.setPreviewSize(width, height, fps)
                            log("✅ 成功: ${width}x${height} @ ${fps}fps")
                            bestResolution = "${width}x${height}"
                            bestFps = fps
                            found = true
                            break
                        } catch (e: Exception) {
                            log("❌ 失败: ${width}x${height} @ ${fps}fps, error: ${e.message}")
                        }
                    }
                    if (found) break
                }

                if (!found) {
                    log("所有组合均失败")
                    runOnUiThread {
                        tvStatus.text = "无法找到可用的预览参数"
                        Toast.makeText(this@USBCameraTestActivity, "所有分辨率/帧率组合均失败", Toast.LENGTH_LONG).show()
                    }
                    camera?.close()
                    camera = null
                    return
                }

                val bestText = "最佳参数: $bestResolution @ ${bestFps}fps"
                log(bestText)
                runOnUiThread {
                    tvStatus.text = bestText
                    Toast.makeText(this@USBCameraTestActivity, bestText, Toast.LENGTH_LONG).show()
                }

                // 设置预览显示
                if (surfaceView.holder.surface != null) {
                    camera?.setPreviewDisplay(surfaceView.holder)
                    camera?.startPreview()
                    isPreviewing = true
                    runOnUiThread {
                        tvStatus.text = "预览中 ($bestResolution @ ${bestFps}fps)"
                        btnStart.isEnabled = false
                        btnCapture.visibility = Button.VISIBLE
                    }
                    log("预览已启动")
                }
            } catch (e: Exception) {
                log("打开摄像头异常: ${e.message}")
                e.printStackTrace()
                runOnUiThread {
                    tvStatus.text = "异常: ${e.message}"
                    Toast.makeText(this@USBCameraTestActivity, "异常: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
        }

        override fun onDeviceClose(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?) {
            camera?.close()
            camera = null
            isConnected = false
            isPreviewing = false
            runOnUiThread {
                tvStatus.text = "设备已关闭"
                btnStart.isEnabled = true
                btnCapture.visibility = Button.GONE
            }
            log("设备已关闭")
        }

        override fun onDetach(device: UsbDevice?) {
            camera?.close()
            camera = null
            isConnected = false
            isPreviewing = false
            runOnUiThread {
                tvStatus.text = "设备已拔出"
                Toast.makeText(this@USBCameraTestActivity, "设备已拔出", Toast.LENGTH_SHORT).show()
                btnStart.isEnabled = true
                btnCapture.visibility = Button.GONE
            }
            log("设备已拔出")
        }

        override fun onCancel(device: UsbDevice?) {
            runOnUiThread {
                tvStatus.text = "USB 权限被拒绝"
                Toast.makeText(this@USBCameraTestActivity, "USB 权限被拒绝", Toast.LENGTH_LONG).show()
            }
            log("USB 权限被拒绝")
        }
    }

    private val surfaceHolderCallback = object : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder) {
            if (camera != null && isPreviewing) {
                camera?.setPreviewDisplay(holder)
                camera?.startPreview()
                runOnUiThread {
                    tvStatus.text = "预览中 ($bestResolution @ ${bestFps}fps)"
                }
                log("Surface 创建,预览恢复")
            }
        }
        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
        override fun surfaceDestroyed(holder: SurfaceHolder) {
            camera?.stopPreview()
            runOnUiThread {
                tvStatus.text = "预览已停止"
            }
            log("Surface 销毁,预览停止")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_usb_camera_test)

        initLogFile()

        surfaceView = findViewById(R.id.surfaceView)
        tvStatus = findViewById(R.id.tvStatus)
        btnStart = findViewById(R.id.btnStart)
        btnCapture = findViewById(R.id.btnCapture)
        ivCaptured = findViewById(R.id.ivCaptured)

        surfaceView.holder.addCallback(surfaceHolderCallback)

        usbMonitor = USBMonitor(this, deviceConnectListener)

        btnStart.setOnClickListener {
            startCamera()
        }

        btnCapture.setOnClickListener {
            capturePhoto()
        }
        btnCapture.visibility = Button.GONE // 初始隐藏

        // 申请权限
        val needPerms = mutableListOf<String>()
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            needPerms.add(Manifest.permission.CAMERA)
        }
        // Android13+ 外部存储媒体权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
                needPerms.add(Manifest.permission.READ_MEDIA_IMAGES)
            }
        } else {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                needPerms.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            }
        }
        if (needPerms.isNotEmpty()) {
            ActivityCompat.requestPermissions(this, needPerms.toTypedArray(), 100)
        }
    }

    private fun capturePhoto() {
        if (camera == null || !isPreviewing) {
            Toast.makeText(this, "请先启动预览", Toast.LENGTH_SHORT).show()
            return
        }

        val previewSize = camera?.getPreviewSize()
        if (previewSize == null) {
            Toast.makeText(this, "无法获取预览尺寸", Toast.LENGTH_SHORT).show()
            return
        }
        val width = previewSize.width
        val height = previewSize.height
        log("拍照尺寸: ${width}x${height}")

        val PIXEL_FORMAT_YUY2 = 0
        camera?.setFrameCallback(object : IFrameCallback {
            override fun onFrame(frame: ByteBuffer) {
                try {
                    val remaining = frame.remaining()
                    log("帧数据长度: $remaining, 预期 NV21: ${width * height * 3 / 2}, 预期 YUY2: ${width * height * 2}")

                    val bytes = ByteArray(remaining)
                    frame.get(bytes)

                    val jpegData = convertYUY2toJPEG(bytes, width, height)

                    if (jpegData == null || jpegData.isEmpty()) {
                        runOnUiThread {
                            Toast.makeText(this@USBCameraTestActivity, "图片转换失败", Toast.LENGTH_SHORT).show()
                        }
                        return
                    }

                    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
                    val fileName = "UVC_${timeStamp}.jpg"
                    val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
                    val file = File(storageDir, fileName)
                    FileOutputStream(file).use { fos ->
                        fos.write(jpegData)
                    }

                    runOnUiThread {
                        val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
                        ivCaptured.setImageBitmap(bitmap)
                        Toast.makeText(this@USBCameraTestActivity, "拍照成功: ${file.absolutePath}", Toast.LENGTH_LONG).show()
                        tvStatus.text = "拍照成功"
                    }
                    log("拍照成功: ${file.absolutePath}")

                } catch (e: Exception) {
                    log("拍照异常: ${e.message}")
                    e.printStackTrace()
                    runOnUiThread {
                        Toast.makeText(this@USBCameraTestActivity, "拍照失败: ${e.message}", Toast.LENGTH_SHORT).show()
                        tvStatus.text = "拍照失败"
                    }
                } finally {
                    camera?.setFrameCallback(null, 0)
                }
            }
        }, PIXEL_FORMAT_YUY2)
    }

    private fun convertYUVtoJPEG(data: ByteArray, width: Int, height: Int, format: Int): ByteArray? {
        return try {
            val yuvImage = YuvImage(data, format, width, height, null)
            val out = ByteArrayOutputStream()
            yuvImage.compressToJpeg(android.graphics.Rect(0, 0, width, height), 80, out)
            out.toByteArray()
        } catch (e: Exception) {
            log("YUV转换失败: ${e.message}")
            null
        }
    }

    private fun convertYUY2toJPEG(data: ByteArray, width: Int, height: Int): ByteArray? {
        try {
            val nv21 = ByteArray(width * height * 3 / 2)
            var yIndex = 0
            var uvIndex = width * height
            var i = 0
            while (i < data.size) {
                // YUY2 排列: Y1 U Y2 V
                val y1 = data[i].toInt() and 0xFF
                val u  = data[i + 1].toInt() and 0xFF
                val y2 = data[i + 2].toInt() and 0xFF
                val v  = data[i + 3].toInt() and 0xFF

                // 写入两个 Y 分量
                nv21[yIndex++] = y1.toByte()
                nv21[yIndex++] = y2.toByte()

                // 计算当前像素对所属的行
                val pairIndex = i / 4
                val pixelIndex = pairIndex * 2
                val row = pixelIndex / width

                // 只保留偶数行的 UV(垂直下采样)
                if (row % 2 == 0) {
                    // ✅ 修正:NV21 要求先 V 后 U(V U V U ...)
                    nv21[uvIndex++] = v.toByte()
                    nv21[uvIndex++] = u.toByte()
                }

                i += 4
            }

            val yuvImage = YuvImage(nv21, android.graphics.ImageFormat.NV21, width, height, null)
            val out = ByteArrayOutputStream()
            yuvImage.compressToJpeg(android.graphics.Rect(0, 0, width, height), 80, out)
            return out.toByteArray()
        } catch (e: Exception) {
            log("YUY2转换失败: ${e.message}")
            e.printStackTrace()
            return null
        }
    }

    private fun saveBitmap(bitmap: android.graphics.Bitmap) {
        try {
            val dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
            if (dir != null && !dir.exists()) dir.mkdirs()
            val file = File(dir, "UVC_${System.currentTimeMillis()}.jpg")
            FileOutputStream(file).use { fos ->
                bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 90, fos)
            }
            Toast.makeText(this, "已保存: ${file.absolutePath}", Toast.LENGTH_LONG).show()
        } catch (e: Exception) {
            log("保存图片失败: ${e.message}")
        }
    }

    private fun initLogFile() {
        try {
            val dir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: filesDir
            val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
            logFile = File(dir, "uvc_log_$timestamp.txt")
            log("=== UVC 测试日志开始 ===")
            log("设备信息: ${Build.MANUFACTURER} ${Build.MODEL}, Android ${Build.VERSION.RELEASE}")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun log(message: String) {
        try {
            FileWriter(logFile, true).use { writer ->
                PrintWriter(writer).use { pw ->
                    val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
                    pw.println("[$timestamp] $message")
                    pw.flush()
                }
            }
        } catch (e: Exception) {
        }
    }

    private fun startCamera() {
        if (isConnected) {
            Toast.makeText(this, "摄像头已连接,无需重复启动", Toast.LENGTH_SHORT).show()
            return
        }
        log("启动摄像头")
        usbMonitor?.register()

        val usbManager = getSystemService(USB_SERVICE) as UsbManager
        val deviceList = usbManager.deviceList
        log("检测到 USB 设备数量: ${deviceList.size}")
        if (deviceList.isEmpty()) {
            runOnUiThread {
                Toast.makeText(this, "未检测到 USB 设备,请连接摄像头", Toast.LENGTH_SHORT).show()
                tvStatus.text = "未检测到设备"
            }
            log("未检测到 USB 设备")
            return
        }

        val device = deviceList.values.firstOrNull()
        if (device == null) {
            runOnUiThread {
                Toast.makeText(this, "未找到 USB 摄像头", Toast.LENGTH_SHORT).show()
                tvStatus.text = "未找到摄像头"
            }
            log("USB 设备列表为空")
            return
        }

        mUsbDevice = device
        log("请求权限设备: ${device.deviceName}, VID: ${device.vendorId}, PID: ${device.productId}")
        runOnUiThread {
            tvStatus.text = "正在请求 USB 权限..."
        }
        usbMonitor?.requestPermission(device)
    }

    override fun onResume() {
        super.onResume()
        usbMonitor?.register()
        log("onResume")
    }

    override fun onPause() {
        super.onPause()
        usbMonitor?.unregister()
        if (isPreviewing) {
            camera?.stopPreview()
        }
        log("onPause")
    }

    override fun onDestroy() {
        super.onDestroy()
        camera?.stopPreview()
        camera?.close()
        camera = null
        usbMonitor?.destroy()
        log("onDestroy")
        log("=== 日志结束 ===")
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == 100) {
            var allGranted = true
            grantResults.forEach {
                if (it != PackageManager.PERMISSION_GRANTED) allGranted = false
            }
            if (allGranted) {
                log("全部权限已授予")
            } else {
                log("权限被拒绝,部分功能无法使用")
                Toast.makeText(this, "缺少必要存储/相机权限", Toast.LENGTH_LONG).show()
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#000000"
    android:padding="10dp">

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="等待设备..."
        android:textColor="#FFFFFF"
        android:textSize="16sp"
        android:gravity="center"
        android:padding="8dp"/>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#000000">
        <!-- 使用 SurfaceView,并固定 16:9 比例(通过 layout_weight + 背景色占位) -->
        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"/>
    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp">

        <Button
            android:id="@+id/btnStart"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="启动预览"
            android:backgroundTint="#2196F3"
            android:textColor="#FFFFFF"/>
        <Button
            android:id="@+id/btnCapture"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="拍照"
            android:backgroundTint="#4CAF50"
            android:textColor="#FFFFFF"
            android:visibility="gone"/>
    </LinearLayout>

    <ImageView
        android:id="@+id/ivCaptured"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:layout_marginTop="8dp"
        android:background="#333333"
        android:scaleType="centerInside"/>
</LinearLayout>

 

posted @ 2026-06-28 07:04  猫狼  阅读(13)  评论(0)    收藏  举报