ライブストリーミング上下スワイプ
ライブストリーミングシーンでは、多数の配信者による多様な動画コンテンツが提供されており、上下にスワイプして素早く閲覧し、好みのコンテンツを選択することで、ユーザーに優れた使用体験を提供できます。本文では、Android端末とiOS端末それぞれのソリューションについて紹介します。
iOS端末のライブストリーミング上下スワイプソリューション
このシーンでは、ライブストリーミングルームを切り替える際、新しいルームに入室してコンテンツを受信するのに時間がかかるため、前後のルームの動画映像が途切れる可能性があります。この問題に対して、一般的に以下の3つのソリューションがあります。本文では、これら3つのソリューションについてそれぞれ詳しく説明します。
実現方法 | ユーザー体験 | リソース消費 | 実現ロジック |
一般、ライブストリーミングルームを切り替える際には、まず黒画面が表示され、その後新しい画面が表示されます | 追加リソース消費なし | 簡単、追加ロジックなし | |
やや良い、ライブストリーミングルームを切り替える際には、まず対応する配信者の固定プレースホルダー画像が表示され、その後新しい画面が表示されます | やや多い、各配信者に対してプレースホルダー画像を追加で保存し、クライアントに読み込む必要があります | やや複雑、切り替え前に非同期でプレースホルダー画像の読み込み完了が必要です | |
最も良い、ライブストリーミングルームを切り替える際には、前後の2人の配信者の画面がスムーズに表示されます | 比較的に多い、リスト内では2つのストリームを同時に受信する必要があり、ライブストリーミングルームに入った後は1つのストリームを受信するだけで十分です | 比較的に複雑、複数のインスタンスを使用し、異なるインスタンスのオーディオ・ビデオ受信を制御する必要があります |
黒画面
システムのスワイプイベントを監視し、ルーム切り替えのインターフェースを呼び出してライブストリーミングルームを切り替えます。新しいライブストリーミングルームが読み込まれる前に、コンテンツが表示されず、一時的な黒画面が表示されます。説明の便宜上、ここでの黒画面時間は約1秒としますが、具体的な黒画面時間はネットワークとビデオビットレートの影響を受けます。効果は以下の通りです。


ルーム切り替えのコードスニペットは以下の通りです。
let src = TRTCSwitchRoomConfig()// 業務に応じて対応するルーム番号と入室証明書を生成します。サンプルではクライアント側で入室証明書を生成していますが、オンライン業務ではバックエンドから取得してくださいsrc.strRoomId = strRoomIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.switchRoom(src)
プレースホルダー画像
システムのスワイプイベントを監視し、ルーム切り替えのインターフェースを呼び出してライブストリーミングルームを切り替えますが、黒画面ソリューションとは異なり、このソリューションでは各ライブストリーミングルームのプレースホルダー画像を事前に読み込む必要があります。ライブストリーミングルームのビデオストリームが表示される前に、対応するライブストリーミングルームのプレースホルダー画像が表示されます。説明の便宜上、ここではプレースホルダー画像の表示時間を約1秒としますが、具体的な時間はネットワークとビデオビットレートの影響を受けます。
効果図


