1対1通話画面共有
1対1通話で画面共有を実装するサンプルコードです。
ここでは、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を使用して画面キャプチャーの開始および中止作業を処理し、1対1通話と連携します。
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 var callInstanceId:Int = 0
private lateinit var mediaProjectionResult:ActivityResult
@Synchronized
@JvmStatic
fun startScreenCapturing(context: Context, instanceId:Int, projectionPermissionResult: ActivityResult) {
val intent = Intent(context, MyScreenCaptureService::class.java).apply {
action = ACTION_START_SCREEN_CAPTURE
this@Companion.callInstanceId = instanceId
mediaProjectionResult = projectionPermissionResult
}
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.getCall(callInstanceId)?.let {
it.startMyScreenShare(screenSource) { response ->
if (!response.isSuccessful) {
onScreenShareStopped()
}
}
}
}
}
private fun onScreenShareStopped() {
PlanetKit.getCall(callInstanceId)?.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 MyCallActivity : AppCompatActivity() {
private var callInstanceId: Int = 0
private val requestMediaProjectionActivityResultLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { projectionPermissionResult: ActivityResult ->
val data = projectionPermissionResult.data
if (projectionPermissionResult.resultCode != RESULT_OK || data == null) {
return@registerForActivityResult
}
MyScreenCaptureService.startScreenCapturing(this, callInstanceId, projectionPermissionResult)
}
private fun requestMyScreenCapture(call: PlanetKitCall) {
callInstanceId = call.instanceId
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)
}
}
受信側 - 画面共有のアップデートイベントを受信する
まずは、CallListener
のonPeerScreenShareStarted
またはonPeerScreenShareStopped
イベントを通じて、ピアの画面共有が開始あるいは中止されたかを確認します。
private val makeAcceptCallListener = object : MakeCallListener, AcceptCallListener {
override fun onPeerScreenShareStarted(call: PlanetKitCall) {
// Add your own implementation.
...
}
override fun onPeerScreenShareStopped(call: PlanetKitCall, hasReason: Boolean, reason: Int) {
// Add your own implementation.
...
}
...
}
受信側 - ピアの画面共有ビデオをレンダリングする
onPeerScreenShareStarted
イベントが発生した後、PlanetKitVideoView
を追加して共有画面を表示できます。
var videoView: PlanetKitVideoView
call.addPeerScreenShareView(videoView)
受信側 - ピアの画面共有ビデオを除去またはビューを非表示する
ピアの共有画面をレンダリングしたくない場合は、PlanetKitCall
でPlanetKitVideoView
を除去するか、非表示にします。
-
PlanetKitVideoView
の除去var videoView: PlanetKitVideoView
call.removePeerScreenShareView(videoView) -
PlanetKitVideoView
の非表示var videoView: PlanetKitVideoView
videoView.isVisible = false