主播开播(Web 桌面浏览器)

最近更新时间:2026-01-04 14:33:42

我的收藏
本文将介绍如何接入 TUILiveKit Demo 的主播开播页,指导您将我们提供的开播页面集成到您的项目中,同时介绍如何对直播页面的样式、布局、以及功能进行自定义修改。

功能展示

横屏开播

竖屏开播


功能概览

画面源管理:支持添加多个摄像头、屏幕分享、窗口分享和本地图片。
直播画面排版:支持对画面源拖拽、缩放,支持通过快捷菜单对画面源设置镜像、旋转和层级调整。
观众连麦:支持房间内观众连麦,内置多种连麦布局。
主播 PK:支持主播跨房连线、PK。
互动聊天:支持收发文字和表情消息,聊天互动。
观众管理:支持对观众禁言、踢出直播间等操作。

快速接入

步骤1:环境配置及开通服务

在进行快速接入之前,请参考 准备工作 集成组件并实现登录。

步骤2: 安装依赖

npm
pnpm
yarn
npm install tuikit-atomicx-vue3 @tencentcloud/uikit-base-component-vue3 --save
pnpm add tuikit-atomicx-vue3 @tencentcloud/uikit-base-component-vue3
yarn add tuikit-atomicx-vue3 @tencentcloud/uikit-base-component-vue3

步骤3:主播开播页面接入

在您的项目下创建 live-pusher.vue 文件,通过复制如下代码至您新建的 live-pusher.vue 文件中集成完整的主播开播页面
注意:
您可以直接复制如下代码至您的工程中集成示例工程,也可以访问 开播页面 地址,查看更加详细的源码内容。
<template>
<UIKitProvider language="zh-CN" theme="dark">
<div class="custom-live-pusher">
<!-- 主要内容区 -->
<div class="main-content">
<!-- 左侧:视频源和工具 -->
<div class="left-panel">
<div class="scene-section">
<div class="panel-title">画面源</div>
<LiveScenePanel />
</div>
</div>

<!-- 中央:直播画面 -->
<div class="center-panel">
<div class="stream-section">
<div class="stream-header">
<div class="stream-title">
<span>{{ liveName }}</span>
</div>
<div class="stream-audience">{{ audienceCount }} 人在观看</div>
</div>
<StreamMixer />
</div>
<div class="live-controls">
<div class="bottom-panel">
<!-- <MediaDeviceSetting />// 媒体设置能力请参考本文高级功能集成部分
<CoGuestButton /> // 连麦观众能力请参考本文高级功能集成部分
<OrientationSwitch /> // 布局设置请参考本文高级功能集成部分
<LayoutSwitch /> // 横竖屏推流能力请参考本文高级功能集成部分 -->
</div>
<TUIButton type="primary" v-if="!currentLive?.liveId" @click="handleStartLive">开始直播</TUIButton>
<TUIButton color="red" v-else @click="handleStopLive">停止直播</TUIButton>
</div>
</div>

<!-- 右侧:观众互动 -->
<div class="right-panel">
<div class="audience-section">
<div class="panel-title">在线观众({{ audienceCount }})</div>
<LiveAudienceList />
</div>
<div class="message-section">
<div class="panel-title">消息列表</div>
<BarrageList />
<BarrageInput />
</div>
</div>

</div>
</div>
</UIKitProvider>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { UIKitProvider, TUIButton } from '@tencentcloud/uikit-base-component-vue3';
import { LiveScenePanel, StreamMixer, LiveAudienceList, BarrageList, BarrageInput, useLiveListState, useLiveAudienceState, useLoginState } from 'tuikit-atomicx-vue3';

const { login, setSelfInfo } = useLoginState();
const { createLive, currentLive, endLive } = useLiveListState();
const { audienceCount } = useLiveAudienceState();
const liveName = '我的直播间';

const handleStartLive = async () => {
// 创建直播间
await createLive({
liveId: 'my-live-room', // 直播间 ID
liveName: liveName, // 直播间名称
coverUrl: '', // 直播间封面 URL
isPublicVisible: true, // 是否公开可见
seatLayoutTemplateId: 600, // 麦位布局模板 ID
});
// 设置个人信息
await setSelfInfo({
userName: '我的名字/昵称', // 用户名
avatarUrl: '', // 头像 URL 地址
});
};

const handleStopLive = async () => {
await endLive();
};

async function initLogin() {
try {
await login({
sdkAppId: 0, // SDKAppID, 可以参考步骤 1 获取
userId: '', // UserID, 可以参考步骤 1 获取
userSig: '', // userSig, 可以参考步骤 1 获取
});
} catch (error) {
console.error('登录失败:', error);
}
}

