跳轉到

iOS 自訂螢幕佈局

arctos links iOS SDK(後續簡稱 SDK)支援自訂螢幕佈局。

本文件並不會講解一般的 User Interface 介面要怎麼實作,而是著重在提供 SDK 對外所開放的元件與相關流程的運作方式,目的在幫助開發者盡可能地理解該如何透過 SDK 來實現自己想要的呈現的模板介面。

物件介紹與名詞解釋

SDK 除了開放原本的 ArctosLinks 系列的 API 之外,提供額外四個物件作為處理自動連線機制,同時開放這些元件的部分存取權讓開發者可以建立客製化模板,分別為

  • MeetingRoomInfoManager

  • MeetingMemberInfo

  • ConnectInfo

  • LiveStreamView

以下為它們的關係圖:

主持端

Host

參與端

Participant

其中,有虛線的部分,代表該物件不一定存在,會隨著使用情境而產生、或消失,例如:

  • Participants 內的所有 MeetingMemberInfo 是根據參與者加入會議室的行為而產生,離開而消失。

  • MeetingMemberInfo 內的 ConnectInfo 是在各成員進入會議室之後,開始互相連線後,才會產生。

紅色箭頭部分則代表著,從 MeetingMemberInfo 中取得其內部的 ConnectInfo,並綁定在 LiveStreamView 上的過程。

MeetingRoomInfoManager

用於管理會議室相關資訊用物件,設計模式屬於 Singleton Pattern,透過調用 SDK 會議室相關 API,會自動更新會議室內的相關細節,無需開發者自行管理,僅需在特定的流程場合下取用本物件即可。

MeetingRoomInfoManager 包含了以下屬性:

class MeetingRoomInfoManager {
    public static let shared = MeetingRoomInfoManager()

    /// 會議室 ID
    let roomID: String
    let roomID_Subject: BehaviorSubject<String>

    /// 會議室名稱
    let name: String
    let name_Subject: BehaviorSubject<String>

    /// 會議室描述
    let description: String
    let description_Subject: BehaviorSubject<String>

    /// 主持人成員
    let hostInfo: MeetingMemberInfo?

    /// 參與者成員(們)
    let participants: [MeetingMemberInfo]
    let participants_Subject: BehaviorSubject<[MeetingMemberInfo]>
}
其中,可以注意到某些屬性中會帶有關鍵字 _Subject,其用途可透過 RxSwift 語法協助開發者監聽該屬性的變化。

例如:我可能需要對會議室參與人數進行監聽,就可以使用以下方式:

class SampleVC: UIVIewController {
    @IBOutlet weak var tableView: UITableView!
    private var disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        /// Observe something you want 
        _ = MeetingRoomInfoManager.shared.participants_Subject
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { participants in

                // This closure triggered when number of participants changed, handle them than refresh tableView
                tableView.reloadData()

            }).disposed(by: disposeBag)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        /// Stop observe
        disposeBag = DisposeBag()
    }
}

詳情參閱 iOS SDK API 說明文件

MeetingMemberInfo

代表會議中成員的物件,依附在 MeetingRoomInfoManager 內,可在特定場合內直接取用,(參閱 連線流程與物件取用時機點),根據成員型態可分為兩類:

  1. hostInfo 主持端
  2. participant 參與端

MeetingMemberInfo 包含了以下屬性:

class MeetingMemberInfo: NSObject {
    /// cms 專屬 UserId
    let cmsUID: Int

    /// 是否為自己,會議室成員人數眾多,可透過此屬性判別是否為自己
    let isSelf: Bool

    /// 該成員為什麼樣的角色,host or participant
    let role: ArctosLinks.Conference.UserIdentity
    let role_Subject: BehaviorSubject<ArctosLinks.Conference.UserIdentity>

    /// 成員名稱
    let displayName: String
    let displayName_Subject: BehaviorSubject<String>

    /// 連線狀態
    let isOnline: Bool
    let isOnline_Subject: BehaviorSubject<Bool>

    /// 連線中可額外取的詳細內容
    let connectInfo: ConnectInfo?
    let connectInfo_Subject: BehaviorSubject<ConnectInfo?>
}

