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