USB 摄像头+远程遥控+中英的适配+topic的集成和状态的使用到相应页面

This commit is contained in:
2026-10-20 17:03:37 +08:00
parent 178cd2533c
commit 582242e9aa
144 changed files with 15759 additions and 406 deletions

View File

@@ -0,0 +1,83 @@
package com.maibu.usb_camera_android;
import android.content.Context;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformViewRegistry;
public class UsbCameraAndroidPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {
private static final String METHOD_CHANNEL = "com.maibu.usb_camera_android/methods";
private static final String EVENT_CHANNEL = "com.maibu.usb_camera_android/events";
private static final String VIEW_TYPE = "com.maibu.usb_camera_android/preview";
private MethodChannel methodChannel;
private EventChannel eventChannel;
private UsbCameraEngine engine;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
final Context context = binding.getApplicationContext();
final BinaryMessenger messenger = binding.getBinaryMessenger();
engine = new UsbCameraEngine(context);
methodChannel = new MethodChannel(messenger, METHOD_CHANNEL);
methodChannel.setMethodCallHandler(this);
eventChannel = new EventChannel(messenger, EVENT_CHANNEL);
eventChannel.setStreamHandler(engine);
final PlatformViewRegistry registry = binding.getPlatformViewRegistry();
registry.registerViewFactory(VIEW_TYPE, new UsbCameraViewFactory(engine));
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "getDeviceList":
result.success(engine.getDeviceList());
break;
case "getSupportedResolutions":
result.success(engine.getSupportedResolutions());
break;
case "getPreviewSize":
result.success(engine.getPreviewSize());
break;
case "open":
final String deviceId = call.argument("deviceId");
final String resolution = call.argument("resolution");
engine.open(deviceId, resolution, result);
break;
case "close":
engine.close(result);
break;
case "setResolution":
final String res = call.argument("resolution");
engine.setResolution(res, result);
break;
default:
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
if (methodChannel != null) {
methodChannel.setMethodCallHandler(null);
methodChannel = null;
}
if (eventChannel != null) {
eventChannel.setStreamHandler(null);
eventChannel = null;
}
if (engine != null) {
engine.dispose();
engine = null;
}
}
}

View File

