Camera2 APIによるカメラのプレビュー表示について
Androidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。
SurfaceViewを使う場合、画面の回転はやってくれるので、アスペクト比の調整のみを実装すればよいですが、TextureViewを使う場合、画面の回転はやってくれないので、そちらの実装もやらなければなりません。
本稿ではAndroidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。
実装
レイアウト
SurfaceViewをTextureViewに置き換えます。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<!-- SurfaceViewをTextureViewに置き換える -->
<TextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
カメラ起動
surfaceHolderをsurfaceTextureに置き換えます。
private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cameraDevice = device
cameraDevice?.let { cameraDevice ->
val surfaceList: ArrayList<Surface> = arrayListOf()
// surfaceHolderをsurfaceTextureに置き換えます。
surfaceTexture?.let {
surfaceList.add(Surface(it))
}
try {
captureRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
surfaceList.forEach {
addTarget(it)
}
}.build()
cameraDevice.createCaptureSession(
surfaceList,
cameraCaptureSessionStateCallback,
null
)
} catch (cameraAccessException: CameraAccessException) {
cameraAccessException.printStackTrace()
}
}
}
override fun onDisconnected(device: CameraDevice) {
cameraDevice = null
}
override fun onError(device: CameraDevice, error: Int) {
cameraDevice = null
}
}
Camera2 APIにCamera.Parameters.setPreviewDisplay()のようなプレビューサイズを直接的に設定するAPIがないが、TextureView.surfaceTexture.setDefaultBufferSize()でプレビューサイズを設定できます。そこで、まずCameraCharacteristicsからサポートサイズを取得します。
private fun openCamera() {
val context = context ?: return
val cameraManager: CameraManager = cameraManager ?: return
val cameraId = getCameraId() ?: return
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
return
}
try {
// CameraCharacteristicsからカメラのサポートサイズを取得します。
cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(SurfaceTexture::class.java)?.getOrNull(0)?.let { size ->
updateSurfaceSize(binding.textureView, size.width, size.height)
}
cameraManager.openCamera(cameraId, cameraDeviceStateCallback, null)
} catch (cameraAccessException: CameraAccessException) {
cameraAccessException.printStackTrace()
}
}
TextureView
- SurfaceViewをTextureViewに置き換えます。
- [注意点]:onSurfaceTextureSizeChangedの中でプレビューサイズと回転角度を設定します。
// SurfaceHolderをSurfaceTextureに置き換える
// private var surfaceHolder: SurfaceHolder? = null
private var surfaceTexture: SurfaceTexture? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<MainFragmentBinding>(
inflater,
R.layout.main_fragment,
container,
false
)
// SurfaceViewをTextureViewに置き換える
binding.textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
surfaceTexture = surface
surfaceWidth = width
surfaceHeight = height
checkAndAskPermission()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
surfaceTexture = surface.apply {
// [注意点]:ここでプレビューサイズと回転角度を設定する。
updateSurfaceTexture(this, width, height)
}
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
return false
}
}
return binding.root
}
[注意点]:プレイビューサイズと回転角度の設定
- 幅と高さを決めます。端末が横の場合、幅=width、高さ=heightになります。端末が縦の場合、幅=height、高さ=widthになります。
- プレビューの中心点を軸に、プレビューを回転させ、かつ正しいアスペクト比にする行列を作ります。
- 行列をカメラに渡し、プレビューを調整します。
private fun updateSurfaceTexture(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
// 端末が横の場合、幅=width、高さ=heightになります。
var newWidth = width
var newHeight = height
// 端末が縦の場合、幅=height、高さ=widthになります。
if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
newWidth = height
newHeight = width
}
// setDefaultBufferSizeでプレビューサイズを設定する。
surfaceTexture.setDefaultBufferSize(newWidth, newHeight)
val transform = Matrix()
val center = Point(newWidth / 2, newHeight / 2)
// プレビューの中心点を軸に、プレビューを回転させる行列を作る
when (activity?.windowManager?.defaultDisplay?.rotation) {
Surface.ROTATION_90 -> {
transform.postRotate(270.0f, center.x.toFloat(), center.y.toFloat())
}
Surface.ROTATION_180 -> {
transform.postRotate(180.0f, center.x.toFloat(), center.y.toFloat())
}
Surface.ROTATION_270 -> {
transform.postRotate(90.0f, center.x.toFloat(), center.y.toFloat())
}
}
// 正しいアスペクト比に戻す
when (activity?.windowManager?.defaultDisplay?.rotation) {
Surface.ROTATION_90, Surface.ROTATION_270 -> {
transform.postScale(newWidth.toFloat() / newHeight.toFloat() * newWidth.toFloat() / newHeight.toFloat(),
1.0f,
center.x.toFloat(),
center.y.toFloat())
}
}
// カメラに行列を渡し、プレビューを調整する
binding.textureView.setTransform(transform)
surfaceWidth = width
surfaceHeight = height
}
まとめ
Camera2 API + TextureViewの方法は、Camera2 API + SurfaceView方法とCamera API + TextureView方法よりも自由度が高いですが、プレビュー表示処理がだいぶ複雑になります。
個人的には仕様制限がないのプレビュー表示について
Androidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。
SurfaceViewを使う場合、画面の回転はやってくれるので、アスペクト比の調整のみを実装すればよいですが、TextureViewを使う場合、画面の回転はやってくれないので、そちらの実装もやらなければなりません。
本稿ではAndroidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。
実装
レイアウト
SurfaceViewをTextureViewに置き換えます。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<!-- SurfaceViewをTextureViewに置き換える -->
<TextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
カメラ起動
surfaceHolderをsurfaceTextureに置き換えます。
private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cameraDevice = device
cameraDevice?.let { cameraDevice ->
val surfaceList: ArrayList<Surface> = arrayListOf()
// surfaceHolderをsurfaceTextureに置き換えます。
surfaceTexture?.let {
surfaceList.add(Surface(it))
}
try {
captureRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
surfaceList.forEach {
addTarget(it)
}
}.build()
cameraDevice.createCaptureSession(
surfaceList,
cameraCaptureSessionStateCallback,
null
)
} catch (cameraAccessException: CameraAccessException) {
cameraAccessException.printStackTrace()
}
}
}
override fun onDisconnected(device: CameraDevice) {
cameraDevice = null
}
override fun onError(device: CameraDevice, error: Int) {
cameraDevice = null
}
}
Camera2 APIにCamera.Parameters.setPreviewDisplay()のようなプレビューサイズを直接的に設定するAPIがないが、TextureView.surfaceTexture.setDefaultBufferSize()でプレビューサイズを設定できます。そこで、まずCameraCharacteristicsからサポートサイズを取得します。
private fun openCamera() {
val context = context ?: return
val cameraManager: CameraManager = cameraManager ?: return
val cameraId = getCameraId() ?: return
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
return
}
try {
// CameraCharacteristicsからカメラのサポートサイズを取得します。
cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(SurfaceTexture::class.java)?.getOrNull(0)?.let { size ->
updateSurfaceSize(binding.textureView, size.width, size.height)
}
cameraManager.openCamera(cameraId, cameraDeviceStateCallback, null)
} catch (cameraAccessException: CameraAccessException) {
cameraAccessException.printStackTrace()
}
}
TextureView
- SurfaceViewをTextureViewに置き換えます。
- [注意点]:onSurfaceTextureSizeChangedの中でプレビューサイズと回転角度を設定します。
// SurfaceHolderをSurfaceTextureに置き換える
// private var surfaceHolder: SurfaceHolder? = null
private var surfaceTexture: SurfaceTexture? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<MainFragmentBinding>(
inflater,
R.layout.main_fragment,
container,
false
)
// SurfaceViewをTextureViewに置き換える
binding.textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
surfaceTexture = surface
surfaceWidth = width
surfaceHeight = height
checkAndAskPermission()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
surfaceTexture = surface.apply {
// [注意点]:ここでプレビューサイズと回転角度を設定する。
updateSurfaceTexture(this, width, height)
}
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
return false
}
}
return binding.root
}
[注意点]:プレイビューサイズと回転角度の設定
- 幅と高さを決めます。端末が横の場合、幅=width、高さ=heightになります。端末が縦の場合、幅=height、高さ=widthになります。
- プレビューの中心点を軸に、プレビューを回転させ、かつ正しいアスペクト比にする行列を作ります。
- 行列をカメラに渡し、プレビューを調整します。
private fun updateSurfaceTexture(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
// 端末が横の場合、幅=width、高さ=heightになります。
var newWidth = width
var newHeight = height
// 端末が縦の場合、幅=height、高さ=widthになります。
if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
newWidth = height
newHeight = width
}
// setDefaultBufferSizeでプレビューサイズを設定する。
surfaceTexture.setDefaultBufferSize(newWidth, newHeight)
val transform = Matrix()
val center = Point(newWidth / 2, newHeight / 2)
// プレビューの中心点を軸に、プレビューを回転させる行列を作る
when (activity?.windowManager?.defaultDisplay?.rotation) {
Surface.ROTATION_90 -> {
transform.postRotate(270.0f, center.x.toFloat(), center.y.toFloat())
}
Surface.ROTATION_180 -> {
transform.postRotate(180.0f, center.x.toFloat(), center.y.toFloat())
}
Surface.ROTATION_270 -> {
transform.postRotate(90.0f, center.x.toFloat(), center.y.toFloat())
}
}
// 正しいアスペクト比に戻す
when (activity?.windowManager?.defaultDisplay?.rotation) {
Surface.ROTATION_90, Surface.ROTATION_270 -> {
transform.postScale(newWidth.toFloat() / newHeight.toFloat() * newWidth.toFloat() / newHeight.toFloat(),
1.0f,
center.x.toFloat(),
center.y.toFloat())
}
}
// カメラに行列を渡し、プレビューを調整する
binding.textureView.setTransform(transform)
surfaceWidth = width
surfaceHeight = height
}
まとめ
Camera2 API + TextureView方法は、Camera2 API + SurfaceView方法とCamera API + TextureView方法よりも自由度が高いですが、プレビュー表示処理がだいぶ複雑になります。また、Camera2 APIの使い方もCamera APIより複雑ですし、カメラのプレビューサイズを直接に指定するAPIもありません。