!!! warning "注意事項" connectInfo 在取得 MeetingMemberInfo 的當下不一定會存在,connectInfo 是通過 SDK 內部的自動連線機制所管理,因此建議透過監聽 connectInfo_Subject 的方式處理,以確保連線完成後外部可以收到產生 connectInfo 的通知。

ConnectInfo

ConnectInfo 存在於 MeetingMemberInfo 屬性內,表示該會議成員在連線時的各種狀態、同時也提供部分設定提供給開發者調整。

ConnectInfo 包含了以下屬性與方法:

class ConnectInfo {
    // MARK: == Video == 

    /// 影像源每秒接收量 FPS 
    let videoReceiveFrameRate: UInt
    let videoReceiveFrameRate_Subject: BehaviorSubject<UInt>

    /// 影像源接收解析度
    let videoReceiveResolution: CGSize
    let videoReceiveResolution_Subject: BehaviorSubject<CGSize>

    /// 取得繪圖模式
    /// 寬高比 (aspectFill):透過裁切溢位部分來填滿視圖邊界。
    /// 寬高比 (aspectFit):適合邊界,保持寬高比,多餘留黑。
    let videoRenderMode: VideoRenderMode
    let videoRenderMode_Subject: BehaviorSubject<ConnectInfo.VideoRenderMode>
    /// 設定繪圖模式,可在連線過程中調整,將在下一張影像來源送達後生效
    func videoRenderMode(VideoRenderMode)

    /// 取得是否開啟繪圖,預設為 true = 開啟,false = 關閉(呈現黑畫面)
    let enableVideoRender: Bool
    let enableVideoRender_Subject: BehaviorSubject<Bool>
    /// 是否開啟繪圖
    func enableVideoRender(Bool)

    // MARK: == Audio ==

    /// 聲音源每秒接收量 FPS
    let audioReceiveFrameRate: UInt
    let audioReceiveFrameRate_Subject: BehaviorSubject<UInt>

    /// 聲音源每秒發送量 FPS
    let audioSendFrameRate: UInt
    let audioSendFrameRate_Subject: BehaviorSubject<UInt>

    /// 成員發言音量偵測,可透過監聽得知該成員是否正在說話,true 為偵測到說話、反之為 false
    let audioSpotlight_Subject: BehaviorSubject<Bool>

    /// 取得目前是否阻斷接收聲音源
    let enableAudioPlay: Bool
    let enableAudioPlay_Subject: BehaviorSubject<Bool>
    /// 是否開啟阻斷接收聲音源,阻斷聲音源,表示不處理該成員傳來的聲音資料,並不代表關閉揚聲器功能
    func enableAudioPlay(Bool)

    // MARK: == Bandwidth ==

    /// 本地端接資料接收量,單位 Bytes
    let localRecvBytes: UInt
    let localRecvBytes_Subject: BehaviorSubject<UInt>

    /// 本地端接資料發送量,單位 Bytes
    let localSentBytes: UInt
    let localSentBytes_Subject: BehaviorSubject<UInt>

    /// 本地端接資料遺失量,單位 Bytes
    let localLostBytes: UInt
    let localLostBytes_Subject: BehaviorSubject<UInt>

    /// 本地端,單位統計起始時間,單位 ms
    let localStartTimestamp_ms: UInt
    let localStartTimestamp_ms_Subject: BehaviorSubject<UInt>

    /// 遠端接資料接收量,單位 Bytes
    let remoteRecvBytes: UInt
    let remoteRecvBytes_Subject: BehaviorSubject<UInt>

    /// 遠端接資料發送量,單位 Bytes
    let remoteSentBytes: UInt
    let remoteSentBytes_Subject: BehaviorSubject<UInt>

    /// 遠端接資料遺失量,單位 Bytes
    let remoteLostBytes: UInt
    let remoteLostBytes_Subject: BehaviorSubject<UInt>

    /// 遠端,單位統計起始時間,單位 ms
    let remoteStartTimestamp_ms: UInt
    let remoteStartTimestamp_ms_Subject: BehaviorSubject<UInt>
}

詳情可參閱我司提供的另一份 API(ArctosLinks.doccarchive)說明文件

