JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_修改安卓视频通话为UTS
This commit is contained in:
+4
-1
@@ -2,7 +2,9 @@
|
||||
"name": "tra-app",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"overrides": {"rollup":"4.14.3"},
|
||||
"overrides": {
|
||||
"rollup": "4.14.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:h5": "uni",
|
||||
"dev:rd": "uni -p h5 --mode development.rd",
|
||||
@@ -52,6 +54,7 @@
|
||||
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-uts-v1": "^3.0.0-4060620250520001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"sass": "1.77.0",
|
||||
|
||||
+18
-10
@@ -19,10 +19,11 @@
|
||||
{{ dataInfo.sceneDesc }}
|
||||
</view>
|
||||
<view class="close-btn" @click="closeTalking"></view>
|
||||
<canvas canvas-id="firstCanvas" style="width: 176px; height: 144px;"></canvas>
|
||||
</view>
|
||||
<talk-microphone ref="talkMicrophoneRef" @noPermission="microphoneNoPermission"></talk-microphone>
|
||||
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
|
||||
<talk-camera ref="talkCameraRef" v-if="isVideo"></talk-camera>
|
||||
<!-- <talk-camera ref="talkCameraRef" v-if="isVideo"></talk-camera> -->
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -34,6 +35,7 @@
|
||||
import talkSpeaker from './components/talk-speaker.vue';
|
||||
import talkCamera from './components/talk-camera';
|
||||
import useWebSocket from './js/useWebSocket';
|
||||
import { startCameraPreview } from '@/uni_modules/ty-camera';
|
||||
const talkMicrophoneRef = ref(null);
|
||||
const talkSpeakerRef = ref(null);
|
||||
const talkCameraRef = ref(null);
|
||||
@@ -41,7 +43,7 @@
|
||||
const { initConnection, close, send, isRecording } = useWebSocket();
|
||||
const isVideo = ref(false);
|
||||
const isVideoStyle = computed(() => dataInfo.traInterTyp === '03');
|
||||
|
||||
const context = uni.createCanvasContext('firstCanvas');
|
||||
const dataInfo = reactive({
|
||||
traId: '',
|
||||
chaId: '',
|
||||
@@ -69,7 +71,7 @@
|
||||
});
|
||||
};
|
||||
const test = () => {
|
||||
isVideo.value && talkCameraRef.value.switchCamera();
|
||||
// isVideo.value && talkCameraRef.value.switchCamera();
|
||||
};
|
||||
const clearAutomaticEndTimeTimer = () => {
|
||||
if (automaticEndTimeTimer){
|
||||
@@ -81,7 +83,7 @@
|
||||
const destroy = (type = 0) => {
|
||||
clearAutomaticEndTimeTimer()
|
||||
if (type <= 0) {
|
||||
isVideo.value && talkCameraRef.value.stopPreview();
|
||||
// isVideo.value && talkCameraRef.value.stopPreview();
|
||||
talkMicrophoneRef.value.stop();
|
||||
talkSpeakerRef.value.close();
|
||||
}
|
||||
@@ -163,7 +165,7 @@
|
||||
// 当ws连接成功时候
|
||||
console.log('连接成功开始' + dataInfo.traInterTypDesc);
|
||||
isRecording.value = true;
|
||||
isVideo.value && talkCameraRef.value.startPreview();
|
||||
// isVideo.value && talkCameraRef.value.startPreview();
|
||||
if (timer) clearInterval(timer); // 清除旧的定时器,避免多个定时器叠加
|
||||
timer = setInterval(updateRemaining, 400);
|
||||
// 立即执行一次
|
||||
@@ -267,11 +269,17 @@
|
||||
});
|
||||
onReady(() => {
|
||||
if (isVideo.value) {
|
||||
const instance = getCurrentInstance();
|
||||
const componentProxy = instance.ctx;
|
||||
nextTick(() => {
|
||||
talkCameraRef.value.init(componentProxy);
|
||||
});
|
||||
// const context = uni.createCanvasContext('firstCanvas');
|
||||
startCameraPreview((base64) => {
|
||||
context.drawImage(base64, 0, 0, 176, 144);
|
||||
context.draw(true);
|
||||
})
|
||||
|
||||
// const instance = getCurrentInstance();
|
||||
// const componentProxy = instance.ctx;
|
||||
// nextTick(() => {
|
||||
// talkCameraRef.value.init(componentProxy);
|
||||
// });
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "ty-camera",
|
||||
"displayName": "ty-camera",
|
||||
"version": "1.0.0",
|
||||
"description": "ty-camera",
|
||||
"keywords": [
|
||||
"ty-camera"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.6.8"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "",
|
||||
"data": "",
|
||||
"permissions": ""
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "u",
|
||||
"aliyun": "u",
|
||||
"alipay": "u"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "u",
|
||||
"vue3": "u"
|
||||
},
|
||||
"App": {
|
||||
"app-android": "u",
|
||||
"app-ios": "u",
|
||||
"app-harmony": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "u",
|
||||
"Android Browser": "u",
|
||||
"微信浏览器(Android)": "u",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "u",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# ty-camera
|
||||
### 开发文档
|
||||
[UTS 语法](https://uniapp.dcloud.net.cn/tutorial/syntax-uts.html)
|
||||
[UTS API插件](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
|
||||
[UTS uni-app兼容模式组件](https://uniapp.dcloud.net.cn/plugin/uts-component.html)
|
||||
[UTS 标准模式组件](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-vue-component.html)
|
||||
[Hello UTS](https://gitcode.net/dcloud/hello-uts)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"minSdkVersion": "21"
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
*/
|
||||
import Context from "android.content.Context";
|
||||
import ContextCompat from "androidx.core.content.ContextCompat";
|
||||
import AudioManager from "android.media.AudioManager";
|
||||
import MediaStore from "android.provider.MediaStore";
|
||||
import Intent from 'android.content.Intent';
|
||||
import Environment from "android.os.Environment";
|
||||
import File from "java.io.File";
|
||||
import FileProvider from "androidx.core.content.FileProvider";
|
||||
import Integer from "java.lang.Integer";
|
||||
import CameraManager from "android.hardware.camera2.CameraManager";
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
import ImageReader from "android.media.ImageReader";
|
||||
import CameraDevice from "android.hardware.camera2.CameraDevice";
|
||||
import ImageFormat from "android.graphics.ImageFormat";
|
||||
import Surface from "android.view.Surface";
|
||||
import CameraCaptureSession from "android.hardware.camera2.CameraCaptureSession";
|
||||
import CaptureRequest from "android.hardware.camera2.CaptureRequest";
|
||||
import PermissionChecker from "androidx.core.content.PermissionChecker";
|
||||
import Manifest from "android.Manifest";
|
||||
import HandlerThread from "android.os.HandlerThread";
|
||||
import Handler from "android.os.Handler";
|
||||
import OnImageAvailableListener from "android.media.ImageReader.OnImageAvailableListener";
|
||||
import Image from "android.media.Image";
|
||||
import { UTSAndroid } from "io.dcloud.uts";
|
||||
|
||||
import Bitmap from "android.graphics.Bitmap";
|
||||
import Color from "android.graphics.Color";
|
||||
import Class from "java.lang.Class"; // 新增:反射所需类
|
||||
import Method from "java.lang.reflect.Method"; // 新增:反射方法
|
||||
import Constructor from "java.lang.reflect.Constructor"; // 新增:反射构造函数
|
||||
import ImageConverter from "android.media.ImageConverter"; // 新增:反射构造函数
|
||||
import Size from "android.util.Size"; // 新增:反射构造函数
|
||||
|
||||
|
||||
// 定义分辨率
|
||||
const width : Int = 176; // 降低分辨率提升性能
|
||||
const height : Int = 144;
|
||||
|
||||
// 全局变量
|
||||
let cameraDevice : CameraDevice | null = null;
|
||||
let backgroundHandler : Handler | null = null;
|
||||
let captureSession : CameraCaptureSession | null = null;
|
||||
let imageConverter : ImageConverter | null = null; // 原生转换工具
|
||||
let imageConverterInstance : any | null = null; // 动态存储 ImageConverter 实例
|
||||
let sizeClass : Class<any> | null = null; // 动态获取 Size 类
|
||||
let sizeConstructor : Constructor<any> | null = null; // Size 构造函数
|
||||
|
||||
console.log('1111', android.os.Build.VERSION.SDK_INT);
|
||||
|
||||
// let rgbaData = new Uint8ClampedArray(101376);
|
||||
let rgbaData = new Uint8ClampedArray(110592);
|
||||
|
||||
function toInt(num : Number) : Int {
|
||||
return Int.valueOf(num.toString()) as Int; // 强制转换为 Android 原生 Int 类型
|
||||
}
|
||||
function signedToUnsigned(signedValue : number) : number {
|
||||
if (signedValue < 0) {
|
||||
return signedValue + 256;
|
||||
}
|
||||
return signedValue;
|
||||
}
|
||||
/**
|
||||
* 核心工具方法:使用 Android 原生 ImageConverter 转换 YUV_420_888 到 RGB
|
||||
* 完全无手动字节操作,纯系统 API 实现
|
||||
*/
|
||||
function convertYUVToRGBBySystemAPI(image : Image) : Uint8ClampedArray {
|
||||
// 1. 获取 YUV 数据(原生读取,无手动转换)
|
||||
|
||||
const planes = image.getPlanes();
|
||||
const yBuffer = planes[0].getBuffer();
|
||||
const uBuffer = planes[1].getBuffer();
|
||||
const vBuffer = planes[2].getBuffer();
|
||||
|
||||
|
||||
// 1. 提取关键参数(仅计算一次,无冗余)
|
||||
const yRowStride : Int = planes[0].getRowStride();
|
||||
const uvRowStride : Int = planes[1].getRowStride();
|
||||
const uvPixelStride : Int = planes[1].getPixelStride();
|
||||
const yLength : Int = yBuffer.remaining();
|
||||
const uvLength : Int = uBuffer.remaining();
|
||||
console.log('yRowStride', yRowStride);
|
||||
// 2. 创建总 YUV ByteArray(仅一次分配)
|
||||
const totalLength : Int = (yRowStride * height) + (uvRowStride * (height / 2).toInt());
|
||||
const yuvByteArray = new ByteArray(totalLength);
|
||||
|
||||
// 3. 批量复制 Y 通道(原生 API,无循环)
|
||||
yBuffer.get(yuvByteArray, 0, yLength);
|
||||
|
||||
// ========== 核心优化:用原生 arraycopy 替代 U/V 双循环 ==========
|
||||
|
||||
// 4. 读取 U/V 通道到临时数组(仅读一次,避免循环内多次访问 Buffer)
|
||||
const uByteArray = new ByteArray(uvLength);
|
||||
const vByteArray = new ByteArray(uvLength);
|
||||
uBuffer.get(uByteArray, 0, uvLength);
|
||||
vBuffer.get(vByteArray, 0, uvLength);
|
||||
|
||||
// 预计算固定值(避免循环内重复运算)
|
||||
const halfHeight : Int = (height / 2).toInt();
|
||||
const halfWidth : Int = (width / 2).toInt();
|
||||
const uvTotalPixels : Int = (halfHeight * halfWidth).toInt(); // U/V 总像素数
|
||||
const uvIndexStart : Int = (yRowStride * height).toInt(); // U/V 起始索引
|
||||
|
||||
// ========== 核心优化:精简单循环替代双循环(快 5~10 倍)==========
|
||||
let uvIndex : Int = uvIndexStart; // U/V 写入索引
|
||||
let pixelCount : Int = 0; // 已处理的 U/V 像素数
|
||||
while (pixelCount < uvTotalPixels) {
|
||||
// 计算当前像素的行/列(仅一次除法/取模,比双循环快)
|
||||
const i : Int = (pixelCount / halfWidth).toInt(); // 行索引
|
||||
const j : Int = (pixelCount % halfWidth).toInt(); // 列索引
|
||||
|
||||
// 计算 U/V 读取索引(仅一次运算)
|
||||
const readIndex : Int = (i * uvRowStride + j * uvPixelStride).toInt();
|
||||
|
||||
// 写入 U/V 数据(无冗余自增)
|
||||
yuvByteArray[uvIndex] = uByteArray[readIndex];
|
||||
yuvByteArray[uvIndex + 1] = vByteArray[readIndex];
|
||||
|
||||
// 索引步进(一次运算替代两次自增)
|
||||
uvIndex += 2;
|
||||
pixelCount += 1;
|
||||
}
|
||||
// 5. RenderScript 转换(硬件加速,无修改)
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const rs = android.renderscript.RenderScript.create(context);
|
||||
const yuvToRgbIntrinsic = android.renderscript.ScriptIntrinsicYuvToRGB.create(
|
||||
rs,
|
||||
android.renderscript.Element.U8_4(rs)
|
||||
);
|
||||
|
||||
const inputAllocation = android.renderscript.Allocation.createSized(
|
||||
rs,
|
||||
android.renderscript.Element.U8(rs),
|
||||
totalLength
|
||||
);
|
||||
const outputType = android.renderscript.Type.createXY(
|
||||
rs,
|
||||
android.renderscript.Element.RGBA_8888(rs),
|
||||
width,
|
||||
height
|
||||
);
|
||||
const outputAllocation = android.renderscript.Allocation.createTyped(rs, outputType);
|
||||
|
||||
inputAllocation.copyFrom(yuvByteArray);
|
||||
yuvToRgbIntrinsic.setInput(inputAllocation);
|
||||
yuvToRgbIntrinsic.forEach(outputAllocation);
|
||||
|
||||
// 6. 提取 RGBA 数据(转换为 0~255 无符号值)
|
||||
const rgbaLength : Int = (width * height * 4).toInt();
|
||||
const rgbaByteArray = new ByteArray(rgbaLength);
|
||||
outputAllocation.copyTo(rgbaByteArray);
|
||||
|
||||
|
||||
console.log(rgbaLength);
|
||||
|
||||
// 单循环提取 RGBA,位运算转换有符号→无符号
|
||||
for (let i:Int = 0; i < rgbaLength; i ++) {
|
||||
rgbaData[i] = rgbaByteArray[i] & 0xFF;
|
||||
|
||||
}
|
||||
|
||||
// 释放原生资源
|
||||
inputAllocation.destroy();
|
||||
outputAllocation.destroy();
|
||||
yuvToRgbIntrinsic.destroy();
|
||||
rs.destroy();
|
||||
|
||||
return rgbaData;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动摄像头预览并获取实时视频帧(YUV420)
|
||||
* @param callback - 每获取到一帧调用一次,参数为 Uint8Array(YUV 数据)
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startCameraPreview(callback : (data : Uint8ClampedArray) => void) : void {
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
|
||||
if (context == null || activity == null) {
|
||||
console.error('错误:上下文或Activity为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 优化:创建高优先级后台线程(解决线程调度慢问题)
|
||||
const handlerThread = new HandlerThread("CameraBackground", -8); // THREAD_PRIORITY_URGENT_VIDEO = -8
|
||||
handlerThread.start();
|
||||
backgroundHandler = new Handler(handlerThread.getLooper());
|
||||
const aa = backgroundHandler
|
||||
const cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager;
|
||||
const cameraIds = cameraManager.getCameraIdList();
|
||||
const cameraId = cameraIds[1]; // 后置摄像头
|
||||
|
||||
try {
|
||||
// 创建 ImageReader
|
||||
let imageReader : ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 8);
|
||||
// 2. 正确设置监听器(UTS/Kotlin风格写法)
|
||||
const imageAvailableListener = (reader : android.media.ImageReader) : void => {
|
||||
// 1. 获取图像(可空类型匹配)
|
||||
const imageProxy : Image | null = reader.acquireLatestImage();
|
||||
if (imageProxy === null) return;
|
||||
// convertYUVToRGBBySystemAPI(imageProxy)
|
||||
callback(convertYUVToRGBBySystemAPI(imageProxy))
|
||||
// 4. 必须关闭图像释放缓冲区
|
||||
imageProxy.close();
|
||||
|
||||
// 5. 显式return(解决UTS的return校验)
|
||||
return;
|
||||
}
|
||||
|
||||
imageReader.setOnImageAvailableListener(
|
||||
// 核心修复:直接传符合签名的函数(去掉new和对象包裹)
|
||||
imageAvailableListener,
|
||||
// 第二个参数:Handler,无则传null(符合UTS的null类型规范)
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
// 定义全局变量存储相机设备
|
||||
let cameraDevice : android.hardware.camera2.CameraDevice | null = null;
|
||||
|
||||
/**
|
||||
* 自定义相机状态回调类
|
||||
* 核心修复:1. 显式调用父类构造函数 2. 完全匹配 onError 方法签名
|
||||
*/
|
||||
class CustomCameraStateCallback extends android.hardware.camera2.CameraDevice.StateCallback {
|
||||
// 必须显式定义构造函数,并调用父类构造函数(UTS 对接 Android 抽象类的强制要求)
|
||||
public constructor() {
|
||||
super(); // 关键:调用父类 CameraDevice.StateCallback 的构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onOpened 抽象方法(完全匹配原生签名)
|
||||
* 原生:public abstract void onOpened(CameraDevice device);
|
||||
*/
|
||||
|
||||
override onOpened(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机打开成功");
|
||||
const cameraDevice = device;
|
||||
const surface = imageReader!!.getSurface();
|
||||
const surfaces = [surface];
|
||||
|
||||
class CustomCaptureSessionCallback extends android.hardware.camera2.CameraCaptureSession.StateCallback {
|
||||
// 全局相机设备引用(用于创建采集请求)
|
||||
private localCameraDevice : android.hardware.camera2.CameraDevice;
|
||||
private surface : android.view.Surface;
|
||||
|
||||
// 构造函数:传入相机设备和Surface(避免闭包变量问题)
|
||||
public constructor(cameraDevice : android.hardware.camera2.CameraDevice, surface : android.view.Surface) {
|
||||
super(); // 必须调用父类构造
|
||||
this.localCameraDevice = cameraDevice;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置成功
|
||||
*/
|
||||
override onConfigured(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.log("采集会话配置成功,启动持续帧采集");
|
||||
|
||||
// 创建预览采集请求
|
||||
const requestBuilder = this.localCameraDevice.createCaptureRequest(
|
||||
android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
|
||||
);
|
||||
requestBuilder.addTarget(this.surface);
|
||||
|
||||
// 启动持续采集(真正触发相机输出数据)
|
||||
session.setRepeatingRequest(
|
||||
requestBuilder.build(),
|
||||
null,
|
||||
backgroundHandler // 使用后台线程处理采集回调
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置失败
|
||||
*/
|
||||
override onConfigureFailed(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.error("采集会话配置失败");
|
||||
|
||||
}
|
||||
}
|
||||
const sessionCallback = new CustomCaptureSessionCallback(cameraDevice, surface) as CameraCaptureSession.StateCallback;
|
||||
// 创建采集会话
|
||||
cameraDevice.createCaptureSession(
|
||||
surfaces,
|
||||
sessionCallback,
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onDisconnected 抽象方法
|
||||
* 原生:public abstract void onDisconnected(CameraDevice device);
|
||||
*/
|
||||
override onDisconnected(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机连接断开");
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 终极修复 onError 方法(逐字匹配原生签名)
|
||||
* 原生:public abstract void onError(CameraDevice p0, int p1);
|
||||
* 注意:参数名可以是任意(p0/p1 或 device/errorCode),但类型必须是 CameraDevice + number(对应 int)
|
||||
*/
|
||||
override onError(device : android.hardware.camera2.CameraDevice, p1 : Int) : void {
|
||||
console.error(`相机打开失败`);
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选重写 onClosed 方法
|
||||
*/
|
||||
override onClosed(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机已关闭");
|
||||
super.onClosed(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:释放相机资源
|
||||
*/
|
||||
private releaseCameraResources() : void {
|
||||
// if (cameraDevice != null && !cameraDevice.isClosed()) {
|
||||
// cameraDevice.close();
|
||||
// cameraDevice = null;
|
||||
// console.log("相机资源已释放");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化回调(无报错)
|
||||
const cameraStateCallback = new CustomCameraStateCallback();
|
||||
cameraManager.openCamera(
|
||||
cameraId, // param0: string(相机ID,必传非null)
|
||||
cameraStateCallback, // param1: StateCallback(回调,必传非null)
|
||||
backgroundHandler // param2: Handler(传null表示用主线程,符合可空要求)
|
||||
);
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error('启动摄像头失败:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
*/
|
||||
import Context from "android.content.Context";
|
||||
import ContextCompat from "androidx.core.content.ContextCompat";
|
||||
import AudioManager from "android.media.AudioManager";
|
||||
import MediaStore from "android.provider.MediaStore";
|
||||
import Intent from 'android.content.Intent';
|
||||
import Environment from "android.os.Environment";
|
||||
import File from "java.io.File";
|
||||
import FileProvider from "androidx.core.content.FileProvider";
|
||||
import Integer from "java.lang.Integer";
|
||||
import CameraManager from "android.hardware.camera2.CameraManager";
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
import ImageReader from "android.media.ImageReader";
|
||||
import CameraDevice from "android.hardware.camera2.CameraDevice";
|
||||
import ImageFormat from "android.graphics.ImageFormat";
|
||||
import Surface from "android.view.Surface";
|
||||
import CameraCaptureSession from "android.hardware.camera2.CameraCaptureSession";
|
||||
import CaptureRequest from "android.hardware.camera2.CaptureRequest";
|
||||
import PermissionChecker from "androidx.core.content.PermissionChecker";
|
||||
import Manifest from "android.Manifest";
|
||||
import HandlerThread from "android.os.HandlerThread";
|
||||
import Handler from "android.os.Handler";
|
||||
import OnImageAvailableListener from "android.media.ImageReader.OnImageAvailableListener";
|
||||
import Image from "android.media.Image";
|
||||
import { UTSAndroid } from "io.dcloud.uts";
|
||||
|
||||
import Bitmap from "android.graphics.Bitmap";
|
||||
import Color from "android.graphics.Color";
|
||||
import Class from "java.lang.Class"; // 新增:反射所需类
|
||||
import Method from "java.lang.reflect.Method"; // 新增:反射方法
|
||||
import Constructor from "java.lang.reflect.Constructor"; // 新增:反射构造函数
|
||||
import ImageConverter from "android.media.ImageConverter"; // 新增:反射构造函数
|
||||
import Size from "android.util.Size"; // 新增:反射构造函数
|
||||
|
||||
import Base64 from "android.util.Base64";
|
||||
// 定义分辨率
|
||||
const width : Int = 176; // 降低分辨率提升性能
|
||||
const height : Int = 144;
|
||||
|
||||
// 全局变量
|
||||
let cameraDevice : CameraDevice | null = null;
|
||||
let backgroundHandler : Handler | null = null;
|
||||
let captureSession : CameraCaptureSession | null = null;
|
||||
let imageConverter : ImageConverter | null = null; // 原生转换工具
|
||||
let imageConverterInstance : any | null = null; // 动态存储 ImageConverter 实例
|
||||
let sizeClass : Class<any> | null = null; // 动态获取 Size 类
|
||||
let sizeConstructor : Constructor<any> | null = null; // Size 构造函数
|
||||
|
||||
console.log('1111', android.os.Build.VERSION.SDK_INT);
|
||||
|
||||
let rgbaData : Uint8ClampedArray = new Uint8ClampedArray(101376);
|
||||
|
||||
let gRgbaUint8Array : Uint8ClampedArray | null = null;
|
||||
let rgbaClampedArray : Uint8ClampedArray | null = null;
|
||||
|
||||
// 存储数组长度(避免重复计算)
|
||||
let gRgbaLength : Int = 0;
|
||||
|
||||
function toInt(num : Number) : Int {
|
||||
return Int.valueOf(num.toString()) as Int; // 强制转换为 Android 原生 Int 类型
|
||||
}
|
||||
function signedToUnsigned(signedValue : number) : number {
|
||||
if (signedValue < 0) {
|
||||
return signedValue + 256;
|
||||
}
|
||||
return signedValue;
|
||||
}
|
||||
/**
|
||||
* 核心工具方法:使用 Android 原生 ImageConverter 转换 YUV_420_888 到 RGB
|
||||
* 完全无手动字节操作,纯系统 API 实现
|
||||
*/
|
||||
function convertYUVToRGBBySystemAPI(image : Image) : string {
|
||||
// 1. 获取 YUV 数据(原生读取,无手动转换)
|
||||
|
||||
const planes = image.getPlanes();
|
||||
const yBuffer = planes[0].getBuffer();
|
||||
const uBuffer = planes[1].getBuffer();
|
||||
const vBuffer = planes[2].getBuffer();
|
||||
const width = image.getWidth();
|
||||
const height = image.getHeight();
|
||||
|
||||
// 1. 提取关键参数(仅计算一次,无冗余)
|
||||
const yRowStride : Int = planes[0].getRowStride();
|
||||
const uvRowStride : Int = planes[1].getRowStride();
|
||||
const uvPixelStride : Int = planes[1].getPixelStride();
|
||||
const yLength : Int = yBuffer.remaining();
|
||||
const uvLength : Int = uBuffer.remaining();
|
||||
// 2. 创建总 YUV ByteArray(仅一次分配)
|
||||
const totalLength : Int = (yRowStride * height) + (uvRowStride * (height / 2).toInt());
|
||||
const yuvByteArray = new ByteArray(totalLength);
|
||||
|
||||
// 3. 批量复制 Y 通道(原生 API,无循环)
|
||||
yBuffer.get(yuvByteArray, 0, yLength);
|
||||
|
||||
// ========== 核心优化:用原生 arraycopy 替代 U/V 双循环 ==========
|
||||
|
||||
// 4. 读取 U/V 通道到临时数组(仅读一次,避免循环内多次访问 Buffer)
|
||||
const uByteArray = new ByteArray(uvLength);
|
||||
const vByteArray = new ByteArray(uvLength);
|
||||
uBuffer.get(uByteArray, 0, uvLength);
|
||||
vBuffer.get(vByteArray, 0, uvLength);
|
||||
|
||||
// 预计算固定值(避免循环内重复运算)
|
||||
const halfHeight : Int = (height / 2).toInt();
|
||||
const halfWidth : Int = (width / 2).toInt();
|
||||
const uvTotalPixels : Int = (halfHeight * halfWidth).toInt(); // U/V 总像素数
|
||||
const uvIndexStart : Int = (yRowStride * height).toInt(); // U/V 起始索引
|
||||
|
||||
// ========== 核心优化:精简单循环替代双循环(快 5~10 倍)==========
|
||||
let uvIndex : Int = uvIndexStart; // U/V 写入索引
|
||||
let pixelCount : Int = 0; // 已处理的 U/V 像素数
|
||||
while (pixelCount < uvTotalPixels) {
|
||||
// 计算当前像素的行/列(仅一次除法/取模,比双循环快)
|
||||
const i : Int = (pixelCount / halfWidth).toInt(); // 行索引
|
||||
const j : Int = (pixelCount % halfWidth).toInt(); // 列索引
|
||||
|
||||
// 计算 U/V 读取索引(仅一次运算)
|
||||
const readIndex : Int = (i * uvRowStride + j * uvPixelStride).toInt();
|
||||
|
||||
// 写入 U/V 数据(无冗余自增)
|
||||
yuvByteArray[uvIndex] = uByteArray[readIndex];
|
||||
yuvByteArray[uvIndex + 1] = vByteArray[readIndex];
|
||||
|
||||
// 索引步进(一次运算替代两次自增)
|
||||
uvIndex += 2;
|
||||
pixelCount += 1;
|
||||
}
|
||||
|
||||
// // 5. RenderScript 转换(硬件加速,无修改)
|
||||
// const context = UTSAndroid.getAppContext();
|
||||
// const rs = android.renderscript.RenderScript.create(context);
|
||||
// const yuvToRgbIntrinsic = android.renderscript.ScriptIntrinsicYuvToRGB.create(
|
||||
// rs,
|
||||
// android.renderscript.Element.RGBA_8888(rs)
|
||||
// );
|
||||
// // 新增:指定 YUV 格式(RenderScript 必须,否则转换可能乱码)
|
||||
// // yuvToRgbIntrinsic.setYuvFormat(android.graphics.ImageFormat.NV21);
|
||||
|
||||
// const inputAllocation = android.renderscript.Allocation.createSized(
|
||||
// rs,
|
||||
// android.renderscript.Element.U8(rs),
|
||||
// totalLength
|
||||
// );
|
||||
// const outputType = android.renderscript.Type.createXY(
|
||||
// rs,
|
||||
// android.renderscript.Element.RGBA_8888(rs),
|
||||
// width,
|
||||
// height
|
||||
// );
|
||||
|
||||
|
||||
// const outputAllocation = android.renderscript.Allocation.createTyped(
|
||||
// rs,
|
||||
// outputType,
|
||||
// android.renderscript.Allocation.USAGE_SHARED | android.renderscript.Allocation.USAGE_SCRIPT
|
||||
// );
|
||||
// yuvToRgbIntrinsic.setInput(inputAllocation);
|
||||
// yuvToRgbIntrinsic.forEach(outputAllocation);
|
||||
// const rgbaLength : Int = (width * height * 4).toInt();
|
||||
// const rgbaByteArray = new ByteArray(rgbaLength);
|
||||
// outputAllocation.copyTo(rgbaByteArray);
|
||||
|
||||
// // 释放原生资源
|
||||
// console.log('释放原生资源');
|
||||
// inputAllocation.destroy();
|
||||
// outputAllocation.destroy();
|
||||
// yuvToRgbIntrinsic.destroy();
|
||||
// rs.destroy();
|
||||
|
||||
// return '';
|
||||
|
||||
}
|
||||
function yuvToJpegBase64(image: android.media.Image): string {
|
||||
const width = image.getWidth();
|
||||
const height = image.getHeight();
|
||||
|
||||
// 仅处理YUV_420_888格式
|
||||
if (image.getFormat() !== android.graphics.ImageFormat.YUV_420_888) {
|
||||
console.error("仅支持YUV_420_888格式");
|
||||
return "";
|
||||
}
|
||||
|
||||
const planes = image.getPlanes();
|
||||
const yPlane = planes[0];
|
||||
const uPlane = planes[1];
|
||||
const vPlane = planes[2];
|
||||
|
||||
// 1. 提取所有关键参数(完整适配YUV_420_888)
|
||||
const yRowStride = yPlane.getRowStride();
|
||||
const yPixelStride = yPlane.getPixelStride();
|
||||
const uRowStride = uPlane.getRowStride();
|
||||
const uPixelStride = uPlane.getPixelStride();
|
||||
const vRowStride = vPlane.getRowStride();
|
||||
const vPixelStride = vPlane.getPixelStride();
|
||||
|
||||
// 2. 读取所有通道的ByteBuffer并转换为ByteArray
|
||||
const yBuffer = yPlane.getBuffer();
|
||||
const uBuffer = uPlane.getBuffer();
|
||||
const vBuffer = vPlane.getBuffer();
|
||||
|
||||
const yByteArray = new ByteArray(yBuffer.remaining());
|
||||
const uByteArray = new ByteArray(uBuffer.remaining());
|
||||
const vByteArray = new ByteArray(vBuffer.remaining());
|
||||
yBuffer.get(yByteArray);
|
||||
uBuffer.get(uByteArray);
|
||||
vBuffer.get(vByteArray);
|
||||
|
||||
// 3. 创建标准NV21数组(YUV_420_888转标准NV21)
|
||||
const nv21 = new ByteArray(width * height * 3 / 2); // NV21固定长度
|
||||
let nv21Index:Int = 0;
|
||||
|
||||
// 3.1 复制Y通道(处理yPixelStride,适配所有情况)
|
||||
for (let i:Int = 0; i < height; i++) {
|
||||
for (let j:Int = 0; j < width; j++) {
|
||||
nv21[nv21Index++] = yByteArray[i * yRowStride + j * yPixelStride];
|
||||
}
|
||||
}
|
||||
|
||||
// 3.2 复制UV通道(适配YUV_420_888所有UV排布)
|
||||
const halfHeight = Math.floor(height / 2);
|
||||
const halfWidth = Math.floor(width / 2);
|
||||
for (let i:Int = 0; i < halfHeight; i++) {
|
||||
for (let j:Int = 0; j < halfWidth; j++) {
|
||||
// 计算UV读取索引(适配pixelStride=1/2)
|
||||
const uIndex:Int = i * uRowStride + j * uPixelStride;
|
||||
const vIndex:Int = i * vRowStride + j * vPixelStride;
|
||||
// NV21格式要求:V在前,U在后
|
||||
nv21[nv21Index++] = vByteArray[vIndex];
|
||||
nv21[nv21Index++] = uByteArray[uIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 改用YuvImage直接转JPEG(比RenderScript更稳定)==========
|
||||
try {
|
||||
// 1. 用YuvImage处理标准NV21数据(避免RenderScript格式兼容问题)
|
||||
const yuvImage = new android.graphics.YuvImage(
|
||||
nv21,
|
||||
android.graphics.ImageFormat.NV21,
|
||||
width,
|
||||
height,
|
||||
null
|
||||
);
|
||||
|
||||
// 2. 压缩为JPEG字节数组
|
||||
const outputStream = new java.io.ByteArrayOutputStream();
|
||||
const compressSuccess = yuvImage.compressToJpeg(
|
||||
new android.graphics.Rect(0, 0, width, height),
|
||||
80,
|
||||
outputStream
|
||||
);
|
||||
|
||||
if (!compressSuccess) {
|
||||
console.error("YUV压缩为JPEG失败");
|
||||
outputStream.close();
|
||||
return "";
|
||||
}
|
||||
|
||||
// 3. 编码为Base64
|
||||
const jpegBytes = outputStream.toByteArray();
|
||||
const base64Raw = android.util.Base64.encodeToString(
|
||||
jpegBytes,
|
||||
android.util.Base64.DEFAULT
|
||||
);
|
||||
const base64Jpeg = `data:image/jpeg;base64,${base64Raw}`;
|
||||
|
||||
// 释放资源
|
||||
outputStream.close();
|
||||
return base64Jpeg;
|
||||
} catch (e) {
|
||||
console.error("YUV转JPEG/Base64失败:", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 启动摄像头预览并获取实时视频帧(YUV420)
|
||||
* @param callback - 每获取到一帧调用一次,参数为 Uint8Array(YUV 数据)
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
|
||||
if (context == null || activity == null) {
|
||||
console.error('错误:上下文或Activity为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 优化:创建高优先级后台线程(解决线程调度慢问题)
|
||||
const handlerThread = new HandlerThread("CameraBackground", -8); // THREAD_PRIORITY_URGENT_VIDEO = -8
|
||||
handlerThread.start();
|
||||
backgroundHandler = new Handler(handlerThread.getLooper());
|
||||
const aa = backgroundHandler
|
||||
const cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager;
|
||||
const cameraIds = cameraManager.getCameraIdList();
|
||||
const cameraId = cameraIds[1]; // 后置摄像头
|
||||
|
||||
try {
|
||||
// 创建 ImageReader
|
||||
let imageReader : ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 8);
|
||||
// 2. 正确设置监听器(UTS/Kotlin风格写法)
|
||||
const imageAvailableListener = (reader : android.media.ImageReader) : void => {
|
||||
// 1. 获取图像(可空类型匹配)
|
||||
const imageProxy : Image | null = reader.acquireLatestImage();
|
||||
if (imageProxy === null) return;
|
||||
// yuvToJpeg(imageProxy)
|
||||
callback(yuvToJpegBase64(imageProxy))
|
||||
// callback(convertYUVToRGBBySystemAPI(imageProxy))
|
||||
// 4. 必须关闭图像释放缓冲区
|
||||
imageProxy.close();
|
||||
|
||||
// 5. 显式return(解决UTS的return校验)
|
||||
return;
|
||||
}
|
||||
|
||||
imageReader.setOnImageAvailableListener(
|
||||
// 核心修复:直接传符合签名的函数(去掉new和对象包裹)
|
||||
imageAvailableListener,
|
||||
// 第二个参数:Handler,无则传null(符合UTS的null类型规范)
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
// 定义全局变量存储相机设备
|
||||
let cameraDevice : android.hardware.camera2.CameraDevice | null = null;
|
||||
|
||||
/**
|
||||
* 自定义相机状态回调类
|
||||
* 核心修复:1. 显式调用父类构造函数 2. 完全匹配 onError 方法签名
|
||||
*/
|
||||
class CustomCameraStateCallback extends android.hardware.camera2.CameraDevice.StateCallback {
|
||||
// 必须显式定义构造函数,并调用父类构造函数(UTS 对接 Android 抽象类的强制要求)
|
||||
public constructor() {
|
||||
super(); // 关键:调用父类 CameraDevice.StateCallback 的构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onOpened 抽象方法(完全匹配原生签名)
|
||||
* 原生:public abstract void onOpened(CameraDevice device);
|
||||
*/
|
||||
|
||||
override onOpened(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机打开成功");
|
||||
const cameraDevice = device;
|
||||
const surface = imageReader!!.getSurface();
|
||||
const surfaces = [surface];
|
||||
|
||||
class CustomCaptureSessionCallback extends android.hardware.camera2.CameraCaptureSession.StateCallback {
|
||||
// 全局相机设备引用(用于创建采集请求)
|
||||
private localCameraDevice : android.hardware.camera2.CameraDevice;
|
||||
private surface : android.view.Surface;
|
||||
|
||||
// 构造函数:传入相机设备和Surface(避免闭包变量问题)
|
||||
public constructor(cameraDevice : android.hardware.camera2.CameraDevice, surface : android.view.Surface) {
|
||||
super(); // 必须调用父类构造
|
||||
this.localCameraDevice = cameraDevice;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置成功
|
||||
*/
|
||||
override onConfigured(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.log("采集会话配置成功,启动持续帧采集");
|
||||
|
||||
// 创建预览采集请求
|
||||
const requestBuilder = this.localCameraDevice.createCaptureRequest(
|
||||
android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
|
||||
);
|
||||
requestBuilder.addTarget(this.surface);
|
||||
|
||||
// 启动持续采集(真正触发相机输出数据)
|
||||
session.setRepeatingRequest(
|
||||
requestBuilder.build(),
|
||||
null,
|
||||
backgroundHandler // 使用后台线程处理采集回调
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置失败
|
||||
*/
|
||||
override onConfigureFailed(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.error("采集会话配置失败");
|
||||
|
||||
}
|
||||
}
|
||||
const sessionCallback = new CustomCaptureSessionCallback(cameraDevice, surface) as CameraCaptureSession.StateCallback;
|
||||
// 创建采集会话
|
||||
cameraDevice.createCaptureSession(
|
||||
surfaces,
|
||||
sessionCallback,
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onDisconnected 抽象方法
|
||||
* 原生:public abstract void onDisconnected(CameraDevice device);
|
||||
*/
|
||||
override onDisconnected(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机连接断开");
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 终极修复 onError 方法(逐字匹配原生签名)
|
||||
* 原生:public abstract void onError(CameraDevice p0, int p1);
|
||||
* 注意:参数名可以是任意(p0/p1 或 device/errorCode),但类型必须是 CameraDevice + number(对应 int)
|
||||
*/
|
||||
override onError(device : android.hardware.camera2.CameraDevice, p1 : Int) : void {
|
||||
console.error(`相机打开失败`);
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选重写 onClosed 方法
|
||||
*/
|
||||
override onClosed(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机已关闭");
|
||||
super.onClosed(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:释放相机资源
|
||||
*/
|
||||
private releaseCameraResources() : void {
|
||||
// if (cameraDevice != null && !cameraDevice.isClosed()) {
|
||||
// cameraDevice.close();
|
||||
// cameraDevice = null;
|
||||
// console.log("相机资源已释放");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化回调(无报错)
|
||||
const cameraStateCallback = new CustomCameraStateCallback();
|
||||
cameraManager.openCamera(
|
||||
cameraId, // param0: string(相机ID,必传非null)
|
||||
cameraStateCallback, // param1: StateCallback(回调,必传非null)
|
||||
backgroundHandler // param2: Handler(传null表示用主线程,符合可空要求)
|
||||
);
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error('启动摄像头失败:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
*/
|
||||
import Context from "android.content.Context";
|
||||
import AudioManager from "android.media.AudioManager";
|
||||
import MediaStore from "android.provider.MediaStore";
|
||||
import Intent from 'android.content.Intent';
|
||||
import Environment from "android.os.Environment";
|
||||
import File from "java.io.File";
|
||||
import FileProvider from "androidx.core.content.FileProvider";
|
||||
import Integer from "java.lang.Integer";
|
||||
|
||||
/* 引入接口文件(复用你现有代码的接口规范) */
|
||||
import { MyApiOptions, MyApiResult, MyApi } from '../interface.uts';
|
||||
|
||||
/* 引入错误处理文件 */
|
||||
import { MyApiFailImpl } from '../unierror';
|
||||
|
||||
|
||||
|
||||
let tempPhotoPath: string = "";
|
||||
|
||||
/**
|
||||
* 音量加1功能(保持和你电量代码一致的异步回调风格)
|
||||
* @param options 配置项,包含要调整的音量类型、回调函数等
|
||||
*/
|
||||
export const increaseVolume : MyApi = function (options : MyApiOptions) {
|
||||
// 获取应用上下文
|
||||
const context = UTSAndroid.getAppContext();
|
||||
|
||||
// 上下文为空的错误处理
|
||||
if (context == null) {
|
||||
const err = new MyApiFailImpl(9010001);
|
||||
options.fail?.(err);
|
||||
options.complete?.('xxx');
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
if (activity == null) {
|
||||
const err = new MyApiFailImpl(9010001);
|
||||
options.fail?.(err);
|
||||
options.complete?.(err);
|
||||
return;
|
||||
}
|
||||
// 1. 创建相机Intent
|
||||
const cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
const packageManager = activity.getPackageManager();
|
||||
if (packageManager == null || cameraIntent.resolveActivity(packageManager) == null) {
|
||||
const err = new MyApiFailImpl(9010001);
|
||||
options.fail?.(err);
|
||||
options.complete?.(err);
|
||||
return;
|
||||
}
|
||||
// 检查设备是否有相机应用
|
||||
// 2. 创建临时图片文件
|
||||
const photoFile = createTempPhotoFile(context);
|
||||
if (photoFile == null) {
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
tempPhotoPath = photoFile.getAbsolutePath(); // 保存文件路径
|
||||
const photoUri = FileProvider.getUriForFile(
|
||||
activity,
|
||||
activity.getPackageName() + ".fileprovider",
|
||||
photoFile
|
||||
);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
|
||||
// 4. 启动相机(旧版兼容方案)
|
||||
const REQUEST_CODE_CAMERA: Int = 1001;
|
||||
activity.startActivityForResult(cameraIntent, REQUEST_CODE_CAMERA);
|
||||
|
||||
// options.complete?.(photoUri);
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建临时图片文件
|
||||
*/
|
||||
function createTempPhotoFile(context: Context): File | null {
|
||||
try {
|
||||
const fileName = `IMG_${Date.now()}.jpg`;
|
||||
// 获取应用私有图片目录(无需外部存储权限)
|
||||
const storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||
if (storageDir == null) return null;
|
||||
|
||||
// 确保目录存在
|
||||
if (!storageDir.exists() && !storageDir.mkdirs()) {
|
||||
console.error("创建图片目录失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
const photoFile = new File(storageDir, fileName);
|
||||
// 确保文件可写
|
||||
if (!photoFile.createNewFile()) {
|
||||
console.error("创建图片文件失败");
|
||||
return null;
|
||||
}
|
||||
return photoFile;
|
||||
} catch (e: any) {
|
||||
console.error("创建图片文件异常s", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
*/
|
||||
import Context from "android.content.Context";
|
||||
import ContextCompat from "androidx.core.content.ContextCompat";
|
||||
import AudioManager from "android.media.AudioManager";
|
||||
import MediaStore from "android.provider.MediaStore";
|
||||
import Intent from 'android.content.Intent';
|
||||
import Environment from "android.os.Environment";
|
||||
import File from "java.io.File";
|
||||
import FileProvider from "androidx.core.content.FileProvider";
|
||||
import Integer from "java.lang.Integer";
|
||||
import CameraManager from "android.hardware.camera2.CameraManager";
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
import ImageReader from "android.media.ImageReader";
|
||||
import CameraDevice from "android.hardware.camera2.CameraDevice";
|
||||
import ImageFormat from "android.graphics.ImageFormat";
|
||||
import Surface from "android.view.Surface";
|
||||
import CameraCaptureSession from "android.hardware.camera2.CameraCaptureSession";
|
||||
import CaptureRequest from "android.hardware.camera2.CaptureRequest";
|
||||
import PermissionChecker from "androidx.core.content.PermissionChecker";
|
||||
import Manifest from "android.Manifest";
|
||||
import HandlerThread from "android.os.HandlerThread";
|
||||
import Handler from "android.os.Handler";
|
||||
import OnImageAvailableListener from "android.media.ImageReader.OnImageAvailableListener";
|
||||
import Image from "android.media.Image";
|
||||
import { UTSAndroid } from "io.dcloud.uts";
|
||||
|
||||
import Bitmap from "android.graphics.Bitmap";
|
||||
import Color from "android.graphics.Color";
|
||||
import Class from "java.lang.Class"; // 新增:反射所需类
|
||||
import Method from "java.lang.reflect.Method"; // 新增:反射方法
|
||||
import Constructor from "java.lang.reflect.Constructor"; // 新增:反射构造函数
|
||||
import ImageConverter from "android.media.ImageConverter"; // 新增:反射构造函数
|
||||
import Size from "android.util.Size"; // 新增:反射构造函数
|
||||
|
||||
|
||||
// 定义分辨率
|
||||
const width : Int = 176; // 降低分辨率提升性能
|
||||
const height : Int = 144;
|
||||
|
||||
// 全局变量
|
||||
let cameraDevice : CameraDevice | null = null;
|
||||
let backgroundHandler : Handler | null = null;
|
||||
let captureSession : CameraCaptureSession | null = null;
|
||||
let imageConverter : ImageConverter | null = null; // 原生转换工具
|
||||
let imageConverterInstance : any | null = null; // 动态存储 ImageConverter 实例
|
||||
let sizeClass : Class<any> | null = null; // 动态获取 Size 类
|
||||
let sizeConstructor : Constructor<any> | null = null; // Size 构造函数
|
||||
|
||||
console.log('1111', android.os.Build.VERSION.SDK_INT);
|
||||
|
||||
function toInt(num : Number) : Int {
|
||||
return Int.valueOf(num.toString()) as Int; // 强制转换为 Android 原生 Int 类型
|
||||
}
|
||||
|
||||
let gRgbaData = new Uint8ClampedArray(101376);
|
||||
|
||||
/**
|
||||
* 核心工具方法:使用 Android 原生 ImageConverter 转换 YUV_420_888 到 RGB
|
||||
* 完全无手动字节操作,纯系统 API 实现
|
||||
*/
|
||||
function convertYUVToRGBBySystemAPI(image : Image) : Uint8ClampedArray {
|
||||
// 1. 获取 YUV 数据(原生读取,无手动转换)
|
||||
|
||||
const planes = image.getPlanes();
|
||||
const yBuffer = planes[0].getBuffer();
|
||||
const uBuffer = planes[1].getBuffer();
|
||||
const vBuffer = planes[2].getBuffer();
|
||||
|
||||
|
||||
// 1. 提取关键参数(仅计算一次,无冗余)
|
||||
const yRowStride : Int = planes[0].getRowStride();
|
||||
const uvRowStride : Int = planes[1].getRowStride();
|
||||
const uvPixelStride : Int = planes[1].getPixelStride();
|
||||
const yLength : Int = yBuffer.remaining();
|
||||
const uvLength : Int = uBuffer.remaining();
|
||||
|
||||
// 2. 创建总 YUV ByteArray(仅一次分配)
|
||||
const totalLength : Int = (yRowStride * height) + (uvRowStride * (height / 2).toInt());
|
||||
const yuvByteArray = new ByteArray(totalLength);
|
||||
|
||||
// 3. 批量复制 Y 通道(原生 API,无循环)
|
||||
yBuffer.get(yuvByteArray, 0, yLength);
|
||||
|
||||
// ========== 核心优化:用原生 arraycopy 替代 U/V 双循环 ==========
|
||||
|
||||
// 4. 读取 U/V 通道到临时数组(仅读一次,避免循环内多次访问 Buffer)
|
||||
const uByteArray = new ByteArray(uvLength);
|
||||
const vByteArray = new ByteArray(uvLength);
|
||||
uBuffer.get(uByteArray, 0, uvLength);
|
||||
vBuffer.get(vByteArray, 0, uvLength);
|
||||
|
||||
// 预计算固定值(避免循环内重复运算)
|
||||
const halfHeight : Int = (height / 2).toInt();
|
||||
const halfWidth : Int = (width / 2).toInt();
|
||||
const uvTotalPixels : Int = (halfHeight * halfWidth).toInt(); // U/V 总像素数
|
||||
const uvIndexStart : Int = (yRowStride * height).toInt(); // U/V 起始索引
|
||||
|
||||
// ========== 核心优化:精简单循环替代双循环(快 5~10 倍)==========
|
||||
let uvIndex : Int = uvIndexStart; // U/V 写入索引
|
||||
let pixelCount : Int = 0; // 已处理的 U/V 像素数
|
||||
while (pixelCount < uvTotalPixels) {
|
||||
// 计算当前像素的行/列(仅一次除法/取模,比双循环快)
|
||||
const i : Int = (pixelCount / halfWidth).toInt(); // 行索引
|
||||
const j : Int = (pixelCount % halfWidth).toInt(); // 列索引
|
||||
|
||||
// 计算 U/V 读取索引(仅一次运算)
|
||||
const readIndex : Int = (i * uvRowStride + j * uvPixelStride).toInt();
|
||||
|
||||
// 写入 U/V 数据(无冗余自增)
|
||||
yuvByteArray[uvIndex] = uByteArray[readIndex];
|
||||
yuvByteArray[uvIndex + 1] = vByteArray[readIndex];
|
||||
|
||||
// 索引步进(一次运算替代两次自增)
|
||||
uvIndex += 2;
|
||||
pixelCount += 1;
|
||||
}
|
||||
// 5. RenderScript 转换(硬件加速,无修改)
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const rs = android.renderscript.RenderScript.create(context);
|
||||
const yuvToRgbIntrinsic = android.renderscript.ScriptIntrinsicYuvToRGB.create(
|
||||
rs,
|
||||
android.renderscript.Element.U8_4(rs)
|
||||
);
|
||||
|
||||
const inputAllocation = android.renderscript.Allocation.createSized(
|
||||
rs,
|
||||
android.renderscript.Element.U8(rs),
|
||||
totalLength
|
||||
);
|
||||
const outputType = android.renderscript.Type.createXY(
|
||||
rs,
|
||||
android.renderscript.Element.RGBA_8888(rs),
|
||||
width,
|
||||
height
|
||||
);
|
||||
const outputAllocation = android.renderscript.Allocation.createTyped(rs, outputType);
|
||||
|
||||
inputAllocation.copyFrom(yuvByteArray);
|
||||
yuvToRgbIntrinsic.setInput(inputAllocation);
|
||||
yuvToRgbIntrinsic.forEach(outputAllocation);
|
||||
|
||||
// 6. 提取 RGBA 数据(转换为 0~255 无符号值)
|
||||
const rgbaLength : Int = (width * height * 4).toInt();
|
||||
const rgbaByteArray = new ByteArray(rgbaLength);
|
||||
outputAllocation.copyTo(rgbaByteArray);
|
||||
|
||||
// ========== 核心优化:复用数组 + 极简转换 ==========
|
||||
|
||||
// 2. 预缓存变量(减少属性查找)
|
||||
const byteArr = rgbaByteArray;
|
||||
const len = rgbaLength;
|
||||
// 3. 极简单循环(位运算最快转换)
|
||||
for (let i: Int = 0; i < len; i++) {
|
||||
// 一步到位:Byte→Int + 负数修正
|
||||
const val: Int = byteArr[i].toInt();
|
||||
gRgbaData[i] = 128;
|
||||
}
|
||||
|
||||
|
||||
// 7. 释放资源
|
||||
inputAllocation.destroy();
|
||||
outputAllocation.destroy();
|
||||
yuvToRgbIntrinsic.destroy();
|
||||
rs.destroy();
|
||||
console.log('111', gRgbaData);
|
||||
return gRgbaData
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动摄像头预览并获取实时视频帧(YUV420)
|
||||
* @param callback - 每获取到一帧调用一次,参数为 Uint8Array(YUV 数据)
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startCameraPreview(callback : (data : Uint8ClampedArray) => void) : void {
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
|
||||
if (context == null || activity == null) {
|
||||
console.error('错误:上下文或Activity为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 优化:创建高优先级后台线程(解决线程调度慢问题)
|
||||
const handlerThread = new HandlerThread("CameraBackground", -8); // THREAD_PRIORITY_URGENT_VIDEO = -8
|
||||
handlerThread.start();
|
||||
backgroundHandler = new Handler(handlerThread.getLooper());
|
||||
const aa = backgroundHandler
|
||||
const cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager;
|
||||
const cameraIds = cameraManager.getCameraIdList();
|
||||
const cameraId = cameraIds[1]; // 后置摄像头
|
||||
|
||||
try {
|
||||
// 创建 ImageReader
|
||||
let imageReader : ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 8);
|
||||
// 2. 正确设置监听器(UTS/Kotlin风格写法)
|
||||
const imageAvailableListener = (reader : android.media.ImageReader) : void => {
|
||||
// 1. 获取图像(可空类型匹配)
|
||||
const imageProxy : Image | null = reader.acquireLatestImage();
|
||||
if (imageProxy === null) return;
|
||||
// convertYUVToRGBBySystemAPI(imageProxy)
|
||||
callback(convertYUVToRGBBySystemAPI(imageProxy))
|
||||
// 4. 必须关闭图像释放缓冲区
|
||||
imageProxy.close();
|
||||
|
||||
// 5. 显式return(解决UTS的return校验)
|
||||
return;
|
||||
}
|
||||
|
||||
imageReader.setOnImageAvailableListener(
|
||||
// 核心修复:直接传符合签名的函数(去掉new和对象包裹)
|
||||
imageAvailableListener,
|
||||
// 第二个参数:Handler,无则传null(符合UTS的null类型规范)
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
// 定义全局变量存储相机设备
|
||||
let cameraDevice : android.hardware.camera2.CameraDevice | null = null;
|
||||
|
||||
/**
|
||||
* 自定义相机状态回调类
|
||||
* 核心修复:1. 显式调用父类构造函数 2. 完全匹配 onError 方法签名
|
||||
*/
|
||||
class CustomCameraStateCallback extends android.hardware.camera2.CameraDevice.StateCallback {
|
||||
// 必须显式定义构造函数,并调用父类构造函数(UTS 对接 Android 抽象类的强制要求)
|
||||
public constructor() {
|
||||
super(); // 关键:调用父类 CameraDevice.StateCallback 的构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onOpened 抽象方法(完全匹配原生签名)
|
||||
* 原生:public abstract void onOpened(CameraDevice device);
|
||||
*/
|
||||
|
||||
override onOpened(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机打开成功");
|
||||
const cameraDevice = device;
|
||||
const surface = imageReader!!.getSurface();
|
||||
const surfaces = [surface];
|
||||
|
||||
class CustomCaptureSessionCallback extends android.hardware.camera2.CameraCaptureSession.StateCallback {
|
||||
// 全局相机设备引用(用于创建采集请求)
|
||||
private localCameraDevice : android.hardware.camera2.CameraDevice;
|
||||
private surface : android.view.Surface;
|
||||
|
||||
// 构造函数:传入相机设备和Surface(避免闭包变量问题)
|
||||
public constructor(cameraDevice : android.hardware.camera2.CameraDevice, surface : android.view.Surface) {
|
||||
super(); // 必须调用父类构造
|
||||
this.localCameraDevice = cameraDevice;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置成功
|
||||
*/
|
||||
override onConfigured(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.log("采集会话配置成功,启动持续帧采集");
|
||||
|
||||
// 创建预览采集请求
|
||||
const requestBuilder = this.localCameraDevice.createCaptureRequest(
|
||||
android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
|
||||
);
|
||||
requestBuilder.addTarget(this.surface);
|
||||
|
||||
// 启动持续采集(真正触发相机输出数据)
|
||||
session.setRepeatingRequest(
|
||||
requestBuilder.build(),
|
||||
null,
|
||||
backgroundHandler // 使用后台线程处理采集回调
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置失败
|
||||
*/
|
||||
override onConfigureFailed(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.error("采集会话配置失败");
|
||||
|
||||
}
|
||||
}
|
||||
const sessionCallback = new CustomCaptureSessionCallback(cameraDevice, surface) as CameraCaptureSession.StateCallback;
|
||||
// 创建采集会话
|
||||
cameraDevice.createCaptureSession(
|
||||
surfaces,
|
||||
sessionCallback,
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onDisconnected 抽象方法
|
||||
* 原生:public abstract void onDisconnected(CameraDevice device);
|
||||
*/
|
||||
override onDisconnected(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机连接断开");
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 终极修复 onError 方法(逐字匹配原生签名)
|
||||
* 原生:public abstract void onError(CameraDevice p0, int p1);
|
||||
* 注意:参数名可以是任意(p0/p1 或 device/errorCode),但类型必须是 CameraDevice + number(对应 int)
|
||||
*/
|
||||
override onError(device : android.hardware.camera2.CameraDevice, p1 : Int) : void {
|
||||
console.error(`相机打开失败`);
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选重写 onClosed 方法
|
||||
*/
|
||||
override onClosed(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机已关闭");
|
||||
super.onClosed(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:释放相机资源
|
||||
*/
|
||||
private releaseCameraResources() : void {
|
||||
// if (cameraDevice != null && !cameraDevice.isClosed()) {
|
||||
// cameraDevice.close();
|
||||
// cameraDevice = null;
|
||||
// console.log("相机资源已释放");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化回调(无报错)
|
||||
const cameraStateCallback = new CustomCameraStateCallback();
|
||||
cameraManager.openCamera(
|
||||
cameraId, // param0: string(相机ID,必传非null)
|
||||
cameraStateCallback, // param1: StateCallback(回调,必传非null)
|
||||
backgroundHandler // param2: Handler(传null表示用主线程,符合可空要求)
|
||||
);
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error('启动摄像头失败:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
*/
|
||||
import Context from "android.content.Context";
|
||||
import AudioManager from "android.media.AudioManager";
|
||||
import MediaStore from "android.provider.MediaStore";
|
||||
import Intent from 'android.content.Intent';
|
||||
import Environment from "android.os.Environment";
|
||||
import File from "java.io.File";
|
||||
import FileProvider from "androidx.core.content.FileProvider";
|
||||
import Integer from "java.lang.Integer";
|
||||
import CameraManager from "android.hardware.camera2.CameraManager";
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
import ImageReader from "android.media.ImageReader";
|
||||
import CameraDevice from "android.hardware.camera2.CameraDevice";
|
||||
import ImageFormat from "android.graphics.ImageFormat";
|
||||
import Surface from "android.view.Surface";
|
||||
import CameraCaptureSession from "android.hardware.camera2.CameraCaptureSession";
|
||||
import CaptureRequest from "android.hardware.camera2.CaptureRequest";
|
||||
import PermissionChecker from "androidx.core.content.PermissionChecker";
|
||||
import Manifest from "android.Manifest";
|
||||
import HandlerThread from "android.os.HandlerThread";
|
||||
import Handler from "android.os.Handler";
|
||||
import OnImageAvailableListener from "android.media.ImageReader.OnImageAvailableListener";
|
||||
import Image from "android.media.Image";
|
||||
import { UTSAndroid } from "io.dcloud.uts";
|
||||
// 定义分辨率
|
||||
const width : Int = 24; // 降低分辨率提升性能
|
||||
const height : Int = 32;
|
||||
|
||||
// 全局变量
|
||||
let cameraDevice : CameraDevice | null = null;
|
||||
let backgroundHandler : Handler | null = null;
|
||||
let captureSession : CameraCaptureSession | null = null;
|
||||
|
||||
/**
|
||||
* 最终无错版:YUV_420_888转JPEG ByteArray(解决所有类型/参数错误)
|
||||
*/
|
||||
function yuvToJpeg(image : android.media.Image) : ByteArray | null {
|
||||
try {
|
||||
const width = image.getWidth();
|
||||
const height = image.getHeight();
|
||||
const planes = image.getPlanes();
|
||||
|
||||
if (planes.size < 3) {
|
||||
console.error("Image平面数量不足,无法转换YUV");
|
||||
return null;
|
||||
}
|
||||
|
||||
const yPlane = planes[0];
|
||||
const uPlane = planes[1];
|
||||
const vPlane = planes[2];
|
||||
|
||||
// 步骤1:使用UTS专属API将ByteBuffer转为ArrayBuffer(核心优化)
|
||||
// Y平面
|
||||
const yByteBuffer = yPlane.getBuffer();
|
||||
const yArrayBuffer = ArrayBuffer.fromByteBuffer(yByteBuffer);
|
||||
const yByteArray = new Int8Array(yArrayBuffer); // 转为字节数组
|
||||
yByteBuffer.rewind(); // 重置指针
|
||||
|
||||
// U平面
|
||||
const uByteBuffer = uPlane.getBuffer();
|
||||
const uArrayBuffer = ArrayBuffer.fromByteBuffer(uByteBuffer);
|
||||
const uByteArray = new Int8Array(uArrayBuffer);
|
||||
uByteBuffer.rewind();
|
||||
|
||||
// V平面
|
||||
const vByteBuffer = vPlane.getBuffer();
|
||||
const vArrayBuffer = ArrayBuffer.fromByteBuffer(vByteBuffer);
|
||||
const vByteArray = new Int8Array(vArrayBuffer);
|
||||
vByteBuffer.rewind();
|
||||
|
||||
// 步骤2:获取步长参数
|
||||
const yStride = yPlane.getRowStride();
|
||||
const uvStride = uPlane.getRowStride();
|
||||
const uvPixelStride = uPlane.getPixelStride();
|
||||
|
||||
// 步骤3:创建NV21格式的目标ByteArray
|
||||
const ySize = yByteArray.size;
|
||||
const uvHeight = Math.floor(height / 2);
|
||||
const uvWidth = Math.floor(width / 2);
|
||||
const uvSize = uvHeight * uvWidth * 2;
|
||||
const totalSize = ySize + uvSize;
|
||||
// const nv21Bytes = new Array<number>(totalSize) as ByteArray;
|
||||
const nv21Bytes = new java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, totalSize as Int ) as ByteArray;
|
||||
// 步骤4:复制Y数据
|
||||
for (let i:Int = 0; i < ySize; i++) {
|
||||
// nv21Bytes[i] = yByteArray[i] & 0xFF; // 确保是0-255的无符号字节值
|
||||
const yValue: Number = yByteArray[i] & 0xFF;
|
||||
nv21Bytes[i] = yValue.toByte(); // 显式转换为Byte,匹配类型要求
|
||||
}
|
||||
|
||||
// 步骤5:复制UV数据(NV21格式:V在前,U在后)
|
||||
let uvIndex = ySize;
|
||||
for (let i = 0; i < uvHeight; i++) {
|
||||
for (let j = 0; j < uvWidth; j++) {
|
||||
if (uvIndex >= totalSize) break;
|
||||
|
||||
const uvArrayIndex = i * uvStride + j * uvPixelStride;
|
||||
if (uvArrayIndex >= vByteArray.length || uvArrayIndex >= uByteArray.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换为无符号字节值(避免负数)
|
||||
const vValue = vByteArray[uvArrayIndex] & 0xFF;
|
||||
const uValue = uByteArray[uvArrayIndex] & 0xFF;
|
||||
|
||||
|
||||
nv21Bytes[uvIndex++] = vValue.toByte();;
|
||||
nv21Bytes[uvIndex++] = uValue.toByte();;
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤6:NV21转JPEG
|
||||
|
||||
const yuvImage = new android.graphics.YuvImage(
|
||||
nv21Bytes,
|
||||
android.graphics.ImageFormat.NV21,
|
||||
width,
|
||||
height,
|
||||
null
|
||||
);
|
||||
|
||||
const outputStream = new java.io.ByteArrayOutputStream();
|
||||
const compressSuccess = yuvImage.compressToJpeg(
|
||||
new android.graphics.Rect(0, 0, width, height),
|
||||
80, // 压缩质量
|
||||
outputStream
|
||||
);
|
||||
|
||||
if (!compressSuccess) {
|
||||
console.error("YUV压缩为JPEG失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
const jpegBytes = outputStream.toByteArray();
|
||||
outputStream.close();
|
||||
return jpegBytes;
|
||||
} catch (e : any) {
|
||||
console.error("YUV转JPEG异常:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Image转前端可展示的Base64(最终无错版)
|
||||
*/
|
||||
function imageToFrontendBase64(imageProxy : android.media.Image) : ByteArray | null {
|
||||
|
||||
const jpegBytes = yuvToJpeg(imageProxy);
|
||||
return jpegBytes
|
||||
// if (jpegBytes === null) return '';
|
||||
// // 安卓原生Base64编码(带前端前缀)
|
||||
// const base64Raw = android.util.Base64.encodeToString(
|
||||
// jpegBytes,
|
||||
// android.util.Base64.DEFAULT
|
||||
// );
|
||||
// return `data:image/jpeg;base64,${base64Raw}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动摄像头预览并获取实时视频帧(YUV420)
|
||||
* @param callback - 每获取到一帧调用一次,参数为 Uint8Array(YUV 数据)
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startCameraPreview(callback : (data : ByteArray) => void) : void {
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
|
||||
if (context == null || activity == null) {
|
||||
console.error('错误:上下文或Activity为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建后台线程
|
||||
const handlerThread = new HandlerThread("CameraBackground");
|
||||
handlerThread.start();
|
||||
backgroundHandler = new Handler(handlerThread.getLooper());
|
||||
|
||||
const cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager;
|
||||
const cameraIds = cameraManager.getCameraIdList();
|
||||
const cameraId = cameraIds[1]; // 后置摄像头
|
||||
try {
|
||||
let imageReader : ImageReader | null = null;
|
||||
// 创建 ImageReader
|
||||
imageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2);
|
||||
// console.log(imageReader!!.getWidth());
|
||||
// 修复关键:显式实现OnImageAvailableListener接口
|
||||
imageReader.setOnImageAvailableListener(
|
||||
function (reader : android.media.ImageReader) { // 纯函数形式,参数为ImageReader
|
||||
const imageProxy: android.media.Image = reader.acquireLatestImage();
|
||||
|
||||
// console.log(reader);
|
||||
// 获取最新帧(只处理最新的,避免缓冲区堆积)
|
||||
|
||||
// if (imageProxy) {
|
||||
// processVideoFrame(imageProxy);
|
||||
// 必须关闭,否则缓冲区会快速耗尽
|
||||
// console.log(imageProxy);
|
||||
|
||||
// callback('ss')
|
||||
// const beta = imageToFrontendBase64(imageProxy)
|
||||
// if(beta !== null) {
|
||||
callback(imageProxy)
|
||||
// }
|
||||
|
||||
|
||||
imageProxy.close();
|
||||
// }
|
||||
},
|
||||
null // Handler参数直接传null,无需类型转换
|
||||
);
|
||||
|
||||
// 定义全局变量存储相机设备
|
||||
let cameraDevice : android.hardware.camera2.CameraDevice | null = null;
|
||||
|
||||
/**
|
||||
* 自定义相机状态回调类
|
||||
* 核心修复:1. 显式调用父类构造函数 2. 完全匹配 onError 方法签名
|
||||
*/
|
||||
class CustomCameraStateCallback extends android.hardware.camera2.CameraDevice.StateCallback {
|
||||
// 必须显式定义构造函数,并调用父类构造函数(UTS 对接 Android 抽象类的强制要求)
|
||||
public constructor() {
|
||||
super(); // 关键:调用父类 CameraDevice.StateCallback 的构造函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onOpened 抽象方法(完全匹配原生签名)
|
||||
* 原生:public abstract void onOpened(CameraDevice device);
|
||||
*/
|
||||
|
||||
override onOpened(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机打开成功");
|
||||
const cameraDevice = device;
|
||||
const surface = imageReader!!.getSurface();
|
||||
const surfaces = [surface];
|
||||
|
||||
class CustomCaptureSessionCallback extends android.hardware.camera2.CameraCaptureSession.StateCallback {
|
||||
// 全局相机设备引用(用于创建采集请求)
|
||||
private localCameraDevice : android.hardware.camera2.CameraDevice;
|
||||
private surface : android.view.Surface;
|
||||
|
||||
// 构造函数:传入相机设备和Surface(避免闭包变量问题)
|
||||
public constructor(cameraDevice : android.hardware.camera2.CameraDevice, surface : android.view.Surface) {
|
||||
super(); // 必须调用父类构造
|
||||
this.localCameraDevice = cameraDevice;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置成功
|
||||
*/
|
||||
override onConfigured(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
captureSession = session;
|
||||
console.log("采集会话配置成功,启动持续帧采集");
|
||||
|
||||
// 创建预览采集请求
|
||||
const requestBuilder = this.localCameraDevice.createCaptureRequest(
|
||||
android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
|
||||
);
|
||||
requestBuilder.addTarget(this.surface);
|
||||
|
||||
// 启动持续采集(真正触发相机输出数据)
|
||||
session.setRepeatingRequest(
|
||||
requestBuilder.build(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现抽象方法:采集会话配置失败
|
||||
*/
|
||||
override onConfigureFailed(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.error("采集会话配置失败");
|
||||
|
||||
}
|
||||
}
|
||||
const sessionCallback = new CustomCaptureSessionCallback(cameraDevice, surface);
|
||||
|
||||
|
||||
|
||||
cameraDevice.createCaptureSession(
|
||||
surfaces,
|
||||
sessionCallback, // 传显式实例化的StateCallback
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 onDisconnected 抽象方法
|
||||
* 原生:public abstract void onDisconnected(CameraDevice device);
|
||||
*/
|
||||
override onDisconnected(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机连接断开");
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 终极修复 onError 方法(逐字匹配原生签名)
|
||||
* 原生:public abstract void onError(CameraDevice p0, int p1);
|
||||
* 注意:参数名可以是任意(p0/p1 或 device/errorCode),但类型必须是 CameraDevice + number(对应 int)
|
||||
*/
|
||||
override onError(device : android.hardware.camera2.CameraDevice, p1 : Int) : void {
|
||||
console.error(`相机打开失败`);
|
||||
this.releaseCameraResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选重写 onClosed 方法
|
||||
*/
|
||||
override onClosed(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机已关闭");
|
||||
super.onClosed(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:释放相机资源
|
||||
*/
|
||||
private releaseCameraResources() : void {
|
||||
// if (cameraDevice != null && !cameraDevice.isClosed()) {
|
||||
// cameraDevice.close();
|
||||
// cameraDevice = null;
|
||||
// console.log("相机资源已释放");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化回调(无报错)
|
||||
const cameraStateCallback = new CustomCameraStateCallback();
|
||||
cameraManager.openCamera(
|
||||
cameraId, // param0: string(相机ID,必传非null)
|
||||
cameraStateCallback, // param1: StateCallback(回调,必传非null)
|
||||
null // param2: Handler(传null表示用主线程,符合可空要求)
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.error('启动摄像头失败:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "12"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 引用 iOS 系统库,示例如下:
|
||||
* import { UIDevice } from "UIKit";
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
|
||||
|
||||
/* 引入 unierror.uts 文件中定义的变量 */
|
||||
import { MyApiFailImpl } from '../unierror';
|
||||
|
||||
/**
|
||||
* 引入三方库
|
||||
* [可选实现,按需引入]
|
||||
*
|
||||
* 在 iOS 平台引入三方库有以下两种方式:
|
||||
* 1、通过引入三方库framework 或者.a 等方式,需要将 .framework 放到 ./Frameworks 目录下,将.a 放到 ./Libs 目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-平台原生配置)
|
||||
* 2、通过 cocoaPods 方式引入,将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
|
||||
*
|
||||
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import:
|
||||
* 示例:import { LottieLoopMode } from 'Lottie'
|
||||
*/
|
||||
|
||||
/**
|
||||
* UTSiOS 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 异步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApi } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApi({
|
||||
* paramA: false,
|
||||
* complete: (res) => {
|
||||
* console.log(res)
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*/
|
||||
export const myApi : MyApi = function (options : MyApiOptions) {
|
||||
|
||||
if (options.paramA == true) {
|
||||
// 返回数据
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: true,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
options.success?.(res);
|
||||
options.complete?.(res);
|
||||
|
||||
} else {
|
||||
// 返回错误
|
||||
let failResult = new MyApiFailImpl(9010001);
|
||||
options.fail?.(failResult)
|
||||
options.complete?.(failResult)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApiSync } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApiSync(true);
|
||||
*
|
||||
*/
|
||||
export const myApiSync : MyApiSync = function (paramA : boolean) : MyApiResult {
|
||||
// 返回数据,根据插件功能获取实际的返回值
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: paramA,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更多插件开发的信息详见:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html
|
||||
*/
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* interface.uts
|
||||
* uts插件接口定义文件,按规范定义接口文件可以在HBuilderX中更好的做到语法提示
|
||||
*/
|
||||
|
||||
/**
|
||||
* myApi 异步函数的参数,在type里定义函数需要的参数以及api成功、失败的相关回调函数。
|
||||
*/
|
||||
export type MyApiOptions = {
|
||||
paramA : boolean
|
||||
success ?: (res : MyApiResult) => void
|
||||
fail ?: (res : MyApiFail) => void
|
||||
complete ?: (res : any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数返回结果
|
||||
* 可以是void, 基本数据类型,自定义type, 或者其他类型。
|
||||
* [可选实现]
|
||||
*/
|
||||
export type MyApiResult = {
|
||||
fieldA : number,
|
||||
fieldB : boolean,
|
||||
fieldC : string
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
* 根据uni错误码规范要求,建议错误码以90开头,以下是错误码示例:
|
||||
* - 9010001 错误信息1
|
||||
* - 9010002 错误信息2
|
||||
*/
|
||||
export type MyApiErrorCode = 9010001 | 9010002 | 9010003 | 9010004 | 9010005 | 9010006 | 9010007 | 9010008 | 9010009 | 9010010 | 9010011 | 9010012;
|
||||
/**
|
||||
* myApi 的错误回调参数
|
||||
*/
|
||||
export interface MyApiFail extends IUniError {
|
||||
errCode : MyApiErrorCode
|
||||
};
|
||||
|
||||
/* 异步函数定义 */
|
||||
export type MyApi = (options : MyApiOptions) => void
|
||||
|
||||
/* 同步函数定义 */
|
||||
export type MyApiSync = (paramA : boolean) => MyApiResult
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 此规范为 uni 规范,可以按照自己的需要选择是否实现 */
|
||||
import { MyApiErrorCode, MyApiFail } from "./interface.uts"
|
||||
/**
|
||||
* 错误主题
|
||||
* 注意:错误主题一般为插件名称,每个组件不同,需要使用时请更改。
|
||||
* [可选实现]
|
||||
*/
|
||||
export const UniErrorSubject = 'uts-api';
|
||||
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
* @UniError
|
||||
* [可选实现]
|
||||
*/
|
||||
export const MyAPIErrors : Map<MyApiErrorCode, string> = new Map([
|
||||
/**
|
||||
* 错误码及对应的错误信息
|
||||
*/
|
||||
[9010001, 'custom error mseeage1'],
|
||||
[9010002, 'custom error mseeage2'],
|
||||
[9010003, 'custom error mseeage2'],
|
||||
[9010004, 'custom error mseeage2'],
|
||||
[9010005, 'custom error mseeage2'],
|
||||
[9010006, 'custom error mseeage2'],
|
||||
[9010007, 'custom error mseeage2'],
|
||||
[9010008, 'custom error mseeage2'],
|
||||
[9010009, 'custom error mseeage2'],
|
||||
[9010010, 'custom error mseeage2'],
|
||||
[9010011, 'custom error mseeage2']
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* 错误对象实现
|
||||
*/
|
||||
export class MyApiFailImpl extends UniError implements MyApiFail {
|
||||
|
||||
/**
|
||||
* 错误对象构造函数
|
||||
*/
|
||||
constructor(errCode : MyApiErrorCode) {
|
||||
super();
|
||||
this.errSubject = UniErrorSubject;
|
||||
this.errCode = errCode;
|
||||
this.errMsg = MyAPIErrors.get(errCode) ?? "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user