Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Media Projection APIを使用した簡単なアプリを作成する

はじめに

こんにちは、Androidエンジニアの菅沼です。

ミラティブは Android 5.0 (API Level 21)から実装された Media Projection API を使用して画面をキャプチャしてサーバーに送り、それを視聴者さんに映像として配信することでアプリを実現しています。

今回は画面をキャプチャする部分にフォーカスし、配信はせず画面をそのまま端末上で表示するだけのシンプルな実装を行う方法を紹介させていただこうと思います。

Media Projection API の公式のドキュメントはこちらにあります。

developer.android.com

順を追って実装してみる

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 については以下の記事で詳細を取り扱っていますので、よかったら参照してください。

tech.mirrativ.stream

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!

ミラティブでは一緒に開発してくれるエンジニアを募集しています!

hrmos.co

www.mirrativ.co.jp

mirrativ.notion.site