라이브 방송 위-아래로 스크롤
라이브 방송 시나리오에서 수 많은 스트리머가 제공하는 다양한 비디오 콘텐츠를 위-아래로 스크롤하여 빠르게 탐색하고 자기가 원하는 콘텐츠를 선택하는 것은 사용자에게 좋은 사용 체험을 제공할 수 있습니다. 이 글에서는 Android와 iOS 측의 솔루션을 각각 소개하겠습니다.
iOS 측 라이브 방송 위-아래로 스크롤 솔루션
이러한 시나리오에서 라이브 방송을 전환할 때, 새로운 방에 들어가고 스트림을 가져오는 데 시간이 걸리기 때문에 전후 두 방의 비디오 화면이 연속되지 않을 수 있습니다. 이 문제에 대해 일반적으로 3가지 솔루션이 있으며, 이 글에서는 이 3가지 솔루션을 각각 자세히 설명하겠습니다.
구현 방식 | 사용자 경험 | 리소스 소비 | 구현 로직 |
일반적으로 라이브 방송을 전환할 때 검은 화면이 먼저 나온 후 새로운 화면이 나타납니다. | 추가 리소스 소모 없음 | 간단하고 추가 로직 없음 | |
좀 괜찮습니다.라이브 방송을 전환할 때 해당 스트리머의 고정 플레이스홀더 이미지가 먼저 나온 후 새로운 화면이 나타납니다. | 좀 많습니다.각 스트리머마다 플레이스홀더 이미지를 추가로 저장하고 클라이언트에 로드해야 합니다 | 좀 복잡합니다.전환 전에 플레이스홀더 이미지를 추가로 비동기적으로 로드해야 합니다. | |
최고입니다. 라이브 방송 전환 시 전후 두 스트리머의 화면이 부드럽게 표시됩니다. | 많습니다.목록에서 2개의 스트림을 동시에 가져와야 하며, 라이브 방송에 들어간 후에는 1개의 스트림만 가져와도 됩니다. | 좀 복잡합니다. 여러 인스턴스를 사용하고 다른 인스턴스의 오디오 및 비디오 스트리밍을 제어해야 합니다 |
검은 화면
시스템의 슬라이드 이벤트를 모니터링하여 방 전환 인터페이스를 호출해서 라이브 방송을 전환하며, 새로운 라이브 방송이 로드되기 전에는 내용이 표시되지 않아 짧은 검은 화면이 나타납니다. 설명의 편의를 위해 여기서 검은 화면 시간은 약 1초 정도이며 실제 검은 화면 시간은 네트워크 및 비디오 비트레이트에 따라 영향을 받습니다. 효과는 다음과 같습니다.


방 전환 코드 조각은 다음과 같습니다
let src = TRTCSwitchRoomConfig()// 업무에 따라 해당 방 번호 및 입장 증명서를 생성하며 예시에서는 클라이언트에서 입장 증명서를 생성하지만, 온라인 업무에서는 백엔드에서 받으십시오.src.strRoomId = strRoomIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.switchRoom(src)
플레이스홀더 이미지
시스템의 슬라이드 이벤트를 모니터링하여 방 전환 인터페이스를 호출해서 라이브 방송을 전환하지만, 검은 화면 방식과 달라 이 방식은 각 라이브 방의 플레이스홀더 이미지를 미리 로드해야 하며, 라이브 방송 비디오 스트림이 표시되기 전에 해당 라이브 방의 플레이스홀더 이미지를 표시합니다. 설명의 편의를 위해 여기서 플레이스홀더 이미지 지속 시간은 약 1초 정도이며 구체적인 시간은 네트워크 및 비디오 비트레이트의 영향을 받습니다.
효과 이미지


