目的
Android搭載ウェアラブル端末触る際に出力されてる画面を手元のスマホに写すようにすれば楽じゃね?ってのがスタート。
初Androidアプリ作成&初Kotlinなのでたくさん叩いてください。
今回は非同期処理になるのでMediaCodecのCallBackで処理します。
送信端末がZenfone5、受信側がPixel3で動作確認をしています。
OSのバージョンによっては修正が必要な箇所がでますが適宜書き換えてください。
使ったもの、参考にしたもの
-
AndroidDevelopers
- 公式。
-
BluetoothChat
- Bluetoothの接続、送受信に使用
- Bluetooth周りは全てここ使ってます。
-
Androidの画面をPCにミラーリングするソフトを作る1
- この記事をBluetooth版にしたものが今回作成したもの。
- MediaProjectionのところは全部真似しています。
-
MediaCodec クラス概要 和訳
- MediaCodecの理解、日本語助かります。
流れ
送信側
- エンコーダの作成
- 解像度、FPSなどの指定
- コールバック関数の設定
- エンコーダに設定値反映
- エンコーダ入力用Surfaceの取得
- エンコーダの開始
- エンコーダが出力完了する度にコールバック関数が呼ばれ
- フレームごとにBluetoothで送信
受信側
- デコーダの作成
- 解像度、FPS、出力先などの指定
- エンコーダに設定値反映
- コールバック関数の設定
- デコーダの開始
- デコーダへ入力
- Surfaceが更新される
解説
送信側
エンコーダの作成
createEncoderByTypeでcodecのインスタンスを生成
戻り値にcodecインスタンス、引数には使用するエンコード方法を指定。
文字列で指定しますが定数があるので覚えなくても問題なし。
エンコード/デコードの種類
codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
解像度、FPSなどの指定
MediaFormat.createVideoFormatでformatの作成。
format = MediaFormat.createVideoFormat(MIMETYPE, width, height)
引数にはエンコード方法、縦横の解像度を指定。
作成したformatにsetIntegerメソッドで足りていない情報を設定。
今回は
- カラーフォーマット
- ビットレート
- FPS
- キャプチャレート
- Iフレームインターバル
を設定しています。
formatの設定値
format.setInteger(
MediaFormat.KEY_COLOR_FORMAT, // 設定するパラメータ
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface // 設定値
)
コールバック関数の設定
冒頭にも書きましたが非同期処理の場合はコールバックにて入出力のタイミングが通知される。
コーデックの抽象メソッドは下記4つ
// コーデックの入力バッファが使用可能になった時に呼ばれる
onInputBufferAvailable(codec: MediaCodec, index: Int)
// コーデックの出力バッファが使用可能になった時に呼ばれる
onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo)
// コーデックエラー時に呼ばれる
onError(codec: MediaCodec, e: MediaCodec.CodecException)
// bitrateやfps、解像度などフォーマット変更時に呼ばれる(ここは理解不足です)
onOutputFormatChanged(codec: MediaCodec, format: MediaFormat)
エンコード側は入力→Surfaceから自動取得、出力→フレームをBluetooth送信なので
onOutputBufferAvailableのみ実装、残りは空実装してください。
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
val buffer = codec.getOutputBuffer(index)
val array = ByteArray(info.size + 4)
buffer?.run {
this.get(array, 0, info.size)
// emulation_prevention_three_byte
// 0x00000002はH264のNALパケットには出てこない
array[0 + info.size] = 0.toByte()
array[1 + info.size] = 0.toByte()
array[2 + info.size] = 0.toByte()
array[3 + info.size] = 2.toByte()
}
val intent = Intent()
intent.action = "DISPLAY_UPDATE"
intent.putExtra("IMAGE", array)
// アプリ内のみに対してブロードキャスト送信
val mBroadcastReceiver = LocalBroadcastManager.getInstance(context)
mBroadcastReceiver.sendBroadcast(intent)
// バッファを解放
codec.releaseOutputBuffer(index, false)
}
codec.getOutputBuffer(index)でByteBuffer型のフレームデータを取得。
このデータがH264フォーマットの1フレーム分ですが、このまま投げると受信側で切れ目がわからなくなるのでフッタを追加。
codec.releaseOutputBuffer()でバッファの開放を忘れないこと。
第二引数をTrueにするとSurfaceへ出力される。エンコーダ側なのでFalse。
送るデータの準備ができたらBluetoothクラスに送信します!が理解不足のため、onOutputBufferAvailable内で送信することができないのでIntentでMainActivityに渡しています。
エンコーダに設定値反映
MediaFormat.createVideoFormatとformat.setIntegerで設定したformatをcodecに反映させる。
// エンコーダを設定
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
第一引数にformat、第二引数に出力先Surfaceを指定。
第四引数にはフラグを設定しますが、送信側(エンコーダ)の場合はCONFIGURE_FLAG_ENCODEを設定。
エンコーダ入力用Surfaceの取得
コーデック側の対応はcodec.createInputSurface()でSurfaceを取得しておくことのみ。
// エンコーダにフレームを渡すのに使うSurfaceを取得
// configureとstartの間で呼ぶ必要あり
surface = codec.createInputSurface()
エンコーダの開始
設定とコールバックの実装が終わったらエンコーダの開始。
引数もなしにstart,のみ。
この時点ではコーデックになにも入力されていないため出力無し。
codec.start()
仮想ディスプレイの出力先を↑のSurfaceに指定
入力はSurafceを使うのでMediaProjectionを使ってAndroidの画面をキャプチャし続ける。
その辺りはAndroidの画面をPCにミラーリングするソフトを作る1を参考
エンコーダが出力完了する度にコールバック関数が呼ばれ、
フレームごとにBluetoothで送信
↑この2つはコールバック関数の設定のところに記載。
Bluetooth部分についてはBluetoothChatのコードや解説記事などがたくさんあるのでそちらを参照。
受信側
デコーダの作成
エンコーダの作成をほとんど同じです。
create En coderByTypeとcreate De coderByTypeの違いのみ。
もちろんコーデック方法があっていないとデコードできないので送信側と合わせる。
codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
解像度、FPS、出力先などの指定
ここもエンコーダとほとんど同じ。
デコーダは送られてくるデータを処理するだけなのでIフレームの設定など、エンコーダに比べて設定箇所は減る。
// この2つはエンコーダのみ必要
// format.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
// format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
コールバック関数の設定
デコーダ側は 入力→Bluetoothでフレーム受信、出力→Surfaceへ出力
onInputBufferAvailableとonOutputBufferAvailableを実装。
override fun onInputBufferAvailable(mc:MediaCodec, inputBufferId:Int) {
codecBufId = inputBufferId
codecInputBuffer = codec.getInputBuffer(inputBufferId)!!
isCodecBufAvailabile = true
}
// 出力バッファ使用可能時(フレーム作成完了時)
override fun onOutputBufferAvailable(
mc: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
codec.releaseOutputBuffer(index, false)
return
}
// 第2引数がTrueならSurfaceに出力
codec.releaseOutputBuffer(index, true)
}
入力処理は
- 入力バッファが使用可能になる(onInputBufferAvailableが呼ばれる)
- codecInputBuffer = codec.getInputBuffer()でバッファを取得
- codecInputBuffer.put(ByteArray, offset, size)でバッファにデータを格納
- codec.queueInputBuffer()でデコーダにデータをキューイング
今回は2まではonInputBufferAvailableで処理し、3,4はBluetoothの受信処理の方で行っている。
出力処理は
codec.releaseOutputBuffer(index, true)
のtrue,falseが肝。
infoをみてコンフィグデータだった場合は、falseを指定してSurfaceへ出力しないようにしている。
残りのフレームデータの場合はtrueを指定。
キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い【GOP】
エンコーダに設定値反映
デコーダの場合は第二引数にSurfaceを出力先として指定、第四引数はエンコーダの時のみ指定なので0で良い。
codec.configure(format, surface, null, 0)
デコーダの開始
ここはエンコーダと同じ
codec.start()
デコーダへ入力
コールバック関数の設定でバッファを取得後、フレームデータを入力。
/*
受信処理
・受信完了時、recvStateをWAIT_INPUTBUFFER_AVAILABLE
・ダブルバッファで2フレーム分保持できるようにしておく
受信用:readBuf,確定した1フレーム分:queuingBuffer
*/
// 入力バッファ使用許可 + 入力フレーム格納済みの場合
if(isCodecBufAvailabile && recvState == WAIT_INPUTBUFFER_AVAILABLE) {
// フラグOff デコーダのCallbackによってOnされる
isCodecBufAvailabile = false
val bufDataSize = queuingBuffer.position()
// 1フレーム分をデコーダのバッファにコピー
codecInputBuffer.put(queuingBuffer.array(), 0, bufDataSize)
codec.queueInputBuffer(
codecBufId, 0,
bufDataSize, 0, 0
)
// デコーダのバッファにコピーしたので古いフレームはクリアしておく
codecInputBuffer.clear()
queuingBuffer.clear()
}
Surfaceが更新される
コールバックのところに書いたようにフレームデータがデコードされ、出力準備が完了すると
onOutputBufferAvailableが呼ばれる。
releaseOutputBufferで第二引数をtrueにしてSurfaceを更新させる。
結果
480x360@30fps
Iフレームは1枚/秒
で設定したもの。
Bluetoothでミラーリング pic.twitter.com/zQqscysH7L
— February 14, 2020
つぎ
次は受信側のタッチイベントを送信してなんちゃって遠隔操作をしたいと思います。