另外,您可能會發現 ConnectInfo 並沒有提供 video 的 fps 發送統計(videoSendFrameRate), 但卻有接受統計(videoReceiveFrameRate),這是因為對主持端來說,它作為所有成員的整合端,將整合後影像源一次性地發送給其他連線中的成員,因此在統計上是無法將此參數分散至各成員的 ConnectInfo 內;而對參與端來說,它僅針對主持端連線,對其他成員來說並不會傳輸任何資料,因此也沒有必要統計 videoSendFrameRate 並設計在 ConnectInfo 內。

但如果您還是想得知主持端能夠發送的 FPS 數量,SDK 還是有提供 API 能夠取用,您可以從以下 Sample Code 來得知 video 的 fps 發送量:

_ = ArctosLinks.ConferenceEventCallback.videoSendFrameRate
    .observe(on: MainScheduler.instance)
    .subscribe(onNext: { [weak self] fps in
        guard let self else { return }
        // Set fps value to your UI object.
        videoSendFpsLabel.text = "\(fps)fps"
    }).disposed(by: disposeBag)

LiveStreamView

LiveStreamView 是一個需要綁定 connectInfo 的專用繪圖元件,也是客製化會議室介面最重要的元件。

當取得指定成員的 connectInfo,並注入在 LiveStreamViewLiveStreamView 將根據開發者的設定,自動執行影像繪圖;相對地,若 connectInfo 為 nil,相當於解除 LiveStreamViewconnectInfo 兩者之間的關係,LiveStreamView 就沒有影像來源可以繪圖,而舊有的殘留畫面將會自動清除,留下黑畫面。

LiveStreamView 包含了以下屬性:

class LiveStreamView: MTKView {
    /// 基本宣告
    init(frame: CGRect)

    /// 可注入綁定的 connectInfo 內容,設置 nil 將解除綁定
    var connectInfo: ConnectInfo?
}
使用方式參閱 LiveStreamView 使用方法

LiveStreamView 使用方法

SDK 所提供的 LiveStreamView 採用 UIKit 的原生做法,分別支持 CodeViewStoryboard、Xib,分別詳細介紹。

CodeView

以下為 LiveStreamViewconnectInfo 綁定 Sample Code:

主持端

主持端需要處理不同參與者的連線,因此會需要用到許多 LiveStreamView,同時您可能需要通過一些方式去辨別各自的 LiveStreamView 分別對應於哪一個參與者,以 Sample Code 的案例來說,則是根據 LiveStreamViewtag 屬性對應參與者的 cmsUID

import UIKit
import RxSwift
import ArctosLinks

class RoomVC: UIViewController {
    /// 宣告各會議室人員所需的 LiveStreamView
    private lazy var hostLiveStreamView = LiveStreamView(frame: CGRect(origin: .zero, size: CGSize(width: 160, height: 90)))
    private lazy var liveStreamView2 = LiveStreamView(frame: CGRect(origin: .zero, size: CGSize(width: 160, height: 90)))
    private lazy var liveStreamView3 = LiveStreamView(frame: CGRect(origin: .zero, size: CGSize(width: 160, height: 90)))
    private lazy var liveStreamView4 = LiveStreamView(frame: CGRect(origin: .zero, size: CGSize(width: 160, height: 90)))

    private var disposeBag = DisposeBag()
    private var connectDisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // !!!: Important, don't forget to set your merge view to SDK at the first. 
        ArctosLinks.Media.setHostSourceView(self.view) { result in
            if case .failure(let error) = result {
                print("Failed to set host view: \(error.localizedDescription)")
            }
        }

        // Set frame of hostLiveStreamView to self.view
        hostLiveStreamView.frame = CGRect(x: 0, y: 0, width: 160, height: 90)
        self.view.addSubview(hostLiveStreamView)

        // Or you can also using constraint to self.view
        hostLiveStreamView.translatesAutoresizingMaskIntoConstraints = true
        hostLiveStreamView.autoresizingMask = []
        self.view.addSubview(hostLiveStreamView)
        self.view.topAnchor.constraint(equalTo: hostLiveStreamView.topAnchor).isActive = true
        self.view.rightAnchor.constraint(equalTo: hostLiveStreamView.rightAnchor).isActive = true
        self.view.leftAnchor.constraint(equalTo: hostLiveStreamView.leftAnchor).isActive = true
        self.view.bottomAnchor.constraint(equalTo: hostLiveStreamView.bottomAnchor).isActive = true