onMounted(async () => {
await initLogin();
});

</script>

<style>html,body,#app{height:100%;width:100%;margin:0;padding:0}</style>
<style scoped>.custom-live-pusher,.left-panel{flex-direction:column;display:flex}.custom-live-pusher,.live-title{color:var(--text-color-primary)}.live-controls,.tools-section,.top-controls{backdrop-filter:blur(10px)}*{box-sizing:border-box;margin:0;padding:0}:global(::before){box-sizing:border-box;margin:0;padding:0}.layout-label,.template-options{margin-bottom:16px}:global(body){line-height:1.6;color:var(--text-color-primary);background:var(--bg-color-default)}.custom-live-pusher{height:100vh;width:100vw;background:linear-gradient(135deg,var(--bg-color-default) 0,var(--bg-color-function) 100%);overflow:hidden}.top-controls{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;background:var(--bg-color-operate);border-bottom:1px solid var(--stroke-color-primary);z-index:100;min-height:60px}.live-title{font-size:18px;font-weight:600;text-shadow:0 2px 4px var(--shadow-color)}.audience-count{font-size:14px;color:var(--text-color-error);background:var(--uikit-color-red-1);padding:6px 12px;border-radius:20px;border:1px solid var(--uikit-color-red-3)}.live-controls,.tools-section{background:var(--bg-color-operate)}.main-content{display:flex;flex:1;height:calc(100vh - 60px);gap:16px;padding:16px;overflow:hidden}.left-panel{width:320px;gap:16px;flex-shrink:0}.panel-title{font-size:16px;font-weight:600;color:var(--text-color-primary);margin-bottom:12px;text-align:left}.audience-section,.message-section,.scene-section{display:flex;flex-direction:column;background:var(--bg-color-operate);border-radius:12px;border:1px solid var(--stroke-color-primary);padding:16px;overflow:hidden}.scene-section{flex:1;min-height:0}.message-section{flex:1;min-height:0}.message-section :deep(input),.message-section :deep(textarea){text-align:left}.tools-section{border-radius:12px;padding:16px;border:1px solid var(--stroke-color-primary)}.center-panel{display:flex;flex-direction:column;flex:1;gap:16px;min-width:0}.stream-section{display:flex;flex-direction:column;flex:1;min-height:0;background:var(--bg-color-operate);border-radius:12px;border:1px solid var(--stroke-color-primary);overflow:hidden}.stream-header{display:flex;justify-content:space-between;align-items:center;padding:22px;border-bottom:1px solid var(--stroke-color-primary)}.stream-title{display:flex;align-items:center;gap:8px;font-size:18px;font-weight:500;color:var(--text-color-primary)}.stream-audience{font-size:14px;color:var(--text-color-primary)}.live-controls{display:flex;justify-content:space-between;align-items:center;padding:16px;border-radius:12px;border:1px solid var(--stroke-color-primary);gap:16px}.live-controls button{padding:18px 20px}.right-panel{display:flex;flex-direction:column;width:320px;gap:16px;flex-shrink:0}.center-panel>.live-controls,.left-panel>*,.right-panel>*{background:var(--bg-color-operate);border-radius:12px;border:1px solid var(--stroke-color-primary);overflow:hidden}.left-panel>*{padding:16px}.left-panel>.panel-title,.scene-section>.panel-title,.audience-section>.panel-title,.message-section>.panel-title{padding:0;background:transparent;border:none;border-radius:0}.custom-icon-container,.device-setting{padding:8px 12px;background:var(--bg-color-function)}.stream-section :deep(>*:last-child){flex:1;min-height:300px}.bottom-panel,.device-setting{align-items:center;display:flex}.custom-icon-container:hover,.option-card:hover{border-color:var(--stroke-color-primary);background:var(--list-color-hover)}.bottom-panel{gap:16px;flex:1}.device-setting{gap:8px;border-radius:8px;border:1px solid var(--stroke-color-secondary)}.device-icon{cursor:pointer;color:var(--text-color-primary);transition:color .2s}.device-icon:hover{color:var(--text-color-link)}.device-slider{width:80px}.custom-icon-container{display:flex;align-items:center;gap:6px;border-radius:8px;border:1px solid var(--stroke-color-secondary);cursor:pointer;transition:.2s;position:relative}.custom-icon-container.disabled{opacity:.5;cursor:not-allowed}.custom-icon-container.disabled:hover{background:var(--bg-color-function);border-color:var(--stroke-color-secondary)}.custom-icon{width:16px;height:16px;display:inline-block;background-size:contain;background-repeat:no-repeat;background-position:center}.custom-text{font-size:12px;color:var(--text-color-secondary);white-space:nowrap}.unread-count{position:absolute;top:-4px;right:-4px;background:var(--text-color-error);color:var(--text-color-button);border-radius:10px;padding:2px 6px;font-size:10px;font-weight:600;min-width:16px;text-align:center;line-height:1}.layout-label,.option-info h4{color:var(--text-color-primary)}.layout-dialog{max-width:600px}.layout-label{font-size:16px;font-weight:600}.options-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px}.option-card{padding:16px;background:var(--bg-color-function);border:2px solid var(--stroke-color-secondary);border-radius:8px;cursor:pointer;transition:.2s;text-align:center}.option-card.active{border-color:var(--text-color-link);background:var(--bg-color-operate)}.option-info h4{margin:8px 0 0;font-size:12px}.option-icon{width:32px;height:32px;margin:0 auto;color:var(--text-color-secondary)}.co-guest-dialog{max-width:500px}.co-guest-panel{min-height:300px}</style>