@@ -0,0 +1,395 @@
package com.maibu.usb_camera_android;
import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.herohan.uvcapp.CameraHelper;
import com.herohan.uvcapp.ICameraHelper;
import com.serenegiant.usb.Size;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
/**
* UVC 摄像头引擎:封装 CameraHelper,管理设备状态和预览。
*
* 权限流程:
* 1. open() 先查 hasPermission() → 有则直接 selectDevice()
* 2. 无权限 → 通过静态 UsbPermissionReceiver 申请(Android 14+ 兼容)
* 3. 用户点"允许"→ Receiver 回调 → selectDevice()
* 用户点"拒绝"→ Receiver 回调 → completeOpen(拒绝)
* 4. 若异常(PendingIntent 创建失败等),立即 completeOpen 避免 Flutter
* MethodChannel 双重回复崩溃。
*
* 为什么必须用静态 Receiver(详见 UsbPermissionReceiver 类注释):
* Android 14+ 禁止 FLAG_MUTABLE + 隐式 Intent,动态 BroadcastReceiver
* 无法构造显式 Intent → 只能用 manifest 静态注册。
*/
public class UsbCameraEngine implements EventChannel.StreamHandler {
private static final String TAG = "UsbCameraEngine";
private final Context appContext;
private final UsbManager usbManager;
private final Handler mainHandler;
private final ICameraHelper cameraHelper;
private Surface currentSurface;
private boolean isPreviewing = false;
private UsbDevice currentDevice;
private List<Size> supportedSizes = new ArrayList<>();
private EventChannel.EventSink eventSink;
private MethodChannel.Result pendingOpenResult;
public UsbCameraEngine(@NonNull Context context) {
this.appContext = context.getApplicationContext();
this.usbManager = (UsbManager) appContext.getSystemService(Context.USB_SERVICE);
this.mainHandler = new Handler(Looper.getMainLooper());
this.cameraHelper = new CameraHelper();
this.cameraHelper.setStateCallback(stateCallback);
}
// ---------- 设备枚举 ----------
public List<Map<String, Object>> getDeviceList() {
List<Map<String, Object>> result = new ArrayList<>();
for (UsbDevice device : usbManager.getDeviceList().values()) {
if (isUvcDevice(device)) {
result.add(deviceToMap(device));
}
}
return result;
}
/**
* 判断是否 UVC 设备。
* 1. 接口级 class==14 (USB Video Class)
* 2. 设备级 class==239/0xEF(复合设备 Miscellaneous,UVC采集卡常用)
*/
private boolean isUvcDevice(@NonNull UsbDevice device) {
if (device.getDeviceClass() == 239
&& device.getDeviceSubclass() == 2
&& device.getDeviceProtocol() == 1) {
return true;
}
for (int i = 0; i < device.getInterfaceCount(); i++) {
if (device.getInterface(i).getInterfaceClass() == 14) {
return true;
}
}
return false;
}
private Map<String, Object> deviceToMap(@NonNull UsbDevice device) {
Map<String, Object> map = new HashMap<>();
map.put("deviceId", String.valueOf(device.getDeviceId()));
map.put("vendorId", device.getVendorId());
map.put("productId", device.getProductId());
map.put("productName", device.getProductName());
map.put("hasPermission", usbManager.hasPermission(device) ? 1 : 0);
return map;
}
// ---------- 打开 / 关闭 ----------
/**
* 打开指定设备:
* 1. 找设备
* 2. hasPermission() → selectDevice / requestUsbPermission
* 3. try-catch 保护:PendingIntent 异常时立即报错给 Dart,避免 result 残留
* 导致后续 close() 二次回复崩溃 "Reply already submitted"。
*/
public void open(
@NonNull String deviceId,
@Nullable String resolution,
@NonNull MethodChannel.Result result) {
UsbDevice target = null;
for (UsbDevice device : usbManager.getDeviceList().values()) {
if (String.valueOf(device.getDeviceId()).equals(deviceId)) {
target = device;
break;
}
}
if (target == null) {
result.error("DEVICE_NOT_FOUND", "未找到指定 USB 设备: " + deviceId, null);
return;
}
cancelPendingOpen();
pendingOpenResult = result;
currentDevice = target;
try {
if (usbManager.hasPermission(target)) {
Log.d(TAG, "已有 USB 权限,直接 selectDevice");
cameraHelper.selectDevice(target);
} else {
Log.d(TAG, "无 USB 权限,通过 UsbPermissionReceiver 申请: " + deviceId);
requestUsbPermission(target);
}
} catch (Throwable e) {
// 例如 Android 14+ PendingIntent 创建异常、UsbManager 服务不可用等
Log.e(TAG, "requestPermission 异常: " + e.getMessage(), e);
completeOpen("申请 USB 权限失败: " + e.getMessage());
}
}
/**
* 通过静态 UsbPermissionReceiver + 显式 PendingIntent 申请 USB 权限。
*
* 关键点:
* - 显式 Intent(new Intent(context, UsbPermissionReceiver.class))
* 使 FLAG_MUTABLE 在 Android 14+ 合法。
* - FLAG_MUTABLE 允许系统注入 EXTRA_DEVICE / EXTRA_PERMISSION_GRANTED。
* - 一次性静态回调 sCallback 在 Receiver 内部触发后立即清空。
*/
private void requestUsbPermission(@NonNull UsbDevice device) {
UsbPermissionReceiver.sCallback = (recvDevice, granted) -> mainHandler.post(() -> {
if (granted) {
Log.d(TAG, "用户已授予 USB 权限 → selectDevice");
cameraHelper.selectDevice(recvDevice);
} else {
Log.d(TAG, "用户拒绝了 USB 权限");
completeOpen("用户拒绝了 USB 权限,请点击「重试」并在弹窗中选择「允许」");
postEvent("cancelled", deviceToMap(recvDevice));
}
});
android.app.PendingIntent pi =
UsbPermissionReceiver.createPermissionIntent(appContext, device);
usbManager.requestPermission(device, pi);
}
public void close(@NonNull MethodChannel.Result result) {
cancelPendingOpen();
UsbPermissionReceiver.sCallback = null; // 取消等待中的权限回调
if (cameraHelper.isCameraOpened()) {
cameraHelper.closeCamera();
}
currentDevice = null;
isPreviewing = false;
if (currentSurface != null) {
cameraHelper.removeSurface(currentSurface);
}
result.success(null);
}
/**
* 取消等待中的 open MethodChannel 回调。
* 用 try-catch 保护:如果 Flutter 框架已经回复过(例如异常时自动 error),
* 重复调用 result.error() 会抛 IllegalStateException("Reply already submitted")
* 并导致进程崩溃。忽略即可。
*/
private void cancelPendingOpen() {
if (pendingOpenResult != null) {
final MethodChannel.Result result = pendingOpenResult;
pendingOpenResult = null;
mainHandler.post(() -> {
try {
result.error("CANCELLED", "操作被取消", null);
} catch (IllegalStateException ignored) {
// 框架已回复,重复调用无效,忽略
}
});
}
}
// ---------- 分辨率 ----------
/**
* 获取当前预览尺寸(width x height),用于 Flutter 侧计算 AspectRatio。
* 摄像头未打开时返回 null。
*/
@Nullable
public Map<String, Object> getPreviewSize() {
if (!isPreviewing) return null;
Size size = cameraHelper.getPreviewSize();
if (size == null) return null;
Map<String, Object> map = new HashMap<>();
map.put("width", size.width);
map.put("height", size.height);
return map;
}
public List<String> getSupportedResolutions() {
List<String> list = new ArrayList<>();
for (Size size : supportedSizes) {
list.add(size.width + "x" + size.height);
}
return list;
}
public void setResolution(@NonNull String resolution, @NonNull MethodChannel.Result result) {
Size target = findResolution(resolution);
if (target == null) {
result.error("INVALID_RESOLUTION", "无法解析分辨率: " + resolution, null);
return;
}
if (isPreviewing) {
cameraHelper.stopPreview();
cameraHelper.setPreviewSize(target);
cameraHelper.startPreview();
if (currentSurface != null && currentSurface.isValid()) {
cameraHelper.addSurface(currentSurface, false);
}
}
result.success(null);
}
@Nullable
private Size findResolution(@NonNull String resolution) {
String[] parts = resolution.split("x");
if (parts.length != 2) return null;
try {
int width = Integer.parseInt(parts[0].trim());
int height = Integer.parseInt(parts[1].trim());
for (Size size : supportedSizes) {
if (size.width == width && size.height == height) {
return size;
}
}
} catch (NumberFormatException ignored) {
}
return null;
}
// ---------- Surface 管理 ----------
public void addSurface(@NonNull Surface surface) {
currentSurface = surface;
if (isPreviewing && surface.isValid()) {
cameraHelper.addSurface(surface, false);
}
}
public void removeSurface(@NonNull Surface surface) {
if (currentSurface == surface) {
if (isPreviewing) {
cameraHelper.removeSurface(surface);
}
currentSurface = null;
}
}
// ---------- 生命周期 ----------
public void dispose() {
UsbPermissionReceiver.sCallback = null;
cameraHelper.release();
}
// ---------- CameraHelper 状态回调 ----------
private final ICameraHelper.StateCallback stateCallback = new ICameraHelper.StateCallback() {
@Override
public void onAttach(@NonNull UsbDevice device) {
postEvent("attached", deviceToMap(device));
}
@Override
public void onDeviceOpen(@NonNull UsbDevice device, boolean isFirstOpen) {
cameraHelper.openCamera();
}
@Override
public void onCameraOpen(@NonNull UsbDevice device) {
supportedSizes = cameraHelper.getSupportedSizeList();
startPreview();
completeOpen(null);
postEvent("opened", deviceToMap(device));
}
@Override
public void onCameraClose(@NonNull UsbDevice device) {
isPreviewing = false;
if (currentSurface != null) {
cameraHelper.removeSurface(currentSurface);
}
}
@Override
public void onDeviceClose(@NonNull UsbDevice device) {
}
@Override
public void onDetach(@NonNull UsbDevice device) {
if (currentDevice != null && currentDevice.getDeviceId() == device.getDeviceId()) {
currentDevice = null;
isPreviewing = false;
postEvent("detached", deviceToMap(device));
}
}
@Override
public void onCancel(@NonNull UsbDevice device) {
// UVCAndroid 内部权限被拒(通常被 UsbPermissionReceiver 拦截,此处兜底)
completeOpen("用户拒绝了 USB 权限,请点击「重试」并在弹窗中选择「允许」");
postEvent("cancelled", deviceToMap(device));
}
};
// ---------- 预览启动 ----------
private void startPreview() {
if (isPreviewing) return;
cameraHelper.startPreview();
isPreviewing = true;
if (currentSurface != null && currentSurface.isValid()) {
cameraHelper.addSurface(currentSurface, false);
}
}
// ---------- MethodChannel 异步回调 ----------
private void completeOpen(@Nullable String error) {
if (pendingOpenResult == null) return;
final MethodChannel.Result result = pendingOpenResult;
pendingOpenResult = null;
mainHandler.post(() -> {
try {
if (error == null) {
result.success(null);
} else {
result.error("OPEN_FAILED", error, null);
}
} catch (IllegalStateException ignored) {
// Flutter 框架已回复,重复调用无效,忽略
}
});
}
// ---------- EventChannel.StreamHandler ----------
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = events;
}
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
private void postEvent(@NonNull String type, @NonNull Map<String, Object> data) {
mainHandler.post(() -> {
if (eventSink == null) return;
Map<String, Object> event = new HashMap<>();
event.put("type", type);
event.put("device", data);
eventSink.success(event);
});
}
}