구현 단계
1. 첫 번째 스트리머의 배경 이미지 설정합니다
bgView = UIImageView(frame: self.view.bounds)// 해당 이미지는 해당 라이브 방송의 플레이스홀더 이미지여야 하며 사전에 업무상에서 받아야 합니다bgView.image = UIImage(named: "1.png")bgView.contentMode = .scaleAspectFillbgView.translatesAutoresizingMaskIntoConstraints = falseself.view.insertSubview(bgView, at: 0)NSLayoutConstraint.activate([bgView.topAnchor.constraint(equalTo: view.topAnchor),bgView.bottomAnchor.constraint(equalTo: view.bottomAnchor),bgView.leadingAnchor.constraint(equalTo: view.leadingAnchor),bgView.trailingAnchor.constraint(equalTo: view.trailingAnchor),])
2. 방송 전환 전에 배경 이미지 전환합니다
DispatchQueue.main.async {UIView.transition(with: self.bgView,duration: 0,options: .transitionCrossDissolve,animations: {// 업무에 따라 해당 플레이스홀더 이미지로 전환합니다self.bgView.image = UIImage(named: strRoomId)}, completion: nil)}let src = TRTCSwitchRoomConfig()// 업무에 따라 해당 방 번호 및 입장 증명서를 생성하며, 예시에서는 클라이언트에서 입장 증명서를 생성하지만, 온라인 업무에서는 백엔드에서 받으십시오.src.strRoomId = strRoomIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.switchRoom(src)
3. 새로운 방송의 첫 번째 비디오 프레임 렌더링이 시작될 때 배경 이미지를 비디오 화면으로 전환합니다.
// 비디오 스트림 가져오기func onUserVideoAvailable(_ userId: String, available: Bool) {if available {trtcCloud.startRemoteView(userId, streamType: .big, view: view)} else {trtcCloud.stopRemoteView(userId, streamType: .big)}}// 첫 번째 비디오 프레임 렌더링이 시작될 때 플레이스홀더 이미지를 배경으로 전환하고 비디오 화면을 표시합니다.func onFirstVideoFrame(_ userId: String, streamType: TRTCVideoStreamType, width: Int32, height: Int32) {// 여기는 배경 이미지와 비디오 렌더링 컨트롤의 전후 순서를 조정하는 곳이고 실제 업무 상황에 따라 조정합니다.self.view.exchangeSubview(at: 1, withSubviewAt: 0)}
듀얼 인스턴스
주의:
이 방안은 비록 효과가 가장 좋고 사용자 체험도 최고이지만, 슬라이드 목록에서 전후 2개의 라이브 방송실의 2개 스트림을 동시에 가져와야 합니다. 사용자가 방송실에 들어갈 때 다음 방송실의 스트림 미리 로딩을 중지할 수 있지만, 전체적으로 트래픽과 비용 소모가 더 많아집니다.
효과 이미지
가장 부드러운 위-아래 슬라이드 효과를 구현하기 위해 2개의 인스턴스를 동시에 사용해야 합니다. 현재 방송실을 시청하면서 다음 방송실의 비디오 화면을 미리 로딩하고, UIPageViewController를 활용하거나 슬라이드 위치에 따라 상하 2개의 비디오 표시 위치를 수동으로 조정하여 자연스럽고 부드러운 전환을 구현합니다. 다음 방송실로 슬라이드하여 시청하는 효과는 아래 왼쪽 예시와 같습니다.




이 방안의 전체적인 로직은 현재 방송실에 들어간 후 즉시 하위 인스턴스를 사용해서 다음 방송실에 진입하고, 다음 방송실의 비디오 스트림을 가져오고UIPageViewController의 다음 Page에 표시하는 것입니다. 새로운 방송실에 들어갈 때 새로운 방송실의 오디오 스트림을 켜면, 이러한 과정을 반복함으로써 가장 부드러운 위-아래 슬라이드 효과를 구현할 수 있습니다. 동시에 과도한 리소스 소모를 방지하기 위해 다음 방송실 시청만 미리 로딩하고, 사용 빈도가 낮은 이전의 방송실 시청은 미리 로딩하지 않으며 전환 시 여전히 검은 화면을 사용합니다. 이전 방송실 시청 효과는 위 오른쪽 예시와 같습니다.
구현 단계
1. 하위 인스턴스 도구 클래스를 정의합니다.
import Foundationimport ObjectiveCimport TXLiteAVSDK_Professional@objc protocol SubCloudHelperDelegate : NSObjectProtocol {@objc optional func onUserVideoAvailableWithSubId(subId: Int, userId: String, available: Bool)}class SubCloudHelper:NSObject,TRTCCloudDelegate {var trtcCloud: TRTCCloud!var subId: Int!weak var delegate : SubCloudHelperDelegate? = nilfunc initWithSubId(subId: Int, trtcIns: TRTCCloud) {self.subId = subIdself.trtcCloud = trtcInsself.trtcCloud.addDelegate(self)}func getCloud()->TRTCCloud {return trtcCloud}func onUserVideoAvailable(_ userId: String, available: Bool) {if self.delegate?.onUserVideoAvailableWithSubId?(subId: subId, userId: userId, available: available) == nil {return}}}
2. 하위 인스턴스를 사용합니다.
let trtcCloud = TRTCCloud()let subCloudHelper = SubCloudHelper()override func viewDidLoad() {super.viewDidLoad()subCloudHelper.initWithSubId(subId: 0, trtcIns: trtcCloud.createSub())subCloudHelper.delegate = self}
3. 전환을 위한 UIPageViewController를 준비합니다.
private var atRoom: Bool = falsevar pageViewController: UIPageViewController!var pageZero: UIViewController!var pageOne: UIViewController!var pageTwo: UIViewController!var pages: [UIViewController] = []var curPageIdx = 0var curIsSub = falsefunc setupPages() {pageViewController = UIPageViewController(transitionStyle: .scroll,navigationOrientation: .vertical)pageViewController.dataSource = selfpageViewController.delegate = selfaddChild(pageViewController)view.addSubview(pageViewController.view)pageViewController.didMove(toParent: self)// pagespageZero = UIViewController()pageZero.view.backgroundColor = .blackpageOne = UIViewController()pageOne.view.backgroundColor = .blackpageTwo = UIViewController()pageTwo.view.backgroundColor = .blackpages = [pageZero, pageOne, pageTwo]pageViewController.setViewControllers([pages[curPageIdx]], direction: .forward, animated: false)}
4. pageViewController의 페이지 전환을 구현합니다.
// 다음/이전 페이지 가져오기func getShowPage(isNext: Bool) -> UIViewController {var newPageIdx = 0if isNext {newPageIdx = curPageIdx + 1} else {newPageIdx = curPageIdx - 1}if newPageIdx >= pages.count {newPageIdx = 0} else if newPageIdx < 0 {newPageIdx = pages.count - 1}return pages[newPageIdx]}extension RtcDuplexVC: UIPageViewControllerDataSource {func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {return getShowPage(isNext: false)}func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {return getShowPage(isNext: true)}}
5. 메인 인스턴스가 방에 들어가고 하위 인스턴스 사용하여 다음 라이브 방송을 미리 로드합니다.
// 메인 인스턴스로 방에 들어가기// 실제 업무에 따라 sdkAppId, roomID, strRoomId, userId 및 userSig를 교체합니다// 예시에서는 클라이언트에서 userSig를 생성하며, 온라인 업무의 경우 백엔드에서 가져오십시오let params = TRTCParams()params.sdkAppId = UInt32(SDKAppID)params.roomId = 0params.strRoomId = strRoomIdLst.first ?? "1"params.userId = userIdparams.role = .anchorparams.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringtrtcCloud.addDelegate(self)trtcCloud.enterRoom(params, appScene: .LIVE)// 하위 인스턴스로 다음 방을 미리 로드합니다// 실제 업무에 따라 sdkAppId, roomID, strRoomId, userId 및 userSig를 교체합니다// 예시에서는 클라이언트 사용하여 userSig를 생성하며 온라인 업무의 경우 백엔드에서 가져오십시오let subParams = TRTCParams()subParams.sdkAppId = UInt32(SDKAppID)subParams.roomId = 0subParams.strRoomId = strRoomIdLst[1]subParams.userId = userIdsubParams.role = .anchorsubParams.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as StringsubCloudHelper.trtcCloud.enterRoom(subParams, appScene: .LIVE)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)
6. 콜백에 따라 비디오 스트림을 가져와 해당 페이지에 렌더링합니다.
func getPageByIdx(isNext: Bool) -> UIViewController {var newPageIdx = curPageIdxif isNext {newPageIdx += 1}if newPageIdx >= pages.count {newPageIdx = 0}return pages[newPageIdx]}extension RtcDuplexVC: TRTCCloudDelegate {func onUserVideoAvailable(_ userId: String, available: Bool) {if available {trtcCloud.startRemoteView(userId, streamType: .big, view: getPageByIdx(isNext: curIsSub).view)} else {trtcCloud.stopRemoteView(userId, streamType: .big)}}}extension RtcDuplexVC: SubCloudHelperDelegate {func onUserVideoAvailableWithSubId(subId: Int, userId: String, available: Bool) {if available {subCloudHelper.trtcCloud.startRemoteView(userId, streamType: .big, view: getPageByIdx(isNext: !curIsSub).view)} else {subCloudHelper.trtcCloud.stopRemoteView(userId, streamType: .big)}}}
7. 새로운 방으로 전환한 후, 미리 로드된 방을 업데이트하거나 위로 스크롤할 때 현재 표시된 방을 업데이트합니다.
func updateCurRoomIdx(isNext: Bool) {if isNext {curRoomIdx += 1if curRoomIdx >= strRoomIdLst.count {curRoomIdx = 0}} else {curRoomIdx -= 1if curRoomIdx < 0 {curRoomIdx = strRoomIdLst.count - 1}}}// 실제 업무 로직에 따라 방 번호를 전환해야 합니다func updateNewRoom(isNext: Bool) {var newRoomIdx = 0if isNext{newRoomIdx = curRoomIdx + 1} else {newRoomIdx = curRoomIdx - 1}if newRoomIdx >= strRoomIdLst.count {newRoomIdx = 0} else if newRoomIdx < 0 {newRoomIdx = strRoomIdLst.count - 1}let newRoomStrId = strRoomIdLst[newRoomIdx]let src = TRTCSwitchRoomConfig()src.strRoomId = newRoomStrIdsrc.userSig = GenerateTestUserSig.genTestUserSig(identifier: userId) as Stringif curIsSub {trtcCloud.switchRoom(src)trtcCloud.muteAllRemoteAudio(true)} else {subCloudHelper.trtcCloud.switchRoom(src)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}}extension RtcDuplexVC: UIPageViewControllerDelegate {func pageViewController(_ pageViewController: UIPageViewController,didFinishAnimating finished: Bool,previousViewControllers: [UIViewController],transitionCompleted completed: Bool) {if completed {guard let currentVC = pageViewController.viewControllers?.first else {return}if let index = pages.firstIndex(of: currentVC) {// 위-아래로 스크롤하는 판단 기준 가져오기let iden = index - curPageIdx// 현재 표시된 페이지의 번호 업데이트하기curPageIdx = index// 아래로 스크롤if iden == 1 || iden == -2 {// 현재 위치한 방의 번호 업데이트하기updateCurRoomIdx(isNext: true)// 현재 표시된 페이지의 인스턴스 업데이트하기curIsSub.toggle()// 방 업데이트하기updateNewRoom(isNext: true)}// 위로 스크롤if iden == -1 || iden == 2 {// 방 업데이트하기updateNewRoom(isNext: false)// 현재 위치한 방의 번호 업데이트하기updateCurRoomIdx(isNext: false)// 현재 표시된 페이지의 인스턴스 업데이트하기curIsSub.toggle()trtcCloud.muteAllRemoteAudio(true)subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}// 현재 방의 음소거 해제하기if curIsSub {subCloudHelper.trtcCloud.muteAllRemoteAudio(false)} else {trtcCloud.muteAllRemoteAudio(false)}}}}}
또한, 업무에서 "슬라이드 목록 내"와 "라이브 방송 진입"이라는 2가지 상태를 구분할 수 있습니다. 위-아래로 슬라이드할 때는 일반적으로 방의 상세 정보나 채팅 등의 정보가 필요하지 않으며, "라이브 방송 진입" 후에야 표시가 필요하기 때문입니다. 이렇게 구분하면 빈번한 위-아래 슬라이드로 인한 라이브 방송 상태 기록 부담을 줄일 수 있을 뿐만 아니라, 사전 로딩으로 인한 자원 소모도 감소시킬 수 있습니다.
구체적인 방법: "슬라이드 목록 내" 상태에서만 위-아래 슬라이드로 라이브 방송 전환이 가능하며, 이때는 라이브 방송 화면과 일부 정보만 표시됩니다. "라이브 방송 진입" 시에는 전체 라이브 방송 및 채팅 등의 정보가 표시되며, 이때는 위-아래 슬라이드로 라이브 방송 전환이 불가능합니다. 또한 "라이브 방송 진입" 시 다음 라이브 방송의 비디오 스트림 사전 로딩이 중지되고, "슬라이드 목록 내" 상태로 돌아갈 때 다음 라이브 방송의 비디오 스트림 사전 로딩이 재개됩니다. 효과는 다음과 같습니다.


구체적인 구현은 다음과 같습니다.
// 라이브 방송 진입 로직의 처리@objc func enterBtnClick() {// 위-아래 스크롤 목록의 UI 컴포넌트의 숨김self.enterBtn.isHidden = true// 라이브 방송 진입 후 UI 컴포넌트의 표시self.exitBtn.isHidden = false// ...self.atRoom = true// 라이브 방송 진입 후 위-아래 스크롤의 금지pageViewController.dataSource = nil// 사전 로드의 중지if curIsSub{trtcCloud.muteAllRemoteVideoStreams(true)} else {subCloudHelper.trtcCloud.muteAllRemoteAudio(true)}}// 라이브 방송 퇴장 로직의 처리@objc func exitBtnClick() {// 라이브 방송 중 UI 컴포넌트의 숨김self.enterBtn.isHidden = false// 위-아래 스크롤 목록의 UI 컴포넌트의 표시self.exitBtn.isHidden = trueself.atRoom = false// 위-아래 스크롤 목록 진입 후 위-아래 스크롤의 복원pageViewController.dataSource = self// 사전 로드의 재개if curIsSub{trtcCloud.muteAllRemoteVideoStreams(false)} else {subCloudHelper.trtcCloud.muteAllRemoteAudio(false)}}
Android 측 라이브 방송 위-아래로 스크롤 솔루션
아래의 듀얼 인스턴스와 싱글 인스턴스 챕터에서, 효과 이미지와 예제 코드에는 세 개의 페이지가 있으며 페이지 순서는 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scroll_switch_room);//제목 표시줄 숨기기getSupportActionBar().hide();mRoomIds = new String[]{"1231", "1232", "1233"};if (checkPermission()) {initView();}}@Overrideprotected void onPermissionGranted() {initView();}private void initView() {mAdapter = new PageAdapter(this, mRoomIds);ViewPager2 viewPager = findViewById(R.id.viewPager);viewPager.setAdapter(mAdapter);//viewPager 슬라이드 방향의 설정viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);// viewPager 프리로드의 설정viewPager.setOffscreenPageLimit(1);// 페이지 전환 리스너의 추가viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {public void onPageSelected(int position) {Log.d("ScrollSwitchRoom", "onPageSelected: " + position);if (mCurPos == position) {return;}RecyclerView recyclerViewImpl = (RecyclerView) viewPager.getChildAt(0);// 현재 방에서 먼저 나가기exitRoom();// 다음 방으로 들어가기View itemView = recyclerViewImpl.getChildAt(position);mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);enterRoom(position);mCurPos = position;}});// Initialize your views heremTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext());mTRTCCloud.addListener(mTRTCCloudListener);}private void enterRoom(int roomIdIndex) {TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = GenerateTestUserSig.SDKAPPID;mTRTCParams.userId = "123";mTRTCParams.strRoomId = mRoomIds[roomIdIndex];mTRTCParams.userSig = GenerateTestUserSig.genTestUserSig(mTRTCParams.userId);mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);}private TRTCCloudListener mTRTCCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mTRTCCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mRemoteVideoView);} else {mTRTCCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}public void onError(int errCode, String errMsg, Bundle extraInfo) {// print ErrorLog.e("ScrollSwitchRoom", "Error: " + errCode + " " + errMsg);}};private void exitRoom() {mTRTCCloud.stopAllRemoteView();mTRTCCloud.exitRoom();// mTRTCCloud.setListener(null);}public class PageAdapter extends RecyclerView.Adapter<PageAdapter.PageViewHolder> {private Context context;public String[] mRoomIds;public PageAdapter(Context context, String[] roomIds) {this.context = context;this.mRoomIds = roomIds;}public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.item_scroll_page, parent, false);return new PageViewHolder(view);}public void onBindViewHolder(@NonNull PageViewHolder holder, int position) {TextView textView = holder.itemView.findViewById(R.id.tv_room_number);textView.setText(getString(R.string.switchroom_roomid) + ":" + mRoomIds[position]);}public int getItemCount() {return mRoomIds.length;}public int getItemViewType(int position) {return position;}class PageViewHolder extends RecyclerView.ViewHolder {PageViewHolder(@NonNull View itemView) {super(itemView);}}}@Overrideprotected void onDestroy() {super.onDestroy();exitRoom();}}
activity_scroll_switch_room.xml은 다음과 같습니다.
<?xml version="1.0" encoding="utf-8"?><androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent" />
item_scroll_page.xml은 다음과 같습니다.
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/item_layout"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/iv_placeholder"android:scaleType="centerCrop"android:background="@drawable/placeholder_img"android:layout_width="match_parent"android:layout_height="match_parent"/><com.tencent.rtmp.ui.TXCloudVideoViewandroid:id="@+id/txcvv_main_local"android:layout_width="match_parent"android:layout_height="match_parent" /><TextViewandroid:id="@+id/tv_room_number"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintEnd_toEndOf="@id/item_layout"app:layout_constraintStart_toStartOf="@id/item_layout" /></androidx.constraintlayout.widget.ConstraintLayout>
듀얼 인스턴스
라이브 방송 목록을 위-아래로 스크롤하면 두 개의 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scroll_switch_room);//제목 표시줄 숨기기getSupportActionBar().hide();mRoomIds = new String[]{"1231", "1232", "1233"};if (checkPermission()) {initView();}}@Overrideprotected void onPermissionGranted() {initView();}private void initView() {mAdapter = new PageAdapter(this,mRoomIds);ViewPager2 viewPager = findViewById(R.id.viewPager);viewPager.setAdapter(mAdapter);//viewPager 슬라이드 방향의 설정viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);// viewPager 프리로드의 설정viewPager.setOffscreenPageLimit(1);// 페이지 전환 리스너의 추가viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {public void onPageSelected(int position) {Log.d("ScrollSwitchRoom", "onPageSelected: " + position);RecyclerView recyclerViewImpl = (RecyclerView) viewPager.getChildAt(0);if (mIsInMainRoom == null) {View itemView = recyclerViewImpl.getChildAt(position);mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);View subItemView = recyclerViewImpl.getChildAt(position + 1);mSubRemoteVideoView = subItemView.findViewById(R.id.txcvv_main_local);enterRoom();mIsInMainRoom = true;} else {if (mIsInMainRoom) {mTRTCCloud.muteAllRemoteAudio(true);mSubCloud.muteAllRemoteAudio(false);} else {mTRTCCloud.muteAllRemoteAudio(false);mSubCloud.muteAllRemoteAudio(true);}if (position != (mRoomIds.length - 1)) {String roomId;TRTCCloud trtcCloud;if (mCurPos < position) {// 화면을 위로 슬라이드roomId = mRoomIds[position + 1];trtcCloud = mIsInMainRoom ? mTRTCCloud : mSubCloud;View itemView = recyclerViewImpl.getChildAt(position + 1);if (mIsInMainRoom) {mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);} else {mSubRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);}} else {//화면을 아래로 슬라이드roomId = mRoomIds[position];trtcCloud = mIsInMainRoom ? mSubCloud : mTRTCCloud;View itemView = recyclerViewImpl.getChildAt(position);if (mIsInMainRoom) {mSubRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);} else {mRemoteVideoView = itemView.findViewById(R.id.txcvv_main_local);}}switchRoom(roomId, trtcCloud);}mIsInMainRoom = !mIsInMainRoom;}mCurPos = position;}});// Initialize your views heremTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext());mSubCloud = mTRTCCloud.createSubCloud();}/*** 초기화 시에만 방 입장을 호출하고 이후 방을 전환할 때는 switchRoom을 호출합니다.*/private void enterRoom() {mTRTCCloud.addListener(mTRTCCloudListener);mSubCloud.addListener(mSubCloudListener);TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = GenerateTestUserSig.SDKAPPID;mTRTCParams.userId = "123";// mTRTCParams.roomId = Integer.parseInt(roomId);mTRTCParams.strRoomId = mRoomIds[0];mTRTCParams.userSig = GenerateTestUserSig.genTestUserSig(mTRTCParams.userId);mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);mTRTCParams.strRoomId = mRoomIds[1];mSubCloud.muteAllRemoteAudio(true);mSubCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);}private TRTCCloudListener mTRTCCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mTRTCCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mRemoteVideoView);} else {mTRTCCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}public void onError(int errCode, String errMsg, Bundle extraInfo) {// print ErrorLog.e("ScrollSwitchRoom", "Error: " + errCode + " " + errMsg);}public void onSwitchRoom(long err, String errMsg) {// Switch room}};private TRTCCloudListener mSubCloudListener = new TRTCCloudListener() {public void onEnterRoom(long result) {if (result == 0) {// Enter room success} else {// Enter room failed}}public void onExitRoom(int reason) {// Exit room}@Overridepublic void onUserVideoAvailable(String userId, boolean available) {super.onUserVideoAvailable(userId, available);if (available) {mSubCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, mSubRemoteVideoView);} else {mSubCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);}}};private void exitRoom() {mTRTCCloud.stopAllRemoteView();mTRTCCloud.exitRoom();mTRTCCloud.setListener(null);mSubCloud.stopAllRemoteView();mSubCloud.exitRoom();mSubCloud.setListener(null);}private void switchRoom(String roomId, TRTCCloud trtcCloud) {TRTCCloudDef.TRTCSwitchRoomConfig config = new TRTCCloudDef.TRTCSwitchRoomConfig();// config.roomId = Integer.parseInt(roomId);config.strRoomId = roomId;trtcCloud.switchRoom(config);}public class PageAdapter extends RecyclerView.Adapter<PageAdapter.PageViewHolder> {private Context context;public String[] mRoomIds;public PageAdapter(Context context, String[] roomIds) {this.context = context;this.mRoomIds = roomIds;}public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.item_scroll_page, parent, false);return new PageViewHolder(view);}public void onBindViewHolder(@NonNull PageViewHolder holder, int position) {TextView textView = holder.itemView.findViewById(R.id.tv_room_number);textView.setText(getString(R.string.switchroom_roomid) + ":" + mRoomIds[position]);}public int getItemCount() {return mRoomIds.length;}public int getItemViewType(int position) {return position;}class PageViewHolder extends RecyclerView.ViewHolder {PageViewHolder(@NonNull View itemView) {super(itemView);}}}@Overrideprotected void onDestroy() {super.onDestroy();exitRoom();}}