1
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
import { AVCaptureSession, AVAssetExportPresetHighestQuality, AVCaptureDevice, AVMediaType, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureDeviceInput, AVCaptureInput, AVCaptureVideoDataOutput, AVCaptureConnection, AVCaptureVideoPreviewLayer, AVLayerVideoGravity, AVCaptureVideoOrientation, AVCaptureMovieFileOutput, AVCaptureFileOutputRecordingDelegate, AVCaptureOutput, AVCaptureFileOutput, AVAsset, AVAssetExportSession, AVFileType } from 'AVFoundation';
|
||||
import { PHPhotoLibrary, PHAuthorizationStatus } from 'Photos';
|
||||
import { CMSampleBuffer, CMSampleBufferGetFormatDescription, CMFormatDescription, CMVideoFormatDescriptionGetDimensions, CMSampleBufferGetImageBuffer } from 'CoreMedia';
|
||||
import { DispatchQueue } from "Dispatch"
|
||||
import { UIView, UIImage, UISaveVideoAtPathToSavedPhotosAlbum, UIImageWriteToSavedPhotosAlbum, UIApplication, UIImageView, UILabel, UIViewController, UIFontDescriptor, UIFont, NSTextAlignment, UITapGestureRecognizer, UIAlertController, UIGraphicsImageRenderer, UIGraphicsRendererContext, UIBezierPath, CameraDevice, UIGraphicsGetCurrentContext, UIGraphicsBeginImageContextWithOptions, UIGraphicsGetImageFromCurrentImageContext, UIGraphicsEndImageContext, UIDevice, UIDeviceOrientation } from 'UIKit';
|
||||
import { CGRect, CGFloat, CGPoint, CGAffineTransform, CGSize } from 'CoreFoundation';
|
||||
|
||||
import { NotificationCenter, NSNotification, Notification, FileManager, Data, URL, NSError } from 'Foundation';
|
||||
import { Selector } from 'ObjectiveC';
|
||||
import { Alignment } from 'ARKit';
|
||||
import { CIImage, CIContext } from 'CoreImage';
|
||||
import { CGPath } from 'CoreGraphics';
|
||||
|
||||
type TAKE_CALL_BACK = (path : CMSampleBuffer) => void;
|
||||
|
||||
|
||||
let captureSession : null | AVCaptureSession = null;
|
||||
// 图像捕捉事件代理。
|
||||
let captureOutDelegate : CaptureOutSessionBuffer | null = null;
|
||||
let recorderingDelegate : recorderingDelegateObj | null = null;
|
||||
let isOpeing : boolean = false;
|
||||
let taking : boolean = false;
|
||||
let takingTaron = false
|
||||
let takePhotoCall = (path : string) => { }
|
||||
let takvideoRecoderCall = (path : string) => { }
|
||||
class CaptureOutSessionBuffer implements AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
callback : TAKE_CALL_BACK = (imgBuffer : CMSampleBuffer) => { }
|
||||
camrea : xCamrea;
|
||||
camreaDevsBuilder : AVCaptureDevice
|
||||
constructor(dv : TAKE_CALL_BACK, ca : xCamrea, cdv : AVCaptureDevice) {
|
||||
this.callback = dv;
|
||||
this.camrea = ca;
|
||||
this.camreaDevsBuilder = cdv;
|
||||
super()
|
||||
}
|
||||
captureOutput(output : AVCaptureOutput, @argumentLabel("didOutput") sampleBuffer : CMSampleBuffer, @argumentLabel("from") connection : AVCaptureConnection) {
|
||||
|
||||
if (taking) {
|
||||
|
||||
taking = false
|
||||
// this.camrea.pasue()
|
||||
|
||||
takingTaron = false;
|
||||
let islock = UTSiOS.try(this.camreaDevsBuilder.lockForConfiguration(), "?")
|
||||
if (islock != null) {
|
||||
if (this.camreaDevsBuilder.isTorchActive) {
|
||||
this.camreaDevsBuilder.torchMode = AVCaptureDevice.TorchMode.off
|
||||
}
|
||||
this.camreaDevsBuilder.unlockForConfiguration()
|
||||
}
|
||||
|
||||
|
||||
this.callback(sampleBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class recorderingDelegateObj implements AVCaptureFileOutputRecordingDelegate {
|
||||
callback : TAKE_CALL_BACK = (imgBuffer : CMSampleBuffer) => { }
|
||||
// 保存到相册的回调方法
|
||||
@objc video(videoPath : String,
|
||||
@argumentLabel("didFinishSavingWithError") error ?: NSError,
|
||||
contextInfo ?: UnsafeMutableRawPointer) {
|
||||
if (error != null) {
|
||||
console.error("tmui4x:Error recording movie: ", error)
|
||||
return
|
||||
}
|
||||
console.log("Video saved successfully")
|
||||
}
|
||||
|
||||
fileOutput(
|
||||
output : AVCaptureFileOutput,
|
||||
@argumentLabel("didStartRecordingTo") fileURL : URL,
|
||||
@argumentLabel("from") connections : AVCaptureConnection[]) {
|
||||
// console.log("开始录制")
|
||||
}
|
||||
fileOutput(
|
||||
output : AVCaptureFileOutput,
|
||||
@argumentLabel("didFinishRecordingTo") outputFileURL : URL,
|
||||
@argumentLabel("from") connections : AVCaptureConnection[],
|
||||
@argumentLabel("error") error ?: NSError) {
|
||||
if (error != null) {
|
||||
console.error("tmui4x:Error recording movie: ", error)
|
||||
return
|
||||
}
|
||||
// UISaveVideoAtPathToSavedPhotosAlbum(
|
||||
// outputFileURL.path,
|
||||
// nil,
|
||||
// #selector(this.video),
|
||||
// nil
|
||||
// )
|
||||
// recorderingDelegate = null;
|
||||
let mp4FileURL = outputFileURL.deletingPathExtension().appendingPathExtension("mp4") // 转换后的文件路径
|
||||
let _this = this;
|
||||
convertMovToMp4(outputFileURL, mp4FileURL, (okmp4str : string) => {
|
||||
// console.log('end...recodervideo', okmp4str)
|
||||
takvideoRecoderCall(okmp4str)
|
||||
// 保存到相册
|
||||
// UISaveVideoAtPathToSavedPhotosAlbum(
|
||||
// mp4FileURL.path,
|
||||
// nil,
|
||||
// #selector(_this.video),
|
||||
// nil
|
||||
// )
|
||||
recorderingDelegate = null;
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function getUid(rdix = 1, length = 12) : string {
|
||||
let ix = "";
|
||||
// #ifdef APP
|
||||
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
|
||||
// #endif
|
||||
return ix;
|
||||
}
|
||||
function getDevRation():AVCaptureVideoOrientation{
|
||||
// let ration = 0
|
||||
let deviceOrientation = UIDevice.current.orientation
|
||||
switch (deviceOrientation) {
|
||||
case UIDeviceOrientation.portrait:
|
||||
console.log("竖屏正向")
|
||||
// ration = 90
|
||||
return AVCaptureVideoOrientation.portrait
|
||||
|
||||
case UIDeviceOrientation.portraitUpsideDown:
|
||||
console.log("竖屏倒向")
|
||||
return AVCaptureVideoOrientation.portraitUpsideDown
|
||||
case UIDeviceOrientation.landscapeLeft:
|
||||
console.log("横屏左")
|
||||
return AVCaptureVideoOrientation.landscapeRight
|
||||
case UIDeviceOrientation.landscapeRight:
|
||||
console.log("横屏右")
|
||||
return AVCaptureVideoOrientation.landscapeLeft
|
||||
case UIDeviceOrientation.faceDown:
|
||||
console.log("屏幕朝下")
|
||||
break;
|
||||
case UIDeviceOrientation.unknown:
|
||||
console.log("未知方向")
|
||||
break;
|
||||
default:
|
||||
console.log("其他方向")
|
||||
}
|
||||
|
||||
return AVCaptureVideoOrientation.portrait
|
||||
}
|
||||
function fixImageOrientation(image : UIImage) : UIImage {
|
||||
let cgImage = image.cgImage!
|
||||
const width = cgImage.width
|
||||
const height = cgImage.width
|
||||
|
||||
// let ration = 0
|
||||
// 计算绘制尺寸
|
||||
let drawRect = CGRect.zero
|
||||
let imageSize = image.size
|
||||
// 获取设备物理方向
|
||||
let deviceOrientation = UIDevice.current.orientation
|
||||
if (deviceOrientation == UIDeviceOrientation.portrait) {
|
||||
drawRect.size = new CGSize(width = imageSize.height, height = imageSize.width)
|
||||
}else{
|
||||
drawRect.size = image.size
|
||||
}
|
||||
// 竖屏正向
|
||||
if (deviceOrientation == UIDeviceOrientation.portrait) {
|
||||
// ration = 90
|
||||
UIGraphicsBeginImageContextWithOptions(drawRect.size, false, image.scale)
|
||||
// 获取当前上下文
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
// 逆时针旋转90度
|
||||
context.translateBy(x = drawRect.size.width, y = 0)
|
||||
context.rotate(by = new CGFloat(Math.PI / 2))
|
||||
} else if (deviceOrientation == UIDeviceOrientation.landscapeRight) {
|
||||
// ration = 180
|
||||
UIGraphicsBeginImageContextWithOptions(drawRect.size, false, image.scale)
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
context.translateBy(x = drawRect.size.width, y = drawRect.size.height)
|
||||
context.rotate(by = new CGFloat(Math.PI))
|
||||
|
||||
} else if (deviceOrientation == UIDeviceOrientation.landscapeLeft) {
|
||||
|
||||
}
|
||||
|
||||
image.draw(in = new CGRect(origin = CGPoint.zero, size = imageSize))
|
||||
let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return normalizedImage == null ? image : normalizedImage!
|
||||
}
|
||||
|
||||
|
||||
function convertMovToMp4(movFileURL : URL, mp4FileURL : URL, ok : (str : string) => void) {
|
||||
// 加载 AVAsset
|
||||
let asset = new AVAsset(url = movFileURL)
|
||||
let exportSession = new AVAssetExportSession(asset = asset, presetName = AVAssetExportPresetHighestQuality)
|
||||
// 创建 AVAssetExportSession
|
||||
if (exportSession == null) {
|
||||
ok('')
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置导出设置
|
||||
exportSession!.outputURL = mp4FileURL
|
||||
exportSession!.outputFileType = AVFileType.mp4
|
||||
exportSession!.shouldOptimizeForNetworkUse = true
|
||||
exportSession!.exportAsynchronously(completionHandler = () => {
|
||||
switch (exportSession!.status) {
|
||||
case AVAssetExportSession.Status.completed:
|
||||
// console.log("导出成功", mp4FileURL.path)
|
||||
ok(mp4FileURL.path)
|
||||
break
|
||||
case AVAssetExportSession.Status.failed:
|
||||
if (exportSession!.error != null) {
|
||||
// console.error("导出失败", mp4FileURL.path)
|
||||
ok('')
|
||||
}
|
||||
break
|
||||
case AVAssetExportSession.Status.cancelled:
|
||||
// console.error("导出被取消")
|
||||
ok('')
|
||||
break
|
||||
default:
|
||||
ok('')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
export class xCamrea {
|
||||
parentView : UIView | null = null;
|
||||
camreaViewLayer : UIView | null = null;
|
||||
imageUiView : UIImageView | null = null;
|
||||
isSaveToPhoto : boolean = false;
|
||||
//摄像头朝向,默认是向后置摄像头.
|
||||
CamerDeviceDir = AVCaptureDevice.Position.back
|
||||
//摄像头的是否开
|
||||
flashMode = false
|
||||
// 是否拍照 中
|
||||
isTakePhoto = false
|
||||
// AVCaptureMovieFileOutput
|
||||
movieFileOutput : AVCaptureMovieFileOutput | null = null;
|
||||
takeModelType = 'photo'; //photo,video
|
||||
isPaused = false
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
setView(view : UIView) {
|
||||
this.parentView = view;
|
||||
}
|
||||
getIsOpeningCameraing() : boolean {
|
||||
return captureSession != null
|
||||
}
|
||||
openCamrea() {
|
||||
this.closeCamrea();
|
||||
if (captureSession == null) {
|
||||
this._openCamrea()
|
||||
}
|
||||
}
|
||||
|
||||
startRecoderVideo(call : (path : string) => void) {
|
||||
takvideoRecoderCall = call;
|
||||
let _this = this;
|
||||
if (_this.movieFileOutput?.isRecording == true && _this.isPaused) {
|
||||
// if (UTSiOS.available("iOS 18.0, *")) {
|
||||
// _this.movieFileOutput?.resumeRecording()
|
||||
// _this.isPaused = false;
|
||||
// }
|
||||
} else {
|
||||
if (isOpeing || _this.movieFileOutput != null) {
|
||||
this.stopRecoderVideo()
|
||||
this.closeCamrea()
|
||||
}
|
||||
_this.takeModelType = 'video'
|
||||
_this._openCamrea();
|
||||
}
|
||||
}
|
||||
pauseRecoderVideo() {
|
||||
let _this = this;
|
||||
// if (UTSiOS.available("iOS 18.0, *")) {
|
||||
// _this.movieFileOutput?.pauseRecording()
|
||||
// _this.isPaused = true;
|
||||
// }
|
||||
|
||||
}
|
||||
stopRecoderVideo() {
|
||||
let _this = this;
|
||||
_this.takeModelType = 'photo'
|
||||
_this.movieFileOutput?.stopRecording()
|
||||
_this.isPaused = false;
|
||||
_this.movieFileOutput = null;
|
||||
|
||||
}
|
||||
|
||||
recoderVideoCan() {
|
||||
if (this.movieFileOutput == null || isOpeing == false) {
|
||||
// console.error('no recoder video by tmui4x,')
|
||||
return;
|
||||
}
|
||||
let userDir = FileManager.default.urls(for = FileManager.SearchPathDirectory.cachesDirectory, in = FileManager.SearchPathDomainMask.userDomainMask).first!
|
||||
let filename = 'tmui4xCamreaVido' + getUid(1, 12) + '.mov';
|
||||
let outputURL = userDir.appendingPathComponent(filename) as URL
|
||||
// AVCaptureFileOutputRecordingDelegate
|
||||
recorderingDelegate = new recorderingDelegateObj()
|
||||
// 开始录制
|
||||
this.movieFileOutput!.startRecording(to = outputURL, recordingDelegate = recorderingDelegate!)
|
||||
|
||||
this.isPaused = false
|
||||
}
|
||||
takePhoto(call : (path : string) => void, savetophoto : boolean) {
|
||||
|
||||
this.isSaveToPhoto = savetophoto
|
||||
// isOpeing = false;
|
||||
taking = true;
|
||||
takePhotoCall = call;
|
||||
|
||||
}
|
||||
savePhoto(imageData : UIImage) {
|
||||
DispatchQueue.main.asyncAfter(deadline = (DispatchTime.now() + 1) as DispatchTime, execute = () => {
|
||||
new UIImageWriteToSavedPhotosAlbum(imageData, nil, nil, nil)
|
||||
let alertController = new UIAlertController(title = "提醒", message = "保存成功", preferredStyle = UIAlertController.Style.alert);
|
||||
UTSiOS.getCurrentViewController().present(alertController, animated = true, completion = () => {
|
||||
alertController.dismiss(animated = true, completion = nil)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
setCameraDir(type : string) {
|
||||
this.CamerDeviceDir = type == 'back' ? AVCaptureDevice.Position.back : AVCaptureDevice.Position.front
|
||||
}
|
||||
setFlash(type : boolean) {
|
||||
this.flashMode = type;
|
||||
}
|
||||
saveCacheDir(imgs : UIImage) {
|
||||
let img = fixImageOrientation(imgs)
|
||||
if (this.isSaveToPhoto) {
|
||||
this.savePhoto(img)
|
||||
}
|
||||
// 获得当前用户的缓存目录路径主目录。
|
||||
let userDir = FileManager.default.urls(for = FileManager.SearchPathDirectory.cachesDirectory, in = FileManager.SearchPathDomainMask.userDomainMask).first!
|
||||
let filename = 'tmui4xCamrea' + getUid(1, 12) + '.jpg';
|
||||
let destinationURL = userDir.appendingPathComponent(filename)
|
||||
let imageData = img.jpegData(compressionQuality = 1);
|
||||
if (imageData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let real = UTSiOS.try(imageData!.write(to = destinationURL), "?");
|
||||
|
||||
if (real != null) {
|
||||
takePhotoCall(destinationURL.path)
|
||||
}
|
||||
|
||||
}
|
||||
closeCamrea() {
|
||||
this.pasue()
|
||||
if (this.camreaViewLayer != null) {
|
||||
this.camreaViewLayer!.removeFromSuperview()
|
||||
}
|
||||
if (this.imageUiView != null) {
|
||||
this.imageUiView!.removeFromSuperview()
|
||||
}
|
||||
this.camreaViewLayer = null
|
||||
this.imageUiView = null
|
||||
// this.movieFileOutput?.stopRecording()
|
||||
this.movieFileOutput = null;
|
||||
recorderingDelegate = null;
|
||||
}
|
||||
pasue() {
|
||||
if (captureSession != null) {
|
||||
if (captureSession!.isRunning) {
|
||||
captureSession!.stopRunning()
|
||||
}
|
||||
for (input in captureSession!.inputs) {
|
||||
captureSession!.removeInput(input)
|
||||
}
|
||||
for (output in captureSession!.outputs) {
|
||||
captureSession!.removeOutput(output)
|
||||
}
|
||||
}
|
||||
captureSession = null;
|
||||
captureOutDelegate = null;
|
||||
isOpeing = false;
|
||||
taking = false
|
||||
|
||||
}
|
||||
private _openCamrea() {
|
||||
let t = this;
|
||||
isOpeing = true;
|
||||
captureSession = new AVCaptureSession();
|
||||
if (this.flashMode) {
|
||||
takingTaron = true;
|
||||
} else {
|
||||
takingTaron = false
|
||||
}
|
||||
let captureDevice = AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera, for = AVMediaType.video, position = this.CamerDeviceDir)
|
||||
|
||||
let captureOut = new AVCaptureVideoDataOutput();
|
||||
captureSession!.sessionPreset = AVCaptureSession.Preset.photo
|
||||
captureOutDelegate = new CaptureOutSessionBuffer((result : CMSampleBuffer) => {
|
||||
|
||||
let pixelBuffer = CMSampleBufferGetImageBuffer(result)
|
||||
if (pixelBuffer == null) return;
|
||||
let ciImage = new CIImage(cvPixelBuffer = pixelBuffer!)
|
||||
let context = new CIContext(options = nil)
|
||||
if (context != null) {
|
||||
let cgImage = context.createCGImage(ciImage, from = ciImage.extent)
|
||||
let image = new UIImage(cgImage = cgImage!)
|
||||
// t.rotateImage(image, Math.PI/2)
|
||||
t.saveCacheDir(image)
|
||||
}
|
||||
|
||||
}, this, captureDevice!)
|
||||
|
||||
// 新建设备的输入设备
|
||||
let inputDevice = UTSiOS.try(new AVCaptureDeviceInput(device = captureDevice!), "?")
|
||||
if (inputDevice != null) {
|
||||
// 添加输入到会话中。
|
||||
captureSession!.addInput(inputDevice!)
|
||||
}
|
||||
|
||||
captureOut.alwaysDiscardsLateVideoFrames = true;
|
||||
// 设置输出会话
|
||||
captureOut.setSampleBufferDelegate(captureOutDelegate!, queue = DispatchQueue.global())
|
||||
if (captureSession!.canAddOutput(captureOut)) {
|
||||
captureSession!.addOutput(captureOut)
|
||||
console.log("绑定输出层正确!")
|
||||
}
|
||||
|
||||
this.xCamreaInit(captureDevice!);
|
||||
}
|
||||
|
||||
xCamreaInit(cdv : AVCaptureDevice) {
|
||||
let camreaDevsBuilder : AVCaptureDevice = cdv
|
||||
|
||||
|
||||
let t = this;
|
||||
|
||||
let parentView = this.parentView!
|
||||
|
||||
|
||||
// 绘制界面
|
||||
// 创建一张图片预览层。
|
||||
|
||||
// imageUiView!.frame = this.parentView!.frame
|
||||
// imageUiView!.alpha = new CGFloat(0)
|
||||
|
||||
// parentView!.addSubview(imageUiView!);
|
||||
|
||||
// 创建视频预览层。
|
||||
let camelayer = new UIView()
|
||||
let parentCGrect = parentView.layer.bounds
|
||||
let width = Int(parentCGrect.width)
|
||||
let height = Int(parentCGrect.height)
|
||||
camelayer.frame = new CGRect(x = 0, y = 0, width = width, height = height)
|
||||
this.camreaViewLayer = camelayer;
|
||||
|
||||
let isopenRecoderVideo = true;
|
||||
|
||||
if (this.takeModelType == 'video') {
|
||||
console.log('开始录制视频')
|
||||
this.movieFileOutput = new AVCaptureMovieFileOutput();
|
||||
let audioDevice = AVCaptureDevice.default(for = AVMediaType.audio)
|
||||
let audioInput = UTSiOS.try(new AVCaptureDeviceInput(device = audioDevice!), '?')
|
||||
console.log('配置声音', audioInput)
|
||||
if (audioInput != null && captureSession!.canAddInput(audioInput!)) {
|
||||
captureSession!.addInput(audioInput!)
|
||||
}
|
||||
console.log('配置输出视频文件', this.movieFileOutput)
|
||||
if (captureSession!.canAddOutput(this.movieFileOutput!)) {
|
||||
captureSession!.addOutput(this.movieFileOutput!)
|
||||
} else {
|
||||
isopenRecoderVideo = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
let cameraView = new AVCaptureVideoPreviewLayer(session = captureSession!)
|
||||
|
||||
cameraView.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
// AVCaptureVideoOrientation.portrait
|
||||
cameraView.connection!.videoOrientation = getDevRation()
|
||||
cameraView.frame = new CGRect(x = 0, y = 0, width = width, height = height)
|
||||
|
||||
|
||||
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
parentView.addSubview(camelayer);
|
||||
// 绑定渲染层。
|
||||
camelayer.layer.addSublayer(cameraView)
|
||||
captureSession!.startRunning()
|
||||
|
||||
t.isPaused = false
|
||||
|
||||
if (t.takeModelType == 'video') {
|
||||
if (isopenRecoderVideo) {
|
||||
setTimeout(function () {
|
||||
t.recoderVideoCan()
|
||||
}, 500);
|
||||
} else {
|
||||
takvideoRecoderCall('')
|
||||
}
|
||||
}
|
||||
|
||||
if (takingTaron && !camreaDevsBuilder.isTorchActive) {
|
||||
let islock = UTSiOS.try(camreaDevsBuilder.lockForConfiguration(), "?")
|
||||
if (islock != null) {
|
||||
camreaDevsBuilder.torchMode = AVCaptureDevice.TorchMode.on
|
||||
camreaDevsBuilder.unlockForConfiguration()
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user