こちらのページは旧SkyWayの情報です。新しいSkyWayに関する情報はこちら

# Android SDK

# 概要

Androidデバイス用アプリケーションにSkyWayを実装するためのSDKです。

# SDKのダウンロード

GitHubよりダウンロード

# 対応OS

Android 5.0+ (API Level 21+)

# チュートリアル

Android SDKの基本機能を利用して、1:1のシンプルなビデオ通話アプリを作成することで、Android SDKの使い方について理解を深めます。 現在サーバに接続されているユーザーの一覧を表示し、通話相手を選び、1対1のビデオ通話を開始し、終了する機能、また着信を受け付ける機能を実装していきます。

このチュートリアルで作成するアプリは、サンプルコードとして提供している1対1のビデオ通話と同じものになります。 完成したアプリを試したい場合は、ソースコードをダウンロードし、このチュートリアルのビルド手順に沿ってビルドししてください。

SkyWayでシグナリングをして、端末間がビデオ通話で繋がる ビデオ通話のスクリーンショット

# 開発前の準備

# SkyWayのAPIキー発行

ダッシュボードへログインし、以下の3つを行います。

  1. 「新しくアプリケーションを作成する」ボタンを押す

「新しくアプリケーションを作成する」ボタン

  1. 「利用可能ドメイン」に"localhost"を追加して、「アプリケーションを作成する」を押す

「アプリケーションを作成する」を押す

  1. APIキーをコピーして保存

APIキーをコピーして保存

# 開発環境の準備

このチュートリアルでは以下の環境を前提に開発を進めます。

  • Androdi Studio 3.5.2
  • 動作確認端末
    • Google Pixel 3a
  • OSバージョン
    • 9
  • 開発言語
    • Java
  • Peer認証
    • Peer認証はダッシュボードにてOFFに設定してください。

# プロジェクトの作成

チュートリアルで利用するAndroid Studioのプロジェクトは以下のgithubリポジトリからダウンロードしてください。

# SDKをプロジェクトに追加する

SDKのバイナリファイルを配置します。

  1. SDKをGitHubからダウンロード
  2. ZIPファイルを解凍後、skyway.aarを、app/libsディレクトリ直下に配置
  3. 開発用プロジェクトをAndroid Studio等のIDEで開き、ビルドツールGradle等の設定を済ませる

