Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop

This commit is contained in:
杨航
2025-12-22 15:22:14 +08:00
16 changed files with 1927 additions and 60 deletions
+4 -1
View File
@@ -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",
-1
View File
@@ -25,7 +25,6 @@ const DEFAULT_AUTH_PARAMS = {
// device_id: uni.getSystemInfoSync().deviceId || '', // 设备ID(补充)
// timestamp: Date.now() // 时间戳(防篡改)
};
// const wsUrl = 'ws://127.0.0.1:8000/ws/audio'
const url = '/trapractice/voiceCallSocket/voiceCall'
const _baseUrl = get_base_url(url).split('http').join('ws')
export default function useWebSocket(aaas = null, options = {}) {
+60 -58
View File
@@ -19,52 +19,31 @@
{{ 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>
import {
ref,
reactive,
onMounted,
onUnmounted,
computed,
getCurrentInstance,
nextTick,
toRaw
} from 'vue';
import {
onShow,
onLoad,
onUnload,
onBackPress,
onReady
} from '@dcloudio/uni-app';
import {
ProtocolCodec,
MessageType
} from './js/ProtocolCodec';
import { ref, reactive, onMounted, onUnmounted, computed, getCurrentInstance, nextTick, toRaw } from 'vue';
import { onShow, onLoad, onUnload, onBackPress, onReady } from '@dcloudio/uni-app';
import { ProtocolCodec, MessageType } from './js/ProtocolCodec';
import common from '@/common/common';
import talkMicrophone from './components/talk-microphone.vue';
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);
const {
initConnection,
close,
send,
isRecording
} = useWebSocket();
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: '',
@@ -92,12 +71,19 @@
});
};
const test = () => {
isVideo.value && talkCameraRef.value.switchCamera();
// isVideo.value && talkCameraRef.value.switchCamera();
};
const clearAutomaticEndTimeTimer = () => {
if (automaticEndTimeTimer){
clearTimeout(automaticEndTimeTimer)
automaticEndTimeTimer = null
}
}
// 关闭销毁流程,逐层清理
const destroy = (type = 0) => {
clearAutomaticEndTimeTimer()
if (type <= 0) {
isVideo.value && talkCameraRef.value.stopPreview();
// isVideo.value && talkCameraRef.value.stopPreview();
talkMicrophoneRef.value.stop();
talkSpeakerRef.value.close();
}
@@ -110,16 +96,16 @@
if (dataInfo.execId) {
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.traType +
'&isPreview=' +
(dataInfo.preview ? '1' : '')
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.traType +
'&isPreview=' +
(dataInfo.preview ? '1' : '')
);
} else {
common.navigateBack()
common.navigateBack();
}
}, 300);
});
@@ -137,15 +123,15 @@
const onCurrentDecibels = (res) => {
console.log('当前分贝', res);
};
// 麦克风录音结束
const microphoneNoPermission= () =>{
const microphoneNoPermission = () => {
common.show('', '该功能需要授予麦克风权限', false).then((res) => {
if (res) {
destroy(); // 走关闭销毁流程
}
});
}
};
// 定时器实例
let timer = null;
@@ -160,6 +146,8 @@
};
const targetTimestamp = ref(0);
let automaticEndTime = 1200000; // 不说话就中断的时间
let automaticEndTimeTimer = null; // 不说话就中断的定时器
// 计算属性:实时格式化剩余时间
const formattedTime = ref('00:00');
// 3. 更新剩余时间的核心方法
@@ -171,12 +159,13 @@
};
const onStartRecord = (body) => {
automaticEndTime = body.automaticEndTime ?? 1200000;
dataInfo.execId = body.execId;
targetTimestamp.value = Date.now();
// 当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);
// 立即执行一次
@@ -186,10 +175,7 @@
frameSize: 1024,
gain: 1.0,
// 音频数据的帧接口回调,直接转码后接入到websocket的发送函数
onFrameRecorded: ({
isLastFrame,
frameBuffer
}) => send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
onFrameRecorded: ({ isLastFrame, frameBuffer }) => send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
});
};
const playOver = (id) => {
@@ -202,6 +188,10 @@
lock: id
})
);
// 打开一个计时器,时间到了就中断
automaticEndTimeTimer = setTimeout(() => {
destroy();
}, automaticEndTime);
}, 300);
}
};
@@ -268,23 +258,34 @@
},
onStartRecord: onStartRecord,
playPcmCallback: (res) => talkSpeakerRef.value.playPCM(res),
setPlayNumber: (id) => talkSpeakerRef.value.setPlayNumber(id),
setPlayNumber: (id) => {
clearAutomaticEndTimeTimer()
console.log('新一句话开始');
talkSpeakerRef.value.setPlayNumber(id)
},
onError,
onClose
});
});
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(() => {
console.log('组件卸载');
if (timer) clearInterval(timer);
clearAutomaticEndTimeTimer()
});
onBackPress((res) => {
if (res.from === 'backbutton') {
@@ -296,17 +297,18 @@
<style scoped lang="scss">
.isVideo {
// background-color: red;
.role-info {
display: none;
}
&.show-box {
background: linear-gradient(120deg,
rgba(51, 51, 51, 0.2) 0%,
rgba(102, 102, 102, 0.2) 50%,
rgba(51, 51, 51, 0.2) 100%);
background: linear-gradient(
120deg,
rgba(51, 51, 51, 0.2) 0%,
rgba(102, 102, 102, 0.2) 50%,
rgba(51, 51, 51, 0.2) 100%
);
backdrop-filter: blur(0px);
}
}
+83
View File
@@ -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"
}
}
}
}
}
+7
View File
@@ -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 - 每获取到一帧调用一次,参数为 Uint8ArrayYUV 数据)
*/
@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 - 每获取到一帧调用一次,参数为 Uint8ArrayYUV 数据)
*/
@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 - 每获取到一帧调用一次,参数为 Uint8ArrayYUV 数据)
*/
@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();;
}
}
// 步骤6NV21转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 - 每获取到一帧调用一次,参数为 Uint8ArrayYUV 数据)
*/
@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) ?? "";
}
}