一、開(kāi)發(fā)背景
由于在項(xiàng)目中需要集成多個(gè)人臉識(shí)別SDK,然后這些SDK有些有自己的Camera API(但又不是特別獨(dú)立),有的SDK甚至都沒(méi)有像樣的Camera API。所以我就自己寫了一個(gè)通用的,用來(lái)抓取視頻流的Camera1的代碼封裝。
二、特色
快速適配各類相機(jī)(物理旋轉(zhuǎn)、鏡像、各種角度旋轉(zhuǎn)等)
控件大小自動(dòng)適配
人臉位置映射
使用緩沖機(jī)制獲取視頻流
代碼簡(jiǎn)單(一個(gè)Class)、引入方面,CameraPreview的完整代碼在第五點(diǎn)

1.png

2.png
三、基本使用,炒雞簡(jiǎn)單
- 1、一個(gè) Layout文件
<?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"
>
<com.lfork.cameraimagecollect.camera.CameraPreview
android:id="@+id/camera_preview"
android:layout_width="300dp"
android:layout_height="500dp"
/>
</LinearLayout>
- 2、恢復(fù)和停止 (默認(rèn)是前置攝像頭 適配設(shè)備oneplus 6)
package com.lfork.cameraimagecollect.camera1
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.lfork.cameraimagecollect.R
import kotlinx.android.synthetic.main.activity_camera_texture.*
class CameraTextureActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera_texture)
}
override fun onResume() {
super.onResume()
camera_preview.resumeCamera()
}
override fun onStop() {
super.onStop()
camera_preview.releaseCamera()
}
}
- 3、相關(guān)的權(quán)限處理就需要自己完成了
四、進(jìn)階使用
- 1、設(shè)置預(yù)覽回調(diào)幀 確保在resumeCamea()之前調(diào)用即可
val previewFrameCallBack = Camera.PreviewCallback { data, _ ->
Log.d("CameraTextureActivity", "preview camera byte data $data")
}
camera_preview.previewCallBack = previewFrameCallBack
- 2、針對(duì)特定的相機(jī)進(jìn)行適配
val params = CameraPreview.Params()
params.cameraId = 0
params.isHorizontalMirrored = false
params.previewRotation = 90f
params.previewWidth = 640
params.previewHeight = 480
camera_preview.setupCameraParams(params)
- 3、人臉位置映射
/**
* 原始圖片(傳到SDK里面進(jìn)行處理的圖片)中的坐標(biāo)到預(yù)覽View坐標(biāo)中的映射。應(yīng)用場(chǎng)景舉例:預(yù)覽頁(yè)面顯示人臉框。
*
* @param rectF 原始圖中的坐標(biāo)
*/
fun mapFromOriginalRect(rectF: RectF, rawImgWeight: Int, rawImgHeight: Int)
五、引入:一個(gè)class,拷貝到項(xiàng)目中即可
package com.lfork.cameraimagecollect.camera
import android.content.Context
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.SurfaceTexture
import android.hardware.Camera
import android.os.Handler
import android.os.Looper
import android.support.annotation.AttrRes
import android.util.AttributeSet
import android.view.TextureView
import android.widget.FrameLayout
import java.io.IOException
/**
* 基于系統(tǒng)TextureView和Camera1實(shí)現(xiàn)的預(yù)覽View;
*/
class CameraPreview : FrameLayout,
TextureView.SurfaceTextureListener {
lateinit var textureView: TextureView
/**
* 設(shè)置預(yù)覽的縮放類型
* 縮放類型
*/
var scaleType: ScaleType = ScaleType.CROP_INSIDE
/**
* 圖片幀縮放類型。
*/
enum class ScaleType {
/**
* 寬度與父控件一致,高度自適應(yīng)
*/
FIT_WIDTH,
/**
* 調(diào)試與父控件一致,寬度自適應(yīng)
*/
FIT_HEIGHT,
/**
* 全屏顯示 ,保持顯示比例,多余的部分會(huì)被裁剪掉。
*/
CROP_INSIDE
}
private var surface: SurfaceTexture? = null
var mCamera: Camera? = null
private var params = Params()
/**
* 默認(rèn)配置是一加6的前置攝像頭
*/
class Params {
var cameraId = 1
/**
* 預(yù)覽幀的旋轉(zhuǎn)角度(順時(shí)針)
*/
var previewRotation = 90f
/**
* 經(jīng)過(guò)Camera處理后(比如旋轉(zhuǎn))的PreviewFrame的圖像寬度 比如1280*720旋轉(zhuǎn)后就會(huì)變成720*1280
*/
var frameWidth: Int = 0
/**
* 經(jīng)過(guò)Camera處理后(比如旋轉(zhuǎn))的PreviewFrame的圖像高度
*/
var frameHeight: Int = 0
/**
* 是否對(duì)視頻進(jìn)行了鏡像處理(水平鏡像)
*/
var isHorizontalMirrored = false
/**
* 屏幕是否是豎著擺放
*/
var isPortrait = true
}
/**
* UI線程處理
*/
private val mHandler = Handler(Looper.getMainLooper())
constructor(context: Context) : super(context) {
init()
}
constructor(
context: Context,
attrs: AttributeSet?
) : super(context, attrs) {
init()
}
constructor(
context: Context, attrs: AttributeSet?,
@AttrRes defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
textureView = TextureView(context)
textureView.surfaceTextureListener = this
addView(textureView)
}
private var mPreviewBuffer: ByteArray? = null
/**
* 設(shè)置視頻流回調(diào)(帶緩沖)
*/
var previewCallBack: Camera.PreviewCallback? = null
/**
* 對(duì)特定的相機(jī)進(jìn)行參數(shù)配置
*/
fun setupCameraParams(params: Params) {
this.params = params
}
/**
* 根據(jù)現(xiàn)有的參數(shù)對(duì)相機(jī)進(jìn)行設(shè)置
*/
private fun applyParams() {
if (mCamera == null) {
mCamera = Camera.open(params.cameraId) //1
}
val cameraParams = mCamera!!.parameters
//使用默認(rèn)參數(shù)
if (params.frameHeight == 0 || params.frameWidth == 0) {
val size = getCloselyPreSize(
params.isPortrait,
width,
height,
cameraParams.supportedPreviewSizes
)
if (size == null) {
releaseCamera()
return
}
params.frameWidth = size.width
params.frameHeight = size.height
}
//預(yù)覽相片的尺寸:相機(jī)傳過(guò)來(lái)的 注意是橫著的
cameraParams.setPreviewSize(params.frameWidth, params.frameHeight)
mCamera?.parameters = cameraParams
mCamera?.setDisplayOrientation(params.previewRotation.toInt())
if (params.isHorizontalMirrored) {
scaleX = -1f
}
if (params.previewRotation % 360f == 90f || params.previewRotation % 360f == 270f) {
//進(jìn)行90度旋轉(zhuǎn)后,需要進(jìn)行寬高轉(zhuǎn)換
val temp = params.frameHeight
params.frameHeight = params.frameWidth
params.frameWidth = temp
} else {
//進(jìn)行90度旋轉(zhuǎn)后,需要進(jìn)行寬高轉(zhuǎn)換
val temp = params.frameWidth
params.frameWidth = params.frameHeight
params.frameHeight = temp
}
//讓視頻根據(jù)控件大小進(jìn)行適配
val ratio = 1.0 * params.frameHeight / params.frameWidth
//獲取控件高度 注意都是 height/ width
val deviceRatio = 1.0 * height / width
if (ratio >= deviceRatio) {
scaleType = ScaleType.FIT_WIDTH
} else {
scaleType = ScaleType.FIT_HEIGHT
}
mHandler.post { requestLayout() }
}
fun resumeCamera() {
if (mCamera == null) {
applyParams()
}
try {
if (mPreviewBuffer == null) {
//不要二次設(shè)置mPreviewBuffer 否則可能會(huì)有畫面延遲,原因還不知道
mPreviewBuffer = ByteArray(params.frameWidth * params.frameHeight * 2)
}
mCamera?.addCallbackBuffer(mPreviewBuffer);
mCamera?.setPreviewCallbackWithBuffer { data, camera ->
previewCallBack?.onPreviewFrame(data, camera)
camera?.addCallbackBuffer(mPreviewBuffer)
};
mCamera?.setPreviewTexture(surface)
mCamera?.startPreview()
} catch (ioe: IOException) {
// Something bad happened
ioe.printStackTrace()
}
}
fun releaseCamera() {
mCamera?.setPreviewCallbackWithBuffer(null);
mCamera?.addCallbackBuffer(null)
mCamera?.setPreviewCallback(null);
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}
fun onDestroy() {
previewCallBack == null
}
/**
* 對(duì)布局大小進(jìn)行重繪
*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val selfWidth = width
val selfHeight = height
if (params.frameWidth == 0 || params.frameHeight == 0 || selfWidth == 0 || selfHeight == 0) {
return
}
val scaleType = resolveScaleType()
if (scaleType == ScaleType.FIT_HEIGHT) {
val targetWith = params.frameWidth * selfHeight / params.frameHeight
val delta = (targetWith - selfWidth) / 2
textureView.layout(left - delta, top, right + delta, bottom)
} else {
val targetHeight = params.frameHeight * selfWidth / params.frameWidth
val delta = (targetHeight - selfHeight) / 2
textureView.layout(left, top - delta, right, bottom + delta)
}
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
//屏幕旋轉(zhuǎn)的時(shí)候這個(gè)方法才會(huì)執(zhí)行
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
surface?.release()
releaseCamera()
return true
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
//這個(gè)方法只會(huì)執(zhí)行一次
this.surface = surface
//在外面的onResume第一次調(diào)用resumeCamera()是打不開(kāi)相機(jī)的,需要在這里調(diào)用才能開(kāi)啟第一次相機(jī)預(yù)覽
//因?yàn)閛nResume的時(shí)候還無(wú)法獲取到當(dāng)前控件的寬度
resumeCamera()
}
/**
* 通過(guò)對(duì)比得到與寬高比最接近的預(yù)覽尺寸(如果有相同尺寸,優(yōu)先選擇)
*
* @param isPortrait 是否豎屏
* @param surfaceWidth 需要被進(jìn)行對(duì)比的原寬
* @param surfaceHeight 需要被進(jìn)行對(duì)比的原高
* @param preSizeList 需要對(duì)比的預(yù)覽尺寸列表
* @return 得到與原寬高比例最接近的尺寸
*/
private fun getCloselyPreSize(
isPortrait: Boolean,
surfaceWidth: Int,
surfaceHeight: Int,
preSizeList: List<Camera.Size>
): Camera.Size? {
val reqTmpWidth: Int
val reqTmpHeight: Int
// 當(dāng)屏幕為垂直的時(shí)候需要把寬高值進(jìn)行調(diào)換,保證寬大于高
if (isPortrait) {
reqTmpWidth = surfaceHeight
reqTmpHeight = surfaceWidth
} else {
reqTmpWidth = surfaceWidth
reqTmpHeight = surfaceHeight
}
//先查找preview中是否存在與surfaceview相同寬高的尺寸
for (size in preSizeList) {
if (size.width == reqTmpWidth && size.height == reqTmpHeight) {
return size
}
}
// 得到與傳入的寬高比最接近的size
val reqRatio = reqTmpWidth.toFloat() / reqTmpHeight
var curRatio: Float
var deltaRatio: Float
var deltaRatioMin = java.lang.Float.MAX_VALUE
var retSize: Camera.Size? = null
for (size in preSizeList) {
curRatio = size.width.toFloat() / size.height
deltaRatio = Math.abs(reqRatio - curRatio)
if (deltaRatio < deltaRatioMin) {
deltaRatioMin = deltaRatio
retSize = size
}
}
return retSize
}
/**
* 預(yù)覽View中的坐標(biāo)映射到,原始圖片中。應(yīng)用場(chǎng)景舉例:裁剪框
*
* @param rect 預(yù)覽View中的坐標(biāo)
*/
fun mapToOriginalRect(rect: RectF) {
val selfWidth = width
val selfHeight = height
if (params.frameWidth == 0 || params.frameHeight == 0 || selfWidth == 0 || selfHeight == 0) {
return
// TODO
}
val matrix = Matrix()
val scaleType = resolveScaleType()
if (scaleType == ScaleType.FIT_HEIGHT) {
val targetWith = params.frameWidth * selfHeight / params.frameHeight
val delta = (targetWith - selfWidth) / 2
val ratio = 1.0f * params.frameHeight / selfHeight
matrix.postTranslate(delta.toFloat(), 0f)
matrix.postScale(ratio, ratio)
} else {
val targetHeight = params.frameHeight * selfWidth / params.frameWidth
val delta = (targetHeight - selfHeight) / 2
val ratio = 1.0f * params.frameWidth / selfWidth
matrix.postTranslate(0f, delta.toFloat())
matrix.postScale(ratio, ratio)
}
matrix.mapRect(rect)
}
/**
* 原始圖片(傳到SDK里面進(jìn)行處理的圖片)中的坐標(biāo)到預(yù)覽View坐標(biāo)中的映射。應(yīng)用場(chǎng)景舉例:預(yù)覽頁(yè)面顯示人臉框。
*
* @param rectF 原始圖中的坐標(biāo)
*/
fun mapFromOriginalRect(rectF: RectF, rawImgWeight: Int, rawImgHeight: Int) {
val selfWidth = width
val selfHeight = height
if (rawImgWeight == 0 || rawImgHeight == 0 || selfWidth == 0 || selfHeight == 0) {
return
}
val matrix = Matrix()
val scaleType = resolveScaleType()
if (scaleType == ScaleType.FIT_HEIGHT) {
val targetWith = rawImgWeight * selfHeight / rawImgHeight
val delta = (targetWith - selfWidth) / 2
val ratio = 1.0f * selfHeight / rawImgHeight
matrix.postScale(ratio, ratio)
matrix.postTranslate((-delta).toFloat(), 0f)
} else {
val targetHeight = rawImgHeight * selfWidth / rawImgWeight
val delta = (targetHeight - selfHeight) / 2
val ratio = 1.0f * selfWidth / rawImgWeight
matrix.postScale(ratio, ratio)
matrix.postTranslate(0f, (-delta).toFloat())
}
matrix.mapRect(rectF)
if (params.isHorizontalMirrored) {
val left = selfWidth - rectF.right
val right = left + rectF.width()
rectF.left = left
rectF.right = right
}
}
private fun resolveScaleType(): ScaleType {
val selfRatio = 1.0f * width / height
val targetRatio = 1.0f * params.frameWidth / params.frameHeight
var scaleType = this.scaleType
if (this.scaleType == ScaleType.CROP_INSIDE) {
scaleType =
if (selfRatio > targetRatio) ScaleType.FIT_WIDTH else ScaleType.FIT_HEIGHT
}
return scaleType
}
}