LiveCoreView 是一个跨平台的视频直播核心组件,提供开播、观看、连麦和主播 PK 等关键能力。它通过“挂件”机制,支持在视频区域实时显示用户名、等级、PK 进度条等自定义信息。本文档将指导您如何通过实现代理协议,在 iOS 平台上快速定制专属的视频挂件 UI。效果展示
连线视频挂件 | PK 视频挂件 |
![]() |
![]() |
准备工作
核心原理
LiveCoreView 通过 VideoViewDelegate 代理支持自定义视图渲染。当业务场景发生变化(例如有人上麦、开始 PK)时,LiveCoreView 会调用代理方法询问应显示的视图。实现对应的代理方法并返回自定义的 UIView 实例即可。方法 | 描述 | 对应业务场景 |
createCoGuestView | 创建观众连麦的挂件视图 | 观众连麦、邀请上麦 |
createCoHostView | 创建跨房连麦(主播连线)的挂件视图 | 主播连线 |
createBattleView | 创建 PK 场景中单个用户的挂件视图(例如头像、分数) | 主播 PK |
createBattleContainerView | 创建 PK 场景的整体容器视图(例如背景、分数 PK 条) | 主播 PK |
调整连线场景挂件
当观众与主播进行连线或者主播间跨房连线时,直播间界面会从单人模式切换为多人布局。此时,需要在连线的视频窗口上展示特定的用户信息,例如昵称、等级或“静音”状态提示,以便区分不同麦位。
适用场景
主播和观众视频连麦时,修改麦位上的用户信息(例如昵称、等级、静音图标)。
主播与主播跨房连线时,修改对方主播的显示样式(例如昵称、等级、静音图标)。
修改无视频画面时的默认背景(例如占位头像)。
修改空麦位时的视图实现。
视图层级示意

