


























package net.staredu import android.Manifest import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.graphics.YuvImage 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.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.FileWriter import java.io.PrintWriter import java.nio.ByteBuffer import java.text.SimpleDateFormat import java.util.* 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 btnSwitchFormat: Button private lateinit var ivCaptured: ImageView private var usbMonitor: USBMonitor? = null private var camera: UVCCamera? = null private var mUsbDevice: UsbDevice? = null private var mCtrlBlock: USBMonitor.UsbControlBlock? = null private var isConnected = false private var isPreviewing = false private var bestResolution = "未找到" private var bestFps = 0 private var selectedPixelFormat = -1 private val supportedFormatList = mutableListOf<Format>() private var currentFormatPos = 0 private val triedPixelFormats = mutableSetOf<Int>() // 常见像素格式常量(根据 serenegiant 库定义) private val PIXEL_FORMAT_YUYV = 0 private val PIXEL_FORMAT_MJPEG = 1 // 备选值 private val PIXEL_FORMAT_YUV420 = 2 private val PIXEL_FORMAT_NV12 = 3 private lateinit var logFile: File 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) btnSwitchFormat = findViewById(R.id.btnSwitchFormat) ivCaptured = findViewById(R.id.ivCaptured) surfaceView.holder.addCallback(surfaceHolderCallback) usbMonitor = USBMonitor(this, deviceConnectListener) btnStart.setOnClickListener { startCamera() } btnCapture.setOnClickListener { capturePhoto() } btnCapture.visibility = Button.GONE btnSwitchFormat.setOnClickListener { switchToNextFormat() } requestPermissionsIfNeeded() } private fun requestPermissionsIfNeeded() { val needPerms = mutableListOf<String>() if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { needPerms.add(Manifest.permission.CAMERA) } 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 switchToNextFormat() { if (supportedFormatList.isEmpty()) { Toast.makeText(this, "没有可用的格式列表,请先启动预览", Toast.LENGTH_SHORT).show() return } currentFormatPos = (currentFormatPos + 1) % supportedFormatList.size val format = supportedFormatList[currentFormatPos] // 这里我们无法获取 pixelFormat,只能使用 format.index 作为索引,但实际我们使用预设常量尝试 // 所以我们跳过,改为循环预设常量列表 // 改为手动切换像素格式常量 val pixelFormats = intArrayOf(PIXEL_FORMAT_YUYV, PIXEL_FORMAT_MJPEG, PIXEL_FORMAT_YUV420, PIXEL_FORMAT_NV12) val nextPixelFormat = pixelFormats[(currentFormatPos) % pixelFormats.size] Toast.makeText(this, "切换到像素格式 $nextPixelFormat", Toast.LENGTH_SHORT).show() log("用户切换格式 -> 像素格式 $nextPixelFormat") triedPixelFormats.clear() val ctrlBlock = mCtrlBlock camera?.apply { stopPreview() setFrameCallback(null, 0) close() } camera = null isPreviewing = false isConnected = false if (ctrlBlock != null) { surfaceView.postDelayed({ openCameraWithCtrlBlock(ctrlBlock, nextPixelFormat) }, 200) } else { runOnUiThread { tvStatus.text = "无设备连接,请重新启动" Toast.makeText(this, "请先连接摄像头并点击「启动预览」", Toast.LENGTH_LONG).show() } } } private fun openCameraWithCtrlBlock(ctrlBlock: USBMonitor.UsbControlBlock, pixelFormat: Int) { mCtrlBlock = ctrlBlock log("openCameraWithCtrlBlock 被调用,pixelFormat=$pixelFormat") 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, "打开摄像头失败,错误码: $result", Toast.LENGTH_LONG).show() } triedPixelFormats.add(pixelFormat) tryNextFormatAfterFailure() return } isConnected = true log("设备已连接并打开,尝试像素格式 $pixelFormat") if (supportedFormatList.isEmpty()) { val list = camera?.getSupportedFormatList() if (list != null && list.isNotEmpty()) { supportedFormatList.addAll(list) currentFormatPos = 0 log("摄像头支持的格式列表:") supportedFormatList.forEachIndexed { idx, f -> log(" [$idx] index=${f.index}, toString=${f.toString()}") } } else { log("无法获取支持的格式列表") } } findBestResolutionAndFps(pixelFormat) } catch (e: Exception) { log("打开摄像头异常: ${e.message}") e.printStackTrace() runOnUiThread { tvStatus.text = "异常: ${e.message}" Toast.makeText(this, "异常: ${e.message}", Toast.LENGTH_LONG).show() } triedPixelFormats.add(pixelFormat) tryNextFormatAfterFailure() } } private fun tryNextFormatAfterFailure() { // 我们使用预设的像素格式列表尝试,而不是依赖 Format 对象 val presetPixelFormats = intArrayOf(PIXEL_FORMAT_YUYV, PIXEL_FORMAT_MJPEG, PIXEL_FORMAT_YUV420, PIXEL_FORMAT_NV12) val untried = presetPixelFormats.filter { !triedPixelFormats.contains(it) } if (untried.isEmpty()) { log("所有预设像素格式均尝试失败,停止探测") runOnUiThread { tvStatus.text = "所有格式均失败,请检查摄像头" Toast.makeText(this, "所有格式均失败,请检查摄像头", Toast.LENGTH_LONG).show() } camera?.close() camera = null isConnected = false return } val nextPixelFormat = untried.first() log("自动切换到下一个未尝试格式: pixelFormat=$nextPixelFormat") runOnUiThread { tvStatus.text = "格式失败,尝试下一个..." Toast.makeText(this, "自动切换格式", Toast.LENGTH_SHORT).show() } camera?.close() camera = null isConnected = false mCtrlBlock?.let { openCameraWithCtrlBlock(it, nextPixelFormat) } } private fun findBestResolutionAndFps(pixelFormat: Int) { log("开始探测,当前像素格式=$pixelFormat") triedPixelFormats.add(pixelFormat) val allSizes = camera?.getSupportedSizeList() ?: emptyList() val sizeList = if (allSizes.isNotEmpty()) { log("获取到 ${allSizes.size} 个支持的尺寸: ${allSizes.joinToString { "${it.width}x${it.height}" }}") allSizes.map { Pair(it.width, it.height) } } else { log("未获取到支持的尺寸列表,使用预设尺寸(可能不准确)") listOf( Pair(1920, 1080), Pair(1280, 720), Pair(800, 600), Pair(1024, 768), Pair(640, 480), Pair(320, 240) ) } val distinctSizes = sizeList.distinct() val sortedSizes = distinctSizes.sortedWith(compareByDescending { it.first * it.second }) val fpsPriority = intArrayOf(30, 25, 20, 15, 10, 5) var found = false // 先尝试无帧率版本的 setPreviewSize (两个参数) for ((width, height) in sortedSizes) { try { // 尝试只设置尺寸,不指定格式和帧率(使用默认) camera?.setPreviewSize(width, height) log("✅ 成功: ${width}x${height} (无格式无帧率)") bestResolution = "${width}x${height}" bestFps = 0 // 未知 selectedPixelFormat = pixelFormat found = true break } catch (e: Exception) { log("❌ 失败: ${width}x${height} (无格式无帧率), error: ${e.message}") // 继续尝试带帧率版本 for (fps in fpsPriority) { try { // 尝试指定像素格式和帧率 camera?.setPreviewSize(width, height, fps, pixelFormat) log("✅ 成功: ${width}x${height} @ ${fps}fps, pixelFormat=$pixelFormat") bestResolution = "${width}x${height}" bestFps = fps selectedPixelFormat = pixelFormat found = true break } catch (e2: Exception) { log("❌ 失败: ${width}x${height} @ ${fps}fps, pixelFormat=$pixelFormat, error: ${e2.message}") // 尝试不指定像素格式,只指定尺寸和帧率 try { camera?.setPreviewSize(width, height, fps) log("✅ 成功: ${width}x${height} @ ${fps}fps (无格式)") bestResolution = "${width}x${height}" bestFps = fps selectedPixelFormat = -1 // 无格式 found = true break } catch (e3: Exception) { log("❌ 失败: ${width}x${height} @ ${fps}fps (无格式), error: ${e3.message}") } } if (found) break } } if (found) break } if (!found) { log("当前像素格式 $pixelFormat 无可用组合") tryNextFormatAfterFailure() return } val formatName = if (selectedPixelFormat >= 0) "像素格式 $selectedPixelFormat" else "默认格式" val bestText = "最佳参数: $bestResolution @ ${bestFps}fps, $formatName" log(bestText) runOnUiThread { tvStatus.text = bestText Toast.makeText(this, 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("预览已启动") } } // 拍照和转换函数保持不变(略,可沿用之前版本) 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}") camera?.setFrameCallback(object : IFrameCallback { override fun onFrame(frame: ByteBuffer) { try { val remaining = frame.remaining() val bytes = ByteArray(remaining) frame.get(bytes) val jpegData: ByteArray? = when (selectedPixelFormat) { PIXEL_FORMAT_MJPEG -> bytes else -> 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) } } }, if (selectedPixelFormat >= 0) selectedPixelFormat else 0) } 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 + 3 < data.size) { 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 nv21[yIndex++] = y1.toByte() nv21[yIndex++] = y2.toByte() val pairIndex = i / 4 val pixelIndex = pairIndex * 2 val row = pixelIndex / width if (row % 2 == 0) { 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 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 } mCtrlBlock = ctrlBlock runOnUiThread { tvStatus.text = "设备已打开,正在探测最佳参数..." } triedPixelFormats.clear() // 从预设常量开始尝试,不使用 Format 中的 pixelFormat val firstPixelFormat = PIXEL_FORMAT_YUYV openCameraWithCtrlBlock(ctrlBlock, firstPixelFormat) } override fun onDeviceClose(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?) { camera?.close() camera = null isConnected = false isPreviewing = false mCtrlBlock = null runOnUiThread { tvStatus.text = "设备已关闭" btnStart.isEnabled = true btnCapture.visibility = Button.GONE } log("设备已关闭") } override fun onDetach(device: UsbDevice?) { camera?.close() camera = null isConnected = false isPreviewing = false mCtrlBlock = null 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 销毁,预览停止") } } 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) } 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) { } } 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"/> <!-- 预览区域(SurfaceView) --> <FrameLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#000000"> <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"/> <Button android:id="@+id/btnSwitchFormat" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="切换格式" android:backgroundTint="#FF9800" android:textColor="#FFFFFF"/> </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>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。