This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -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()
}
}
})
}
}