Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop
This commit is contained in:
@@ -1,10 +1,60 @@
|
||||
import {
|
||||
getPhoneEnvBool
|
||||
} from '@/common/common'
|
||||
//#ifdef APP-PLUS
|
||||
import {
|
||||
startCameraPreview,
|
||||
stopCameraPreview
|
||||
} from '@/uni_modules/ty-camera';
|
||||
// #endif
|
||||
const isIOS = getPhoneEnvBool('IOS')
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const dpr = systemInfo.pixelRatio; // 设备像素比(通常为2/3)
|
||||
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
export default function useCamera(aaas = null, options = {}) {
|
||||
|
||||
return {
|
||||
ws,
|
||||
close,
|
||||
initConnection,
|
||||
send,
|
||||
isRecording
|
||||
const cameraRef = ref(null);
|
||||
|
||||
const cameraInit = (obj) => {
|
||||
if (isIOS) {
|
||||
// ios的推流初始化
|
||||
cameraRef.value.init(obj);
|
||||
} else {
|
||||
// 安卓
|
||||
cameraRef.value = obj
|
||||
}
|
||||
}
|
||||
const cameraStart = () => {
|
||||
if (isIOS) {
|
||||
cameraRef.value.startPreview();
|
||||
} else {
|
||||
//#ifdef APP-PLUS
|
||||
startCameraPreview({
|
||||
success: (base64) => {
|
||||
|
||||
cameraRef.value.drawImage(base64, 0, 0, 240 * dpr, 360 * dpr, 0, 0, 240, 360);
|
||||
cameraRef.value.draw(true);
|
||||
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
const cameraClose = () => {
|
||||
if (isIOS) {
|
||||
cameraRef.value.startPreview();
|
||||
} else {
|
||||
stopCameraPreview()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cameraRef,
|
||||
isIOS,
|
||||
cameraInit,
|
||||
cameraStart,
|
||||
cameraClose
|
||||
};
|
||||
}
|
||||
+48
-29
@@ -19,11 +19,28 @@
|
||||
{{ dataInfo.sceneDesc }}
|
||||
</view>
|
||||
<view class="close-btn" @click="closeTalking"></view>
|
||||
<!-- <canvas canvas-id="firstCanvas" style="width: 176px; height: 144px;"></canvas> -->
|
||||
|
||||
<view
|
||||
v-if="isVideo && !isIOS"
|
||||
style="
|
||||
width: 240rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 150rpx;
|
||||
"
|
||||
>
|
||||
<canvas
|
||||
canvas-id="firstCanvas"
|
||||
style="width: 240rpx; height: 360rpx; position: absolute; left: 0; top: 0"
|
||||
></canvas>
|
||||
</view>
|
||||
</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="cameraRef" v-if="isVideo && isIOS"></talk-camera>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -35,12 +52,15 @@
|
||||
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';
|
||||
import useCamera from './js/useCamera';
|
||||
|
||||
const talkMicrophoneRef = ref(null);
|
||||
const talkSpeakerRef = ref(null);
|
||||
const talkCameraRef = ref(null);
|
||||
|
||||
const { initConnection, close, send, isRecording } = useWebSocket();
|
||||
const { cameraRef, isIOS, cameraInit, cameraStart, cameraClose } = useCamera();
|
||||
|
||||
const isVideo = ref(false);
|
||||
const isVideoStyle = computed(() => dataInfo.traInterTyp === '03');
|
||||
// const context = uni.createCanvasContext('firstCanvas');
|
||||
@@ -74,16 +94,17 @@
|
||||
isVideo.value && talkCameraRef.value.switchCamera();
|
||||
};
|
||||
const clearAutomaticEndTimeTimer = () => {
|
||||
if (automaticEndTimeTimer){
|
||||
clearTimeout(automaticEndTimeTimer)
|
||||
automaticEndTimeTimer = null
|
||||
if (automaticEndTimeTimer) {
|
||||
clearTimeout(automaticEndTimeTimer);
|
||||
automaticEndTimeTimer = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭销毁流程,逐层清理
|
||||
const destroy = (type = 0) => {
|
||||
clearAutomaticEndTimeTimer()
|
||||
clearAutomaticEndTimeTimer();
|
||||
if (type <= 0) {
|
||||
isVideo.value && talkCameraRef.value.stopPreview();
|
||||
isVideo.value && cameraClose();
|
||||
talkMicrophoneRef.value.stop();
|
||||
talkSpeakerRef.value.close();
|
||||
}
|
||||
@@ -165,12 +186,12 @@
|
||||
// 当ws连接成功时候
|
||||
console.log('连接成功开始' + dataInfo.traInterTypDesc);
|
||||
isRecording.value = true;
|
||||
isVideo.value && talkCameraRef.value.startPreview();
|
||||
isVideo.value && cameraStart();
|
||||
if (timer) clearInterval(timer); // 清除旧的定时器,避免多个定时器叠加
|
||||
timer = setInterval(updateRemaining, 400);
|
||||
// 立即执行一次
|
||||
updateRemaining();
|
||||
startAutomaticEndTimeTimer()
|
||||
startAutomaticEndTimeTimer();
|
||||
talkMicrophoneRef.value.start({
|
||||
sampleRate: 16000,
|
||||
frameSize: 1024,
|
||||
@@ -189,19 +210,19 @@
|
||||
lock: id
|
||||
})
|
||||
);
|
||||
startAutomaticEndTimeTimer()
|
||||
startAutomaticEndTimeTimer();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 启动没人说话定时器
|
||||
const startAutomaticEndTimeTimer = () => {
|
||||
// 打开一个计时器,时间到了就中断
|
||||
|
||||
|
||||
automaticEndTimeTimer = setTimeout(() => {
|
||||
destroy();
|
||||
}, automaticEndTime);
|
||||
}
|
||||
};
|
||||
const onError = (error) => {
|
||||
// websocket错误处理
|
||||
console.log('websocket错误处理', error.code);
|
||||
@@ -266,33 +287,31 @@
|
||||
onStartRecord: onStartRecord,
|
||||
playPcmCallback: (res) => talkSpeakerRef.value.playPCM(res),
|
||||
setPlayNumber: (id) => {
|
||||
clearAutomaticEndTimeTimer()
|
||||
clearAutomaticEndTimeTimer();
|
||||
console.log('新一句话开始');
|
||||
talkSpeakerRef.value.setPlayNumber(id)
|
||||
talkSpeakerRef.value.setPlayNumber(id);
|
||||
},
|
||||
onError,
|
||||
onClose
|
||||
});
|
||||
});
|
||||
|
||||
onReady(() => {
|
||||
if (isVideo.value) {
|
||||
// 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);
|
||||
});
|
||||
if (isIOS) {
|
||||
const instance = getCurrentInstance();
|
||||
const componentProxy = instance.ctx;
|
||||
cameraInit(componentProxy);
|
||||
} else {
|
||||
const context = uni.createCanvasContext('firstCanvas');
|
||||
cameraInit(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
console.log('组件卸载');
|
||||
if (timer) clearInterval(timer);
|
||||
clearAutomaticEndTimeTimer()
|
||||
clearAutomaticEndTimeTimer();
|
||||
});
|
||||
onBackPress((res) => {
|
||||
if (res.from === 'backbutton') {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import CameraManager from "android.hardware.camera2.CameraManager";
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
import CameraDevice from "android.hardware.camera2.CameraDevice";
|
||||
import Context from "android.content.Context";
|
||||
import Handler from "android.os.Handler";
|
||||
import ImageFormat from "android.graphics.ImageFormat";
|
||||
/**
|
||||
* 定义摄像头方向的字符串字面量类型(核心:限定只能传 'BACK' 或 'FRONT')
|
||||
*/
|
||||
export type CameraFacingType = 'BACK' | 'FRONT';
|
||||
|
||||
/**
|
||||
* 摄像头操作辅助类
|
||||
* 封装摄像头ID获取、旋转角度计算等核心功能
|
||||
*/
|
||||
export default class CameraHelper {
|
||||
private context : Context;
|
||||
private cameraManager : CameraManager;
|
||||
private currentCameraId : string | null = null;
|
||||
private totalRotation : number = 0; // 摄像头最终旋转角度
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param cameraManager 摄像头管理器实例
|
||||
*/
|
||||
constructor(context : Context) {
|
||||
this.context = context;
|
||||
this.cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager;
|
||||
if (this.cameraManager == null) {
|
||||
throw new Error("CameraHelper: 获取CameraManager失败,请检查摄像头权限或设备支持性");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心新增】根据指定方向获取摄像头ID
|
||||
* @param facing 摄像头方向(前置/后置)
|
||||
* @returns 对应方向的摄像头ID | null(无对应摄像头时返回null)
|
||||
*/
|
||||
public getCameraIdByFacing(facing : CameraFacingType) : string {
|
||||
|
||||
try {
|
||||
const cameraIds = this.cameraManager.getCameraIdList();
|
||||
if (cameraIds.size === 0) {
|
||||
console.error("CameraHelper: 无可用摄像头ID");
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// 遍历查找指定方向的摄像头
|
||||
let targetCameraId : string | null = null;
|
||||
for (const id of cameraIds) {
|
||||
const characteristics = this.cameraManager.getCameraCharacteristics(id);
|
||||
const lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
// 判空+条件匹配一步到位
|
||||
if (lensFacing !== null &&
|
||||
lensFacing === (facing === 'BACK' ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT)) {
|
||||
targetCameraId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetCameraId === null) {
|
||||
throw new Error();
|
||||
}
|
||||
console.log(`CameraHelper: 成功获取${facing === 'BACK' ? '后置' : '前置'}摄像头ID:${targetCameraId}`);
|
||||
return targetCameraId;
|
||||
} catch (e) {
|
||||
console.error(`CameraHelper: 获取${facing === 'BACK' ? '后置' : '前置'}摄像头ID异常:`, e.message);
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心封装】打开指定方向的摄像头
|
||||
* @param facing 摄像头方向(前置/后置)
|
||||
* @param callback 摄像头状态回调(定制化部分)
|
||||
* @param handler 处理回调的Handler(null表示主线程)
|
||||
*/
|
||||
public openCameraByFacing(
|
||||
facing : CameraFacingType,
|
||||
callback : CameraDevice.StateCallback,
|
||||
handler : Handler | null = null
|
||||
) : void {
|
||||
// 1. 先获取对应方向的摄像头ID
|
||||
let cameraId : string;
|
||||
try {
|
||||
cameraId = this.getCameraIdByFacing(facing);
|
||||
} catch (e) {
|
||||
throw new Error()
|
||||
}
|
||||
const characteristics = this.cameraManager.getCameraCharacteristics(cameraId);
|
||||
const streamConfigurationMap = characteristics.get(
|
||||
android.hardware.camera2.CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
|
||||
);
|
||||
if (streamConfigurationMap !== null) {
|
||||
const supportedSizes = streamConfigurationMap.getOutputSizes(ImageFormat.YUV_420_888);
|
||||
const resolutions : Array<{ width : number, height : number }> = [];
|
||||
|
||||
for (const size of supportedSizes) {
|
||||
console.log({
|
||||
width: size.getWidth(),
|
||||
height: size.getHeight()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.cameraManager.openCamera(
|
||||
cameraId,
|
||||
callback,
|
||||
handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭摄像头(封装资源释放)
|
||||
*/
|
||||
public closeCamera() : void {
|
||||
// if (this.cameraDevice) {
|
||||
// try {
|
||||
// this.cameraDevice.close();
|
||||
// } catch (e) {
|
||||
// console.error("CameraHelper: 关闭摄像头异常:", e.message);
|
||||
// } finally {
|
||||
// this.cameraDevice = null;
|
||||
// this.currentCameraId = null;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前选中的摄像头ID
|
||||
* @returns 摄像头ID | null
|
||||
*/
|
||||
public getCurrentCameraId() : string | null {
|
||||
return this.currentCameraId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摄像头最终旋转角度
|
||||
* @returns 旋转角度(0-359)
|
||||
*/
|
||||
public getTotalRotation() : number {
|
||||
return this.totalRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从摄像头ID列表中筛选后置摄像头ID
|
||||
* @param cameraIds 摄像头ID数组
|
||||
* @returns 后置摄像头ID | null
|
||||
*/
|
||||
private getBackCameraId(cameraIds : Array<string>) : string | null {
|
||||
for (const id of cameraIds) {
|
||||
const characteristics = this.cameraManager.getCameraCharacteristics(id);
|
||||
const facing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
// LENS_FACING_BACK = 0 后置,LENS_FACING_FRONT = 1 前置
|
||||
if (facing !== null && facing === CameraCharacteristics.LENS_FACING_BACK) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算摄像头需要的旋转角度
|
||||
* @param cameraId 摄像头ID
|
||||
*/
|
||||
private calculateCameraRotation(cameraId : string) : void {
|
||||
// const activity = UTSAndroid.getUniActivity();
|
||||
// if (!activity) {
|
||||
// console.warn("CameraHelper: 获取Activity失败,无法计算旋转角度");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 获取屏幕旋转角度
|
||||
// const rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||
// // 获取摄像头传感器方向
|
||||
// const characteristics = this.cameraManager.getCameraCharacteristics(cameraId);
|
||||
// const sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) as number;
|
||||
|
||||
// // 计算总旋转角度
|
||||
// switch (rotation) {
|
||||
// case android.view.Surface.ROTATION_0:
|
||||
// this.totalRotation = sensorOrientation;
|
||||
// break;
|
||||
// case android.view.Surface.ROTATION_90:
|
||||
// this.totalRotation = sensorOrientation + 90;
|
||||
// break;
|
||||
// case android.view.Surface.ROTATION_180:
|
||||
// this.totalRotation = sensorOrientation + 180;
|
||||
// break;
|
||||
// case android.view.Surface.ROTATION_270:
|
||||
// this.totalRotation = sensorOrientation + 270;
|
||||
// break;
|
||||
// default:
|
||||
// this.totalRotation = sensorOrientation;
|
||||
// break;
|
||||
// }
|
||||
|
||||
// // 确保角度在0-359范围内
|
||||
// this.totalRotation %= 360;
|
||||
|
||||
// // 前置摄像头特殊处理(可在这里扩展镜像逻辑)
|
||||
// const lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
// if (lensFacing === CameraCharacteristics.LENS_FACING_FRONT) {
|
||||
// console.log(`CameraHelper: 前置摄像头,旋转角度:${this.totalRotation}(需水平翻转)`);
|
||||
// } else {
|
||||
// console.log(`CameraHelper: 后置摄像头,旋转角度:${this.totalRotation}`);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放摄像头相关资源
|
||||
*/
|
||||
public release() : void {
|
||||
this.currentCameraId = null;
|
||||
this.totalRotation = 0;
|
||||
console.log("CameraHelper: 摄像头辅助类资源已释放");
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 使用示例 ==================
|
||||
// // 1. 创建CameraHelper实例
|
||||
// const cameraManager = getCameraManager(); // 替换为你的CameraManager获取逻辑
|
||||
// const cameraHelper = new CameraHelper(cameraManager);
|
||||
|
||||
// // 2. 获取有效摄像头ID
|
||||
// const cameraId = cameraHelper.getValidCameraId();
|
||||
// if (cameraId) {
|
||||
// console.log("当前选中摄像头ID:", cameraId);
|
||||
// console.log("摄像头旋转角度:", cameraHelper.getTotalRotation());
|
||||
// console.log("是否是前置摄像头:", cameraHelper.isFrontCamera());
|
||||
// }
|
||||
|
||||
// // 3. 释放资源(如页面销毁时)
|
||||
// // cameraHelper.release();
|
||||
@@ -1,347 +0,0 @@
|
||||
/**
|
||||
* 引用 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);
|
||||
}
|
||||
}
|
||||
@@ -1,284 +1,171 @@
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
* 引用 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 CameraManager from "android.hardware.camera2.CameraManager";
|
||||
|
||||
import CameraCharacteristics from "android.hardware.camera2.CameraCharacteristics";
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { MyApiOptions } from '../interface.uts';
|
||||
import CameraHelper from './CameraHelper.uts'
|
||||
|
||||
|
||||
import Base64 from "android.util.Base64";
|
||||
// 定义分辨率
|
||||
const width : Int = 176; // 降低分辨率提升性能
|
||||
const height : Int = 144;
|
||||
const width : Int = 720;
|
||||
const height : Int = 480;
|
||||
|
||||
// 全局变量
|
||||
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 构造函数
|
||||
let handlerThread : HandlerThread | null = null; // 新增:保存线程引用
|
||||
let imageReader : ImageReader | null = null; // 新增:保存ImageReader引用
|
||||
let captureSession : CameraCaptureSession | null = null; // 新增:保存会话引用
|
||||
|
||||
// 打开相机设备的全局变量
|
||||
let cameraDevice : CameraDevice | null = null;
|
||||
|
||||
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();
|
||||
function yuvToJpegBase64(image : android.media.Image) : string {
|
||||
const width = image.getWidth();
|
||||
const height = image.getHeight();
|
||||
// console.log('width', width, height);
|
||||
// 仅处理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. 提取关键参数(仅计算一次,无冗余)
|
||||
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);
|
||||
// 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();
|
||||
|
||||
// 3. 批量复制 Y 通道(原生 API,无循环)
|
||||
yBuffer.get(yuvByteArray, 0, yLength);
|
||||
// 2. 读取所有通道的ByteBuffer并转换为ByteArray
|
||||
const yBuffer = yPlane.getBuffer();
|
||||
const uBuffer = uPlane.getBuffer();
|
||||
const vBuffer = vPlane.getBuffer();
|
||||
|
||||
// ========== 核心优化:用原生 arraycopy 替代 U/V 双循环 ==========
|
||||
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);
|
||||
|
||||
// 4. 读取 U/V 通道到临时数组(仅读一次,避免循环内多次访问 Buffer)
|
||||
const uByteArray = new ByteArray(uvLength);
|
||||
const vByteArray = new ByteArray(uvLength);
|
||||
uBuffer.get(uByteArray, 0, uvLength);
|
||||
vBuffer.get(vByteArray, 0, uvLength);
|
||||
// 3. 创建标准NV21数组(YUV_420_888转标准NV21)
|
||||
const nv21 = new ByteArray(width * height * 3 / 2); // NV21固定长度
|
||||
let nv21Index : Int = 0;
|
||||
|
||||
// 预计算固定值(避免循环内重复运算)
|
||||
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 起始索引
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 核心优化:精简单循环替代双循环(快 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;
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
// // 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);
|
||||
// ========== 改用YuvImage直接转JPEG(比RenderScript更稳定)==========
|
||||
try {
|
||||
// 1. 用YuvImage处理标准NV21数据(避免RenderScript格式兼容问题)
|
||||
const yuvImage = new android.graphics.YuvImage(
|
||||
nv21,
|
||||
android.graphics.ImageFormat.NV21,
|
||||
width,
|
||||
height,
|
||||
null
|
||||
);
|
||||
|
||||
// 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
|
||||
// );
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
|
||||
// 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 '';
|
||||
// 3. 编码为Base64
|
||||
const jpegBytes = outputStream.toByteArray();
|
||||
const srcBitmap : Bitmap = android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size);
|
||||
const matrix = new android.graphics.Matrix();
|
||||
matrix.postRotate((-90 as Number).toFloat()); // 旋转角度:-90/90/180,根据需求调整
|
||||
const outputStream2 = new java.io.ByteArrayOutputStream();
|
||||
const rotatedBitmap = android.graphics.Bitmap.createBitmap(
|
||||
srcBitmap,
|
||||
0,
|
||||
0,
|
||||
srcBitmap.getWidth(),
|
||||
srcBitmap.getHeight(),
|
||||
matrix,
|
||||
true // 过滤,保证旋转后图片清晰
|
||||
);
|
||||
rotatedBitmap.compress(
|
||||
android.graphics.Bitmap.CompressFormat.JPEG,
|
||||
80,
|
||||
outputStream2
|
||||
);
|
||||
|
||||
const BitmapBytes = outputStream2.toByteArray();
|
||||
const base64Raw = android.util.Base64.encodeToString(
|
||||
BitmapBytes,
|
||||
android.util.Base64.DEFAULT
|
||||
);
|
||||
const base64Jpeg = `data:image/jpeg;base64,${base64Raw}`;
|
||||
|
||||
// 释放资源
|
||||
outputStream.close();
|
||||
return base64Jpeg;
|
||||
} catch (e) {
|
||||
console.error("YUV转JPEG/Base64失败:", e);
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
// callback : (data : any) => void
|
||||
/**
|
||||
* 启动摄像头预览并获取实时视频帧(YUV420)
|
||||
* @param callback - 每获取到一帧调用一次,参数为 Uint8Array(YUV 数据)
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
export function startCameraPreview(options : MyApiOptions) : void {
|
||||
|
||||
const context = UTSAndroid.getAppContext();
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
|
||||
@@ -286,44 +173,42 @@ export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
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]; // 后置摄像头
|
||||
handlerThread = new HandlerThread("CameraBackground", -8); // THREAD_PRIORITY_URGENT_VIDEO = -8
|
||||
handlerThread!!.start();
|
||||
backgroundHandler = new Handler(handlerThread!!.getLooper());
|
||||
|
||||
const cameraHelper = new CameraHelper(context); // 相机相关操作类
|
||||
const totalRotation = cameraHelper.getTotalRotation()
|
||||
|
||||
try {
|
||||
console.log('width, height', width, height);
|
||||
// 创建 ImageReader
|
||||
let imageReader : ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 8);
|
||||
imageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 8);
|
||||
const localImageReader = imageReader; // 局部常量暂存,避免可变问题
|
||||
if (localImageReader == null) {
|
||||
console.error("ImageReader 创建失败,无法设置监听器");
|
||||
return;
|
||||
}
|
||||
// 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))
|
||||
// console.log("正确设置监听器");
|
||||
options.success?.(yuvToJpegBase64(imageProxy));
|
||||
// callback(yuvToJpegBase64(imageProxy))
|
||||
// 4. 必须关闭图像释放缓冲区
|
||||
imageProxy.close();
|
||||
|
||||
// 5. 显式return(解决UTS的return校验)
|
||||
return;
|
||||
}
|
||||
|
||||
imageReader.setOnImageAvailableListener(
|
||||
// 核心修复:直接传符合签名的函数(去掉new和对象包裹)
|
||||
localImageReader.setOnImageAvailableListener(
|
||||
imageAvailableListener,
|
||||
// 第二个参数:Handler,无则传null(符合UTS的null类型规范)
|
||||
backgroundHandler
|
||||
);
|
||||
|
||||
// 定义全局变量存储相机设备
|
||||
let cameraDevice : android.hardware.camera2.CameraDevice | null = null;
|
||||
|
||||
/**
|
||||
* 自定义相机状态回调类
|
||||
* 核心修复:1. 显式调用父类构造函数 2. 完全匹配 onError 方法签名
|
||||
@@ -341,7 +226,8 @@ export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
|
||||
override onOpened(device : android.hardware.camera2.CameraDevice) : void {
|
||||
console.log("相机打开成功");
|
||||
const cameraDevice = device;
|
||||
cameraDevice = device // 为了关闭,保存对象
|
||||
const localCameraDevice = device; // 局部常量暂存,避免可变问题
|
||||
const surface = imageReader!!.getSurface();
|
||||
const surfaces = [surface];
|
||||
|
||||
@@ -362,13 +248,16 @@ export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
*/
|
||||
override onConfigured(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.log("采集会话配置成功,启动持续帧采集");
|
||||
|
||||
captureSession = session; // 保存会话到全局
|
||||
// 创建预览采集请求
|
||||
const requestBuilder = this.localCameraDevice.createCaptureRequest(
|
||||
android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
|
||||
);
|
||||
requestBuilder.addTarget(this.surface);
|
||||
|
||||
|
||||
// 告诉相机设备:你采集到的每一帧图像数据,都要输出到这个 Surface 目标上
|
||||
requestBuilder.addTarget(this.surface);
|
||||
requestBuilder.set(CaptureRequest.JPEG_ORIENTATION, 270);
|
||||
// 启动持续采集(真正触发相机输出数据)
|
||||
session.setRepeatingRequest(
|
||||
requestBuilder.build(),
|
||||
@@ -382,12 +271,12 @@ export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
*/
|
||||
override onConfigureFailed(session : android.hardware.camera2.CameraCaptureSession) : void {
|
||||
console.error("采集会话配置失败");
|
||||
|
||||
captureSession = null;
|
||||
}
|
||||
}
|
||||
const sessionCallback = new CustomCaptureSessionCallback(cameraDevice, surface) as CameraCaptureSession.StateCallback;
|
||||
const sessionCallback = new CustomCaptureSessionCallback(localCameraDevice, surface) as CameraCaptureSession.StateCallback;
|
||||
// 创建采集会话
|
||||
cameraDevice.createCaptureSession(
|
||||
localCameraDevice.createCaptureSession(
|
||||
surfaces,
|
||||
sessionCallback,
|
||||
backgroundHandler
|
||||
@@ -426,24 +315,170 @@ export function startCameraPreview(callback : (data : any) => void) : void {
|
||||
* 辅助方法:释放相机资源
|
||||
*/
|
||||
private releaseCameraResources() : void {
|
||||
// if (cameraDevice != null && !cameraDevice.isClosed()) {
|
||||
// cameraDevice.close();
|
||||
// cameraDevice = null;
|
||||
// console.log("相机资源已释放");
|
||||
// }
|
||||
if (cameraDevice != null) {
|
||||
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表示用主线程,符合可空要求)
|
||||
);
|
||||
cameraHelper.openCameraByFacing(
|
||||
'FRONT', // string(相机方向)
|
||||
cameraStateCallback, // StateCallback(回调,必传非null)
|
||||
backgroundHandler// Handler(传null表示用主线程,符合可空要求)
|
||||
)
|
||||
|
||||
// cameraManager.openCamera(
|
||||
// cameraId, // string(相机ID,必传非null)
|
||||
// cameraStateCallback, // StateCallback(回调,必传非null)
|
||||
// backgroundHandler // Handler(传null表示用主线程,符合可空要求)
|
||||
// );
|
||||
|
||||
} catch (e) {
|
||||
console.error('启动摄像头失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止摄像头预览并释放所有相关资源
|
||||
* 核心逻辑:按「会话→相机→ImageReader→线程」的顺序释放,避免资源泄漏
|
||||
*/
|
||||
export function stopCameraPreview() : void {
|
||||
try {
|
||||
console.log("开始释放摄像头资源");
|
||||
|
||||
// 1. 停止采集会话(最优先,避免持续回调)
|
||||
if (captureSession != null) {
|
||||
try {
|
||||
captureSession!!.stopRepeating(); // 停止重复采集
|
||||
captureSession!!.abortCaptures(); // 终止所有待处理的采集请求
|
||||
captureSession!!.close(); // 关闭会话
|
||||
console.log("采集会话已释放");
|
||||
} catch (e) {
|
||||
console.warn("释放采集会话时出现异常:", e);
|
||||
} finally {
|
||||
captureSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 关闭相机设备(必须在会话关闭后)
|
||||
if (cameraDevice != null) {
|
||||
try {
|
||||
cameraDevice!!.close();
|
||||
console.log("相机设备已关闭");
|
||||
} catch (e) {
|
||||
console.warn("关闭相机设备时出现异常:", e);
|
||||
} finally {
|
||||
cameraDevice = null; // 关闭后置null,避免重复操作
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 释放ImageReader(避免缓冲区泄漏)
|
||||
if (imageReader != null) {
|
||||
try {
|
||||
imageReader!!.close();
|
||||
console.log("ImageReader已释放");
|
||||
} catch (e) {
|
||||
console.warn("释放ImageReader时出现异常:", e);
|
||||
} finally {
|
||||
imageReader = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 停止后台线程(最后释放,避免线程未处理完回调)
|
||||
if (handlerThread != null) {
|
||||
try {
|
||||
// 先移除所有待处理的消息
|
||||
if (backgroundHandler != null) {
|
||||
console.log('先移除所有待处理的消息');
|
||||
backgroundHandler!!.removeCallbacksAndMessages(null);
|
||||
backgroundHandler = null;
|
||||
}
|
||||
// 退出并等待线程结束
|
||||
handlerThread!!.quitSafely(); // 安全退出(处理完现有消息)
|
||||
handlerThread!!.join(); // 等待线程终止
|
||||
console.log("后台线程已停止");
|
||||
} catch (e) {
|
||||
console.warn("停止后台线程时出现异常:", e);
|
||||
} finally {
|
||||
handlerThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("摄像头资源释放完成");
|
||||
} catch (e) {
|
||||
console.error("释放摄像头资源时发生全局异常:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 核心函数:获取可用的摄像头管理器和摄像头ID
|
||||
* 按你的规则:优先选择cameraIds[1](后置摄像头)
|
||||
* @param context 应用上下文
|
||||
* @returns { cameraManager: CameraManager, cameraId: string } | null 成功返回有效对象,失败返回null
|
||||
*/
|
||||
function getValidCameraInfo(cameraManager : CameraManager) : string | null {
|
||||
const activity = UTSAndroid.getUniActivity();
|
||||
const rotation = activity!!.getWindowManager().getDefaultDisplay().getRotation();
|
||||
|
||||
try {
|
||||
// 2. 获取摄像头ID列表
|
||||
const cameraIds = cameraManager.getCameraIdList();
|
||||
if (cameraIds.size == 0) {
|
||||
console.error("无可用摄像头ID");
|
||||
return null;
|
||||
}
|
||||
let cameraId = "";
|
||||
let cameraFacing : number | null = null;
|
||||
for (const id of cameraIds) {
|
||||
const characteristics = cameraManager.getCameraCharacteristics(id);
|
||||
const facing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
// 优先选择后置摄像头(LENS_FACING_BACK = 0,前置LENS_FACING_FRONT=1)
|
||||
if (facing != null && facing === CameraCharacteristics.LENS_FACING_FRONT) {
|
||||
cameraId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果没有后置,选第一个可用摄像头
|
||||
if (cameraId === "" && cameraIds.size > 0) {
|
||||
cameraId = cameraIds[0];
|
||||
}
|
||||
if (cameraId === "") {
|
||||
console.error("无可用摄像头ID");
|
||||
return null;
|
||||
}
|
||||
console.log('cameraFacing', cameraFacing);
|
||||
const characteristics = cameraManager.getCameraCharacteristics(cameraId);
|
||||
const sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) as Int;
|
||||
// // 计算最终需要设置的旋转角度
|
||||
let totalRotation : Int = 0;
|
||||
if (rotation === android.view.Surface.ROTATION_0) {
|
||||
totalRotation = sensorOrientation;
|
||||
} else if (rotation === android.view.Surface.ROTATION_90) {
|
||||
totalRotation = sensorOrientation + 90;
|
||||
} else if (rotation === android.view.Surface.ROTATION_180) {
|
||||
totalRotation = sensorOrientation + 180;
|
||||
} else if (rotation === android.view.Surface.ROTATION_270) {
|
||||
totalRotation = sensorOrientation + 270;
|
||||
}
|
||||
totalRotation %= 360;
|
||||
// 前置摄像头需要额外镜像(否则画面左右相反)
|
||||
const lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
if (lensFacing === Integer.valueOf(CameraCharacteristics.LENS_FACING_FRONT)) {
|
||||
// 前置摄像头旋转后需要水平翻转
|
||||
console.log(CaptureRequest.JPEG_ORIENTATION, totalRotation);
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
return cameraId;
|
||||
} catch (e) {
|
||||
console.error("获取摄像头信息异常:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* 引用 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;
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
/**
|
||||
* 引用 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);
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
/**
|
||||
* 引用 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);
|
||||
}
|
||||
}
|
||||
@@ -8,21 +8,12 @@
|
||||
*/
|
||||
export type MyApiOptions = {
|
||||
paramA : boolean
|
||||
success ?: (res : MyApiResult) => void
|
||||
success ?: (res : string) => void
|
||||
fail ?: (res : MyApiFail) => void
|
||||
complete ?: (res : any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数返回结果
|
||||
* 可以是void, 基本数据类型,自定义type, 或者其他类型。
|
||||
* [可选实现]
|
||||
*/
|
||||
export type MyApiResult = {
|
||||
fieldA : number,
|
||||
fieldB : boolean,
|
||||
fieldC : string
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
@@ -41,5 +32,3 @@ export interface MyApiFail extends IUniError {
|
||||
/* 异步函数定义 */
|
||||
export type MyApi = (options : MyApiOptions) => void
|
||||
|
||||
/* 同步函数定义 */
|
||||
export type MyApiSync = (paramA : boolean) => MyApiResult
|
||||
Reference in New Issue
Block a user