Androidで動画配信アプリを作成する
こんにちはtkyです。
前回GCP上に動画配信サーバを作成しました。
今回はこの動画配信サーバにアクセスできるクライアントアプリを作成してみようと思います。
何作ったの
Androidで動画配信アプリを作成しました。言語はKotlinです。
構成は前回PCから配信していたのをAndroid端末に変更しただけで、サーバ構成は変わらずです。
コードはGitHubのRtmpClientを参照してください。
前回ストリームキーの話をしましたが、今回はandroid
固定で配信することとします。
※rtmp://xxx.xxx.xxx.xxx/live/android
というURLに固定で配信、視聴することになります。
一応、動画を送信する側を「配信」、受信する側を「視聴」と呼ぶことにして進めたいと思います。
どんな感じで作ったの
実装については後述でポイントでコードを載せます。UI関連の説明はしないのでGithub見てもらえたらと思います。
技術的な話
配信側についてはRtmpPublisherライブラリを利用させていただきました。
視聴側についてはExoPlayerのextension-rtmpという拡張がありましたのでこの2つのライブラリで実現できそうです。
それぞれAndroid配信⇒VLC視聴、OBS配信⇒Android視聴という流れで画面毎に確認して、最後にAndroid配信⇒Android視聴を確認して完成としたいと思います。
OBSはPCで利用できる配信ツールの事です。
VLCはPCで利用できる動画再生ソフトですが、リアルタイム動画配信の視聴ツールとしても利用できます。
画面的な話
コードはGitHub見てもらえたらですが、簡単に構成だけ。
一応、各画面はFragmentで構成して、1Activity(MainActivity)で作ってみます。(※後にはまることになるとはこの時は知る由もなかった・・・)
- メイン画面(MainFragment)
配信ボタン、視聴ボタンを作成します。
- 配信ボタン 配信画面に移動
- 視聴ボタン 視聴画面に移動
- 配信画面(PublisherFragment)
配信開始/停止ボタンを作成します。
- 配信開始/停止ボタン 配信停止中:文言「開始」、タップ時:配信開始する 配信中:文言「停止」、タップ時:配信停止する
- 視聴画面(PlayerFragment)
遷移したら視聴開始します。また更新ボタンを設置します。
- 更新ボタン 視聴停止⇒視聴開始する
視聴側
視聴側についてはExoPlayerを使っていきます。
gradle
gradleでexoplayerに必要なモジュールとrtmp拡張の記述を記載します。
ExoPlayerは2.6.0
を使っています。
// ExoPlayer // https://github.com/google/ExoPlayer implementation "com.google.android.exoplayer:exoplayer-core:$EXO_PLAYER_VERSION" implementation "com.google.android.exoplayer:exoplayer-ui:$EXO_PLAYER_VERSION" implementation "com.google.android.exoplayer:extension-rtmp:$EXO_PLAYER_VERSION"
(余談)バージョンについて
バージョン情報はすべてgradle.properties
に記述して各gradleでこの値を参照するように作成しています。
このようにすることで1つのバージョン変更でこれを使用しているライブラリバージョンは一気に変更できて楽ですね。gradleありがとうって感じです。
# libraries GRADLE_PLUGIN_VERSION=3.1.4 APP_COMPAT_VERSION=27.1.1 CONSTRAINT_LAYOUT_VERSION=1.1.3 TIMBER_VERSION=4.5.1 BUTTERKNIFE_VERSION=8.8.1 RTMP_PUBLISHER_VERSION=1.1.2 EXO_PLAYER_VERSION=2.6.0
Kotlinコード
Kotlinに入る前にまずはXMLから。案外コード量が少ないです。
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/fragment_player_exp" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:resize_mode="fill" app:surface_type="texture_view" app:use_controller="false" >
Fragment側でこのViewに対して操作します。ButterKnifeを使ってちょっとだけすっきりさせます。
基本的にExoPlayerを使用するときと同じように定義して、RTMPプロトコルを使用する場合はMediaSource
のインスタンスを作るときのDataSourceFactory
の指定でRtmpDataSourceFactory
を指定するだけです。
rtmp://xxx.xxx.xxx.xxx/live/stream_key url中の
live
はrtmpサーバのアプリケーション名ですね。前回の記事をご覧ください。
・・・中略・・・ @BindView(R.id.fragment_player_exp) lateinit var playerView: SimpleExoPlayerView private var player: SimpleExoPlayer? = null ・・・中略・・・ /** * 再生を開始する * */ private fun playStart() { // rtmp://xxx.xxx.xxx.xxx/live/stream_key val uri = Uri.parse("rtmp://$ipAddress/live/$streamKey") val player = ExoPlayerFactory.newSimpleInstance( DefaultRenderersFactory(context), DefaultTrackSelector(), DefaultLoadControl()) playerView.player = player // rtmpプロトコルを使用する場合はRtmpDataSourceFactory()を使用する val mediaSource = ExtractorMediaSource(uri, RtmpDataSourceFactory(), DefaultExtractorsFactory(), null, null) player.prepare(mediaSource) player.playWhenReady = true this.player = player } /** * 再生を停止する * */ private fun playStop() { player?.let { it.playWhenReady = false it.release() } }
ButterKnifeについて
ButterKnifeはButtonとかViewの定義をアノテーションを使って楽に定義できますって感じのライブラリです。 OnClickのイベントとかもアノテーションだけで定義するのでわざわざButton定義して、onClickListener定義して・・・ということをしなくてよくなるのは個人的に負荷が下がります。
視聴確認
OBSでrpmt://xxx.xxx.xxx.xxx/live/android
に配信してみます。
で、アプリ起動して待ち受けてみると・・・
でた!!細長い!!! 16:9の配信に対して、スマホの画面いっぱいに拡縮して表示しようとしているのでまぁしょうがないですね 😅
こういったところを整備していくの大変そう・・・ とはいえ、視聴側は確認できました。
配信側
配信側はRtmpPublisherを使っていきます。 本当にありがたいことに使い方等がしっかりREADMEに書かれており特に不自由なく利用できました。
usageを見ると
val publisher: Publisher = Publisher.Builder(this) .setGlView(glView) .setUrl(rtmpUrl) .setSize(Publisher.Builder.DEFAULT_WIDTH, Publisher.Builder.DEFAULT_HEIGHT) .setAudioBitrate(Publisher.Builder.DEFAULT_AUDIO_BITRATE) .setVideoBitrate(Publisher.Builder.DEFAULT_VIDEO_BITRATE) .setCameraMode(Publisher.Builder.DEFAULT_MODE) .setListener(this) .build()
ということなので画面に以下のようなGLSurfaceView
を張り付けて、インスタンスをPublisher
に設定すればよさそうです。
あとはこのpublisher
に対して、startやstop処理をしてあげます。
ライブラリではカメラプレビューの実装も入っているので本当にこれだけで実現できる仕様になっているのがうれしい限りです。
カメラ(動画)を使用するので、Manifestには以下が必要です。 で、今回パーミッションチェックの機構は実装しないので自分でパーミッション許可します・・・
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
自分でチェックしてね ✅
Kotlinコード
こちらもKotlinに入る前にまずはXMLからいきましょう。Publisherに渡すためのGLSurfaceViewを定義します。
<android.opengl.GLSurfaceView android:id="@+id/fragment_publisher_glv" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
Kotlinコードは抜粋で載せますので、詳細はRtmpClientを見てください。
/** * preparePublisher * * */ private fun preparePublisher() { // rtmp://xxx.xxx.xxx.xxx/live/stream_key val url = "rtmp://$ipAddress/live/$streamKey" // 転送不可を下げるため動画サイズは320x240とする publisher = Publisher.Builder(activity as AppCompatActivity) .setGlView(glView) .setUrl(url) .setSize(320, 240) .setAudioBitrate(Publisher.Builder.DEFAULT_AUDIO_BITRATE) .setVideoBitrate(Publisher.Builder.DEFAULT_VIDEO_BITRATE) .setCameraMode(Publisher.Builder.DEFAULT_MODE) .setListener(this) .build() }
で、実行してメイン画面から「配信ボタン」を押してみます。
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.hardware.Camera$Parameters com.takusemba.rtmppublisher.CameraClient.open()' on a null object reference at com.takusemba.rtmppublisher.RtmpPublisher.onResume(RtmpPublisher.java:100) at java.lang.reflect.Method.invoke(Native Method) at android.arch.lifecycle.ClassesInfoCache$MethodReference.invokeCallback(ClassesInfoCache.java:218) at android.arch.lifecycle.ClassesInfoCache$CallbackInfo.invokeMethodsForEvent(ClassesInfoCache.java:193) at android.arch.lifecycle.ClassesInfoCache$CallbackInfo.invokeCallbacks(ClassesInfoCache.java:184) at android.arch.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:36) at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354) at android.arch.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:180) at com.takusemba.rtmppublisher.RtmpPublisher.<init>(RtmpPublisher.java:39) at com.takusemba.rtmppublisher.Publisher$Builder.build(Publisher.java:158) ・・・中略・・・
あれーーーー落ちるんですけどーーーー!!!
ライブラリを調べてみるとLifecycleObserver
使っているようでした。さらに調べてみると、RtmpPublisher
クラスのコンストラクタの初めにactivity.getLifecycle().addObserver(this);
しているようですね。
これによりaddObserver(this)
した瞬間に@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
アノテーションが付いているメソッドが呼ばれて、まだカメラインスタンスが作成できていないので、NPEの運びとなります。
fragmentはまだonResume
していないのですが、ActivityはすでにResume状態なので、簡単に言うと1ActivityにしてFragmentでこれを実装しようとすると詰みということがわかりました 😇😇😇😇
ということでPublisherActivity
を作成し急遽2Activity構成に変更です・・・
PublisherActivity
についてはGithub参照ください。
で、ようやく配信!ちゃんと映像がでました! VLCでも確認すると、画像は非常に乱れていますが、配信出来ていることが確認できました。
もう一台あるAndroid端末で配信、別のAndroid端末で視聴をやってみても3,4秒程度の遅延(かつ画像解像度は低い・・・)で視聴することができました。
配信時のインスタンス負荷状況
一応、配信時のGCPの負荷状況載せておきます。 GCPのインスタンスについては前回記事見てください。
ネットワークの転送自体はとがったようになっているのに対して、ディスクIOはピークの後ろにも若干波がありますね。
RTMPサーバの配信データの置き場所
/usr/local/nginx/html/live/hls
を確認すると何も入っていなかったことから、一定時間たつと配信データは削除されるようですね。
この一定時間(約5分?)は誰が設定して、どう処理しているのかなどは調査していません。余裕を見て調べつつサーバサイドと仲良くなれたらよいなと思います。
まとめ
- Androidで動画配信及び視聴クライアントを作成した
- 今回ストリームキーは
android
固定にしたが、できればストリームキー生成はサーバ側に任せてみたい - 視聴側はExoPlayer使えば良いが、配信側は独自で作った方が良さそう(動画配信サービスとしてちゃんと作ろうと思った場合)
- 想像以上に画像が荒かった。動画のサイズと通信速度かな?と思いつつどこが原因か切り分けてはいないので、余裕を見て調べてみたい。
今回作成したコードはGitHubにアップしていますのでRtmpClientを参照してください。
参考文献
様々なサイトを活用させていただきました。感謝です!!
RtmpPublisher https://github.com/TakuSemba/RtmpPublisher
ExoPlayer関連 https://qiita.com/niusounds/items/cce4ff69f5911908259b