diff --git a/pages/index/index.uvue b/pages/index/index.uvue index 54b66f4..9609ed2 100644 --- a/pages/index/index.uvue +++ b/pages/index/index.uvue @@ -1,24 +1,188 @@ - - - - - \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/changelog.md b/uni_modules/XF-cameraUTS/changelog.md new file mode 100644 index 0000000..5e5871e --- /dev/null +++ b/uni_modules/XF-cameraUTS/changelog.md @@ -0,0 +1,24 @@ +## 1.1.1(2025-10-15) + 修复监听持续返回事件问题。 +## 1.1.0(2024-10-13) +修改AndroidManifest.xml下的包名。 +## 1.0.9(2024-10-12) +删除takePhoto下的takeMode参数逻辑 +## 1.0.8(2024-10-10) +兼容处理uniapp无法切换相机和拍照的问题。 +## 1.0.7(2024-10-09) +修复uniapp环境下不能正常使用插件问题。 +## 1.0.6(2024-09-13) +去掉插件内部权限申请逻辑。 +## 1.0.5(2024-08-10) +修复Android不能正常录像的问题。 +## 1.0.4(2024-08-08) +修复iOS默认相机设置不生效的问题 +## 1.0.3(2024-08-06) +修改iOS拍照接口默认质量为原图。 +## 1.0.2(2024-07-22) +修复iOS编译问题。 +## 1.0.1(2024-07-04) +修复一些bug。 +## 1.0.0(2024-07-04) +初始化发布 diff --git a/uni_modules/XF-cameraUTS/encrypt b/uni_modules/XF-cameraUTS/encrypt new file mode 100644 index 0000000..708c6d5 Binary files /dev/null and b/uni_modules/XF-cameraUTS/encrypt differ diff --git a/uni_modules/XF-cameraUTS/package.json b/uni_modules/XF-cameraUTS/package.json new file mode 100644 index 0000000..0603086 --- /dev/null +++ b/uni_modules/XF-cameraUTS/package.json @@ -0,0 +1,114 @@ +{ + "id": "XF-cameraUTS", + "displayName": "UTS实现自定义相机拍照和录像功能", + "version": "1.1.1", + "description": "XF-cameraUTS封装了Android和iOS自定义相机预览的功能。", + "keywords": [ + "相机", + "录像", + "拍照" +], + "repository": "", + "engines": { + "HBuilderX": "^3.7.0", + "uni-app": "^4.74", + "uni-app-x": "^4.74" + }, + "dcloudext": { + "type": "component-uts", + "sale": { + "regular": { + "price": "168.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "android.permission.CAMERA\nandroid.permission.RECORD_AUDIO\nandroid.permission.WRITE_EXTERNAL_STORAGE\nandroid.permission.READ_EXTERNAL_STORAGE\nPrivacy - Camera Usage Description" + }, + "npmurl": "", + "darkmode": "x", + "i18n": "x", + "widescreen": "x" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "√", + "aliyun": "√", + "alipay": "√" + }, + "client": { + "uni-app": { + "vue": { + "vue2": "x", + "vue3": "x" + }, + "web": { + "safari": "x", + "chrome": "x" + }, + "app": { + "vue": "x", + "nvue": { + "extVersion": "1.0.1", + "minVersion": "" + }, + "android": { + "extVersion": "1.0.1", + "minVersion": "21" + }, + "ios": { + "extVersion": "1.0.1", + "minVersion": "12" + }, + "harmony": "x" + }, + "mp": { + "weixin": "x", + "alipay": "x", + "toutiao": "x", + "baidu": "x", + "kuaishou": "x", + "jd": "x", + "harmony": "x", + "qq": "x", + "lark": "x" + }, + "quickapp": { + "huawei": "x", + "union": "x" + } + }, + "uni-app-x": { + "web": { + "safari": "x", + "chrome": "x" + }, + "app": { + "android": { + "extVersion": "1.0.1", + "minVersion": "21" + }, + "ios": { + "extVersion": "1.0.1", + "minVersion": "12" + }, + "harmony": "x" + }, + "mp": { + "weixin": "x" + } + } + } + } + } +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/readme.md b/uni_modules/XF-cameraUTS/readme.md new file mode 100644 index 0000000..9871e04 --- /dev/null +++ b/uni_modules/XF-cameraUTS/readme.md @@ -0,0 +1,183 @@ +# **概述** + +XF-cameraUTS封装了Android和iOS自定义相机预览的功能。 + +## **模块调用** + +```js + + +``` + +## 参数说明: + +position: + +- 类型:字符串型 +- 描述:(可选项)前置或者后置相机。(front前置 |back 后置) + +flash: + +- 类型:字符串型 +- 描述:(可选项)是否开启闪光灯。( auto, on, off, torch) + + +## **插件接口** + +
+ +# **takePhoto** + +拍照 + +## uni-app项目中(nvue)调用示例: + +```js +import { takePhoto } from "@/uni_modules/XF-cameraUTS" + +takePhoto({ + success: (res) => { + console.log(res) + } +}); +``` + +## uni-app x项目(uvue)中调用示例: + +```js +import { takePhoto } from "@/uni_modules/XF-cameraUTS"; +import { TakePhotoOption, TakePhotoSuccessCallbackResult } from "@/uni_modules/XF-cameraUTS/utssdk/interface.uts"; + +let options = { + success: (res : TakePhotoSuccessCallbackResult) => { + console.log(res) + } +} as TakePhotoOption; +takePhoto(options); +``` + +## 可用性 + +iOS、Android系统 + +可提供的1.0.0及更高版本 + + +
+ +# **startRecord** + +开始录像 + +## uni-app项目中(nvue)调用示例: + +```js +import { startRecord } from "@/uni_modules/XF-cameraUTS" + +startRecord({ + success: (res) => { + console.log(res) + } +}); +``` + +## uni-app x项目(uvue)中调用示例: + +```js +import { startRecord } from "@/uni_modules/XF-cameraUTS"; +import { CameraContextStartRecordOption, GeneralCallbackResult } from "@/uni_modules/XF-cameraUTS/utssdk/interface.uts"; + +let options = { + success: (res : GeneralCallbackResult) => { + console.log(res) + } +} as CameraContextStartRecordOption; +startRecord(options); +``` + +## 可用性 + +iOS、Android系统 + +可提供的1.0.0及更高版本 + + + +
+ +# **stopRecord** + +停止录像 + +## uni-app项目中(nvue)调用示例: + +```js +import { stopRecord } from "@/uni_modules/XF-cameraUTS" + +stopRecord({ + success: (res) => { + console.log(res) + } +}); +``` + +## uni-app x项目(uvue)中调用示例: + +```js +import { stopRecord } from "@/uni_modules/XF-cameraUTS"; +import { CameraContextStopRecordOption, StopRecordSuccessCallbackResult } from "@/uni_modules/XF-cameraUTS/utssdk/interface.uts"; + +let options = { + success: (res : StopRecordSuccessCallbackResult) => { + console.log(res) + } +} as CameraContextStopRecordOption; +stopRecord(options); +``` + +## 可用性 + +iOS、Android系统 + +可提供的1.0.0及更高版本 + + +
+ +# **switchCamera** + +切换相机 + +## uni-app项目中(nvue)调用示例: + +```js +import { switchCamera } from "@/uni_modules/XF-cameraUTS" + +switchCamera({ + position: 'back', + success: (res) => { + console.log(res) + } +}); +``` + +## uni-app x项目(uvue)中调用示例: + +```js +import { switchCamera } from "@/uni_modules/XF-cameraUTS"; +import { SwitchOption, GeneralCallbackResult } from "@/uni_modules/XF-cameraUTS/utssdk/interface.uts"; + +let options = { + position: 'back', + success: (res : GeneralCallbackResult) => { + console.log(res) + } +} as SwitchOption; +switchCamera(options); +``` + +## 可用性 + +iOS、Android系统 + +可提供的1.0.8及更高版本 \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/AndroidManifest.xml b/uni_modules/XF-cameraUTS/utssdk/app-android/AndroidManifest.xml new file mode 100644 index 0000000..86f5b64 --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/CameraManager.uts b/uni_modules/XF-cameraUTS/utssdk/app-android/CameraManager.uts new file mode 100644 index 0000000..fb0aafc --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/CameraManager.uts @@ -0,0 +1,26 @@ +import { CameraView } from "./camera"; + +export class CameraManager { + private cameraView : CameraView | null = null; + + private static instance : CameraManager | null = null; + + public static getInstance() : CameraManager { + if (CameraManager.instance == null) { + CameraManager.instance = new CameraManager(); + } + return CameraManager.instance!; + } + + public setCameraView(cameraView ?: CameraView) : void { + this.cameraView = cameraView! + } + + public getCameraView() : CameraView | null { + return this.cameraView; + } + + public dispose() : void { + this.cameraView = null; + } +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/camera.uts b/uni_modules/XF-cameraUTS/utssdk/app-android/camera.uts new file mode 100644 index 0000000..e223355 --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/camera.uts @@ -0,0 +1,474 @@ +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([ + ['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([ + ['back', CameraSelector.LENS_FACING_BACK], + ['front', CameraSelector.LENS_FACING_FRONT], +]) + + +const RESOLUTION_MAP = new Map([ + ['low', Quality.LOWEST], + ['medium', Quality.HD], + ['high', Quality.HIGHEST], +]) + +const QUALITY_MAP = new Map([ + ["high", ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY], + ["low", ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY], +]) + +const MIRROR_MODE = new Map([ + [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 | null = null + private recording : Recording | null = null + private comp : UTSComponent; + 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, 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 = new Map(); + ret.set("errMsg", JSON.stringify(e)) + this.comp.$emit('error', ret) + } + }, ContextCompat.getMainExecutor(UTSAndroid.getUniActivity()!)) + // }).catch((err) => { + // let ret : Map = 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 = new Map(); + ret.set("maxZoom", maxZoom) + this.comp.$emit('ready', ret) + } + + } + unbindCamera() { + this.cameraProvider?.unbindAll() + this.cameraExecutor?.shutdown() + } + // checkSelfPermission() : Promise { + // 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 +// } \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/config.json b/uni_modules/XF-cameraUTS/utssdk/app-android/config.json new file mode 100644 index 0000000..39e876f --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/config.json @@ -0,0 +1,20 @@ +{ + "minSdkVersion": "19", + "dependencies": [ + // 使用camera2实现的CameraX核心库 + // 定义camerax_version为"1.4.0-alpha04" + // 下面的行是可选的,因为core库通过camera-camera2间接包含 + "androidx.camera:camera-core:1.4.0-alpha04" + "androidx.camera:camera-camera2:1.4.0-alpha04" + // 如果你想要额外使用CameraX生命周期库 + "androidx.camera:camera-lifecycle:1.4.0-alpha04" + // 如果你想要额外使用CameraX视频捕获库 + "androidx.camera:camera-video:1.4.0-alpha04" + // 如果你想要额外使用CameraX视图类 + "androidx.camera:camera-view:1.4.0-alpha04" + // 如果你想要额外添加CameraX ML Kit Vision集成 + // "androidx.camera:camera-mlkit-vision:1.4.0-alpha04" + // 如果你想要额外使用CameraX扩展库 + // "androidx.camera:camera-extensions:1.4.0-alpha04" + ] +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/index.uts b/uni_modules/XF-cameraUTS/utssdk/app-android/index.uts new file mode 100644 index 0000000..c44f570 --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/index.uts @@ -0,0 +1,252 @@ +import { CameraView } from './camera' +import { + TakePhotoOption, + GeneralCallbackResult, + CameraContextSetZoomOption, + CameraContextStartRecordOption, + CameraContextStopRecordOption, + // CameraFrameListener, + OnCameraFrameCallback, + // OnCameraFrameCallbackResult, + CameraFrameListenerStartOption, + StopOption, + TakePhoto, + SetZoom, + StartRecord, + StopRecord, + StartCameraFrame, + StopCameraFrame, + OnCameraFrameListenerOption, + SwitchOption, + SwitchCamera +} from '../interface' +import { CameraManager } from "./CameraManager"; + + +class CameraFrameListener { + context : CameraContext + constructor(context : CameraContext) { + this.context = context + } + start() : void + start(option ?: CameraFrameListenerStartOption) { + this.context.getCamera().then((camera : CameraView) => { + camera.cameraFrameOnStart(option) + }) + } + stop() : void + stop(option ?: StopOption) { + this.context.getCamera().then((camera : CameraView) => { + camera.cameraFrameOnStop(option) + }) + } +} + +class CameraContext { + private mCameraFrameListener : CameraFrameListener | null = null + constructor() { + } + getCamera() : Promise { + let cameraPreview = CameraManager.getInstance().getCameraView() + if (cameraPreview != null) { + return Promise.resolve(cameraPreview); + } + let resolveFunc : ((res : CameraView) => void) | null = null + let rejectFunc : ((res : string) => void) | null = null + + let maxAttempts : number = 5 + let currentAttempt : number = 0 + + function attemptGetCamera() { + let camera = CameraManager.getInstance().getCameraView() + if (camera !== null) { + resolveFunc?.(camera) + } else if (currentAttempt < maxAttempts) { + currentAttempt++ + setTimeout(() => { + attemptGetCamera() + }, 100) + } else { + rejectFunc?.('Camera not found after multiple attempts') + } + } + return new Promise((resolve, reject) => { + resolveFunc = resolve + rejectFunc = reject + attemptGetCamera() + }) + } + + + takePhoto(option : TakePhotoOption) { + this.getCamera().then((camera : CameraView) => { + camera.takePhoto(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + + } + setZoom(option : CameraContextSetZoomOption) { + this.getCamera().then((camera : CameraView) => { + camera.setZoom(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } + startRecord(option : CameraContextStartRecordOption) { + this.getCamera().then((camera : CameraView) => { + camera.startRecord(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } + stopRecord(option : CameraContextStopRecordOption) { + this.getCamera().then((camera : CameraView) => { + camera.stopRecord(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } + onCameraFrame() : CameraFrameListener + onCameraFrame(callback : OnCameraFrameCallback | null = null) : CameraFrameListener { + if (this.mCameraFrameListener == null) { + this.mCameraFrameListener = new CameraFrameListener(this) + } + this.getCamera().then((camera : CameraView) => { + camera.onCameraFrame(callback) + }) + return this.mCameraFrameListener! + } + ////////////////////////////////////////////////////// + onCameraFrameListener(option : OnCameraFrameListenerOption) { + this.getCamera().then((camera : CameraView) => { + camera.onCameraFrameListener(option) + }).catch(() => { + console.log("未找到相机") + }) + } + + cameraFrameOnStart(option : CameraFrameListenerStartOption) { + this.getCamera().then((camera : CameraView) => { + camera.cameraFrameOnStart(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } + + cameraFrameOnStop(option : StopOption) { + this.getCamera().then((camera : CameraView) => { + camera.cameraFrameOnStop(option) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } + + switchCamera(option : SwitchOption) { + this.getCamera().then((camera : CameraView) => { + camera.switchCamera(option.position) + + option.success?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + }).catch(() => { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + }) + } +} + +let context : CameraContext = new CameraContext(); + +export function createCameraContext() : CameraContext { + return context; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +export const takePhoto : TakePhoto = function (options : TakePhotoOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.takePhoto(options) + }, null) +} + +export const setZoom : SetZoom = function (options : CameraContextSetZoomOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.setZoom(options) + }, null) +} + +export const startRecord : StartRecord = function (options : CameraContextStartRecordOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.startRecord(options) + }, null) +} + +export const stopRecord : StopRecord = function (options : CameraContextStopRecordOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.stopRecord(options) + }, null) +} + +@UTSJS.keepAlive +export function onCameraFrameListener(options : OnCameraFrameListenerOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.onCameraFrameListener(options) + }, null) +} + +export const startCameraFrame : StartCameraFrame = function (options : CameraFrameListenerStartOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.cameraFrameOnStart(options) + }, null) +} + +export const stopCameraFrame : StopCameraFrame = function (options : StopOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.cameraFrameOnStop(options) + }, null) +} + + +export const switchCamera : SwitchCamera = function (options : SwitchOption) { + UTSAndroid.getDispatcher("main").async(function (_) { + context.switchCamera(options) + }, null) +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-android/index.vue b/uni_modules/XF-cameraUTS/utssdk/app-android/index.vue new file mode 100644 index 0000000..c37a8c4 --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-android/index.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-ios/CameraManager.uts b/uni_modules/XF-cameraUTS/utssdk/app-ios/CameraManager.uts new file mode 100644 index 0000000..fb0aafc --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-ios/CameraManager.uts @@ -0,0 +1,26 @@ +import { CameraView } from "./camera"; + +export class CameraManager { + private cameraView : CameraView | null = null; + + private static instance : CameraManager | null = null; + + public static getInstance() : CameraManager { + if (CameraManager.instance == null) { + CameraManager.instance = new CameraManager(); + } + return CameraManager.instance!; + } + + public setCameraView(cameraView ?: CameraView) : void { + this.cameraView = cameraView! + } + + public getCameraView() : CameraView | null { + return this.cameraView; + } + + public dispose() : void { + this.cameraView = null; + } +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-ios/camera.uts b/uni_modules/XF-cameraUTS/utssdk/app-ios/camera.uts new file mode 100644 index 0000000..270f31f --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-ios/camera.uts @@ -0,0 +1,469 @@ +import { UIImage, UIView } from 'UIKit'; +import { AVCaptureDevice, AVCaptureDeviceInput, AVCaptureSession, AVCaptureVideoPreviewLayer, AVMediaType, AVLayerVideoGravity, AVCaptureVideoDataOutput, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureOutput, AVCaptureConnection, AVCapturePhotoOutput, AVCapturePhotoSettings, AVCapturePhotoCaptureDelegate, AVCaptureMovieFileOutput, AVCaptureFileOutputRecordingDelegate, AVCaptureFileOutput } from "AVFoundation"; +import { CMSampleBuffer, CMSampleBufferGetImageBuffer } from 'CoreMedia'; +import { DispatchQueue } from 'Dispatch'; +import { CVPixelBufferGetWidth, CVPixelBufferGetHeight } from "CoreVideo"; +import { NSError, URL } from "Foundation"; + +import { + TakePhotoOption, + GeneralCallbackResult, + TakePhotoSuccessCallbackResult, + CameraContextSetZoomOption, + SetZoomSuccessCallbackResult, + CameraContextStopRecordOption, + CameraContextStartRecordOption, + StartRecordTimeoutCallbackResult, + StopRecordSuccessCallbackResult, + OnCameraFrameCallback, + OnCameraFrameCallbackResult, + OnCameraFrameListenerOption, + CameraFrameListenerStartOption, + StopOption, + + CameraConfig, + FlashMode, + DevicePosition, + Resolution +} from '../interface' +import { AVCapturePhoto } from 'AVFoundation'; + + + +const RESOLUTION_MAP = new Map([ + ['low', AVCaptureSession.Preset.low], + ['medium', AVCaptureSession.Preset.medium], + ['high', AVCaptureSession.Preset.high], +]) + +const FLASH_MODE_MAP = new Map([ + ['auto', AVCaptureDevice.TorchMode.auto], + ['on', AVCaptureDevice.TorchMode.on], + ['torch', AVCaptureDevice.TorchMode.on], + ['off', AVCaptureDevice.TorchMode.off], +]) + + +let fileOutputRecordingDelegate : MyAVCaptureFileOutputRecordingDelegate | null = null; +let capturePhotoCaptureDelegate : MyAVCapturePhotoCaptureDelegate | null = null; +let onCameraFrameListenerOption : OnCameraFrameListenerOption | null = null; +let cameraContextStopRecordOption : CameraContextStopRecordOption | null = null; + +class MyAVCaptureFileOutputRecordingDelegate implements AVCaptureFileOutputRecordingDelegate { + private option : CameraContextStartRecordOption; + + constructor(option : CameraContextStartRecordOption) { + this.option = option; + } + + fileOutput(output : AVCaptureFileOutput, @argumentLabel("didStartRecordingTo") fileURL : URL, @argumentLabel("from") connections : [AVCaptureConnection]) : void { + this.option.success?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + } + + fileOutput(output : AVCaptureFileOutput, @argumentLabel("didFinishRecordingTo") outputFileURL : URL, @argumentLabel("from") connections : [AVCaptureConnection], @argumentLabel("error") error ?: NSError) : void { + if (error == null) { + cameraContextStopRecordOption?.success?.({ + tempVideoPath: outputFileURL.path + "", + errMsg: 'ok' + } as StopRecordSuccessCallbackResult) + + this.option.timeoutCallback?.({ + tempVideoPath: outputFileURL.path + "" + } as StartRecordTimeoutCallbackResult) + } else { + cameraContextStopRecordOption?.fail?.({ + errMsg: '录制失败' + } as GeneralCallbackResult) + cameraContextStopRecordOption?.complete?.({ + errMsg: '录制失败' + } as GeneralCallbackResult) + } + + } + +} + +class MyAVCapturePhotoCaptureDelegate implements AVCapturePhotoCaptureDelegate { + private option : TakePhotoOption; + + constructor(option : TakePhotoOption) { + this.option = option; + } + + captureOutput(output : AVCapturePhotoOutput, @argumentLabel("didFinishProcessingPhoto") photo : AVCapturePhoto, @argumentLabel("error") error : Error) : void { + let imageData = photo.fileDataRepresentation() + if (imageData == null) { + const result : GeneralCallbackResult = { + errMsg: "拍照失败" + } + this.option.fail?.(result) + this.option.complete?.(result) + return; + } + let path = UTSiOS.getDataPath() + (new Date().getTime()).toString() + ".jpg"; + let url = new URL(fileURLWithPath = path); + + let quality = this.option.quality ?? "original" + if (quality == 'original') { + try { + UTSiOS.try(imageData!.write(to = url, options = NSData.WritingOptions.atomic)) + + this.option.success?.({ + tempImagePath: path, + quality: quality, + errMsg: 'ok' + } as TakePhotoSuccessCallbackResult) + this.option.complete?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + } catch (e) { + const result : GeneralCallbackResult = { + errMsg: JSON.stringify(e) + } + this.option.fail?.(result) + this.option.complete?.(result) + } + } else { + let image = UIImage(data = imageData!) + if (image != null) { + let _quality : number = 0.5; + if (quality == 'high') { + _quality = 1.0; + } else if (quality == 'normal') { + _quality = 0.8; + } else if (quality == 'low') { + _quality = 0.5; + } + let data = image!.jpegData(compressionQuality = _quality.toDouble())! + + try { + UTSiOS.try(data.write(to = url, options = NSData.WritingOptions.atomic)) + + this.option.success?.({ + tempImagePath: path, + quality: quality, + errMsg: 'ok' + } as TakePhotoSuccessCallbackResult) + this.option.complete?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + } catch (e) { + const result : GeneralCallbackResult = { + errMsg: JSON.stringify(e) + } + this.option.fail?.(result) + this.option.complete?.(result) + } + } else { + const result : GeneralCallbackResult = { + errMsg: "拍照失败" + } + this.option.fail?.(result) + this.option.complete?.(result) + } + } + } + +} + +export class CameraView implements AVCaptureVideoDataOutputSampleBufferDelegate { + + private captureSession ?: AVCaptureSession; + private videoDeviceInput ?: AVCaptureDeviceInput; + private photoDeviceOutput ?: AVCapturePhotoOutput; + private movieDeviceOutput ?: AVCaptureMovieFileOutput; + private previewLayer ?: AVCaptureVideoPreviewLayer; + private previewView ?: UIView; + private comp ?: UTSComponent; + private flashMode : FlashMode = 'auto' + private devicePosition : DevicePosition = 'back' + private resolution : Resolution = 'medium' + private isListening : boolean = false + private onCameraFrameCallback : OnCameraFrameCallback | null = null; + + constructor(previewView : UIView, comp : UTSComponent, config : CameraConfig = {} as CameraConfig) { + super.init(); + this.previewView = previewView + this.comp = comp + this.flashMode = config.flash ?? 'auto' + this.devicePosition = config.devicePosition ?? 'back' + this.resolution = config.resolution ?? 'medium' + this.setupCamera(); + } + + setupCamera() { + this.captureSession = new AVCaptureSession(); + let targetResolution = RESOLUTION_MAP.get(this.resolution) ?? AVCaptureSession.Preset.high; + this.captureSession!.sessionPreset = targetResolution; + let device = this.getCameraDevice(); + try { + //相机 + let deviceInput = UTSiOS.try(new AVCaptureDeviceInput(device = device)) + if (this.captureSession!.canAddInput(deviceInput)) { + this.captureSession!.addInput(deviceInput); + } + //实时流输出 + let output = new AVCaptureVideoDataOutput(); + output.setSampleBufferDelegate(this, queue = DispatchQueue.main); + if (this.captureSession!.canAddOutput(output)) { + this.captureSession!.addOutput(output); + } + + let photoOutput = new AVCapturePhotoOutput(); + // let photoSettings = new AVCapturePhotoSettings(format = [AVVideoCodecKey = AVVideoCodecType.jpeg]); + // photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler = null); + if (this.captureSession!.canAddOutput(photoOutput)) { + this.captureSession!.addOutput(photoOutput); + } + + let movieOutput = new AVCaptureMovieFileOutput(); + if (this.captureSession!.canAddOutput(movieOutput)) { + this.captureSession!.addOutput(movieOutput); + } + + this.videoDeviceInput = deviceInput; + this.photoDeviceOutput = photoOutput; + this.movieDeviceOutput = movieOutput; + this.previewLayer = new AVCaptureVideoPreviewLayer(session = this.captureSession!) + this.previewLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill; + this.previewLayer!.frame = this.previewView!.layer.bounds; + this.previewView!.layer.addSublayer(this.previewLayer!) + this.captureSession!.startRunning(); + + let ret : Map = new Map(); + ret.set("maxZoom", device.maxAvailableVideoZoomFactor) + let detail : Map = new Map(); + detail.set("detail", ret) + this.comp?.$emit("ready", detail); + } catch (e) { + let ret : Map = new Map(); + ret.set("errMsg", JSON.stringify(e)) + let detail : Map = new Map(); + detail.set("detail", ret) + this.comp?.$emit("error", detail); + } + } + + getCameraDevice() : AVCaptureDevice { + let cameras = AVCaptureDevice.devices(for = AVMediaType.video) + let frontCamera : AVCaptureDevice | null = null; + let backCamera : AVCaptureDevice | null = null; + + let index = 0; + while (index < cameras.length) { + if (index == cameras.length) { + break; + } + let camera = cameras[index.toInt()] as AVCaptureDevice; + if (camera.position == AVCaptureDevice.Position.back) { + backCamera = camera; + } else { + frontCamera = camera; + } + index++; + } + + if (this.devicePosition == 'back') { + return backCamera!; + } else { + return frontCamera!; + } + } + + captureOutput(output : AVCaptureOutput, @argumentLabel("didOutput") sampleBuffer : CMSampleBuffer, @argumentLabel("from") connection : AVCaptureConnection) : void { + + if (this.isListening) { + let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) + let width = CVPixelBufferGetWidth(imageBuffer!) + let height = CVPixelBufferGetHeight(imageBuffer!); + + const res : OnCameraFrameCallbackResult = { + width: width as Int, + height: height as Int, + data: imageBuffer! + }; + this.onCameraFrameCallback?.(res) + onCameraFrameListenerOption?.success?.({ + width: width as Int, + height: height as Int, + data: imageBuffer! + } as OnCameraFrameCallbackResult) + + // this.onCameraFrameListenerOption?.success({ + // width: width as Int, + // height: height as Int, + // data: imageBuffer! + // } as OnCameraFrameCallbackResult) + + + // if (imageBuffer != null) { + // let ciImage = new CIImage(cvPixelBuffer = imageBuffer!) + // let temporaryContext = new CIContext(options = null) + // let width = CVPixelBufferGetWidth(imageBuffer!) + // let height = CVPixelBufferGetHeight(imageBuffer!); + // let videoImage = temporaryContext.createCGImage(ciImage, from = new CGRect(x = 0, y = 0, width = width, height = height)) + // let image = new UIImage(cgImage = videoImage!) + + // this.onCameraFrameCallback?.({ + // width: width as Int, + // height: height as Int, + // data: image + // } as OnCameraFrameCallbackResult) + // } + } + } + + bindCamera() { + this.captureSession!.startRunning(); + } + + + unbindCamera() { + this.captureSession!.stopRunning(); + } + + + setConfig(config : CameraConfig = {} as CameraConfig) { + this.flashMode = config.flash ?? 'auto' + this.devicePosition = config.devicePosition ?? 'back' + this.resolution = config.resolution ?? 'medium' + } + + setFlash(mode : FlashMode) { + this.flashMode = mode + let targetFlash = FLASH_MODE_MAP.get(this.flashMode) ?? AVCaptureDevice.TorchMode.auto; + try { + let device = this.getCameraDevice(); + if (device.hasTorch && device.isTorchAvailable) { + UTSiOS.try(device.lockForConfiguration()); + device.torchMode = targetFlash; + device.unlockForConfiguration() + } + } catch (e) { + console.log(e) + } + } + + + switchCamera(position : DevicePosition) { + if (!this.captureSession!.isRunning) { + return; + } + this.devicePosition = position; + try { + this.captureSession!.beginConfiguration(); + this.captureSession!.removeInput(this.videoDeviceInput!); + let device = this.getCameraDevice(); + + let deviceInput = UTSiOS.try(new AVCaptureDeviceInput(device = device)) + this.captureSession!.addInput(deviceInput); + this.videoDeviceInput = deviceInput; + this.captureSession!.commitConfiguration(); + } catch (e) { + console.log(e) + } + } + + takePhoto(option : TakePhotoOption) { + capturePhotoCaptureDelegate = new MyAVCapturePhotoCaptureDelegate(option); + let settings = new AVCapturePhotoSettings() + this.photoDeviceOutput?.capturePhoto(with = settings, delegate = capturePhotoCaptureDelegate!); + } + + + setZoom(option : CameraContextSetZoomOption) { + try { + let device = this.getCameraDevice(); + // if (device.isRampingVideoZoom) { + UTSiOS.try(device.lockForConfiguration()); + device.videoZoomFactor = option.zoom.toDouble(); + device.unlockForConfiguration() + + option.success?.({ + zoom: option.zoom, + errMsg: 'ok' + } as SetZoomSuccessCallbackResult) + option.complete?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + // } else { + // const result : GeneralCallbackResult = { + // errMsg: '相机不支持缩放' + // } + // option.complete?.(result) + // option.fail?.(result) + // } + } catch (e) { + const result : GeneralCallbackResult = { + errMsg: JSON.stringify(e) + } + option.complete?.(result) + option.fail?.(result) + } + } + + startRecord(option : CameraContextStartRecordOption) { + let isRecording = this.movieDeviceOutput?.isRecording ?? false; + if (isRecording) { + option.fail?.({ + errMsg: '已经在录制或相机不存在' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '已经在录制或相机不存在' + } as GeneralCallbackResult) + return + } + + fileOutputRecordingDelegate = new MyAVCaptureFileOutputRecordingDelegate(option); + let path = UTSiOS.getDataPath() + (new Date().getTime()).toString() + ".mp4"; + let url = new URL(fileURLWithPath = path); + this.movieDeviceOutput?.startRecording(to = url, recordingDelegate = fileOutputRecordingDelegate!); + + if (option.timeout != null && option.timeout! > 0) { + setTimeout(() => { + this.movieDeviceOutput?.stopRecording(); + }, (option.timeout!).toInt()) + } + } + + + stopRecord(option : CameraContextStopRecordOption) { + let isRecording = this.movieDeviceOutput?.isRecording ?? false; + if (!isRecording) { + option.fail?.({ + errMsg: '未开始录制' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未开始录制' + } as GeneralCallbackResult) + return + } + cameraContextStopRecordOption = option; + this.movieDeviceOutput?.stopRecording(); + } + + + onCameraFrame(callback : OnCameraFrameCallback | null = null) { + this.onCameraFrameCallback = callback + } + + onCameraFrameListener(option : OnCameraFrameListenerOption) { + 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) + } +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-ios/config.json b/uni_modules/XF-cameraUTS/utssdk/app-ios/config.json new file mode 100644 index 0000000..dfb9a96 --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-ios/config.json @@ -0,0 +1,3 @@ +{ + "deploymentTarget": "11" +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-ios/index.uts b/uni_modules/XF-cameraUTS/utssdk/app-ios/index.uts new file mode 100644 index 0000000..16422bc --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-ios/index.uts @@ -0,0 +1,207 @@ +import { + TakePhotoOption, + GeneralCallbackResult, + CameraContextSetZoomOption, + CameraContextStartRecordOption, + CameraContextStopRecordOption, + OnCameraFrameCallback, + CameraFrameListenerStartOption, + StopOption, + TakePhoto, + SetZoom, + StartRecord, + StopRecord, + StartCameraFrame, + StopCameraFrame, + OnCameraFrameListenerOption, + SwitchCamera, + SwitchOption +} from '../interface' +import { CameraManager } from './CameraManager'; + +class CameraFrameListener { + context : CameraContext + constructor(context : CameraContext) { + this.context = context + } + start() : void + start(option ?: CameraFrameListenerStartOption) { + CameraManager.getInstance().getCameraView()?.cameraFrameOnStart(option) + } + stop() : void + stop(option ?: StopOption) { + CameraManager.getInstance().getCameraView()?.cameraFrameOnStop(option) + } +} + +class CameraContext { + private mCameraFrameListener : CameraFrameListener | null = null + + constructor() { + + } + + takePhoto(option : TakePhotoOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.takePhoto(option) + } + + setZoom(option : CameraContextSetZoomOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + + camera?.setZoom(option) + } + + startRecord(option : CameraContextStartRecordOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.startRecord(option) + } + + stopRecord(option : CameraContextStopRecordOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.stopRecord(option) + } + + onCameraFrame() : CameraFrameListener + onCameraFrame(callback : OnCameraFrameCallback | null = null) : CameraFrameListener { + if (this.mCameraFrameListener == null) { + this.mCameraFrameListener = new CameraFrameListener(this) + } + CameraManager.getInstance().getCameraView()?.onCameraFrame(callback) + return this.mCameraFrameListener! + } + ////////////////////////////////////////////////////// + onCameraFrameListener(option : OnCameraFrameListenerOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + return; + } + camera?.onCameraFrameListener(option) + } + + cameraFrameOnStart(option : CameraFrameListenerStartOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.cameraFrameOnStart(option) + } + + cameraFrameOnStop(option : StopOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.cameraFrameOnStop(option) + } + + switchCamera(option : SwitchOption) { + let camera = CameraManager.getInstance().getCameraView() + if (camera == null) { + option.fail?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: '未找到相机' + } as GeneralCallbackResult) + return; + } + camera?.switchCamera(option.position) + + option.success?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + option.complete?.({ + errMsg: 'ok' + } as GeneralCallbackResult) + } +} + +let context : CameraContext = new CameraContext(); + +export function createCameraContext() : CameraContext { + return context; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +export const takePhoto : TakePhoto = function (options : TakePhotoOption) { + context.takePhoto(options) +} + +export const setZoom : SetZoom = function (options : CameraContextSetZoomOption) { + context.setZoom(options) +} + +export const startRecord : StartRecord = function (options : CameraContextStartRecordOption) { + context.startRecord(options) +} + +export const stopRecord : StopRecord = function (options : CameraContextStopRecordOption) { + context.stopRecord(options) +} + +@UTSJS.keepAlive +export function onCameraFrameListener(options : OnCameraFrameListenerOption) { + context.onCameraFrameListener(options) +} + +export const startCameraFrame : StartCameraFrame = function (options : CameraFrameListenerStartOption) { + context.cameraFrameOnStart(options) +} + +export const stopCameraFrame : StopCameraFrame = function (options : StopOption) { + context.cameraFrameOnStop(options) +} + +export const switchCamera : SwitchCamera = function (options : SwitchOption) { + context.switchCamera(options) +} \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/app-ios/index.vue b/uni_modules/XF-cameraUTS/utssdk/app-ios/index.vue new file mode 100644 index 0000000..fed3abc --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/app-ios/index.vue @@ -0,0 +1,167 @@ + + + + \ No newline at end of file diff --git a/uni_modules/XF-cameraUTS/utssdk/interface.uts b/uni_modules/XF-cameraUTS/utssdk/interface.uts new file mode 100644 index 0000000..ae3b66f --- /dev/null +++ b/uni_modules/XF-cameraUTS/utssdk/interface.uts @@ -0,0 +1,214 @@ +export type GeneralCallbackResult = { + /** 错误信息 */ + errMsg : string +} +export type TakePhotoSuccessCallbackResult = { + /** 照片文件的临时路径 (本地路径),安卓是jpg图片格式,ios是png */ + tempImagePath : string + quality : string + errMsg : string +} +/** 接口调用失败的回调函数 */ +export type TakePhotoFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用成功的回调函数 */ +export type TakePhotoSuccessCallback = ( + result : TakePhotoSuccessCallbackResult +) => void + +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type TakePhotoCompleteCallback = (res : GeneralCallbackResult) => void + +export type TakePhotoOption = { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: TakePhotoCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: TakePhotoFailCallback + /** 成像质量 + * + * 可选值: + * - 'high': 高质量; + * - 'normal': 普通质量; + * - 'low': 低质量; + * - 'original': 原图; */ + quality ?: 'high' | 'normal' | 'low' | 'original' + /** + * 是否开启镜像 */ + selfieMirror ?: boolean + /** 接口调用成功的回调函数 */ + success ?: TakePhotoSuccessCallback +} + + +export type SetZoomSuccessCallbackResult = { + /** 实际设置的缩放级别。由于系统限制,某些机型可能无法设置成指定值,会改用最接近的可设值。 */ + zoom : number + errMsg : string +} +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type SetZoomCompleteCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +export type SetZoomFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用成功的回调函数 */ +export type CameraContextSetZoomSuccessCallback = ( + result : SetZoomSuccessCallbackResult +) => void + +export type CameraContextSetZoomOption = { + /** 缩放级别,范围[1, maxZoom]。zoom 可取小数,精确到小数后一位。maxZoom 可在 bindinitdone 返回值中获取。 */ + zoom : number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: SetZoomCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: SetZoomFailCallback + /** 接口调用成功的回调函数 */ + success ?: CameraContextSetZoomSuccessCallback +} + +export type StartRecordTimeoutCallbackResult = { + /** 视频的文件的临时路径 (本地路径) */ + tempVideoPath : string +} + +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type StartRecordCompleteCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +export type StartRecordFailCallback = (res : GeneralCallbackResult) => void +/** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */ +export type StartRecordTimeoutCallback = ( + result : StartRecordTimeoutCallbackResult +) => void +/** 接口调用成功的回调函数 */ +export type CameraContextStartRecordSuccessCallback = ( + res : GeneralCallbackResult +) => void +export type CameraContextStartRecordOption = { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: StartRecordCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: StartRecordFailCallback + /** + * 是否开启镜像 */ + selfieMirror ?: boolean + /** 接口调用成功的回调函数 */ + success ?: CameraContextStartRecordSuccessCallback + /** + * 录制时长上限,单位为秒,最长不能超过 5 分钟 */ + timeout ?: number + /** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */ + timeoutCallback ?: StartRecordTimeoutCallback +} + +export type StopRecordSuccessCallbackResult = { + /** 视频的文件的临时路径 (本地路径) */ + tempVideoPath : string + errMsg : string +} +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type StopRecordCompleteCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +export type StopRecordFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用成功的回调函数 */ +export type CameraContextStopRecordSuccessCallback = ( + result : StopRecordSuccessCallbackResult +) => void +export type CameraContextStopRecordOption = { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: StopRecordCompleteCallback + /** 启动视频压缩,压缩效果同`chooseVideo` */ + compressed ?: boolean + /** 接口调用失败的回调函数 */ + fail ?: StopRecordFailCallback + /** 接口调用成功的回调函数 */ + success ?: CameraContextStopRecordSuccessCallback +} + +/** 回调函数 */ +export type OnCameraFrameCallback = (result : OnCameraFrameCallbackResult) => void +export type OnCameraFrameCallbackResult = { + /** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */ + data : any + /** 图像数据矩形的高度 */ + height : number + /** 图像数据矩形的宽度 */ + width : number +} +/** 接口调用成功的回调函数 */ +export type StartSuccessCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +type StartFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +type StartCompleteCallback = (res : GeneralCallbackResult) => void +export type CameraFrameListenerStartOption = { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: StartCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: StartFailCallback + /** 接口调用成功的回调函数 */ + success ?: StartSuccessCallback + /** [Worker](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.html) + * + * 可选参数。如果需要在 iOS ExperimentalWorker 内监听摄像头帧数据,则需要传入对应 Worker 对象。详情 [Worker.getCameraFrameData](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.getCameraFrameData.html) */ + // worker ?: Worker +} + + +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type StopCompleteCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +export type StopFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用成功的回调函数 */ +export type StopSuccessCallback = (res : GeneralCallbackResult) => void +export type StopOption = { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: StopCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: StopFailCallback + /** 接口调用成功的回调函数 */ + success ?: StopSuccessCallback +} + +export type FlashMode = 'auto' | 'on' | 'off' | 'torch' +export type DevicePosition = 'back' | 'front' +export type Resolution = 'low' | 'medium' | 'high' +export type FrameSize = 'medium' | 'small' | 'large' +export type QualityType = 'high' | 'normal' | 'low' | 'original' + + +export type CameraConfig = { + flash ?: FlashMode + devicePosition ?: DevicePosition + resolution ?: Resolution + frameSize ?: FrameSize +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** 接口调用结束的回调函数(调用成功、失败都会执行) */ +export type SwitchCompleteCallback = (res : GeneralCallbackResult) => void +/** 接口调用失败的回调函数 */ +export type SwitchFailCallback = (res : GeneralCallbackResult) => void +/** 接口调用成功的回调函数 */ +export type SwitchSuccessCallback = (res : GeneralCallbackResult) => void +export type SwitchOption = { + position : DevicePosition + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete ?: SwitchCompleteCallback + /** 接口调用失败的回调函数 */ + fail ?: SwitchFailCallback + /** 接口调用成功的回调函数 */ + success ?: SwitchSuccessCallback +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +export type OnCameraFrameListenerOption = { + success ?: OnCameraFrameCallback +} + +export type TakePhoto = (options : TakePhotoOption) => void +export type SetZoom = (options : CameraContextSetZoomOption) => void +export type StartRecord = (options : CameraContextStartRecordOption) => void +export type StopRecord = (options : CameraContextStopRecordOption) => void +export type OnCameraFrameListener = (options : OnCameraFrameListenerOption) => void +export type StartCameraFrame = (options : CameraFrameListenerStartOption) => void +export type StopCameraFrame = (options : StopOption) => void +export type SwitchCamera = (options : SwitchOption) => void \ No newline at end of file