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