在开发直播场景的业务时,很多人想模仿抖音、视频号 的滑动交互效果, 在滑出下一页时,可立即看到直播画面。 下面以 腾讯云实时音视频的使用 为例,分别介绍Android和iOS的实现方式:
Android
以下的双实例和单实例章节中,其中的效果图和示例代码中,有三个页面,页面顺序是: A->B->C, A对应房间1231,B对应房间1232,C对应房间1233。
双实例
该方案,实现了与抖音、视频号一样的效果,直播列表上下滑动,在滑动过程中可同时看到两个直播画面(双实例),有更好的用户体验。
效果图:
在B页滑动过程中,可快速看到C页的直播画面,此时不松开手指再滑动到A页,可快速看到A页的直播画面。
快速滑动,切换房间,效果如下:
方案原理:
在页面滑动中,最多可同时看到两个页面,使用两个TRTCCloud实例,来交替进房拉流,在页面切换时,会同时拉取两个房间的流,切换完成后,只会拉取当前房间的流。
在从A页滑动到B页,各阶段的操作和状态如下:
- 在A页显示到屏幕上时,用TRTCCloud实例1,进入房间1231并拉流,播放音视频,在A页面显示。
- 从A页滑动到B页的过程中,用TRTCCloud实例2,进入房间1232并拉流,播放视频,在B页面显示,静音音频。此时可同时看到A、B页面在同时播放视频,听到房间1231的音频。
- B页完全显示后,用TRTCCloud实例1,退出房间1231。用TRTCCloud实例2,开启房间1232的音频,视频继续播放。
- 从B页滑动到C页的过程中,用TRTCCloud实例1,进入房间1233并拉流,播放视频, 在C页面显示,静音音频。此时可同时看到B、C页面在同时播放视频,听到房间1232的音频。
5、C页完全显示后,用TRTCCloud实例2,退出房间1232。用TRTCCloud实例1,开启房间1233的音频,视频继续播放。
实现代码:
使用ViewPager2、RecyclerView.Adapter来实现整屏滑动效果,在RecyclerView.Adapter的onViewAttachedToWindow回调中进房拉流,在onViewDetachedFromWindow回调中退出房间。下面给出完整代码:
ScrollSwitchRoomActivity代码如下:
public class ScrollSwitchRoomActivity extends TRTCBaseActivity {
PageAdapter mAdapter;
public String[] mRoomIds;
private TRTCCloud mTRTCCloud;
private TRTCCloud mSubCloud;
private TXCloudVideoView mRemoteVideoView;
private TXCloudVideoView mSubRemoteVideoView;
@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);
// 创建两个TRTCCloud实例,用于交替进房
mTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext());
mSubCloud = mTRTCCloud.createSubCloud();
mTRTCCloud.addListener(mTRTCCloudListener);
mSubCloud.addListener(mSubCloudListener);
}
private TRTCCloudListener mTRTCCloudListener = new TRTCCloudListener() {
@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);
}
}
};
private TRTCCloudListener mSubCloudListener = new TRTCCloudListener() {
@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);
}
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) {
Log.d("ScrollSwitchRoom", "onBindViewHolder: " + position);
TextView textView = holder.itemView.findViewById(R.id.tv_room_number);
textView.setText(getString(R.string.switchroom_roomid) + ":" + mRoomIds[position]);
holder.setIndex(position);
}
@Override
public void onViewAttachedToWindow(@NonNull PageViewHolder holder) {
super.onViewAttachedToWindow(holder);
//进房参数
TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();
mTRTCParams.sdkAppId = GenerateTestUserSig.SDKAPPID;
mTRTCParams.userId = "123";
mTRTCParams.strRoomId = mRoomIds[holder.getIndex()];
mTRTCParams.userSig = GenerateTestUserSig.genTestUserSig(mTRTCParams.userId);
mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;
boolean isCurMainRoom = (holder.getIndex() % 2) == 0;
Log.d("ScrollSwitchRoom", "onViewAttachedToWindow 切换房间 roomId: " + mTRTCParams.strRoomId + "mIsCurMainRoom: " + isCurMainRoom);
//根据当前页面的位置,选择实例,进房拉流,静音播放
if (isCurMainRoom) {
mSubRemoteVideoView = holder.itemView.findViewById(R.id.txcvv_main_local);
mSubCloud.muteAllRemoteAudio(true);
mSubCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
} else {
mRemoteVideoView = holder.itemView.findViewById(R.id.txcvv_main_local);
mTRTCCloud.muteAllRemoteAudio(true);
mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
}
}
@Override
public void onViewDetachedFromWindow(@NonNull PageViewHolder holder) {
super.onViewDetachedFromWindow(holder);
Log.d("ScrollSwitchRoom", "onViewDetachedFromWindow: " + holder.toString());
//根据当前页面的位置,选择实例,退房,并对另一个实例开启声音播放
boolean isCurMainRoom = (holder.getIndex() % 2) == 0;
if (isCurMainRoom) {
mSubCloud.exitRoom();
mTRTCCloud.muteAllRemoteAudio(false);
} else {
mTRTCCloud.exitRoom();
mSubCloud.muteAllRemoteAudio(false);
}
}
public int getItemCount() {
return mRoomIds.length;
}
public int getItemViewType(int position) {
return position;
}
class PageViewHolder extends RecyclerView.ViewHolder {
public int index = 0;
PageViewHolder(@NonNull View itemView) {
super(itemView);
}
public void setIndex(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
}
}
@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>
单实例
直播列表上下滑动,在滑动过程中只能看到单个直播画面(单实例), 可节省费用。
效果图:
在A页滑动过程中,无法同时看到B页的直播画面, 在切换到B页,可看到B页的直播画面,无法看到A页的直播画面。
快速滑动,切换房间,效果如下:
方案原理:
在页面滑动中,只能同时看到一个直播画面,在页面切换后,停止上一个直播画面,拉取下一个直播画面。
在从A页滑动到B页,各阶段的操作和状态如下:
- 在A页显示到屏幕上时,用TRTCCloud实例1,进入房间1231并拉流,播放音视频,在A页面显示。
- 从A页滑动到B页的过程中,没有收到切换到B页的回调,A页面还是正常播放房间1231的音视频流,B页面显示占位图或黑屏。
- 从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();
}
}
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秒左右,具体时间受网络和视频码率影响。
效果图
实现步骤
- 设置第一个主播的背景图。
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),
])
- 在切换直播间之前切换背景图。
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)
- 在新直播间第一帧视频画面开始渲染时,切换背景图到视频画面。
// 拉视频流
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 中。在进入新的直播间时,开启新的直播间的音频流,如此循环,可以达到最流畅的上下滑效果;同时为了避免过多的资源消耗,只对观看下一个直播间进行预加载,使用场景较少的观看上一个直播间不做预加载,切换时依旧使用黑屏,观看上一个直播间的效果如上图右侧示例。
实现步骤
- 定义子实例工具类。
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
}
}
}
- 使用子实例。
let trtcCloud = TRTCCloud()
let subCloudHelper = SubCloudHelper()
override func viewDidLoad() {
super.viewDidLoad()
subCloudHelper.initWithSubId(subId: 0, trtcIns: trtcCloud.createSub())
subCloudHelper.delegate = self
}
- 准备好用于切换的 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)
}
- 实现 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)
}
}
- 主实例进房,并使用子实现预加载下一个直播间。
// 主实例进房
// 根据实际业务替换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)
- 根据回调拉视频流并渲染到对应的 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)
}
}
}
- 切换到新的房间后,更新预加载的房间,或者在向上滑时,更新当前显示的房间。
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)
}
}