Android
ミキシングストリームのプッシュ転送およびプッシュバック業務フロー
このセクションでは、ボイスチャットルームでよくある業務フローをまとめて、全体のシナリオの実装プロセスをよりよく理解するのに役立ちます。
下図は、ルーム管理プロセスを示しており、作成、参加、退出、解散の実装が含まれています。


下の図は、ルームマスターによる座席管理プロセスを示しており、マイクオン招待、強制マイクオフ、マイクミュートの実装が含まれています。


下図は、聞き手の座席管理の流れを示しており、自分からマイクオン、自分からマイクオフ、マイクポジション移動の実装が含まれています。


アクセスの準備
ステップ1:サービスを利用する
1. まず、コンソールにログインしてアプリケーションを作成し、RTC Engineのアプリケーションを作成した後、Chatアプリケーションを作成する必要があります。


説明:
テスト環境と本番環境にそれぞれ使用するために2つのアプリケーションを作成することをお勧めします。1年間に各Tencent Cloudアカウント(UIN)には、毎月10,000分の無料時間が提供されます。
RTC Engineの月額プランは体験版(デフォルト)、軽量版、標準版、プロフェッショナル版に分かれており、さまざまな付加価値機能サービスを利用できます。詳細はバージョン機能と月額プランの説明をご参照ください。
2. アプリケーションが作成された後、アプリケーション管理-アプリケーション概要セクションでそのアプリケーションの基本情報を確認できます。その中で、後で使用するためにSDKAppIDとSDKSecretKeyを大切に保管してください。同時に、キーの漏洩はトラフィックの不正利用に繋がりますのでご注意ください。