实现步骤
步骤1:准备自定义挂件 View
定义三个基础视图类,分别用于展示用户信息、空麦位提示和无视频流时的背景。
创建自定义的用户信息视图:
说明:
import UIKitclass CustomInfoView: UIView {let nameLabel = UILabel()let muteIcon = UIImageView(image: UIImage(named: "mute_icon"))init(name: String, isMuted: Bool) {super.init(frame: .zero)nameLabel.text = namemuteIcon.isHidden = !isMuted}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}// ... layout code ...}
创建自定义的空麦位视图:
说明:
import UIKitclass EmptySeatView: UIView {let addIcon = UIImageView(image: UIImage(named: "add_icon"))let addLabel = UILabel()// ... layout code ...}
创建自定义无视频流时的头像占位视图:
说明:
import UIKitclass CustomAvatarView: UIView {let avatarView = UIImageView(frame: .zero)init(avatarURL: String) {super.init(frame: .zero)// load avatar URL}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}// ... layout code ...}
步骤2:实现代理逻辑
创建一个代理类(或在现有
ViewController 中扩展),实现VideoViewDelegate的方法 createCoGuestView(观众连线)和 createCoHostView(主播连线),返回刚才创建的自定义视图。实现
VideoViewDelegate 中的 createCoGuestView 方法,返回观众连线视频挂件。class VideoWidgetProvider: VideoViewDelegate {/// seatInfo: 麦位数据 (用户信息、音视频状态)/// viewLayer: 视图层级 (.foreground 前景视图 / .background 背景视图)func createCoGuestView(seatInfo: SeatInfo, viewLayer: ViewLayer) -> UIView? {let isUserOnSeat = !seatInfo.userInfo.userID.isEmptyswitch viewLayer {case .foreground:if isUserOnSeat {// 非空麦位,返回自定义的前景视图let widget = CustomInfoView(name: seatInfo.userInfo.userName,isMuted: seatInfo.userInfo.microphoneStatus == .off)return widget}// 空麦位,返回自定义空麦位视图return EmptySeatView()case .background:if isUserOnSeat {// 返回自定义的背景视图(未开摄像头时显示)let bgView = CustomAvatarView(avatarURL: seatInfo.userInfo.avatarURL)return bgView}return nil}}}
实现
VideoViewDelegate 中的 createCoHostView 方法返回主播连线的视频挂件。class VideoWidgetProvider: VideoViewDelegate {/// seatInfo: 麦位数据 (用户信息、音视频状态)/// viewLayer: 视图层级 (.foreground 前景视图 / .background 背景视图)func createCoHostView(seatInfo: SeatInfo, viewLayer: ViewLayer) -> UIView? {let isUserOnSeat = !seatInfo.userInfo.userID.isEmptyswitch viewLayer {case .foreground:if isUserOnSeat {// 返回自定义的前景视图,可以返回与观众连线不同的样式let widget = CustomInfoView(name: seatInfo.userInfo.userName,isMuted: seatInfo.userInfo.microphoneStatus == .off)return widget}// 返回自定义空麦位视图,可以返回与观众连线不同的样式return EmptySeatView()case .background:if isUserOnSeat {// 返回自定义的背景视图(未开摄像头时显示),可以返回与观众连线不同的样式let bgView = CustomAvatarView(avatarURL: seatInfo.userInfo.avatarURL)return bgView}return nil}}}
参数说明:
参数 | 类型 | 说明 |
seatInfo | SeatInfo | 麦位信息对象,包含麦上用户的详细信息。 |
seatInfo.userInfo.userName | String | 麦上用户的昵称。 |
seatInfo.userInfo.avatarURL | String | 麦上用户的头像 URL。 |
seatInfo.userInfo.microphoneStatus | DeviceStatus | 麦上用户的麦克风状态。 |
seatInfo.userInfo.cameraStatus | DeviceStatus | 麦上用户的摄像头状态。 |
viewLayer | ViewLayer | 视图层级枚举。 .foreground 表示前景挂件视图,始终显示在视频画面的最上层。.background 表示背景挂件视图,位于前景视图下层,仅在对应用户没有视频流(例如未开摄像头)的情况下显示,通常用于展示用户的默认头像或占位图。 |
调整主播 PK 场景挂件
PK 是直播中互动性最强的环节。在 PK 过程中,画面通常根据参与人数被分割为几部分。开发者需要在画面上方或中间区域添加 PK 专用的 UI,分数PK条、每个麦位上的分数显示、倒计时动画或“VS”特效图标,以营造紧张的竞技氛围。
视图层级示意

说明:
PK 功能依赖于连线功能,所以需要先在主播连线的情况下才能发起 PK 请求。
实现步骤
步骤1:准备自定义 UI 组件
PK 场景通常需要两类视图:
1. 单人挂件:显示在每个主播窗口上(例如:分数胶囊)。
2. 全局容器:覆盖在整体画面上(例如:VS 动画、倒计时)。
说明:
您可以参考 TUILiveKit 开源项目中的 AnchorBattleMemberInfoView.swift 和 AnchorBattleInfoView.swift 文件来了解默认效果的实现逻辑。
// 示例:单人分数条class MyBattleScoreView: UIView {private let scoreView = UIView()// ... layout code ...}// 示例:全局 VS 面板class MyBattleContainer: UIView {private let battleTimeView = UIImageView(frame: .zero)// 内部实现倒计时和 VS 动画}
步骤2: 实现代理逻辑
实现
VideoViewDelegate的剩余方法 createBattleView 和 createBattleContainerView。class VideoWidgetProvider: VideoViewDelegate {/// 1. 创建【PK 单人信息】挂件 (显示在每个主播视频上方)func createBattleView(seatInfo: SeatInfo) -> UIView? {let scoreView = MyBattleScoreView()return scoreView}/// 2. 创建【PK 全局容器】挂件 (显示在整个视频区域上方)func createBattleContainerView() -> UIView? {let container = MyBattleContainer()return container}}
集成与生效(重要)
这是最关键的一步。您需要将实现了代理逻辑的
VideoWidgetProvider 注入到直播核心流程中。主播端集成:在初始化
AnchorView 之前,先初始化 LiveCoreView 并接管其代理。import AtomicXCoreimport SnapKitclass YourAnchorViewController: UIViewController {private let anchorView: AnchorViewprivate let widgetProvider = VideoWidgetProvider()init(liveInfo: LiveInfo) {let videoView = LiveCoreView(viewType: .pushView)videoView.videoViewDelegate = widgetProvider // 接管代理anchorView = AnchorView(liveInfo: liveInfo, coreView: videoView)super.init(nibName: nil, bundle: nil)}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}override func viewDidLoad() {super.viewDidLoad()view.addSubview(anchorView)anchorView.snp.makeConstraints { make inmake.edges.equalToSuperview()}}}
观众端集成:观众端通过
AudienceContainerViewDelegate 来回调核心视图的创建时机。class YourAudienceViewController: UIViewController {private let audienceView: AudienceContainerViewprivate let widgetProvider = VideoWidgetProvider()public init(roomId: String) {self.audienceView = AudienceContainerView(roomId: roomId)super.init(nibName: nil, bundle: nil)self.audienceView.delegate = self}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}}extension YourAudienceViewController: AudienceContainerViewDelegate {func onCreateCoreView(for liveInfo: LiveInfo) -> LiveCoreView? {let view = LiveCoreView(viewType: .playView)view.videoViewDelegate = widgetProvider // 接管代理return view}}
进阶:获取实时业务数据
在开发 PK 等复杂场景时,您会发现
SeatInfo 只提供了基础麦位信息。如果您需要获取实时倒计时、PK 分数等业务数据,就需要在您的自定义视图中对接 AtomicXCore中的核心数据。常见问题
为什么我设置了代理,但自定义视图没有显示?
请检查以下两点:
1. 代理对象被释放了:
videoViewDelegate 是弱引用。如果您在函数内部定义代理(例如 let delegate = VideoWidgetProvider()),函数执行结束后代理对象会被立即释放。解决:请务必将代理对象声明为
ViewController 的属性(成员变量),保持强引用。2. 设置时机太晚:创建主播视图的挂件时必须在创建
AnchorView之前完成 LiveCoreView的videoViewDelegate 的赋值,否则AnchorView已经完成了默认视图的加载我只想修改“连麦”视图,“PK”视图想保留默认的,怎么办?
VideoViewDelegate 是一种完全接管模式。一旦您设置了自定义的代理,SDK 默认的 AnchorView/AudienceView 内部实现的代理逻辑就会完全失效。解决:您需要在您的代理类中实现所有相关方法。对于您不想修改的部分(例如 PK 视图),您可以:
1. 复制源码:找到
LiveKit 源码中默认的实现类(例如 AnchorBattleInfoView),在您的代理中返回这些默认类的实例。2. 手动实现:参考默认样式,快速写一个类似的简单视图返回。
自定义挂件显示了,但无法点击?
因为前景视图 (
.foreground) 始终位于视频层之上,所以您要检查您的 View 是否位于 .foreground 层,且 isUserInteractionEnabled 为 true,同时确保它的父视图没有禁用交互。