        // Get connectInfo from host.
        let hostInfo = MeetingRoomInfoManager.shared.hostInfo
        _ = hostInfo?.connectInfo_Subject
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] connectInfo in
                guard let self else { return }
                // Callback object type of connectInfo is `ConnectInfo?`.
                // Set connectInfo to hostLiveStreamView.
                hostLiveStreamView.connectInfo = connectInfo
            }).disposed(by: disposeBag)


        // Get MeetingMemberInfo from connecting participants.
        _ = MeetingRoomInfoManager.shared.participants_Subject
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] participants in
                guard let self else { return }

                // Callback object type of participants is [MeetingMemberInfo].

                // Stop observe all participants
                connectDisposeBag = DisposeBag()

                let streamViewArray = [liveStreamView2, liveStreamView3, liveStreamView4]

                // Number of participant
                participants.forEach { memberInfo in

                    // Search view tag which is same as cmsUID
                    guard let streamView = streamViewArray.first(where: { $0.tag == memberInfo.cmsUID }) else {
                        // Set cmsUID at first time.
                        if let first = streamViewArray.first(where: { $0.tag != memberInfo.cmsUID }) {
                            first.tag = memberInfo.cmsUID
                            self.view.addSubview(first)

                            // Observe connectInfo
                            _ = memberInfo.connectInfo_Subject
                                .observe(on: MainScheduler.instance)
                                .subscribe(onNext: { connectInfo in
                                    first.connectInfo = connectInfo
                                }).disposed(by: self.connectDisposeBag)
                        }
                        return
                    }

                    self.view.addSubview(streamView)

                    // Observe connectInfo
                    _ = memberInfo.connectInfo_Subject
                        .observe(on: MainScheduler.instance)
                        .subscribe(onNext: { connectInfo in
                            streamView.connectInfo = connectInfo
                        }).disposed(by: self.connectDisposeBag)
                }
            }).disposed(by: disposeBag)
    }
}

注意事項

主持端除了需要顯示各自成員的串流內容外,也必須有一個畫面整合用的 View 元件(簡稱 SourceView)作為基底,提供給 SDK 做整合輸出,參考 API 設定串流視圖位置,主持端的 SDK 會自動將 SourceView 下的所有元件(subView)做截頻整合發送,也就是說,如果你想要將所有的成員內容都呈現出來,開發者必須將所有的 LiveStreamView 都作為 SourceView 下的 subView,這樣截頻下來的內容才會包含所有的成員內容。除此之外,還必須留意一個限制,若您想將 AVPlayer、WKWebView 相關的元件作為 SourceView 的 subView,他們的影片播放功能是無法取得畫面的,此為官方 API 限制,這點請留意。

參與端

每個參與端都只需處理主持端的連線,因此只會用到一個 LiveStreamView,而 HostInfo 內所取得的 connectInfo,即為來自主持端整合後的畫面,Sample Code 如下:

import UIKit
import RxSwift
import ArctosLinks

class RoomVC: UIViewController {
    /// 只會有 Host StreamView
    private lazy var hostLiveStreamView = LiveStreamView(frame: CGRect(origin: .zero, size: CGSize(width: 160, height: 90)))

    private var disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set frame of hostLiveStreamView to self.view
        hostLiveStreamView.frame = CGRect(x: 0, y: 0, width: 160, height: 90)
        self.view.addSubview(hostLiveStreamView)

        // Get connectInfo from host.
        let hostInfo = MeetingRoomInfoManager.shared.hostInfo
        _ = hostInfo?.connectInfo_Subject
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] connectInfo in
                guard let self else { return }
                // Callback object type of connectInfo is `ConnectInfo?`.
                // Set connectInfo to hostLiveStreamView.
                hostLiveStreamView.connectInfo = connectInfo
            }).disposed(by: disposeBag)
    }
}

Storyboard or Xib

以下為使用 Xib 構建 CustomLiveStreamView.xib 的案例,而 Storyboard 使用方式雷同就不多提,其中 Subview 元件包含一個命名為 Live Stream View 的 UIView(左側選定綠色反白),只要將 Custom Class 的部分 -> Class 欄位指定為 LiveStreamViewModule 欄位指定為 ArctosLinks 即可,如圖(右上角橢圓紅框部分):

