안드로이드 5.0(Lollipop : API 21) 부터 MediaProjection 이 지원되었습니다. MediaProjection 은 화면 데이터를 가져오기 위한 API 로 캡처 또는 녹화에 자주 사용됩니다. 근데 따로 문서에서 제공되는 예제 또는...

[안드로이드] MediaProjection 캡처 예제&정리 (ForegroundService, PermissionOnce)

안드로이드 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]

android developer

stackoverflow

MediaProjection Foreground Service 이용하기

 

 

 

0 comments: