This commit is contained in:
xsl
2026-03-12 09:54:31 +08:00
commit 7da266c2ff
24 changed files with 2791 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
## 1.1.12025-10-15
修复监听持续返回事件问题。
## 1.1.02024-10-13
修改AndroidManifest.xml下的包名。
## 1.0.92024-10-12
删除takePhoto下的takeMode参数逻辑
## 1.0.82024-10-10
兼容处理uniapp无法切换相机和拍照的问题。
## 1.0.72024-10-09
修复uniapp环境下不能正常使用插件问题。
## 1.0.62024-09-13
去掉插件内部权限申请逻辑。
## 1.0.52024-08-10
修复Android不能正常录像的问题。
## 1.0.42024-08-08
修复iOS默认相机设置不生效的问题
## 1.0.32024-08-06
修改iOS拍照接口默认质量为原图。
## 1.0.22024-07-22
修复iOS编译问题。
## 1.0.12024-07-04
修复一些bug。
## 1.0.02024-07-04
初始化发布
+114
View File
@@ -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"
}
}
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
# **概述**
XF-cameraUTS封装了Android和iOS自定义相机预览的功能。
## **模块调用**
```js
<xf-camera style="height:750rpx; background-color: red;" @ready="ready" @error="onError" @stop="stop" :flash="flash" :position="device">
</xf-camera>
```
## 参数说明:
position
- 类型:字符串型
- 描述:(可选项)前置或者后置相机。(front前置 |back 后置)
flash
- 类型:字符串型
- 描述:(可选项)是否开启闪光灯。( auto, on, off, torch)
## **插件接口**
<div id="a0"></div>
# **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及更高版本
<div id="a1"></div>
# **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及更高版本
<div id="a2"></div>
# **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及更高版本
<div id="a3"></div>
# **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及更高版本
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="uts.sdk.modules.XFCameraUTS">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application android:requestLegacyExternalStorage="true">
<meta-data android:name="ScopedStorage" android:value="true" />
</application>
</manifest>
@@ -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;
}
}
@@ -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<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
// }
@@ -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"
]
}
@@ -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<CameraView> {
let cameraPreview = CameraManager.getInstance().getCameraView()
if (cameraPreview != null) {
return Promise.resolve<CameraView>(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<CameraView>((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)
}
@@ -0,0 +1,152 @@
<template>
<view>
</view>
</template>
<script lang="uts">
import { CameraView } from './camera';
import { CameraConfig } from '../interface'
import { CameraManager } from './CameraManager';
//原生提供以下属性或方法的实现
export default {
/**
* 组件名称,也就是开发者使用的标签
*/
name: "xf-camera",
/**
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
*/
emits: ['stop', 'error', 'ready'],
/**
* 属性声明,组件的使用者会传递这些属性值到组件
*/
props: {
"mode": {
type: String,
default: "normal"
},
"resolution": {
type: String,
default: "medium" // low | high
},
"position": {
type: String,
default: "back" // front前置 |back 后置
},
"flash": {
type: String,
default: "auto" // auto, on, off, torch
},
"frameSize": {
type: String,
default: "medium" // small|large
}
},
/**
* 组件内部变量声明
*/
data() {
return {}
},
/**
* 属性变化监听器实现
*/
watch: {
"position": {
/**
* 这里监听属性变化,并进行组件内部更新
*/
handler(newValue : string, _ : string) {
this.$el?.switchCamera(newValue)
},
immediate: false // 创建时是否通过此方法更新属性,默认值为false
},
"flash": {
/**
* 这里监听属性变化,并进行组件内部更新
*/
handler(newValue : string, _ : string) {
this.$el?.setFlash(newValue)
},
immediate: false // 创建时是否通过此方法更新属性,默认值为false
}
},
/**
* 规则:如果没有配置expose,则methods中的方法均对外暴露,如果配置了expose,则以expose的配置为准向外暴露
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
*/
expose: [],
methods: {
},
/**
* [可选实现] 组件被创建,组件第一个生命周期,
* 在内存中被占用的时候被调用,开发者可以在这里执行一些需要提前执行的初始化逻辑
*/
created() {
},
/**
* [可选实现] 对应平台的view载体即将被创建,对应前端beforeMount
*/
NVBeforeLoad() {
},
/**
* [必须实现] 创建原生View,必须定义返回值类型
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
* Android需要明确知道View类型,需特殊校验)
*/
NVLoad() : CameraView {
let previewView = new CameraView(this.$androidContext!, this)
previewView.setTag("9527");
CameraManager.getInstance().setCameraView(previewView);
return previewView
},
/**
* [可选实现] 原生View已创建
*/
NVLoaded() {
},
/**
* [可选实现] 原生View布局完成
*/
NVLayouted() {
this.$el?.setConfig({
resolution: this.resolution,
frameSize: this.frameSize,
flash: this.flash
} as CameraConfig)
this.$el?.switchCamera(this.position)
},
/**
* [可选实现] 原生View将释放
*/
NVBeforeUnload() {
let ret : Map<string, any> = new Map();
this.$emit('stop', ret)
this.$el?.unbindCamera()
},
/**
* [可选实现] 原生View已释放,这里可以做释放View之后的操作
*/
NVUnloaded() {
CameraManager.getInstance().dispose();
},
/**
* [可选实现] 组件销毁
*/
unmounted() {
},
}
</script>
<style>
</style>
@@ -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;
}
}
@@ -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<string, AVCaptureSession.Preset>([
['low', AVCaptureSession.Preset.low],
['medium', AVCaptureSession.Preset.medium],
['high', AVCaptureSession.Preset.high],
])
const FLASH_MODE_MAP = new Map<string, AVCaptureDevice.TorchMode>([
['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<UIView>;
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<UIView>, 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<string, any> = new Map();
ret.set("maxZoom", device.maxAvailableVideoZoomFactor)
let detail : Map<string, any> = new Map();
detail.set("detail", ret)
this.comp?.$emit("ready", detail);
} catch (e) {
let ret : Map<string, any> = new Map();
ret.set("errMsg", JSON.stringify(e))
let detail : Map<string, any> = 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)
}
}
@@ -0,0 +1,3 @@
{
"deploymentTarget": "11"
}
@@ -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)
}
@@ -0,0 +1,167 @@
<template>
<view>
</view>
</template>
<script lang="uts">
import { UIView } from "UIKit"
import { CameraView } from "./camera";
import { CameraConfig } from '../interface'
import { CameraManager } from "./CameraManager";
//原生提供以下属性或方法的实现
export default {
data() {
return {
};
},
/**
* 组件名称,也就是开发者使用的标签
*/
name: "xf-camera",
/**
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
*/
emits: ['stop', 'error', 'ready'],
/**
* 属性声明,组件的使用者会传递这些属性值到组件
*/
props: {
"mode": {
type: String,
default: "normal"
},
"resolution": {
type: String,
default: "medium" // low | high
},
"position": {
type: String,
default: "back" // front前置 |back 后置
},
"flash": {
type: String,
default: "auto" // auto, on, off, torch
},
"frameSize": {
type: String,
default: "medium" // small|large
}
},
/**
* 属性变化监听器实现
*/
watch: {
"position": {
/**
* 这里监听属性变化,并进行组件内部更新
*/
handler(newValue : String, _oldValue : String) {
CameraManager.getInstance().getCameraView()?.switchCamera(newValue)
},
/**
* 创建时是否通过此方法更新属性,默认值为false
*/
immediate: false
},
"flash": {
/**
* 这里监听属性变化,并进行组件内部更新
*/
handler(newValue : String, _oldValue : String) {
CameraManager.getInstance().getCameraView()?.setFlash(newValue)
},
/**
* 创建时是否通过此方法更新属性,默认值为false
*/
immediate: false
}
},
/**
* 规则:如果没有配置expose,则methods中的方法均对外暴露,如果配置了expose,则以expose的配置为准向外暴露
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
*/
expose: [],
methods: {
},
/**
* 组件被创建,组件第一个生命周期,
* 在内存中被占用的时候被调用,开发者可以在这里执行一些需要提前执行的初始化逻辑
* [可选实现]
*/
created() {
},
/**
* 对应平台的view载体即将被创建,对应前端beforeMount
* [可选实现]
*/
NVBeforeLoad() {
},
/**
* 创建原生View,必须定义返回值类型
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
* [必须实现]
*/
NVLoad() : UIView {
let previewView = new UIView()
previewView.tag = 9527
return previewView
},
/**
* 原生View已创建
* [可选实现]
*/
NVLoaded() {
},
/**
* 原生View布局完成
* [可选实现]
*/
NVLayouted() {
let previewView = this.$el as UIView;
let cameraView = new CameraView(previewView, this, {
resolution: this.resolution,
frameSize: this.frameSize,
devicePosition: this.position,
flash: this.flash
} as CameraConfig)
CameraManager.getInstance().setCameraView(cameraView);
},
/**
* 原生View将释放
* [可选实现]
*/
NVBeforeUnload() {
let ret : Map<string, any> = new Map();
let detail : Map<string, any> = new Map();
detail.set("detail", ret)
this.$emit("stop", detail);
CameraManager.getInstance().getCameraView()?.unbindCamera()
},
/**
* 原生View已释放,这里可以做释放View之后的操作
* [可选实现]
*/
NVUnloaded() {
CameraManager.getInstance().dispose()
},
/**
* 组件销毁
* [可选实现]
*/
unmounted() {
}
}
</script>
<style>
</style>
@@ -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