Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

ReactNativeとKotlinで叶える夢のリアルタイム音声配信

 ReactNativeとKotlinで叶える夢のリアルタイム音声配信

yu mitsuhori

October 02, 2021
Tweet

More Decks by yu mitsuhori

Other Decks in Technology

Transcript

  1. 発表のテーマ、ゴール感 - テーマ - ReactNative×Kotlin(Android) - 音声収録・配信アプリのクライアント実装イメージについて - ゴール感 -

    ReactNativeにおけるAndroidのNativeModuleの実装方法がわかる - Kotlinの魅力が伝わっている、導入方法がわかる - 音声収録・配信アプリの配信クライアントの実装イメージが伝わっている 音声収録・配信アプリの実装を例に上げながら ReactNative×Kotlinの魅力を伝えたい!!
 3
  2. 本発表で話さないこと
 - iOS、サーバーサイドの実装方法について 
 - 音声配信アプリの受信クライアントの実装方法(配信音声を聞く側) 
 - 使用ライブラリの優劣については議論しません 


    - サンプルで使用する例が必ずしもベストプラクティスではないです。あくまで一例という前提でご覧 いただければと思います 
 4
  3. NativeModuleとは
 - ReactNativeでネイティブのコードを実行する仕組み 
 - Android: Java, Kotlin 
 -

    iOS: Swift, Objective-C 
 - https://reactnative.dev/docs/native-modules-intro 
 引用: https://medium.com/hackernoon/first-experiences-with-react-native-bridging-an-andr oid-native-module-for-app-authentication-501fec247b2b 
 7
  4. AndroidのNativeModuleを実装する準備 
 1. AndroidStudioをインストール
 a. https://developer.android.com/studio 
 2. AndroidStudioでReactNativeプロジェクトの 


    androidディレクトリを開く(→のようになる) 
 9 ↑ReactNativeプロジェクトをAndroidStudioで開く 
 2つのJavaファイルが 生成される

  5. 11 NativeModuleの主な実装ステップ 
 1. Java側
 a. Moduleクラスを作る(JS側に公開するインターフェース) 
 b. Packageクラスを作る

    
 c. MainApplication.javaでbのpackageをわたす 
 2. JS側
 a. Javaで定義したNativeModuleのインターフェースを実装する 

  6. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 a. ToastModuleクラスを作る 
 13 public class ToastModule extends

    ReactContextBaseJavaModule { public ToastModule(ReactApplicationContext context) { super(context); } @NonNull @Override public String getName() { return "ToastModule"; } @ReactMethod public void showToast(String text) { final Toast toast = Toast.makeText(getReactApplicationContext(), text, Toast.LENGTH_LONG); toast.show(); } } @ReactMethodアノテーションをつけたメソッドがJS側から実行できる 
 JSに公開するNativeModuleの名前を定義 
 ReactContextBaseJavaModule 
 を継承

  7. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 b. Packageクラスを作る 
 14 public class ToastPackage implements

    ReactPackage { @NonNull @Override public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ToastModule(reactContext)); return modules; } @NonNull @Override public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.emptyList(); } } ReactPackageインターフェースを実装 
 ToastModuleインスタンスを初期化し、追加 

  8. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 c. MainApplication.javaにToastPackageのインスタンスを追加する 
 15 public class MainApplication extends

    Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages(); packages.add(new ToastPackage()); return packages; } }; } Packageインスタンスを初期化し、追加 

  9. NativeModuleの例(JS側)〜Androidのトースト表示〜 
 a. NativeModuleのインターフェースを定義(toast.js) 
 16 // @flow import {

    NativeModules } from 'react-native' const { ToastModule } = NativeModules const showToast = React.useCallback(() => { ToastModule.showToast('Tapped!') }, [showToast]) b. タップ時showToastメソッドをコールする→ 
 return ( <SafeAreaView> <Pressable onPress={show}> <Text>SHOW TOAST!</Text> </Pressable> </SafeAreaView> ); 先程定義したToastModuleをimport 
 ToastModuleのメソッドを呼び出す 

  10. 17 NativeModule(Native→JSにイベントを通知する場合) 
 - ネイティブ側からのEventをjs側に伝えるための手段 
 - NativeEventEmitterというものを使う 
 WritableMap

    params = Arguments.createMap(); params.putString("message", "Something is occured."); context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("SomethingEvent", params); import { NativeModules, NativeEventEmitter } from 'react-native' const eventEmitter = new NativeEventEmitter(NativeModules.SomethingModule) eventEmitter.addListener('SomethingEvent', (event) => { console.log(event.message) // output: Something is occured }) イベントパラメータの作成 
 イベント名とパラメータを指定してemit()を呼ぶ 
 ネイティブで指定したイベント名でイベントをListenする 
 指定したModuleのNativeEventEmitterを定義 

  11. 20 Kotlinの基本文法
 val immutableValue = "" immutableValue = "mutable" //

    error var mutableValue = "" mutableValue = "mutable" // ok val text1: String? = null // ok val text2: String? = "hoge" // ok val text3: String = null // error val text4: String = "hoge" // ok ・変数定義(val, var)
 ・Nullable(?), NonNull
 ・関数定義はfun
 fun something(): String { return "something" } ・クラス定義
 class Car(val name: String) interface Vehicle { fun start() } class Car(val name: String): Vehicle { override fun start() { } } ・インターフェースの継承(実装) 
 ・ラムダ(jsのアロー関数のようなイメージ) 
 val callback: (String) -> Unit = { data: String -> Log.d("Log", data) }
  12. ReactNativeでKotlinを使う方法
 1. AndroidStudioでKotlinのプラグインをインストール 
 a. Preferences > Plugins > 「Kotlin」を検索し、Installをする

    
 2. build.gradleでKotlinの依存を含める 
 a. build.gradleとapp/build.gradleの2つにKotlinを利用するための宣言を書く 
 22
  13. 23 build.gradleでKotlinの依存を含める 
 build.gradle
 buildscript { ext { kotlin_version =

    '1.4.10' // add } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // add } } apply plugin: "com.android.application" apply plugin: "kotlin-android" // add apply plugin: "kotlin-android-extensions" // add … dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // add } app/build.gradle
 SyncNowを押下し、依存モジュールをインストール 

  14. 24 既存コードをKotlinに変換する
 - AndroidStudioでは「Convert Java File to Kotlin File」 


    というコマンドがある
 - これを実行すると自動的にJavaをKotlinに変換してくれる 
 - 変換されたコードはKotlinらしい書き方になっていない場合 
 があるので気になる方は直すと良い 

  15. 変換前:ToastModule.java
 25 public class ToastModule extends ReactContextBaseJavaModule { public ToastModule(ReactApplicationContext

    context) { super(context); } @NonNull @Override public String getName() { return "ToastModule"; } @ReactMethod public void showToast(String text) { final Toast toast = Toast.makeText(getReactApplicationContext(), text, Toast.LENGTH_LONG); toast.show(); } }
  16. 変換後:ToastModule.kt
 26 class ToastModule(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { override fun

    getName(): String = "ToastModule" @ReactMethod fun showToast(text: String?) { val toast = Toast.makeText(reactApplicationContext, text, Toast.LENGTH_LONG) toast.show() } }
  17. 27 Kotlinに変換するメリット
 - とにかく記述量が少なく済む
 - 冗長性のあるコードを削減できる 
 - classはデフォルトでpublicになる 


    - 変数定義(val)はデフォルトでfinal扱いになる 
 - 安全性のあるコードが書ける
 - Nullsafetyのサポートなど 
 とにかく開発体験が良いので
 音声収録・配信機能を実装しながら見ていきましょう

  18. 34 音声の録音機能の実装〜システムの録音権限の要求〜 
 1. android/app/src/AndroidManifest.xmlにRECORD_AUDIO権限を追加 
 <manifest ... package="com.rnmatsurisampleapp"> <uses-permission

    android:name="android.permission.RECORD_AUDIO" /> </manifest> 2. ユーザーにパーミッションを明示的に要求 
 export const requestPermission = async (): Promise<boolean> => { const granted = await PermissionsAndroid.request( PermissionsAndroid.RECORD_AUDIO, { title: "録音の開始にはシステム権限の許可が必要です", message: "音声の録音に使用します。許可しますか", } ) return granted === PermissionsAndroid.RESULTS.GRANTED } RECORD_AUDIO権限の追加 
 PermissionsAndroidモジュールでユーザーにマイク権限をリクエスト 
 このような権限要求ダイアログが表示される 

  19. 音声の録音機能の実装〜音声ファイルの準備〜 
 35 - RecorderModuleを定義
 - startRecordingのReactMethodを定義する 
 - 録音した音声の出力先ファイルを作成しておく

    
 class RecorderModule(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { private var recordingFilePath: String? = null @ReactMethod fun startRecording(promise: Promise) { val fileName = System.currentTimeMillis().toString() recordingFilePath = "${this.reactApplicationContext.filesDir.absolutePath}/${fileName}.m4a" } } アプリ内領域のストレージの絶対パスを取得 

  20. 音声の録音機能の実装〜録音開始〜 
 36 - 音声の録音にはMediaRecorderというAndroidのAPIを使う 
 - https://developer.android.com/guide/topics/media/mediarecorder?hl=ja
 - 録音に使用されるサンプリングレートや出力フォーマットなどの設定をする

    
 - prepare()を呼んだあとstart()を呼ぶことで録音が開始される 
 private var recorder: MediaRecorder? = null @ReactMethod fun startRecording(promise: Promise) { recorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setOutputFile(recordingFilePath) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) try { prepare() start() } catch (e: IOException) { promise.reject(e) } } promise.resolve(null) } ・入力ソース:マイク 
 ・出力ファイルフォーマット:MP4(.m4a) 
 ・エンコーダーの指定:AAC 
 ・サンプリングレート:44.1kHz 
 録音の開始
 以下の項目を設定 

  21. 音声の録音機能の実装〜録音停止〜 
 37 - MediaRecorderのstop()とrelease()を実行し、録音を停止する
 - 録音したファイルパスをjs側に返す
 @ReactMethod fun stopRecording(promise:

    Promise) { recorder?.apply { stop() release() } recorder = null recordingFilePath = null promise.resolve(recordingFilePath) } 録音された音声ファイルのパスを返す 
 録音を停止する

  22. PromiseとAndroidのNativeModule 
 38 - Promiseの結果としてNativeModuleでの結果を返したい場合は、Promiseオブジェクトを ReactMethodの引数に渡す
 - resolveする場合はpromise.resolveを呼び、返り値を引数に渡す 
 -

    rejectする場合はpromise.rejectを呼ぶ 
 - 引数にはErrorCode, Message, Throwableを渡すことができ、js側のreject時のオブジェクトにまとめ て渡される
 @ReactMethod fun startRecording(promise: Promise) { … try { promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } try { const result = await RecorderModule.startRecording() } catch (err) { console.error(err) } Promiseの結果を渡す

  23. 音声の録音機能の実装〜js側の呼び出し〜 
 39 - 録音開始時と終了時にNativeModuleを呼び出すuseCallbackを定義 
 const onStartRecordingPressed = useCallback(async

    () => { try { await RecorderModule.startRecording() } catch (err) { // エラー処理 } }, []) const onStopRecordingPressed = useCallback(async () => { try { const filePath = await RecorderModule.stopRecording() } catch (err) { // エラー処理 } }, []) 録音の開始
 録音の停止

  24. - あるオブジェクトに対してスコープを新たに作ることで、冗長な表現を削減できる 
 val recorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)

    setOutputFile(recordingFilePath) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) } 40 Kotlin 小話1: スコープ関数〜apply〜 
 MediaRecorder recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setOutputFile(recordingFilePath); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); recorder.setAudioSamplingRate(44100); ↑Javaのrecorder. を省略できる
 apply: このスコープの中はMediaRecorderがthisに なる

  25. 41 Kotlin 小話1: 5つのスコープ関数 
 recorder?.let { it.prepare() it.start() }

    ・let(Nullableをアンラップする) 
 val recorder: MediaRecorder = MediaRecorder().apply { setOutputFile(recordingFilePath) setAudioSamplingRate(44100) } ・apply
 (繰り返しインスタンスのメソッドを呼び出す) 
 val recorder: MediaRecorder = MediaRecorder().also { Log.d("Log", it.hashCode().toString()) } ・also
 (初期化ついでにそのインスタンスを使いたい 
 val result: String = with(recorder) { this?.start() "ok" } // result: "ok" ・with(runの代替手段)
 val result: String = recorder?.run { this.start() "ok" } // result: "ok" ・run(ラムダ内で結果を返したい) 

  26. 45 RTMP(Real-Time Messaging Protocol)とは 
 - Adobe社が開発したビデオや音声をリアルタイムに転送するためのストリーミングプロトコル 
 - TCPベースのプロトコルで低遅延接続を維持できるように設計されている

    
 - 対応音声コーデック:
 - AAC、AAC-LC、MP3、Speexなど 
 - 関連フォーマット:
 - RTMPS (SSL経由で暗号化) 
 - RTMFP (TCPの代わりにUDP経由でレイヤー化) 

  27. RTMPコネクションと録音の開始〜コネクション開始〜 
 48 class StreamingModule(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { override

    fun getName(): String = "StreamingModule" private var rtmpClient: RtmpOnlyAudio? = null @ReactMethod fun startStreaming(promise: Promise) { val rtmpUrl = "rtmp://exmaple.com:1935/rnmatsurisampleapp/abcdefg" rtmpClient = RtmpOnlyAudio(connectCheckerRtmp).apply { setAuthorization("username", "password") prepareAudio(64 * 1024, 44100, false) startStream(rtmpUrl) } } } - StreamingModuleというNativeModuleを定義 
 - startStreamingメソッドを実装
 - RTMPクライアント(RtmpOnlyAudio)の初期化と認証パラメータの設定 
 - 録音音声のエンコーダのパラメータ設定 
 - startStreamを呼び出してRTMPコネクションを開始要求を発行 
 RTMPクライアントのインスタンス 
 RTMPの認証パラメータのセット 
 音声関連の設定(bitrate, sampleRate, isStereo) 
 マイク、エンコーダーの初期化をする 
 RTMPコネクションを開始 

  28. RTMPコネクションと録音の開始〜RTMPイベントのコールバック設定〜 
 - RtmpOnlyAudioの初期化にはConnectCheckerRtmpというコールバック受け取り用のオブジェクトを渡す 必要がある
 49 private val connectCheckerRtmp =

    object: ConnectCheckerRtmp { override fun onAuthErrorRtmp() { // RTMPの認証時にエラーが起きた場合 } override fun onAuthSuccessRtmp() { // RTMPの認証に成功した場合 } override fun onConnectionFailedRtmp(reason: String) { // RTMPコネクションの接続が失敗した場合 } override fun onConnectionStartedRtmp(rtmpUrl: String) { // RTMPコネクションの接続が開始されたとき } override fun onConnectionSuccessRtmp() { // RTMPコネクションの接続が成功したとき } override fun onDisconnectRtmp() { // RTMPコネクションのSocketが切れたとき } ... }
  29. 51 ストリーミング開始の成功と失敗のハンドリング 
 - startStreamとstopStreamは非同期で実行される 
 - それらの結果を利用したい場合は、ConnectCheckerRtmpのコールバック結果を元にしてハンド リングをする
 -

    今回はNativeEventEmitterで成功と失敗のコールバックをjs側に伝えるようにする 
 fun sendEvent(context: ReactApplicationContext, event: StreamingEvent) { val body = event.toBodyMap() context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(event.name, body) } private val connectCheckerRtmp = object: ConnectCheckerRtmp { override fun onConnectionFailedRtmp(reason: String) { sendEvent(context, StreamingEvent.OnError(StreamingErrorCode.RTMP_CONNECTION_FAILED_OR_CLOSED)) } override fun onConnectionSuccessRtmp() { sendEvent(context, StreamingEvent.OnStartedStream) } } KotlinのNativeEventEmitterの実装 
 RTMPコネクションの失敗イベントをemit 
 RTMPコネクションの成功イベントをemit 

  30. 52 Kotlin小話2: SealedClass
 - SealedClassを使うと、通常のenumではできないクラスインスタンスなどをenumの要素の一つの ように扱えるようになる
 sealed class StreamingEvent(val name:

    String) { object OnSuccessStream: StreamingEvent("onSuccessStream") class OnError(val code: StreamingErrorCode): StreamingEvent("onError") } ↑
 ・別のイベントだが同じ型として扱え、共通のプロパティも定義で きる
 ・onErrorにしかないプロパティも定義できる
 (型推論も効く)
 ↑whenとis演算子で比較でき、 
 eventタイプごとにEmitパラメータのス キーマを定義することができる 
 2つの型を定義

  31. 53 音声の配信〜js側の呼び出し〜 
 - startStreamingとstopStreamingを呼び出すcallbackを定義 
 - NativeのEventListenerを登録し、イベントをハンドリングする 
 import

    { NativeModules, NativeEventEmitter } from 'react-native' const { StreamingModule } = NativeModules const eventEmitter = new NativeEventEmitter(StreamingModule) const onStartStreamingPressed = useCallback(async () => { await StreamingModule.startStreaming() }, []) const onStopStreamingPressed = useCallback(async () => { await StreamingModule.stopStreaming() }, []) useEffect(() => { const listener = eventEmitter.addListener( 'onSuccessStream', (event) => { showToast('ストリーミングの開始に成功しました ') }) return () => { listener.remove() } }, [showToast]) useEffect(() => { const listener = eventEmitter.addListener( 'onError', (event) => { showToast('ストリーミングに失敗しました ') }) return () => { listener.remove() } }, [showToast])
  32. まとめ
 - ReactNativeでAndroidのNativeModuleを書くときはKotlinで 🐤
 - Androidにおける音声録音・配信アプリの実装について 
 - 音声を録音し、ファイルに保存するには MediaRecorder

    を使う
 - 音声の配信には RTMP などのストリーミングプロトコルとライブラリを使う 
 - ライブラリには、 rtmp-rtsp-stream-client-java が使える
 - 実際の音声配信アプリは、配信クライアントだけではなくサーバー側や受信クライアント側も適 切に実装する必要があるので広範な知識が必要 
 56
  33. We are hiring! エンジニア積極的に募集中です https://corp.stand.fm/recruit 詳細はこちら • CTO候補 • VPoE候補

    • クライアントエンジニア • バックエンドエンジニア • 機械学習エンジニア • 配信基盤エンジニア • QAエンジニア • エンジニアリングマネージャー • UI/UXデザイナー 積極募集しているプロダクト開発メンバー 57
  34. 58