Xib

Xib 對應 CustomLiveStreamView.swift 代碼:

import UIKit
import ArctosLinks
import RxSwift

class CustomLiveStreamView: UIView {
    @IBOutlet weak var liveStreamView: LiveStreamView!
    private var disposeBag = DisposeBag()

    deinit {
        disposeBag = DisposeBag()
    }

    class func instantiateFromNib() -> CustomLiveStreamView {
        guard let view = UINib(nibName: String(describing: Self.self), bundle: nil).instantiate(withOwner: nil).first as? CustomLiveStreamView else { return CustomLiveStreamView() }
        return view
    }

    /// 注意用 weak 處理 meetingMemberInfo,避免 retain cycle
    weak var meetingMemberInfo: MeetingMemberInfo? {
        didSet {
            synchronized(self) {
                disposeBag = DisposeBag()

                guard let meetingMemberInfo else { return }

                // 訂閱連線資訊
                _ = meetingMemberInfo.connectInfo_Subject
                    .observe(on: MainScheduler.instance)
                    .subscribe(onNext: { [weak self] connectInfo in
                        guard let self else { return }
                        liveStreamView.connectInfo = connectInfo
                    }).disposed(by: disposeBag)
            }
        }
    }
}

在 VC 內使用 CustomLiveStreamView:

class RoomVC: UIViewController {
    private lazy var streamView = CustomLiveStreamView.instantiateFromNib()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set frame of streamView to self.view
        streamView.frame = CGRect(x: 0, y: 0, width: 160, height: 90)
        self.view.addSubview(streamView)

        // Get meetingMemberInfo of host.
        let hostInfo = MeetingRoomInfoManager.shared.hostInfo
        streamView.meetingMemberInfo = hostInfo
    }
}

連線流程與物件取用時機點

本章節重點在介紹整體的連線流程與後續會發生的行為,以便讓開發者能理解什麼時候該從 SDK 取用對應的相關物件,並做出相應的操作。

連線流程

MeetingRoomInfoManager 物件負責管理會議室內的所有內容,所以了解該物件在什麼時候最做出相應的更新,並在什麼時機點取得很重要。

關於 SDK 進入會議室的相關流程,根據角色不同,會有不同對應 API、EventCallback,以下為 主持端 Host參與端 Participant 的基本流程分析,開發者們可以參照下方所提供的流程說明、調用 API、UI 轉場切換、監聽 EventCallback 切入點排序,深入理解會議室操作的行為。

主持端 Host 流程

- 從列表選擇會議室並加入 - ArctosLinks.Conference.joinRoom ✅

    - 進入會議室(UI轉場)- viewDidLoad ✅

        - 取得會議室資訊 - ArctosLinks.Conference.getRoomInfo ✅

        - 同意/取消申請端加入 - ArctosLinks.Conference.hostJoinHandle ✅

        - 請離參與者 - ArctosLinks.Conference.hostKickParticipant

        - 離開會議室 - ArctosLinks.Conference.leaveRoom

            - 會議室頁面關閉(UI轉場)- viewWillDisappear

        - 監聽事件

            - 會議室內容更新 - ArctosLinks.ConferenceEventCallback.roomInfoUpdated ✅

            - 申請端申請加入會議室 - ArctosLinks.ConferenceEventCallback.requestJoinRoom

            - 申請端取消加入會議室 - ArctosLinks.ConferenceEventCallback.cancelJoinRequest

            - 有參與端加入會議室 - ArctosLinks.ConferenceEventCallback.participantJoinRoom

            - 與參與端的連線狀態 - ArctosLinks.ConferenceEventCallback.updateParticipant

            - 有參與端離開會議室 - ArctosLinks.ConferenceEventCallback.participantLeftRoom

其中,在流程中會看見綠色勾勾✅,即為主要 MeetingRoomInfoManager 更新資料內容的地方,開發者可以從這些切入點著手,取得 MeetingRoomInfoManager 下所提供的相關物件,建立屬於自己的模板介面。

