SkyWay Android SDKでgetDisplayMediaを利用して画面共有を行う方法

# はじめに
WebRTCライブラリ(以下、libwebrtcと呼ぶ)では、[2016秋にリリースされたChrome M55相当のバージョン](https://groups.google.com/forum/#!msg/discuss-webrtc/BqqFMSR6s1E/rlPYFD0NCQAJ)から、Android Screen Capture in WebRTCという機能（以下、libwebrtcのScreen Capture機能）が実装され、libwebrtcベースのNative Android AppでもScreen Captureが出来るようになりました。2020.06現在の同機能の最新のソースコードは [こちら](https://source.chromium.org/chromium/external/webrtc/src/+/master:sdk/android/api/org/webrtc/ScreenCapturerAndroid.java)です。

SkyWayでは[Android SDK v1.3.0](https://github.com/skyway/skyway-android-sdk/releases/tag/v1.3.0)でこの機能を利用した、[getDisplyaMedia](https://skyway.github.io/android-reference/classio_1_1skyway_1_1_peer_1_1_browser_1_1_navigator.html#ac890a254271dd9a2a42503fff701438a)というAPIを提供開始しました。[Community Edition](https://webrtc.ecl.ntt.com/pricing.html)でももちろん利用可能です。

ちなみに、getDisplyaMediaは[ブラウザに搭載されているScreen Capture API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia)として有名ですが、SkyWay Android SDKにおけるそれは、libwebrtcのScreen Capture機能をベースにしたSkyWay独自のAPIとなります。他のWebRTCプラットフォームサービスでも、同様の機能を提供しているところはあるようですが、ネーミングはプラットフォーム毎に異なりますのでご注意下さい。

この記事では、SkyWay Android SDKで画面共有を行う方法を解説します。尚、前半部分のユーザに許可を求める部分の処理は、同じようにlibwebrtcのScreen Capture機能を使う他のSDKでも応用可能だと思います。


# 画面共有のイメージ

画面共有を行った際の利用イメージは以下の通りです。アプリ画面を共有するのではなく、Android端末の画面全体が共有されます。共有するアプリや画面を選ぶことは出来ません。

![画面共有イメージ3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/1d5dcf41-7be1-050a-ef58-324bec8d3211.gif)

# 実装の流れ

SkyWay Android SDKを使って画面共有を行うために必要な手順は以下の通りです。

1. MediaProjectionManagerを使ってScreen Captureの許可をユーザに求める
2. 結果を取得
3. getDisplayMediaを実行
4. Screen Captureの停止

詳しく見ていきます。

## 1.MediaProjectionManagerを使ってScreen Captureの許可をユーザに求める

AndroidでScreen Captureを実行するために、MediaProjectionManagerインスタンスを取得します。この時、getSystemServiceメソッドでContext.MEDIA_PROJECTION_SERVICEを引数指定して下さい。
次にMediaProjectionManagerのcreateScreen CaptureIntentメソッドでIntentを取得し、startActivityForResultでユーザにScreen Captureの許可を求め、その結果を取得します。
CAPTURE_PERMISSION_REQUEST_CODE については適当な整数を設定します。今回の処理の本質では有りませんが、複数のActivityとデータをやり取りするようなプログラムの場合は識別できるように値を分けます。

```java
private static final int CAPTURE_PERMISSION_REQUEST_CODE = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);

	MediaProjectionManager mediaProjectionManager =
			(MediaProjectionManager) getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
	startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), CAPTURE_PERMISSION_REQUEST_CODE);
}
```
UI上は以下のようなダイアログが表示されます。
![許可ダイアログ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/129d5081-509e-1d22-4f23-f2db4aeec006.png)


## 2. 許可状況を取得

startActivityForResultで実行したActivityの結果は、onActivityResultで取得します。
必要に応じて、CAPTURE_PERMISSION_REQUEST_CODEを使ってActivityを識別し、resultCodeとdataを得ます。
この情報はgetDisplayMediaの実行に必要になります。

```java
private static int mediaProjectionPermissionResultCode;
private static Intent mediaProjectionPermissionResultData;

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
	if (requestCode != CAPTURE_PERMISSION_REQUEST_CODE)
		return;
	mediaProjectionPermissionResultCode = resultCode;
	mediaProjectionPermissionResultData = data;
	startScreenCapture();
}
```

## 3. getDisplayMediaを実行

mediaProjectionPermissionResultCodeの結果を参照し、許可が得られていればgetDisplayMediaを実行します。
基本的な使い方はNavigator.getUserMediaと変わりません。

```java
private MediaStream _screenStream;

void startScreenCapture() {
	if(mediaProjectionPermissionResultCode == Activity.RESULT_OK){
		Navigator.initialize(_peer);  // 事前にnew Peer()で_peerインスタンスを作成して下さい
		MediaConstraints constraints = new MediaConstraints();
		constraints.maxWidth = 1080;
		constraints.maxHeight = 2280;
		_screenStream = Navigator.getDisplayMedia(constraints,mediaProjectionPermissionResultData,new MediaProjectionCallback());
		Canvas canvas = (Canvas) findViewById(R.id.svLocalView);
		_screenStream.addVideoRenderer(canvas,0);
	}
}

private class MediaProjectionCallback extends MediaProjection.Callback {
	@Override
	public void onStop() {
		Log.e("MediaProjection", "onStop");
	}
}

```

getDisplayMediaのコードは詳しめに解説していきます。

### constrainsで指定できるのは maxWidth / maxHeight のみ

getUserMediaではもう少し柔軟な設定が可能ですが、getDisplayMediaではmaxWidthとmaxHeightの2種類のみです。
これは、[libwebrtcのコード](https://source.chromium.org/chromium/external/webrtc/src/+/master:sdk/android/api/org/webrtc/ScreenCapturerAndroid.java;l=106)を見て頂ければわかりますが、そもそも解像度（WidthとHeight）の設定しか出来ないためです。
挙動としては、maxWidthとmaxHeightで指定した値でScreen Captureを実行します。
また、フレームレートはlibwebrtcのコード上ignoredFramerateと書かれてある通り、設定しても効きません。

尚、WebRTC全般に言われる話ですが、ここで解像度を設定して設定どおりにScreen Captureが出来たとしても、その解像度で終始相手に映像が送信されるわけでは有りません。通信環境などにより動的に変化します。

### getDisplayMediaの第二引数と第三引数

getDisplayMediaの第二引数には、先程取得したmediaProjectionPermissionResultDataを設定します。
第三引数にはMediaProjectionCallbackを設定します。これは、MediaProjectionセッションが無効になった場合に発火します。詳しくは [Android Developers - MediaProjection.Callback ](https://developer.android.com/reference/android/media/projection/MediaProjection.Callback#onStop())を御覧下さい。

### 描画・通話での利用

getDisplayMediaで取得したMediaStreamは、SkyWayのSDK上は、getUserMediaで取得したカメラ映像と同じ扱いです。
addVideoRendererを利用して指定のcanvasに描画できますし、peer.callやpeer.joinRoomにStreamの引数として渡すことで、ビデオ通話に利用できます。

## 4. Screen Captureの停止

getUserMediaで取得したカメラ映像と同じく、MediaStream.close()を使うことでScreen Captureの停止が可能です。これも、SkyWayのSDKとしての動作になります。

```java
_screenStream.close();
```

# 余談: SkyWay iOS SDKで画面共有はできるの？

現時点では出来ません。

以下、参考情報です。
iOS向けのScreen Capture機能はlibwebrtcにも用意されていません。
しかしながら、ReplayKitが提供する[RPScreenRecorder#startCapture](https://developer.apple.com/documentation/replaykit/rpscreenrecorder/2867291-startcapture)でScreen Captureした結果がビデオフレームとして取得できるため、そのビデオフレームをWebRTCで相手に送ることで、画面共有機能は実現可能です（SkyWay iOS SDKではまだ未対応）。