视频号直播上下滑如何做到无缝切换

在开发直播场景的业务时,很多人想模仿抖音、视频号 的滑动交互效果, 在滑出下一页时,可立即看到直播画面。 下面以 腾讯云实时音视频的使用 为例,分别介绍Android和iOS的实现方式:

Android

以下的双实例和单实例章节中,其中的效果图和示例代码中,有三个页面,页面顺序是: A->B->C, A对应房间1231,B对应房间1232,C对应房间1233。

双实例

该方案,实现了与抖音、视频号一样的效果,直播列表上下滑动,在滑动过程中可同时看到两个直播画面(双实例),有更好的用户体验。

效果图:

在B页滑动过程中,可快速看到C页的直播画面,此时不松开手指再滑动到A页,可快速看到A页的直播画面。

在这里插入图片描述

快速滑动,切换房间,效果如下:

在这里插入图片描述

方案原理:

在页面滑动中,最多可同时看到两个页面,使用两个TRTCCloud实例,来交替进房拉流,在页面切换时,会同时拉取两个房间的流,切换完成后,只会拉取当前房间的流。

在从A页滑动到B页,各阶段的操作和状态如下:

  1. 在A页显示到屏幕上时,用TRTCCloud实例1,进入房间1231并拉流,播放音视频,在A页面显示。
  2. 从A页滑动到B页的过程中,用TRTCCloud实例2,进入房间1232并拉流,播放视频,在B页面显示,静音音频。此时可同时看到A、B页面在同时播放视频,听到房间1231的音频。
  3. B页完全显示后,用TRTCCloud实例1,退出房间1231。用TRTCCloud实例2,开启房间1232的音频,视频继续播放。
  4. 从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页,各阶段的操作和状态如下:

  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();
    }
}

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),
])

  1. 在切换直播间之前切换背景图。

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)
  1. 在新直播间第一帧视频画面开始渲染时,切换背景图到视频画面。
// 拉视频流
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
        }
    }
}
  1. 使用子实例。

let trtcCloud = TRTCCloud()
let subCloudHelper = SubCloudHelper()


override func viewDidLoad() {
    super.viewDidLoad()
    
    subCloudHelper.initWithSubId(subId: 0, trtcIns: trtcCloud.createSub())
    subCloudHelper.delegate = self
}

  1. 准备好用于切换的 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)
}

  1. 实现 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)
    }
}
  1. 主实例进房,并使用子实现预加载下一个直播间。

// 主实例进房
// 根据实际业务替换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)

  1. 根据回调拉视频流并渲染到对应的 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)
        }
    }
}
  1. 切换到新的房间后,更新预加载的房间,或者在向上滑时,更新当前显示的房间。

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)
    }
}

### STM32与OLED显示屏下滑问题解决方案 对于STM32控制下的OLED显示屏出现的下滑问题,通常涉及硬件连接、驱动程序以及显示缓冲区管理等多个方面。针对这些问题的具体处理方法如下: #### 1. 硬件检查 确保所有硬件连接稳固无误,特别是SPI/IIC接口线路是否接触良好。任何松动都可能导致数据传输不稳定,进而引发屏幕刷新异常。 #### 2. 驱动库优化 如果使用的是SSD1306或其他常见型号的OLED控制器,则可以考虑更新至最新版本的官方驱动库[^1]。新的固件往往修复了一些已知缺陷并提高了性能稳定性。 ```c #include "ssd1306.h" void SSD1306_Init(void){ // 初始化代码... } ``` #### 3. 缓冲区同步机制改进 为了避免图像撕裂现象,在每次绘制新帧之前先清除整个画面再重新渲染全部内容并非最佳实践;相反地,应该仅更新发生变化的部分区域,并通过双缓冲技术来实现无缝切换旧新两版图像之间的过渡效果。 ```c // 定义两个交替使用的静态缓存数组用于存储前后景图层像素值 static uint8_t bufferA[DISPLAY_WIDTH * DISPLAY_HEIGHT / 8]; static uint8_t bufferB[DISPLAY_WIDTH * DISPLAY_HEIGHT / 8]; void DrawFrame(const char* text, bool isBufferAActive){ if (isBufferAActive) { ssd1306_DrawString(0, 0, text, Font_7x10, bufferA); ssd1306_SetDisplayStartLine(bufferB); // 切换到另一个buffer作为前台展示 } else { ssd1306_DrawString(0, 0, text, Font_7x10, bufferB); ssd1306_SetDisplayStartLine(bufferA); } } ``` 上述措施能够有效缓解乃至彻底消除由软件层面引起的滚动条卡顿或跳变情况。然而值得注意的是,实际应用环境中还可能存在其他干扰因素影响最终视觉体验质量——比如电源波动造成的瞬间失真等外部条件限制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值