一、效果展示
| 连线视频挂件 | PK 视频挂件 |
| | |
二、准备工作
在开始调整视频挂件前,请先参考 主播开播 和 观众观看 完成主流程的搭建。
三、核心原理
LiveCoreView 通过 VideoViewDelegate 代理支持自定义视图渲染。当业务场景发生变化(例如有人上麦、开始 PK)时,LiveCoreView 会调用代理方法询问应显示的视图。实现对应的代理方法并返回自定义的 UIView 实例即可。
| 方法 | 描述 | 对应业务场景 |
| createCoGuestView | 创建观众连麦的挂件视图 | 观众连麦、邀请上麦 |
| createCoHostView | 创建跨房连麦(主播连线)的挂件视图 | 主播连线 |
| createBattleView | 创建 PK 场景中单个用户的挂件视图(例如头像、分数) | 主播 PK |
| createBattleContainerView | 创建 PK 场景的整体容器视图(例如背景、分数 PK 条) | 主播 PK |
四、调整连线场景挂件
当观众与主播进行连线或者主播间跨房连线时,直播间界面会从单人模式切换为多人布局。此时,需要在连线的视频窗口上展示特定的用户信息,例如昵称、等级或“静音”状态提示,以便区分不同麦位。
五、适用场景
·主播和观众视频连麦时,修改麦位上的用户信息(例如昵称、等级、静音图标)。
·主播与主播跨房连线时,修改对方主播的显示样式(例如昵称、等级、静音图标)。
·修改无视频画面时的默认背景(例如占位头像)。
·修改空麦位时的视图实现。
八、步骤1:准备自定义挂件 View
定义三个基础视图类,分别用于展示用户信息、空麦位提示和无视频流时的背景。
创建自定义的用户信息视图:
注意:说明:
注意:你可以参考 TUILiveKit 开源项目中的 AnchorCoGuestView.swift 文件来了解默认效果的实现逻辑。
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 = name muteIcon.isHidden = !isMuted } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // … layout code …}
创建自定义的空麦位视图:
注意:说明:
注意:你可以参考 TUILiveKit 开源项目中的 AnchorEmptySeatView.swift 文件来了解默认效果的实现逻辑。
import UIKitclass EmptySeatView: UIView { let addIcon = UIImageView(image: UIImage(named: "add_icon")) let addLabel = UILabel() // … layout code …}
创建自定义无视频流时的头像占位视图:
注意:说明:
注意:你可以参考 TUILiveKit 开源项目中的 AnchorBackgroundWidgetView.swift 文件来了解默认效果的实现逻辑。
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.isEmpty switch 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.isEmpty switch 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: AnchorView private 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 in make.edges.equalToSuperview() } }}
观众端集成:观众端通过 AudienceContainerViewDelegate 来回调核心视图的创建时机。
class YourAudienceViewController: UIViewController { private let audienceView: AudienceContainerView private 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 中的核心数据。
| Store/Component | 功能描述 | API 文档 |
| CoGuestStore | 观众连线数据:已连线用户列表、邀请列表,申请列表等。 | API 文档 |
| CoHostStore | 主播连线数据:已连线用户列表、邀请列表,申请列表等。 | API 文档 |
| BattleStore | PK 数据:当前 PK 信息、PK 用户列表、PK 分数列表。 | API 文档 |
十九、为什么我设置了代理,但自定义视图没有显示?
请检查以下两点:
1. 代理对象被释放了:videoViewDelegate 是弱引用。如果你在函数内部定义代理(例如 let delegate = VideoWidgetProvider()),函数执行结束后代理对象会被立即释放。
解决:请务必将代理对象声明为 ViewController 的属性(成员变量),保持强引用。
2. 设置时机太晚:创建主播视图的挂件时必须在创建 AnchorView 之前完成 LiveCoreView 的 videoViewDelegate 的赋值,否则 AnchorView 已经完成了默认视图的加载
20、我只想修改“连麦”视图,“PK”视图想保留默认的,怎么办?
VideoViewDelegate 是一种完全接管模式。一旦你设置了自定义的代理,SDK 默认的 AnchorView/AudienceView 内部实现的代理逻辑就会完全失效。
解决:你需要在你的代理类中实现所有相关方法。对于你不想修改的部分(例如 PK 视图),你可以:
1. 复制源码:找到 LiveKit 源码中默认的实现类(例如 AnchorBattleInfoView),在你的代理中返回这些默认类的实例。
2. 手动实现:参考默认样式,快速写一个类似的简单视图返回。
21、自定义挂件显示了,但无法点击?
因为前景视图 (.foreground) 始终位于视频层之上,所以你要检查你的 View 是否位于 .foreground 层,且 isUserInteractionEnabled 为 true,同时确保它的父视图没有禁用交互。