안드로이드 5.0(Lollipop : API 21) 부터 MediaProjection 이 지원되었습니다.
MediaProjection 은 화면 데이터를 가져오기 위한 API 로 캡처 또는 녹화에 자주 사용됩니다.
근데 따로 문서에서 제공되는 예제 또는 샘플 같은 정보가 없더라구요.. (내가 못 찾은건가..ㅠㅠ)
아무튼 이번 기회에 간단하게 예제를 만들어보고 내용을 정리하려고 합니다.
1. 예제 다운로드
해당 예제는 MediaProjection 화면 캡처/녹화 가 포함되어있습니다.https://github.com/bictoselfdev/MediaProjectionEx
2. MediaProjection 이해
가장 먼저 MediaProjection 를 사용하려면 권한을 받아야 합니다.
권한을 받았다면 MediaProjection 는 화면에 출력 되는 데이터를 올려 줄 수 있습니다.
그리고 이를 가져오기 위해서는 Surface 가 연결된 Virtual Display 를 생성해야 합니다.
- Virtual Display : MediaProjection 이 올려 주는 데이터를 받음- Surface : Virtual Display 와 연결되어 받은 데이터를 활용 할 수 있음. (Capture 또는 Record)
요약해서 2단계로 나눠보면 아래와 같습니다.
1단계. 화면 데이터를 가져오기 위한 권한 받아오기
2단계. 가상 화면 생성 (surface 연결)
3. MediaProjection 권한 받기: Foreground Service
MediaProjection 권한을 받아오기 위해서는 반드시 Foreground Service 가 필요합니다.
Foreground Service 를 충족하지 못하면 권한을 처리하는 동작에서 아래와 같은 오류를 확인 할 수 있습니다.
java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
AndroidManifest.xml 에 아래내용이 포함되어야합니다.
저 같은 경우는 MainService class 를 Foround Service 로 동작시켰습니다.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<service
android:name=".MainService"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />
3. MediaProjection 권한 받기: 권한은 한번만
startActivityForResult 를 통해서 권한 요청을 합니다.
이때, prevIntentData / prevResultCode 를 통해 이전값을 재사용해서 최초 한번만 권한을 받도록 하였습니다.
private var prevIntentData: Intent? = null
private var prevResultCode = 0
fun screenCapture(activity: Activity, action: Consumer<Bitmap>?) {
captureCompletedAction = action
if (prevIntentData != null) {
// If you have received permission even once, proceed without requesting permission
getMediaProjectionCapture(activity, prevResultCode, prevIntentData)
} else {
// permission request
projectionManager = activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
activity.startActivityForResult(projectionManager?.createScreenCaptureIntent(), mediaScreenCapture)
}
}
정상적으로 권한을 받았다면 MediaProjectionManager 를 통해서 MediaProjection 을 가져올 수 있습니다.
fun getMediaProjectionCapture(activity: Activity, resultCode: Int, intentData: Intent?) {
projectionCapture = projectionManager?.getMediaProjection(resultCode, intentData!!)
if (projectionCapture != null) {
prevIntentData = intentData
prevResultCode = resultCode
// Create virtualDisplay
createVirtualDisplayCapture(activity)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
MediaProjectionController.mediaScreenCapture -> {
MediaProjectionController.getMediaProjectionCapture(this, resultCode, data)
}
MediaProjectionController.mediaScreenRecord -> {
MediaProjectionController.getMediaProjectionRecord(this, resultCode, data)
}
}
}
5. Create Virtual Display
캡처 데이터를 가져오기위해서 ImageReader 의 surface 를 사용합니다.
이때 새로운 이미지가 있을 때 호출되도록 ImageAvailableListener 를 등록해줍니다.
그리고 createVirualDisplay 인자에 ImageReader.surface 를 넣어줘서 가상화면을 생성합니다.
private fun createVirtualDisplayCapture(activity: Activity) {
val metrics = activity.resources?.displayMetrics!!
val density = metrics.densityDpi
val flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
width = metrics.widthPixels
height = metrics.heightPixels
// called when there is a new image : OnImageAvailableListener
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
imageReader?.setOnImageAvailableListener(ImageAvailableListener(), null)
// ImageReader Surface rendering
virtualDisplayCapture = projectionCapture?.createVirtualDisplay(
"screenCapture", width, height, density, flags,
imageReader?.surface, null, null
)
}
새로운 이미지가 들어왔을 때 호출되며 캡처 데이터를 bitmap 으로 만들어서 사용 할 수 있습니다.
private class ImageAvailableListener : OnImageAvailableListener {
override fun onImageAvailable(reader: ImageReader) {
var image: Image? = null
try {
image = reader.acquireLatestImage()
if (image != null) {
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * width
// Create bitmap
var bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height)
projectionCapture?.stop()
captureCompletedAction?.accept(bitmap)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
image?.close()
}
}
}
[Related Post]
[안드로이드] MediaProjection 화면녹화 예제&정리
[Reference]
MediaProjection Foreground Service 이용하기
0 comments: