本指南说明如何使用 RealtimeKit UI Kit 组件来构建自定义会议界面,而不是使用默认的全屏会议视图。
本页以 初始化 SDK、渲染默认会议 UI 和 UI Kit 状态指南为基础。请先参阅这些页面以了解核心概念。
本页上的代码示例假定你已经导入了必要的包并初始化了 SDK。
如果默认会议组件无法对布局或行为提供足够的控制,可使用 UI Kit 组件构建自定义界面。UI Kit 在 Core SDK 之上提供了预建组件。你可以混合和搭配使用这些部分,这比从头开始构建要节省大量时间。
构建自定义 UI 需要你亲自管理参与者音频、通知、对话框、组件布局以及屏幕过渡。
与 rtk-meeting 类似,另一个作为 provider 的 UI Kit 组件 rtk-ui-provider 也会监听状态并将其与 UI Kit 组件同步。
与 rtk-meeting 不同,rtk-ui-provider 允许你向其传递任何子组件。如果有任何子组件是以前缀 rtk- 开头的 RealtimeKit 组件,rtk-ui-provider 将与其进行协调以同步状态。
与 RtkMeeting 类似,另一个作为 provider 的 UI Kit 组件 RtkUiProvider 也会监听状态并将其与 UI Kit 组件同步。
与 RtkMeeting 不同, RtkUiProvider 允许你向其传递任何子组件。如果有任何子组件是以前缀 Rtk 开头的 RealtimeKit 组件,RtkUiProvider 将与其进行协调以同步状态。
与 rtk-meeting 类似,另一个作为 provider 的 UI Kit 组件 rtk-ui-provider 也会监听状态并将其与 UI Kit 组件同步。
与 rtk-meeting 不同,rtk-ui-provider 允许你向其传递任何子组件。如果有任何子组件是以前缀 rtk- 开头的 RealtimeKit 组件,rtk-ui-provider 将与其进行协调以同步状态。
iOS SDK 在自定义 UI 中使用委托模式(delegation pattern)。与其在 provider 中包裹子视图,你可以实现 RtkUIFlowCoordinatorDelegate,并为会议流程中的每个屏幕返回你自己的视图控制器。
有两个级别的自定义:
- 替换整个屏幕 — 实现
RtkUIFlowCoordinatorDelegate并为设置屏幕、群组通话屏幕或网络研讨会(webinar)屏幕返回你自己的UIViewController。返回nil以使用默认屏幕。 - 替换子组件 — 实现
MeetingViewControllerDataSource,并在默认的MeetingViewController中更换顶栏、中间视图(参与者网格)或底部控制栏。
public protocol RtkUIFlowCoordinatorDelegate {
func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource?
func showGroupCallMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController?
func showWebinarMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController?
}当用户成功加入时,SDK 会调用 showGroupCallMeetingScreen(meeting:completion:) 或 showWebinarMeetingScreen(meeting:completion:)。使用 RealtimeKitClient 参数将单个 UI Kit 组件(如 RtkMeetingHeaderView、RtkMeetingControlBar 或 RtkParticipantTileView)绑定到会议。
Android SDK 不使用 provider/wrapper 模式。相反,每个单独的 View 组件都具有一个 activate(meeting) 方法,可将其绑定到会议中。
此模式为:
- 使用
RealtimeKitUIBuilder.build()构建RealtimeKitUI实例。 - 通过
rtkUI.meeting访问RealtimeKitClient。 - 在你自己的 XML 布局中放置单个 UI Kit 视图,或者在代码中创建它们。
- 在每个组件上调用
activate(meeting)以将其绑定到会议中。
会议级组件(网格、页眉、控制栏)接收一个 RealtimeKitClient:
gridView.activate(meeting)
headerView.activate(meeting)
controlBarView.activate(meeting)参与者级组件(卡片、头像、姓名标签)接收一个 RtkMeetingParticipant:
tileView.activate(meeting.localUser)
tileView.activate(remoteParticipant)Flutter SDK 提供了 RtkProvider,这是一个包装器挂件(wrapper widget),用于为单个 UI Kit 挂件设置依赖注入和主题。
将你的自定义挂件树包装在 RtkProvider 中,并使用单独导出的挂件,如 RtkSelfAudioToggleButton、RtkSelfVideoToggleButton、RtkParticipantTile、RtkJoinButton 和 RtkLeaveButton。
RtkProvider(
meeting: meeting,
uiKitInfo: RealtimeKitUIInfo(meetingInfo),
child: YourCustomMeetingWidget(),
)每个需要会议客户端的挂件都接受它作为一个必填的 meeting 参数。参与者级别的挂件(如 RtkParticipantTile)直接接受 RtkMeetingParticipant。
与 RtkMeeting 类似,RtkUIProvider 是一个包装器组件,用于向所有子组件提供设计系统和 UI 状态上下文。
与 RtkMeeting 不同,RtkUIProvider 允许你向其传递任何子组件。所有 RealtimeKit UI Kit 组件都会从 RtkUIContext 读取设计令牌和状态。
使用 useContext(RtkUIContext) 读取 storeStates.meeting 并决定要渲染哪个屏幕。在不使用 RtkMeeting 进行构建时,你必须监听 SDK 事件并调用 setStates 来亲自驱动状态转换。
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { defineCustomElements } from "https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit-ui@latest/loader/index.es2017.js";
defineCustomElements();
</script>
<script src="https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit@latest/dist/browser.js"></script>
</head>
<body style="margin: 0;">
<rtk-ui-provider
id="rtk-ui-provider"
style="display: flex; flex-direction: column; height: 100vh; margin: 0;"
>
<div
id="meeting-container"
style="display: flex; flex-direction: column; flex: 1; flex-grow: 1; flex-shrink: 1;"
>
会议将在此处渲染...
</div>
<rtk-participants-audio></rtk-participants-audio>
<rtk-dialog-manager></rtk-dialog-manager>
<rtk-notifications></rtk-notifications>
</rtk-ui-provider>
<script type="module">
async function initializeMeeting() {
let currentState = "idle";
const meeting = await RealtimeKitClient.init({
authToken: "participant_auth_token",
});
function renderSetupScreen() {
document.querySelector("#meeting-container").innerHTML = `
<rtk-setup-screen></rtk-setup-screen>
`;
}
function renderWaitingScreen() {
document.querySelector("#meeting-container").innerHTML = `
<rtk-waiting-screen></rtk-waiting-screen>
`;
}
function renderJoinedScreen() {
document.querySelector("#meeting-container").innerHTML = `
<rtk-header style="display: flex; justify-content: space-between;"></rtk-header>
<rtk-stage style="flex: 1; flex-grow: 1; flex-shrink: 1;">
<rtk-grid></rtk-grid>
<rtk-sidebar style="position: fixed; top:0px;"></rtk-sidebar>
</rtk-stage>
<rtk-controlbar style="display: flex; justify-content: space-between;"></rtk-controlbar>
`;
}
function renderEndedScreen() {
document.querySelector("#meeting-container").innerHTML = `
<rtk-ended-screen></rtk-ended-screen>
`;
}
// Listen for state updates from rtk-ui-provider
document
.querySelector("rtk-ui-provider")
.addEventListener("rtkStatesUpdate", (event) => {
// Store states to update your custom UI
const states = event.detail;
if (states.meeting === "idle" && currentState !== "idle") {
currentState = "idle";
document
.querySelector("rtk-ui-provider")
.querySelector("#meeting-container").innerHTML =
"Meeting is loading...";
} else if (states.meeting === "setup" && currentState !== "setup") {
currentState = "setup";
renderSetupScreen();
} else if (
states.meeting === "waiting" &&
currentState !== "waiting"
) {
currentState = "waiting";
renderWaitingScreen();
} else if (
states.meeting === "joined" &&
currentState !== "joined"
) {
currentState = "joined";
renderJoinedScreen();
} else if (states.meeting === "ended" && currentState !== "ended") {
currentState = "ended";
renderEndedScreen();
}
const sidebarComponent = document
.querySelector("rtk-ui-provider")
.querySelector("#meeting-container")
.querySelector("rtk-sidebar");
if (sidebarComponent) {
if (states.activeSidebar) {
sidebarComponent.style.display = "block";
} else {
sidebarComponent.style.display = "none";
}
}
});
document.querySelector("rtk-ui-provider").showSetupScreen = true;
document.querySelector("rtk-ui-provider").meeting = meeting;
}
initializeMeeting();
</script>
</body>
</html>使用 rtk-ui-provider 对 rtk-meeting 进行第一级拆分包含以下组件:
rtk-header 是显示会话名称和会话控制项的页眉组件。
rtk-stage 是包含网格和侧边栏组件的容器组件。
rtk-grid 是在会话中展示参与者的网格组件。
rtk-sidebar 是展示侧边栏的组件,其中会显示聊天、投票等内容。
rtk-controlbar 是展示控制项(如摄像头、麦克风等)的控制栏组件。
rtk-notifications 是显示会话通知的通知组件。
rtk-participants-audio 是帮助你听取会话中其他参与者声音的音频组件。
rtk-dialog-manager 是显示会话中所有支持的对话框(如设置、分组讨论室等)的对话框管理器组件。
你可以将所有这些组件进一步拆分。若要查看更多此类组件,请参阅我们的组件库。
我们的 UI Kit 在 GitHub 上开源,你可以在此处 ↗找到它。
import {
RealtimeKitProvider,
useRealtimeKitClient,
} from "@cloudflare/realtimekit-react";
import {
RtkUiProvider,
RtkHeader,
RtkStage,
RtkGrid,
RtkSidebar,
RtkControlbar,
RtkNotifications,
RtkParticipantsAudio,
RtkDialogManager,
RtkSetupScreen,
RtkWaitingScreen,
RtkEndedScreen,
States,
} from "@cloudflare/realtimekit-react-ui";
import { useEffect, useState } from "react";
function MeetingContainer() {
const [meeting, initMeeting] = useRealtimeKitClient();
const [currentState, setCurrentState] = useState("idle");
const [showSidebar, setShowSidebar] = useState(false);
useEffect(() => {
initMeeting({
authToken: "participant_auth_token",
});
}, []);
const renderSetupScreen = () => {
return <RtkSetupScreen />;
};
const renderWaitingScreen = () => {
return <RtkWaitingScreen />;
};
const renderJoinedScreen = () => {
return (
<>
<RtkHeader
style={{ display: "flex", justifyContent: "space-between" }}
/>
<RtkStage style={{ flex: 1, flexGrow: 1, flexShrink: 1 }}>
<RtkGrid />
<RtkSidebar
style={{
position: "fixed",
top: "0px",
display: showSidebar ? "block" : "none",
}}
/>
</RtkStage>
<RtkControlbar
style={{ display: "flex", justifyContent: "space-between" }}
/>
</>
);
};
const renderEndedScreen = () => {
return <RtkEndedScreen />;
};
// Listen for state updates from RtkUiProvider
const handleStatesUpdate = (event: { detail: States }) => {
const meetingState = event.detail.meeting;
const states = event.detail;
// Store states to update your custom UI
if (meetingState === "idle" && currentState !== "idle") {
setCurrentState("idle");
} else if (meetingState === "setup" && currentState !== "setup") {
setCurrentState("setup");
} else if (meetingState === "waiting" && currentState !== "waiting") {
setCurrentState("waiting");
} else if (meetingState === "joined" && currentState !== "joined") {
setCurrentState("joined");
} else if (meetingState === "ended" && currentState !== "ended") {
setCurrentState("ended");
}
// Update sidebar visibility based on state
if (states.activeSidebar !== undefined) {
setShowSidebar(states.activeSidebar);
}
};
return (
<RealtimeKitProvider value={meeting}>
<RtkUiProvider
meeting={meeting}
showSetupScreen={true}
onRtkStatesUpdate={handleStatesUpdate}
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
margin: 0,
}}
>
<div
id="meeting-container"
style={{
display: "flex",
flexDirection: "column",
flex: 1,
flexGrow: 1,
flexShrink: 1,
}}
>
{currentState === "idle" && <div>Meeting is loading...</div>}
{currentState === "setup" && renderSetupScreen()}
{currentState === "waiting" && renderWaitingScreen()}
{currentState === "joined" && renderJoinedScreen()}
{currentState === "ended" && renderEndedScreen()}
</div>
<RtkParticipantsAudio />
<RtkDialogManager />
<RtkNotifications />
</RtkUiProvider>
</RealtimeKitProvider>
);
}
function App() {
return <MeetingContainer />;
}使用 RtkUiProvider 对 RtkMeeting 进行第一级拆分包含以下组件:
RtkHeader 是显示会话名称和会话控制项的页眉组件。
RtkStage 是包含网格和侧边栏组件的容器组件。
RtkGrid 是在会话中展示参与者网格的组件。
RtkSidebar 是展示侧边栏的组件,其中会显示聊天、投票等内容。
RtkControlbar 是展示控制项(如摄像头、麦克风等)的控制栏组件。
RtkNotifications 是显示会话通知的通知组件。
RtkParticipantsAudio 是帮助你听取会话中其他参与者声音的音频组件。
RtkDialogManager 是显示会话中所有支持的对话框的对话框管理器组件。
你可以将所有这些组件进一步拆分。若要查看更多此类组件,请参阅我们的组件库。
我们的 UI Kit 在 GitHub 上开源,你可以在此处 ↗找到它。
在你的 app.module.ts 中,导入 RealtimeKitComponentsModule 以及你使用组件库构建的所有自定义模块(示例如下所示)。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import { CustomRtkMeetingComponent } from "./components/custom-rtk-meeting.component";
import { SetupScreenComponent } from "./components/setup-screen.component";
import { InMeetingComponent } from "./components/in-meeting.component";
import { MeetingHeaderComponent } from "./components/meeting-header.component";
import { MeetingControlBarComponent } from "./components/meeting-control-bar.component";
import { MeetingSidebarComponent } from "./components/meeting-sidebar.component";
import { MediaPreviewModalComponent } from "./components/media-preview-modal.component";
import { AudioPreviewComponent } from "./components/audio-preview.component";
import { VideoPreviewComponent } from "./components/video-preview.component";
import { RealtimeKitComponentsModule } from "@cloudflare/realtimekit-angular-ui";
@NgModule({
declarations: [
AppComponent,
CustomRtkMeetingComponent,
SetupScreenComponent,
InMeetingComponent,
MeetingHeaderComponent,
MeetingControlBarComponent,
MeetingSidebarComponent,
MediaPreviewModalComponent,
AudioPreviewComponent,
VideoPreviewComponent,
],
imports: [BrowserModule, FormsModule, RealtimeKitComponentsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}在你的 app.component.ts 中初始化会议
import { Component, OnInit, Inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";
import { StatesService } from "./services/states.service";
import RealtimeKitClient from "@cloudflare/realtimekit";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent implements OnInit {
meeting: any = null;
constructor(
private statesService: StatesService,
@Inject(DOCUMENT) private document: Document,
) {}
async ngOnInit() {
await this.initializeMeeting();
}
private async initializeMeeting() {
const searchParams = new URLSearchParams(
this.document.defaultView?.location.search,
);
const authToken = searchParams.get("authToken");
if (!authToken) {
alert(
"An authToken wasn't passed, please pass an authToken in the URL query to join a meeting.",
);
return;
}
if (!this.meeting) {
try {
// Initialize RealtimeKit client
const meeting = await RealtimeKitClient.init({
authToken,
});
this.meeting = meeting;
// Expose meeting object to window for debugging
Object.assign(this.document.defaultView as any, {
meeting: this.meeting,
});
} catch (error) {
console.error("Failed to initialize meeting:", error);
}
}
}
onRtkStatesUpdate(event: any) {
this.statesService.setStates(event.detail);
}
}<rtk-ui-provider
[meeting]="meeting"
(rtkStatesUpdate)="onRtkStatesUpdate($event)"
[showSetupScreen]="true"
style="height: 100%; width: 100%; display: block;"
>
<app-custom-rtk-meeting></app-custom-rtk-meeting>
<rtk-dialog-manager></rtk-dialog-manager>
</rtk-ui-provider>以下是使用 UI Kit 组件构建的自定义元素示例:
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { StatesService, CustomStatesService } from "../services/states.service";
import { States } from "@cloudflare/realtimekit-ui";
import { CustomStates } from "../types";
@Component({
selector: "app-custom-rtk-meeting",
template: `
<rtk-idle-screen *ngIf="states.meeting === 'idle'"></rtk-idle-screen>
<app-setup-screen *ngIf="states.meeting === 'setup'"></app-setup-screen>
<rtk-waiting-screen
*ngIf="states.meeting === 'waiting'"
></rtk-waiting-screen>
<rtk-ended-screen *ngIf="states.meeting === 'ended'"></rtk-ended-screen>
<app-in-meeting
*ngIf="states.meeting === 'joined' || !states.meeting"
></app-in-meeting>
`,
})
export class CustomRtkMeetingComponent implements OnInit, OnDestroy {
states: States = { meeting: "idle" } as States;
customStates: CustomStates = {};
private destroy$ = new Subject<void>();
constructor(
private statesService: StatesService,
private customStatesService: CustomStatesService,
) {}
ngOnInit() {
this.statesService.states$
.pipe(takeUntil(this.destroy$))
.subscribe((states) => {
this.states = states;
console.log(states, this.customStates);
});
this.customStatesService.customStates$
.pipe(takeUntil(this.destroy$))
.subscribe((customStates) => {
this.customStates = customStates;
console.log(this.states, customStates);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}使用 rtk-ui-provider 对 rtk-meeting 进行第一级拆分包含以下组件:
rtk-header 是显示会话名称和会话控制项的页眉组件。
rtk-stage 是包含网格和侧边栏组件的容器组件。
rtk-grid 是在会话中展示参与者的网格组件。
rtk-sidebar 是展示侧边栏的组件,其中会显示聊天、投票等内容。
rtk-controlbar 是展示控制项(如摄像头、麦克风等)的控制栏组件。
rtk-notifications 是显示会话通知的通知组件。
rtk-participants-audio 是帮助你听取会话中其他参与者声音的音频组件。
rtk-dialog-manager 是显示会话中所有支持的对话框(如设置、分组讨论室等)的对话框管理器组件。
你可以将所有这些组件进一步拆分。若要查看更多此类组件,请参阅我们的组件库。
我们的 UI Kit 在 GitHub 上开源,你可以在此处 ↗找到它。
以下示例使用包含单个 UI Kit 组件的自定义视图控制器替换了群组通话会议屏幕:
import UIKit
import RealtimeKit
import RealtimeKitUI
class CustomFlowDelegate: RtkUIFlowCoordinatorDelegate {
// 返回 nil 以使用默认设置屏幕
func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource? {
return nil
}
// 返回用于群组通话的自定义视图控制器
func showGroupCallMeetingScreen(
meeting: RealtimeKitClient,
completion: @escaping () -> Void
) -> UIViewController? {
return CustomMeetingViewController(meeting: meeting, completion: completion)
}
// 返回 nil 以使用默认网络研讨会屏幕
func showWebinarMeetingScreen(
meeting: RealtimeKitClient,
completion: @escaping () -> Void
) -> UIViewController? {
return nil
}
}
class CustomMeetingViewController: UIViewController {
private let meeting: RealtimeKitClient
private let completion: () -> Void
private var selfListener: RtkEventSelfListener?
init(meeting: RealtimeKitClient, completion: @escaping () -> Void) {
self.meeting = meeting
self.completion = completion
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// 页眉
let header = RtkMeetingHeaderView(meeting: meeting)
header.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(header)
// 控制栏
let controlBar = RtkMeetingControlBar(
meeting: meeting,
delegate: nil,
presentingViewController: self
)
controlBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(controlBar)
NSLayoutConstraint.activate([
header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor),
header.heightAnchor.constraint(equalToConstant: 50),
controlBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
controlBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controlBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controlBar.heightAnchor.constraint(equalToConstant: 80),
])
// 监听会议事件
selfListener = RtkEventSelfListener(rtkClient: meeting)
selfListener?.observeSelfRemoved { [weak self] _ in
self?.dismiss(animated: true, completion: self?.completion)
}
selfListener?.observeSelfMeetingEndForAll { [weak self] _ in
self?.dismiss(animated: true, completion: self?.completion)
}
}
}使用你的自定义流程委托来展示会议:
let delegate = CustomFlowDelegate()
let rtkUI = RealtimeKitUI(
meetingInfo: RtkMeetingInfo(
authToken: "<auth-token>",
enableAudio: true,
enableVideo: true
),
flowDelegate: delegate
)
let controller = rtkUI.startMeeting {
self.dismiss(animated: true)
}
controller.modalPresentationStyle = .fullScreen
present(controller, animated: true)可用于自定义会议屏幕的单个组件:
| 组件 | 描述 |
|---|---|
RtkMeetingHeaderView |
包含会议标题、参与者人数、时钟和录制指示器的页眉 |
RtkMeetingControlBar |
包含麦克风、摄像头和结束会议按钮的控制栏 |
RtkParticipantTileView |
包含头像回退和姓名标签的单个参与者视频卡片 |
RtkVideoView |
原始视频渲染视图 |
RtkAvatarView |
包含首字母缩写的参与者头像 |
RtkNameTag |
包含麦克风状态图标的参与者姓名 |
RtkAudioButtonControlBar |
独立麦克风切换按钮 |
RtkVideoButtonControlBar |
独立摄像头切换按钮 |
RtkEndMeetingControlBarButton |
结束/离开会议按钮 |
RtkEventSelfListener |
音频/视频状态、移除、重新连接和权限的观察者 |
以下示例使用 activate(meeting) 模式在 XML 布局中使用单个 UI Kit 视图创建自定义会议屏幕:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000">
<com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView
android:id="@+id/headerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.cloudflare.realtimekit.ui.view.grid.RtkGridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarView
android:id="@+id/controlBarView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.cloudflare.realtimekit.RealtimeKitClient
import com.cloudflare.realtimekit.RtkMeetingRoomEventListener
import com.cloudflare.realtimekit.models.RtkMeetingInfo
import com.cloudflare.realtimekit.ui.RealtimeKitUIBuilder
import com.cloudflare.realtimekit.ui.RealtimeKitUIInfo
import com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarView
import com.cloudflare.realtimekit.ui.view.grid.RtkGridView
import com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView
class CustomMeetingActivity : AppCompatActivity() {
private lateinit var headerView: RtkMeetingHeaderView
private lateinit var gridView: RtkGridView
private lateinit var controlBarView: RtkMeetingControlBarView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_meeting)
headerView = findViewById(R.id.headerView)
gridView = findViewById(R.id.gridView)
controlBarView = findViewById(R.id.controlBarView)
val meetingInfo = RtkMeetingInfo(authToken = "<auth-token>")
val uiKitInfo = RealtimeKitUIInfo(
activity = this,
rtkMeetingInfo = meetingInfo,
)
val rtkUI = RealtimeKitUIBuilder.build(uiKitInfo)
val meeting: RealtimeKitClient = rtkUI.meeting
// 监听会议生命周期事件
meeting.addMeetingRoomEventListener(object : RtkMeetingRoomEventListener {
override fun onMeetingRoomJoinCompleted(meeting: RealtimeKitClient) {
runOnUiThread {
// 加入会议后激活所有组件
headerView.activate(meeting)
gridView.activate(meeting)
controlBarView.activate(meeting)
}
}
override fun onMeetingEnded() {
finish()
}
override fun onMeetingRoomLeaveCompleted() {
finish()
}
})
}
}可用于自定义布局的单个组件:
| 组件 | 激活媒介 | 描述 |
|---|---|---|
RtkGridView |
RealtimeKitClient |
分页参与者视频网格 |
RtkMeetingHeaderView |
RealtimeKitClient |
包含标题、参与者人数、时钟和录制指示器的页眉 |
RtkMeetingControlBarView |
RealtimeKitClient |
包含麦克风、摄像头、更多和离开按钮的控制栏 |
RtkParticipantTileView |
RtkMeetingParticipant |
包含头像和姓名标签的单个参与者视频卡片 |
RtkMicToggleButton |
RealtimeKitClient |
独立麦克风切换 |
RtkCameraToggleButton |
RealtimeKitClient |
独立摄像头切换 |
RtkLeaveButton |
RealtimeKitClient |
离开/结束会议按钮 |
RtkAvatarView |
RtkMeetingParticipant |
参与者头像 |
RtkNameTagView |
RtkMeetingParticipant |
包含音频指示器的姓名标签 |
RtkJoinButton |
RealtimeKitClient |
用于设置屏幕的加入会议按钮 |
以下示例在 RtkProvider 中包装了一个自定义挂件树,并使用了单个 UI Kit 挂件:
import 'package:flutter/material.dart';
import 'package:realtimekit_ui/realtimekit_ui.dart';
class CustomMeetingPage extends StatefulWidget {
final String authToken;
const CustomMeetingPage({required this.authToken, super.key});
@override
State<CustomMeetingPage> createState() => _CustomMeetingPageState();
}
class _CustomMeetingPageState extends State<CustomMeetingPage>
implements RtkMeetingRoomEventListener {
late final RealtimekitClient meeting;
late final RealtimeKitUIInfo uiKitInfo;
String meetingState = 'idle';
@override
void initState() {
super.initState();
meeting = RealtimekitClient();
uiKitInfo = RealtimeKitUIInfo(
RtkMeetingInfo(authToken: widget.authToken),
);
meeting.addMeetingRoomEventListener(this);
meeting.init(
RtkMeetingInfo(authToken: widget.authToken),
onSuccess: () => setState(() => meetingState = 'setup'),
onError: (error) => setState(() => meetingState = 'error'),
);
}
@override
void onMeetingRoomJoinCompleted() {
setState(() => meetingState = 'joined');
}
@override
void onMeetingEnded() {
setState(() => meetingState = 'ended');
}
@override
void onMeetingRoomLeaveCompleted() {
Navigator.of(context).pop();
}
// 要求的接口方法 — 本例中为空操作
@override
void onMeetingInitCompleted() {}
@override
void onMeetingInitFailed(Exception exception) {}
@override
void onMeetingInitStarted() {}
@override
void onMeetingRoomJoinStarted() {}
@override
void onMeetingRoomJoinFailed(Exception exception) {}
@override
void onMeetingRoomLeaveStarted() {}
@override
void onActiveTabUpdate(ActiveTab? activeTab) {}
@override
void onSocketConnectionUpdate(SocketConnectionState state) {}
@override
Widget build(BuildContext context) {
return RtkProvider(
meeting: meeting,
uiKitInfo: uiKitInfo,
child: Scaffold(
backgroundColor: Colors.black,
body: _buildForState(),
),
);
}
Widget _buildForState() {
switch (meetingState) {
case 'setup':
return Column(
children: [
Expanded(
child: RtkParticipantTile(meeting.localUser, height: 300, width: 200),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RtkSelfAudioToggleButton(meeting: meeting),
const SizedBox(width: 16),
RtkSelfVideoToggleButton(meeting: meeting),
const SizedBox(width: 16),
RtkJoinButton(meeting: meeting, onMeetingJoined: () {}),
],
),
],
);
case 'joined':
return Column(
children: [
RtkMeetingTitle(meeting: meeting),
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 4,
),
itemCount: meeting.participants.active.length + 1,
itemBuilder: (context, index) {
final participant = index == 0
? meeting.localUser
: meeting.participants.active[index - 1];
return RtkParticipantTile(participant);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RtkSelfAudioToggleButton(meeting: meeting),
RtkSelfVideoToggleButton(meeting: meeting),
RtkLeaveButton(meeting: meeting),
],
),
],
);
case 'ended':
return const Center(child: Text('Meeting ended', style: TextStyle(color: Colors.white)));
default:
return const Center(child: CircularProgressIndicator());
}
}
@override
void dispose() {
meeting.removeMeetingRoomEventListener(this);
super.dispose();
}
}可用于自定义 UI 的单个挂件(widgets):
| 挂件 | 必填参数 | 描述 |
|---|---|---|
RtkSelfAudioToggleButton |
RealtimekitClient |
包含权限处理的麦克风切换 |
RtkSelfVideoToggleButton |
RealtimekitClient |
包含权限处理的摄像头切换 |
RtkJoinButton |
RealtimekitClient |
包含加载状态的加入会议按钮 |
RtkLeaveButton |
RealtimekitClient |
包含确认对话框的离开会议按钮 |
RtkParticipantTile |
RtkMeetingParticipant |
包含头像回退、姓名标签和音频指示器的视频卡片 |
RtkMeetingTitle |
RealtimekitClient |
会议标题文本 |
RtkNameTag |
RtkMeetingParticipant |
参与者姓名标签 |
RtkAudioIndicatorIconWidget |
RtkMeetingParticipant |
麦克风状态图标 |
以下示例使用 RtkUIProvider 和单个组件来进行基于状态的屏幕渲染,类似于 Web React 模式:
import React, { useContext, useEffect } from "react";
import { View, Text } from "react-native";
import {
useRealtimeKitClient,
RealtimeKitProvider,
} from "@cloudflare/realtimekit-react-native";
import {
RtkUIProvider,
RtkUIContext,
RtkHeader,
RtkGrid,
RtkControlbar,
RtkSidebar,
RtkDialogManager,
RtkSetupScreen,
RtkWaitingScreen,
RtkEndedScreen,
} from "@cloudflare/realtimekit-react-native-ui";
function MeetingScreens({ meeting }) {
const { storeStates, setStates } = useContext(RtkUIContext);
const currentState = storeStates.meeting;
useEffect(() => {
// 监听会议事件并驱动状态转换
const onRoomJoined = () => {
setStates({ ...storeStates, meeting: "joined" });
};
const onRoomLeft = ({ state }) => {
if (state === "ended" || state === "left") {
setStates({ ...storeStates, meeting: "ended" });
}
};
meeting.self.addListener("roomJoined", onRoomJoined);
meeting.self.addListener("roomLeft", onRoomLeft);
return () => {
meeting.self.removeListener("roomJoined", onRoomJoined);
meeting.self.removeListener("roomLeft", onRoomLeft);
};
}, [meeting]);
if (currentState === "setup") {
return <RtkSetupScreen meeting={meeting} />;
}
if (currentState === "waiting") {
return <RtkWaitingScreen />;
}
if (currentState === "joined") {
return (
<View style={{ flex: 1 }}>
<RtkHeader meeting={meeting} />
<View style={{ flex: 1 }}>
<RtkGrid meeting={meeting} />
{storeStates.activeSidebar && <RtkSidebar meeting={meeting} />}
</View>
<RtkControlbar meeting={meeting} />
<RtkDialogManager meeting={meeting} />
</View>
);
}
if (currentState === "ended") {
return <RtkEndedScreen meeting={meeting} />;
}
return <Text>Loading...</Text>;
}
function CustomMeeting({ authToken }) {
const [meet, initMeeting] = useRealtimeKitClient();
const { setStates, storeStates } = useContext(RtkUIContext);
useEffect(() => {
initMeeting({
authToken,
defaults: { audio: true, video: true },
});
}, [authToken]);
useEffect(() => {
if (meet) {
setStates({ ...storeStates, meeting: "setup" });
}
}, [meet]);
if (!meet) return <Text>Initializing...</Text>;
return (
<RealtimeKitProvider value={meet}>
<MeetingScreens meeting={meet} />
</RealtimeKitProvider>
);
}
export default function App() {
return (
<RtkUIProvider>
<CustomMeeting authToken="<auth-token>" />
</RtkUIProvider>
);
}对 RtkMeeting 的第一级拆分包含以下组件:
| 组件 | 必填 prop | 描述 |
|---|---|---|
RtkHeader |
meeting |
包含会议标题、参与者人数、时钟和录制指示器的页眉 |
RtkGrid |
meeting |
分页参与者视频网格 |
RtkControlbar |
meeting |
包含麦克风、摄像头、屏幕共享和更多切换项的控制栏 |
RtkSidebar |
meeting |
包含聊天、投票、参与者和插件面板的侧边栏 |
RtkDialogManager |
meeting |
管理所有对话框(设置、离开确认、加入舞台) |
RtkSetupScreen |
meeting |
包含视频预览和设备选择的加入前设置屏幕 |
RtkWaitingScreen |
— | 等候室屏幕 |
RtkEndedScreen |
meeting |
会议结束屏幕 |
RtkMicToggle |
meeting |
独立麦克风切换按钮 |
RtkCameraToggle |
meeting |
独立摄像头切换按钮 |
RtkLeaveButton |
— | 离开/结束会议按钮 |
RtkParticipantTile |
meeting, participant |
单个参与者视频卡片 |
RtkNotifications |
meeting |
用于加入/离开/聊天事件的 Toast 通知 |