直播上下滑

最近更新时间:2025-07-25 14:56:01

我的收藏
直播场景中,面对大量主播提供的多种多样的视频内容,通过上下滑动的方式快速浏览和选择自己喜爱的内容可以给用户带来较好的使用体验。本文将分别介绍 Android 和 iOS 端的方案。

iOS 端直播上下滑方案

在这种场景下,切换直播间时,因为进新的房间和拉流需要时间,会导致前后两个房间的视频画面不连贯,对于该问题,一般有以下3种解决方案,本文将分别对这3种方案进行详细说明。
实现方式
用户体验
资源消耗
实现逻辑
黑屏
一般, 切换直播间时先显示黑屏,再出现新的画面
无额外资源消耗
简单,无额外逻辑
占位图
稍好,切换直播间时,先显示对应主播的固定占位图,再出现新的画面
稍多,需额外为每个主播存储一个占位图,并加载到客户端
稍复杂,切换前需额外异步加载完成占位图
双实例
最好,切换直播间时,流畅显示前后2个主播的画面
较多,在列表中需同时拉2路流,进入直播间后可以只拉1路流
较复杂,需使用多个实例,并控制不同实例的音视频拉取

黑屏

通过监听系统的滑动事件,调用切换房间的接口来切换直播间,在新的直播间加载出来之前,没有内容显示,体现为短暂黑屏,为了方便说明,这里黑屏时间为1秒左右,具体黑屏时间受网络和视频码率影响,效果如下。



其中切换房间的代码片段如下:
let src = TRTCSwitchRoomConfig()
// 根据业务生成对应的房间号以及进房凭证,示例中使用客户端生成进房凭证,线上业务请从后台获取
src.strRoomId = strRoomId
src.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as String
trtcCloud.switchRoom(src)

占位图

通过监听系统的滑动事件,调用切换房间的接口来切换直播间,但和黑屏方案不同,该方案需要提前加载每个直播间的占位图,在直播间视频流显示之前,显示对应直播间的占位图,为了方便说明,这里占位图持续时间1秒左右,具体时间受网络和视频码率影响。

效果图





实现步骤

1. 设置第一个主播的背景图。
bgView = UIImageView(frame: self.view.bounds)
// 该图片需为对应直播间的占位图,需业务上提前获取
bgView.image = UIImage(named: "1.png")
bgView.contentMode = .scaleAspectFill
bgView.translatesAutoresizingMaskIntoConstraints = false
self.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 = strRoomId
src.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as String
trtcCloud.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 Foundation
import ObjectiveC
import 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 = false

var pageViewController: UIPageViewController!
var pageZero: UIViewController!
var pageOne: UIViewController!
var pageTwo: UIViewController!
var pages: [UIViewController] = []

var curPageIdx = 0
var curIsSub = false


func 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 的页面切换。
// 获取下/上一个Page
func 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 = 0
params.strRoomId = strRoomIdLst.first ?? "1"
params.userId = userId
params.role = .anchor
params.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as String
trtcCloud.addDelegate(self)
trtcCloud.enterRoom(params, appScene: .LIVE)

// 子实例预加载进入下一个房间
// 根据实际业务替换sdkAppId,roomID,strRoomId,userId和userSig
// 示例中使用客户端生成userSig,线上业务请从后台获取
let subParams = TRTCParams()
subParams.sdkAppId = UInt32(SDKAppID)
subParams.roomId = 0
subParams.strRoomId = strRoomIdLst[1]
subParams.userId = userId
subParams.role = .anchor
subParams.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as String
subCloudHelper.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();
}
}