はじめに
こんにちは、Androidエンジニアの菅沼です。
ミラティブは Android 5.0 (API Level 21)から実装された Media Projection API を使用して画面をキャプチャしてサーバーに送り、それを視聴者さんに映像として配信することでアプリを実現しています。
今回は画面をキャプチャする部分にフォーカスし、配信はせず画面をそのまま端末上で表示するだけのシンプルな実装を行う方法を紹介させていただこうと思います。
Media Projection API の公式のドキュメントはこちらにあります。
順を追って実装してみる
View を用意する
まずは AndroidEmbeddedExternalSurface と Button をColumn で並べてみました。 AndroidEmbeddedExternalSurface は androidx.compose.foundation 1.6 から加わった Jetpack Compose で利用できる TextureView のラッパー実装です。ここにミラーリングした映像を表示するコードを書いていきます。
var externalSurface by remember { mutableStateOf<Surface?>(null) } var surfaceWidth = 0 var surfaceHeight = 0 Column { AndroidEmbeddedExternalSurface(modifier = Modifier.size(300.dp)) { onSurface { surface, width, height -> externalSurface = surface surfaceWidth = width surfaceHeight = height } } Button(onClick = {}) { Text(text = "Launch ScreenCaptureIntent!") } }
キャプチャ開始許可を得るためのダイアログを表示する
上記のボタンに以下のようなコードでキャプチャを開始するための許可を得るためのコード挿入します。
val context = LocalContext.current val mediaProjectionManager = context.getSystemService(MediaProjectionManager::class.java) val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { _ -> } val intent = mediaProjectionManager.createScreenCaptureIntent() launcher.launch(intent)
実行すると以下のようなダイアログが表示されます。
仮想ディスプレイを作成する
先ほどの rememberLauncherForActivityResult では何もしてませんでしたが、今度はここで AcitivtyResult を受け取り、createVirtualDisplay を行ってミラーリングを開始します。
引数には AndroidEmbeddedExternalSurface の onSurface から得られる surface, width, height をそれぞれ指定します。
val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult val mediaProjection = mediaProjectionManager.getMediaProjection(result.resultCode, result.data!!) mediaProjection.registerCallback(object : MediaProjection.Callback() {}, null) // Android 14 から必要になりました mediaProjection.createVirtualDisplay( "ScreenCapture", surfaceWidth, surfaceHeight, DisplayMetrics.DENSITY_DEFAULT, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, externalSurface, null, null ) }
mediaProjection .registerCallback については以下の記事で詳細を取り扱っていますので、よかったら参照してください。
Android.xml の設定とサービスの設定
Media Projection API を使用するためにはパーミッションの設定とフォアグラウンドサービスが必要になるため、以下を加えます。
// これは単に通知を出すためのサービスで特に Media Projection 特有のコードはありません。 class SimpleProjectionService : Service() { override fun onBind(intent: Intent?): IBinder? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val channelId = "channel_id" val notificationManager = getSystemService(NotificationManager::class.java) if (notificationManager.getNotificationChannel(channelId) == null) { notificationManager.createNotificationChannel( NotificationChannel(channelId, "Channel 1", NotificationManager.IMPORTANCE_HIGH) ) } val notification = NotificationCompat.Builder(this, channelId) notification.setContentTitle("キャプチャしてます") startForeground(1, notification.build()) return START_STICKY } }
<manifest ...> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <application ...> ... <service android:name=".SimpleProjectionService" android:exported="false" android:foregroundServiceType="mediaProjection" /> </application> </manifest>
- Android 9(API Level 28)以降をターゲットとするアプリでは android.permission.FOREGROUND_SERVICE を設定しないと SecurityExceptionが発行されます
- android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTIONも追加します
- Media Projection を稼働させるために foregroundServiceType = mediaProjection が設定されたサービスを追加しました
これで完成です。
実装コード全体
先ほどの実際にそのまま実行できるコードをまとめると以下になります。
サービスのコードと AndroidManifest.xml は上記と同様です。
@Preview @Composable fun StartMediaProjection() { val context = LocalContext.current val mediaProjectionManager = context.getSystemService(MediaProjectionManager::class.java) var externalSurface by remember { mutableStateOf<Surface?>(null) } var surfaceWidth = 0 var surfaceHeight = 0 val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult val mediaProjection = mediaProjectionManager.getMediaProjection(result.resultCode, result.data!!) mediaProjection.registerCallback(object : MediaProjection.Callback() {}, null) // Android 14 から必要になりました mediaProjection.createVirtualDisplay( "ScreenCapture", surfaceWidth, surfaceHeight, DisplayMetrics.DENSITY_DEFAULT, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, externalSurface, null, null ) } Column { AndroidEmbeddedExternalSurface(modifier = Modifier.size(300.dp)) { onSurface { surface, width, height -> externalSurface = surface surfaceWidth = width surfaceHeight = height } } Button( onClick = { // createVirtualDisplay() を実行する前にサービスが起動している必要があるため、ここで開始しておく context.startForegroundService(Intent(context, SimpleProjectionService::class.java)) val intent = mediaProjectionManager.createScreenCaptureIntent() launcher.launch(intent) } ) { Text(text = "Launch ScreenCaptureIntent!") } } }
実行結果
以下はアプリを起動し「Launch ScreenCatureIntent」を押した後、「画面全体」を選択し、開始した後の gif です。
画面全体がキャプチャされているため、タスク一覧を表示した時に画面がそのまま写っているのがわかると思います。
まとめ
Media Projection API を使用したアプリを作成する方法を説明いたしました。
ぜひ色々試して遊んでいただけると幸いです。
We are hiring!
ミラティブでは一緒に開発してくれるエンジニアを募集しています!