核心 API 参数说明

createLive
创建一个新的直播间,并设置直播间的基础信息。
参数
类型
必填
说明
liveId
String
直播间 ID,需保证全局唯一。
liveName
String
直播间名称,用于界面展示。
coverUrl
String
直播间封面图片的 URL 地址。
isPublicVisible
Boolean
是否公开可见。默认为 true,表示公开。
seatLayoutTemplateId
Number
麦位布局模板 ID。600 代表竖屏动态宫格布局,更多模板 ID 请参考高级功能章节。
setSelfInfo
设置用户信息。
参数
类型
必填
说明
userName
String
用户昵称,将在直播间及聊天区域显示。
avatarUrl
String
用户头像的 URL 地址。

步骤4:开启直播

开启您的第一场直播。
npm run dev
说明:
上述命令执行成功后,请在浏览器地址栏输入本地访问地址(您可以根据项目参考,例如 http://localhost:5173/live-pusher,具体端口号可能因您的项目配置而有所不同),即可看到开播页面。


步骤5:观看直播

方式一:(推荐)
通过我们提供的 在线观看网站,立刻观看您开启的直播。
方式二:
集成我们提供的 观众观看(Web 桌面浏览器)或者 观众观看(H5 移动端浏览器) 页面,观看直播。
重要提示:
请使用不同的用户 ID 登录观看: 开播端和观看端必须使用不同的用户 ID,否则后登录的设备会强制先登录的设备下线(即“被踢下线”)。

高级功能集成

注意:
以下高级功能均是基于 步骤 3 中创建的 live-pusher.vue 示例文件进行的扩展。请根据您的业务需求,将下方的代码片段集成到 live-pusher.vue 的对应位置(template 区域或 script 逻辑中),以启用相应的能力。
支持媒体设置能力
支持横竖屏推流能力
支持连观众能力
支持布局设置能力
若您需要支持媒体设置能力,包括设置扬声器、麦克风音量大小等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<!-- MediaDeviceSetting 媒体设备设置 -->
<template>
<div class="media-device-container">
<!-- MicVolumeSetting 麦克风设置 -->
<div class="device-setting">
<TUIIcon class="device-icon" :icon="microphoneIsOn ? IconAudioOpen : IconAudioClose" :size="20" @click="switchMicrophoneStatus" />
<TUISlider v-model="microphoneVolume" class="device-slider" :min="0" :max="100" :disabled="!microphoneIsOn" @change="handleMicrophoneVolumeChange" />
</div>
<!-- SpeakerVolumeSetting 扬声器设置 -->
<div class="device-setting">
<TUIIcon class="device-icon" :icon="speakerIsOn ? IconSpeakerOn : IconSpeakerOff" :size="20" @click="switchSpeaker(!speakerIsOn)" />
<TUISlider v-model="speakerVolume" class="device-slider" :min="0" :max="100" :disabled="!speakerIsOn" @change="handleSpeakerVolumeChange" />
</div>
</div>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue';
import { TUIIcon, TUISlider, IconSpeakerOn, IconSpeakerOff, IconAudioOpen, IconAudioClose } from '@tencentcloud/uikit-base-component-vue3';
import { useDeviceState } from 'tuikit-atomicx-vue3';

const {
captureVolume,
setCaptureVolume,
openLocalMicrophone,
closeLocalMicrophone,
} = useDeviceState();
const { outputVolume, setOutputVolume } = useDeviceState();
const microphoneVolume = ref(captureVolume.value);
const speakerVolume = ref(outputVolume.value);
const microphoneIsOn = ref(true);
const speakerIsOn = ref(true);
const templateSpeakerVolume = ref(outputVolume.value);
const templateMicrophoneVolume = ref(captureVolume.value);

const handleMicrophoneVolumeChange = (value: number) => {
if (value !== captureVolume.value) {
setCaptureVolume(value);
}
};

const switchMicrophoneStatus = () => {
microphoneIsOn.value = !microphoneIsOn.value;
if (!microphoneIsOn.value) {
templateMicrophoneVolume.value = captureVolume.value;
closeLocalMicrophone();
} else {
openLocalMicrophone();
setCaptureVolume(templateMicrophoneVolume.value);
}
};

const switchSpeaker = (open: boolean) => {
speakerIsOn.value = open;
if (!open) {
templateSpeakerVolume.value = outputVolume.value;
setOutputVolume(0);
} else {
setOutputVolume(templateSpeakerVolume.value);
}
};

const handleSpeakerVolumeChange = (value: number) => {
if (value !== outputVolume.value) {
setOutputVolume(value);
}
};

watch(captureVolume, (newVal) => {
microphoneVolume.value = newVal;
});

watch(outputVolume, (newVal) => {
speakerVolume.value = newVal;
});
</script>

<style scoped>.media-device-container{display:flex;align-items:center;gap:12px}.device-setting{display:flex;align-items:center;gap:8px;padding:12px 16px;background:var(--bg-color-function);border-radius:8px;border:1px solid var(--stroke-color-secondary)}.device-slider{width:100px}.device-icon{cursor:pointer;color:var(--text-color-primary);transition:color .2s}.device-icon:hover{color:var(--text-color-link)}</style>

若您需要支持横竖屏推流能力,包括设置切换横竖屏等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<!-- LayoutSwitch 横竖屏推流设置 -->
<template>
<div
class="custom-icon-container"
:class="{ disabled: currentLive?.liveId }"
@click="handleOrientationSwitch"
>
<IconHorizontalMode
v-if="currentOrientation === LiveOrientation.Landscape"
class="custom-icon"
/>
<IconPortrait
v-else
class="custom-icon"
/>
<span class="custom-text co-guest-text">{{
currentOrientation === LiveOrientation.Portrait ? t('Portrait') : t('Landscape')
}}</span>
</div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { useUIKit, TUIToast, TOAST_TYPE, IconPortrait, IconHorizontalMode } from '@tencentcloud/uikit-base-component-vue3';
import { useLiveListState, LiveOrientation } from 'tuikit-atomicx-vue3';

const { t } = useUIKit();

enum TUISeatLayoutTemplate {
LandscapeDynamic_1v3 = 200,
PortraitDynamic_Grid9 = 600,
PortraitDynamic_1v6 = 601,
PortraitFixed_Grid9 = 800,
PortraitFixed_1v6 = 801,
PortraitFixed_6v6 = 802,
}

const { currentLive, updateLiveInfo } = useLiveListState();
const currentOrientation = ref(LiveOrientation.Portrait);

watch(
() => currentLive.value?.layoutTemplate,
newVal => {
if (newVal === TUISeatLayoutTemplate.LandscapeDynamic_1v3) {
currentOrientation.value = LiveOrientation.Landscape;
} else {
currentOrientation.value = LiveOrientation.Portrait;
}
},
{ immediate: true }
);

const handleOrientationSwitch = () => {
if (currentLive.value?.liveId) {
TUIToast({
message: t('Cannot switch orientation during live streaming'),
type: TOAST_TYPE.ERROR,
});
return;
}
if (currentOrientation.value === LiveOrientation.Portrait) {
updateLiveInfo({ layoutTemplate: TUISeatLayoutTemplate.LandscapeDynamic_1v3 });
} else {
updateLiveInfo({ layoutTemplate: TUISeatLayoutTemplate.PortraitDynamic_Grid9 });
}
};
</script>

<style scoped>.custom-icon-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:56px;height:56px;cursor:pointer;color:var(--text-color-primary);border-radius:12px;position:relative}.custom-icon{width:24px;height:24px;background:transparent}.custom-text{font-size:12px}.custom-icon-container:not(.disabled):hover{box-shadow:0 0 10px 0 var(--bg-color-mask)}.custom-icon-container:not(.disabled):hover .custom-icon,.custom-icon-container:not(.disabled):hover .custom-text{color:var(--text-color-link)}.custom-icon-container.disabled{cursor:not-allowed;opacity:.5;color:var(--text-color-secondary)}.custom-icon-container.disabled .custom-icon,.custom-icon-container.disabled .custom-text{color:var(--text-color-secondary);cursor:not-allowed}</style>
若您需要支持观众连麦能力,包括设置连麦申请、连麦管理等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<!-- CoGuestButton 观众连麦设置 -->
<template>
<div class="custom-icon-container" :class="{ disabled: disabled }" @click="handleCoGuest">
<span v-if="applicants.length > 0" class="unread-count">{{ applicants.length }}</span>
<IconCoGuest class="custom-icon" />
<span class="custom-text co-guest-text">{{ t('CoGuest') }}</span>
</div>
<TUIDialog :title="t('CoGuest')" :visible="coGuestPanelVisible" :custom-classes="['co-guest-dialog']" @close="coGuestPanelVisible = false" @confirm="coGuestPanelVisible = false" @cancel="coGuestPanelVisible = false">
<CoGuestPanel class="co-guest-panel" />
<template #footer>
<div />
</template>
</TUIDialog>
</template>

