Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

yu mitsuhori
October 20, 2021

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

DroidKaigi2021での発表資料です

yu mitsuhori

October 20, 2021
Tweet

More Decks by yu mitsuhori

Other Decks in Technology

Transcript

  1. 本発表で話さないこと
 - iOS、サーバーサイドの実装方法について 
 - 音声配信アプリの受信クライアントの実装方法(配信音声を聞く側) 
 - 使用ライブラリの優劣については議論しません 


    - サンプルで使用する例が必ずしもベストプラクティスではないです。あくまで一例という前提でご覧 いただければと思います 
 - 実装例はstand.fmと全く同じものではありません 
 - あくまで簡易的なサンプルになります 
 5
  2. アジェンダ - ReactNative、NativeModuleとは - ReactNativeでのKotlinの導入方法 - ReactNativeとKotlinを用いた音声収録・配信機能の実装 - 収録 -

    配信(RTMP) - コラボ機能の紹介(WebRTC)※実装は割愛 - ReactNativeの音声ライブラリ紹介 - まとめ 6
  3. ReactNativeについて
 - Facebookが実装したモバイルアプリケーションフレームワーク 
 - クロスプラットフォーム開発の一つとして上げられる 
 - Reactのエコシステムを生かした開発ができ、近年流行りの宣言的UIパターンやライブラリなど の恩恵を受けれる


    - JavaScriptCoreやHermesなどのJavaScriptエンジンをベースとして動作し、JavaScript、 TypeScriptが利用可能
 - レンダリングにはネイティブのコンポーネントが使われる 
 - TextViewやFrameLayoutなど 
 8
  4. ReactNativeの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 
 9
  5. AndroidのNativeModuleを実装する準備 
 1. AndroidStudioをインストール
 a. https://developer.android.com/studio 
 2. react-native init

    コマンドで
 ReactNativeプロジェクトを作成
 3. AndroidStudioでReactNativeプロジェクトの 
 androidディレクトリを開く(→のようになる) 
 11 ↑ReactNativeプロジェクトをAndroidStudioで開く

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

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

  7. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 a. ToastModuleクラスを作る 
 15 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 
 を継承

  8. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 b. Packageクラスを作る 
 16 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インスタンスを初期化し、追加 

  9. NativeModuleの例(Java側)〜Androidのトースト表示〜 
 c. MainApplication.javaにToastPackageのインスタンスを追加する 
 17 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インスタンスを初期化し、追加 

  10. NativeModuleの例(JS側)〜Androidのトースト表示〜 
 a. NativeModuleのインターフェースを定義(toast.js) 
 18 // @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のメソッドを呼び出す 

  11. 19 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を定義 

  12. ReactNativeでKotlinを使う方法
 1. AndroidStudioでKotlinのプラグインをインストール 
 a. Preferences > Plugins > 「Kotlin」を検索し、Installをする

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

    dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // add } } 24 build.gradleでKotlinの依存を含める 
 build.gradle
 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 } SyncNowを押下し、依存モジュールをインストール 
 app/build.gradle

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


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

  15. 変換前:ToastModule.java(例)
 26 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(例)
 27 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() } } 1. 大幅にコード量を削減
 2. 冗長性のあるコードを削減
 3. 安全性のあるコードが書ける

  17. 35 音声の録音機能の実装〜システムの録音権限の要求〜 
 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モジュールでユーザーにマイク権限をリクエスト 
 このような権限要求ダイアログが表示される 

  18. 音声の録音機能の実装〜音声ファイルの準備〜 
 36 - 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" } } アプリ内領域のストレージの絶対パスを取得 

  19. 音声の録音機能の実装〜録音開始〜 
 37 - 音声の録音には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 
 録音の開始
 以下の項目を設定 

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

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

  21. PromiseとAndroidのNativeModule 
 39 - 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の結果を渡す

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

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

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

    
 - 対応音声コーデック:
 - AAC、AAC-LC、MP3、Speexなど 
 - 関連フォーマット:
 - RTMPS (SSL経由で暗号化) 
 - RTMFP (TCPの代わりにUDP経由でレイヤー化) 
 - 2020年末のFlashサポート終了とともにサポートが終了しているが、メディアサーバーへの転送 プロトコルとしてはいまだ利用されているところが多い 

  24. RTMPコネクションと録音の開始〜コネクション開始〜 
 47 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クライアントのインスタンス
 RTMPの認証パラメータのセット 
 音声関連の設定(bitrate, sampleRate, isStereo) 
 マイク、エンコーダーの初期化をする 
 RTMPコネクションの開始要求 

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

    object: ConnectCheckerRtmp { override fun onConnectionFailedRtmp(reason: String) { // RTMPコネクションの接続が失敗した場合 } override fun onConnectionStartedRtmp(rtmpUrl: String) { // RTMPコネクションの接続が開始されたとき } override fun onConnectionSuccessRtmp() { // RTMPコネクションの接続が成功したとき } override fun onDisconnectRtmp() { // RTMPコネクションのSocketが切れたとき } ... }
  26. 50 ストリーミング開始の成功と失敗のハンドリング 
 - startStreamとstopStreamは非同期で実行される 
 - それらの結果を利用したい場合は、ConnectCheckerRtmpのコールバック結果を元にしてハンド リングをする
 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.OnSuccessStream) } } KotlinのNativeEventEmitterの実装 
 RTMPコネクションの失敗イベントをemit 
 RTMPコネクションの成功イベントをemit 

  27. 51 SealedClassを用いたNativeEventEmiterへの応用 
 sealed class StreamingEvent(val name: String) { object

    OnSuccessStream: StreamingEvent("onSuccessStream") class OnError(val code: StreamingErrorCode): StreamingEvent("onError") } SealedClassで
 2つのイベント型を定義
 fun sendEvent(context: ReactApplicationContext, event: StreamingEvent) { val body = event.toBodyMap() context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(event.name, body) } sendEvent(context, StreamingEvent.OnError(StreamingErrorCode.RTMP_CONNECTION_FAILED_OR_CLOSED)) sendEvent(context, StreamingEvent.OnSuccessStream) イベントタイプごとにReadableMapを生成 する拡張関数
 onErrorイベントの場合: { code: 1, message: “Failed to RTMP connection” } onSuccessStreamの場合: {}
  28. 52 音声の配信〜js側の呼び出し〜 
 import { NativeModules, NativeEventEmitter } from 'react-native'

    const { StreamingModule } = NativeModules const eventEmitter = new NativeEventEmitter(StreamingModule) StreamingModule.startStreaming() StreamingModule.stopStreaming() const onSuccessListener = eventEmitter.addListener( 'onSuccessStream', (event) => { showToast('ストリーミングの開始に成功しました ') }) onSuccessListener.remove() const onErrorListener = eventEmitter.addListener( 'onError', (event) => { showToast('ストリーミングに失敗しました ') }) onErrorListener.remove() イベントハンドラの実装
 (useEffectなどで定義)
 ストリーミングの開始と停止
 (useCallbackなどで定義)

  29. 54 WebRTCとは
 - 複数クライアント間でリアルタイムでの通信を提供する 
 - Webブラウザ、モバイルアプリでの音声、ビデオ通話などに利用される 
 - P2P通信を使ってリアルタイム通信を実現する

    
 - 実際には通信に必要な相手のIPアドレスなどをやりとりするシグナリングサーバーが必要 
 - STUN, TURNなどの、NATやファイアウォールを超える仕組みも必要 
 - WebRTCの実装には専用のSDKやソフトウェアを提供しているサービスを利用することが多い 
 - agora.io
 - Twillio
 - etc...

  30. 58 ReactNativeの音声関連ライブラリ紹介 
 - 録音
 - react-native-audio-toolkit https://github.com/react-native-audio-toolkit/react-native-audio-toolkit 
 -

    (archived) react-native-audio https://github.com/jsierles/react-native-audio 
 - 配信(RTMP)
 - 見つかりませんでした(ありましたら教えて下さい) 
 - 音声再生
 - react-native-track-player https://github.com/DoubleSymmetry/react-native-track-player
 - react-native-sound https://github.com/zmxv/react-native-sound

  31. まとめ
 - ReactNativeでAndroidのNativeModuleを書くときはKotlinで 🐤
 - ReactNativeでも音声録音・配信アプリは作れる 
 - 音声を録音し、ファイルに保存するには MediaRecorder

    を使う
 - 音声の配信には RTMP などのストリーミングプロトコルとライブラリを使う 
 - ライブラリには、 rtmp-rtsp-stream-client-java が使える
 - コラボ機能には WebRTC を利用するが、agora.io などの開発プラットフォームを使うことが多い 
 - 音声関連のReactNativeのライブラリはいくつかあるため、基本的な機能の実装にはこれらを使うと 良い
 - 実際の配信機能は、配信クライアントだけではなくサーバー側や受信クライアント側も適切に実 装する必要があるので広範な知識が必要 
 60
  32. We are hiring! エンジニア積極的に募集中です https://corp.stand.fm/recruit 詳細はこちら • CTO候補 • VPoE候補

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