janus-gatewayとの接続手順を文章で書くとようわからんな。
janus-gatewayとの接続では、janus-gateway自体へのリクエスト/レスポンス・プラグインへのリクエスト/レスポンスとwebrtc関係のメッセージの三種類をハンドリングする必要があります。これ以降の記事ではvideoroomプラグインについて書きます。
前回の記事で書いたとおり、janus-gatewayから届くレスポンスは次の3種類があります。
- POST/GETに対する直接の応答
- long poll(transaction)
- long poll(sender)
POST/GETに対して直接返ってくるレスポンスは普通のRESTFulなAPIの場合の処理なのでいつも通りに処理してくだされ。ちなみに直接返ってくるレスポンスについてもtransactionは送る必要があるしまたレスポンスにも含まれているので、transactionで仕分けるようにプログラムしたほうがいいと思う。
まずはRetrofit2でのlong pollのAPIインターフェース定義。pathの先頭に{api}として文字を挿入するようにしています。この値のjanus-gateway側でのデフォルト値は”janus”ですが変更可能なのでサーバーの管理者に問い合わせてください。
1 2 3 4 |
@GET("{api}/{session_id}") public Call<ResponseBody> getEvent( @Path("api") final String api, @Path("session_id") final BigInteger sessionId); |
long pollから返ってくるレスポンスは何のイベントかによって中身が大きく違うので、POJOにするのは先送りにしてまずはJSON文字列のまま受け取れるように、レスポンスの型は ResponseBody にして Call<> で包んでおきます。呼び出す時はこんな感じに。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/** * long poll asynchronously */ private void longPoll() { if (DEBUG) Log.v(TAG, "longPoll:"); if (mSession == null) return; final Call<ResponseBody> call = mLongPoll.getEvent(apiName, mSession.id()); addCall(call); call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(@NonNull final Call<ResponseBody> call, @NonNull final Response<ResponseBody> response) { if (DEBUG) Log.v(TAG, "longPoll:onResponse"); removeCall(call); if ((mConnectionState == ConnectionState.READY) || (mConnectionState == ConnectionState.CONNECTED)) { try { executor.execute(() -> { handleLongPoll(call, response); }); recall(call); } catch (final Exception e) { reportError(e); } } else { Log.w(TAG, "unexpected state:" + mConnectionState); } } @Override public void onFailure(@NonNull final Call<ResponseBody> call, @NonNull final Throwable t) { if (DEBUG) Log.v(TAG, "longPoll:onFailure=" + t); removeCall(call); if (!(t instanceof IOException) || !"Canceled".equals(t.getMessage())) { reportError(t); } if (mConnectionState != ConnectionState.ERROR) { recall(call); } } private void recall(final Call<ResponseBody> call) { final Call<ResponseBody> newCall = call.clone(); addCall(newCall); newCall.enqueue(this); } }); } |
もう少しは短く書けるし、載せてないメソッドも入ってるけどな?
コールバックの中身が長くなるのは嫌いなので実際の処理は別メソッドへ丸投げします。処理が終わると再度long pollを投げます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
/** * long pollによるjanus-gatewayサーバーからの受信イベントの処理の実体 * @param call * @param response */ private void handleLongPoll(@NonNull final Call<ResponseBody> call, @NonNull final Response<ResponseBody> response) { if (DEBUG) Log.v(TAG, "handleLongPoll:"); final ResponseBody responseBody = response.body(); if (response.isSuccessful() && (responseBody != null)) { try { final JSONObject body = new JSONObject(responseBody.string()); final String transaction = body.optString("transaction"); final BigInteger sender = BigInteger.valueOf(body.optLong("sender")); if (!TextUtils.isEmpty(transaction)) { // トランザクションコールバックでの処理を試みる // WebRTCイベントはトランザクションがない if (TransactionManager.handleTransaction(transaction, body)) { return; // 処理済みの時はここで終了 } } final JanusPlugin plugin = getPlugin(sender); if (plugin != null) { if (DEBUG) Log.v(TAG, "handlePluginEvent: try handle message on plugin specified by sender"); if (plugin.onReceived("", body)) { return; } } if (DEBUG) Log.v(TAG, "handleLongPoll:unhandled transaction"); final String janus = body.optString("janus"); if (!TextUtils.isEmpty(janus)) { switch (janus) { case "ack": // do nothing return; case "keepalive": // サーバー側がタイムアウト(30秒?)した時は{"janus": "keepalive"}が来る // do nothing return; case "event": // プラグインイベント handlePluginEvent(body); break; case "media": case "webrtcup": case "slowlink": case "hangup": // event for WebRTC handleWebRTCEvent(body); break; case "error": reportError(new RuntimeException("error response " + response)); break; default: Log.d(TAG, "handleLongPoll:unknown event:" + body); break; } } } catch (final JSONException | IOException e) { reportError(e); } } } |
Retrofit2からのコールバックは Response<ResponseBody> なので、 ResponseBody を取り出して、レスポンスが正常であれば、
1 |
final JSONObject body = new JSONObject(responseBody.string()); |
とすることであっという間にJSONObjectに早変わり、便利になったもんじゃのぉ?
janus-gatewayから正常にレスポンスが返ってきた時には、トップレベルの要素として”janus”という値が必ず設定されています。”janus”要素の値は次のうちのどれかです。
- server_info
これはjanus-gatewayへサーバー情報を取り合わせたときの応答です。APIアクセスのための転送方式に対応しているか(デフォルトだとHTTP/HTTPSを使ったREST風API、大抵はWebSocketも有効になっているはず)や、インストールされて有効になっているプラグインの種類やバージョンなどを取得することができます。中身はちょっと複雑なので本家のドキュメントを参照してくださいな。 - ack
これはjanus-gatewayがPOST/GET要求を受け取って処理中である時の応答です。 - event
これはjanus-gatewayはjanus-gatewayが処理を完了をした時の通知です。 - keepalive
これはlong poll中にjanus-gateway側でタイムアウト(30秒)した時の通知です。 - error
これは呼んで字のごとくエラー通知です。 - media
ここより下はWebRTC関係の通知です。WebRTC関係の通知にはtransactionがありませんので、senderで処理を行う必要があります。 - webrtcup
- slowlink
- hangup
ということで?transactionを生成してコールバックリスナーとの対応付けをおこなうTransactionManagerクラスを作成。まずはコールバックリスナーの定義、なんの工夫もありませんキッパリ。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * callback listener when app receives transaction message */ public interface TransactionCallback { /** * usually this is called from long poll * @param transaction * @param body * @return true: handled, if return true, assignment will be removed. */ public boolean onReceived(@NonNull final String transaction, final JSONObject body); } |
TransactionManagerクラスではtransactionコードの生成と同時にコールバックリスナーとtransactionコードの対応を保持しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * hold transaction id - TransactionCallback pair(s) */ private static final Map<String, TransactionCallback> sTransactions = new HashMap<String, TransactionCallback>(); /** * get transaction and assign it to specific callback * @param length * @param callback * @return */ public static String get(int length, @Nullable final TransactionCallback callback) { final String transaction = mRandomString.get(length); if (callback != null) { synchronized (sTransactions) { sTransactions.put(transaction, callback); } } return transaction; } |
long pollでレスポンスを受け取った時にTransactionManagerのメソッドを呼び出すとtransactionコードに対応するコールバックリスナーを探して見つかれば呼び出すようにします。といっても簡単ですのじゃぁ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * call callback related to the specific transaction * @param transaction * @param body * @return true: handled */ public static boolean handleTransaction( @NonNull final String transaction, @NonNull final JSONObject body) { TransactionCallback callback = null; final boolean result; synchronized (sTransactions) { if (sTransactions.containsKey(transaction)) { callback = sTransactions.get(transaction); } result = callback != null && callback.onReceived(transaction, body); } return result; } |
ついでに前のコードでどさくさ紛れに載っているsenderの処理も載せてしまおう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@NonNull private final Map<biginteger , JanusPlugin> mAttachedPlugins = new ConcurrentHashMap</biginteger><biginteger , JanusPlugin>(); private JanusPlugin getPlugin(@Nullable final BigInteger key) { synchronized (mAttachedPlugins) { if ((key != null) && mAttachedPlugins.containsKey(key)) { return mAttachedPlugins.get(key); } } return null; } </biginteger> |
相変わらず大したことしてない。この JanusPlugin は、publisherとしてのREST風APIアクセスとsubscriberとしてのREST風APIアクセスの共通クラス。前回書いたとおり、videoroomプラグインでは、音声(+映像)を送信するpublisherが1つと、音声(+映像)を受信するsubscriberがn個の1+n個のPeerConnectionをハンドリングする必要があります。ただし、publisherとsubscriberといっても実際にアクセスするAPIに大した違いがあるわけではないので、共通クラスとしてまとめています。
とか言っているとあっという間に6000文字を超えてしまいました。コードを載せると中身は大したこと書かなくても直ぐに文字数が多くなってしまうなぁ^^;
ということで今回はこれでおしまい。
っとっとっと、記事の元になっているサンプルはGitHubにおいてあります。
↓↓↓↓↓
JanusRTCAndroid
お疲れ様でした(^.^)/~~~