<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useUIKit, TUIDialog, TUIToast, TOAST_TYPE, IconCoGuest } from '@tencentcloud/uikit-base-component-vue3';
import { CoGuestPanel, CoHostStatus, useCoGuestState, useCoHostState, useLiveListState } from 'tuikit-atomicx-vue3';

const { t } = useUIKit();
const { applicants } = useCoGuestState();
const { currentLive } = useLiveListState();
const { coHostStatus } = useCoHostState();
const disabled = computed(() => !currentLive.value?.liveId || coHostStatus.value !== CoHostStatus.Disconnected);

const coGuestPanelVisible = ref(false);

const handleCoGuest = () => {
if (disabled.value) {
const message = !currentLive.value?.liveId
? t('Cannot use co-guest before live starts')
: t('Cannot enable audience co-hosting while co-hosting with other hosts');
TUIToast({ type: TOAST_TYPE.ERROR, message });
return;
}
coGuestPanelVisible.value = true;
};

watch(disabled, () => {
if (disabled.value) {
coGuestPanelVisible.value = false;
}
});
</script>

<style scoped>.custom-icon-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:56px;height:56px;cursor:pointer;color:var(--text-color-primary);border-radius:12px;position:relative}.unread-count{position:absolute;top:0;right:0;background-color:var(--text-color-error);border-radius:50%;width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:12px}.custom-icon{width:24px;height:24px;background:transparent}.custom-text{font-size:12px}.custom-icon-container:not(.disabled):hover{box-shadow:0 0 10px 0 var(--bg-color-mask)}.custom-icon-container:not(.disabled):hover .custom-icon,.custom-icon-container:not(.disabled):hover .custom-text{color:var(--text-color-link)}.custom-icon-container.disabled{cursor:not-allowed;opacity:.5;color:var(--text-color-secondary)}.custom-icon-container.disabled .custom-icon{cursor:not-allowed}.custom-icon-container.disabled .custom-text{color:var(--text-color-secondary)}.co-guest-panel{height:560px}:deep(.co-guest-dialog){width:520px}</style>
若您需要支持视频流切换布局能力,包括设置动态宫格布局、静态宫格布局、静态小窗布局、浮动小窗布局等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<!-- OrientationSwitch 布局设置 -->
<template>
<div
class="custom-icon-container"
:class="{ 'disabled': disabled }"
@click="handleSwitchLayout"
>
<IconLayoutTemplate class="custom-icon" />
<span class="custom-text setting-text">{{ t('Layout Settings') }}</span>
</div>
<TUIDialog
:customClasses="['layout-dialog']"
:title="t('Layout Settings')"
:visible="layoutSwitchVisible"
@close="handleCancel"
@confirm="handleConfirm"
@cancel="handleCancel"
appendTo="body"
>
<div class="layout-label">
{{ t('Audience Layout') }}
</div>
<div class="template-options">
<div class="options-grid">
<template
v-for="template in layoutOptions"
:key="template.id"
>
<div
class="option-card"
:class="{ active: selectedTemplate === template.templateId }"
@click="selectTemplate(template.templateId)"
>
<div class="option-info">
<component
:is="template.icon"
v-if="template.icon"
class="option-icon"
/>
<h4>{{ template.label }}</h4>
</div>
</div>
</template>
</div>
</div>
</TUIDialog>
</template>

