init
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user