Files
SkinCamera/uni_modules/XF-cameraUTS/utssdk/app-android/camera.uts
T
2026-03-12 15:47:25 +08:00

474 lines
16 KiB
Plaintext

import ProcessCameraProvider from 'androidx.camera.lifecycle.ProcessCameraProvider'
import Preview from 'androidx.camera.core.Preview'
import MirrorMode from 'androidx.camera.core.MirrorMode'
import PreviewView from 'androidx.camera.view.PreviewView'
import Context from 'android.content.Context'
import FrameLayout from 'android.widget.FrameLayout'
import ViewGroup from 'android.view.ViewGroup'
import CameraSelector from 'androidx.camera.core.CameraSelector'
import ImageAnalysis from 'androidx.camera.core.ImageAnalysis'
import UseCaseGroup from 'androidx.camera.core.UseCaseGroup'
import ContextCompat from 'androidx.core.content.ContextCompat'
import LifecycleOwner from 'androidx.lifecycle.LifecycleOwner'
import ImageCapture from 'androidx.camera.core.ImageCapture'
import Size from 'android.util.Size'
import ImageCaptureException from 'androidx.camera.core.ImageCaptureException'
import File from 'java.io.File'
import ImageProxy from 'androidx.camera.core.ImageProxy';
import CameraControl from 'androidx.camera.core.CameraControl';
import VideoCapture from 'androidx.camera.video.VideoCapture'
import Recorder from 'androidx.camera.video.Recorder'
import Recording from 'androidx.camera.video.Recording'
import FileOutputOptions from 'androidx.camera.video.FileOutputOptions'
import Quality from 'androidx.camera.video.Quality'
import QualitySelector from 'androidx.camera.video.QualitySelector'
import VideoRecordEvent from 'androidx.camera.video.VideoRecordEvent'
// import Bitmap from 'android.graphics.Bitmap';
// import FileOutputStream from 'java.io.FileOutputStream';
// import MediaMetadataRetriever from 'android.media.MediaMetadataRetriever';
import ExecutorService from 'java.util.concurrent.ExecutorService'
import Executors from 'java.util.concurrent.Executors'
import ImageFormat from 'android.graphics.ImageFormat';
import {
TakePhotoOption,
GeneralCallbackResult,
TakePhotoSuccessCallbackResult,
CameraContextSetZoomOption,
SetZoomSuccessCallbackResult,
CameraContextStopRecordOption,
CameraContextStartRecordOption,
StartRecordTimeoutCallbackResult,
StopRecordSuccessCallbackResult,
OnCameraFrameCallback,
OnCameraFrameCallbackResult,
CameraFrameListenerStartOption,
StopOption,
CameraConfig,
FlashMode,
DevicePosition,
Resolution,
//FrameSize,
QualityType,
OnCameraFrameListenerOption
} from '../interface'
import Bitmap from "android.graphics.Bitmap";
import FileOutputStream from "java.io.FileOutputStream";
const FLASH_MODE_MAP = new Map<string, Int>([
['auto', ImageCapture.FLASH_MODE_AUTO],
['on', ImageCapture.FLASH_MODE_ON],
['torch', ImageCapture.FLASH_MODE_ON],
['off', ImageCapture.FLASH_MODE_OFF],
])
const DEVICE_POSITION_MAP = new Map<string, Int>([
['back', CameraSelector.LENS_FACING_BACK],
['front', CameraSelector.LENS_FACING_FRONT],
])
const RESOLUTION_MAP = new Map<string, Quality>([
['low', Quality.LOWEST],
['medium', Quality.HD],
['high', Quality.HIGHEST],
])
const QUALITY_MAP = new Map<string, Int>([
["high", ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY],
["low", ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY],
])
const MIRROR_MODE = new Map<boolean, Int>([
[true, MirrorMode.MIRROR_MODE_ON],
[false, MirrorMode.MIRROR_MODE_OFF],
])
export class CameraView extends FrameLayout {
private previewView : PreviewView;
public cameraProvider : ProcessCameraProvider | null = null;
public cameraControl : CameraControl | null = null;
private imageCapture : ImageCapture | null = null
private videoCapture : VideoCapture<Recorder> | null = null
private recording : Recording | null = null
private comp : UTSComponent<CameraView>;
private flashMode : FlashMode = 'auto'
private devicePosition : DevicePosition = 'back'
private resolution : Resolution = 'medium'
// private frameSize : FrameSize = 'medium'
private quality : QualityType = 'normal'
private videoFile : File | null = null
maxZoom : Float = (1).toFloat()
minZoom : Float = (1).toFloat()
private initialized : boolean = false
private isListening : boolean = false
private videoSelfieMirror : boolean | null = null
private cameraContextStopRecordOption : CameraContextStopRecordOption | null = null
private onCameraFrameCallback : OnCameraFrameCallback | null = null
private cameraExecutor : ExecutorService | null = null
private onCameraFrameListenerOption : OnCameraFrameListenerOption | null = null;
constructor(context : Context, comp : UTSComponent<CameraView>, config : CameraConfig = {} as CameraConfig) {
super(context);
this.comp = comp
this.flashMode = config.flash ?? 'auto'
this.devicePosition = config.devicePosition ?? 'back'
this.resolution = config.resolution ?? 'medium'
// this.frameSize = config.frameSize ?? 'medium'
this.previewView = new PreviewView(context);
let layoutParam = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(this.previewView, layoutParam);
this.setupCamera()
}
setupCamera() {
// this.checkSelfPermission().then(() => {
let cameraProviderFuture = ProcessCameraProvider.getInstance(UTSAndroid.getUniActivity()!)
cameraProviderFuture.addListener(() => {
try {
this.cameraProvider = cameraProviderFuture.get()
this.bindCamera();
} catch (e) {
let ret : Map<string, any> = new Map();
ret.set("errMsg", JSON.stringify(e))
this.comp.$emit('error', ret)
}
}, ContextCompat.getMainExecutor(UTSAndroid.getUniActivity()!))
// }).catch((err) => {
// let ret : Map<string, any> = new Map();
// ret.set("errMsg", err ?? '权限不足')
// this.comp.$emit('error', ret)
// })
}
bindCamera() {
let targetResolution = RESOLUTION_MAP.get(this.resolution) ?? Quality.HD;
let captureMode = QUALITY_MAP.get(this.quality) ?? ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
// 创建一个Preview对象,用于显示相机预览
let preview = new Preview.Builder()
.build();
preview.setSurfaceProvider(this.previewView.getSurfaceProvider());
// 创建一个ImageCapture对象,用于拍照
let imageCapture = new ImageCapture.Builder()
.setFlashMode(FLASH_MODE_MAP.get(this.flashMode)!)
.setCaptureMode(captureMode)
.build();
// 选择要使用的相机(后置或前置)
let cameraSelector = new CameraSelector.Builder()
.requireLensFacing(DEVICE_POSITION_MAP.get(this.devicePosition)!)
.build();
let recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(targetResolution))
let mirrorMode = this.videoSelfieMirror == null ? MirrorMode.MIRROR_MODE_ON_FRONT_ONLY : MIRROR_MODE.get(this.videoSelfieMirror!);
let videoCapture = new VideoCapture.Builder(recorder.build())
.setMirrorMode(mirrorMode!)
.build();
this.videoCapture = videoCapture
// 将参数应用到 VideoCapture
this.imageCapture = imageCapture;
this.cameraProvider!.unbindAll();
let imageAnalysis = ImageAnalysis.Builder()
// .setTargetResolution(new Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
let viewPort = this.previewView.getViewPort()!
let useCaseGroup = new UseCaseGroup.Builder()
.addUseCase(preview)
.addUseCase(imageAnalysis)
.addUseCase(imageCapture)
.addUseCase(videoCapture)
.setViewPort(viewPort)
.build();
this.cameraExecutor = Executors.newSingleThreadExecutor()
let _this = this
class Analyzer implements ImageAnalysis.Analyzer {
constructor() {
}
override analyze(image : ImageProxy) {
if (_this.isListening) {
let imageFormat = image.getFormat();
// 获取图像的宽度和高度
let width = image.width
let height = image.height
if (imageFormat == ImageFormat.YUV_420_888) {
_this.onCameraFrameCallback?.({
width,
height,
data: image
} as OnCameraFrameCallbackResult)
_this.onCameraFrameListenerOption?.success?.({
width,
height,
data: image
} as OnCameraFrameCallbackResult)
}
}
image.close();
}
override getDefaultTargetResolution() : Size {
// 返回默认的目标分辨率
return Size(640, 480)
}
}
imageAnalysis.setAnalyzer(this.cameraExecutor!, new Analyzer() as ImageAnalysis.Analyzer)
let camera = this.cameraProvider!.bindToLifecycle(
UTSAndroid.getUniActivity()! as LifecycleOwner,
cameraSelector,
useCaseGroup
// preview,
// imageCapture,
// videoCapture
);
this.cameraControl = camera.getCameraControl();
if (!this.initialized) {
this.initialized = true
let zoomState = camera.getCameraInfo().getZoomState().getValue();
this.maxZoom = zoomState?.getMaxZoomRatio() ?? (1).toFloat();
this.minZoom = zoomState?.getMinZoomRatio() ?? (1).toFloat();
let maxZoom = this.maxZoom
let ret : Map<string, any> = new Map();
ret.set("maxZoom", maxZoom)
this.comp.$emit('ready', ret)
}
}
unbindCamera() {
this.cameraProvider?.unbindAll()
this.cameraExecutor?.shutdown()
}
// checkSelfPermission() : Promise<boolean> {
// let permissionNeed = ["android.permission.CAMERA", "android.permission.RECORD_AUDIO", "android.permission.READ_EXTERNAL_STORAGE"]
// return new Promise((resoleve, _) => {
// resoleve(true);
// UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionNeed, function (allRight : boolean, list : string[]) {
// if (allRight) {
// resoleve(true);
// } else {
// reject(list);
// }
// }, function (_ : boolean, grantedList : string[]) {
// //grantedList
// reject(grantedList);
// })
// })
// }
setConfig(config : CameraConfig = {} as CameraConfig) {
this.flashMode = config.flash ?? 'auto'
this.devicePosition = config.devicePosition ?? 'back'
this.resolution = config.resolution ?? 'medium'
// this.frameSize = config.frameSize ?? 'medium'
}
setFlash(mode : FlashMode) {
this.flashMode = mode
if (FLASH_MODE_MAP.get(mode) == null || this.imageCapture == null) return
this.imageCapture!.setFlashMode(FLASH_MODE_MAP.get(mode)!)
}
switchCamera(position : DevicePosition) {
this.devicePosition = position
if (DEVICE_POSITION_MAP.get(position) == null || this.cameraProvider == null) return
this.unbindCamera()
this.bindCamera()
}
isResolutionSupported(context : Context, cameraSelector : CameraSelector, targetResolution : Size) : boolean {
return true
}
takePhoto(option : TakePhotoOption) {
let quality = option.quality ?? "high";
this.quality = quality;
if (quality == 'low') {
this.unbindCamera()
this.bindCamera()
}
// 设置输出目录
let privateDir = UTSAndroid.getAppCachePath()!;
const tempFile = new File(privateDir);
if (!tempFile.exists()) {
tempFile.mkdirs()
}
// 设置文件名
const photoFile = new File(privateDir, Date.now() + '.png');
// 处理镜像
let metadata = new ImageCapture.Metadata();
metadata.setReversedHorizontal(option.selfieMirror ?? DEVICE_POSITION_MAP.get(this.devicePosition)! == CameraSelector.LENS_FACING_FRONT);
const outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
class OnImageSavedCallback implements ImageCapture.OnImageSavedCallback {
override onImageSaved(outputFileResults : ImageCapture.OutputFileResults) {
option.success?.({
tempImagePath: photoFile.getAbsolutePath(),
quality: quality,
errMsg: 'ok'
} as TakePhotoSuccessCallbackResult)
option.complete?.({
errMsg: 'ok'
} as GeneralCallbackResult)
}
override onError(err : ImageCaptureException) {
const result : GeneralCallbackResult = {
errMsg: err.toString()
}
option.fail?.(result)
option.complete?.(result)
}
override onCaptureStarted() {
}
}
this.imageCapture!.takePicture(outputOptions, ContextCompat.getMainExecutor(UTSAndroid.getUniActivity()!), new OnImageSavedCallback() as ImageCapture.OnImageSavedCallback)
}
setZoom(option : CameraContextSetZoomOption) {
if (this.cameraControl != null) {
try {
this.cameraControl!.setZoomRatio(option.zoom.toFloat())
option.success?.({
zoom: option.zoom,
errMsg: 'ok'
} as SetZoomSuccessCallbackResult)
option.complete?.({
errMsg: 'ok'
} as GeneralCallbackResult)
} catch (e) {
const result : GeneralCallbackResult = {
errMsg: e.toString()
}
option.complete?.(result)
option.fail?.(result)
}
} else {
const result : GeneralCallbackResult = {
errMsg: '相机不存在'
}
option.complete?.(result)
option.fail?.(result)
}
}
startRecord(option : CameraContextStartRecordOption) {
if (option.selfieMirror != null && this.videoSelfieMirror != option.selfieMirror) {
this.videoSelfieMirror = option.selfieMirror
this.unbindCamera()
this.bindCamera()
}
if (this.recording != null || this.videoCapture == null) {
option.fail?.({
errMsg: '已经在录制或相机不存在'
} as GeneralCallbackResult)
option.complete?.({
errMsg: '已经在录制或相机不存在'
} as GeneralCallbackResult)
return
}
// 设置输出目录
let privateDir = UTSAndroid.getAppCachePath()!;
// 设置文件名
this.videoFile = new File(privateDir, Date.now() + 'video.mp4');
let outputFileOptions = new FileOutputOptions.Builder(this.videoFile!).build();
this.recording = this.videoCapture!.output
.prepareRecording(UTSAndroid.getUniActivity()!, outputFileOptions)
.withAudioEnabled()
// .apply(()=>{
// })
.start(ContextCompat.getMainExecutor(UTSAndroid.getUniActivity()!), (recordEvent : VideoRecordEvent) => {
if (recordEvent instanceof VideoRecordEvent.Start) {
option.success?.({
errMsg: 'ok'
} as GeneralCallbackResult)
if (option.timeout != null && option.timeout! > 0) {
setTimeout(() => {
this.recording?.stop()
}, option.timeout!)
}
} else if (recordEvent instanceof VideoRecordEvent.Finalize) {
this.recording?.close()
this.recording = null
let tempVideoPath = this.videoFile?.getAbsolutePath()!
//let tempThumbPath = extractThumbnail(tempVideoPath) ?? ''
this.cameraContextStopRecordOption?.success?.({
tempVideoPath,
errMsg: 'ok'
} as StopRecordSuccessCallbackResult)
option.timeoutCallback?.({
tempVideoPath,
} as StartRecordTimeoutCallbackResult)
}
})
}
stopRecord(option : CameraContextStopRecordOption) {
if (this.recording == null) {
option.fail?.({
errMsg: '未开始录制'
} as GeneralCallbackResult)
option.complete?.({
errMsg: '未开始录制'
} as GeneralCallbackResult)
return
}
this.cameraContextStopRecordOption = option
this.recording?.stop()
}
onCameraFrame(callback : OnCameraFrameCallback | null = null) {
this.onCameraFrameCallback = callback
}
onCameraFrameListener(option : OnCameraFrameListenerOption) {
this.onCameraFrameListenerOption = option
}
cameraFrameOnStart(option ?: CameraFrameListenerStartOption) {
this.isListening = true
option?.success?.({
errMsg: 'ok'
} as GeneralCallbackResult)
}
cameraFrameOnStop(option ?: StopOption) {
this.isListening = false
option?.success?.({
errMsg: 'ok'
} as GeneralCallbackResult)
}
}
// // 从视频文件中提取封面的函数
// function extractThumbnail(videoUri : string) : string | null {
// let retriever = new MediaMetadataRetriever()
// retriever.setDataSource(videoUri)
// // 提取视频的某一帧作为封面
// let thumbnail = retriever.frameAtTime
// // 保存封面到文件
// if (thumbnail != null) {
// let privateDir = UTSAndroid.getAppCachePath();
// const thumbnailFile = new File(privateDir, Date.now() + '_thumbnail.jpg');
// let outputStream = FileOutputStream(thumbnailFile)
// thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
// outputStream.close()
// return thumbnailFile.getAbsolutePath()
// }
// retriever.release()
// return null
// }