<script lang="ts" setup>
import { ref, computed, watch, h, defineComponent } from 'vue';
import { TUIErrorCode } from '@tencentcloud/tuiroom-engine-js';
import { useUIKit, TUIDialog, TUIToast, TOAST_TYPE, IconLayoutTemplate } from '@tencentcloud/uikit-base-component-vue3';
import { useLiveListState, useCoHostState, CoHostStatus } from 'tuikit-atomicx-vue3';

/**
* 直播间麦位排版模板
*/
enum TUISeatLayoutTemplate {
LandscapeDynamic_1v3 = 200,
PortraitDynamic_Grid9 = 600,
PortraitDynamic_1v6 = 601,
PortraitFixed_Grid9 = 800,
PortraitFixed_1v6 = 801,
PortraitFixed_6v6 = 802,
}

const createSvgIcon = (pathD: string) => defineComponent({
render: () => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'currentColor', width: '24', height: '24' }, [h('path', { d: pathD })])
});

const Dynamic1v6 = createSvgIcon('M3 3h7v7H3V3zm11 0h7v4h-7V3zm0 6h7v4h-7V9zm0 6h7v6h-7v-6zM3 12h7v9H3v-9z');
const DynamicGrid9 = createSvgIcon('M3 3h5v5H3V3zm7 0h4v5h-4V3zm6 0h5v5h-5V3zM3 10h5v4H3v-4zm7 0h4v4h-4v-4zm6 0h5v4h-5v-4zM3 16h5v5H3v-5zm7 0h4v5h-4v-5zm6 0h5v5h-5v-5z');
const Fixed1v6 = createSvgIcon('M2 2h9v9H2V2zm11 0h9v4h-9V2zm0 5h9v4h-9V7zm0 5h9v4h-9v-4zm0 5h9v5h-9v-5zM2 13h9v9H2v-9z');
const FixedGrid9 = createSvgIcon('M2 2h6v6H2V2zm7 0h6v6H9V2zm7 0h6v6h-6V2zM2 9h6v6H2V9zm7 0h6v6H9V9zm7 0h6v6h-6V9zM2 16h6v6H2v-6zm7 0h6v6H9v-6zm7 0h6v6h-6v-6z');
const HorizontalFloat = createSvgIcon('M2 4h20v12H2V4zm3 14h4v2H5v-2zm6 0h4v2h-4v-2zm6 0h4v2h-4v-2z');

