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,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="io.dcloud.uni_modules.xCamreaU">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
</manifest>
@@ -0,0 +1,11 @@
{
"minSdkVersion": "21",
"dependencies":[
"androidx.camera:camera-core:1.3.4",
"androidx.camera:camera-camera2:1.3.4",
"androidx.camera:camera-lifecycle:1.3.4",
"androidx.camera:camera-video:1.3.4",
"androidx.camera:camera-view:1.3.4",
"androidx.camera:camera-extensions:1.3.4"
]
}
@@ -0,0 +1,82 @@
import FrameLayout from "android.widget.FrameLayout";
import { xCamreaUOpts } from '../../utssdk/interface';
import { xCamrea } from "./libs/camrea.uts"
import LinearLayout from 'android.widget.LinearLayout';
import Color from 'android.graphics.Color';
import View from "android.view.View";
import ViewGroup from 'android.view.ViewGroup';
import TextView from 'android.widget.TextView'
/**
* 检查相机权限
*/
export function checkPermissions() : Promise<boolean> {
let permissionCheck = ["android.permission.CAMERA"]
return new Promise((res, rej) => {
if (UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, permissionCheck)) {
res(true)
} else {
console.log("当前不具备指定权限")
// 请求拍照权限
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionCheck, function (a : boolean, _ : string[]) {
res(true)
}, function (a : boolean, _ : string[]) {
//用户拒绝了部分权限
rej(false)
})
}
})
}
export class xCamreaU {
viewBox_width = 0;
viewBox_height = 0;
$element : UniNativeViewElement;
targetView : FrameLayout | null = null;
config : xCamreaUOpts;
camera : xCamrea | null = null;
constructor(element : UniNativeViewElement, opts : xCamreaUOpts) {
this.config = opts;
this.$element = element;
this.bindView();
this.camera = new xCamrea(this.targetView!)
}
bindView() {
let layview = new FrameLayout(this.$element.getAndroidActivity()!);
this.targetView = layview
this.$element.bindAndroidView(this.targetView!);
}
getIsOpeningCameraing() : boolean {
return this.camera!.getIsOpeningCameraing()
}
setCameraDir(dir : string) {
this.camera!.setCameraDir(dir)
}
setFlash(flash : boolean) {
this.camera!.setFlash(flash)
}
openCamrea() {
this.camera!.openCamrea()
}
takePhoto(call : (path : string) => void) {
this.camera!.takePhoto(call)
}
start(call : (path : string) => void) {
this.camera!.startRecoderVideo(call)
}
pause() {
this.camera!.pauseRecoderVideo()
}
stop() {
this.camera!.stopRecoderVideo()
}
/** 关闭相机 */
close() {
this.camera!.closeCamrea()
}
}
@@ -0,0 +1,509 @@
import ListView from 'android.widget.ListView';
import FrameLayout from 'android.widget.FrameLayout';
import CameraManager from 'android.hardware.camera2.CameraManager'
import CameraDevice from 'android.hardware.camera2.CameraDevice'
import StateCallback from 'android.hardware.camera2.CameraDevice.StateCallback'
import CameraCaptureSession from 'android.hardware.camera2.CameraCaptureSession'
import CaptureRequest from 'android.hardware.camera2.CaptureRequest'
import TotalCaptureResult from 'android.hardware.camera2.TotalCaptureResult'
import StreamConfigurationMap from 'android.hardware.camera2.params.StreamConfigurationMap'
import MeteringRectangle from 'android.hardware.camera2.params.MeteringRectangle'
import OutputConfiguration from 'android.hardware.camera2.params.OutputConfiguration'
import SessionConfiguration from 'android.hardware.camera2.params.SessionConfiguration'
import CameraCharacteristics from 'android.hardware.camera2.CameraCharacteristics'
import CameraMetadata from 'android.hardware.camera2.CameraMetadata'
import Range from 'android.util.Range';
import Executor from 'java.util.concurrent.Executor'
import Looper from 'android.os.Looper'
import ImageFormat from 'android.graphics.ImageFormat'
import Size from 'android.util.Size'
import SurfaceTexture from 'android.graphics.SurfaceTexture'
import Handler from 'android.os.Handler'
import Surface from 'android.view.Surface'
import Image from 'android.media.Image'
import LinearLayout from 'android.widget.LinearLayout';
import Intent from 'android.content.Intent';
import Integer from 'java.lang.Integer';
import ByteOrder from 'java.nio.ByteOrder';
import Matrix from 'android.graphics.Matrix';
import SurfaceHolder from 'android.view.SurfaceHolder'
import SurfaceView from 'android.view.SurfaceView'
import TextureView from 'android.view.TextureView'
import View from 'android.view.View'
import Context from 'android.content.Context'
import Manifest from "android.Manifest";
import Uri from 'android.net.Uri';
import File from 'java.io.File'
import System from 'java.lang.System'
import Activity from 'android.app.Activity';
import Bundle from 'android.os.Bundle';
import IntentFilter from 'android.content.IntentFilter';
import Rect from 'android.graphics.Rect';
import Matrix from 'android.graphics.Matrix'
import ImageReader from 'android.media.ImageReader'
import OnImageAvailableListener from 'android.media.ImageReader.OnImageAvailableListener'
import ByteBuffer from 'java.nio.ByteBuffer'
import MediaStore from 'android.provider.MediaStore'
import Plane from 'android.media.Image.Plane'
import Bitmap from "android.graphics.Bitmap"
import BitmapFactory from "android.graphics.BitmapFactory"
import Vibrator from "android.os.Vibrator"
import Animation from "android.view.animation.Animation"
import ScaleAnimation from "android.view.animation.ScaleAnimation"
import AnimationUtils from "android.view.animation.AnimationUtils"
import Color from "android.graphics.Color"
import Window from "android.view.Window"
import WindowManager from "android.view.WindowManager"
import TextView from "android.widget.TextView"
import RelativeLayout from "android.widget.RelativeLayout"
import ImageView from "android.widget.ImageView"
import HandlerThread from 'android.os.HandlerThread'
import YuvImage from 'android.graphics.YuvImage'
import ByteArrayOutputStream from 'java.io.ByteArrayOutputStream'
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import ViewGroup from 'android.view.ViewGroup';
import IOException from 'java.io.IOException'
import InputStream from 'java.io.InputStream'
import Task from "com.google.android.gms.tasks.Task"
import List from "java.util.List"
// x
import ProcessCameraProvider from "androidx.camera.lifecycle.ProcessCameraProvider"
import PreviewView from "androidx.camera.view.PreviewView"
import CameraController from "androidx.camera.view.CameraController"
import CameraSelector from "androidx.camera.core.CameraSelector"
import Preview from 'androidx.camera.core.Preview'
import Camera from 'androidx.camera.core.Camera'
import LifecycleObserver from "androidx.lifecycle.LifecycleObserver"
import LifecycleOwner from "androidx.lifecycle.LifecycleOwner"
import OnLifecycleEvent from "androidx.lifecycle.OnLifecycleEvent"
import ImageAnalysis from 'androidx.camera.core.ImageAnalysis'
import ImageProxy from 'androidx.camera.core.ImageProxy'
import ImageCapture from 'androidx.camera.core.ImageCapture';
import AspectRatio from 'androidx.camera.core.AspectRatio';
import CameraInfo from 'androidx.camera.core.CameraInfo';
import NonNull from 'androidx.annotation.NonNull'
import ExecutorService from 'java.util.concurrent.ExecutorService'
import Executors from 'java.util.concurrent.Executors'
import ContextCompat from 'androidx.core.content.ContextCompat';
import FileOutputStream from 'java.io.FileOutputStream';
import PreviewConfig from 'androidx.camera.core.impl.PreviewConfig';
import VideoSource from 'android.media.MediaRecorder.VideoSource';
import VideoCapture from 'androidx.camera.video.VideoCapture';
import OutputFileOptions from 'androidx.camera.core.ImageCapture.OutputFileOptions';
import Recorder from 'androidx.camera.video.Recorder';
import Quality from 'androidx.camera.video.Quality';
import QualitySelector from 'androidx.camera.video.QualitySelector';
import OutputFileResults from 'androidx.camera.core.ImageCapture.OutputFileResults';
import FileDescriptor from 'java.io.FileDescriptor';
import FileOutputOptions from 'androidx.camera.video.FileOutputOptions';
import VideoRecordEvent from 'androidx.camera.video.VideoRecordEvent';
import Consumer from 'androidx.core.util.Consumer';
import Recording from 'androidx.camera.video.Recording';
import ResolutionCorrector from 'androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector';
import AspectRatioStrategy from 'androidx.camera.core.resolutionselector.AspectRatioStrategy';
import ResolutionSelector from 'androidx.camera.core.resolutionselector.ResolutionSelector';
import Display from 'android.view.Display';
import { CAMERA_PHOTO_SIZE,xCamreaUOpts } from '../../../utssdk/interface';
type buffoptsPlanesType = {
data : ByteBuffer,
getRowStride : number,
getPixelStride : number,
}
type buffoptsType = {
width : number,
height : number,
format : number,
rect : Rect,
planes : buffoptsPlanesType[],
}
type cameraListNameType = { id : string, device : CameraCharacteristics }
type TAKE_CALL_BACK = (path : string) => void;
let callFuntake = (path : string) => { }
let callVideoFun = (path : string) => { }
function px2dp(n : number) : number {
const mets = UTSAndroid.getAppContext()!.resources!.getDisplayMetrics()
return mets.density * n
}
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;
}
export class xCamrea {
parentView : FrameLayout;
viewBox_width = 0
viewBox_height = 0
imgPointsBycanmarea = [] as RelativeLayout[]
/** 手动震动服务 */
vibrator = null as null | Vibrator
context : Context;
// 相机旋转的角度。
cameraRate = new Map<Number, Number>([
[Surface.ROTATION_0, 0],
[Surface.ROTATION_90, 0],
[Surface.ROTATION_180, 0],
[Surface.ROTATION_270, 0]
])
// 相机服务
cameraService : CameraManager
// 相机列表,name,id
cameraListName = new Map<string, cameraListNameType>();
//
cameraPreviewSize = { width: 800, height: 800 } as CAMERA_PHOTO_SIZE
// 当前预览的什么是设备
cameraDevice : CameraDevice | null = null
cameraDeviceCaptureSession : CameraCaptureSession | null = null
sufaceHolderView : Surface | null = null
// 相机区域视图
cameraView : PreviewView | null = null;
cameraProvider : ProcessCameraProvider | null = null
camera : Camera | null = null;
cameraPreview : Preview | null = null;
videoCapture : VideoCapture<Recorder> | null = null;
// Recording
recoderVideoObj : Recording | null = null;
// 是否拍照中
isDecoderQring = false
// 当前相机是否正在预览中。
isOpeningCameraing = false;
callFunEvent : TAKE_CALL_BACK = (str : string) => { }
//摄像头朝向,默认是向后置摄像头.
CamerDeviceDir = CameraSelector.LENS_FACING_FRONT
//摄像头的是否开
flashMode = false
// 拍照模式,还是录像模式.
takeModelType = 'photo'; //photo,video
private PHOTO_TYPE = "image/jpeg"
private RATIO_4_3_VALUE = 4.0 / 3.0
private RATIO_16_9_VALUE = 16.0 / 9.0
constructor(GroupView : FrameLayout) {
this.context = UTSAndroid.getAppContext()! as Context;
this.parentView = GroupView;
this.cameraService = this.context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
this.vibrator = this.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
this._createCameraView()
}
/** 创建相机预览区域 */
private _createCameraView() {
let sv = new PreviewView(UTSAndroid.getAppContext()!)
sv.scaleType = PreviewView.ScaleType.FILL_CENTER
// 让相机的高和宽与组件对齐。
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
sv.setLayoutParams(layaout)
this.cameraView = sv;
this.parentView.addView(this.cameraView!)
}
openCamrea() {
let _this = this;
if (this.isOpeningCameraing) {
this.closeCamrea()
}
_this._openCamera();
}
startRecoderVideo(call : (path : string) => void) {
callVideoFun = call;
let _this = this;
if (_this.recoderVideoObj?.isClosed() == false) {
_this.recoderVideoObj?.resume()
} else {
if (this.isOpeningCameraing || this.recoderVideoObj != null) {
this.stopRecoderVideo()
this.closeCamrea()
}
_this.takeModelType = 'video'
_this._openCamera();
}
}
pauseRecoderVideo() {
let _this = this;
_this.recoderVideoObj?.pause()
}
stopRecoderVideo() {
let _this = this;
_this.takeModelType = 'photo'
_this.recoderVideoObj?.stop()
_this.recoderVideoObj?.close()
_this.recoderVideoObj = null;
_this.videoCapture = null;
// closeCamrea();
}
// 设置识别成功的回调函数
setCallEvent(call : TAKE_CALL_BACK) {
this.callFunEvent = call;
}
getCamraRotation() : Number {
return 0
}
getIsOpeningCameraing():boolean{
return this.isOpeningCameraing;
}
getDevRation(){
const display:Display = UTSAndroid.getUniActivity()!.getDisplay()!
let rotations = display.getRotation()
let rotation = 90
if(rotations==0){
rotation =90
}else if(rotations==1){
rotation = 0
}else if(rotations==2){
rotation = 270
}else if(rotations==3){
rotation = 180
}
console.log(rotations)
return rotation
}
closeCamrea() {
let t = this;
class IntentRunable2 extends Runnable {
override run() {
t.cameraProvider?.unbindAll()
t.cameraPreview?.setSurfaceProvider(null)
t.isOpeningCameraing = false;
}
}
// 确保在主线程中关闭相机
new Handler(Looper.getMainLooper()).post(new IntentRunable2());
}
private aspectRatio(width : Int, height : Int) : Int {
let previewRatio = Math.max(width, height).toDouble() / Math.min(width, height)
if (Math.abs(previewRatio - this.RATIO_4_3_VALUE) <= Math.abs(previewRatio - this.RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private _openCamera() {
let _this = this;
let cameraProviderFuture = ProcessCameraProvider.getInstance(this.context)
class IntentRunable extends Runnable {
override run() {
let cameraProvider : ProcessCameraProvider = cameraProviderFuture.get()
let previewView = _this.cameraView! as PreviewView
const display:Display = UTSAndroid.getUniActivity()!.getDisplay()!
let rotations = display.getRotation()
let cameraSelector : CameraSelector = new CameraSelector.Builder()
.requireLensFacing(_this.CamerDeviceDir)
.build();
// 预览比例
let screenAspectRatio = _this.aspectRatio((1280).toInt(), (720).toInt())
let preview : Preview = new Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider())
// 创建ImageCapture用例,并设置闪光灯模式
let imageCapture : ImageCapture = new ImageCapture.Builder()
.setTargetAspectRatio(screenAspectRatio)
.build();
imageCapture.targetRotation = rotations
let imageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
class MyImageAnalyzer implements ImageAnalysis.Analyzer {
constructor() {
super()
}
override analyze(image : ImageProxy) {
// image
if (_this.isDecoderQring) {
// _this.closeCamrea()
_this.savePhotoTocache(image)
} else {
image.close()
}
}
override getDefaultTargetResolution() : Size {
return new Size(_this.viewBox_width.toInt(), _this.viewBox_height.toInt());
}
}
let cameraExecutor = Executors.newSingleThreadExecutor();
imageAnalysis.setAnalyzer(cameraExecutor, new MyImageAnalyzer());
let recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST)) // 选择录制质量,HD、FHD等
.build()
let tempCapture = VideoCapture.withOutput(recorder);
tempCapture.targetRotation = rotations
// tempCapture.setTargetRotation(1)
_this.videoCapture = tempCapture
let camera : Camera = cameraProvider.bindToLifecycle(UTSAndroid.getUniActivity()! as LifecycleOwner,
cameraSelector,
imageCapture,
imageAnalysis,
preview,
videoCapture!
);
if (_this.takeModelType == 'video') {
// 录制视频
let filepath = _this.context.getExternalCacheDir()?.getPath() ?? ""
let videoFile = new File(filepath, 'tmui4xCamreaVideo' + getUid(1, 12) + '.mp4')
let outputOptions = FileOutputOptions.Builder(videoFile).build()
class DirectExecutor implements Executor {
override execute(r : Runnable) {
r.run();
console.error('录制错误')
callVideoFun('')
}
}
// Consumer<VideoRecordEvent>
class reocdereventClass implements Consumer<VideoRecordEvent> {
override accept(event : VideoRecordEvent) {
if (event instanceof VideoRecordEvent.Start) {
console.log("开始录制")
} else if (event instanceof VideoRecordEvent.Finalize) {
console.log("结束录制", videoFile.absolutePath)
callVideoFun(videoFile.absolutePath)
}
}
}
// ContextCompat.getMainExecutor(_this.context)
_this.recoderVideoObj = _this.videoCapture!.getOutput()
.prepareRecording(_this.context, outputOptions)
.start(ContextCompat.getMainExecutor(_this.context), new reocdereventClass() as Consumer<VideoRecordEvent>)
}
let cameraControl = camera.cameraControl
let cameraInfo : CameraInfo = camera.cameraInfo
// 闪光灯.
cameraControl.enableTorch(_this.flashMode)
_this.camera = camera
_this.cameraPreview = preview
_this.cameraProvider = cameraProvider
_this.isDecoderQring = false
_this.isOpeningCameraing = true
}
}
try {
cameraProviderFuture.addListener(new IntentRunable(), ContextCompat.getMainExecutor(this.context))
} catch (e) {
console.error('开启错误.')
}
}
takePhoto(call : (path : string) => void) {
callFuntake = call;
this.isDecoderQring = true;
}
//front,前置,back后置
setCameraDir(type : string) {
this.CamerDeviceDir = type == 'back' ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT
}
setFlash(type : boolean) {
this.flashMode = type;
}
/**
* 将Bitmap图片根据指定角度旋转为正确方向
* @param bitmap 需要旋转的Bitmap
* @param rotationDegrees 需要旋转的角度(如90, 180, 270等)
* @return 正确方向的Bitmap
*/
rotateBitmap( bitmap:Bitmap, rotationDegrees:Number):Bitmap {
if (rotationDegrees == 0) return bitmap;
const matrix:Matrix = new Matrix();
matrix.postRotate(rotationDegrees.toFloat());
const rotatedBitmap:Bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
return rotatedBitmap;
}
savePhotoTocache(image : ImageProxy) {
let context = UTSAndroid.getAppContext()!
let filepath = context.getExternalCacheDir()?.getPath() ?? ""
let img : Bitmap | null = image.toBitmap()
img = this.rotateBitmap(img!,this.getDevRation())
let file = new File(filepath, 'tmui4xCamrea' + getUid(1, 12) + '.jpg');
let fos = new FileOutputStream(file)
let conunt = (img?.getByteCount() ?? 0) / 1024 / 1024
let saved = img!.compress(Bitmap.CompressFormat.JPEG, conunt > 1 ? 64 : 100, fos);
let t = this;
class IntentRunable2 extends Runnable {
override run() {
if (saved) {
callFuntake(file.getPath())
t.isDecoderQring = false
}
// ,拍照暂不关闭相机让用户手动关闭.
// t.closeCamrea()
}
}
image.close()
new Handler(Looper.getMainLooper()).post(new IntentRunable2());
}
}
@@ -0,0 +1,2 @@
export function checkPermissions() : Promise<boolean> {}
export class xCamreaU {}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
</dict>
</plist>
@@ -0,0 +1,3 @@
{
"deploymentTarget": "12"
}
@@ -0,0 +1,90 @@
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, UIImagePickerController } 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';
import { xCamreaUOpts } from '../interface.uts';
import { xCamrea } from './libs/camrea.uts';
const camera = new xCamrea();
export class xCamreaU {
$element : UniNativeViewElement;
targetView : UIView = new UIView();
config : xCamreaUOpts;
constructor(element : UniNativeViewElement, opts : xCamreaUOpts) {
this.config = opts;
this.$element = element;
this.$element.bindIOSView(this.targetView);
camera.setView(this.targetView)
}
getIsOpeningCameraing():boolean{
return camera.getIsOpeningCameraing()
}
setCameraDir(dir:string){
camera.setCameraDir(dir)
}
setFlash(flash:boolean){
camera.setFlash(flash)
}
openCamrea(){
camera.openCamrea()
}
takePhoto(call:(path:string)=>void){
camera.takePhoto((path:string)=>{
call(path)
},false)
}
start(call:(path:string)=>void) {
camera.startRecoderVideo(call)
}
pause(){
camera.pauseRecoderVideo()
}
stop(){
camera.stopRecoderVideo()
}
/** 关闭相机 */
close() {
camera.closeCamrea()
}
}
// 检查权限
export function checkPermissions():Promise<boolean> {
return new Promise((res, rej) => {
PHPhotoLibrary.requestAuthorization((isok : PHAuthorizationStatus) => {
// 拒绝了相册选取权限。
if (isok != PHAuthorizationStatus.authorized) {
rej(false)
return;
}
AVCaptureDevice.requestAccess(for = AVMediaType.video, completionHandler = (isok2 : boolean) => {
if (!isok2) {
rej(false)
return;
}
AVCaptureDevice.requestAccess(for = AVMediaType.audio, completionHandler = (isok3 : boolean) => {
if (!isok3) {
rej(false)
return;
}
res(true)
})
})
})
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>我们需要您的相机权限来拍照或录像。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要使用你的相册进行选择视频及图片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要保存图片和视频至你的相册中</string>
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<string>需要保存图片和视频至你的相册中</string>
</dict>
</plist>
@@ -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()
}
}
})
}
}
@@ -0,0 +1,35 @@
export type AUTH_CALL_BACK_TYPE = (auth : boolean) => void;
export type CAMERA_PHOTO_SIZE = {
width : number,
height : number
}
export type xCamreaUOpts = {
autoOpenCamera : boolean,
/**
* 是否打开闪光灯,对于orientation为back有效.
*/
flash : boolean,
/**
* 摄像头朝向,默认是后置
* back,front
*/
orientation : string,
/**
* 相机的像素宽
*/
cameraWidth : number,
/**
* 相机的像素高
*/
cameraHeight : number,
/**
* 容器的宽和高
*/
width : number,
/**
* 容器的宽和高
*/
height : number
}
@@ -0,0 +1,119 @@
export type xCamreaUOpts = {
autoOpenCamera : boolean,
/**
* 是否打开闪光灯,对于orientation为back有效.
*/
flash : boolean,
/**
* 摄像头朝向,默认是后置
* back,front
*/
orientation : string,
/**
* 相机的像素宽
*/
cameraWidth : number,
/**
* 相机的像素高
*/
cameraHeight : number,
/**
* 容器的宽和高
*/
width : number,
/**
* 容器的宽和高
*/
height : number
}
export class xCamreaU {
$element : any;
targetView : WechatMiniprogram.CameraContext = wx.createCameraContext();
config : xCamreaUOpts;
opening = false;
timeId = 232
private startRecoderVideoFunc = (path : string) => { }
constructor(element : any, opts : xCamreaUOpts) {
this.config = opts;
this.$element = element;
}
getIsOpeningCameraing() : boolean {
return this.opening
}
setCameraDir(dir : string) {
}
setFlash(flash : boolean) {
}
start(call : (path : string) => void) {
let t = this;
this.takeModelType = 'video'
this.opening = true;
this.startRecoderVideoFunc = call;
clearTimeout(this.timeId)
this.targetView.startRecord({
quality: 'high',
timeout: 5 * 60,
timeoutCallback(ok) {
call(ok.tempVideoPath || "")
},
success: (res) => {
t.timeId = setTimeout(function () {
t.stop()
}, (4 * 60 + 57) * 1000);
},
fail(er) {
console.error(er)
}
})
}
/** 关闭相机 */
close() {
this.stop()
}
openCamrea() {
this.opening = true;
}
takePhoto(call : (path : string) => void) {
this.targetView.takePhoto({
quality: 'high',
success: (res) => {
call(res.tempImagePath)
},
fail(er) {
console.error(er)
}
})
}
pause() {
this.stop()
}
stop() {
let t = this;
this.opening = false;
clearTimeout(this.timeId)
this.targetView.stopRecord({
compressed: true,
timeout: 5 * 60,
success(ok) {
t.startRecoderVideoFunc(ok.tempVideoPath || "")
},
fail(er) {
console.error(er)
}
})
}
}
// 检查权限
export function checkPermissions() : Promise<boolean> {
return new Promise(async (res, rej) => {
res(true)
})
}
+200
View File
@@ -0,0 +1,200 @@
export type xCamreaUOpts = {
autoOpenCamera : boolean,
/**
* 是否打开闪光灯,对于orientation为back有效.
*/
flash : boolean,
/**
* 摄像头朝向,默认是后置
* back,front
*/
orientation : string,
/**
* 相机的像素宽
*/
cameraWidth : number,
/**
* 相机的像素高
*/
cameraHeight : number,
/**
* 容器的宽和高
*/
width : number,
/**
* 容器的宽和高
*/
height : number
}
export class xCamreaU {
$element : HTMLDivElement;
targetView : HTMLVideoElement = document.createElement("video");
config : xCamreaUOpts;
opening = false;
private stream:any = null;
private takeModelType = 'photo'
private mediaRecorder: null | MediaRecorder = null
private startRecoderVideoFunc = (path:string)=>{}
constructor(element : HTMLDivElement, opts : xCamreaUOpts) {
this.config = opts;
this.$element = element;
this.createVideo();
}
private createVideo(){
this.targetView.style.width = "100%"
this.targetView.style.height = "100%"
this.targetView.style.objectFit = "cover"
this.targetView.playsInline = true;
this.targetView.muted = true;
this.targetView.autoplay = true;
this.targetView.controls = false;
// 针对微信的x5内核
this.targetView.setAttribute('playsinline',true)
this.targetView.setAttribute('webkit-playsinline',true)
this.targetView.setAttribute('x5-playsinline',true)
this.targetView.setAttribute('x5-video-player-type','h5')
this.targetView.setAttribute('x5-video-player-fullscreen','false')
this.$element.appendChild(this.targetView)
}
getIsOpeningCameraing():boolean{
return this.opening
}
setCameraDir(dir:string){
}
setFlash(flash:boolean){
}
start(call:(path:string)=>void) {
this.takeModelType = 'video'
this.startRecoderVideoFunc = call;
if(!this.opening){
this.openCamrea();
}
}
/** 关闭相机 */
close() {
let t = this;
this.targetView.pause()
if (t.stream) {
t.stream.getTracks().forEach(function (track) {
track.stop();
});
t.stream = null;
}
this.mediaRecorder = null;
this.opening = false;
}
createMediaRecorder() {
let t = this;
if (!MediaRecorder.isTypeSupported('video/mp4')) {
console.error("设备不支持录制 mp4 格式")
this.startRecoderVideoFunc('')
return;
}
const options = { mimeType: 'video/mp4' }; // 默认格式为 WebM
this.mediaRecorder = new MediaRecorder(t.stream, options);
let chunks = [];
this.mediaRecorder!.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
this.mediaRecorder!.onstop = () => {
// 将录制的片段合并为 Blob 对象
const blob = new Blob(chunks, { type: 'video/mp4' });
chunks = [];
const dataURL = URL.createObjectURL(blob)
this.startRecoderVideoFunc(dataURL)
};
this.mediaRecorder!.onerror = () => {
console.error("录制出错,请重试。")
this.startRecoderVideoFunc('')
}
this.mediaRecorder!.start()
}
openCamrea(){
if(this.opening) return;
this.opening = true;
var constraints = {
audio: true,
video: {
width: 1920,
height: 1080,
facingMode:this.config.orientation=='front'?{ exact: 'user' }:{ exact: 'environment' }
}
};
let t = this;
window.navigator.mediaDevices
.getUserMedia(constraints)
.then(function (mediaStream) {
t.stream = mediaStream
t.targetView.srcObject = t.stream;
t.targetView.onloadedmetadata = function (e) {
t.targetView.play();
if (t.takeModelType == 'video') {
t.createMediaRecorder()
}
};
})
.catch(function (err) {
console.log(err.name + ": " + err.message);
t.close()
});
}
takePhoto(call:(path:string)=>void){
let t = this;
if (!t.targetView || !t.stream || !t.opening) return call('');
var canvas = document.createElement('canvas');
canvas.width = t.targetView.videoWidth; // 设置canvas的宽度为视频的宽度
canvas.height = t.targetView.videoHeight; // 设置canvas的高度为视频的高度
var ctx = canvas.getContext('2d');
ctx!.drawImage(t.targetView, 0, 0, canvas.width, canvas.height);
canvas.toBlob((data) => {
const dataURL = data ? URL.createObjectURL(data) : ""
call(dataURL)
}, 'image/png', 1)
}
pause(){
// this.mediaRecorder?.pause()
this.close();
}
stop(){
this.mediaRecorder?.stop()
}
}
// 检查权限
export function checkPermissions():Promise<boolean> {
return new Promise(async (res, rej) => {
let devices = await navigator.mediaDevices.enumerateDevices()
let videoDevices = devices.filter(
(device) => device.kind === 'videoinput'
);
if (videoDevices.length == 0) {
rej(false)
console.error('没有找到可用的摄像设备')
return;
}
if(!(window.navigator?.mediaDevices??undefined)){
rej(false)
console.error('设备不支持')
return;
}
res(true)
})
}