プロジェクトに含まれる主要ファイルの説明は以下のとおりです。

  • app/src/main/java/com.ntt.ecl.webrtc.sample_p2p_videochat/MainActivity
    • 今回のチュートリアルで主に必要なコードを追記していくコントローラー
  • app/src/main/java/com.ntt.ecl.webrtc.sample_p2p_videochat/PeerListDialogFragment
    • PeerID一覧を表示するListDialogを生成するコントローラー
    • 完成版が同梱されており、今回のチュートリアルでは触れません
  • res/**
    • リソースやレイアウトについては完成版が同梱されており、今回のチュートリアルでは触れません

# ヘッダーファイルインポート

チュートリアルでは既に記載済みですが、SDK用のimport文を追記します。

Java

//
// Import for SkyWay
//
import io.skyway.Peer.Browser.Canvas;
import io.skyway.Peer.Browser.MediaConstraints;
import io.skyway.Peer.Browser.MediaStream;
import io.skyway.Peer.Browser.Navigator;
import io.skyway.Peer.CallOption;
import io.skyway.Peer.MediaConnection;
import io.skyway.Peer.OnCallback;
import io.skyway.Peer.Peer;
import io.skyway.Peer.PeerError;
import io.skyway.Peer.PeerOption;

# マニフェストファイルへの追加

SDKの機能を利用するために、内容をマニフェストファイルに追記してください。

Java

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

# ビルドする

実機を接続しビルドします。実機での処理は途中で止まりますが、ビルドできることを確認してください。

# SkyWayサーバへの接続

# 宣言

MainActivityにプログラム中で利用する定数を追記してください。
API_KEYには先程ダッシュボードで発行したAPIキーを指定してください。
DOMAINには先程ダッシュボードで指定した利用可能ドメイン名のうち一つを指定してください。

Java

//
// Set your APIkey and Domain
//
private static final String API_KEY = "apikey";
private static final String DOMAIN = "domain";

プログラム中で利用するインスタンス変数の宣言を追記してください。

  • _peer : Peerオブジェクト
  • _localStream : 自分自身のMediaStreamオブジェクト
  • _remoteStream : 相手のMediaStreamオブジェクト
  • _mediaConnection : MediaConnectionオブジェクト

Java

//
// declaration
//
private Peer			_peer;
private MediaStream		_localStream;
private MediaStream		_remoteStream;
private MediaConnection	_mediaConnection;

private String			_strOwnId;
private boolean			_bConnected;

private Handler			_handler;

# UI関連処理

onCreateメソッドの冒頭で、メインウィンドウのタイトルを非表示に設定し、UIスレッド処理のためのHandlerを生成する処理を追記してください。

Java

//
// Windows title hidden
//
Window wnd = getWindow();
wnd.addFlags(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);

//
// Set UI handler
//
_handler = new Handler(Looper.getMainLooper());
final Activity activity = this;

# Peerオブジェクトの作成

続けて、Peerオブジェクトを作成するための処理を追記してください。
Peerオブジェクトには、PeerOptionクラスを利用し、APIキー、ドメイン名、デバッグレベルを指定してください。

Java

//
// Initialize Peer
//
PeerOption option = new PeerOption();
option.key = API_KEY;
option.domain = DOMAIN;
option.debug = Peer.DebugLevelEnum.ALL_LOGS;
_peer = new Peer(this, option);

Peerオブジェクトで指定可能なその他のオプションについては、APIリファレンスをご覧ください。

# 接続成功・失敗・切断時の処理

続けて、Peerオブジェクトに必要なイベントコールバックを追記してください。

# openイベント

SkyWayのシグナリングサーバと接続し、利用する準備が整ったら発火します。 SkyWayCのすべての処理はこのイベント発火後に利用できるようになります。
PeerIDと呼ばれるクライアント識別用のIDがシグナリングサーバで発行され、コールバックイベントで取得できます。 PeerIDはクライアントサイドで指定することもできます。
以下の処理では、PeerIDが発行されたら、その情報をUIに表示する処理を行っています。

Java

//
// Set Peer event callbacks
//

// OPEN
_peer.on(Peer.PeerEventEnum.OPEN, new OnCallback() {
  @Override
  public void onCallback(Object object) {

    // Show my ID
    _strOwnId = (String) object;
    TextView tvOwnId = (TextView) findViewById(R.id.tvOwnId);
    tvOwnId.setText(_strOwnId);
    
    }
});

# カメラ映像、マイク音声の取得

openイベントのコールバック内に、カメラ映像とマイク音声を取得するための処理を追記してください。

# 権限リクエスト(1)

カメラ、マイクにアクセスするための権限があるかどうかのチェックを行い、無ければ権限を要求します。
権限がある場合は、startLocalStreamメソッドを実行してカメラ映像とマイク音声を取得します。

Java

// Request permissions
if (ContextCompat.checkSelfPermission(activity,
    Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(activity,
    Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
  ActivityCompat.requestPermissions(activity,new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},0);
}
else {

  // Get a local MediaStream & show it
  startLocalStream();
}
# 権限リクエスト(2)

requestPermissionsメソッドで権限が取得できた場合は、startLocalStreamメソッドを実行してカメラ映像とマイク音声を取得します。

Java

//
// onRequestPermissionResult
//
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case 0: {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startLocalStream();
            }
    else {
                Toast.makeText(this,"Failed to access the camera and microphone.\nclick allow when asked for permission.", Toast.LENGTH_LONG).show();
            }
            break;
        }
    }
}
# オプション設定

MediaConstraintsクラスでカメラ映像・マイク音声取得に関するオプションを設定可能です。
ここで設定している項目の説明は以下のとおりです。

  • maxWidth: キャプチャ映像の横サイズ上限(単位:ピクセル)
  • maxHeight: キャプチャ映像の縦サイズ上限(単位:ピクセル)
  • cameraPosition: 使用するカメラの選択(ディフォルトはFRONT
    • カメラポジションは前面カメラ(FRONT)と背面カメラ(BACK)が選択可能

これ以外の項目については、APIリファレンスをご覧ください。

Java

//
// Get a local MediaStream & show it
//
void startLocalStream() {
  MediaConstraints constraints = new MediaConstraints();
  constraints.maxWidth = 960;
  constraints.maxHeight = 540;
  constraints.cameraPosition = MediaConstraints.CameraPositionEnum.FRONT;

}
# 取得と再生

Navigatorクラスの初期化を行い、getUserMediaメソッドの引数にconstraintsを指定して実行することで、自分のカメラ映像(ローカルストリーム)が取得できます。
取得したMediaStreamオブジェクトに、addVideoRendererメソッドを利用して、ビデオレンダラー(表示用のCanvasオブジェクト)を割り当てます。

Java

//
// Get a local MediaStream & show it
//
void startLocalStream() {

  // 省略

  Navigator.initialize(_peer);
  _localStream = Navigator.getUserMedia(constraints);
  Canvas canvas = (Canvas) findViewById(R.id.svLocalView);
  _localStream.addVideoRenderer(canvas,0);
}       
# errorイベント

何らかのエラーが発生した場合に発火します。エラーが発生したら、ログにその内容を表示できるようにします。

Java

// ERROR
_peer.on(Peer.PeerEventEnum.ERROR, new OnCallback() {
  @Override
  public void onCallback(Object object) {
    PeerError error = (PeerError) object;
    Log.d(TAG, "[On/Error]" + error);
  }
});
# closeイベント

Peer(相手)との接続が切れた際に発火します。チュートリアルでは特に処理は行いません。

Java

// CLOSE
_peer.on(Peer.PeerEventEnum.CLOSE, new OnCallback()	{
  @Override
  public void onCallback(Object object) {
    Log.d(TAG, "[On/Close]");
  }
});

# 発信・切断・着信処理

発信、切断、着信をするための処理を追記してください。

# 発信処理

相手のPeerIDを選択して発信します。

# 発信先のPeerIDを取得(1)

Make Callボタンをタップし未接続状態であれば、showPeerIDsメソッドを実行します。

Java

// Set GUI event listner for Button (make/hang up a call)
Button btnAction = (Button) findViewById(R.id.btnAction);
btnAction.setEnabled(true);
btnAction.setOnClickListener(new View.OnClickListener()	{
  @Override
  public void onClick(View v)	{
    v.setEnabled(false);

    if (!_bConnected) {

      // Select remote peer & make a call
      showPeerIDs();
    }
    else {

    }

    v.setEnabled(true);
  }
});
# 発信先のPeerIDを取得(2)

showPeerIDsメソッドでは、listAllPeersメソッドを利用して、接続先のPeerID一覧を取得します。 取得した一覧から自分自身のIDを削除し、PeerListDialogFragmentで一覧表示します。

Java

//
// Listing all peers
//
void showPeerIDs() {
  if ((null == _peer) || (null == _strOwnId) || (0 == _strOwnId.length())) {
    Toast.makeText(this, "Your PeerID is null or invalid.", Toast.LENGTH_SHORT).show();
    return;
  }

  // Get all IDs connected to the server
  final Context fContext = this;
  _peer.listAllPeers(new OnCallback() {
    @Override
    public void onCallback(Object object) {
      if (!(object instanceof JSONArray)) {
        return;
      }

      JSONArray peers = (JSONArray) object;
      ArrayList<String> _listPeerIds = new ArrayList<>();
      String peerId;

      // Exclude my own ID
      for (int i = 0; peers.length() > i; i++) {
        try {
          peerId = peers.getString(i);
          if (!_strOwnId.equals(peerId)) {
            _listPeerIds.add(peerId);
          }
        } catch(Exception e){
          e.printStackTrace();
        }
      }

      // Show IDs using DialogFragment
      if (0 < _listPeerIds.size()) {
        FragmentManager mgr = getFragmentManager();
        PeerListDialogFragment dialog = new PeerListDialogFragment();
        dialog.setListener(
          new PeerListDialogFragment.PeerListDialogFragmentListener() {
            @Override
            public void onItemClick(final String item) {
              _handler.post(new Runnable() {
                @Override
                public void run() {
                  onPeerSelected(item);
                }
              });
            }
          });
        dialog.setItems(_listPeerIds);
        dialog.show(mgr, "peerlist");
      }
      else{
        Toast.makeText(fContext, "PeerID list (other than your ID) is empty.", Toast.LENGTH_SHORT).show();
      }
    }
  });

}
# 発信

PeerListDialogFragmentでPeerIDが選択されたら、onPeerSelectedメソッドが呼ばれます。 相手のPeerID、自分自身のlocalStreamを引数にセットし発信します。
発信後は必要なイベントコールバックをセットします。setMediaCallbacksの中身については後ほど説明します。

Java

//
// Create a MediaConnection
//
void onPeerSelected(String strPeerId) {
  if (null == _peer) {
    return;
  }

  if (null != _mediaConnection) {
    _mediaConnection.close();
  }

  CallOption option = new CallOption();
  _mediaConnection = _peer.call(strPeerId, _localStream, option);

  if (null != _mediaConnection) {
    setMediaCallbacks();
    _bConnected = true;
  }

  updateActionButtonTitle();
}

# 切断処理

相手との接続を切断します。

# MediaConnectionの切断

actionButton(Make Callボタン)をタップし接続中であれば、MediaConnectionオブジェクトのCloseメソッドで該当するMediaConnectionを切断し、後ほど説明するcloseRemoteStreamで必要な処理を行います。

Java

// Set GUI event listner for Button (make/hang up a call)
Button btnAction = (Button) findViewById(R.id.btnAction);
btnAction.setEnabled(true);
btnAction.setOnClickListener(new View.OnClickListener()	{
  @Override
  public void onClick(View v)	{
    v.setEnabled(false);

    if (!_bConnected) {

      // 省略

    }
    else {

      // Hang up a call
      closeRemoteStream();
      _mediaConnection.close();

    }

    v.setEnabled(true);
  }
});
# MediaStreamのクローズ

MediaConnectionオブジェクトのCloseメソッドが実行された後は、removeVideoRendererメソッドを利用して該当のMediaStreamに割り当てられた、ビデオレンダラーを取り外します。

Java

//
// Close a remote MediaStream
//
void closeRemoteStream(){
  if (null == _remoteStream) {
    return;
  }

  Canvas canvas = (Canvas) findViewById(R.id.svRemoteView);
  _remoteStream.removeVideoRenderer(canvas,0);
  _remoteStream.close();
}

# 着信処理

相手から接続要求がきた場合に応答します。
相手から接続要求が来た場合はPeer.PeerEventEnum.CALLが発火します。 引数として相手との接続を管理するためのMediaConnectionオブジェクトが取得できるため、answerメソッドを実行し接続要求に応答します。
この時に、自分自身の_localStreamをセットすると、相手にカメラ映像・マイク音声を送信することができるようになります。
発信時の処理と同じくsetMediaCallbacksを実行し、イベントをセットします。中身については後ほど説明します。

Java

// CALL (Incoming call)
_peer.on(Peer.PeerEventEnum.CALL, new OnCallback() {
  @Override
  public void onCallback(Object object) {
    if (!(object instanceof MediaConnection)) {
      return;
    }

    _mediaConnection = (MediaConnection) object;
    setMediaCallbacks();
    _mediaConnection.answer(_localStream);

    _bConnected = true;
    updateActionButtonTitle();
  }
});

# MediaConnectionオブジェクトに必要なイベント

MediaConnectionオブジェクトに必要なイベントコールバックです。
MediaConnection.MediaEventEnum.STREAMは相手のカメラ映像・マイク音声を受信した際に発火します。
コールバック内では、UI上の接続ステータスのアップデート処理と、取得した相手のMediaStreamオブジェクトにaddVideoRendererメソッドを利用して、ビデオレンダラーを割り当てます。

Java

//
// Set callbacks for MediaConnection.MediaEvents
//
void setMediaCallbacks() {

  _mediaConnection.on(MediaConnection.MediaEventEnum.STREAM, new OnCallback() {
    @Override
    public void onCallback(Object object) {
      _remoteStream = (MediaStream) object;
      Canvas canvas = (Canvas) findViewById(R.id.svRemoteView);
      _remoteStream.addVideoRenderer(canvas,0);
    }
  });

}

SKW_MEDIACONNECTION_EVENT_CLOSEは相手がメディアコネクションの切断処理を実行し、実際に切断されたら発火します。
コールバック内では、必要な切断処理を実行します。closeRemoteStreamupdateActionButtonTitleの中身については後ほど説明します。

Java

//
// Set callbacks for MediaConnection.MediaEvents
//
void setMediaCallbacks() {

  // 省略

  _mediaConnection.on(MediaConnection.MediaEventEnum.CLOSE, new OnCallback()	{
    @Override
    public void onCallback(Object object) {
      closeRemoteStream();
      _bConnected = false;
      updateActionButtonTitle();
    }
  });

}

MediaConnection.MediaEventEnum.ERRORは何らかのエラーが発生した際に発火します。 エラーが発生したら、ログにその内容を表示できるようにします。

Java

//
// Set callbacks for MediaConnection.MediaEvents
//
void setMediaCallbacks() {

  // 省略

  _mediaConnection.on(MediaConnection.MediaEventEnum.ERROR, new OnCallback()	{
    @Override
    public void onCallback(Object object) {
      PeerError error = (PeerError) object;
      Log.d(TAG, "[On/MediaError]" + error);
    }
  });

}

# Activityライフサイクルに必要な処理

Activityライフサイクルに必要な処理を追記してください。

# Overrideメソッドの処理

Ovverrideされたメソッドに必要な処理を追記してください。
onDestoryメソッド内では、Peerオブジェクトを破棄するためにdestoryPeerを実行します。中身については後ほど説明します。

Java

//
// Activity Lifecycle
//
@Override
protected void onStart() {
  super.onStart();

  // Disable Sleep and Screen Lock
  Window wnd = getWindow();
  wnd.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
  wnd.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

@Override
protected void onResume() {
  super.onResume();

  // Set volume control stream type to WebRTC audio.
  setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
}

@Override
protected void onPause() {
  // Set default volume control stream type.
  setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
  super.onPause();
}

@Override
protected void onStop()	{
  // Enable Sleep and Screen Lock
  Window wnd = getWindow();
  wnd.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  wnd.clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
  super.onStop();
}

@Override
protected void onDestroy() {
  destroyPeer();
  super.onDestroy();
}

# Activity破棄時の処理

Activityが破棄されるタイミングで必要な処理を追記してください。
ここで実行されている処理の概要は以下のとおりです。

  • リモート/ローカルのメディアストリームのクローズ
  • MediaConnectionオブジェクトに関するコールバックイベントの開放(unsetMediaCallbacks)
  • Navigatorオブジェクトの初期化
  • Peerオブジェクトに関するコールバックイベントの開放(unsetPeerCallback)
  • Peerオブジェクトの破棄

unsetMediaCallbacksunsetPeerCallbackの中身については後ほど説明します。

Java

//
// Clean up objects
//
private void destroyPeer() {
  closeRemoteStream();

  if (null != _localStream) {
    Canvas canvas = (Canvas) findViewById(R.id.svLocalView);
    _localStream.removeVideoRenderer(canvas,0);
    _localStream.close();
  }

  if (null != _mediaConnection)	{
    if (_mediaConnection.isOpen()) {
      _mediaConnection.close();
    }
    unsetMediaCallbacks();
  }

  Navigator.terminate();

  if (null != _peer) {
    unsetPeerCallback(_peer);

    if (!_peer.isDestroyed()) {
      _peer.destroy();
    }

    _peer = null;
  }
}

# コールバックイベントの開放処理

MediaConnectionオブジェクト、Peerオブジェクトに関するコールバックイベントの開放処理を追記してください。

Java

//
// Unset callbacks for PeerEvents
//
void unsetPeerCallback(Peer peer) {
  if(null == _peer){
    return;
  }

  peer.on(Peer.PeerEventEnum.OPEN, null);
  peer.on(Peer.PeerEventEnum.CONNECTION, null);
  peer.on(Peer.PeerEventEnum.CALL, null);
  peer.on(Peer.PeerEventEnum.CLOSE, null);
  peer.on(Peer.PeerEventEnum.ERROR, null);
}

//
// Unset callbacks for MediaConnection.MediaEvents
//
void unsetMediaCallbacks() {
  if(null == _mediaConnection){
    return;
  }

  _mediaConnection.on(MediaConnection.MediaEventEnum.STREAM, null);
  _mediaConnection.on(MediaConnection.MediaEventEnum.CLOSE, null);
  _mediaConnection.on(MediaConnection.MediaEventEnum.ERROR, null);
}

# UIのセットアップ

UI関連の必要な処理を追記してください。
actionButtonはトグルで利用するため、接続状態に応じてラベルを張り替えます。updateActionButtonTitleメソッドの中身を追記してください。

Java

//
// Update actionButton title
//
void updateActionButtonTitle() {
  _handler.post(new Runnable() {
    @Override
    public void run() {
      Button btnAction = (Button) findViewById(R.id.btnAction);
      if (null != btnAction) {
        if (false == _bConnected) {
          btnAction.setText("Make Call");
        } else {
          btnAction.setText("Hang up");
        }
      }
    }
  });
}

# カメラの切り替え

最後にカメラの切り替え処理を追記してください。
switchCameraメソッドで、該当メディアストリームで利用しているカメラ位置をFRONT、BACKで交互に切り替えます。

Java

//
// Action for switchCameraButton
//
Button switchCameraAction = (Button)findViewById(R.id.switchCameraAction);
switchCameraAction.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v)	{
    if(null != _localStream){
      Boolean result = _localStream.switchCamera();
      if(true == result)	{
        //Success
      }
      else {
        //Failed
      }
    }

  }
});

# 動作確認

実機でビルドし動作を確認してください。listAllPeersで取得したPeerIDに対して発信し、相手とビデオ通話ができれば成功です。 実機が1台しかない場合は、JavaScript SDKで実装したWebアプリケーションとの相互接続で動作を確認することができます。

# サンプルコード

java
1対1、P2P ビデオ通話 テキストチャット
多人数、P2P ビデオ通話 テキストチャット
多人数、SFU ビデオ通話 テキストチャット

# サンプルアプリのビルド・実行手順

# 1.skyway.aar をダウンロードしプロジェクトに移動

skyway.aarGitHub's Releases からダウンロードできます。 その後、skyway.aarexamples/{project_name}/app/libs ディレクトリ直下に移動します。

# 2. プロジェクトを開く & build using Android Studio

Android Studioから {project_name} を開きます。 その後、 以下をあなたが登録したAPIキーとドメイン名に置き換えてください。

private static final String API_KEY = "yourAPIKEY";
private static final String DOMAIN = "yourDomain";