LiveCoreView is a cross-platform core component for live video streaming that provides essential capabilities such as going live, viewing streams, co-hosting, and host PK (player-kill) battles. Using its widget system, you can display real-time custom information—like usernames, levels, and PK progress bars—directly in the video area. This guide walks you through customizing your own video widget UI on iOS by implementing the delegate protocol.Co-hosting Video Widget | PK Video Widget |
![]() | ![]() |
LiveCoreView enables custom view rendering through the VideoViewDelegate protocol. When the live streaming scenario changes (for example, a user joins the mic or a PK session starts), LiveCoreView invokes delegate methods to determine which view to display. To customize the UI, simply implement the corresponding delegate methods and return your custom UIView instances.Method | Description | Applicable Scenario |
createCoGuestView | Creates the widget view for audience co-hosting. | Audience co-hosting, invite to co-host |
createCoHostView | Creates the widget view for cross-room co-hosting (host connection) | Host co-hosting |
createBattleView | Creates the widget view for an individual user in a PK scenario (e.g., avatar, score) | Host PK |
createBattleContainerView | Creates the overall container view for PK scenarios (e.g., background, PK score bar) | Host PK |

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 ...}
ViewController) and implement the VideoViewDelegate methods createCoGuestView (for audience connecting) and createCoHostView (for host connecting), returning your custom views.createCoGuestView method in VideoViewDelegate to return a custom widget for audience connecting:class VideoWidgetProvider: VideoViewDelegate {/// seatInfo: Data for the seat (user info, audio/video status)/// viewLayer: View layer (.foreground for the top view / .background for the background view)func createCoGuestView(seatInfo: SeatInfo, viewLayer: ViewLayer) -> UIView? {let isUserOnSeat = !seatInfo.userInfo.userID.isEmptyswitch viewLayer {case .foreground:if isUserOnSeat {// Non-empty seat: return custom foreground viewlet widget = CustomInfoView(name: seatInfo.userInfo.userName,isMuted: seatInfo.userInfo.microphoneStatus == .off)return widget}// Empty seat: return custom empty seat viewreturn EmptySeatView()case .background:if isUserOnSeat {// Custom background view (shown when camera is off)let bgView = CustomAvatarView(avatarURL: seatInfo.userInfo.avatarURL)return bgView}return nil}}}
createCoHostView method for host connecting:class VideoWidgetProvider: VideoViewDelegate {/// seatInfo: Data for the seat (user info, audio/video status)/// viewLayer: View layer (.foreground for the top view / .background for the background view)func createCoHostView(seatInfo: SeatInfo, viewLayer: ViewLayer) -> UIView? {let isUserOnSeat = !seatInfo.userInfo.userID.isEmptyswitch viewLayer {case .foreground:if isUserOnSeat {// Custom foreground view—can style differently from audience connectinglet widget = CustomInfoView(name: seatInfo.userInfo.userName,isMuted: seatInfo.userInfo.microphoneStatus == .off)return widget}// Custom empty seat view—can style differently from audience connectingreturn EmptySeatView()case .background:if isUserOnSeat {// Custom background view (shown when camera is off)—can style differently from audience co-hostinglet bgView = CustomAvatarView(avatarURL: seatInfo.userInfo.avatarURL)return bgView}return nil}}}
Parameter | Type | Description |
seatInfo | SeatInfo | Contains detailed information about the user on the seat. |
seatInfo.userInfo.userName | String | The nickname of the user occupying the seat. |
seatInfo.userInfo.avatarURL | String | The avatar URL for the user occupying the seat. |
seatInfo.userInfo.microphoneStatus | DeviceStatus | The microphone status of the user on the seat. |
seatInfo.userInfo.cameraStatus | DeviceStatus | The camera status of the user on the seat. |
viewLayer | ViewLayer | Enum for the widget layer: .foreground is always drawn on top of the video..background appears below the foreground view and is only shown when the user has no video stream (camera off). Commonly used to show the user's default avatar or a placeholder image. |

// Single-user score bar exampleclass MyBattleScoreView: UIView {private let scoreView = UIView()// ... layout code ...}// Global VS panel exampleclass MyBattleContainer: UIView {private let battleTimeView = UIImageView(frame: .zero)// Implements countdown and VS animation}
VideoViewDelegate methods createBattleView and createBattleContainerView:class VideoWidgetProvider: VideoViewDelegate {/// 1. Create [PK single-user info] widget (displayed above each host's video)func createBattleView(seatInfo: SeatInfo) -> UIView? {let scoreView = MyBattleScoreView()return scoreView}/// 2. Create [PK global container] widget (displayed above the entire video area)func createBattleContainerView() -> UIView? {let container = MyBattleContainer()return container}}
VideoWidgetProvider (the delegate implementation) into the core live streaming workflow.AnchorView, initialize LiveCoreView and assign its delegate.import AtomicXCoreimport SnapKitclass YourAnchorViewController: UIViewController {private let anchorView: AnchorViewprivate let widgetProvider = VideoWidgetProvider()init(liveInfo: LiveInfo) {let videoView = LiveCoreView(viewType: .pushView)videoView.videoViewDelegate = widgetProvider // Assign delegateanchorView = 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 to inject the core view at the right time.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 // Assign delegatereturn view}}
SeatInfo, such as countdown timers or PK scores. In these cases, integrate with the core data stores in AtomicXCore from your custom views.Store/Component | Function Description | API Documentation |
CoGuestStore | Audience connecting data: list of connected users, invitation list, application list, etc. | |
CoHostStore | Host connecting data: list of connected users, invitation list, application list, etc. | |
BattleStore | PK data: current PK info, PK user list, PK score list. |
videoViewDelegate is a weak reference. If you define the delegate inside a function (e.g., let delegate = VideoWidgetProvider()), it will be released as soon as the function exits.ViewController to ensure it remains in memory.LiveCoreView’s videoViewDelegate before creating the AnchorView. Assigning it afterward means AnchorView will have already loaded the default view.VideoViewDelegate operates in full takeover mode. Once you assign a custom delegate, the SDK's default delegate logic in AnchorView and AudienceView is fully disabled.LiveKit source code (e.g., AnchorBattleInfoView) and return instances of these defaults in your delegate..foreground view layer is always on top of the video, check the following:.foreground layer.isUserInteractionEnabled is set to true on your view.피드백