ステップ2:SDKをインポートする
RTC Engine SDKとChat SDKはmavenCentralライブラリに公開されてます。gradleを設定することで自動的にダウンロードおよび更新を行うことができます。
1. dependenciesに適切なバージョンのSDK依存を追加してください。
dependencies {// TRTC Lite版SDK、TRTCとライブ再生の2つの機能を含みます。implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'// Chat SDKを追加、最新のバージョン番号を記入することがお勧めです。implementation 'com.tencent.imsdk:imsdk-plus:Version number'// Quicプラグインを追加する必要がある場合、次の行のコメントを解除(注意:プラグインのバージョン番号とChat SDKのバージョン番号が同じである必要がある)// implementation 'com.tencent.imsdk:timquic-plugin:Version number'}
説明:
推奨される自動ロード方式に加えて、SDKをダウンロードして手動でインポートすることもできます。詳細はRTC Engine SDK の手動統合とChat SDK の手動統合をご参照ください。
Quicプラグインは、axp-quicマルチパストランスポートプロトコルを提供し、劣悪なネットワークに対する耐性がさらに優れています。ネットワークのパケット損失率が70%に達した条件下でも、サービスを提供できます。プロフェッショナル版、プロフェッショナル版plus、およびエンタープライズ版ユーザーのみに開放されています。プロフェッショナル版、プロフェッショナル版plus、またはエンタープライズ版を購入した後にご利用いただけます。機能を正常に使用するために、端末SDKを7.7.5282以上のバージョンに更新してください。
2. defaultConfigで、Appが使用するCPUアーキテクチャを指定します。
defaultConfig {ndk {abiFilters "armeabi-v7a", "arm64-v8a"}}
説明:
RTC Engine SDK は armeabi/armeabi-v7a/arm64-v8a アーキテクチャをサポートし、さらにエミュレータ専用の x86/x86_64 アーキテクチャもサポートしています。
Chat SDK は armeabi-v7a/arm64-v8a/x86/x86_64 アーキテクチャをサポートしています。インストールパッケージのサイズを縮小するために、一部のアーキテクチャのSOファイルのみをパッケージ化することを選択できます。
ステップ3:プロジェクトの設定
1. AndroidManifest.xmlで, 権限を設定し、ボイスチャットシナリオではRTC Engine SDKおよびChat SDKに以下の権限が必要です。
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permission android:name="android.permission.BLUETOOTH" />
注意:
RTC Engine SDKには権限申請ロジックが組み込まれていないため、適切な権限と機能を自身で宣言する必要があります。一部の権限(ストレージ、録音など)は実行時に動的に申請する必要があります。
もしAndroidプロジェクトの
targetSdkVersionが31または対象デバイスがAndroid 12以上のシステムバージョンに関連している場合、Bluetooth機能を正常に使用するため、公式にはコード内でandroid.permission.BLUETOOTH_CONNECT権限を動的な申請する必要があります。詳細はBluetooth権限を参照してください。2. SDK内部でJavaのリフレクション機能を使用しているため、proguard-rules.proファイルにRTC Engine SDK関連クラスを難読化除外リストに追加する必要があります。
-keep class com.tencent.** { *; }
アクセスの流れ
ステップ1:認証クレデンシャルを生成する
UserSigは、Tencentのリアルタイム通信サービスが設計したセキュリティ保護署名であり、悪意のある攻撃者によるクラウドサービスの使用権の盗用を防ぐことを目的としています。リアルタイム・オーディオ・ビデオ(RTC Engine)とインスタントメッセージング(Chat)サービスはどちらもこのセキュリティ保護メカニズムを採用しており、RTC Engineは入室時に認証を行い、Chatはログイン時に認証を行います。
デバッグフェーズ:クライアントサンプルコードとコントロールパネル取得の2つの方法でUserSigを計算生成でき、デバッグテストのみに使用します。
本番フェーズ:クライアントがリバースエンジニアリングでキーが漏洩するのを防ぐため、より高いセキュリティレベルのサーバー側UserSig計算を推奨します。
具体的な実装は以下の通りです:
1. AppがSDKの初期化関数を呼び出す前に、最初にサーバーにUserSigをリクエストします。
2. サーバーはSDKAppIDとUserIDに基づいてUserSigを計算します。
3. サーバーは計算されたUserSigをAppに返します。
4. Appは、特定のAPIを通じてSDKにUserSigを伝達します。
5. SDKがSDKAppID + UserID + UserSigをTencent Cloudのクラウドサーバーに提出して検証します。
6. Tencent CloudはUserSigを検証し、合法性を確認します。
7. 検証が完了すると、Chat SDKにインスタントメッセージングサービスを提供し、RTC Engine SDKにリアルタイム・オーディオ・ビデオサービスを提供します。


注意:
デバッグフェーズのローカルUserSig計算方式は、オンライン環境に適用することは推奨しません。逆コンパイルによって容易に解読され、キーが漏洩する可能性があります。
複数の言語(Java/Go/PHP/Nodejs/Python/C#/C++)のUserSigサーバーサイド計算のソースコードを提供しています。詳細はサーバーサイドUserSig計算を参照してください。
ステップ2:初期化とリスニング
タイムライン図

1. Chat SDKの初期化とイベントリスナーの追加。
// イベントリスナーを追加V2TIMManager.getInstance().addIMSDKListener(imSdkListener);// Chat SDKを初期化し、このインターフェースを呼び出した後、すぐにログインインターフェースを呼び出すことができます。V2TIMManager.getInstance().initSDK(context, sdkAppID, null);// SDK初期化後にはいくつかのイベントが発生します。例えば、接続状態、ログインチケットの有効期限切れなど。private V2TIMSDKListener imSdkListener = new V2TIMSDKListener() {@Overridepublic void onConnecting() {Log.d(TAG, "Chat SDKはTencent Cloudサーバーに接続中です");}@Overridepublic void onConnectSuccess() {Log.d(TAG, "Chat SDKはTencent Cloudサーバーへの接続に成功しました");}};// イベントリスナーを削除V2TIMManager.getInstance().removeIMSDKListener(imSdkListener);// Chat SDKの初期化を解除しますV2TIMManager.getInstance().unInitSDK();
説明:
アプリケーションのライフサイクルがSDKのライフサイクルと一致している場合、アプリケーションを終了する前に初期化解除を行う必要はありません。特定の画面に入った後にのみSDKを初期化し、その画面を離れた後は使用しない場合は、SDKを初期化解除することができます。
2. RTC Engine SDK インスタンスの作成とイベントリスナーの設定。
// RTC Engine SDK インスタンスの作成(シングルトンパターン)TRTCCloud mTRTCCloud = TRTCCloud.sharedInstance(context);// イベントリスナーを設定するmTRTCCloud.setListener(trtcSdkListener);// SDKからの各種イベント通知(例:エラーコード、警告コード、オーディオ・ビデオの状態パラメータなど)private TRTCCloudListener trtcSdkListener = new TRTCCloudListener() {@Overridepublic void onError(int errCode, String errMsg, Bundle extraInfo) {Log.d(TAG, errCode + errMsg);}@Overridepublic void onWarning(int warningCode, String warningMsg, Bundle extraInfo) {Log.d(TAG, warningCode + warningMsg);}};// イベントリスナーを削除mTRTCCloud.setListener(null);// RTC Engine SDK インスタンスの破棄(シングルトンパターン)TRTCCloud.destroySharedInstance();
説明:
ステップ3:ログインとログアウト
Chat SDKを初期化した後、SDKのログインインターフェースを呼び出してアカウントの身元を確認し、アカウントの機能使用権限を取得する必要があります。そのため、他の機能を使用する前に、必ずログインが成功していることを確認してください。そうでない場合、機能異常または使用不可になる可能性があります。RTC Engineのオーディオ・ビデオサービスのみを使用する場合は、このステップを省略できます。
タイムライン図

1. ログイン。
// ログイン:userIDはカスタマイズ可能、userSigはステップ1を参照して取得します。V2TIMManager.getInstance().login(userID, userSig, new V2TIMCallback() {@Overridepublic void onSuccess() {Log.i("imsdk", "success");}@Overridepublic void onError(int code, String desc) {// 以下のエラーコードが返された場合、UserSigの使用期限が切れている。新しく発行されたUserSigを使用して再ログインします。// 1. ERR_USER_SIG_EXPIRED(6206)// 2. ERR_SVR_ACCOUNT_USERSIG_EXPIRED(70001)// 注意:他のエラーコードの場合は、ここでログインインターフェースを呼び出さないでください。Chat SDK のログインが無限ループになるのを避けるためです。Log.i("imsdk", "failure, code:" + code + ", desc:" + desc);}});
2. ログアウト。
// ログアウトV2TIMManager.getInstance().logout(new V2TIMCallback() {@Overridepublic void onSuccess() {Log.i("imsdk", "success");}@Overridepublic void onError(int code, String desc) {Log.i("imsdk", "failure, code:" + code + ", desc:" + desc);}});
説明:
アプリのライフサイクルがChat SDKのライフサイクルと一致する場合、アプリケーション終了前にログアウトする必要はありません。特定のインターフェースに入った後にのみChat SDKを使用し、インターフェース退出後は使用しない場合は、ログアウト操作と Chat SDK の初期化解除を行うことができます。
ステップ4:ルーム管理
タイムライン図

1. ルームを作成。
配信者(ルームオーナー)が配信を開始する際にルームを作成する必要があります。ここでの「ルーム」という概念は、Chatにおける「グループ」に対応します。本例ではクライアント側でChatグループを作成する方法のみを示していますが、実際にはサーバー側でグループを作成することも可能です。
V2TIMManager.getInstance().createGroup(V2TIMManager.GROUP_TYPE_AVCHATROOM, groupID, groupName, new V2TIMValueCallback<String>() {@Overridepublic void onSuccess(String s) {// グループの作成に成功}@Overridepublic void onError(int code, String desc) {// グループの作成に失敗}});// グループ作成通知をリスニングV2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupCreated(String groupID) {// グループ作成コールバック、groupIDは新しく作成されたグループのID}});
注意:
音声チャットルームシーンでChatグループを作成するには、ライブグループタイプ
GROUP_TYPE_AVCHATROOMを選択する必要があります。RTC Engineにはルームを作成するAPIはありません。ユーザーが参加しようとするルームが存在しない場合、バックグラウンドで自動的にルームが作成されます。
2. ルームに参加。
Chat グループに参加。
V2TIMManager.getInstance().joinGroup(groupID, message, new V2TIMCallback() {@Overridepublic void onSuccess() {// グループへの参加に成功}@Overridepublic void onError(int code, String desc) {// グループへの参加に失敗}});// グループ参加イベントをリスニングV2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onMemberEnter(String groupID, List<V2TIMGroupMemberInfo> memberList) {// 誰かがグループに参加しました。}});
RTC Engine ルームに参加。
private void enterRoom(String roomId, String userId) {TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();// 文字列のルーム番号を例にしています。Chatのグループ番号と一致させることをお勧めします。params.strRoomId = roomId;params.userId = userId;// 業務バックエンドから取得したUserSigparams.userSig = getUserSig(userId);// 自分のSDKAppIDに置き換えるparams.sdkAppId = SDKAppID;// ボイスチャットインタラクションシナリオでの入室には、指定されたユーザーロールが必要です。params.role = TRTCCloudDef.TRTCRoleAudience;// ボイスチャットのインタラクションでの入室シナリオを例にmTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_VOICE_CHATROOM);}// 入室結果イベントコールバック@Overridepublic void onEnterRoom(long result) {if (result > 0) {// resultは入室にかかった時間(ミリ秒)Log.d(TAG, "Enter room succeed");} else {// result入室失敗のエラーコードLog.d(TAG, "Enter room failed");}}
注意:
RTC Engineのルーム番号は整数型の
roomIdと文字列型のstrRoomIdに分かれており、2種類のルームは相互接続されません。ルーム番号のタイプを統一することをお勧めします。ボイスチャットインタラクションシナリオでは、入室時にユーザーのロール(アンカー/視聴者)を指定する必要があります。アンカーのみがプッシュ権限を持っています。指定されていない場合は、デフォルトでアンカーロールとなります。
ボイスチャットインタラクションでの入室シナリオは
TRTC_APP_SCENE_VOICE_CHATROOMがお勧めです。3. ルームから退出。
Chat グループから退室。
V2TIMManager.getInstance().quitGroup(groupID, new V2TIMCallback() {@Overridepublic void onSuccess() {// グループからの退出に成功}@Overridepublic void onError(int code, String desc) {// グループからの退出に失敗}});V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onMemberLeave(String groupID, V2TIMGroupMemberInfo member) {// グループメンバー退出コールバック}});
注意:
ライブグループ(AVChatRoom)内では、グループマスターがグループを退出することはできません。グループマスターは
dismissGroupを使用してグループを解散することのみが可能です。RTC Engine ルームから退室。
private void exitRoom() {mTRTCCloud.stopLocalAudio();mTRTCCloud.exitRoom();}// 退室イベントコールバック@Overridepublic void onExitRoom(int reason) {if (reason == 0) {Log.d(TAG, "exitRoomアクティブコールでルーム退出します");} else if (reason == 1) {Log.d(TAG, "現在のルームからサーバーによってキックされました");} else if (reason == 2) {Log.d(TAG, "現在のルームは解散されました");}}
注意:
SDKが使うすべてのリソースがリリースされた後、SDKは
onExitRoomコールバック通知をスローして知らせます。再度
enterRoomを呼び出す場合や他のオーディオ・ビデオSDKに切り替える場合は、onExitRoomのコールバックが返ってくるまで関連操作を行わないでください。そうしないと、カメラやマイクが強制的に使用されるなど、さまざまな異常が発生する可能性があります。4. ルームを解散する。
Chat グループを解散。
V2TIMManager.getInstance().dismissGroup(groupID, new V2TIMCallback() {@Overridepublic void onSuccess() {// グループ解散に成功}@Overridepublic void onError(int code, String desc) {// グループ解散に失敗}});V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupDismissed(String groupID, V2TIMGroupMemberInfo opUser) {// グループ解散コールバック}});
RTC Engine ルームを解散
サーバー側解散:RTC Engineはサーバー側でルームを解散するAPI
DismissRoom(数字ルームIDと文字列ルームIDを区別)を提供しています。このインターフェースを呼び出すことで、ルーム内の全ユーザーを退室させ、ルームを解散することができます。クライアント側解散:各クライアントのルーム退室
exitRoom インターフェースを通じて、ルーム内の全ての配信者と視聴者の退室を完了させます。RTC Engineのルームライフサイクルルールに従い、ルームは自動的に解散されます。詳細はルーム退室をご参照ください。警告:
ライブ配信が終了した後、サーバー側でAPIを呼び出してルームを確実に解散することをお勧めします。聞き手が誤って入室し、予期しない費用が発生するのを防げます。
ステップ5:座席管理
タイムライン図

まず、マイク情報を保存するためのJavaBeanを作成します。
public class SeatInfo implements Serializable {public static final transient int STATUS_UNUSED = 0;public static final transient int STATUS_USED = 1;public static final transient int STATUS_LOCKED = 2;// 座席状態、3つの状態に対応public int status;// 座席はミュート状態かpublic boolean mute;// 座席が埋まっている場合、ユーザー情報を保存public String userId;@Overridepublic String toString() {return "TXSeatInfo{"+ "status=" + status+ ", mute=" + mute+ ", user='" + userId + '\''+ '}';}}
1. 自分からマイクオン。
自分からマイクオンとは、聞き手がルームマスターや管理者にマイクオンの申請を送り、同意の信号を受け取った後にマイクオンになることを指します。常に有効の場合は、シグナルリクエスト部分を無視できます。
聞き手からのマイクオン申請。
// 聞き手がマイクオンの申請を送信、userIdはアンカーのID、dataは識別シグナルを伝えるためのjsonprivate String sendInvitation(String userId, String data) {return V2TIMManager.getSignalingManager().invite(userId, data, true, null, 0, new V2TIMCallback() {@Overridepublic void onError(int i, String s) {Log.e(TAG, "sendInvitation error " + i);}@Overridepublic void onSuccess() {Log.i(TAG, "sendInvitation success ");}});}// ホストがマイクオンの申請を受信、inviteIDはこの申請のID、inviterは申請者のIDV2TIMManager.getSignalingManager().addSignalingListener(new V2TIMSignalingListener() {@Overridepublic void onReceiveNewInvitation(String inviteID, String inviter,String groupId, List<String> inviteeList, String data) {Log.i(TAG, "received invitation: " + inviteID + " from " + inviter);}});
アンカーがマイクオンの申請を処理します。
// マイクオンの申請を承認private void acceptInvitation(String inviteID, String data) {V2TIMManager.getSignalingManager().accept(inviteID, data, new V2TIMCallback() {@Overridepublic void onError(int i, String s) {Log.e(TAG, "acceptInvitation error " + i);}@Overridepublic void onSuccess() {Log.i(TAG, "acceptInvitation success ");}});}// マイクオンの申請を拒否private void rejectInvitation(String inviteID, String data) {V2TIMManager.getSignalingManager().reject(inviteID, data, new V2TIMCallback() {@Overridepublic void onError(int i, String s) {Log.e(TAG, "rejectInvitation error " + i);}@Overridepublic void onSuccess() {Log.i(TAG, "rejectInvitation success ");}});}
聞き手がマイクオン。
アンカーが視聴者のマイクオンの申請を承認する場合、聞き手はグループ属性を変更することでマイク情報を追加でき、他のユーザーはグループ属性の変更コールバックを受け取り、ローカルのマイク情報を更新します。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// マイクオン申請承認のコールバックV2TIMManager.getSignalingManager().addSignalingListener(new V2TIMSignalingListener() {@Overridepublic void onInviteeAccepted(String inviteID, String invitee, String data) {Log.i(TAG, "received accept invitation: " + inviteID + " from " + invitee);takeSeat(seatIndex);}});// 聞き手がマイクオンprivate void takeSeat(int seatIndex) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = SeatInfo.STATUS_USED;seatInfo.mute = localInfo.mute;seatInfo.userId = mUserId;// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、マイクオン失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、TRTCのロールを切り替えてプッシュを開始mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor);mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);}});}
2. マイクオン招待。
配信者が視聴者を強制的にマイクオンさせる(視聴者の同意不要)場合、グループ属性に保存されたマイク情報を直接変更し、対応する視聴者はグループ属性変更コールバックを受信後、userIdのマッチングに成功すればRTC Engineのロールを切り替えて配信を開始できます。招待によるマイクオンのモードの場合、能動的マイクオンの実装ロジックを参照し、シグナリングの送信側と受信側を入れ替えるだけで済みます。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// アンカー側がこのインターフェースを呼び出し、グループ属性に保存されたマイク情報を変更private void pickSeat(String userId, int seatIndex) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = SeatInfo.STATUS_USED;seatInfo.mute = localInfo.mute;seatInfo.userId = userId;// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、視聴者のマイクオン招待に失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、onGroupAttributeChangedコールバックをトリガー}});}// 聞き手側がグループ属性の変更コールバックを受け取り、自身の情報と一致した後にプッシュを開始V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupAttributeChanged(String groupID, Map<String, String> groupAttributeMap) {// 最後にローカルに保存された全マイク情報リストfinal List<SeatInfo> oldSeatInfoList = mSeatInfoList;// groupAttributeMapから解析された全てのマイク情報リストfinal List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize);// 全マイク情報リストをトラバースし、新旧のマイク情報を比較for (int i = 0; i < seatSize; i++) {SeatInfo oldInfo = oldSeatInfoList.get(i);SeatInfo newInfo = newSeatInfoList.get(i);if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_USED) {if (newInfo.userId.equals(mUserId)) {// 自身情報のマッチングに成功、TRTCのロールに切り替えてプッシュを開始mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor);mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);} else {// ローカルマイクリストを更新し、ローカルマイクのビューをレンダリング}}}}});
3. 自分からマイクオフ。
マイクオン聞き手は、グループ属性を変更することでマイク情報をリセットでき、他のユーザーはグループ属性の変更コールバックを受け取り、ローカルのマイク情報を更新します。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;private void leaveSeat(int seatIndex) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = SeatInfo.STATUS_UNUSED;seatInfo.mute = localInfo.mute;seatInfo.userId = "";// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、マイクオフに失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、TRTCのロールに切り替えプッシュを停止mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);mTRTCCloud.stopLocalAudio();}});}
4. 強制マイクオフ。
配信者が視聴者を強制的にマイクオフさせる場合、グループ属性に保存されたマイク情報を直接変更し、対応する視聴者はグループ属性変更コールバックを受信後、userIdのマッチングに成功すればRTC Engineのロールを切り替えて配信を停止できます。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// アンカー側がこのインターフェースを呼び出し、グループ属性に保存されたマイク情報を変更private void kickSeat(int seatIndex) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = SeatInfo.STATUS_UNUSED;seatInfo.mute = localInfo.mute;seatInfo.userId = "";// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、強制マイクオフ失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、onGroupAttributeChangedコールバックをトリガー}});}// マイクオンの聞き手側がグループ属性の変更コールバックを受信し、自身の情報とマッチした後にプッシュを停止V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupAttributeChanged(String groupID, Map<String, String> groupAttributeMap) {// 最後にローカルに保存された全マイク情報リストfinal List<SeatInfo> oldSeatInfoList = mSeatInfoList;// groupAttributeMapから解析された全てのマイク情報リストfinal List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize);// 全マイク情報リストをトラバースし、新旧のマイク情報を比較for (int i = 0; i < seatSize; i++) {SeatInfo oldInfo = oldSeatInfoList.get(i);SeatInfo newInfo = newSeatInfoList.get(i);if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_UNUSED) {if (oldInfo.userId.equals(mUserId)) {// 自身情報のマッチングに成功、TRTCのロールに切り替えプッシュを停止mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);mTRTCCloud.stopLocalAudio();} else {// ローカルマイクリストを更新し、ローカルマイクのビューをレンダリング}}}}});
5. マイクミュート。
アンカーが特定のマイクをミュート/ミュート解除します。グループ属性に保存されているマイク情報を直接変更し、対応のマイクオン聞き手はグループ属性の変更コールバックを受け取った後、userIdがマッチすればローカルプッシュを一時停止/再開します。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// アンカー側がこのインターフェースを呼び出し、グループ属性に保存されたマイク情報を変更private void muteSeat(int seatIndex, boolean mute) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = localInfo.status;seatInfo.mute = mute;seatInfo.userId = localInfo.userId;// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、マイクミュートに失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、onGroupAttributeChangedコールバックをトリガー}});}// マイクオンの聞き手側がグループ属性の変更コールバックを受信し、自身の情報とマッチした後にプッシュを一時停止/再開V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupAttributeChanged(String groupID, Map<String, String> groupAttributeMap) {// 最後にローカルに保存された全マイク情報リストfinal List<SeatInfo> oldSeatInfoList = mSeatInfoList;// groupAttributeMapから解析された全てのマイク情報リストfinal List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize);// 全マイク情報リストをトラバースし、新旧のマイク情報を比較for (int i = 0; i < seatSize; i++) {SeatInfo oldInfo = oldSeatInfoList.get(i);SeatInfo newInfo = newSeatInfoList.get(i);if (oldInfo.mute != newInfo.mute) {if (oldInfo.userId.equals(mUserId)) {// 自身情報のマッチングに成功し、ローカルプッシュの一時停止/再開mTRTCCloud.muteLocalAudio(newInfo.mute);} else {// ローカルマイクリストを更新し、ローカルマイクのビューをレンダリング}}}}});
6. マイクロック。
アンカーが特定のマイクポジションをロック/アンロックします。グループ属性に保存されているマイク情報を直接変更し、聞き手はグループ属性の変更コールバックを受け取った後、対応するマイクのビューを更新します。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// アンカー側がこのインターフェースを呼び出し、グループ属性に保存されたマイク情報を変更private void lockSeat(int seatIndex, boolean isLock) {// マイク情報のインスタンスを作成し、変更後のマイク情報を保存SeatInfo localInfo = mSeatInfoList.get(seatIndex);SeatInfo seatInfo = new SeatInfo();seatInfo.status = isLock ? SeatInfo.STATUS_LOCKED : SeatInfo.STATUS_UNUSED;seatInfo.mute = localInfo.mute;seatInfo.userId = "";// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();String json = gson.toJson(seatInfo, SeatInfo.class);HashMap<String, String> map = new HashMap<>();map.put("seat" + seatIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、マイクロックに失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、onGroupAttributeChangedコールバックをトリガー}});}// 聞き手側がグループ属性の変更コールバックを受信し、対応するマイクのビューを更新V2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupAttributeChanged(String groupID, Map<String, String> groupAttributeMap) {// 最後にローカルに保存された全マイク情報リストfinal List<SeatInfo> oldSeatInfoList = mSeatInfoList;// groupAttributeMapから解析された全てのマイク情報リストfinal List<SeatInfo> newSeatInfoList = getSeatListFromAttr(groupAttributeMap, seatSize);// 全マイク情報リストをトラバースし、新旧のマイク情報を比較for (int i = 0; i < seatSize; i++) {SeatInfo oldInfo = oldSeatInfoList.get(i);SeatInfo newInfo = newSeatInfoList.get(i);if (oldInfo.status == SeatInfo.STATUS_LOCKED && newInfo.status == SeatInfo.STATUS_UNUSED) {// マイクのロックを解除} else if (oldInfo.status != newInfo.status && newInfo.status == SeatInfo.STATUS_LOCKED) {// マイクをロック}}}});
7. マイクポジション移動。
マイクオンアンカーがマイクポジションを移動する場合、グループ属性に保存されているソースとターゲットのマイク情報をそれぞれ変更する必要があります。聞き手はグループ属性の変更コールバックを受け取った後、対応するマイクのビューを更新します。
// ローカルに保存された全部のマイク情報リストprivate List<SeatInfo> mSeatInfoList;// マイクオンアンカーがこのインターフェースを呼び出して、グループ属性に保存されたマイク情報を変更しますprivate void moveSeat(int dstIndex) {// userIdからソースマイクポジションの番号を取得int srcIndex = -1;for (int i = 0; i < mSeatInfoList.size(); i++) {SeatInfo seatInfo = mSeatInfoList.get(i);if (seatInfo != null && mUserId.equals(seatInfo.userId)) {srcIndex = i;break;}}// マイクポジション番号に基づいて対応するマイク情報を取得SeatInfo srcSeatInfo = mSeatInfoList.get(srcIndex);SeatInfo dstSeatInfo = mSeatInfoList.get(dstIndex);// マイク情報インスタンスを作成し、変更後のソースマイク情報を保存SeatInfo srcChangeInfo = new SeatInfo();srcChangeInfo.status = SeatInfo.STATUS_UNUSED;srcChangeInfo.mute = srcSeatInfo.mute;srcChangeInfo.userId = "";// マイク情報インスタンスを作成し、変更後のターゲットマイク情報を保存SeatInfo dstChangeInfo = new SeatInfo();dstChangeInfo.status = SeatInfo.STATUS_USED;dstChangeInfo.mute = dstSeatInfo.mute;dstChangeInfo.userId = mUserId;// マイク情報オブジェクトをJSON形式にシリアライズGson gson = new Gson();HashMap<String, String> map = new HashMap<>();String json = gson.toJson(srcChangeInfo, SeatInfo.class);map.put("seat" + srcIndex, json);json = gson.toJson(dstChangeInfo, SeatInfo.class);map.put("seat" + dstIndex, json);// グループ属性を設定し、そのグループ属性が既に存在する場合はそのvalueの値を更新、存在しない場合はその属性を追加V2TIMManager.getGroupManager().setGroupAttributes(groupId, map, new V2TIMCallback() {@Overridepublic void onError(int code, String message) {// グループ属性の変更に失敗、マイクポジション移動に失敗}@Overridepublic void onSuccess() {// グループ属性の変更に成功、マイクポジション移動に成功}});}
ステップ6:オーディオ管理
タイムライン図

1. 購読モード。
RTC Engine SDKはデフォルトでオーディオストリームの自動購読ロジックを採用しており、ユーザーがルームに入ると自動的にリモートユーザーの音声の再生を開始します。手動でオーディオストリームを購読する必要がある場合は、
muteRemoteAudio(userId, mute)を追加で呼び出してリモートユーザーのオーディオストリームを購読および再生する必要があります。// 自動購読モード(デフォルト)mTRTCCloud.setDefaultStreamRecvMode(true, true);// 手動購読モード(カスタム)mTRTCCloud.setDefaultStreamRecvMode(false, false);
注意:
購読モードの設定
setDefaultStreamRecvModeは、入室enterRoomする前に呼び出す必要があります。2. キャプチャーとパブリッシュ。
// ローカルオーディオのキャプチャーとパブリッシュを有効にします。mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT)// ローカルオーディオのキャプチャーとパブリッシュを停止するmTRTCCloud.stopLocalAudio();
説明:
startLocalAudioはマイクの使用権限を申請し、stopLocalAudioはマイクの使用権限をリリースします。3. マイクオフとマイクオン。
// ローカルオーディオストリームのパブリッシュを一時停止(マイクオフ)mTRTCCloud.muteLocalAudio(true);// ローカルオーディオストリームのパブリッシュを再開(マイクオン)mTRTCCloud.muteLocalAudio(false);// 特定のリモートユーザーのオーディオストリームの購読と再生を一時停止mTRTCCloud.muteRemoteAudio(userId, true);// 特定のリモートユーザーのオーディオストリームの購読と再生を再開mTRTCCloud.muteRemoteAudio(userId, false);// すべてのリモートユーザーのオーディオストリームの購読と再生を一時停止mTRTCCloud.muteAllRemoteAudio(true);// すべてのリモートユーザーのオーディオストリームの購読と再生を再開mTRTCCloud.muteAllRemoteAudio(false);
説明:
一方、
muteLocalAudioはソフトウェアレベルでデータフローを一時停止または再開するだけでよいため、効率が高くスムーズで、頻繁にマイクのオンオフが必要なシナリオに適しています。4.
音質および音量タイプ
。音質設定
// ローカルオーディオのキャプチャーとパブリッシュ時の音質設定mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);// オーディオプッシュ中に音質を動的に設定mTRTCCloud.setAudioQuality(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);
説明:
RTC Engineのプリセット音質は全部で3段階(Speech/Default/Music)に分かれており、それぞれ異なるオーディオパラメータに対応しています。詳細はTRTCAudioQualityをご参照ください。
音量タイプの設定
RTC Engineの各音質レベルにはデフォルトの音量タイプが対応しています。音量タイプを強制的に指定する必要がある場合は、以下のインターフェースを使用できます。
// 音量タイプの設定mTRTCCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeAuto);
説明:
RTC Engine の音量タイプは全部で3段階(VOIP/Auto/Media)に分かれており、それぞれ異なる音量チャネルに対応します。詳細はTRTCSystemVolumeTypeをご参照ください。
オーディオルーティング設定
携帯電話などのモバイルデバイスには通常、スピーカーとイヤピースの2つの再生経路があります。音声ルーティングを強制的に指定する必要がある場合は、以下のインターフェースを使用できます。
// オーディオルーティングの設定mTRTCCloud.setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER);
説明:
高度機能
弾幕メッセージのインタラクション
音声ライブ配信ルームでは通常、テキスト形式の弾幕メッセージによるインタラクションがあり、これは Chat のグループチャット通常テキストメッセージの送信及び受信によって実現できます。
// パブリックチャットに弾幕メッセージを送信V2TIMManager.getInstance().sendGroupTextMessage(text, groupID, V2TIMMessage.V2TIM_PRIORITY_NORMAL, new V2TIMValueCallback<V2TIMMessage>() {@Overridepublic void onError(int i, String s) {// 弾幕メッセージの送信に失敗}@Overridepublic void onSuccess(V2TIMMessage v2TIMMessage) {// 弾幕メッセージの送信に成功}});// パブリックチャットの弾幕メッセージを受信V2TIMManager.getInstance().addSimpleMsgListener(new V2TIMSimpleMsgListener() {@Overridepublic void onRecvGroupTextMessage(String msgID, String groupID, V2TIMGroupMemberInfo sender, String text) {Log.i(TAG, sender.getNickName + ": " + text);}});
音量コールバック
RTC Engine は固定頻度でマイク上の配信者の音量サイズをコールバックすることができ、通常は音波やサウンドビジュアライザーの表示、発言中の配信者を示すために使用されます。
// 音量コールバックを有効にすることをお勧めします。入室に成功した後すぐに開始してください。// interval: コールバック間(ms); enable_vad: ボイス検出の有無mTRTCCloud.enableAudioVolumeEvaluation(int interval, boolean enable_vad);private class TRTCCloudImplListener extends TRTCCloudListener {public void onUserVoiceVolume(ArrayList<TRTCCloudDef.TRTCVolumeInfo> userVolumes, int totalVolume) {super.onUserVoiceVolume(userVolumes, totalVolume);// userVolumesはすべての話しているユーザーの音量で、ローカルユーザーとリモートプッシュユーザーを含みます。// totalVolumeはリモートのプッシュユーザーの最大音量値のフィードバックです。...// 音量の大きさに応じてUI上で適切な表示を行います。...}}
注意:
ボイス検出は、ローカルのボイス検出結果のみをフィードバックし、自身のロールがアンカーでなければならず、ユーザーにマイクオンの提示に便利です。
userVolumesは配列であり、配列内の各要素において、userIdが空の場合はローカルのマイクからキャプチャーした音量の大きさを表し、が空でない場合はリモートユーザーの音量の大きさを表します。音楽および効果音の再生
BGMや効果音の再生は、ボイスチャットルームシナリオで高頻度な需要です。以下では、一般的なBGM関連のインターフェースの使用と注意事項について説明します。
1. 開始/停止/一時停止/再開。
// BGM、効果音、およびボイスエフェクトの設定を行うための管理クラスを取得TXAudioEffectManager mTXAudioEffectManager = mTRTCCloud.getAudioEffectManager();TXAudioEffectManager.AudioMusicParam param = new TXAudioEffectManager.AudioMusicParam(musicID, musicPath);// 音楽をリモートにパブリッシュするか(そうでなければローカルのみで再生)param.publish = true;// 効果音ファイルかparam.isShortFile = false;// BGMの再生を開始mTXAudioEffectManager.startPlayMusic(param);// BGMの再生を停止mTXAudioEffectManager.stopPlayMusic(musicID);// BGMの再生を一時停止mTXAudioEffectManager.pausePlayMusic(musicID);// BGMの再生を再開mTXAudioEffectManager.resumePlayMusic(musicID);
注意:
RTC Engine は複数の音楽の同時再生をサポートしており、musicID で一意に識別されます。同一時刻に1曲のみ再生したい場合は、他の音楽の再生を停止してから新しい再生を開始するように注意する必要があります。または、同じ musicID を使用して異なる音楽を再生することもでき、その場合 SDK は古い音楽の再生を停止してから新しい音楽を再生します。
RTC Engine はローカル及びネットワークオーディオファイルの再生をサポートしており、
musicPath にローカルの絶対パスまたは URL アドレスを渡します。MP3/AAC/M4A/WAV 形式をサポートします。2. 音楽とボーカルの音量の比率を調整。
// BGMのローカル再生ボリュームの設定mTXAudioEffectManager.setMusicPlayoutVolume(musicID, volume);// BGMのリモート再生ボリュームの設定mTXAudioEffectManager.setMusicPublishVolume(musicID, volume);// すべてのBGMのローカルとリモート音量の設定mTXAudioEffectManager.setAllMusicVolume(volume);// ボーカルのキャプチャーボリュームの設定mTXAudioEffectManager.setVoiceCaptureVolume(volume);
注意:
音量値volumeの正常な範囲は0-100で、デフォルト値は60、最大設定可能値は150ですが、音割れのリスクがあります。
BGMがボーカルを圧倒する場合は、音楽の再生音量を適切に下げ、ボーカルのキャプチャーボリュームを上げてください。
マイクをオフにしてもBGMはオフにしない:
muteLocalAudio(true)をsetVoiceCaptureVolume(0)で置き換えてください。3. 音楽再生のイベントコールバックを設定します。
mTXAudioEffectManager.setMusicObserver(mCurPlayMusicId, new TXAudioEffectManager.TXMusicPlayObserver() {@Override// BGMの再生を開始public void onStart(int id, int errCode) {// -4001: パスのオープンに失敗// -4002: デコード失敗// -4003: URLアドレス無効// -4004: 再生中if (errCode < 0) {// 再生失敗後、再開する前に現在の再生を停止する必要があります。mTXAudioEffectManager.stopPlayMusic(id);}}@Override// BGMの再生の進捗状況public void onPlayProgress(int id, long curPtsMs, long durationMs) {// curPtsMS現在の再生時間(ミリ秒)// durationMs現在の音楽の総時間(ミリ秒)}@Override// BGMの再生が終了public void onComplete(int id, int errCode) {// 回線品質低下によって引き起こされる再生の失敗もこのコールバックをスロー、この時errCode < 0// 途中で一時停止または停止してもonCompleteコールバックはトリガーされません。}});
注意:
BGMを再生する前に、このインターフェースを使用して再生イベントのコールバックを設定し、BGMの再生進行状況を把握してください。
もしMusicIdを再利用する必要がない場合、再生が完了した後に
setMusicObserver(musicId, null)を実行してObserverを完全にリリースできます。4. BGMと効果音のループ再生。
プラン1:
AudioMusicParamのloopCountパラメータを使用して、ループ再生回数を設定します。値の範囲は0から任意の正の整数です。デフォルト値:0。0は音楽を1回再生することを意味し、1は音楽を2回再生することを意味し、以降同様です。
private void startPlayMusic(int id, String path, int loopCount) {TXAudioEffectManager.AudioMusicParam param = new TXAudioEffectManager.AudioMusicParam(id, path);// 音楽をリモートにパブリッシュするかparam.publish = true;// 効果音ファイルかparam.isShortFile = true;// ループ再生回数を設定、負数は無限ループparam.loopCount = loopCount < 0 ? Integer.MAX_VALUE : loopCount;mTRTCCloud.getAudioEffectManager().startPlayMusic(param);}
注意:
プラン1では、各ループが完了しても
onCompleteコールバックはトリガーされません。設定されたループ回数がすべて完了した後にのみ、そのコールバックがトリガーされます。プラン2:「BGMが再生終了した」というイベントコールバック
onCompleteを利用してループ再生を実装します。通常はリストループまたは単曲ループに使用されます。// ループ再生するかどうかを示すメンバー変数private boolean loopPlay;private void startPlayMusic(int id, String path) {TXAudioEffectManager.AudioMusicParam param = new TXAudioEffectManager.AudioMusicParam(id, path);mTXAudioEffectManager.setMusicObserver(id, new MusicPlayObserver(id, path));mTXAudioEffectManager.startPlayMusic(param);}private class MusicPlayObserver implements TXAudioEffectManager.TXMusicPlayObserver {private final int mId;private final String mPath;public MusicPlayObserver(int id, String path) {mId = id;mPath = path;}@Overridepublic void onStart(int i, int i1) {}@Overridepublic void onPlayProgress(int i, long l, long l1) {}@Overridepublic void onComplete(int i, int i1) {mTXAudioEffectManager.stopPlayMusic(i);if (i1 >= 0 && loopPlay) {// ここでループリストの音楽ID、Pathに置き換えることができます。startPlayMusic(mId, mPath);}}}
ミキシング転送及びプッシュバック
1. ミキシングストリームを RTC Engine ルームにプッシュバックします。
private void startPublishMediaToRoom(String roomId, String userId) {// TRTCPublishTargetオブジェクトを作成TRTCCloudDef.TRTCPublishTarget target = new TRTCCloudDef.TRTCPublishTarget();// ミキシング後にルームにプッシュバックtarget.mode = TRTCCloudDef.TRTC_PublishMixStream_ToRoom;target.mixStreamIdentity.strRoomId = roomId;// ミキシングロボットのuseridは、ルームの他のユーザーのuseridと重複してはいけません。target.mixStreamIdentity.userId = userId + MIX_ROBOT;// トランスコード後のオーディオストリームのエンコードパラメータを設定する(カスタマイズ可能)TRTCCloudDef.TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCCloudDef.TRTCStreamEncoderParam();trtcStreamEncoderParam.audioEncodedChannelNum = 2;trtcStreamEncoderParam.audioEncodedKbps = 64;trtcStreamEncoderParam.audioEncodedCodecType = 2;trtcStreamEncoderParam.audioEncodedSampleRate = 48000;// トランスコードされたビデオストリームのエンコードパラメータを設定(オーディオストリームミックスの場合は無視しても良い)trtcStreamEncoderParam.videoEncodedFPS = 15;trtcStreamEncoderParam.videoEncodedGOP = 3;trtcStreamEncoderParam.videoEncodedKbps = 30;trtcStreamEncoderParam.videoEncodedWidth = 64;trtcStreamEncoderParam.videoEncodedHeight = 64;// オーディオストリームミックスのパラメータを設定TRTCCloudDef.TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCCloudDef.TRTCStreamMixingConfig();// デフォルトでは空欄のままで大丈夫です。これは、ルーム内のすべてのオーディオがミキシングされることを意味します。trtcStreamMixingConfig.audioMixUserList = null;// ビデオストリームミックステンプレートの配置(オーディオストリームミックスの場合は無視しても良い)TRTCCloudDef.TRTCVideoLayout videoLayout = new TRTCCloudDef.TRTCVideoLayout();trtcStreamMixingConfig.videoLayoutList.add(videoLayout);// オーディオ・ビデオストリームプッシュバックを開始mTRTCCloud.startPublishMediaStream(target, trtcStreamEncoderParam, trtcStreamMixingConfig);}
2. イベントコールバックおよび更新停止タスク。
タスク結果イベントコールバック。
private class TRTCCloudImplListener extends TRTCCloudListener {@Overridepublic void onStartPublishMediaStream(String taskId, int code, String message, Bundle extraInfo) {// taskId: リクエストが成功した場合、TRTCバックエンドはコールバックでこのタスクのtaskIdを提供し、その後、そのtaskIdをupdatePublishMediaStreamとstopPublishMediaStreamと組み合わせて更新および停止することができます。// code: コールバック結果、0は成功を意味し、その他の値は失敗を意味します。}@Overridepublic void onUpdatePublishMediaStream(String taskId, int code, String message, Bundle extraInfo) {// メディアストリームのパブリッシュインターフェース(updatePublishMediaStream)を呼び出す際にに渡したtaskIdは、このコールバックを通じて再度返され、どの更新リクエストに属するかを識別するために使用されます。// code: コールバック結果、0は成功を意味し、その他の値は失敗を意味します。}@Overridepublic void onStopPublishMediaStream(String taskId, int code, String message, Bundle extraInfo) {// メディアストリームのパブリッシュ停止(stopPublishMediaStream)を呼び出す際にに渡したtaskIdは、このコールバックを通じて再度返され、どの停止リクエストに属するかを識別するために使用されます。// code: コールバック結果、0は成功を意味し、その他の値は失敗を意味します。}}
メディアストリームの更新とパブリッシュ。
このインターフェースは RTC Engine サーバーに指令を送信し、
startPublishMediaStream で起動したメディアストリームを更新します。// taskId: onStartPublishMediaStreamでコールバックされたタスクID// target: 例えば、パブリッシュしたCDN URLの追加、削除// params: メディアストリームのエンコード出力パラメータを一貫して保持することがお勧めです。これにより、再生側での中断を避けることができます。// config: ストリームミックストランスコーディングに参加するユーザーリストを更新。例えば、クロスルームPKなど。mTRTCCloud.updatePublishMediaStream(taskId, target, trtcStreamEncoderParam, trtcStreamMixingConfig);
注意:
同じタスクでは、オーディオのみ、オーディオ・ビデオ、ビデオのみの間での切り替えはサポートされていません。
メディアストリームのパブリッシュ停止。
このインターフェースは RTC Engine サーバーに指令を送信し、
startPublishMediaStream で起動したメディアストリームを停止します。// taskId: onStartPublishMediaStreamでコールバックされたタスクIDmTRTCCloud.stopPublishMediaStream(taskId);
注意:
taskIdに空の文字列を入力すると、
startPublishMediaStreamで開始されたそのユーザーのすべてのメディアストリームが停止します。1つのメディアストリームのみを開始した場合や、自分が開始したすべてのメディアストリームを停止したい場合は、この方法をお勧めします。ネットワーク品質リアルタイムコールバック
ローカルおよびリモートユーザーのネットワーク品質をリアルタイムで統計するために、
onNetworkQualityをリスニングすることができます。このコールバックは2秒ごとに一度発生します。private class TRTCCloudImplListener extends TRTCCloudListener {@Overridepublic void onNetworkQuality(TRTCCloudDef.TRTCQuality localQuality,ArrayList<TRTCCloudDef.TRTCQuality> remoteQuality) {// localQuality userIdは空、ローカルユーザーのネットワーク品質評価結果を表します// remoteQualityは、リモートユーザーのネットワーク品質評価結果を表しており、その結果はリモートとローカルの両方の影響を受けます。switch (localQuality.quality) {case TRTCCloudDef.TRTC_QUALITY_Excellent:Log.i(TAG, "現在のネットワークは非常に良い");break;case TRTCCloudDef.TRTC_QUALITY_Good:Log.i(TAG, "現在のネットワークは比較的良い");break;case TRTCCloudDef.TRTC_QUALITY_Poor:Log.i(TAG, "現在のネットワークは普通");break;case TRTCCloudDef.TRTC_QUALITY_Bad:Log.i(TAG, "現在のネットワークが不安定");break;case TRTCCloudDef.TRTC_QUALITY_Vbad:Log.i(TAG, "現在のネットワークが非常に悪い");break;case TRTCCloudDef.TRTC_QUALITY_Down:Log.i(TAG, "現在のネットワークはTRTC最低要件を満たしていない");break;default:Log.i(TAG, "未定義");break;}}}
高度な権限制御
RTC Engine 高度な権限制御は、異なるルームに異なる入室権限を設定する(例:高级VIPルーム)ため、またはリスナーのマイクオン権限を制御する(例:ゴーストマイクの処理)ために使用できます。
ステップ3:PrivateMapKeyの入室検証&マイクオン検証。
入室検証
TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = SDKAPPID;mTRTCParams.userId = mUserId;mTRTCParams.strRoomId = mRoomId;// 業務バックエンドから取得したUserSigmTRTCParams.userSig = getUserSig();// 業務バックエンドから取得したPrivateMapKeymTRTCParams.privateMapKey = getPrivateMapKey();mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_VOICE_CHATROOM);
マイクオン検証
// 業務バックエンドから最新のPrivateMapKeyを取得し、ロール切り替えインターフェースに渡します。mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor, getPrivateMapKey());
異常処理
異常エラー処理
UserSig関連
列挙値 | 取得値 | 説明 |
ERR_TRTC_INVALID_USER_SIG | -3320 | 入室パラメータUserSigが正しくありません。 TRTCParams.userSigが空であるかどうかを確認してください。 |
ERR_TRTC_USER_SIG_CHECK_FAILED | -100018 | UserSig検証失敗、パラメータ TRTCParams.userSigが正しく入力されているか、または期限切れでないかを確認してください。 |
入退室関連
入室に失敗した場合は、まず入室パラメータが正しいかどうかを確認してください。また、入退室インターフェースは必ずペアで呼び出す必要があります。入室に失敗した場合でも、退室インターフェースを呼び出す必要があります。
列挙値 | 取得値 | 説明 |
ERR_TRTC_CONNECT_SERVER_TIMEOUT | -3308 | 入室リクエストがタイムアウトしました。ネットワークが切断されているか、VPNが使用されているかを確認してください。また、4Gに切り替えてテストすることもできます。 |
ERR_TRTC_INVALID_SDK_APPID | -3317 | 入室パラメータsdkAppIdエラー。 TRTCParams.sdkAppIdが空であるかどうか確認してください。 |
ERR_TRTC_INVALID_ROOM_ID | -3318 | 入室パラメータroomIdエラー。 TRTCParams.roomIdまたはTRTCParams.strRoomIdが空であるかどうか確認してください。roomIdとstrRoomIdは混在できません。 |
ERR_TRTC_INVALID_USER_ID | -3319 | 入室パラメータuserIdが正しくありません。 TRTCParams.userIdが空であるかどうかを確認してください。 |
ERR_TRTC_ENTER_ROOM_REFUSED | -3340 | 入室リクエストが拒否されました。 enterRoomで同じIdのルームに連続して入室しようとしていないか確認してください。 |
デバイス関連
デバイス関連のエラーをリスニングし、関連するエラーが発生した場合にUIでユーザーに通知します。
列挙値 | 取得値 | 説明 |
ERR_MIC_START_FAIL | -1302 | マイクの起動に失敗しました。例えば、WindowsまたはMacデバイスで、マイクの設定プログラム(ドライバー)に異常があります。デバイスを無効にしてから再度有効にするか、マシンを再起動するか、設定プログラムを更新してください。 |
ERR_SPEAKER_START_FAIL | -1321 | スピーカーの起動に失敗しました。例えば、WindowsやMacのデバイスで、スピーカーの設定プログラム(ドライバー)に異常があります。デバイスを無効にしてから再度有効にするか、マシンを再起動するか、設定プログラムを更新してください。 |
ERR_MIC_OCCUPY | -1319 | マイクが使用中です。たとえば、モバイルデバイスが通話中の場合、マイクを開くと失敗します。 |
異常終了処理
1. 断線検知とタイムアウトによる退室。
以下のコールバックを通じて、RTC Engine のネットワーク切断及び再接続イベント通知を監視できます。
onConnectionLostコールバック受信後、ローカルのマイクポジションUIにネットワーク切断の警告を表示し、ユーザーに通知します。同時に、ローカルでタイマーを起動し、設定された時間閾値を超えてもonConnectionRecoveryコールバックが受信されない場合、つまりネットワークが継続して切断状態にある場合は、ローカルでマイクオフにし、退室プロセスを開始し、同時にポップアップウィンドウでユーザーにルームからの退出とページの破棄を通知します。ネットワーク切断が90秒(デフォルト)を超えると、タイムアウトによる退房がトリガーされ、RTC Engine サーバーは該当ユーザーをルームから退出させます。もし該当ユーザーが配信者ロールの場合、ルーム内の他のユーザーは onRemoteUserLeaveRoom コールバックを受信します。private class TRTCCloudImplListener extends TRTCCloudListener {@Overridepublic void onConnectionLost() {// SDKクラウドとの接続が切断されました。}@Overridepublic void onTryToReconnect() {// SDKクラウドに再接続しています。}@Overridepublic void onConnectionRecovery() {// SDKクラウドとの接続が復旧されました。}}
2. オフライン状態で自動的にマイクオフ
Chat ユーザーの通常状態は、オンライン(ONLINE)、オフライン(OFFLINE)、未ログイン(UNLOGGED)に分かれており、其中オフライン状態は通常、ユーザーによる強制終了やネットワーク異常中断が原因で発生します。アンカーがマイクオン聞き手ユーザーの状態を購読することで、オフラインしたマイクオン聞き手ユーザーを検出し、彼らを強制マイクオフすることができます。
// アンカーがマイクオン聞き手ユーザーの状態を購読V2TIMManager.getInstance().subscribeUserStatus(userList, new V2TIMCallback() {@Overridepublic void onSuccess() {// ユーザーステータスの購読に成功}@Overridepublic void onError(int code, String message) {// ユーザーステータスの購読に失敗}});// アンカーがマイクオフの聞き手ユーザーのステータスの購読をキャンセルV2TIMManager.getInstance().unsubscribeUserStatus(userList, new V2TIMCallback() {@Overridepublic void onSuccess() {// ユーザーステータスの購読解除に成功}@Overridepublic void onError(int code, String message) {// ユーザーステータスの購読解除に失敗}});// ユーザーステータス変更通知と処理V2TIMManager.getInstance().addIMSDKListener(new V2TIMSDKListener() {@Overridepublic void onUserStatusChanged(List<V2TIMUserStatus> userStatusList) {for (V2TIMUserStatus userStatus : userStatusList) {final String userId = userStatus.getUserID();int status = userStatus.getStatusType();if (status == V2TIMUserStatus.V2TIM_USER_STATUS_OFFLINE) {// オフライン状態での強制マイクオフkickSeat(getSeatIndexFromUserId(userId));}}}});


注意:
ユーザー状態の購読にはプロフェッショナル版パッケージへのアップグレードが必要です。詳細は 基礎サービス詳細 をご参照ください。
ユーザー状態を購読するには、事前に Chat コンソール で ユーザー状態クエリー及び状態変更通知設定 を有効にする必要があります。有効にしていない場合、
subscribeUserStatus を呼び出すとエラーが報告されます。サーバー側のユーザーキックとルーム解散
1. サーバー側のユーザーキック。
まず、RTC Engine サーバー側退出インターフェース RemoveUser(整数型ルーム番号)または RemoveUserByStrRoomId(文字列型ルーム番号)を呼び出して、対象ユーザーを RTC Engine ルームから退出させます。入力例は以下の通りです:
https://trtc.tencentcloudapi.com/?Action=RemoveUser&SdkAppId=1400000001&RoomId=1234&UserIds.0=test1&UserIds.1=test2&<公共リクエストパラメータ>
ユーザーをキックした後、対象ユーザーはクライアントで
onExitRoom()コールバックを受け取り、reasonの値は1になります。この時、このコールバック内でマイクオフ、Chat グループ退出などの操作を処理できます。// TRTCルーム退出イベントコールバック@Overridepublic void onExitRoom(int reason) {if (reason == 0) {Log.d(TAG, "exitRoomアクティブコールでルーム退出します");} else {// reason 1: 現在のルームからサーバーによってキックされました。// reason 2: 現在のルームは解散されました。Log.d(TAG, "現在のルームからサーバーによってキックされたか、ルームが解散ました");// マイクオフleaveSeat(seatIndex);// IMグループから退出quitGroup(groupID, new V2TIMCallback() {});}}
2. サーバー側のルーム解散。
https://xxxxxx/v4/group_open_http_svc/destroy_group?sdkappid=88888888&identifier=admin&usersig=xxx&random=99999999&contenttype=json
グループ解散の実行に成功した後、対象グループ内の全メンバーはクライアントで
onGroupDismissed()コールバックを受け取ります。この時、そのコールバック内でTRTCルーム退出などの処理ができます。// グループ解散コールバックV2TIMManager.getInstance().addGroupListener(new V2TIMGroupListener() {@Overridepublic void onGroupDismissed(String groupID, V2TIMGroupMemberInfo opUser) {// TRTCルームから退出mTRTCCloud.stopLocalAudio();mTRTCCloud.exitRoom();}});
説明:
ルーム内の全ユーザーが
exitRoom() を呼び出して退房を完了すると、RTC Engine ルームは自動的に解散します。もちろん、サーバー側インターフェース DismissRoom(整数型ルーム番号)または DismissRoomByStrRoomId(文字列型ルーム番号)を呼び出して RTC Engine ルームを強制的に解散することもできます。入室してライブルームのメッセージ履歴を確認します。
AVChatRoomはデフォルトではライブルームのメッセージ履歴を保存せず、新しいユーザーがライブルームに入ると、入室後に送信されたメッージのみを見ることができます。新しいグループユーザーの体験を最適化するために、コントロールパネルでライブグループユーザーが閲覧できるグループに参加する前のメッセージ数を設定ができます。以下の図に示されたように。


ライブグループユーザーがグループ参加前の履歴メッセージを取得するのは、他のグループの履歴メッセージを取得するのと同じです。コード例:
V2TIMMessageListGetOption option = new V2TIMMessageListGetOption();option.setGetType(V2TIMMessageListGetOption.V2TIM_GET_CLOUD_OLDER_MSG); // クラウドからより古いメッセージを取得option.setGetTimeBegin(1640966400); // 2022-01-01 00:00:00から開始option.setGetTimePeriod(1 * 24 * 60 * 60); // 丸一日のメッセージを取得option.setCount(Integer.MAX_VALUE); // 指定された時間範囲内のすべてのメッセージを返します。option.setGroupID(#you group id#); // グループチャットメッセージの取得V2TIMManager.getMessageManager().getHistoryMessageList(option, new V2TIMValueCallback<List<V2TIMMessage>>() {@Overridepublic void onSuccess(List<V2TIMMessage> v2TIMMessages) {Log.i("imsdk", "success");}@Overridepublic void onError(int code, String desc) {Log.i("imsdk", "failure, code:" + code + ", desc:" + desc);}});
注意:
この機能はプレミアムプランのユーザーのみ利用可能で、24時間以内に最大20件のメッセージ履歴しか閲覧できません。
入室時のマイク上配信者ミュート状態の認識
方案1: 入室時には全ての配信者をデフォルトでミュート状態とし、その後
onUserAudioAvailable(userId, true) コールバックに基づいて対応する配信者のミュート状態を解除します。private class TRTCCloudImplListener extends TRTCCloudListener {@Overridepublic void onUserAudioAvailable(String userId, boolean available) {if (available) {// 対応するアンカーのミュート状態を解除}}}
方案2: 配信者のミュート状態を RTC Engine グループ属性に保存し、リスナーが入室時に全量グループ属性を取得し、マイク上配信者のミュート状態を解析します。
V2TIMManager.getGroupManager().getGroupAttributes(groupID, null, new V2TIMValueCallback<Map<String, String>>() {@Overridepublic void onError(int i, String s) {// グループ属性の取得に失敗}@Overridepublic void onSuccess(Map<String, String> attrMap) {// グループ属性の取得に成功、アンカーのミュート状態のkeyをmuteStatusと仮定String muteStatus = attrMap.get("muteStatus");// muteStatusを分析し、各マイクオンアンカーのミュート状態を取得}});
Bluetoothヘッドセットのオーディオ入出力問題
携帯電話がBluetoothヘッドセットに正常に接続されているが、RTC Engine アプリケーションのオーディオ入力または出力が依然として携帯電話のマイクまたはスピーカーを使用しています。
1. オーディオ出力が正常にBluetoothヘッドセットを使用している場合でも、オーディオ入力が依然として携帯電話のマイクを使用している場合は、音量タイプの設定を確認してください。通話音量のみがBluetoothヘッドセットのマイクを介しての集音できます。詳細については、オーディオ管理-音質および音量タイプを参照してください。
mTRTCCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeVOIP);
2. もしオーディオの入力と出力にBluetoothヘッドセットを使用できない場合は、App権限でBluetoothの権限が設定されているかどうかを確認してください。Androidデバイスについては、Android 12以下のシステムでは少なくとも
BLUETOOTH権限を設定する必要があり、Andorid 12以上のシステムでは少なくともBLUETOOTH_CONNECT権限を設定し、コード中で動的な権限の申請が必要です。AndroidManifest.xmlでBluetoothの権限を設定し、Android 12未満のシステムとの互換性のため、以下のように宣言することをお勧めします。
<!--通常権限:基本Bluetooth接続権限--><uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/><!--通常権限:Bluetooth管理、スキャン権限--><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /><!--実行時権限:Android 12 Bluetooth権限 Bluetoothデバイスを検索--><uses-permission android:name="android.permission.BLUETOOTH_SCAN" /><!--実行時権限:Android 12 Bluetooth権限 現在のデバイスが他のBluetoothデバイスに検出可能にする--><uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /><!--実行時権限:Android 12 Bluetooth権限 ペアリングされたBluetoothデバイスとの通信または現在の携帯電話のBluetoothオンオフ状態を取得--><uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
Android 12以降のシステムに新たに追加されたBluetoothの細分化された権限について、動的な申請する方法は以下の通りです。
private List<String> permissionList = new ArrayList<>();protected void initPermission() {// Android SDKのバージョンを判断、Android 12以上かif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {// ニーズに応じて、動的な申請が必要な権限を追加permissionList.add(Manifest.permission.BLUETOOTH_SCAN);permissionList.add(Manifest.permission.BLUETOOTH_ADVERTISE);permissionList.add(Manifest.permission.BLUETOOTH_CONNECT);}if (permissionList.size() != 0) {// 動的な権限申請ActivityCompat.requestPermissions(this, permissionList.toArray(new String[0]), REQ_PERMISSION_CODE);}}
音楽再生がサポートするリソースパスの問題
RTC Engine SDK API
startPlayMusic を使用してバックグラウンドミュージックを再生する場合、音楽リソースパスパラメータ path には、Android 開発における assets/raw など、アプリケーションリソースファイルを保存するディレクトリ下のファイルパスを渡すことはサポートされていません。これらのディレクトリ下のファイルは APK にパッケージ化され、インストール後は携帯電話のファイルシステムに解凍されないためです。現在サポートされているのは、ネットワークリソース URL、Android デバイスの外部ストレージ、及びアプリケーションのプライベートディレクトリ下のリソースファイルの絶対パスのみです。この問題を回避する方法として、assetsディレクトリ内のリソースファイルを事前にデバイスの外部ストレージまたはアプリのプライベートディレクトリにコピーすることができます。サンプルコードは以下の通りです。
public static void copyAssetsToFile(Context context, String name) {// アプリケーション自体のディレクトリ内のfilesディレクトリString savePath = ContextCompat.getExternalFilesDirs(context, null)[0].getAbsolutePath();// アプリケーション自体のディレクトリ内のcacheディレクトリ// String savePath = getApplication().getExternalCacheDir().getAbsolutePath();// アプリケーションのプライベートストレージディレクトリ内のfilesディレクトリ// String savePath = getApplication().getFilesDir().getAbsolutePath();String filename = savePath + "/" + name;File dir = new File(savePath);// ディレクトリが存在しない場合、このディレクトリを作成if (!dir.exists()) {dir.mkdir();}try {if (!(new File(filename)).exists()) {InputStream is = context.getResources().getAssets().open(name);FileOutputStream fos = new FileOutputStream(filename);byte[] buffer = new byte[1024];int count = 0;while ((count = is.read(buffer)) > 0) {fos.write(buffer, 0, count);}fos.close();is.close();}} catch (Exception e) {e.printStackTrace();}}
プライベートストレージを適用filesディレクトリパス:
/data/user/0/<package_name>/files/<file_name>外部ストレージを使用するfilesディレクトリパス:
/storage/emulated/0/Android/data/<package_name>/files/<file_name>外部ストレージを使用するcacheディレクトリパス:
/storage/emulated/0/Android/data/<package_name>/cache/file_name>注意:
もし渡されたパスがアプリケーション自体の特定ディレクトリ以外の他の外部ストレージパスである場合、Android 10以降のデバイスでは、Googleに新しいストレージ管理システム、パーティションストレージが導入されたため、アクセス拒否される可能性があります。AndroidManifest.xmlファイルの<application>タグ内に以下のコードを追加することで、一時的に回避することができます:
android:requestLegacyExternalStorage="true"。この属性はtargetSdkVersionが29(Android 10)のアプリケーションでのみ有効で、より高いバージョンのtargetSdkVersionのアプリケーションでは、アプリのプライベートまたは外部ストレージパスの使用を引き続き推奨します。RTC Engine SDK 11.5 以降では、Content Provider コンポーネントの Content URI を使用して、Android デバイス上のローカル音楽リソースを再生できます。
Android 11およびHarmonyOS 3.0以上のシステムでは、外部ストレージディレクトリのリソースファイルにアクセスできない場合、
MANAGE_EXTERNAL_STORAGE権限の申請が必要です。まず、アプリケーションのAndroidManifestファイルに以下のエントリを追加する必要があります。
<manifest ...><!-- This is the permission itself --><uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><application ...>...</application></manifest>
その後、アプリがこの権限を使用する必要がある箇所で、ユーザーに手動での承認を促してください。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {if (!Environment.isExternalStorageManager()) {Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);Uri uri = Uri.fromParts("package", getPackageName(), null);intent.setData(uri);startActivity(intent);}} else {// For Android versions less than Android 11, you can use the old permissions modelActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);}