至於其他的切入點,在通常的情況下,您不需要進行額外的處理,SDK 會協助您自動管理,只要您在取用物件後並使用得當,確保您的資源在會議室流程結束後能有效釋放,就不會有其他問題產生。(參閱注意事項

參與端 Participant 流程

- 從列表選擇會議室並申請 - ArctosLinks.Conference.joinRoom

- 監聽專用事件

    - 主持端同意 - ArctosLinks.ConferenceEventCallback.responseJoinRoom ✅

        - 進入會議室(UI轉場)- viewDidLoad ✅

            - 取得會議室資訊 - ArctosLinks.Conference.getRoomInfo ✅

            - 離開會議室 - ArctosLinks.Conference.leaveRoom

                - 會議室頁面關閉(UI轉場)- viewWillDisappear

            - 監聽事件

                - 會議室內容更新 - ArctosLinks.ConferenceEventCallback.roomInfoUpdated ✅

                - 與主持端連線狀態 - ArctosLinks.ConferenceEventCallback.updateHost

                - 被主持端請離會議室 - ArctosLinks.ConferenceEventCallback.kickOutByHost

                    - 會議室頁面關閉(UI轉場)- viewWillDisappear

                - 主持端已關閉會議室 - ArctosLinks.ConferenceEventCallback.hostCloseRoom

                - 其他參與端離開會議室 - ArctosLinks.ConferenceEventCallback.participantKickedOutRoom

                - 參與端加入會議室 - ArctosLinks.ConferenceEventCallback.participantJoinRoom

                - 有參與端離開會議室 - ArctosLinks.ConferenceEventCallback.participantLeftRoom

參與端 Participant 流程 下,要額外注意的是,在 MeetingRoomInfoManager 的取得過程中,雖然可取得所有的成員物件 MeetingMemberInfo,但實際上只有 hostInfo 存在 connectInfo 可供 LiveStreamView 綁定,而 participants 內將不存在任何的 connectInfo

class MeetingRoomInfoManager {
    ... 以上略 ...

    /// 主持人成員
    let hostInfo: MeetingMemberInfo?  <= 內部存在 connectInfo,且為畫面整合後的內容

    /// 參與者成員(們)
    let participants: [MeetingMemberInfo] <= 不存在 connectInfo 
}

注意事項

GPU 繪圖最大限制

SDK 所提供的 LiveStreamView 物件,雖採用 iOS 原生、且繪圖效率最高的 Metal GPU 繪圖,但在管理上還是有相關條件限制,以下為該注意的條件:

  1. iOS 設備的 Metal GPU 繪圖,其最大限制為每秒鐘可繪圖 120 次,也就是說,單個 LiveStreamView 繪圖理論上可以支持到 120 fps。

  2. 當會議室內同時有多人進行視訊,一般就會多個 LiveStreamView 在同時進行繪圖,而這些元件實際上是同時共享 120 fps 的 GPU 資源。例如:會議室內有四個成員,每道影像來源都有 30 fps 張數正在繪圖,那麼 GPU 資源將會被佔滿(4 個 LiveStreamView * 30 fps = 120 fps)。

  3. 當 GPU 資源 120 fps 被佔滿時,還需要額外的 LiveStreamView 繪製其他的影像源,開發者就必須做出對應的取捨,否則將影響到畫面呈現的流暢度,有幾個建議供開發者進行處理:

    • 降低各別影像源的繪圖頻率、影像傳輸率,例如:在一個頁面上可能要同時呈現 8 個 LiveStreamView 的繪圖量,平均每一個 LiveStreamView 的繪圖量就必須控制在 15 fps 左右(120 / 8 = 15)

    • ConnectInfo有額外提供接口 public func enableVideoRender(_ enable: Bool),讓開發者自行開關控制,預設為 true 可繪圖狀態,設定 false 為自動屏蔽繪圖功能以節省 GPU 繪圖資源。

Retain Cycle、Memory Leak 的產生

MeetingMemberInfoConnectInfo 兩者皆為繼承 Class 物件,同時也由 SDK 內部自動進行管理,當 SDK 外部開發者需取用相關資源暫存使用時,建議使用 weak 修飾符作為變數儲存,因為 SDK 內部自動管理 MeetingMemberInfoConnectInfo 元件時,較不易收到干擾,導致預期外的行為,例如 Retain Cycle 所造成的 Memory Leak。