View File

@@ -0,0 +1,49 @@
package com.maibu.usb_camera_android;
import android.content.Context;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import io.flutter.plugin.platform.PlatformView;
public class UsbCameraView implements PlatformView {
private final SurfaceView surfaceView;
public UsbCameraView(Context context, UsbCameraEngine engine) {
surfaceView = new SurfaceView(context);
surfaceView.setZOrderMediaOverlay(true);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
engine.addSurface(holder.getSurface());
}
@Override
public void surfaceChanged(
@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
engine.removeSurface(holder.getSurface());
}
});
}
@Override
public View getView() {
return surfaceView;
}
@Override
public void dispose() {
Surface surface = surfaceView.getHolder().getSurface();
if (surface != null) {
surface.release();
}
}
}

View File

@@ -0,0 +1,27 @@
package com.maibu.usb_camera_android;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
public class UsbCameraViewFactory extends PlatformViewFactory {
private final UsbCameraEngine engine;
public UsbCameraViewFactory(@NonNull UsbCameraEngine engine) {
super(StandardMessageCodec.INSTANCE);
this.engine = engine;
}
@NonNull
@Override
public PlatformView create(
@NonNull Context context, int id, @Nullable Object args) {
return new UsbCameraView(context, engine);
}
}

View File

@@ -0,0 +1,72 @@
package com.maibu.usb_camera_android;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
/**
* USB 权限结果广播接收器(manifest 静态注册)。
*
* 为什么必须用静态 Receiver 而不能用动态注册的 BroadcastReceiver:
* - Android 14 (API 34+) 禁止 FLAG_MUTABLE + 隐式 Intent 组合,直接抛
* IllegalArgumentException("Targeting U+ disallows creating a PendingIntent
* with FLAG_MUTABLE, an implicit Intent...")。
* - 动态 BroadcastReceiver 无法在 Intent 里指定目标 class,只能是隐式 →
* FLAG_MUTABLE 在 Android 14+ 崩溃;FLAG_IMMUTABLE 又会导致系统无法注入
* EXTRA_DEVICE / EXTRA_PERMISSION_GRANTED,权限结果永远读不到。
* - 静态 Receiver 在 manifest 声明,可用 new Intent(context, 本类.class)
* 构造显式 Intent → FLAG_MUTABLE 合法 → 系统正常注入 extras。
*
* 权限结果通过一次性静态回调 sCallback 传给 UsbCameraEngine,收到后立刻清空。
*/
public class UsbPermissionReceiver extends BroadcastReceiver {
static final String ACTION_USB_PERMISSION =
"com.maibu.usb_camera_android.USB_PERMISSION";
interface Callback {
void onResult(UsbDevice device, boolean granted);
}
/** 同一时刻只有一个等待中的权限请求 */
static volatile Callback sCallback;
@Override
public void onReceive(Context context, Intent intent) {
if (!ACTION_USB_PERMISSION.equals(intent.getAction())) return;
@SuppressWarnings("deprecation")
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
boolean granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false);
Callback cb = sCallback;
sCallback = null; // 一次性,防止旧回调串场
if (cb != null && device != null) {
cb.onResult(device, granted);
}
}
/**
* 构造 Android 14+ 合法的 USB 权限 PendingIntent。
*
* 显式 Intent(指定 UsbPermissionReceiver.class)
* → 允许搭配 FLAG_MUTABLE → 系统可注入权限结果 extras。
* API 30 及以下 PendingIntent 默认可变,不需要显式 FLAG_MUTABLE。
*/
static PendingIntent createPermissionIntent(Context context, UsbDevice device) {
Intent intent = new Intent(context, UsbPermissionReceiver.class);
intent.setAction(ACTION_USB_PERMISSION);
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // API 31+
flags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(
context, device.getDeviceId(), intent, flags);
}
}