前言
在某机系统上 unity 导出的android 工程 使用camerax 悬浮窗口 bindToLifecycle 加上预览界面就显示不出来(手机上没问题,原因没找到),不得以 改用camera2 ,一切正常
本章 讲述 camera2 的悬浮窗 摄像头 预览分析
1 环境
unity 2020.2
as 4.1.1
2 unity 代码
布局如下
代码 脚本拖到 Main Camera 上
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SampleScene : MonoBehaviour
{
Button btn1;
Button btn2;
AndroidJavaClass jc = null;
AndroidJavaObject jo = null;
// Start is called before the first frame update
void Start()
{
//第一种
/*
{
GameObject obj1 = GameObject.Find("Canvas/Button1");
if(obj1 != null)
{
btn1 = obj1.GetComponent<Button>();
btn1.onClick.AddListener(delegate() { OnClickBtn1(); });
}
GameObject obj12 = GameObject.Find("Canvas/Button2");
if (obj12 != null)
{
btn2 = obj12.GetComponent<Button>();
btn1.onClick.AddListener(delegate () { OnClickBtn2(); });
}
}
*/
//第二种
{
GameObject obj11 = GameObject.Find("Canvas/Button1");
if (obj11 != null)
{
btn1 = obj11.GetComponent<Button>();
btn1.onClick.AddListener(delegate () { OnClickAllBtn(obj11); });
}
GameObject obj112 = GameObject.Find("Canvas/Button2");
if (obj112 != null)
{
btn2 = obj112.GetComponent<Button>();
btn2.onClick.AddListener(delegate () { OnClickAllBtn(obj112); });
}
}
if (Application.platform == RuntimePlatform.Android)
{
//1: unity调用Android的非静态方法。继承UnityPlayerActivity
// 注意:使用这种方法的时候,你在Android Studio 中写的类要继承UnityPlayerActivity才能调到,
//但是只能有一个类继承它,写多个类都继承的时候,其他类也是调不到的
// AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
// AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
// jo.Call("testfun", "");
// package com.example.test;
//public class MainActivity extends UnityPlayerActivity {
// public void testfun(String str) { }
// }
{
jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
}
//2://unity调用Android的非静态方法 包名加类名
// AndroidJavaObject jo = new AndroidJavaObject("com.example.test.Test"); //包名加类名
// jo.Call("testfun", "");
//package com.example.test;
//public class Test
//{
// public void testfun(String str) { }
//}
/*
string packname = "com.test.testfloatingwindow";
string activityname = ".MainActivity";
jo = new AndroidJavaObject(packname + activityname);
if (jo == null)
{
OutputLogErr("AndroidJavaObject:" + packname + activityname);
}
*/
//3: unity调用Android的静态方法
// 注意:这里的AndroidJavaClass()里面的是自己的包名 + 类名
//这样写的可以不用继承UnityPlayerActivity也可以用
// AndroidJavaClass jc = new AndroidJavaClass("com.example.test.Test");
// jc.GetStatic<AndroidJavaObject>("testfun", "");
// package com.example.test;
//public class Test
//{
// public static void testfun(String str){}
//}
/*
jc = new AndroidJavaClass(packname + activityname);
if(jc == null)
{
OutputLogErr("AndroidJavaClass:" + packname + activityname);
}
*/
}
}
// Update is called once per frame
void Update()
{
}
void OnClickBtn1()
{
OutputLog("OnClickBtn1");
if (jo != null)
{
jo.Call("OnClickBtn1");
}
}
void OnClickBtn2()
{
OutputLog("OnClickBtn2");
if (jo != null)
{
jo.Call("OnClickBtn2");
}
}
void OnClickAllBtn(GameObject obj)
{
OutputLog(obj.name);
switch (obj.name)
{
case "Button1":
OnClickBtn1();
break;
case "Button2":
OnClickBtn2();
break;
}
}
void OutputLog(string str)
{
string date = DateTime.Now.ToLocalTime().ToString(); // 2008-9-4 20:12:12
Debug.Log(date+str);
}
void OutputLogErr(string str)
{
string date = DateTime.Now.ToLocalTime().ToString(); // 2008-9-4 20:12:12
Debug.LogError(date + str);
}
//接受android 消息
//android 代码里 UnityPlayer.UnitySendMessage("Main Camera","onmessage","adroid"+str);//这里参数是string
把消息发送给Unity场景中Main Camera物体上的onmessage方法
//这里要把 这个脚本拖到 Main Camera 上
把消息发送给Unity场景中某物体上的某方法
//UnityPlayer.UnitySendMessage("某物体名","某方法","参数");//这里参数是string
void onmessage(string str)
{
try
{
if (!string.IsNullOrEmpty(str))
{
OutputLog("onmessage" + str);
}
else
{
}
}
catch(Exception e)
{
OutputLogErr("onmessage err"+e.Message );
}
}
}
3 导出android 工程
导出前线配置下
配置下包名 这里导出的是 IL2CPP minnum api level 23
4 导出Android 工程
5 增加悬浮窗 摄像头预览
调用过程
//
//启动过程
//1: 申请权限,得到相机与屏幕 信息 多出相依处理
//applyForPermission() //放到 onCreate 里就OK了
//有权限 dosomething
//2:dosomething 里 初始化相机
//createFloatingWindowLayout() 初始化各种监听 等
//3: 点击事件 或其他地方调用
//addFloatingWindowView 触发
// TextureView.SurfaceTextureListener() 的
// public void onSurfaceTextureAvailable 从而 启动相机
//4: 启动相机
//openCamera 在 mCameraManager.openCamera 前 回调函数都设置好
//createCameraDeviceCallback() ; //相机状态监听回调
// createImageReader(); //图像解析
//触发 createCameraPreviewSession
//5 创建预览会话
// 预览 解析 加入会话申请中
1> UnityPlayerActivity.java
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
package com.unity3d.player;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.os.Process;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents
{
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
// Override this in your custom UnityPlayerActivity to tweak the command line arguments passed to the Unity Android Player
// The command line arguments are passed as a string, separated by spaces
// UnityPlayerActivity calls this from 'onCreate'
// Supported: -force-gles20, -force-gles30, -force-gles31, -force-gles31aep, -force-gles32, -force-gles, -force-vulkan
// See https://docs.unity3d.com/Manual/CommandLineArguments.html
// @param cmdLine the current command line arguments, may be null
// @return the modified command line string or null
protected String updateUnityCommandLineArguments(String cmdLine)
{
return cmdLine;
}
// Setup activity layout
@Override protected void onCreate(Bundle savedInstanceState)
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
getIntent().putExtra("unity", cmdLine);
mUnityPlayer = new UnityPlayer(this, this);
setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();
applyForPermission(); // add by eh
}
// When Unity player unloaded move task to background
@Override public void onUnityPlayerUnloaded() {
moveTaskToBack(true);
}
// Callback before Unity player process is killed
@Override public void onUnityPlayerQuitted() {
}
@Override protected void onNewIntent(Intent intent)
{
// To support deep linking, we need to make sure that the client can get access to
// the last sent intent. The clients access this through a JNI api that allows them
// to get the intent set on launch. To update that after launch we have to manually
// replace the intent with the one caught here.
setIntent(intent);
mUnityPlayer.newIntent(intent);
}
// Quit Unity
@Override protected void onDestroy ()
{
mUnityPlayer.destroy();
super.onDestroy();
}
// Pause Unity
@Override protected void onPause()
{
super.onPause();
mUnityPlayer.pause();
}
// Resume Unity
@Override protected void onResume()
{
super.onResume();
mUnityPlayer.resume();
}
// Low Memory Unity
@Override public void onLowMemory()
{
super.onLowMemory();
mUnityPlayer.lowMemory();
}
// Trim Memory Unity
@Override public void onTrimMemory(int level)
{
super.onTrimMemory(level);
if (level == TRIM_MEMORY_RUNNING_CRITICAL)
{
mUnityPlayer.lowMemory();
}
}
// This ensures the layout will be correct.
@Override public void onConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
mUnityPlayer.configurationChanged(newConfig);
}
// Notify Unity of the focus change.
@Override public void onWindowFocusChanged(boolean hasFocus)
{
super.onWindowFocusChanged(hasFocus);
mUnityPlayer.windowFocusChanged(hasFocus);
}
// For some reason the multiple keyevent type is not supported by the ndk.
// Force event injection by overriding dispatchKeyEvent().
@Override public boolean dispatchKeyEvent(KeyEvent event)
{
if (event.getAction() == KeyEvent.ACTION_MULTIPLE)
return mUnityPlayer.injectEvent(event);
return super.dispatchKeyEvent(event);
}
// Pass any events not handled by (unfocused) views straight to UnityPlayer
@Override public boolean onKeyUp(int keyCode, KeyEvent event) { return mUnityPlayer.injectEvent(event); }
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { return mUnityPlayer.injectEvent(event); }
@Override public boolean onTouchEvent(MotionEvent event) { return mUnityPlayer.injectEvent(event); }
/*API12*/ public boolean onGenericMotionEvent(MotionEvent event) { return mUnityPlayer.injectEvent(event); }
//增加的代码开始处
final private static String TAG = UnityPlayerActivity.class.getName() ;
//供unity 调用 接口
public void OnClickBtn1(){
Log.d(TAG,"android OnClickBtn1");
addFloatingWindowView(); //增加悬浮窗后 ,
}
//供unity 调用 接口
public void OnClickBtn2(){
Log.d(TAG,"android OnClickBtn2");
// addFloatingWindowView(); //增加悬浮窗后 ,
}
//供unity 调用 接口 暂时无用
public static void OnClickBtn3(){
Log.d(TAG,"android OnClickBtn3");
// myActivity.addFloatingWindowView();
}
@Override
protected void onStart() {
super.onStart();
}
/
//增加camera2 摄像头
private CameraManager mCameraManager;
private CameraDevice.StateCallback mStateCallback;
private CameraDevice mCameraDevice;
private TextureView mTextureView;
private CameraCaptureSession mCaptureSession;
private CaptureRequest mPreviewRequest;
private int mCameraSensorOrientation = 0 ; //摄像头方向
private int mCameraFacing = CameraCharacteristics.LENS_FACING_FRONT ; //默认使用前置摄像头
private int mDisplayRotation = 0; //getDisplay().defaultDisplay.rotation //手机方向
private String cameraIdFront = ""; //相机ID
private Size[] cameraSizeFront ; //相机支持的尺寸
private CameraCharacteristics cameraCharacteristicsFront ; //
private static final int RECV_MAX_IMAGES = 2 ; //同时最多接收2张图片
private ImageReader mImageReader;
private TextureView.SurfaceTextureListener surfaceTextureListener ;
private ImageReader.OnImageAvailableListener imageAvailableListener;
private boolean mFlashSupported =false; //判断是否支持闪关灯
// private HandlerThread mBackgroundThread; //用于运行不应阻塞UI的任务的附加线程
// private Handler mBackgroundHandler; //用于在后台运行任务的{@link Handler}
android.util.Range<Integer> mAverageFPS =null ; //中间帧率
//悬浮窗相关
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
// public static final int DMS_INPUT_IMG_W = 640;
// public static final int DMS_INPUT_IMG_H = 480;
public static final int PREVIEW_WIDTH = 640 ; //预览的宽度
public static final int PREVIEW_HEIGHT = 480 ; //预览的高度
public static final int DEFAULT_SCREEN_X = 300 ; //默认开始位置
public static final int DEFAULT_SCREEN_Y = 300 ; //默认开始位置
private Size mPreviewSize = null; //预览的尺寸 默认 PREVIEW_WIDTH PREVIEW_HEIGHT
private View mPreviewdisplayView = null; //预览窗口
private Surface mPreviewSurface; //预览窗口surface
// LayoutInflater floatingWindowlayoutInflater = null ; //悬浮窗口布局填充器 //放这里也可以,用临时变量也可以的
//
//权限 相关
private final int REQUEST_CODE_CONTACT = 1;
final String[] AppPermissions = {
// Manifest.permission.WRITE_EXTERNAL_STORAGE,
// Manifest.permission.READ_EXTERNAL_STORAGE,
// Manifest.permission.READ_PHONE_STATE,
Manifest.permission.INTERNET,
Manifest.permission.CAMERA,
//Manifest.permission.RECORD_AUDIO,
};
///
//创建悬浮窗布局
void createFloatingWindowLayout(){
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
layoutParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; //TYPE_APPLICATION_SUB_PANEL ; //TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = getPreviewWidth(); // PREVIEW_WIDTH; //窗口宽度
layoutParams.height = getPreviewHeight();//PREVIEW_HEIGHT; //窗口高度
layoutParams.x = DEFAULT_SCREEN_X; //开始的位置
layoutParams.y = DEFAULT_SCREEN_Y; //开始的位置
}
//增加悬浮窗口
void addFloatingWindowView(){
LayoutInflater floatingWindowlayoutInflater = LayoutInflater.from(this);
mPreviewdisplayView = floatingWindowlayoutInflater.inflate(R.layout.camera_display, null);
mPreviewdisplayView.setOnTouchListener(new FloatingOnTouchListener());
mTextureView = mPreviewdisplayView.findViewById(R.id.preview);
mTextureView.setSurfaceTextureListener(surfaceTextureListener);
windowManager.addView(mPreviewdisplayView, layoutParams);
}
private void doSomeThing(){
if(initCameraInfo()){
initSurfaceTextureListener(); //监听SurfaceTexture
initImageAvailableListener(); //图像监听,也可以放到createImageReader前
createFloatingWindowLayout(); //创建布局
}
}
//申请权限
private void applyForPermission(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
for (String str : AppPermissions) {
if (ContextCompat.checkSelfPermission(this, str) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, AppPermissions, REQUEST_CODE_CONTACT);
return;
}
}
//do some thing
doSomeThing();
}else{
//使用的比Build.VERSION_CODES.M大 所以这里不可能走到
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_CONTACT) {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//do some thing
doSomeThing();
} else if (grantResults.length == 0) {
// 已知魅族手机会进这个结果
} else {
Toast.makeText(this, "您拒绝了相关权限,无法使用!", Toast.LENGTH_SHORT).show();
finish();
}
}
}
///相机相关//
//初始化Surface 监听
void initSurfaceTextureListener(){
surfaceTextureListener= new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
texture.setDefaultBufferSize(getPreviewWidth(), getPreviewHeight());
mPreviewSurface = new Surface(texture);
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
};
}
private boolean initWindowManager(Context context){
if(windowManager == null){
if (context instanceof Activity) {
Activity activity = (Activity) context;
windowManager = activity.getWindowManager();
} else {
windowManager = (WindowManager) context.getSystemService(UnityPlayerActivity.WINDOW_SERVICE);
}
}
return windowManager !=null ;
}
//得到屏幕旋转角度
private int getDisplayRotation(Context context) {
if(initWindowManager(context))
{
int rotation = windowManager.getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
}
}
return 0;
}
//得到屏幕信息
private Display getDisplay(Context context) {
if(initWindowManager(context)) return windowManager.getDefaultDisplay() ;
return null;
}
private class CompareSizeByArea implements java.util.Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum((long) lhs.getWidth() * lhs.getHeight()
- (long) rhs.getWidth() * rhs.getHeight());
}
}
private int getPreviewWidth(){
return mPreviewSize!=null?mPreviewSize.getWidth():PREVIEW_WIDTH ;
}
private int getPreviewHeight(){
return mPreviewSize!=null?mPreviewSize.getHeight():PREVIEW_HEIGHT;
}
//是否支持闪光灯模式
private boolean isFlashSupported(){
return mFlashSupported ;
}
//获取设备的方向
private static int sensorToDeviceRotation(CameraCharacteristics characteristics, int deviceOrientation) {
int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
///
// ORIENTATIONS.append(Surface.ROTATION_0, 90);
//
// ORIENTATIONS.append(Surface.ROTATION_90, 0);
//
// ORIENTATIONS.append(Surface.ROTATION_180, 270);
//
// ORIENTATIONS.append(Surface.ROTATION_270, 180);
//
// deviceOrientation = ORIENTATIONS.get(deviceOrientation);
//int deviceOrientation = 0 ;
switch (deviceOrientation)
{
case Surface.ROTATION_0: deviceOrientation = 90 ; break;
case Surface.ROTATION_90: deviceOrientation = 0 ; break;
case Surface.ROTATION_180: deviceOrientation = 270 ; break;
case Surface.ROTATION_270: deviceOrientation = 180 ; break;
}
return (sensorOrientation + deviceOrientation + 360) % 360;
}
//初始化相机
private boolean initCameraInfo() {
mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
try {
String [] carmeraids = mCameraManager.getCameraIdList() ;
for(int i =0 ;i<carmeraids.length ;i++){
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(carmeraids[i]);
// cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)== mCameraFacing) { //前置
cameraIdFront = carmeraids[i] ;
cameraCharacteristicsFront = cameraCharacteristics ;
cameraSizeFront = cameraCharacteristics .get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) .getOutputSizes(ImageReader.class);
break;
}
}
}catch (CameraAccessException e){
Log.e(TAG,e.getMessage());
return false;
}
// int supportLevel = cameraCharacteristicsFront.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
// if (supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
// // mActivity.toast("相机硬件不支持新特性")
// }
int supportLevel = cameraCharacteristicsFront.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if(!isHardwareLevelSupported(supportLevel,CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)){ //希望支持
return false ;
}
//判断是否支持闪光灯
Boolean available = cameraCharacteristicsFront.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mFlashSupported = available == null ? false : available;
StreamConfigurationMap map = cameraCharacteristicsFront.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size [] outputSizes = map.getOutputSizes(ImageFormat.YUV_420_888);
Size largest = Collections.max(Arrays.asList(outputSizes), new CompareSizeByArea());
// 获取预览画面的尺寸
if(!(outputSizes!= null && outputSizes.length > 0)){
return false;
}
boolean bFound = false ;
for(Size s :outputSizes){
if(s.getWidth() == PREVIEW_WIDTH && s.getHeight() == PREVIEW_HEIGHT){
bFound = true ; //
break;
}
}
if(bFound){
mPreviewSize = new Size(PREVIEW_WIDTH,PREVIEW_HEIGHT);
}else {
int average = (int)(outputSizes.length/2); //平均值 偶数个 取大的
mPreviewSize = outputSizes[average] ;//map.getOutputSizes(SurfaceTexture.class)[0]; //0 为最大尺寸, 感觉不合理,所以取平均值
}
//帧率
//Key<android.util.Range<Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
android.util.Range<Integer>[] ranges = cameraCharacteristicsFront.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
if(ranges != null && ranges.length > 0){
//取中间值 从低往高 0 帧率最低
int average = (int)(ranges.length/2); //平均值 偶数个 取大的
mAverageFPS = ranges[average]; //中间值
}
return true ;
}
// 对于Camera2采集系统来说,每个摄像头都有一个支持等级: 从高到底为 越高越好
// INFO_SUPPORTED_HARDWARE_LEVEL_3 支持YUV再处理和原始数据采集功能,并且具备先进的功能。
// INFO_SUPPORTED_HARDWARE_LEVEL_FULL支持先进的摄像头功能。
// INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED向后兼容模式,底层等同于Camera1的实现。
// INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY 随机赠送的功能支持,支持性不足。
// 总的来说如果摄像头等级是LEVEL_3和LEVEL_FULL才建议使用Camera2进行采集,否则推荐采用兼容性更好的Camera1进行视频采集。
//判断是否支持要求的等级
// Returns true if the device supports the required hardware level, or better.
boolean isHardwareLevelSupported(int supportLevel , int requiredLevel) {
final int[] sortedHwLevels = {
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
};
int deviceLevel = supportLevel ; CameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (requiredLevel == deviceLevel) {
return true;
}
for (int sortedlevel : sortedHwLevels) {
if (sortedlevel == requiredLevel) {
return true;
} else if (sortedlevel == deviceLevel) {
return false;
}
}
return false; // Should never reach here
}
//初始化
private void initImageAvailableListener(){
imageAvailableListener = new ImageReader.OnImageAvailableListener(){
@Override
public void onImageAvailable(ImageReader var1){
Image image = var1.acquireLatestImage() ;
if(image== null){
return;
}
//得到图片 转成nv21格式
byte [] date = YUV_420_888toNV21(image);
// ByteBuffer buffer = image.getPlanes()[0].getBuffer();
// int length = buffer.remaining();
// var bytes = ByteArray(length);
// 转成 Bitmap
// ByteBuffer buffer = image.getPlanes()[0].getBuffer();
// byte[] b = new byte[buffer.capacity()];
// buffer.get(b, 0 , b.length);
// Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
image.close();
}
};
}
private void createCameraDeviceCallback(){
mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
mCameraDevice = null;
}
};
}
public void openCamera(){
mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
createCameraDeviceCallback() ; //相机状态监听
createImageReader(); //图像解析
try {
mCameraManager.openCamera(cameraIdFront, mStateCallback, null); //"0" 前置 "1" 后置 // handler null 主线程处理 否 创建个独立线程区处理
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//创建分析
private void createImageReader() {
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
//mPreviewSize.getWidth()
mImageReader = ImageReader.newInstance(getPreviewWidth(), getPreviewHeight(),
ImageFormat.YUV_420_888, RECV_MAX_IMAGES);
// imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 2)
mImageReader.setOnImageAvailableListener(imageAvailableListener, null);
}
//创建预览会话
private void createCameraPreviewSession() {
try {
final CaptureRequest.Builder previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(mPreviewSurface);
previewRequestBuilder.addTarget(mImageReader.getSurface());
//这几句可以放到 // 放到设置预览时连续捕获图像数据前 也可以
// 设置自动对焦模式
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 设置自动曝光模式
if(isFlashSupported()){ //如果支持就开启
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
}
//设置帧率
if(mAverageFPS!=null){
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mAverageFPS);
}
// 开始显示相机预览
mPreviewRequest = previewRequestBuilder.build();
//
// width 和 height 与 surfaceview 的相同
//ImageReader 用于接收处理(拍照、录像)的相机流
//SurfaceView 用于接收预览的相机流
// ImageReader mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
List<Surface> surfaces = new ArrayList<Surface>();
surfaces.add(mPreviewSurface);
surfaces.add(mImageReader.getSurface());
// surfaces.add(surfaceView.getHolder().getSurface());
// surfaces.add(mImageReader.getSurface());
// Arrays.asList(surface)
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
mCaptureSession = cameraCaptureSession;
try {
//这几句可以放到 // 放到设置预览时连续捕获图像数据 也可以
// 设置自动对焦模式
// previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// // 设置自动曝光模式
// if(isFlashSupported()){ //如果支持就开启
// previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// }
// //设置帧率
// if(mAverageFPS!=null){
// previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mAverageFPS);
// }
// // 开始显示相机预览
// mPreviewRequest = previewRequestBuilder.build();
//
// 设置预览时连续捕获图像数据
mCaptureSession.setRepeatingRequest(mPreviewRequest, null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
Log.e("MainActivity", "onConfigureFailed");
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
数据格式处理//
//数据转化 抄别人的 //
//Planar格式(P)的处理
private static ByteBuffer getuvBufferWithoutPaddingP(ByteBuffer uBuffer, ByteBuffer vBuffer, int width, int height, int rowStride, int pixelStride){
int pos = 0;
byte []byteArray = new byte[height*width/2];
for (int row=0; row<height/2; row++) {
for (int col=0; col<width/2; col++) {
int vuPos = col*pixelStride + row*rowStride;
byteArray[pos++] = vBuffer.get(vuPos);
byteArray[pos++] = uBuffer.get(vuPos);
}
}
ByteBuffer bufferWithoutPaddings=ByteBuffer.allocate(byteArray.length);
// 数组放到buffer中
bufferWithoutPaddings.put(byteArray);
//重置 limit 和postion 值否则 buffer 读取数据不对
bufferWithoutPaddings.flip();
return bufferWithoutPaddings;
}
//Semi-Planar格式(SP)的处理和y通道的数据
private static ByteBuffer getBufferWithoutPadding(ByteBuffer buffer, int width, int rowStride, int times,boolean isVbuffer){
if(width == rowStride) return buffer; //没有buffer,不用处理。
int bufferPos = buffer.position();
int cap = buffer.capacity();
byte []byteArray = new byte[times*width];
int pos = 0;
//对于y平面,要逐行赋值的次数就是height次。对于uv交替的平面,赋值的次数是height/2次
for (int i=0;i<times;i++) {
buffer.position(bufferPos);
//part 1.1 对于u,v通道,会缺失最后一个像u值或者v值,因此需要特殊处理,否则会crash
if(isVbuffer && i==times-1){
width = width -1;
}
buffer.get(byteArray, pos, width);
bufferPos+= rowStride;
pos = pos+width;
}
//nv21数组转成buffer并返回
ByteBuffer bufferWithoutPaddings=ByteBuffer.allocate(byteArray.length);
// 数组放到buffer中
bufferWithoutPaddings.put(byteArray);
//重置 limit 和postion 值否则 buffer 读取数据不对
bufferWithoutPaddings.flip();
return bufferWithoutPaddings;
}
private static byte[] YUV_420_888toNV21(Image image) {
int width = image.getWidth();
int height = image.getHeight();
ByteBuffer yBuffer = getBufferWithoutPadding(image.getPlanes()[0].getBuffer(), image.getWidth(), image.getPlanes()[0].getRowStride(),image.getHeight(),false);
ByteBuffer vBuffer;
//part1 获得真正的消除padding的ybuffer和ubuffer。需要对P格式和SP格式做不同的处理。如果是P格式的话只能逐像素去做,性能会降低。
if(image.getPlanes()[2].getPixelStride()==1){ //如果为true,说明是P格式。
vBuffer = getuvBufferWithoutPaddingP(image.getPlanes()[1].getBuffer(), image.getPlanes()[2].getBuffer(),
width,height,image.getPlanes()[1].getRowStride(),image.getPlanes()[1].getPixelStride());
}else{
vBuffer = getBufferWithoutPadding(image.getPlanes()[2].getBuffer(), image.getWidth(), image.getPlanes()[2].getRowStride(),image.getHeight()/2,true);
}
//part2 将y数据和uv的交替数据(除去最后一个v值)赋值给nv21
int ySize = yBuffer.remaining();
int vSize = vBuffer.remaining();
byte[] nv21;
int byteSize = width*height*3/2;
nv21 = new byte[byteSize];
yBuffer.get(nv21, 0, ySize);
vBuffer.get(nv21, ySize, vSize);
//part3 最后一个像素值的u值是缺失的,因此需要从u平面取一下。
ByteBuffer uPlane = image.getPlanes()[1].getBuffer();
byte lastValue = uPlane.get(uPlane.capacity() - 1);
nv21[byteSize - 1] = lastValue;
return nv21;
}
//悬浮窗口 移动 更新位置
private class FloatingOnTouchListener implements View.OnTouchListener {
private int x;
private int y;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - x;
int movedY = nowY - y;
x = nowX;
y = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
windowManager.updateViewLayout(view, layoutParams);
break;
default:
break;
}
return true;
}
}
//释放 与切换
private void releaseCamera() {
if(mCaptureSession!=null){
mCaptureSession.close();
mCaptureSession = null ;
}
if(mCameraDevice !=null){
mCameraDevice.close();
mCameraDevice = null;
}
if(mImageReader !=null){
mImageReader.close();
mImageReader = null ;
}
// canExchangeCamera = false
}
private void releaseThread() {
// handlerThread.quitSafely()
}
private void exchangeCamera() {
// if (mCameraDevice == null || !canExchangeCamera || !mTextureView.isAvailable) return
//
// mCameraFacing = if (mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT)
// CameraCharacteristics.LENS_FACING_BACK
// else
// CameraCharacteristics.LENS_FACING_FRONT
//
// mPreviewSize = Size(PREVIEW_WIDTH, PREVIEW_HEIGHT) //重置预览大小
// releaseCamera()
// initCameraInfo()
}
//
//权限
// <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// <uses-permission android:name="android.permission.CAMERA" />
// <uses-permission android:name="android.permission.INTERNET" />
//硬件加速
// android:hardwareAccelerated="true"
//使用 useAndroidX
//android.useAndroidX=true
//增加依赖
//implementation 'androidx.core:core:1.2.0' 跟下面3句有关系,不增加,下面的找不到
// import androidx.annotation.NonNull;
//import androidx.core.app.ActivityCompat;
//import androidx.core.content.ContextCompat;
///
// unity 导出 Plugin Version(3.6.0) Gradle Version(6.1.1) 这里用
// compileSdkVersion 33
// buildToolsVersion '33.0.0'
// 不支持 所以需要提升下
// Android Gradle Plugin Version
//3.6.0 ----> 4.1.1
// Gradle Version
//6.1.1 ----> 6.9
//
//启动过程
//1: 申请权限,得到相机与屏幕 信息 多出相依处理
//applyForPermission() //放到 onCreate 里就OK了
//有权限 dosomething
//2:dosomething 里 初始化相机
//createFloatingWindowLayout() 初始化各种监听 等
//3: 点击事件 或其他地方调用
//addFloatingWindowView 触发
// TextureView.SurfaceTextureListener() 的
// public void onSurfaceTextureAvailable 从而 启动相机
//4: 启动相机
//openCamera 在 mCameraManager.openCamera 前 回调函数都设置好
//createCameraDeviceCallback() ; //相机状态监听回调
// createImageReader(); //图像解析
//触发 createCameraPreviewSession
//5 创建预览会话
// 预览 解析 加入会话申请中
}
camera_display.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</TextureView>
</LinearLayout>
AndroidManifest.xml
增加权限
修改
android:hardwareAccelerated=“false” 为 “true”
<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools">
<application>
<activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector" android:screenOrientation="landscape" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:hardwareAccelerated="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
<meta-data android:name="android.notch_support" android:value="true" />
</activity>
<meta-data android:name="unity.splash-mode" android:value="0" />
<meta-data android:name="unity.splash-enable" android:value="True" />
<meta-data android:name="notch.config" android:value="portrait|landscape" />
<meta-data android:name="unity.build-id" android:value="df8d9e41-33b6-4006-847a-7fc0aeb2ca09" />
</application>
<uses-feature android:glEsVersion="0x00030000" />
<uses-feature android:name="android.hardware.vulkan.version" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
unityLibrary 下 gradle.properties
增加 android.useAndroidX=true
unityLibrary 下 build.gradle
增加依赖库
//by eh add 这句一定要增加 下面2句可以不加
implementation 'androidx.core:core:1.2.0'
1.1.0-beta01
//下面2句没加一样跑,不知道在上面那里是否已经包含了
def camera2_version = "1.2.0"
implementation "androidx.camera:camera-core:${camera2_version}"
implementation "androidx.camera:camera-camera2:${camera2_version}"
文件内容如下
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
apply plugin: 'com.android.library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//by eh add
implementation 'androidx.core:core:1.2.0'
1.1.0-beta01
//下面2句没加一样跑,不知道在上面那里是否已经包含了
def camera2_version = "1.2.0"
implementation "androidx.camera:camera-core:${camera2_version}"
implementation "androidx.camera:camera-camera2:${camera2_version}"
}
android {
compileSdkVersion 33
buildToolsVersion '33.0.0'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 23
targetSdkVersion 30
ndk {
abiFilters 'arm64-v8a'
}
versionCode 1
versionName '1.0'
consumerProguardFiles 'proguard-unity.txt'
}
lintOptions {
abortOnError false
}
aaptOptions {
noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
}
packagingOptions {
doNotStrip '*/arm64-v8a/*.so'
}
}
def getSdkDir() {
Properties local = new Properties()
local.load(new FileInputStream("${rootDir}/local.properties"))
return local.getProperty('sdk.dir')
}
def BuildIl2Cpp(String workingDir, String targetDirectory, String architecture, String abi, String configuration) {
exec {
commandLine(workingDir + "/src/main/Il2CppOutputProject/IL2CPP/build/deploy/netcoreapp3.1/il2cpp.exe",
"--compile-cpp",
"--libil2cpp-static",
"--platform=Android",
"--architecture=" + architecture,
"--configuration=" + configuration,
"--outputpath=" + workingDir + targetDirectory + abi + "/libil2cpp.so",
"--cachedirectory=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_cache",
"--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/external/bdwgc/include",
"--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/libil2cpp/include",
"--tool-chain-path=" + android.ndkDirectory,
"--map-file-parser=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/MapFileParser/MapFileParser.exe",
"--generatedcppdir=" + workingDir + "/src/main/Il2CppOutputProject/Source/il2cppOutput",
"--baselib-directory=" + workingDir + "/src/main/jniStaticLibs/" + abi,
"--dotnetprofile=unityaot")
environment "ANDROID_SDK_ROOT", getSdkDir()
}
delete workingDir + targetDirectory + abi + "/libil2cpp.sym.so"
ant.move(file: workingDir + targetDirectory + abi + "/libil2cpp.dbg.so", tofile: workingDir + "/symbols/" + abi + "/libil2cpp.so")
}
android {
task BuildIl2CppTask {
doLast {
BuildIl2Cpp(projectDir.toString().replaceAll('\\\\', '/'), '/src/main/jniLibs/', 'ARM64', 'arm64-v8a', 'Release');
}
}
afterEvaluate {
if (project(':unityLibrary').tasks.findByName('mergeDebugJniLibFolders'))
project(':unityLibrary').mergeDebugJniLibFolders.dependsOn BuildIl2CppTask
if (project(':unityLibrary').tasks.findByName('mergeReleaseJniLibFolders'))
project(':unityLibrary').mergeReleaseJniLibFolders.dependsOn BuildIl2CppTask
}
sourceSets {
main {
jni.srcDirs = ["src/main/Il2CppOutputProject"]
}
}
}
launcher 下 build.gradle
只修改了目标版本号跟 unityLibrary 下的一致
6 运行结果
7 后续增加其他功能
unity 跟 AS DEMO工程 DEMO下载地址
觉得有用 ,点个赞,加个关注