const { t } = useUIKit();
const { currentLive, updateLiveInfo } = useLiveListState();
const { coHostStatus } = useCoHostState();
const disabled = computed(() => coHostStatus.value === CoHostStatus.Connected);

watch(
() => currentLive.value?.liveId,
(liveId) => {
if (!liveId) {
updateLiveInfo({ layoutTemplate: TUISeatLayoutTemplate.PortraitDynamic_Grid9 });
}
},
{ immediate: true },
);

const layoutSwitchVisible = ref(false);

const handleSwitchLayout = () => {
if (disabled.value) {
TUIToast({ type: TOAST_TYPE.ERROR, message: t('Layout switching is not available during co-hosting') });
return;
}
layoutSwitchVisible.value = true;
};

const portraitLayoutOptions = computed(() => [
{
id: 'PortraitDynamic_Grid9',
icon: DynamicGrid9,
templateId: TUISeatLayoutTemplate.PortraitDynamic_Grid9,
label: t('Dynamic Grid9 Layout'),
},
{
id: 'PortraitFixed_1v6',
icon: Fixed1v6,
templateId: TUISeatLayoutTemplate.PortraitFixed_1v6,
label: t('Fixed 1v6 Layout'),
},
{
id: 'PortraitFixed_Grid9',
icon: FixedGrid9,
templateId: TUISeatLayoutTemplate.PortraitFixed_Grid9,
label: t('Fixed Grid9 Layout'),
},
{
id: 'PortraitDynamic_1v6',
icon: Dynamic1v6,
templateId: TUISeatLayoutTemplate.PortraitDynamic_1v6,
label: t('Dynamic 1v6 Layout'),
},
]);

const horizontalLayoutOptions = computed(() => [
{
id: 'LandscapeDynamic_1v3',
icon: HorizontalFloat,
templateId: TUISeatLayoutTemplate.LandscapeDynamic_1v3,
label: t('Landscape Template'),
},
]);

const layoutOptions = computed(() => {
if (currentLive.value && currentLive.value?.layoutTemplate >= 200 && currentLive.value?.layoutTemplate <= 599) {
return horizontalLayoutOptions.value;
}
return portraitLayoutOptions.value;
});

const selectedTemplate = ref<TUISeatLayoutTemplate | null>(currentLive.value?.layoutTemplate ?? null);

function selectTemplate(template: TUISeatLayoutTemplate) {
selectedTemplate.value = template;
}

watch(() => currentLive.value?.layoutTemplate, (newVal) => {
if (newVal) {
selectedTemplate.value = newVal;
}
});

