save code
This commit is contained in:
@@ -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)
|
||||
初始化发布
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user