一、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? = nil func initWithSubId(subId: Int, trtcIns: TRTCCloud) { self.subId = subId self.trtcCloud = trtcIns self.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 = self pageViewController.delegate = self addChild(pageViewController) view.addSubview(pageViewController.view) pageViewController.didMove(toParent: self) // pages pageZero = UIViewController() pageZero.view.backgroundColor = .black pageOne = UIViewController() pageOne.view.backgroundColor = .black pageTwo = UIViewController() pageTwo.view.backgroundColor = .black pages = [pageZero, pageOne, pageTwo] pageViewController.setViewControllers([pages[curPageIdx]], direction: .forward, animated: false)}
4. 实现 pageViewController 的页面切换。
// 获取下/上一个Pagefunc getShowPage(isNext: Bool) -> UIViewController { var newPageIdx = 0 if 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 = curPageIdx if 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 += 1 if curRoomIdx >= strRoomIdLst.count { curRoomIdx = 0 } } else { curRoomIdx -= 1 if curRoomIdx < 0 { curRoomIdx = strRoomIdLst.count – 1 } }}// 这里需要根据实际的业务逻辑切换房间号func updateNewRoom(isNext: Bool) { var newRoomIdx = 0 if 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 = newRoomStrId src.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as String if 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 = true self.atRoom = false // 进入上下滑列表后恢复上下滑动 pageViewController.dataSource = self // 恢复预加载 if curIsSub{ trtcCloud.muteAllRemoteVideoStreams(false) } else { subCloudHelper.trtcCloud.muteAllRemoteAudio(false) }}
八、Android 端直播上下滑方案
以下的双实例和单实例章节中,其中的效果图和示例代码中,有三个页面,页面顺序是: A > B > C, A 对应房间1231,B 对应房间1232,C 对应房间1233。
| 实现方式 | 用户体验 | 资源消耗 | 实现逻辑 |
| 单实例 | 一般,滑动列表时,不能同时看到两个直播间,需页面完全切换后,才显示对应的直播间。可用占位图提升用户体验。 | 列表中只有一路流量消耗,有一个视频播放对象被使用。 | 简单,根据需要设置占位图。 |
| 双实例 | 较好,同时进入两个直播间,提前加载下一个直播,滑动列表时,能同时看到当前和下一个直播间。 | 列表中会有两路流量消耗,很产生两路的费用,有两个视频播放对象被使用。 | 复杂,需使用多个实例,并控制不同实例的音视频拉取。 |
九、单实例
直播列表上下滑动,在滑动过程中只能看到单个直播画面(单实例), 可节省费用。
十、效果图
在 A 页滑动过程中,无法同时看到 B 页的直播画面,再切换到 B 页,可看到 B 页的直播画面,无法看到 A 页的直播画面。
十一、方案原理
在页面滑动中,只能同时看到一个直播画面,在页面切换后,停止上一个直播画面,拉取下一个直播画面。
在从 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; @Override protected 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(); } } @Override protected 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 here mTRTCCloud = 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 } @Override public 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 Error Log.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); } } } @Override protected 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"> <ImageView android: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.TXCloudVideoView android:id="@+id/txcvv_main_local" android:layout_width="match_parent" android:layout_height="match_parent" /> <TextView android: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>
十三、双实例
直播列表上下滑动,两个 TRTCCloud 实例同时进入两个房间(当前房间和下一个房间),在滑动过程中可同时看到这两个房间的直播。如果从当前房间向下滑动的过程中,是不能同时看到上一个房间的直播,只有在页面完全切换后,才能看到。如果需要可自行使用三个 TRTCCloud 实例实现。
注意:双实例会同时拉取两个房间的流,会造成两路的流量消耗,会产生更多的费用。如果自实现三个 TRTCCloud 实例,则会有三路的流量消耗。
十四、效果图
在 B 页滑动过程中,可直接看到 C 页的直播画面。
十五、方案原理
在页面滑动中,最多可同时看到两个页面,使用两个 TRTCCloud 实例,同时进两个房间,同时拉取两个房间的流。
在从 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; @Override protected 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(); } } @Override protected 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 here mTRTCCloud = 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 } @Override public 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 Error Log.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 } @Override public 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); } } } @Override protected void onDestroy() { super.onDestroy(); exitRoom(); }}