惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Troy Hunt's Blog
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
T
Tenable Blog
L
LINUX DO - 热门话题
V
Visual Studio Blog
I
Intezer
Blog — PlanetScale
Blog — PlanetScale
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
Know Your Adversary
Know Your Adversary
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
Netflix TechBlog - Medium
SecWiki News
SecWiki News
I
InfoQ
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
W
WeLiveSecurity
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
S
Securelist
Spread Privacy
Spread Privacy
L
LangChain Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CERT Recently Published Vulnerability Notes
罗磊的独立博客
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
博客园 - 司徒正美
Help Net Security
Help Net Security
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
PCI Perspectives
PCI Perspectives
博客园 - 【当耐特】
T
Tor Project blog

博客园 - 猫狼

安卓连接usb camera 安装Memurai 把“休眠”功能找回来,并设置成一合上盖子就生效 mathtype安装后在office无法启动,提示运行时错误53, 文件未找到:MathPage.WLL ASP.NET Web Site Project 中集成 Hangfire 并实现图片/视频压缩后台任务 SQL SERVER 数据库压缩日志步骤 Latex的一个错误, SQL 学习笔记 升级 ASP.NET 网站项目到 C# 6.0 或更高版本 C#程序集合并工具-ILRepack manim 安装 manim一个坑爸爸的问题 manim安装纪实 去掉快捷方式的小箭头 VS2022转到定义功能异常解决方案 url 传递加号,asp.net解析参数的正确处理参数 获取自然周数的下拉列表 移出Json对象的三级属性 梦记:又要去交流? 一段VBA的代码,到处是坑 多个AJAX请求,带执行进度及结果 C# 返回文件夹及子目录 解决JS跨域访问的问题 利用Jquery的map函数将json数据行转化为表格 模访京东商城jQuery省市区三级联动选择(横向DIV)
安卓手机连接USB摄像头代码
猫狼 · 2026-06-28 · via 博客园 - 猫狼
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>