Flutter Androidのソースコードを読む(FlutterApplication編)
こんにちは、tkyです。
今日はFlutter Androidのソースコードを読んで、調べた軌跡を残したいと思います。
本記事は確定的は情報はなく、tkyが右往左往しながら調べたことと感じたことをまとめたもので、仕組みを解説するような内容ではないことを予めここに明記致します。
また、リアルタイムで勉強しながら記事を書いているので誤りがある場合、随時訂正していきます。
(というか正直難しい・・・いろいろ読み進めてみて、dartと dart VMのことを知らないと理解できないのではないかと感じました。)
今回読んでいくのはこれ
flutter.jar
Android Studioにはjarのclassファイルに定義ジャンプすることができるので⌘ + クリック(winの場合は ctrl + クリック)で読めますが、 ここで読めるのはあくまでjavaで書かれた領域だけなのでネイティブコード(C/C++の領域のことを指します)も見ることを考慮して flutter/engineもcloneしておきます。
目次
- まずはAndroidManifest.xmlから
- そもそもflutter.jarはどこにあるのか?
- FlutterApplication
- FlutterMain
- startInitialization
- VM snapshotとは
- instrって何
- isolateって何
- app.soとは
- app.flxって何
- startInitialization
FlutterActivityについては別途調べて見ようと思います。
まずはAndroidManifest.xmlから
とにもかくにもこれから見るのが最初の足がかりとしては良いかと思います。
どうやらFlutterApplication
とMainActivity
によって構成されていることがわかりました。
MainActivity
はFlutterActivity
を継承しているようですね。
AndroidManifest.xml抜粋
<application android:name="io.flutter.app.FlutterApplication" android:label="kasikari_memo" android:icon="@mipmap/ic_launcher"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <meta-data android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:value="true" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application>
MainActivity.kt抜粋
class MainActivity: FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith(this) } }
ここで新しいワードが3つ出てきています。上から順番に調べていきますが、本記事ではFlutterApplication
だけフォーカスします。
- FlutterApplication
- FlutterActivity
- GeneratedPluginRegistrant
このクラス郡がflutter.jar
内に格納されている、ということですね。
そもそもflutter.jarはどこにあるのか?
調べたところ、こちらにあるようでした。android-arm
の部分はアーキテクチャによってパスが変わりそうです。
Androidの場合: /Users/${username}/flutter/bin/cache/artifacts/engine/android-arm iOSの場合: /Users/t.takenaka/flutter/bin/cache/artifacts/engine/ios
試しにJD-GUIツール(jarファイル内の.classファイルの可視化ツール)で見るとたしかに、ちゃんとFlutterActivityなどの存在が確認できました。
FlutterApplication
onCreateの処理はこの様になっていました。何かの初期化処理をしているようです。
@CallSuper public void onCreate() { super.onCreate(); FlutterMain.startInitialization(this); }
FlutterMain
ここから一気に難しくなります。startInitialization()はstaticメソッドですね。
startInitialization()
このメソッドではざっくり4つの処理をしていました。3種類の初期化メソッドと、libflutter.so
ファイルのロード処理のようです。
initConfig(applicationContext); initAot(applicationContext); initResources(applicationContext); System.loadLibrary("flutter");
この辺のinit処理で1つ1つコードを見ていくと、理解を超えそうでしたので、
initConfig()内に記述されていた、要所要所の単語を調べていく形で理解していこうと思います。
libflutter.soとは
flutter engineの以下のネイティブコードのライブラリですね。BUILD.gn
がmakefileになります。
FlutterJNI.java
がJava側の定義、
platform_view_android_jni.h
がC++側の定義です。
$ ls shell/platform/android/io/flutter/embedding/engine/ FlutterEngine.java FlutterJNI.java dart renderer $ls shell/platform/android/ AndroidManifest.xml android_surface.cc io BUILD.gn android_surface.h library_loader.cc android_context_gl.cc android_surface_gl.cc platform_message_response_android.cc android_context_gl.h android_surface_gl.h platform_message_response_android.h android_environment_gl.cc android_surface_software.cc platform_view_android.cc android_environment_gl.h android_surface_software.h platform_view_android.h android_external_texture_gl.cc android_surface_vulkan.cc platform_view_android_jni.cc android_external_texture_gl.h android_surface_vulkan.h platform_view_android_jni.h android_native_window.cc apk_asset_provider.cc vsync_waiter_android.cc android_native_window.h apk_asset_provider.h vsync_waiter_android.h android_shell_holder.cc flutter_main.cc android_shell_holder.h flutter_main.h
VM snapshotとは
wikiにちゃんと書いてあるんですね・・・すごい。VMはDart VMのことですね。
アプリの起動を高速化するため仕組みのようです。
dartコードを構文解析して得られたバイナリデータのようです。dartコードをロードしたオブジェクトが書き込まれている感じでしょうか。 そのスナップショットを実行すると短い起動時間で処理を再現できるというものみたいです。
僕らが書いたmain.dart
はapkビルド時にsnapshotになって、snapshotをDart VMに食わせているのかと推測していますが、間違っていたらスミマセン。。
github.com
以下のドキュメントも参考にしました。 https://www.cresc.co.jp/tech/java/Google_Dart/DartLanguageGuide.pdf
instrって何
instructions(命令?)
のことらしいですが、スナップショット自体と処理を別々に管理しているのでしょうか。(ヒープスナップショットと処理スナップショットとか?)
isolateって何
このことであってますかね。Flutter (dart)は基本的にはシングルスレッドですが、 isolateというライブラリを使用することで並列処理が実現できるようになるようです。
その並列の単位をisolate(アイソレート)と呼称し、dart vm上では必ず1アイソレート立ち上がる、と認識しています。 api.dartlang.org
app.soとは
libflutter.soと同じような感じで、libapp.soがapkの中にあるのかなと思いきや存在しない・・・
調べても全然出てこないので、そもそもこのapp.soが使われていない気もしてきました。 一応AOTビルドコマンドが存在します。 github.com
早速試してみましょう。
$ flutter build aot Building AOT snapshot in release mode (android-arm-release)... 14.4s Built to build/aot/. $ ls build/aot/ app.dill kernel_compile.d frontend_server.d kernel_compile.d.fingerprint gen_snapshot.d snapshot.d.fingerprint isolate_snapshot_data vm_snapshot_data isolate_snapshot_instr vm_snapshot_instr
app.soないじゃん 😇 コード検索してもそんなに出てこないし、もう使用されていないのか、根本的になにか勘違いしているのか 🤔
app.flxって何
Flutter SDK側を調べるとそれっぽいものが見つかりました。 ios側のビルドで使用するものっぽいです(flxはFiLe eXtensionの略称であっているのか?)
そもそもapp.flx自体がapkの中に入っていないですね。(app.soのときのapkのキャプチャ参照)
また、flutter build
でビルドできるコマンドを見てみるとflx Deprecated
と非推奨になっているので昔の名残なのかもしれませんね。
$ flutter build Flutter build commands. Usage: flutter build <subcommand> [arguments] -h, --help Print this usage information. Available subcommands: aot Build an ahead-of-time compiled snapshot of your app's Dart code. apk Build an Android APK file from your app. bundle Build the Flutter assets directory from your app. flx Deprecated ios Build an iOS application bundle (Mac OS X host only). Run "flutter help" to see global options.
おわりに
新年の調べもごと『Flutterってどうやって動いているのか?』を知りたくてFlutter Android側のコードを眺めてみました。 FlutterApplicationではlibflutter.soを使用するための準備とFlutterVMを起動する前の設定値やファイルの準備を行っているようでした。 調べてもそんなに出てこないし、まだまだわからないことが多くてツラミ感じていますが、次はMainActivity側のコードを読んで行こうと思います。
Flutterに入門してみて理解したところやハマったところ
前説
こんにちは、tkyです。
2018/10/08 に開催された技術書典5で購入(知人経由で・・・)したFlutterの入門本でいよいよ私もFlutter入門者になりました。 その時学習した内容とハマりポイントなどを知見として残しておこうと思います。
今回学習に際し、mBaaSとしてFirebase Cloud Firestoreを使用しています。
- Flutter
- Dart
- Firebase Cloud Firestore
なお、このときFlutter1.0がリリースしたてだったのでそれなりにハマるだろうな〜という想定はしていましたが、そこまでズボズボではなかったですw
本記事ではFlutterのインストール手順や各プラットフォームのセットアップ方法などは記載しません。
flutter doctor
しまくってエラーに記載されているコマンドをひたすら実行していったら環境整えられたので調べる手間が省けて楽でした。
何作ったの
『Flutte x firebaseで始めるモバイルアプリ開発』という本で解説している貸し借りアプリを書籍ともに作りました。
良かったところ
- hot reload機能
- Android/iOSどちらもandroid studio上で開発できたところ
iOSにはもちろんXCodeは必要ですし、エミュレータも同様ですが、デプロイがAndroid Studio上からできて、hot reloadもちゃんと効くのはまじで便利なのでは?
学習してみて
まだDartの言語仕様部分だったり、どう作る?みたいな深掘りはできていませんが、こんな感じでFlutterって動いてるんだ〜という入門程度には学習できたかと思われます。
今後はWidgetの学習とレイアウトの特訓、アーキテクチャBLoCを使ったアプリ開発あたりに触れて知識の深掘りをしていこうと思います。
今回の気付き
- Widget充実しすぎて便利
- publicとprivateの区分方法がアンダースコア(_)
- 直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい
- 画面遷移がpush,pop形式でわかりやすい
- 画面は基本的にStatefulで良さそう?
Widget充実しすぎて便利
FlutterはWidgetと呼ばれるUIオブジェクトを配置することで描画しますが、かなり種類が豊富なのでは?と感じています。
AndroidのUIっぽくMaterial Componentsが、iOSのUIっぽくCupertinoという種別のUIがありました。 書籍ではMaterial Componentsで実装し、Android, iOSどちらも同じUIとなっています。
publicとprivateの区分方法がアンダースコア(_)
Dartにはpublicやprivateのような修飾子はなく、基本publicの扱いとなります。 アンダースコア(_)を変数、関数、クラスにの先頭につけることで同じような働きができるようになります。
Dart公式ドキュメントにはアンスコつけたらそのライブラリだけで見れるようになるよ、と書いてあります。(この"ライブラリ"のスコープがわからん・・・) www.dartlang.org
start with an underscore (_) are visible only inside the library.
直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい
例えばボタンを配置するときに、以下のように記述します。
ログアウトボタンを押したとき、Topページに戻る
FlatButton( child: const Text("ログアウト"), onPressed: () { _auth.signOut(); Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false); }, ),
FlatButtonのプロパティにイベントを記述する書き方は結構好きです。 またNavigatorを使用して画面遷移を実現しています。画面遷移はルーティングで行うため、画面設計は必要そうですね。
画面遷移がpush,pop形式でわかりやすい
先程の例で説明しましたがNavigator.push or popで画面遷移を実現しています。それなりにわかりやすいかなと思いました。
// Topページに遷移する Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false); // /newというルートを作成して、InputForm画面に遷移する Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: "/new"), builder: (BuildContext context) => InputForm(null))); // 1つ前の画面に遷移する Navigator.pop(context);
画面は基本的にStatefulで良さそう?
Flutterにはスプラッシュ画面など動きがない固定画面で使用するStatelessと、
内部でデータを保持し動的に表示を行う画面などで使用するStatefulという考え方があるようです。
ん〜、基本内部で状態を保持して画面更新するパターンのほうが多い気がするのですがStafefulで良さそうな感じでしょうか?もう少し学びながら掴んでいきたいと思います。
ハマりポインツ
AndroidについてはAndroid Studioで開発できる点でほぼ何もハマることなく開発できました。
iOSがちょっとハマりました。
- iOSで
Could not build Objective-C module 'Firebase
問題 - iOSで
cloud_firestore/CloudFirestorePlugin.h' file not found
問題
iOSで Could not build Objective-C module 'Firebase
問題
最初の問題でした。エラーの内容通り、Ob-cのFirebaseモジュールがSwiftではビルドできない、と言われていますが、なぜ???という感じですね。
Ob-cのモジュールをinstallしてしまったのだろう、という感覚レベルで、対応法がぐぐったらすぐに出てきたので解決でした。
簡単に言うと、「Podfile
系でinstallしたやつ全部消してもっかいinstallし直す」です。
iOSで cloud_firestore/CloudFirestorePlugin.h' file not found
問題
こいつにずっと悩まされました。。
試行錯誤した内容は以下です。まぁ解決できませんでした。
- pod install しなおす
- flutter clean してみる
- コマンドでビルドしてみる「flutter build ios」
- flutter packages getしてみる
また、「flutter firestore」のチュートリアル等を確認して自分の手順と違うところを確認したり。 Firebase for Flutter
Podfileはこんな感じで書いてました。
target 'Runner' do use_frameworks! pod 'Firebase/Core' pod 'Firebase/Firestore' end
この問題2つの共通点
よくよくハマった2点を考えてみるとどちらもPodでした。実はFlutterをやるまでcocoapodsについては未導入だったので今回初めてインストールしたのですが、 setupコマンドを打っていないことに気が付きました。
$pod setup
本にも書いてあるので、ちゃんと本に書いてあることは読みましょう、というn度目の教訓を得ました・・・
setupしたあとにxcode起動したらエラーは消えて無事に動作確認できました。
podfileに書かれていることはまだ理解できていないので、「なぜエラーが消えたのか?」は微妙にわからないままなので要勉強です。
僕はそもそもiOSをやったことなかった
Cordovaを使ったアプリ開発の経験はあるので、XCodeでiOSプロジェクトをビルドするとかはできるのですが、 iOSネイティブをやったことなかったことで基礎的な問題の解決に少し時間がかかりました。
しかしながら、開発の効率が非常に高そうなFlutterは今後とも触れて行きたいと思います。
付録(バージョン情報など)
開発時点でのFlutter doctor内容を載せておきます。
$ flutter doctor -v [✓] Flutter (Channel beta, v1.0.0, on Mac OS X 10.13.6 17G2307, locale ja-JP) • Flutter version 1.0.0 at /Users/ticktakclock/flutter • Framework revision 5391447fae (2 weeks ago), 2018-11-29 19:41:26 -0800 • Engine revision 7375a0f414 • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297) [✓] Android toolchain - develop for Android devices (Android SDK 28.0.3) • Android SDK at /Users/ticktakclock/Library/Android/sdk • Android NDK at /Users/ticktakclock/Library/Android/sdk/ndk-bundle • Platform android-28, build-tools 28.0.3 • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06) • All Android licenses accepted. [✓] iOS toolchain - develop for iOS devices (Xcode 9.2) • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 9.2, Build version 9C40b • ios-deploy 1.9.4 • CocoaPods version 1.5.3 [✓] Android Studio (version 3.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin version 31.1.1 • Dart plugin version 181.5656 • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06) [!] VS Code (version 1.29.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension not installed; install from https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [!] Connected device ! No devices available
pod setupしたときのPodfileの内容も載せておきます。
# Uncomment this line to define a global platform for your project # platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def parse_KV_file(file, separator='=') file_abs_path = File.expand_path(file) if !File.exists? file_abs_path return []; end pods_ary = [] skip_line_start_symbols = ["#", "/"] File.foreach(file_abs_path) { |line| next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } plugin = line.split(pattern=separator) if plugin.length == 2 podname = plugin[0].strip() path = plugin[1].strip() podpath = File.expand_path("#{path}", file_abs_path) pods_ary.push({:name => podname, :path => podpath}); else puts "Invalid plugin specification: #{line}" end } return pods_ary end target 'Runner' do use_frameworks! # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock # referring to absolute paths on developers' machines. system('rm -rf .symlinks') system('mkdir -p .symlinks/plugins') # Flutter Pods generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') if generated_xcode_build_settings.empty? puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." end generated_xcode_build_settings.map { |p| if p[:name] == 'FLUTTER_FRAMEWORK_DIR' symlink = File.join('.symlinks', 'flutter') File.symlink(File.dirname(p[:path]), symlink) pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) end } # Plugin Pods plugin_pods = parse_KV_file('../.flutter-plugins') plugin_pods.map { |p| symlink = File.join('.symlinks', 'plugins', p[:name]) File.symlink(p[:path], symlink) pod p[:name], :path => File.join(symlink, 'ios') } end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' end end end
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