그룹 통화 화면 공유
그룹 통화(컨퍼런스)에서 화면 공유를 구현하는 예제 코드입니다.
여기서는 MediaProjection API를 사용하여 서비스에서 화면 공유 기능을 구현하는 방법을 설명합니다. 이 구현은 앱이 백그라운드에 있는 경우에도 화면 공유를 유지할 수 있도록 지원합니다.
필수 조건
시작하기 전에 다음 작업을 수행해야 합니다.
필수 권한 추가
AndroidManifest.xml
에 다음 권한을 포함시키세요.
<!-- For Foreground Service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- For Screen Capture on Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
포그라운드 서비스 등록
앱이 백그라운드 상태에서도 화면 공유를 유지하도록 mediaProjection
포그라운드 서비스 유형을 지정하여 서비스를 선언하세요.
<service
android:name=".MyScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
포그라운드 서비스와 관련된 더 자세한 정보는 Android 공식 문서(Foreground services, Foreground service types are required)를 참조하세요.
송신 측 - 화면 캡처 서비스 구현
이 서비스는 MediaProjection을 사용하여 화면 캡처의 시작 및 중지 작업을 처리하고 그룹 통화와 연동합니다.
class MyScreenCaptureService : Service() {
companion object {
private const val SCREEN_CAPTURE_ID = "Screen capture"
// This can be any dummy number
private const val NOTIFICATION_ID = 315
private const val ACTION_START_SCREEN_CAPTURE = "ACTION_START_SCREEN_CAPTURE"
private const val ACTION_STOP_SCREEN_CAPTURE = "ACTION_STOP_SCREEN_CAPTURE"
private lateinit var mediaProjectionResult:ActivityResult
private var subgroupName: String? = null
@Synchronized
@JvmStatic
fun startScreenCapturing(context: Context, projectionPermissionResult: ActivityResult, destSubgroupName:String? = null) {
val intent = Intent(context, MyScreenCaptureService::class.java).apply {
action = ACTION_START_SCREEN_CAPTURE
mediaProjectionResult = projectionPermissionResult
subgroupName = destSubgroupName
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
@Synchronized
@JvmStatic
fun stopScreenCapturing(context: Context) {
val intent = Intent(context, MyScreenCaptureService::class.java).apply {
action = ACTION_STOP_SCREEN_CAPTURE
}
context.startService(intent)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action ?: return START_NOT_STICKY
when (action) {
ACTION_START_SCREEN_CAPTURE -> {
onScreenShareStarted()
}
ACTION_STOP_SCREEN_CAPTURE -> {
onScreenShareStopped()
}
}
return START_NOT_STICKY
}
private fun onScreenShareStarted() {
val notification = createNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
} else {
startForeground(NOTIFICATION_ID, notification)
}
mediaProjectionResult.data?.let { data ->
val screenSource = ScreenCapturerVideoSource.getInstance(mediaProjectionResult.resultCode, data)
screenSource.setOnErrorListener { onScreenShareStopped() }
PlanetKit.getConference()?.let {
it.startMyScreenShare(screenSource, subgroupName) { response ->
if (!response.isSuccessful) {
onScreenShareStopped()
}
}
}
}
}
private fun onScreenShareStopped() {
PlanetKit.getConference()?.let {
it.stopMyScreenShare()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
stopSelf()
}
private fun createNotification(): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
SCREEN_CAPTURE_ID,
SCREEN_CAPTURE_ID,
NotificationManager.IMPORTANCE_NONE
)
// Register the channel with the system
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
return NotificationCompat.Builder(
this,
SCREEN_CAPTURE_ID
)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle("Screen share in progress")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
}
송신 측 - 액티비티에서 화면 캡처 관리
이 액티비티는 화면 캡처 권한을 요청하고 서비스를 시작하거나 중지하는 방법을 보여줍니다.
class MyConferenceActivity : AppCompatActivity() {
private var myScreenShareSubgroupName: String? = null
private val requestMediaProjectionActivityResultLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { projectionPermissionResult: ActivityResult ->
val data = projectionPermissionResult.data
if (projectionPermissionResult.resultCode != RESULT_OK || data == null) {
Log.e(TAG, "Could not start screen capture!")
return@registerForActivityResult
}
MyScreenCaptureService.startScreenCapturing(context, permissionResult, myScreenShareSubgroupName)
}
private fun requestMyScreenCapture(subgroupName: String?) {
myScreenShareSubgroupName = subgroupName
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
// This initiates a prompt dialog for the user to confirm screen projection.
requestMediaProjectionActivityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
}
fun stopMyScreenCapture(context: Context) {
MyScreenCaptureService.stopScreenCapturing(context)
}
}
송신 측 - 화면 공유 목적지 변경하기
화면 공유의 목적지는 목적지 유형에 따라 다음과 같이 변경할 수 있습니다.
- 기본 방(main room)이 아닌 다른 서브그룹으로 목적지를 변경하려면 서브그룹 이름을 인자로
changeMyScreenShareDestination()
을 호출하세요. - 기본 방으로 목적지를 변경하려면
changeMyScreenShareDestinationToMainRoom()
을 호출하세요.
// Change the destination to a subgroup other than the main room
conference?.changeMyScreenShareDestination(subgroupName) {
Log.d("Conference", "changeMyScreenShareDestination onResponse: $it")
if(it.isSuccessful) {
...
}
else {
...
}
}
// Change the destination to the main room
conference?.changeMyScreenShareDestinationToMainRoom() {
Log.d("Conference", "changeMyScreenShareDestinationToMainRoom onResponse: $it")
if(it.isSuccessful) {
...
}
else {
...
}
}
수신 측 - 화면 공유 업데이트 이벤트 수신
우선 PeerControlListener
의 onScreenShareUpdated
이벤트를 통해 피어의 화면 공유가 시작 혹은 중지됐는지 확인합니다.
class PeerContainer internal constructor(
peer: PlanetKitConferencePeer
) {
val peerControl: PlanetKitPeerControl = peer.createPeerControl()
?: throw IllegalStateException("Failed to create peer control.")
init {
val peerControlListener = object : PlanetKitPeerControl.PeerControlListener {
...
override fun onScreenShareUpdated(peer: PlanetKitConferencePeer, state: PlanetKitScreenShareState, subgroupName: String?) {
Log.d("Conference", "onScreenShareUpdated: user=(${peer.user}), screenShareState=$screenShareState, subgroup=$subgroupName")
}
}
peerControl.register(peerControlListener)
}
}
수신 측 - 피어의 화면 공유 비디오 시작
피어의 화면 공유 비디오를 시작하려면 PlanetKitPeerControl
의 startScreenShare()
를 호출하세요.
var peer: PlanetKitUser
var videoView: PlanetKitVideoView
conference.addPeerScreenShareView(peer, videoView)
videoView.resetFirstFrameRendered()
peerControl?.startScreenShare(subgroupName, { response ->
Log.d("Conference", "isSuccessful=${response.isSuccessful}")
})
수신 측 - 피어의 화면 공유 비디오 중지
피어의 화면 공유 비디오를 중지하려면 PlanetKitPeerControl
의 stopScreenShare()
를 호출하세요.
peerControl?.stopScreenShare(null, { response ->
Log.d("Conference", "isSuccessful=${response.isSuccessful}")
})
conference?.removePeerScreenShareView(peer, videoView)