🚀 クイックスタート

このセクションでは、SkyWay Android SDK を利用した最小限のアプリケーションを開発する方法について掲載しています。 はじめて Android SDK を利用する方はこちらを参考に導入してください。

完成品は 公式リポジトリ にて公開しています。

注意 このリポジトリは /app/libs 配下の .aar ファイルが含まれておらず、そのままでは動作しません。 公式リポジトリからCloneしたのち、後述の Android SDKのインストール を参照して .aar ファイルを準備してから動作確認をしてください。

尚、この記事は以下を前提に構成しています。

  • Activity を用いたアプリ開発経験があること
  • Kotlin の文法(関数、変数定義、呼び出し、コルーチンなど)がわかること

上記については Android Developers などを参考にしてください。

アプリの概要

このアプリには以下のシンプルな通話機能をP2Pによって実装します。

  1. ユーザーはルーム名を指定して入室できる
  2. ユーザーがパーミッションの要求を許可すれば、入室と同時に同じルームのメンバーに対してカメラ映像とマイク音声を配信する
  3. ユーザーは入室時から同じルームに入室しているユーザーのマイク音声とカメラ映像を視聴できる

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のインストール

aarファイルのダウンロードによるSDKのインストール方法を解説します。 他にも、Maven Packageとしてインストールすることも可能です。詳細はSkyWay Android SDKの概要の「SDKのダウンロード」をご覧ください。

GitHubから以下のファイルをダウンロードします。

  • libwebrtc.aar
  • skyway-core.aar
  • skyway-sfubot.aar
  • skyway-room.aar

ダウンロードしたファイル群はapp/libsディレクトリに配置してください。

ビルドオプションの変更

app/build.gradleに追記します。

ビルドオプション

Android Studioのver.8以降を利用する場合、Jetpack Composeを無効化するオプションを追記します。

android { ... // 省略 buildFeatures { compose false } }

依存ライブラリのインポート

dependenciesに追記します。

dependencies { // ... 省略 // skyway implementation files('libs/libwebrtc.aar') implementation files('libs/skyway-core.aar') implementation files('libs/skyway-sfubot.aar') implementation files('libs/skyway-room.aar') // WebSocket implementation "com.squareup.okhttp3:okhttp:4.10.0" implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" // gson implementation 'com.google.code.gson:gson:2.9.0' }

パーミッションの設定

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.idencode 引数にペーストしてください。

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 した streamsubscribe できないことに注意してください。

今回は先ほどまで編集していた 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 するシンプルな例でした。

その他のサンプルアプリケーションはこちらをご参照ください。

サンプルコード

また、開発の前に開発ドキュメントもご一読ください。

Android SDKの開発ドキュメント