async function handleConfirm() {
if (selectedTemplate.value) {
try {
await updateLiveInfo({ layoutTemplate: selectedTemplate.value });
layoutSwitchVisible.value = false;
} catch (error: any) {
let errorMessage = t('Layout switch failed');
if (error.code === TUIErrorCode.ERR_FREQ_LIMIT) {
errorMessage = t('Operation too frequent, please try again later');
}
TUIToast({ type: TOAST_TYPE.ERROR, message: errorMessage });
}
} else {
layoutSwitchVisible.value = false;
}
}

function handleCancel() {
selectedTemplate.value = currentLive.value?.layoutTemplate ?? null;
layoutSwitchVisible.value = false;
}
</script>

<style scoped>
.custom-icon-container { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; width: 56px; height: 56px; cursor: pointer; color: var(--text-color-primary); border-radius: 12px; position: relative; } .custom-icon-container .custom-icon { display: inline-block; width: 24px; height: 24px; background: transparent; } .custom-icon-container .custom-text { font-size: 12px; font-weight: 400; } .custom-icon-container:not(.disabled):hover { box-shadow: 0 0 10px 0 var(--bg-color-mask); } .custom-icon-container:not(.disabled):hover .custom-icon { color: var(--text-color-link); } .custom-icon-container:not(.disabled):hover .custom-text { color: var(--text-color-link); } .custom-icon-container.disabled { cursor: not-allowed; opacity: 0.5; color: var(--text-color-tertiary); } .custom-icon-container.disabled .custom-icon { color: var(--text-color-tertiary); cursor: not-allowed; } .custom-icon-container.disabled .custom-text { color: var(--text-color-tertiary); } :deep(.layout-dialog) { padding: 24px; width: 480px; } :deep(.layout-dialog) .dialog-body { flex-wrap: wrap; } :deep(.layout-dialog) .dialog-footer { padding-top: 32px; } .layout-label { font-size: 14px; font-weight: 400; color: var(--text-color-primary, #ffffff); margin: 4px 0px 16px 0px; } .template-options { width: 100%; height: 100%; overflow: auto; } .template-options .options-grid { display: flex; flex-wrap: wrap; gap: 16px; justify-content: flex-start; } .template-options .options-grid .option-card { box-sizing: border-box; padding: 12px 13px; width: 208px; background: #3a3a3a; border: 2px solid transparent; border-radius: 12px; cursor: pointer; transition: all 0.2s ease; text-align: center; } .template-options .options-grid .option-card:hover { background: #4a4a4a; border-color: #5a5a5a; } .template-options .options-grid .option-card.active { border: 2px solid var(--text-color-link-hover, #2B6AD6); background: var(--list-color-focused, #243047); } .template-options .options-grid .option-card.active .option-info h4 { color: #ffffff; } .template-options .options-grid .option-card .option-info { display: flex; align-items: center; justify-content: flex-start; gap: 8px; } .template-options .options-grid .option-card .option-info .option-icon { width: 24px; height: 24px; } .template-options .options-grid .option-card .option-info h4 { margin: 0; font-size: 14px; font-weight: 600; color: #ffffff; transition: color 0.2s ease; }
</style>

自定义您的界面布局

TUILiveKit 支持灵活定制开播页与直播页的功能和样式,您可根据业务需求调整布局、隐藏 / 显示功能模块。

横竖屏推流设置

TUILiveKit 支持横屏竖屏两种推流模式,您可根据直播场景选择合适的推流方向:
推流模式
适用场景
说明
竖屏推流
秀场直播、电商带货、聊天互动
默认模式,适合移动端观看,画面比例为 9:16。
横屏推流
游戏直播、在线教育、会议直播
适合 PC 端观看,画面比例为 16:9。
注意:
横竖屏切换必须在开播前进行设置,直播过程中无法切换推流方向。

通过 UI 交互切换

在主播开播页的底部控制栏,点击横屏 / 竖屏按钮即可切换推流方向:
显示竖屏时,当前为竖屏推流模式
显示横屏时,当前为横屏推流模式



通过代码设置

您也可以通过调用 updateLiveInfo 方法,在代码中设置推流方向:
import { useLiveListState } from 'tuikit-atomicx-vue3';

const { updateLiveInfo } = useLiveListState();

// 切换为横屏模式(模板 ID: 200)
updateLiveInfo({ layoutTemplate: 200 });

// 切换为竖屏模式(模板 ID: 600)
updateLiveInfo({ layoutTemplate: 600 });
说明:
横屏模式对应的布局模板 ID 范围为 200-599,默认使用 200(横屏浮动布局)。
竖屏模式对应的布局模板 ID 范围为 600-899,默认使用 600(动态宫格布局)。
切换横竖屏时,系统会自动切换到对应方向的默认布局模板。

直播布局模板选择

TUILiveKit 提供多种直播布局模板,用于控制主播与连麦嘉宾的画面排列方式。您可在主播开播页的布局设置选择合适样式:


竖屏模式布局模板

竖屏模式下提供 4 种布局模板,适用于秀场直播、电商带货等场景:
名称
动态宫格布局
动态 1V6 布局
静态宫格布局
静态小窗布局
模板 ID
600
601
800
801
描述
默认布局,根据连麦人数动态调整宫格大小,画面自适应填充。
主播画面居中大窗显示,连麦嘉宾以浮动小窗形式环绕在主播周围。
固定 9 宫格布局,每个嘉宾占据一个固定大小的宫格位置。
主播画面全屏显示,连麦嘉宾以固定小窗形式显示在画面边缘。
适用场景
通用场景,连麦人数不固定。
主播为主、嘉宾为辅的互动场景。
多人连麦、语音聊天室等固定人数场景。
主播全屏展示、嘉宾辅助的场景。

横屏模式布局模板

横屏模式下提供 1 种布局模板,适用于游戏直播、在线教育等场景:
名称
横屏浮动布局
模板 ID
200
描述
主播画面全屏显示,连麦嘉宾以浮动小窗形式显示在画面底部。
适用场景
游戏直播、屏幕分享、在线教育等需要横屏展示的场景。

通过代码设置布局模板

您可以通过调用 updateLiveInfo 方法,在代码中设置布局模板:
import { useLiveListState } from 'tuikit-atomicx-vue3';

const { updateLiveInfo } = useLiveListState();

// 设置竖屏动态宫格布局
updateLiveInfo({ layoutTemplate: 600 });

// 设置竖屏动态 1v6 布局
updateLiveInfo({ layoutTemplate: 601 });

// 设置竖屏静态宫格布局
updateLiveInfo({ layoutTemplate: 800 });

// 设置竖屏静态 1v6 布局
updateLiveInfo({ layoutTemplate: 801 });

// 设置横屏浮动布局
updateLiveInfo({ layoutTemplate: 200 });
注意:
布局模板可以在开播前直播中进行切换。
在主播 PK 连线期间,无法切换布局模板。
切换横竖屏推流方向时,布局模板会自动切换为对应方向的默认模板。

自由定制

颜色主题及语言

通过配置 App.vueUIKitProvider 的入参,修改主题及语言的默认值。
UIKitProvider 参数
可选值
默认值
theme
"light" | "dark"
"light"
language
"zh-CN" | "en-US"
"en-US"
<UIKitProvider theme="light">
<router-view />
</UIKitProvider>
<script setup lang="ts">
import { UIKitProvider } from '@tencentcloud/uikit-base-component-vue3';

按钮 Button 和图标 Icon

若您需要对按钮 Button 或图标 Icon 等其他控件进行新增或替换等 UI 定制,您可以通过如下方式实现,以live-player.vue 文件中的按钮和图标为例,您可以参考下图找到对应按钮或图标的指定位置源码,对当前部分的控件进行增加、删除、替换等 UI 定制操作。

根据上述示例,我们同样支持您根据项目需求对观众观看页面进行 UI 定制的能力。除了页面 UI 布局调整,我们也支持您对颜色主题、字体、圆角、按钮、图标、输入框、弹框等内容进行增加、删除、修改等操作,满足您的 UI 定制需要。
类别
功能
描述
组件详细定制指引
素材管理
自定义素材管理区域展示
支持:
调整展示 Icon 的大小、颜色或对 Icon 进行替换。
在线观众
自定义观众信息展示
支持:
展示/隐藏观众等级。
观众信息字体、颜色 UI 自定义设置。
替换为您需要的 Icon 风格。
消息列表
自定义消息弹幕区域展示
支持:
展示/隐藏聊天输入区域。
支持 UI 定制聊天气泡风格、定制观众等级等内容。

下一步

恭喜您,现在您已经成功集成了主播开播页面 。接下来,您可以实现观众观看页直播列表页UI 自定义等内容,可参考下表:
功能
描述
集成指引
观众观看
实现观众进入主播的直播间后观看直播,实现观众连麦 、直播间信息、在线观众、弹幕显示等功能。
直播列表
展示直播列表界面和功能,包含直播列表,房间信息展示功能。
UI 自定义
更详细的 UI 组件自定义指引。
概述