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
GCP上に動画配信サーバーを作成する
はじめましてtkyです。
かねてから動画配信の仕組みに興味があったのでいろいろと調べて 理解ついでにやったことをまとめていきます。 GCPのインスタンスの作り方は説明しません。
何作ったの
GCP上にUbuntuの仮想マシンを構築、nginxを使用して動画配信サーバを作成しました。
図としてはこんな感じです。YouTubeLiveやニコニコ生放送のように配信者1名がいて、複数名の視聴者がいるような感じのライブ配信サービスですね。
GCPで仮想マシンを作る
GCPの12か月無料のやつを使いました。 やり方はググってください!クレカ片手にLet's try!
こんなにスペックいらないかな?と思いつつ以下の構成で作成します。
- Ubuntu 18.04.1 LTS x86_64
- n1-standard2(vCPUx2 7.5GB)
- asia-northeast-a
- http,httpsトラフィックを許可する
- ローカルディスク50GB
つくったらこうなります
ssh接続できるようにする
windowsの場合
C:\Users\ticktackclock\.ssh>ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (C:\Users\ticktackclock/.ssh/id_rsa): rtmp Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in rtmp. Your public key has been saved in rtmp.pub. The key fingerprint is: SHA256:******************** ticktackclock@************** The key's randomart image is: +---[RSA 2048]----+ |*****************| |*****************| |*****************| |*****************| |*****************| |*****************| |*****************| |*****************| |*****************| +----[SHA256]-----+
こんな感じでssh接続します。-i rtmp
というのは先ほど作成した鍵情報を利用してssh接続しますよ、という意味ですね。
※IPはVMの外部IPを指定します。
ssh接続時、パスワードを求められます。(sshkey-genで鍵を作成した時に指定したパスワードです)
The authenticity of host 'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)' can't be established. ECDSA key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '***.***.***.***' (ECDSA) to the list of known hosts. Enter passphrase for key 'rtmp': Welcome to Ubuntu 18.04.1 LTS (GNU/Linux 4.15.0-1018-gcp x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sun Sep 9 12:53:03 UTC 2018 System load: 0.0 Processes: 96 Usage of /: 2.3% of 48.29GB Users logged in: 0 Memory usage: 3% IP address for ens4: ***.***.***.*** Swap usage: 0% * Read about Ubuntu updates for L1 Terminal Fault Vulnerabilities (L1TF). - https://ubu.one/L1TF * Want to make a highly secure kiosk, smart display or touchscreen? Here's a step-by-step tutorial for a rainy weekend, or a startup. - https://bit.ly/secure-kiosk Get cloud support with Ubuntu Advantage Cloud Guest: http://www.ubuntu.com/business/services/cloud 0 packages can be updated. 0 updates are security updates. The programs included with the Ubuntu system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. ticktackclock@rtmp-server:~$
nginxをインストールする
先ほど立てた仮想マシンにnginxをインストールしていきます。
nginxとは?
OSSのwebサーバーのこと。 1対多(アクセス)の同時処理が得意。 消費メモリが少ない。 動画配信するような1対多アクセスの構成ならnginxの方がよさそうということで採用。
インストール方法
方法はいくつかあるようですが、 rtmpモジュールがデフォルトのnginxに入っていない為、ソースからコンパイルの方法で自分でビルドする必要があるようです。
必要なもの
# 必要なものインストール $sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev unzip zlib1g-dev # nginxのソースコードダウンロード $wget http://nginx.org/download/nginx-1.14.0.tar.gz # rtmpモジュールをダウンロード $wget https://github.com/arut/nginx-rtmp-module/archive/master.zip # それぞれ解凍 $tar xvzf nginx-1.14.0.tar.gz $unzip master.zip # 解凍できたかlsで確認 $ls master.zip nginx-1.14.0 nginx-1.14.0.tar.gz nginx-rtmp-module-master # nginxソースコードディレクトリでビルド $cd nginx-1.14.0/ $./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master $make $sudo make install
※私の場合、zlib1g-dev
をインストールしおらず、./configureするところでエラー吐きました。zlib1g-dev
をapt-get installしてもう一度./configureすると成功しました。
configureの結果以下のようなものが出力されます。この辺にnginxが入るんだなぁと思ってもらえれば良いかと。
Configuration summary + using system PCRE library + using system OpenSSL library + using system zlib library nginx path prefix: "/usr/local/nginx" nginx binary file: "/usr/local/nginx/sbin/nginx" nginx modules path: "/usr/local/nginx/modules" nginx configuration prefix: "/usr/local/nginx/conf" nginx configuration file: "/usr/local/nginx/conf/nginx.conf" nginx pid file: "/usr/local/nginx/logs/nginx.pid" nginx error log file: "/usr/local/nginx/logs/error.log" nginx http access log file: "/usr/local/nginx/logs/access.log" nginx http client request body temporary files: "client_body_temp" nginx http proxy temporary files: "proxy_temp" nginx http fastcgi temporary files: "fastcgi_temp" nginx http uwsgi temporary files: "uwsgi_temp" nginx http scgi temporary files: "scgi_temp"
(超余談)公式nginxをリポジトリに追加してapt-getする方法
この項目は「そうなんだ~」で読み飛ばしてください。
ubuntuの初期リポジトリにあるnginxはバージョンが古いらしい(?)ので公式のリポジトリからインストールする必要があります。
調べてもらえれば一通り手順は出てくるのですが、備忘録のため記載しておきます。
# nginxサイトが配布するPGPキーを追加 $curl http://nginx.org/keys/nginx_signing.key | sudo apt-key add - # リポジトリを一覧に追加 $VCNAME=`cat /etc/lsb-release | grep DISTRIB_CODENAME | cut -d= -f2` && sudo -E sh -c "echo \"deb http://nginx.org/packages/ubuntu/ $VCNAME nginx\" >> /etc/apt/sources.list" $VCNAME=`cat /etc/lsb-release | grep DISTRIB_CODENAME | cut -d= -f2` && sudo -E sh -c "echo \"deb-src http://nginx.org/packages/ubuntu/ $VCNAME nginx\" >> /etc/apt/sources.list" # updateしてからnginxをインストール $sudo apt-get update $sudo apt-get install nginx
起動確認
余談はさておいて、nginxがinstallできたはずなので起動確認です。
# nginx起動 $sudo /usr/local/nginx/sbin/nginx # nginxが起動しているか確認 $ps aux | grep nginx root 14331 0.0 0.0 32876 820 ? Ss 13:02 0:00 nginx: master process nginx nginx 14332 0.0 0.0 37700 4388 ? S 13:02 0:00 nginx: worker process ticktackclock 15004 0.0 0.0 14856 1096 pts/0 R+ 14:53 0:00 grep --color=auto nginx
ブラウザからも見てみましょう。
/usr/local/nginx/html/
なおここで表示されているhtmlは上記のパスに入っています。 ただの静的なページを載せたいだけならこのディレクトリ直下にindex.html置けば自分のwebサイトを公開できるということですね。
GCPにrtmp通信用のport:1935を開ける
GCPのデフォルトではこのポートはファイアウォールの設定をしておらず、rtmp通信しても為はじかれてしまうのでちゃんとポート開けましょう。
ファイアウォールルールの設定
メニューより
ルールの命名規則は以下のような感じらしいですが、ガン無視してしまいました・・・
- 名前:
allow-{{port}}
- ターゲット:
allow{{port}}-server
作成できると以下のようにルールのリストに表示されます。
※名前間違えると編集では変更できず、もう一度作り直しになるの地味にめんどいです
VMインスタンスにルール適用
これだけで満足しては実はいけないんですよね。
作成したルールをVMインスタンスに適用して初めて作業完了となります。
メニューより
ネットワークタグはファイアウォールルールで設定したターゲットタグ
を指定します(今回の場合allow1935-server
)
nginxにrtmpの設定をする
$sudo vim /usr/local/nginx/conf/nginx.conf
rtmp_auto_push on; rtmp { server { listen 1935; chunk_size 4096; application live { live on; hls on; # 録画しないならoffとする record all; record_path /usr/local/nginx/html/record/hls; record_unique on; # hlsデータをどこに置くか hls_path /usr/local/nginx/html/live/hls; # hlsの分割単位 hls_fragment 5s; hls_type live; } } }
confで指定したディレクトリは事前に作成しておきましょう。
$sudo mkdir -p /usr/local/nginx/html/record/hls $sudo mkdir -p /usr/local/nginx/html/live/hls
再起動する
$sudo /usr/local/nginx/sbin/nginx -s stop $sudo /usr/local/nginx/sbin/nginx
なぜか-s reload
では再起動してくれず、上記の方法で再起動させました。
$sudo /usr/local/nginx/sbin/nginx -s reload //この方法では再起動しなかった
※ちなみにrtmpモジュールが入っていないnginxではこのエラーがでるので、ちゃんとソースコードビルドしましょう。 nginx: [emerg] unknown directive "rtmp" in /etc/nginx/conf.d/default.conf:45
配信確認
これでサーバ側の準備が整ったことになります。自分のPCから配信してみます。
ライブ配信サイトに対して使用できる無料の配信ツールです。インストールしてください。
配信サーバ単体のテストやAndroidなどのモバイル端末から配信した時の問題の切り分けなどに役立ちました。
URLとストリームキー
URLとストリームキーを設定します。
設定出来たら配信開始!!!
- [x] URL:
rtmp://xx.xx.xx.xx/live
- [x] ストリームキー:
android
URL末尾のlive
ってなんぞや
nginxのconfで設定したapplication名のことです
rtmp { ・・・ # ここで設定したliveが「rtmp://xx.xx.xx.xx/live」という配信URLになる application live { ・・・ } ・・・ }
ストリームキーってなんぞや
ライブ配信するときの固有IDとなるもの。ユーザIDをストリームキーとかにするとAさんの配信
、Bさんの配信
という考え方が作れそうですね。
rtmpアプリケーション単位で一意である必要があるので、ユーザIDでも良さそうですが、実際はサーバ側で一意のストリームキー(ハッシュとか)を生成して、配信者(クライアント側)でそれを使ってもらうという運用がよさそうです。
※実際にサービス運用でどうやってストリームキーを作成しているのか気になりますね。
VLCで配信出来ているか確認する
メニューからネットワークストリームを開く
メディア(M) > ネットワークストリームを開く(N)
ネットワークURLにはOBSで設定したURLとストリームキーをくっつけたものを指定して再生!!!
2~3秒くらいの遅延後、VLCで再生されているのが何となくわかりました。
仮想環境上の見え方
OBSで配信している間、サーバ上でどんなファイルができているのか見てみます。
$cd /usr/local/nginx/html/live/hls $ls android-0.ts android-2.ts android-1.ts s android.m3u8
5秒(nginxのconfでhls_fragment 5s;
で設定した為)に1つずつtsが追加されていくのが確認できました。
配信時のインスタンス負荷状況
折角GCP使ってモニタリングできるので確認してみましょう。
こんな感じです。1つの配信、1つの視聴だけならCPU1%ほど。
まとめ
次回はAndroidアプリからこのサーバに対して配信及び受信をしてみたいと思います!
next:Androidで動画配信アプリを作成する
参考文献
様々なサイトを活用させていただきました。感謝です!!
https://qiita.com/sparkgene/items/c3ac042f30cc5d0fe324