Android SDK
概要
クイックスタート(Android View)
- アプリの概要
- Android プロジェクト の作成
- Android SDK のインストール
- パーミッションの設定
- レイアウトの設定
- Video 表示コンポーネントの配置する
- ルーム名の表示枠と参加ボタンを作成する
- SkyWay Auth Token の作成
- 機能の実装
- SkyWayContext, メンバの初期化
- UIの初期化
- パーミッションの要求
- JOINボタンのハンドラを作成する
- SkyWayContextのセットアップ
- カメラからの映像取得と UI への表示、マイクからの音声取得
- Room の作成
- Room への参加
- Stream の Publish
- Stream の Subscribe
- 動作確認
- 次のステップ
クイックスタート(JetPack Compose)
音声・映像入力ソースと LocalStream の作成方法
解放・破棄処理
Tips
既知の問題
🚀 クイックスタート(Android View)
このセクションでは、SkyWay Android SDK を利用した最小限の Android View をベースとするアプリケーションを開発する方法について掲載しています。 はじめて Android SDK を利用する方はこちらを参考に導入してください。
完成品は 公式リポジトリ にて公開しています。
尚、この記事は以下を前提に構成しています。
- Activity を用いたアプリ開発経験があること
- Kotlin の文法(関数、変数定義、呼び出し、コルーチンなど)がわかること
上記については Android Developers などを参考にしてください。
アプリの概要
このアプリには以下のシンプルな通話機能をP2Pによって実装します。
- ユーザーはルーム名を指定して入室できる
- ユーザーがパーミッションの要求を許可すれば、入室と同時に同じルームのメンバーに対してカメラ映像とマイク音声を配信する
- ユーザーは入室時から同じルームに入室しているユーザーのマイク音声とカメラ映像を視聴できる
Android プロジェクト の作成
Android Developers の公式ウェブサイトを参考に、Android プロジェクトを作成します。
https://developer.android.com/training/basics/firstapp/creating-project?hl=ja
「Language」は kotlin を選択してください。また、「Minimum SDK」は Android 6.0(API Level: 23) 以降を選択してください。
Android SDK のインストール
app/build.gradle
に以下の依存関係を追加するだけでSDKのインストールは完了です。
// 最新の SDK バージョンに差し替えてください // 最新の SDK バージョン情報は以下のリンクより、Maven Central Repository にて確認できます // https://central.sonatype.com/search?q=skyway def skywayVersion = 'x.x.x' dependencies { // ... 省略 implementation 'com.ntt.skyway:room:$skywayVersion' }
パーミッションの設定
Android SDK の動作に必要なパーミッションを app/src/main/AndroidManifest.xml
に記述します。
以下に、映像・音声の通信をする際に必要なパーミッションの例を示します。
<!-- ネットワーク接続に必要なパーミッション --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <!-- カメラ映像・マイク音声の取得に必要なパーミッション --> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
レイアウトの設定
res/layout/activity_main.xml
に表示したいコンポーネントを記述します。
Video 表示コンポーネントの配置する
"ローカルのビデオ"には自身の映像、"リモートのビデオ"には通話相手の映像を映します。
初期状態から以下のように変更します。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:id="@+id/videos" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center"> <!-- ローカルのビデオ --> <com.ntt.skyway.core.content.sink.SurfaceViewRenderer android:id="@+id/local_renderer" android:layout_width="150dp" android:layout_height="150dp"> </com.ntt.skyway.core.content.sink.SurfaceViewRenderer> <!-- リモートのビデオ --> <com.ntt.skyway.core.content.sink.SurfaceViewRenderer android:id="@+id/remote_renderer" android:layout_width="150dp" android:layout_height="150dp" android:layout_marginStart="20dp"> </com.ntt.skyway.core.content.sink.SurfaceViewRenderer> </LinearLayout> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
ルーム名の表示枠と参加ボタンを作成する
Video 表示コンポーネントの下にルーム名の表示枠と参加ボタンを追加します。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout> <LinearLayout> <!-- ローカルのビデオ(省略) --> <!-- リモートのビデオ(省略) --> </LinearLayout> <!-- ルーム名の表示枠 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="50dp" android:text=" room: " android:textAlignment="textEnd" android:textColor="#888888" android:textSize="13sp" /> <EditText android:id="@+id/roomName" android:layout_width="match_parent" android:layout_height="50dp" android:textAlignment="textStart" android:textColor="#000000" android:textSize="15sp" /> </LinearLayout> <!-- 参加ボタン --> <Button android:id="@+id/joinButton" android:layout_width="150dp" android:layout_height="70dp" android:layout_gravity="center" android:text="Join" android:textSize="15sp"/> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
SkyWay Auth Token の作成
SkyWay を利用するためには、初めに JWT(JSON Web Token) を用いて SkyWayContext
を初期化します。
認証認可について、詳しくはこちらをご覧ください。
SkyWay Auth Token は本来サーバーサイドで生成するため、Android SDK にはトークンの生成機能はございません。 ここでは JavaScript SDK で配布しているライブラリを利用し、サーバーから認可された後トークンを取得してきたとしましょう。
npm がインストールされた環境でライブラリをインストールします。
$ mkdir skyway_token && cd skyway_token $ npm i @skyway-sdk/token
次に token.js
を作成します。
$ touch token.js
ファイルを開き以下のコードをペーストします。
先ほど作成したアプリケーション ID とシークレットキーをスコープの app.id
と encode
引数にペーストしてください。
const { SkyWayAuthToken, uuidV4 } = require("@skyway-sdk/token"); const token = new SkyWayAuthToken({ jti: uuidV4(), iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, scope: { app: { id: "ここにアプリケーションIDをペーストしてください", turn: true, actions: ["read"], channels: [ { id: "*", name: "*", actions: ["write"], members: [ { id: "*", name: "*", actions: ["write"], publication: { actions: ["write"], }, subscription: { actions: ["write"], }, }, ], sfuBots: [ { actions: ["write"], forwardings: [ { actions: ["write"], }, ], }, ], }, ], }, }, }).encode("ここにシークレットキーをペーストしてください"); console.log(token);
node で token.js
を実行すると SkyWay Auth Token が生成されます。このトークンは後程使うのでコピーしてください。
$ node token.js eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxOTA1MzU5Yi0xOGY3LTRhOWMtYmU4Zi1kMTgxMTQ0OTY1MjMiLCJpYXQiOjE2NzQxOTg3MDQsImV4cCI6MTY3NDI4NTEwNCwic2NvcGUiOnsiYXBwIjp7ImlkIjoi44GT44GT44Gr44Ki44OX44Oq44Kx44O844K344On44OzSUTjgpLjg5rjg7zjgrnjg4jjgZfjgabjgY_jgaDjgZXjgYQiLCJ0dXJuIjp0cnVlLCJhY3Rpb25zIjpbInJlYWQiXSwiY2hhbm5lbHMiOlt7ImlkIjoiKiIsIm5hbWUiOiIqIiwiYWN0aW9ucyI6WyJ3cml0ZSJdLCJtZW1iZXJzIjpbeyJpZCI6IioiLCJuYW1lIjoiKiIsImFjdGlvbnMiOlsid3JpdGUiXSwicHVibGljYXRpb24iOnsiYWN0aW9ucyI6WyJ3cml0ZSJdfSwic3Vic2NyaXB0aW9uIjp7ImFjdGlvbnMiOlsid3JpdGUiXX19XSwic2Z1Qm90cyI6W3siYWN0aW9ucyI6WyJ3cml0ZSJdLCJmb3J3YXJkaW5ncyI6W3siYWN0aW9ucyI6WyJ3cml0ZSJdfV19XX1dfX19.qmLpoOjou0S5JwxAiaBvH0KaGzZqN4-0t1xq708_b3M
なお、このトークンの有効期限は生成から1日です。
機能の実装
MainActivity
クラスを編集します。
//プロジェクト作成時に決めたパッケージ名にしてください package YOUR.SKYWAY.QUICKSTART import ... //省略 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
以降は MainActivity
クラスに機能を実装します。
SkyWayContext, メンバの初期化
MainActivity
クラスから SkyWayContext.Options
を設定します。
また、後ほど必要になるメンバをまとめて宣言しておきます。
class MainActivity : AppCompatActivity() { // SkyWayContext.Optionsの設定 private val option = SkyWayContext.Options( authToken = "YOUR_TOKEN", logLevel = Logger.LogLevel.VERBOSE ) // メンバの宣言 private val scope = CoroutineScope(Dispatchers.IO) private var localRoomMember : LocalRoomMember? = null private var room : P2PRoom? = null private var localVideoStream : LocalVideoStream? = null private var localAudioStream : LocalAudioStream? = null override fun onCreate(savedInstanceState: Bundle?) { } }
UIの初期化
initUI()
関数を作成し、 onCreate()
から呼び出します。
この関数は activity_main.xml
で記述したレイアウトの初期値を設定します。
class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // UIの初期化 initUI() } // roomNameの初期値を生成する private fun initUI() { val roomName = findViewById<TextView>(R.id.roomName) roomName.text = UUID.randomUUID().toString() } }
パーミッションの要求
カメラやマイクから入力を受け取る前に、アプリケーションにはデバイスを使う権限が必要です。
onCreate()
に以下を追記して権限を要求します。
override fun onCreate(savedInstanceState: Bundle?) { // ... 省略 // UIの初期化 initUI() // 権限の要求 if (ContextCompat.checkSelfPermission( applicationContext, Manifest.permission.CAMERA ) != PermissionChecker.PERMISSION_GRANTED || ContextCompat.checkSelfPermission( applicationContext, Manifest.permission.RECORD_AUDIO ) != PermissionChecker.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO ), 0 ) } }
JOINボタンのハンドラを作成する
onCreate()
で activity_main.xml
で定義したJOINボタンの動作を設定し、joinAndPublish()
関数を作成します。
以降の項目では JoinAndPublish()
に機能を実装しています。
override fun onCreate(savedInstanceState: Bundle?) { // ... 省略 // JOINボタンの動作を設定 val btnJoinRoom = findViewById<Button>(R.id.joinButton) btnJoinRoom.setOnClickListener { joinAndPublish() } }
class MainActivity : AppCompatActivity() { // ... 省略 override fun onCreate(savedInstanceState: Bundle?) { // ... 省略 } private fun initUI() { // ... 省略 } // JOINボタンの動作を実装する関数(Roomへの入室、映像・音声の入出力) private fun joinAndPublish(){ } }
SkyWayContextのセットアップ
Android SDKの一部の機能はCoroutineでの非同期的な処理が必須となります。
事前に定義しておいた scope
メンバを用いて別スレッドでSkyWayContext
をセットアップします。
private fun joinAndPublish(){ scope.launch() { val result = SkyWayContext.setup(applicationContext, option) if (result) { Log.d("App", "Setup succeed") } } }
カメラからの映像取得と UI への表示、マイクからの音声取得
先ほどに続いて、同じスレッド内で入力を取得しUIへ反映します。
scope.launch(){ val result = SkyWayContext.setup(applicationContext, option) if (result) { Log.d("App", "Setup succeed") } // cameraリソースの取得 val device = CameraSource.getFrontCameras(applicationContext).first() // camera映像のキャプチャを開始します val cameraOption = CameraSource.CapturingOptions(800, 800) CameraSource.startCapturing(applicationContext, device, cameraOption) // 描画やpublishが可能なStreamを作成します localVideoStream = CameraSource.createStream() // SurfaceViewRenderer を取得して描画します。 runOnUiThread { val localVideoRenderer = findViewById<SurfaceViewRenderer>(R.id.local_renderer) localVideoRenderer.setup() localVideoStream!!.addRenderer(localVideoRenderer) } }
音声を取得します。AudioSource
は静的な object であることに注意してください。
scope.launch(){ // ... 省略 // 音声入力を開始します AudioSource.start() // publishが可能なStreamを作成します val localAudioStream = AudioSource.createStream() }
Room の作成
Streamの作成に続けて、入退室可能な Room
を作成します。
今回はユーザーが編集可能な roomName
要素を部屋の名前として利用します。
scope.launch(){ // ... 省略 room = P2PRoom.findOrCreate(name = findViewById<EditText>(R.id.roomName).toString()) }
Room への参加
続いて Room
に参加し、 publish
/ subscribe
が可能な LocalRoomMember
を取得します。
ユーザー名は Room
内でユニークである必要があります。今回はUUIDを用いてランダムに決定しています。
resultMessage
には Room
への参加の成否を格納し、結果を Toast
によるポップアップで通知しています。
scope.launch(){ // ... 省略 val memberInit = RoomMember.Init(name = "member_" + UUID.randomUUID()) localRoomMember = room?.join(memberInit) val resultMessage = if (localRoomMember == null) "Join failed" else "Joined room" runOnUiThread { Toast.makeText(applicationContext, resultMessage, Toast.LENGTH_SHORT) .show() } }
Stream の Publish
Room
に入室しているあるメンバーから他のメンバーへ Stream
を公開(publish
)をします。
これにより、他のメンバーは対象の Stream
を購読( subscribe
)することが可能になります。 subscribe
は次の項で実装します。
また、 Room
にハンドラを追加することで publish
時に特定の動作を行わせることが可能です。
scope.launch(){ // ... 省略 // ハンドラ room?.onStreamPublishedHandler = { // このRoom内で誰かがPublishするたびに実行される部分 } // 映像、音声のPublish localRoomMember?.publish(localVideoStream!!) localRoomMember?.publish(localAudioStream!!) }
ハンドラの登録は publish
との順序が重要です。例えば publication
が追加されたことをハンドラで検知する場合は、予め Room
にハンドラを追加する必要があります。
Stream の Subscribe
Room
に入室中のメンバーが公開( publish
)している stream を購読( subscribe
)します。
自身で publish
した stream
は subscribe
できないことに注意してください。
今回は先ほどまで編集していた scope.launch(){}
のスレッド内から購読時に subscribe()
関数を呼び出します。
subscribe()
関数は MainActivity
クラスに実装します。
class MainActivity : AppCompatActivity() { // ... 省略 private fun joinAndPublish{ scope.launch(){ // ... 省略 val resultMessage = if (localRoomMember == null) "Join failed" else "Joined room" runOnUiThread { Toast.makeText(applicationContext, resultMessage, Toast.LENGTH_SHORT) .show() } // 入室時に他のメンバーのStreamを購読する room?.publications?.forEach { if (it.publisher?.id == localRoomMember?.id) return@forEach subscribe(it) } // 誰かがStreamを公開したときに購読する room?.onStreamPublishedHandler = Any@{ Log.d("room", "onStreamPublished: ${it.id}") if (it.publisher?.id == localRoomMember?.id) { return@Any } subscribe(it) } localRoomMember?.publish(localVideoStream!!) localRoomMember?.publish(localAudioStream!!) } } // 購読(subscribe)の実装部分 private fun subscribe(publication: RoomPublication) { scope.launch { // Publicationをsubscribeします val subscription = localRoomMember?.subscribe(publication) runOnUiThread { val remoteVideoRenderer = findViewById<SurfaceViewRenderer>(R.id.remote_renderer) remoteVideoRenderer.setup() val remoteStream = subscription?.stream when (remoteStream?.contentType) { // コンポーネントへの描画 Stream.ContentType.VIDEO -> (remoteStream as RemoteVideoStream).addRenderer( remoteVideoRenderer ) else -> {} } } } } }
なお、 AudioStream
の場合は subscribe
を完了したタイミングでスピーカーから音声が流れます。
動作確認
Android Studio の Run ボタンを押下してアプリケーションを実行してください。
次のステップ
今回はメディアを publish
,subscribe
するシンプルな例でした。
その他のサンプルアプリケーションはこちらをご参照ください。
また、開発の前に開発ドキュメントもご一読ください。