import { UIImage, UIView } from 'UIKit'; import { AVCaptureDevice, AVCaptureDeviceInput, AVCaptureSession, AVCaptureVideoPreviewLayer, AVMediaType, AVLayerVideoGravity, AVCaptureVideoDataOutput, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureOutput, AVCaptureConnection, AVCapturePhotoOutput, AVCapturePhotoSettings, AVCapturePhotoCaptureDelegate, AVCaptureMovieFileOutput, AVCaptureFileOutputRecordingDelegate, AVCaptureFileOutput } from "AVFoundation"; import { CMSampleBuffer, CMSampleBufferGetImageBuffer } from 'CoreMedia'; import { DispatchQueue } from 'Dispatch'; import { CVPixelBufferGetWidth, CVPixelBufferGetHeight } from "CoreVideo"; import { NSError, URL } from "Foundation"; import { TakePhotoOption, GeneralCallbackResult, TakePhotoSuccessCallbackResult, CameraContextSetZoomOption, SetZoomSuccessCallbackResult, CameraContextStopRecordOption, CameraContextStartRecordOption, StartRecordTimeoutCallbackResult, StopRecordSuccessCallbackResult, OnCameraFrameCallback, OnCameraFrameCallbackResult, OnCameraFrameListenerOption, CameraFrameListenerStartOption, StopOption, CameraConfig, FlashMode, DevicePosition, Resolution } from '../interface' import { AVCapturePhoto } from 'AVFoundation'; const RESOLUTION_MAP = new Map([ ['low', AVCaptureSession.Preset.low], ['medium', AVCaptureSession.Preset.medium], ['high', AVCaptureSession.Preset.high], ]) const FLASH_MODE_MAP = new Map([ ['auto', AVCaptureDevice.TorchMode.auto], ['on', AVCaptureDevice.TorchMode.on], ['torch', AVCaptureDevice.TorchMode.on], ['off', AVCaptureDevice.TorchMode.off], ]) let fileOutputRecordingDelegate : MyAVCaptureFileOutputRecordingDelegate | null = null; let capturePhotoCaptureDelegate : MyAVCapturePhotoCaptureDelegate | null = null; let onCameraFrameListenerOption : OnCameraFrameListenerOption | null = null; let cameraContextStopRecordOption : CameraContextStopRecordOption | null = null; class MyAVCaptureFileOutputRecordingDelegate implements AVCaptureFileOutputRecordingDelegate { private option : CameraContextStartRecordOption; constructor(option : CameraContextStartRecordOption) { this.option = option; } fileOutput(output : AVCaptureFileOutput, @argumentLabel("didStartRecordingTo") fileURL : URL, @argumentLabel("from") connections : [AVCaptureConnection]) : void { this.option.success?.({ errMsg: 'ok' } as GeneralCallbackResult) } fileOutput(output : AVCaptureFileOutput, @argumentLabel("didFinishRecordingTo") outputFileURL : URL, @argumentLabel("from") connections : [AVCaptureConnection], @argumentLabel("error") error ?: NSError) : void { if (error == null) { cameraContextStopRecordOption?.success?.({ tempVideoPath: outputFileURL.path + "", errMsg: 'ok' } as StopRecordSuccessCallbackResult) this.option.timeoutCallback?.({ tempVideoPath: outputFileURL.path + "" } as StartRecordTimeoutCallbackResult) } else { cameraContextStopRecordOption?.fail?.({ errMsg: '录制失败' } as GeneralCallbackResult) cameraContextStopRecordOption?.complete?.({ errMsg: '录制失败' } as GeneralCallbackResult) } } } class MyAVCapturePhotoCaptureDelegate implements AVCapturePhotoCaptureDelegate { private option : TakePhotoOption; constructor(option : TakePhotoOption) { this.option = option; } captureOutput(output : AVCapturePhotoOutput, @argumentLabel("didFinishProcessingPhoto") photo : AVCapturePhoto, @argumentLabel("error") error : Error) : void { let imageData = photo.fileDataRepresentation() if (imageData == null) { const result : GeneralCallbackResult = { errMsg: "拍照失败" } this.option.fail?.(result) this.option.complete?.(result) return; } let path = UTSiOS.getDataPath() + (new Date().getTime()).toString() + ".jpg"; let url = new URL(fileURLWithPath = path); let quality = this.option.quality ?? "original" if (quality == 'original') { try { UTSiOS.try(imageData!.write(to = url, options = NSData.WritingOptions.atomic)) this.option.success?.({ tempImagePath: path, quality: quality, errMsg: 'ok' } as TakePhotoSuccessCallbackResult) this.option.complete?.({ errMsg: 'ok' } as GeneralCallbackResult) } catch (e) { const result : GeneralCallbackResult = { errMsg: JSON.stringify(e) } this.option.fail?.(result) this.option.complete?.(result) } } else { let image = UIImage(data = imageData!) if (image != null) { let _quality : number = 0.5; if (quality == 'high') { _quality = 1.0; } else if (quality == 'normal') { _quality = 0.8; } else if (quality == 'low') { _quality = 0.5; } let data = image!.jpegData(compressionQuality = _quality.toDouble())! try { UTSiOS.try(data.write(to = url, options = NSData.WritingOptions.atomic)) this.option.success?.({ tempImagePath: path, quality: quality, errMsg: 'ok' } as TakePhotoSuccessCallbackResult) this.option.complete?.({ errMsg: 'ok' } as GeneralCallbackResult) } catch (e) { const result : GeneralCallbackResult = { errMsg: JSON.stringify(e) } this.option.fail?.(result) this.option.complete?.(result) } } else { const result : GeneralCallbackResult = { errMsg: "拍照失败" } this.option.fail?.(result) this.option.complete?.(result) } } } } export class CameraView implements AVCaptureVideoDataOutputSampleBufferDelegate { private captureSession ?: AVCaptureSession; private videoDeviceInput ?: AVCaptureDeviceInput; private photoDeviceOutput ?: AVCapturePhotoOutput; private movieDeviceOutput ?: AVCaptureMovieFileOutput; private previewLayer ?: AVCaptureVideoPreviewLayer; private previewView ?: UIView; private comp ?: UTSComponent; private flashMode : FlashMode = 'auto' private devicePosition : DevicePosition = 'back' private resolution : Resolution = 'medium' private isListening : boolean = false private onCameraFrameCallback : OnCameraFrameCallback | null = null; constructor(previewView : UIView, comp : UTSComponent, config : CameraConfig = {} as CameraConfig) { super.init(); this.previewView = previewView this.comp = comp this.flashMode = config.flash ?? 'auto' this.devicePosition = config.devicePosition ?? 'back' this.resolution = config.resolution ?? 'medium' this.setupCamera(); } setupCamera() { this.captureSession = new AVCaptureSession(); let targetResolution = RESOLUTION_MAP.get(this.resolution) ?? AVCaptureSession.Preset.high; this.captureSession!.sessionPreset = targetResolution; let device = this.getCameraDevice(); try { //相机 let deviceInput = UTSiOS.try(new AVCaptureDeviceInput(device = device)) if (this.captureSession!.canAddInput(deviceInput)) { this.captureSession!.addInput(deviceInput); } //实时流输出 let output = new AVCaptureVideoDataOutput(); output.setSampleBufferDelegate(this, queue = DispatchQueue.main); if (this.captureSession!.canAddOutput(output)) { this.captureSession!.addOutput(output); } let photoOutput = new AVCapturePhotoOutput(); // let photoSettings = new AVCapturePhotoSettings(format = [AVVideoCodecKey = AVVideoCodecType.jpeg]); // photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler = null); if (this.captureSession!.canAddOutput(photoOutput)) { this.captureSession!.addOutput(photoOutput); } let movieOutput = new AVCaptureMovieFileOutput(); if (this.captureSession!.canAddOutput(movieOutput)) { this.captureSession!.addOutput(movieOutput); } this.videoDeviceInput = deviceInput; this.photoDeviceOutput = photoOutput; this.movieDeviceOutput = movieOutput; this.previewLayer = new AVCaptureVideoPreviewLayer(session = this.captureSession!) this.previewLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill; this.previewLayer!.frame = this.previewView!.layer.bounds; this.previewView!.layer.addSublayer(this.previewLayer!) this.captureSession!.startRunning(); let ret : Map = new Map(); ret.set("maxZoom", device.maxAvailableVideoZoomFactor) let detail : Map = new Map(); detail.set("detail", ret) this.comp?.$emit("ready", detail); } catch (e) { let ret : Map = new Map(); ret.set("errMsg", JSON.stringify(e)) let detail : Map = new Map(); detail.set("detail", ret) this.comp?.$emit("error", detail); } } getCameraDevice() : AVCaptureDevice { let cameras = AVCaptureDevice.devices(for = AVMediaType.video) let frontCamera : AVCaptureDevice | null = null; let backCamera : AVCaptureDevice | null = null; let index = 0; while (index < cameras.length) { if (index == cameras.length) { break; } let camera = cameras[index.toInt()] as AVCaptureDevice; if (camera.position == AVCaptureDevice.Position.back) { backCamera = camera; } else { frontCamera = camera; } index++; } if (this.devicePosition == 'back') { return backCamera!; } else { return frontCamera!; } } captureOutput(output : AVCaptureOutput, @argumentLabel("didOutput") sampleBuffer : CMSampleBuffer, @argumentLabel("from") connection : AVCaptureConnection) : void { if (this.isListening) { let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) let width = CVPixelBufferGetWidth(imageBuffer!) let height = CVPixelBufferGetHeight(imageBuffer!); const res : OnCameraFrameCallbackResult = { width: width as Int, height: height as Int, data: imageBuffer! }; this.onCameraFrameCallback?.(res) onCameraFrameListenerOption?.success?.({ width: width as Int, height: height as Int, data: imageBuffer! } as OnCameraFrameCallbackResult) // this.onCameraFrameListenerOption?.success({ // width: width as Int, // height: height as Int, // data: imageBuffer! // } as OnCameraFrameCallbackResult) // if (imageBuffer != null) { // let ciImage = new CIImage(cvPixelBuffer = imageBuffer!) // let temporaryContext = new CIContext(options = null) // let width = CVPixelBufferGetWidth(imageBuffer!) // let height = CVPixelBufferGetHeight(imageBuffer!); // let videoImage = temporaryContext.createCGImage(ciImage, from = new CGRect(x = 0, y = 0, width = width, height = height)) // let image = new UIImage(cgImage = videoImage!) // this.onCameraFrameCallback?.({ // width: width as Int, // height: height as Int, // data: image // } as OnCameraFrameCallbackResult) // } } } bindCamera() { this.captureSession!.startRunning(); } unbindCamera() { this.captureSession!.stopRunning(); } setConfig(config : CameraConfig = {} as CameraConfig) { this.flashMode = config.flash ?? 'auto' this.devicePosition = config.devicePosition ?? 'back' this.resolution = config.resolution ?? 'medium' } setFlash(mode : FlashMode) { this.flashMode = mode let targetFlash = FLASH_MODE_MAP.get(this.flashMode) ?? AVCaptureDevice.TorchMode.auto; try { let device = this.getCameraDevice(); if (device.hasTorch && device.isTorchAvailable) { UTSiOS.try(device.lockForConfiguration()); device.torchMode = targetFlash; device.unlockForConfiguration() } } catch (e) { console.log(e) } } switchCamera(position : DevicePosition) { if (!this.captureSession!.isRunning) { return; } this.devicePosition = position; try { this.captureSession!.beginConfiguration(); this.captureSession!.removeInput(this.videoDeviceInput!); let device = this.getCameraDevice(); let deviceInput = UTSiOS.try(new AVCaptureDeviceInput(device = device)) this.captureSession!.addInput(deviceInput); this.videoDeviceInput = deviceInput; this.captureSession!.commitConfiguration(); } catch (e) { console.log(e) } } takePhoto(option : TakePhotoOption) { capturePhotoCaptureDelegate = new MyAVCapturePhotoCaptureDelegate(option); let settings = new AVCapturePhotoSettings() this.photoDeviceOutput?.capturePhoto(with = settings, delegate = capturePhotoCaptureDelegate!); } setZoom(option : CameraContextSetZoomOption) { try { let device = this.getCameraDevice(); // if (device.isRampingVideoZoom) { UTSiOS.try(device.lockForConfiguration()); device.videoZoomFactor = option.zoom.toDouble(); device.unlockForConfiguration() option.success?.({ zoom: option.zoom, errMsg: 'ok' } as SetZoomSuccessCallbackResult) option.complete?.({ errMsg: 'ok' } as GeneralCallbackResult) // } else { // const result : GeneralCallbackResult = { // errMsg: '相机不支持缩放' // } // option.complete?.(result) // option.fail?.(result) // } } catch (e) { const result : GeneralCallbackResult = { errMsg: JSON.stringify(e) } option.complete?.(result) option.fail?.(result) } } startRecord(option : CameraContextStartRecordOption) { let isRecording = this.movieDeviceOutput?.isRecording ?? false; if (isRecording) { option.fail?.({ errMsg: '已经在录制或相机不存在' } as GeneralCallbackResult) option.complete?.({ errMsg: '已经在录制或相机不存在' } as GeneralCallbackResult) return } fileOutputRecordingDelegate = new MyAVCaptureFileOutputRecordingDelegate(option); let path = UTSiOS.getDataPath() + (new Date().getTime()).toString() + ".mp4"; let url = new URL(fileURLWithPath = path); this.movieDeviceOutput?.startRecording(to = url, recordingDelegate = fileOutputRecordingDelegate!); if (option.timeout != null && option.timeout! > 0) { setTimeout(() => { this.movieDeviceOutput?.stopRecording(); }, (option.timeout!).toInt()) } } stopRecord(option : CameraContextStopRecordOption) { let isRecording = this.movieDeviceOutput?.isRecording ?? false; if (!isRecording) { option.fail?.({ errMsg: '未开始录制' } as GeneralCallbackResult) option.complete?.({ errMsg: '未开始录制' } as GeneralCallbackResult) return } cameraContextStopRecordOption = option; this.movieDeviceOutput?.stopRecording(); } onCameraFrame(callback : OnCameraFrameCallback | null = null) { this.onCameraFrameCallback = callback } onCameraFrameListener(option : OnCameraFrameListenerOption) { onCameraFrameListenerOption = option } cameraFrameOnStart(option ?: CameraFrameListenerStartOption) { this.isListening = true option?.success?.({ errMsg: 'ok' } as GeneralCallbackResult) } cameraFrameOnStop(option ?: StopOption) { this.isListening = false option?.success?.({ errMsg: 'ok' } as GeneralCallbackResult) } }