1
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package com.example.observer
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Build
|
||||
import android.hardware.camera2.CameraCaptureSession
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraDevice
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CaptureRequest
|
||||
import android.graphics.ImageFormat
|
||||
import android.media.Image
|
||||
import android.media.ImageReader
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.Surface
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.view.TextureRegistry
|
||||
|
||||
/**
|
||||
* 自写原生相机:Android 框架 camera2 API 完全自研,零相机三方依赖(含 CameraX)。
|
||||
*
|
||||
* 核心:分析帧在原生侧旋转成竖屏方向再回传,Flutter 侧恒用 rotation=0,
|
||||
* 与 iOS(camera_avfoundation,帧本来就是竖屏)行为对齐,彻底消除
|
||||
* "横屏传感器帧 → 90° 旋转 + FIT_COVER crop 映射"的标注偏移根因。
|
||||
*
|
||||
* 旋转角来自设备标准值 sensorOrientation - displayRotation(camera2 特性,
|
||||
* 非厂商 hack),因此映射跨厂商一致:任何设备上"分析帧 = 预览帧同 sensor
|
||||
* 同旋转",两者几何必然一致。
|
||||
*
|
||||
* 预览:SurfaceTexture 注册进 FlutterTextureRegistry,Flutter 侧 Texture widget
|
||||
* 渲染(与插件 CameraPreview 相同的合成方式,overlay/诊断行/按钮可叠加;
|
||||
* AndroidView+SurfaceView 会盖住 Flutter UI,不可用)。
|
||||
*
|
||||
* 通道:
|
||||
* - MethodChannel "observer/camera":start/stop/getZoomRange/setZoom/isStreaming/errorDescription
|
||||
* - EventChannel "observer/camera/frames":每帧 [width, height, rotationDegrees, bgra, bytes]
|
||||
* - rotationDegrees 恒 0(帧已竖屏);bgra=false(RGBA 字节序)
|
||||
*/
|
||||
class CameraChannel(
|
||||
private val activity: FlutterActivity,
|
||||
private val engine: FlutterEngine,
|
||||
) {
|
||||
private val messenger = engine.dartExecutor.binaryMessenger
|
||||
private val method = MethodChannel(messenger, "observer/camera")
|
||||
private val frames = EventChannel(messenger, "observer/camera/frames")
|
||||
|
||||
private val cameraManager: CameraManager = activity.getSystemService(CameraManager::class.java)
|
||||
|
||||
private var cameraDevice: CameraDevice? = null
|
||||
private var captureSession: CameraCaptureSession? = null
|
||||
private var imageReader: ImageReader? = null
|
||||
private var surfaceEntry: TextureRegistry.SurfaceTextureEntry? = null
|
||||
private var previewSurface: Surface? = null
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
|
||||
private var sensorOrientation = 90
|
||||
private var activeArray = Rect(0, 0, 1920, 1080)
|
||||
private var maxZoom = 1.0f
|
||||
|
||||
/// 预览/分析共用分辨率(传感器方向,从设备流配置动态选择)
|
||||
private var previewSize = Size(1920, 1080)
|
||||
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
@Volatile
|
||||
private var errorDescription: String? = null
|
||||
|
||||
private val mainHandler = Handler(activity.mainLooper)
|
||||
private val analysisThread = HandlerThread("observer-analysis").also { it.start() }
|
||||
private val analysisHandler = Handler(analysisThread.looper)
|
||||
|
||||
@Volatile
|
||||
private var lastEmitMs = 0L
|
||||
|
||||
// 诊断计数(frameListener 线程写,stats 轮询读):
|
||||
// 帧回调到达次数 / 成功发出 / 发出异常 / 无订阅者丢弃
|
||||
@Volatile
|
||||
private var frameCallbacks = 0L
|
||||
|
||||
@Volatile
|
||||
private var emitOk = 0L
|
||||
|
||||
@Volatile
|
||||
private var emitErr = 0L
|
||||
|
||||
@Volatile
|
||||
private var sinkNullCount = 0L
|
||||
|
||||
// 实际发出帧尺寸(缩小后)
|
||||
@Volatile
|
||||
private var emitW = 0
|
||||
|
||||
@Volatile
|
||||
private var emitH = 0
|
||||
|
||||
fun register() {
|
||||
method.setMethodCallHandler(::onMethodCall)
|
||||
frames.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"start" -> start(result)
|
||||
"stop" -> {
|
||||
stop()
|
||||
result.success(true)
|
||||
}
|
||||
"getZoomRange" -> result.success(listOf(1.0, maxZoom.toDouble()))
|
||||
"setZoom" -> {
|
||||
val v = (call.arguments as Number).toFloat()
|
||||
setZoom(v)
|
||||
result.success(true)
|
||||
}
|
||||
"isStreaming" -> result.success(running)
|
||||
"errorDescription" -> result.success(errorDescription)
|
||||
"stats" -> result.success(
|
||||
mapOf(
|
||||
"callbacks" to frameCallbacks,
|
||||
"emitOk" to emitOk,
|
||||
"emitErr" to emitErr,
|
||||
"sinkNull" to sinkNullCount,
|
||||
"sink" to (eventSink != null),
|
||||
"running" to running,
|
||||
"rotation" to rotation,
|
||||
"quarterTurns" to displayDegrees / 90,
|
||||
"displayDegrees" to displayDegrees,
|
||||
"size" to "${previewSize.width}x${previewSize.height}",
|
||||
"emitSize" to "${emitW}x$emitH",
|
||||
"emitAgeMs" to
|
||||
if (lastEmitMs == 0L) -1L
|
||||
else SystemClock.elapsedRealtime() - lastEmitMs,
|
||||
"error" to errorDescription,
|
||||
),
|
||||
)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun start(result: MethodChannel.Result) {
|
||||
if (running) {
|
||||
result.success(true)
|
||||
return
|
||||
}
|
||||
running = true
|
||||
errorDescription = null
|
||||
try {
|
||||
val cameraId = pickBackCamera() ?: throw IllegalStateException("未找到后置摄像头")
|
||||
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
||||
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 90
|
||||
activeArray = characteristics.get(
|
||||
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE,
|
||||
) ?: Rect(0, 0, 1920, 1080)
|
||||
maxZoom = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1.0f
|
||||
// 分辨率不写死:从设备流配置查询(传感器方向尺寸,宽≥高),
|
||||
// 预览与分析共用同一尺寸,保证两者几何一致(对齐根因)。
|
||||
// 查询是优化而非必需:个别设备 getOutputSizes 会返回 null 或抛异常
|
||||
// (实测某设备内部抛 getClass NPE),失败一律回退默认分辨率,
|
||||
// 绝不让相机启动失败
|
||||
try {
|
||||
val streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
||||
if (streamMap != null) {
|
||||
val previewSizes =
|
||||
streamMap.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
|
||||
val analysisSizes =
|
||||
streamMap.getOutputSizes(ImageFormat.YUV_420_888) ?: emptyArray()
|
||||
previewSize = pickSize(previewSizes, analysisSizes)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "size query failed, fallback ${previewSize.width}x${previewSize.height}", e)
|
||||
}
|
||||
|
||||
val entry = surfaceEntry ?: engine.getRenderer().createSurfaceTexture().also {
|
||||
surfaceEntry = it
|
||||
}
|
||||
// 关键:SurfaceTexture 未设默认 buffer 尺寸时 createCaptureSession 会配置失败
|
||||
// (camera2 按该尺寸做 stream 校验)。显式指定与 ImageReader 一致的分辨率,
|
||||
// 保证预览与分析帧同分辨率同裁剪
|
||||
entry.surfaceTexture().setDefaultBufferSize(previewSize.width, previewSize.height)
|
||||
previewSurface?.release()
|
||||
previewSurface = Surface(entry.surfaceTexture())
|
||||
imageReader?.close()
|
||||
// YUV_420_888 是 camera2 对所有设备保证支持的 ImageReader 输出格式;
|
||||
// RGBA_8888 个别设备不支持(实测:查询 NPE + 配置失败双连败),弃用
|
||||
imageReader = ImageReader.newInstance(
|
||||
previewSize.width, previewSize.height, ImageFormat.YUV_420_888, 2,
|
||||
).also { it.setOnImageAvailableListener(frameListener, analysisHandler) }
|
||||
|
||||
cameraManager.openCamera(
|
||||
cameraId,
|
||||
object : CameraDevice.StateCallback() {
|
||||
override fun onOpened(device: CameraDevice) {
|
||||
cameraDevice = device
|
||||
createSession(result)
|
||||
}
|
||||
|
||||
override fun onDisconnected(device: CameraDevice) {
|
||||
device.close()
|
||||
cameraDevice = null
|
||||
}
|
||||
|
||||
override fun onError(device: CameraDevice, error: Int) {
|
||||
device.close()
|
||||
cameraDevice = null
|
||||
running = false
|
||||
errorDescription = "camera open error $error"
|
||||
result.error("start", errorDescription, null)
|
||||
}
|
||||
},
|
||||
mainHandler,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
running = false
|
||||
errorDescription = e.toString()
|
||||
// details 带完整堆栈:任何残留异常都能在诊断行看到精确位置
|
||||
result.error("start", e.toString(), Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSession(result: MethodChannel.Result) {
|
||||
val device = cameraDevice ?: return
|
||||
val surfaces = listOfNotNull(previewSurface, imageReader?.surface)
|
||||
device.createCaptureSession(
|
||||
surfaces,
|
||||
object : CameraCaptureSession.StateCallback() {
|
||||
override fun onConfigured(session: CameraCaptureSession) {
|
||||
captureSession = session
|
||||
try {
|
||||
val request = buildRequest(session.device, surfaces)
|
||||
session.setRepeatingRequest(request, null, mainHandler)
|
||||
result.success(
|
||||
mapOf(
|
||||
"textureId" to (surfaceEntry?.id() ?: -1L),
|
||||
// 传感器方向尺寸(宽≥高):Flutter 侧 SizedBox 在 RotatedBox
|
||||
// 内部声明纹理尺寸,旋转后视觉上才是竖屏 1080x1920
|
||||
"w" to previewSize.width,
|
||||
"h" to previewSize.height,
|
||||
// 预览旋转 = 显示旋转(不是 rotation/90!):
|
||||
// Flutter 引擎渲染 Texture 时已自动应用 SurfaceTexture
|
||||
// 变换矩阵(传感器方向补偿),预览只需再按显示旋转补偿
|
||||
"quarterTurns" to displayDegrees / 90,
|
||||
// 诊断:旋转计算输入值
|
||||
"sensorOrientation" to sensorOrientation,
|
||||
"displayDegrees" to displayDegrees,
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
errorDescription = "onConfigured: $e"
|
||||
result.error("start", errorDescription, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigureFailed(session: CameraCaptureSession) {
|
||||
running = false
|
||||
errorDescription = "capture session configure failed"
|
||||
result.error("start", errorDescription, null)
|
||||
}
|
||||
},
|
||||
mainHandler,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildRequest(device: CameraDevice, targets: List<Surface>): CaptureRequest {
|
||||
val builder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
||||
targets.forEach { builder.addTarget(it) }
|
||||
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
||||
// 连续对焦:无 AF 能力的设备忽略该设置
|
||||
try {
|
||||
builder.set(
|
||||
CaptureRequest.CONTROL_AF_MODE,
|
||||
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
|
||||
)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun setZoom(z: Float) {
|
||||
val session = captureSession ?: return
|
||||
val clamped = z.coerceIn(1.0f, maxZoom)
|
||||
val r = activeArray
|
||||
val insetW = r.width() * (1 - 1 / clamped) / 2
|
||||
val insetH = r.height() * (1 - 1 / clamped) / 2
|
||||
val crop = RectF(
|
||||
r.left + insetW,
|
||||
r.top + insetH,
|
||||
r.right - insetW,
|
||||
r.bottom - insetH,
|
||||
)
|
||||
try {
|
||||
val surface = previewSurface ?: return
|
||||
val builder = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
||||
builder.addTarget(surface)
|
||||
imageReader?.surface?.let { builder.addTarget(it) }
|
||||
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
||||
builder.set(
|
||||
CaptureRequest.SCALER_CROP_REGION,
|
||||
Rect(
|
||||
crop.left.toInt(),
|
||||
crop.top.toInt(),
|
||||
crop.right.toInt(),
|
||||
crop.bottom.toInt(),
|
||||
),
|
||||
)
|
||||
session.setRepeatingRequest(builder.build(), null, mainHandler)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "setZoom failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
running = false
|
||||
try {
|
||||
captureSession?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
captureSession = null
|
||||
try {
|
||||
cameraDevice?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
cameraDevice = null
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
stop()
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
previewSurface?.release()
|
||||
previewSurface = null
|
||||
surfaceEntry?.release()
|
||||
surfaceEntry = null
|
||||
analysisThread.quitSafely()
|
||||
}
|
||||
|
||||
private fun pickBackCamera(): String? {
|
||||
val ids = cameraManager.cameraIdList
|
||||
for (id in ids) {
|
||||
val c = cameraManager.getCameraCharacteristics(id)
|
||||
val facing = c.get(CameraCharacteristics.LENS_FACING)
|
||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) return id
|
||||
}
|
||||
return ids.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从设备流配置挑预览/分析共用分辨率(均为传感器方向尺寸,宽≥高):
|
||||
* 取两者交集,16:9 优先、长边 ≤1920 内取最大(推理输入 640x640,
|
||||
* 更高只增帧传输与旋转开销,无精度收益);无 16:9 时退回最大交集尺寸。
|
||||
*/
|
||||
private fun pickSize(previewSizes: Array<Size>, analysisSizes: Array<Size>): Size {
|
||||
val common = previewSizes.filter { analysisSizes.contains(it) }
|
||||
val fallback = common.maxByOrNull { it.width.toLong() * it.height }
|
||||
?: return Size(1920, 1080)
|
||||
return common
|
||||
.filter {
|
||||
Math.abs(it.width.toDouble() / it.height - 16.0 / 9.0) < 0.03 &&
|
||||
it.width <= 1920 && it.height <= 1920
|
||||
}
|
||||
.maxByOrNull { it.width.toLong() * it.height }
|
||||
?: fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示旋转角(度)。Display.ROTATION_* 在 API 36 是受限常量,直接用其值(0/1/2/3)。
|
||||
* API 30+ 用 activity.display(现代 API,实测可靠跟踪旋转);
|
||||
* 旧版 windowManager.defaultDisplay.rotation 已废弃,实测在部分设备恒返回 0。
|
||||
*/
|
||||
private val displayDegrees: Int
|
||||
get() = try {
|
||||
val rot: Int = if (Build.VERSION.SDK_INT >= 30) {
|
||||
activity.display?.rotation ?: 0
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity.windowManager.defaultDisplay.rotation
|
||||
}
|
||||
when (rot) {
|
||||
1 -> 90
|
||||
2 -> 180
|
||||
3 -> 270
|
||||
else -> 0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "display rotation query failed", e)
|
||||
0
|
||||
}
|
||||
|
||||
/** 传感器 → 竖屏显示的顺时针旋转角(后摄无镜像) */
|
||||
private val rotation: Int
|
||||
get() = ((sensorOrientation - displayDegrees) % 360 + 360) % 360
|
||||
|
||||
private val frameListener = ImageReader.OnImageAvailableListener { reader ->
|
||||
// 诊断计数:回调是否到达(放在一切判定之前,任何丢弃都先计数)
|
||||
frameCallbacks++
|
||||
if (frameCallbacks % 50 == 0L) {
|
||||
Log.i(TAG, "frames cb=$frameCallbacks ok=$emitOk err=$emitErr sinkNull=$sinkNullCount")
|
||||
}
|
||||
val image = reader.acquireLatestImage() ?: return@OnImageAvailableListener
|
||||
try {
|
||||
val sink = eventSink
|
||||
if (sink == null) {
|
||||
// 诊断:有帧但无订阅者(Dart 侧订阅未建立/被取消)
|
||||
sinkNullCount++
|
||||
errorDescription = "帧监听无订阅者"
|
||||
return@OnImageAvailableListener
|
||||
}
|
||||
if (!running) return@OnImageAvailableListener
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
// 节流:推理 ~100ms 一帧,避免数 MB 帧无谓传输
|
||||
if (now - lastEmitMs < 100) return@OnImageAvailableListener
|
||||
lastEmitMs = now
|
||||
val bytes = yuvToRgba(image, rotation)
|
||||
// 缩小后再发:8.3MB/帧对主线程编码与通道传输都过重,540x960 足够
|
||||
// (推理输入 704,坐标归一化,映射不受分辨率影响)
|
||||
val scaled = downscaleToFit(
|
||||
bytes[0] as Int, bytes[1] as Int, bytes[2] as ByteArray, 960,
|
||||
)
|
||||
emitW = scaled[0] as Int
|
||||
emitH = scaled[1] as Int
|
||||
if (emitOk == 0L) Log.i(TAG, "first emit ${emitW}x${emitH}")
|
||||
// 关键:EventSink.success 内部 FlutterJNI.dispatchPlatformMessage 强制主线程
|
||||
// (ensureRunningOnMainThread 抛 RuntimeException),后台线程直接调必失败——
|
||||
// 曾因此每帧抛"必须主线程"异常、Dart 侧永远 流:0。必须 post 到主线程发送
|
||||
mainHandler.post {
|
||||
try {
|
||||
sink.success(listOf(scaled[0], scaled[1], 0, false, scaled[2]))
|
||||
emitOk++
|
||||
} catch (e: Exception) {
|
||||
emitErr++
|
||||
Log.e(TAG, "emit frame failed", e)
|
||||
errorDescription = "帧异常: ${e.javaClass.simpleName}: ${e.message}"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emitErr++
|
||||
Log.e(TAG, "emit frame failed", e)
|
||||
errorDescription = "帧异常: ${e.javaClass.simpleName}: ${e.message}"
|
||||
} finally {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* YUV_420_888 三平面 → RGBA 单平面,同时顺时针旋转成竖屏。
|
||||
* 返回 [width, height, bytes]。整数 BT.601 转换;U/V 兼容 planar
|
||||
* (pixelStride=1)与 semi-planar(pixelStride=2,NV21 式交错)布局。
|
||||
* dst(x', y') = src(y, W-1-x')(90° 顺时针),逐像素处理 rowStride 填充。
|
||||
*/
|
||||
private fun yuvToRgba(image: Image, deg: Int): List<Any> {
|
||||
val yPlane = image.planes[0]
|
||||
val uPlane = image.planes[1]
|
||||
val vPlane = image.planes[2]
|
||||
val srcW = image.width
|
||||
val srcH = image.height
|
||||
val yStride = yPlane.rowStride
|
||||
val uStride = uPlane.rowStride
|
||||
val vStride = vPlane.rowStride
|
||||
val uPixel = uPlane.pixelStride
|
||||
val vPixel = vPlane.pixelStride
|
||||
val yBuf = yPlane.buffer
|
||||
val uBuf = uPlane.buffer
|
||||
val vBuf = vPlane.buffer
|
||||
val dstW = if (deg == 90 || deg == 270) srcH else srcW
|
||||
val dstH = if (deg == 90 || deg == 270) srcW else srcH
|
||||
val dst = ByteArray(dstW * dstH * 4)
|
||||
for (dy in 0 until dstH) {
|
||||
val rowBase = dy * dstW * 4
|
||||
for (dx in 0 until dstW) {
|
||||
val sx: Int
|
||||
val sy: Int
|
||||
when (deg) {
|
||||
90 -> {
|
||||
sx = dy
|
||||
sy = srcH - 1 - dx
|
||||
}
|
||||
180 -> {
|
||||
sx = srcW - 1 - dx
|
||||
sy = srcH - 1 - dy
|
||||
}
|
||||
270 -> {
|
||||
sx = srcW - 1 - dy
|
||||
sy = dx
|
||||
}
|
||||
else -> {
|
||||
sx = dx
|
||||
sy = dy
|
||||
}
|
||||
}
|
||||
val yy = (yBuf.get(sy * yStride + sx).toInt() and 0xFF) - 16
|
||||
val uu = (uBuf.get((sy / 2) * uStride + (sx / 2) * uPixel).toInt() and 0xFF) - 128
|
||||
val vv = (vBuf.get((sy / 2) * vStride + (sx / 2) * vPixel).toInt() and 0xFF) - 128
|
||||
val r = ((298 * yy + 409 * vv + 128) shr 8).coerceIn(0, 255)
|
||||
val g = ((298 * yy - 100 * uu - 208 * vv + 128) shr 8).coerceIn(0, 255)
|
||||
val b = ((298 * yy + 516 * uu + 128) shr 8).coerceIn(0, 255)
|
||||
val di = rowBase + dx * 4
|
||||
dst[di] = r.toByte()
|
||||
dst[di + 1] = g.toByte()
|
||||
dst[di + 2] = b.toByte()
|
||||
dst[di + 3] = 0xFF.toByte()
|
||||
}
|
||||
}
|
||||
return listOf(dstW, dstH, dst)
|
||||
}
|
||||
|
||||
/**
|
||||
* RGBA 帧整数倍缩小,长边 ≤ maxLong 时原样返回(f=1)。
|
||||
* 2x2 盒式平均(对 640x640 推理输入足够;比最近邻平滑,颜色更准)。
|
||||
*/
|
||||
private fun downscaleToFit(w: Int, h: Int, rgba: ByteArray, maxLong: Int): List<Any> {
|
||||
val long = maxOf(w, h)
|
||||
if (long <= maxLong) return listOf(w, h, rgba)
|
||||
val f = (long + maxLong - 1) / maxLong
|
||||
val dw = w / f
|
||||
val dh = h / f
|
||||
val out = ByteArray(dw * dh * 4)
|
||||
for (dy in 0 until dh) {
|
||||
val y0 = dy * f
|
||||
val y1 = minOf(y0 + f, h)
|
||||
val rows = y1 - y0
|
||||
for (dx in 0 until dw) {
|
||||
val x0 = dx * f
|
||||
val x1 = minOf(x0 + f, w)
|
||||
var r = 0L
|
||||
var g = 0L
|
||||
var b = 0L
|
||||
var a = 0L
|
||||
for (sy in y0 until y1) {
|
||||
var si = sy * w * 4 + x0 * 4
|
||||
for (sx in x0 until x1) {
|
||||
r += rgba[si].toInt() and 0xFF
|
||||
g += rgba[si + 1].toInt() and 0xFF
|
||||
b += rgba[si + 2].toInt() and 0xFF
|
||||
a += rgba[si + 3].toInt() and 0xFF
|
||||
si += 4
|
||||
}
|
||||
}
|
||||
val n = rows * (x1 - x0)
|
||||
val di = (dy * dw + dx) * 4
|
||||
out[di] = (r / n).toByte()
|
||||
out[di + 1] = (g / n).toByte()
|
||||
out[di + 2] = (b / n).toByte()
|
||||
out[di + 3] = (a / n).toByte()
|
||||
}
|
||||
}
|
||||
return listOf(dw, dh, out)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraChannel"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
package com.example.observer
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var cameraChannel: CameraChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
// 自写原生相机(替代 camera_android_camerax 插件):
|
||||
// 分析帧在原生侧旋转成竖屏后回传,Flutter 侧恒 rotation=0(对齐 iOS)。
|
||||
// configureFlutterEngine(onCreate 阶段)只注册通道与 viewFactory,
|
||||
// 实际 bindToLifecycle 由 Flutter 相机页 start 时触发(此时已 RESUMED)
|
||||
cameraChannel = CameraChannel(this, flutterEngine).also { it.register() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cameraChannel?.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user