実現手順
1. 最初の配信者の背景画像を設定します。
bgView = UIImageView(frame: self.view.bounds)// この画像は対応するライブストリーミングルームのプレースホルダー画像である必要があり、業務上事前に取得しておく必要がありますbgView.image = UIImage(named: "1.png")bgView.contentMode = .scaleAspectFillbgView.translatesAutoresizingMaskIntoConstraints = falseself.view.insertSubview(bgView, at: 0)NSLayoutConstraint.activate([bgView.topAnchor.constraint(equalTo: view.topAnchor),bgView.bottomAnchor.constraint(equalTo: view.bottomAnchor),bgView.leadingAnchor.constraint(equalTo: view.leadingAnchor),bgView.trailingAnchor.constraint(equalTo: view.trailingAnchor),])
2. ライブストリーミングルームを切り替える前に背景画像を切り替えます。
DispatchQueue.main.async {UIView.transition(with: self.bgView,duration: 0,options: .transitionCrossDissolve,animations: {// 業務上で対応するプレースホルダー画像を切り替えますself.bgView.image = UIImage(named: strRoomId)}, completion: nil)}let src = TRTCSwitchRoomConfig()// 業務に応じて対応するルーム番号と入室証明書を生成します。サンプルではクライアント側で入室証明書を生成していますが、オンライン業務ではバックエンドから取得してくださいsrc.strRoomId = strRoomIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.switchRoom(src)
3. 新しいライブストリーミングルームの最初のフレームのビデオ画面がレンダリングを開始した時に、背景画像をビデオ画面に切り替えます。
// ビデオストリームを受信func onUserVideoAvailable(_ userId: String, available: Bool) {if available {trtcCloud.startRemoteView(userId, streamType: .big, view: view)} else {trtcCloud.stopRemoteView(userId, streamType: .big)}}// 最初のフレームのビデオ画面がレンダリングを開始した時に、プレースホルダー画像を背景に切り替えて、ビデオ画面を表示しますfunc onFirstVideoFrame(_ userId: String, streamType: TRTCVideoStreamType, width: Int32, height: Int32) {// ここでは、背景画像とビデオレンダリングコントロールの前後順序を調整します。実際の業務では状況に応じて調整してくださいself.view.exchangeSubview(at: 1, withSubviewAt: 0)}
デュアルインスタンス
注意:
このソリューションは表示効果が最良で、ユーザー体験も最高ですが、スワイプリストでは前後の2つのライブストリーミングルームの2つのストリームを同時に受信する必要があります。ユーザーがライブストリーミングルームに入った時に次のライブストリーミングルームのストリームのプリロードを停止できますが、全体的なトラフィックとコスト消費はより多くなります。
効果図
最もスムーズな上下スワイプ効果を実現するためには、2つのインスタンスを同時に使用し、前のライブストリーミングルームを視聴しながら次のライブストリーミングルームのビデオ画面をプリロードし、UIPageViewControllerまたは手動でスワイプ位置に基づいて上下2つのビデオの表示位置を調整し、自然でスムーズなトランジションを実現します。次のライブストリーミングルームをスワイプして視聴する効果は下図左側の例の通りです。




このソリューションの全体的なロジックは、現在のライブストリーミングルームに入室後、すぐにサブインスタンスを使用して次のライブストリーミングルームに入室し、次のライブストリーミングルームのビデオストリームを受信し、次のライブストリーミングルームのビデオストリームをUIPageViewControllerの次のPageに表示することです。新しいライブストリーミングルームに入室した時に、新しいライブストリーミングルームのオーディオストリームを開始し、このように循環することで、最もスムーズな上下スワイプ効果を達成できます。同時に、過剰なリソース消費を避けるために、次のライブストリーミングルーム視聴のみをプリロードし、使用頻度の少ない前のライブストリーミングルーム視聴はプリロードせず、切り替え時には依然として黒画面を使用します。前のライブストリーミングルームを視聴する効果は上図右側の例の通りです。
実現手順
1. サブインスタンスツールクラスを定義します。
import Foundationimport ObjectiveCimport TXLiteAVSDK_Professional@objc protocol SubCloudHelperDelegate : NSObjectProtocol {@objc optional func onUserVideoAvailableWithSubId(subId: Int, userId: String, available: Bool)}class SubCloudHelper:NSObject,TRTCCloudDelegate {var trtcCloud: TRTCCloud!var subId: Int!weak var delegate : SubCloudHelperDelegate? = nilfunc initWithSubId(subId: Int, trtcIns: TRTCCloud) {self.subId = subIdself.trtcCloud = trtcInsself.trtcCloud.addDelegate(self)}func getCloud()->TRTCCloud {return trtcCloud}func onUserVideoAvailable(_ userId: String, available: Bool) {if self.delegate?.onUserVideoAvailableWithSubId?(subId: subId, userId: userId, available: available) == nil {return}}}
2. サブインスタンスを使用します。
let trtcCloud = TRTCCloud()let subCloudHelper = SubCloudHelper()override func viewDidLoad() {super.viewDidLoad()subCloudHelper.initWithSubId(subId: 0, trtcIns: trtcCloud.createSub())subCloudHelper.delegate = self}
3. 切り替え用のUIPageViewControllerを準備します。
private var atRoom: Bool = falsevar pageViewController: UIPageViewController!var pageZero: UIViewController!var pageOne: UIViewController!var pageTwo: UIViewController!var pages: [UIViewController] = []var curPageIdx = 0var curIsSub = falsefunc setupPages() {pageViewController = UIPageViewController(transitionStyle: .scroll,navigationOrientation: .vertical)pageViewController.dataSource = selfpageViewController.delegate = selfaddChild(pageViewController)view.addSubview(pageViewController.view)pageViewController.didMove(toParent: self)// pagespageZero = UIViewController()pageZero.view.backgroundColor = .blackpageOne = UIViewController()pageOne.view.backgroundColor = .blackpageTwo = UIViewController()pageTwo.view.backgroundColor = .blackpages = [pageZero, pageOne, pageTwo]pageViewController.setViewControllers([pages[curPageIdx]], direction: .forward, animated: false)}
4. pageViewControllerのページ切り替えを実現します。
// 次/前のPageを取得func getShowPage(isNext: Bool) -> UIViewController {var newPageIdx = 0if isNext {newPageIdx = curPageIdx + 1} else {newPageIdx = curPageIdx - 1}if newPageIdx >= pages.count {newPageIdx = 0} else if newPageIdx < 0 {newPageIdx = pages.count - 1}return pages[newPageIdx]}extension RtcDuplexVC: UIPageViewControllerDataSource {func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {return getShowPage(isNext: false)}func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {return getShowPage(isNext: true)}}
5. メインインスタンスでルームに入室し、サブインスタンスを使用して次のライブストリーミングルームをプリロードします。
// メインインスタンスでルームに入室// 実際の業務に応じてsdkAppId、roomID、strRoomId、userId、userSigを置き換えてください// サンプルではクライアント側でuserSigを生成しますが、オンライン業務ではバックエンドから取得してくださいlet params = TRTCParams()params.sdkAppId = UInt32(SDKAppID)params.roomId = 0params.strRoomId = strRoomIdLst.first ?? "1"params.userId = userIdparams.role = .anchorparams.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.addDelegate(self)trtcCloud.enterRoom(params, appScene: .LIVE)// サブインスタンスでプリロードし、次のルームに入室// 実際の業務に応じてsdkAppId、roomID、strRoomId、userId、userSigを置き換えてください// サンプルではクライアント側でuserSigを生成しますが、オンライン業務ではバックエンドから取得してくださいlet subParams = TRTCParams()subParams.sdkAppId = UInt32(SDKAppID)subParams.roomId = 0subParams.strRoomId = strRoomIdLst[1]subParams.userId = userIdsubParams.role = .anchorsubParams.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringsubCloudHelper.trtcCloud.enterRoom(subParams, appScene: .LIVE)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)
6. コールバックに基づいてビデオストリームを受信し、対応する page にレンダリングします。
func getPageByIdx(isNext: Bool) -> UIViewController {var newPageIdx = curPageIdxif isNext {newPageIdx += 1}if newPageIdx >= pages.count {newPageIdx = 0}return pages[newPageIdx]}extension RtcDuplexVC: TRTCCloudDelegate {func onUserVideoAvailable(_ userId: String, available: Bool) {if available {trtcCloud.startRemoteView(userId, streamType: .big, view: getPageByIdx(isNext: curIsSub).view)} else {trtcCloud.stopRemoteView(userId, streamType: .big)}}}extension RtcDuplexVC: SubCloudHelperDelegate {func onUserVideoAvailableWithSubId(subId: Int, userId: String, available: Bool) {if available {subCloudHelper.trtcCloud.startRemoteView(userId, streamType: .big, view: getPageByIdx(isNext: !curIsSub).view)} else {subCloudHelper.trtcCloud.stopRemoteView(userId, streamType: .big)}}}
7. 新しいルームに切り替えた後、プリロードされたルームを更新するか、上にスワイプした時に、現在表示されているルームを更新します。
func updateCurRoomIdx(isNext: Bool) {if isNext {curRoomIdx += 1if curRoomIdx >= strRoomIdLst.count {curRoomIdx = 0}} else {curRoomIdx -= 1if curRoomIdx < 0 {curRoomIdx = strRoomIdLst.count - 1}}}// ここでは実際の業務ロジックに基づいてルーム番号を切り替える必要がありますfunc updateNewRoom(isNext: Bool) {var newRoomIdx = 0if isNext{newRoomIdx = curRoomIdx + 1} else {newRoomIdx = curRoomIdx - 1}if newRoomIdx >= strRoomIdLst.count {newRoomIdx = 0} else if newRoomIdx < 0 {newRoomIdx = strRoomIdLst.count - 1}let newRoomStrId = strRoomIdLst[newRoomIdx]let src = TRTCSwitchRoomConfig()src.strRoomId = newRoomStrIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as Stringif curIsSub {trtcCloud.switchRoom(src)trtcCloud.muteAllRemoteAudio(true)} else {subCloudHelper.trtcCloud.switchRoom(src)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}}extension RtcDuplexVC: UIPageViewControllerDelegate {func pageViewController(_ pageViewController: UIPageViewController,didFinishAnimating finished: Bool,previousViewControllers: [UIViewController],transitionCompleted completed: Bool) {if completed {guard let currentVC = pageViewController.viewControllers?.first else {return}if let index = pages.firstIndex(of: currentVC) {// 上下スワイプの判断基準を取得let iden = index - curPageIdx// 現在表示されているページのシーケンス番号を更新curPageIdx = index// 下にスワイプif iden == 1 || iden == -2 {// 現在いるルームのシーケンス番号を更新updateCurRoomIdx(isNext: true)// 現在表示されているページのインスタンスを更新curIsSub.toggle()// ルームを更新updateNewRoom(isNext: true)}// 上にスワイプif iden == -1 || iden == 2 {// ルームを更新updateNewRoom(isNext: false)// 現在いるルームのシーケンス番号を更新updateCurRoomIdx(isNext: false)// 現在表示されているページのインスタンスを更新curIsSub.toggle()trtcCloud.muteAllRemoteAudio(true)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}// 現在のルームのミュートを解除if curIsSub {subCloudHelper.trtcCloud.muteAllRemoteAudio(false)} else {trtcCloud.muteAllRemoteAudio(false)}}}}}
さらに、業務上では「スワイプリスト内」と「ライブストリーミングルーム入室」という2つの状態を区別することも可能です。上下にスワイプする際には通常、ルームの詳細やチャットなどの情報は不要であり、「ライブストリーミングルーム入室」後にのみ表示が必要となるためです。このように区別することで、頻繁な上下スワイプによるライブストリーミングルーム状態の業務記録負荷を軽減し、同時にプリロードによるリソース消費を削減できます。
具体的な方法:「スワイプリスト内」でのみ上下スクロールによるライブストリーミングルーム切り替えを許可し、この時はライブストリーミングルームの画面と少量の情報のみを表示します。「ライブストリーミングルーム入室」時には完全なライブストリーミングルームとチャットなどの情報を表示し、この時は上下スワイプによるライブストリーミングルーム切り替えを禁止します。また、「ライブストリーミングルーム入室」時に次のライブストリーミングルームのビデオストリームのプリロードを停止し、「スワイプリスト内」に戻ると同時に次のライブストリーミングルームのビデオストリームのプリロードを再開します。効果は以下の通りです。


具体的な実現は次の通りです。
// ライブストリーミングルーム入室ロジックを処理@objc func enterBtnClick() {// 上下スワイプリストのUIコンポーネントを非表示self.enterBtn.isHidden = true// ライブストリーミングルーム入室後のUIコンポーネントを表示self.exitBtn.isHidden = false// ...self.atRoom = true// ライブストリーミングルーム入室後、上下のスワイプを禁止pageViewController.dataSource = nil// プリロードを停止if curIsSub{trtcCloud.muteAllRemoteVideoStreams(true)} else {subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}}// ライブストリーミングルーム退出ロジックを処理@objc func exitBtnClick() {// ライブストリーミングルームのUIコンポーネントを非表示self.enterBtn.isHidden = false// 上下スワイプリストのUIコンポーネントを表示self.exitBtn.isHidden = trueself.atRoom = false// 上下スワイプリストに進んだ後、上下スワイプを復元pageViewController.dataSource = self// プリロードを再開if curIsSub{trtcCloud.muteAllRemoteVideoStreams(false)} else {subCloudHelper.trtcCloud.muteAllRemoteAudio(false)}}
Android端末のライブストリーミング上下スワイプソリューション
以下のデュアルインスタンスとシングルインスタンスのセクションでは、効果図とサンプルコードに3つのページがあり、ページの順序はA > B > Cで、Aはルーム1231、Bはルーム1232、Cはルーム1233に対応しています。
実現方法 | ユーザー体験 | リソース消費 | 実現ロジック |
一般、リストをスワイプする際、2つのライブストリーミングを同時に見ることはできず、ページが完全に切り替わった後に対応するライブストリーミングが表示されます。プレースホルダー画像を使用してユーザー体験を向上させることができます。 | リストには1ストリームのトラフィック消費のみがあり、1つのビデオ再生オブジェクトが使用されています。 | 簡単、必要に応じてプレースホルダーを設定します。 | |
比較的に良い、同時に2つのライブストリーミングルームに入室し、次のライブストリーミングを事前にロードし、リストをスワイプした時に、現在と次のライブストリーミングルームを同時に表示できます。 | リストには2ストリームのトラフィック消費があり、2ストリームの費用が発生し、2つのビデオ再生オブジェクトが使用されます。 | 複雑、複数のインスタンスを使用し、異なるインスタンスのオーディオ・ビデオ受信を制御する必要があります。 |
シングルインスタンス
ライブストリーミングリストを上下にスワイプすると、スワイプ中は単一のライブストリーミング映像(シングルインスタンス)しか表示されず、コストを節約できます。
効果図
Aページをスワイプ中は、Bページのライブストリーミング映像を同時に表示できず、Bページに切り替えるとBページのライブストリーミング映像が表示され、Aページのライブストリーミング映像は表示されません。

ソリューション原理
ページをスワイプ中は、同時に表示できるライブストリーミング映像は1つだけで、ページを切り替えると前のライブストリーミング映像を停止し、次のライブストリーミング映像を受信します。
AページからBページへスワイプする際の各段階の操作と状態は以下の通りです。
1. Aページが画面に表示されている時、TRTCCloudインスタンス1を使用し、ルーム1231に入室してストリームを受信し、オーディオ・ビデオを再生し、Aページに表示します。
2. AページからBページへスワイプする過程で、Bページに切り替えるコールバックを受信していないため、Aページは引き続きルーム1231のオーディオ・ビデオストリームを正常に再生し、Bページはプレースホルダー画像または黒画面を表示します。
3. AページからBページへスワイプする過程で、Bページに切り替えるコールバックを受信し、TRTCCloudインスタンス1を使用し、ルーム1231のオーディオ・ビデオストリームの受信を停止して退室し、ルーム1232に入室してストリームを受信し、オーディオ・ビデオを再生し、Bページに表示します。Aページはプレースホルダー画像または黒画面を表示します。
実現コード
ViewPager2、RecyclerView.Adapterを使用して全画面スワイプ効果を実現します。ViewPager2のregisterOnPageChangeCallbackのonPageSelectedコールバックで、受信を停止し、退室し、入室し、受信を開始します。以下にScrollSwitchRoomActivityの完全なコードを示します。レイアウトファイルは前節と同じです。
ScrollSwitchRoomActivity のコードは以下の通りです。
public class ScrollSwitchRoomActivity extends TRTCBaseActivity {PageAdapter mAdapter;public String[] mRoomIds;private TRTCCloud mTRTCCloud;private TXCloudVideoView mRemoteVideoView;private int mCurPos = -1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scroll_switch_room);//タイトルバーを非表示getSupportActionBar().hide();mRoomIds = new String[]{"1231", "1232", "1233"};if (checkPermission()) {initView();}}@Overrideprotected void onPermissionGranted() {initView();}private void initView() {mAdapter = new PageAdapter(this, mRoomIds);ViewPager2 viewPager = findViewById(R.id.viewPager);viewPager.setAdapter(mAdapter);//viewPagerのスワイプ方向を設定viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);//viewPagerのプリロードを設定viewPager.setOffscreenPageLimit(1);// ページ切り替えのリスナーを追加viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {public void onPageSelected(int position) {Log.d("ScrollSwitchRoom", "onPageSelected: " + position);if (mCurPos == position) {return;}RecyclerView recyclerViewImpl = (RecyclerView) viewPager.getChildAt(0);// 先に現在のルームから退室exitRoom();// 次のルームに入室View itemView = recyclerViewImpl.getChildAt(position);mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);enterRoom(position);mCurPos = position;}});// Initialize your views heremTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext());mTRTCCloud.addListener(mTRTCCloudListener);}private void enterRoom(int roomIdIndex) {TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = GenerateTestUserSig.SDKAPPID;mTRTCParams.userId = "123";mTRTCParams.strRoomId = mRoomIds[roomIdIndex];mTRTCParams.userSig = GenerateTestUserSig.genTestUserSig(mTRTCParams.userId);mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);}private TRTCCloudListener mTRTCCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mTRTCCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mRemoteVideoView);} else {mTRTCCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}public void onError(int errCode, String errMsg, Bundle extraInfo) {// print ErrorLog.e("ScrollSwitchRoom", "Error: " + errCode + " " + errMsg);}};private void exitRoom() {mTRTCCloud.stopAllRemoteView();mTRTCCloud.exitRoom();// mTRTCCloud.setListener(null);}public class PageAdapter extends RecyclerView.Adapter<PageAdapter.PageViewHolder> {private Context context;public String[] mRoomIds;public PageAdapter(Context context, String[] roomIds) {this.context = context;this.mRoomIds = roomIds;}public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.item_scroll_page, parent, false);return new PageViewHolder(view);}public void onBindViewHolder(@NonNull PageViewHolder holder, int position) {TextView textView = holder.itemView.findViewById(R.id.tv_room_number);textView.setText(getString(R.string.switchroom_roomid) + ":" + mRoomIds[position]);}public int getItemCount() {return mRoomIds.length;}public int getItemViewType(int position) {return position;}class PageViewHolder extends RecyclerView.ViewHolder {PageViewHolder(@NonNull View itemView) {super(itemView);}}}@Overrideprotected void onDestroy() {super.onDestroy();exitRoom();}}
activity_scroll_switch_room.xml は以下の通り。
<?xml version="1.0" encoding="utf-8"?><androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent" />
item_scroll_page.xml は以下の通り。
<?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"android:id="@+id/item_layout"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/iv_placeholder"android:scaleType="centerCrop"android:background="@drawable/placeholder_img"android:layout_width="match_parent"android:layout_height="match_parent"/><com.tencent.rtmp.ui.TXCloudVideoViewandroid:id="@+id/txcvv_main_local"android:layout_width="match_parent"android:layout_height="match_parent" /><TextViewandroid:id="@+id/tv_room_number"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintEnd_toEndOf="@id/item_layout"app:layout_constraintStart_toStartOf="@id/item_layout" /></androidx.constraintlayout.widget.ConstraintLayout>
デュアルインスタンス
ライブストリーミングリストを上下にスワイプすると、2つのTRTCCloudインスタンスが同時に2つのルーム(現在のルームと次のルーム)に入室し、スワイプ中に両方のルームのライブストリーミングを同時に視聴できます。現在のルームから下にスワイプする過程では、前のルームのライブストリーミングを同時に視聴することはできず、ページが完全に切り替わった後にのみ視聴可能です。必要に応じて、3つのTRTCCloudインスタンスを自ら使用して実現できます。
注意:
デュアルインスタンスは2つのルームのストリームを同時に受信し、2ストリームのトラフィック消費が発生するため、より多くの費用がかかります。3つのTRTCCloudインスタンスを自ら実現した場合、3ストリームのトラフィック消費が発生します。
効果図
Bページをスワイプ中は、Cページのライブストリーミング映像を直接見ることができます。

ソリューション原理
ページをスワイプ中は、最大2つのページを同時に表示でき、2つのTRTCCloudインスタンスを使用し、同時に2つのルームに入室し、2つのルームのストリームを同時に受信できます。
AページからBページへスワイプする際の各段階の操作と状態は以下の通りです。
1. Aページが画面に表示される時、TRTCCloudインスタンス1を使用し、ルーム1231に入室してストリームを受信し、オーディオ・ビデオを再生し、Aページに表示します。同時に、TRTCCloudインスタンス2を使用し、ルーム1232に入室してストリームを受信し、ビデオを再生し、Bページに表示し、オーディオをミュートします。
2. AページからBページへスワイプする過程で、この時、AページとBページが同時にビデオを再生しているのを見ることができ、ルーム1231のオーディオを聞くことができます。
3. Bページが完全に表示された後、TRTCCloudインスタンス2を使用し、ルーム1232のオーディオを開始し、ビデオを引き続き再生します。TRTCCloudインスタンス1を使用し、ルーム1231から退室し、ルーム1233に入室してストリームを受信し、ビデオを再生し、Cページに表示し、オーディオをミュートします。
4. BページからAページへスワイプする過程で、ルーム1232のビデオのみを見ることができます。この時、TRTCCloudインスタンス2はBページで使用され、TRTCCloudインスタンス1はCページで使用されているためです。Aページが完全に表示された後、TRTCCloudインスタンス1を使用してルーム1231に入室し、ストリームを受信し、オーディオとビデオを再生します。Bページでは引き続きTRTCCloudインスタンス2を使用してビデオストリームを受信し、オーディオをミュートします。
実現コード
ViewPager2、RecyclerView.Adapterを使用して全画面スワイプ効果を実現します。ScrollSwitchRoomActivityのコードは次の通りです。
public class ScrollSwitchRoomDualActivity extends TRTCBaseActivity {PageAdapter mAdapter;public String[] mRoomIds;private TRTCCloud mTRTCCloud;private TRTCCloud mSubCloud;private TXCloudVideoView mRemoteVideoView;private TXCloudVideoView mSubRemoteVideoView;private Boolean mIsInMainRoom = null;private int mCurPos = 0;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scroll_switch_room);//タイトルバーを非表示getSupportActionBar().hide();mRoomIds = new String[]{"1231", "1232", "1233"};if (checkPermission()) {initView();}}@Overrideprotected void onPermissionGranted() {initView();}private void initView() {mAdapter = new PageAdapter(this,mRoomIds);ViewPager2 viewPager = findViewById(R.id.viewPager);viewPager.setAdapter(mAdapter);//viewPagerのスワイプ方向を設定viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);//viewPagerのプリロードを設定viewPager.setOffscreenPageLimit(1);// ページ切り替えのリスナーを追加viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {public void onPageSelected(int position) {Log.d("ScrollSwitchRoom", "onPageSelected: " + position);RecyclerView recyclerViewImpl = (RecyclerView) viewPager.getChildAt(0);if (mIsInMainRoom == null) {View itemView = recyclerViewImpl.getChildAt(position);mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);View subItemView = recyclerViewImpl.getChildAt(position + 1);mSubRemoteVideoView = subItemView.findViewById(R.id.txcvv_main_local);enterRoom();mIsInMainRoom = true;} else {if (mIsInMainRoom) {mTRTCCloud.muteAllRemoteAudio(true);mSubCloud.muteAllRemoteAudio(false);} else {mTRTCCloud.muteAllRemoteAudio(false);mSubCloud.muteAllRemoteAudio(true);}if (position != (mRoomIds.length - 1)) {String roomId;TRTCCloud trtcCloud;if (mCurPos < position) {// 画面上方向へスワイプroomId = mRoomIds[position + 1];trtcCloud = mIsInMainRoom ? mTRTCCloud : mSubCloud;View itemView = recyclerViewImpl.getChildAt(position + 1);if (mIsInMainRoom) {mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);} else {mSubRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);}} else {//画面下方向へスワイプroomId = mRoomIds[position];trtcCloud = mIsInMainRoom ? mSubCloud : mTRTCCloud;View itemView = recyclerViewImpl.getChildAt(position);if (mIsInMainRoom) {mSubRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);} else {mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);}}switchRoom(roomId, trtcCloud);}mIsInMainRoom = !mIsInMainRoom;}mCurPos = position;}});// Initialize your views heremTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext());mSubCloud = mTRTCCloud.createSubCloud();}/*** 初期化時のみ、ルーム入室を呼び出し、その後ルームを切り替える場合は、switchRoomを呼び出します*/private void enterRoom() {mTRTCCloud.addListener(mTRTCCloudListener);mSubCloud.addListener(mSubCloudListener);TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = GenerateTestUserSig.SDKAPPID;mTRTCParams.userId = "123";// mTRTCParams.roomId = Integer.parseInt(roomId);mTRTCParams.strRoomId = mRoomIds[0];mTRTCParams.userSig = GenerateTestUserSig.genTestUserSig(mTRTCParams.userId);mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);mTRTCParams.strRoomId = mRoomIds[1];mSubCloud.muteAllRemoteAudio(true);mSubCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);}private TRTCCloudListener mTRTCCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mTRTCCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mRemoteVideoView);} else {mTRTCCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}public void onError(int errCode, String errMsg, Bundle extraInfo) {// print ErrorLog.e("ScrollSwitchRoom", "Error: " + errCode + " " + errMsg);}public void onSwitchRoom(long err, String errMsg) {// Switch room}};private TRTCCloudListener mSubCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mSubCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mSubRemoteVideoView);} else {mSubCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}};private void exitRoom() {mTRTCCloud.stopAllRemoteView();mTRTCCloud.exitRoom();mTRTCCloud.setListener(null);mSubCloud.stopAllRemoteView();mSubCloud.exitRoom();mSubCloud.setListener(null);}private void switchRoom(String roomId, TRTCCloud trtcCloud) {TRTCCloudDef.TRTCSwitchRoomConfig config = new TRTCCloudDef.TRTCSwitchRoomConfig();// config.roomId = Integer.parseInt(roomId);config.strRoomId = roomId;trtcCloud.switchRoom(config);}public class PageAdapter extends RecyclerView.Adapter<PageAdapter.PageViewHolder> {private Context context;public String[] mRoomIds;public PageAdapter(Context context, String[] roomIds) {this.context = context;this.mRoomIds = roomIds;}public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.item_scroll_page, parent, false);return new PageViewHolder(view);}public void onBindViewHolder(@NonNull PageViewHolder holder, int position) {TextView textView = holder.itemView.findViewById(R.id.tv_room_number);textView.setText(getString(R.string.switchroom_roomid) + ":" + mRoomIds[position]);}public int getItemCount() {return mRoomIds.length;}public int getItemViewType(int position) {return position;}class PageViewHolder extends RecyclerView.ViewHolder {PageViewHolder(@NonNull View itemView) {super(itemView);}}}@Overrideprotected void onDestroy() {super.onDestroy();exitRoom();}}