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のインストールは完了です。
このセクションではgradleファイルをGroovyによって記述しています。 Kotlin DSLによる記述方法はクイックスタート(JetPack Compose)をご参照ください。
// 最新の 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 にはトークンの生成機能はございません。
クイックスタートでは、Dev環境専用のAPIである SkyWayContext.setupForDev を用いて初期化するため、SkyWay Auth Tokenの作成は省略します。
認証認可について、詳しくはこちらをご覧ください。
機能の実装
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(
logLevel = Logger.LogLevel.VERBOSE
)
// メンバの宣言
private val appId = "YOUR_APP_ID"
private val secretKey = "YOUR_SECRET_KEY"
private val scope = CoroutineScope(Dispatchers.IO)
private var localRoomMember : LocalRoomMember? = null
private var room : Room? = 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 をセットアップします。
SkyWayContext.setupForDev はdev環境での利用が想定されているAPIです。 本番環境では、SecretKeyを秘匿するためSkyWayContext.setupをご利用ください。
private fun joinAndPublish(){
scope.launch() {
val result = SkyWayContext.setupForDev(applicationContext, appId, secretKey, option)
if (result) {
Log.d("App", "Setup succeed")
}
}
}カメラからの映像取得と UI への表示、マイクからの音声取得
先ほどに続いて、同じスレッド内で入力を取得しUIへ反映します。
scope.launch(){
val result = SkyWayContext.setupForDev(applicationContext, appId, secretKey, 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 = Room.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)をします。
今回は type を指定していないため、通信方式はデフォルトのP2Pになります。
これにより、他のメンバーは対象の 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 するシンプルな例でした。
その他のサンプルアプリケーションはこちらをご参照ください。
また、開発の前に開発ドキュメントもご一読ください。