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

最近更新时间:2025-09-17 10:11:12

我的收藏
本文对 TUILiveKit Demo 中的主播开播页面进行了详细的介绍,您可以在已有项目中直接参考本文档集成我们开发好的主播开播页面,也可以根据您的需求按照文档中的内容对页面的样式,布局以及功能项进行深度的定制。

功能概览

功能分类
具体能力
视频源控制
支持摄像头、屏幕共享、图片等多种元素合流。同时,组件内置了强大的画布编辑能力,支持对元素进行旋转、移动、缩放、镜像、层级调整等操作,让您的直播画面更具创意和专业性
连麦功能
支持主播与观众实时音视频互动,并提供了九宫格、1v6、浮窗等多种连麦布局,轻松应对各类互动场景
观众互动
集成实时弹幕、礼物、点赞等功能,增强主播与观众的粘性
横竖屏开播
满足不同场景的直播需求
观众管理
便捷的观众列表与禁言管理

功能展示

主播开播页面提供默认行为和风格,但如果默认行为和样式不能完全满足您的需求,自定义 UI 可见本文中自由定制部分。如下图主播开播页面功能展示主要包含素材管理、直播工具、音视频控制、画面源输出、素材编辑工具、弹幕聊天、观众列表
横屏开播

竖屏开播


快速接入

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

在进行快速接入之前,您需要参见 准备工作,满足相关环境配置及开通对应服务。

步骤2: 安装依赖

npm
pnpm
yarn
npm install tuikit-atomicx-vue3 @tencentcloud/uikit-base-component-vue3 --save
pnpm add tuikit-atomicx-vue3 @tencentcloud/livekit-web-vue3 @tencentcloud/uikit-base-component-vue3
yarn add tuikit-atomicx-vue3 @tencentcloud/livekit-web-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="top-controls">
<div class="live-title">{{ liveName }}</div>
<div class="audience-count">{{ audienceCount }} 人观看</div>
</div>

<!-- 主要内容区 -->
<div class="main-content">
<!-- 左侧:视频源和工具 -->
<div class="left-panel">
<LiveScenePanel />
<div class="tools-section">
<CoGuestButton />
</div>
</div>

<!-- 中央:直播画面 -->
<div class="center-panel">
<StreamMixer />
<div class="live-controls">
<button @click="handleStartLive">开始直播</button>
</div>
</div>

<!-- 右侧:观众互动 -->
<div class="right-panel">
<LiveAudienceList />
<BarrageList />
<BarrageInput />
</div>
<!-- 底部:主播操作 -->
<div class="bottom-panel">
<!-- <MicVolumeSetting /> // 媒体设置能力请参考本文高级功能集成部分
<SpeakerVolumeSetting />
<CoGuestButton /> // 连麦观众能力请参考本文高级功能集成部分
<OrientationSwitch /> // 布局设置请参考本文高级功能集成部分
<LayoutSwitch /> --> // 横竖屏推流能力请参考本文高级功能集成部分
</div>
</div>
</div>
</UIKitProvider>
</template>

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

const { login } = useLoginState();
const { createLive } = useLiveState();
const { audienceCount } = useLiveAudienceState();
const liveName = '我的直播间';

const handleStartLive = async () => {
await createLive({
liveId: 'my-live-room',
liveName: liveName,
});
};

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 scoped>:global(::before){box-sizing:border-box;margin:0;padding:0}:global(body){line-height:1.6;color:var(--text-color-primary);background:var(--bg-color-default)}.custom-live-pusher{display:flex;flex-direction:column;height:100vh;width:100vw;background:linear-gradient(135deg,var(--bg-color-default) 0,var(--bg-color-function) 100%);color:var(--text-color-primary);overflow:hidden}.top-controls{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;background:var(--bg-color-operate);backdrop-filter:blur(10px);border-bottom:1px solid var(--stroke-color-primary);z-index:100;min-height:60px}.live-title{font-size:18px;font-weight:600;color:var(--text-color-primary);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)}.main-content{display:flex;flex:1;height:calc(100vh - 60px);gap:16px;padding:16px;overflow:hidden}.left-panel{display:flex;flex-direction:column;width:280px;gap:16px;flex-shrink:0}.tools-section{background:var(--bg-color-operate);border-radius:12px;padding:16px;border:1px solid var(--stroke-color-primary);backdrop-filter:blur(10px)}.center-panel{display:flex;flex-direction:column;flex:1;gap:16px;min-width:0}.live-controls{display:flex;justify-content:space-between;align-items:center;padding:16px;background:var(--bg-color-operate);border-radius:12px;border:1px solid var(--stroke-color-primary);backdrop-filter:blur(10px);gap:16px}.live-controls button{background:linear-gradient(135deg,var(--text-color-error) 0,var(--uikit-color-red-5) 100%);color:var(--text-color-button);border:none;padding:12px 32px;border-radius:25px;font-size:16px;font-weight:600;cursor:pointer;box-shadow:0 4px 15px var(--shadow-color)}.right-panel{display:flex;flex-direction:column;width:320px;gap:16px;flex-shrink:0}.center-panel>*,.left-panel>*,.right-panel>*{background:var(--bg-color-operate);border-radius:12px;border:1px solid var(--stroke-color-primary);backdrop-filter:blur(10px);overflow:hidden}.left-panel>*{padding:16px}.right-panel>*{padding:16px}.center-panel>:first-child{flex:1;min-height:300px;padding:0;background:var(--uikit-color-black-1);border:2px solid var(--stroke-color-secondary)}.bottom-panel{display:flex;align-items:center;gap:16px;flex:1}.device-setting{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--bg-color-function);border-radius:8px;border:1px solid var(--stroke-color-secondary)}.device-icon{cursor:pointer;color:var(--text-color-primary);transition:color .2s ease}.device-icon:hover{color:var(--text-color-link)}.device-slider{width:80px}.custom-icon-container{display:flex;align-items:center;gap:6px;padding:8px 12px;background:var(--bg-color-function);border-radius:8px;border:1px solid var(--stroke-color-secondary);cursor:pointer;transition:all .2s ease;position:relative}.custom-icon-container:hover{background:var(--list-color-hover);border-color:var(--stroke-color-primary)}.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}.horizontal-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Crect x='3' y='6' width='18' height='12' rx='2'/%3E%3Cpath d='M7 10h10'/%3E%3C/svg%3E")}.portrait-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Crect x='6' y='3' width='12' height='18' rx='2'/%3E%3Cpath d='M10 7h4'/%3E%3C/svg%3E")}.layout-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Crect x='3' y='3' width='7' height='7'/%3E%3Crect x='14' y='3' width='7' height='7'/%3E%3Crect x='14' y='14' width='7' height='7'/%3E%3Crect x='3' y='14' width='7' height='7'/%3E%3C/svg%3E")}.co-guest-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='9' cy='7' r='4'/%3E%3Cpath d='M23 21v-2a4 4 0 0 0-3-3.87'/%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75'/%3E%3C/svg%3E")}.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-dialog{max-width:600px}.layout-label{font-size:16px;font-weight:600;color:var(--text-color-primary);margin-bottom:16px}.template-options{margin-bottom:16px}.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:all .2s ease;text-align:center}.option-card:hover{border-color:var(--stroke-color-primary);background:var(--list-color-hover)}.option-card.active{border-color:var(--text-color-link);background:var(--bg-color-operate)}.option-info h4{margin:8px 0 0 0;font-size:12px;color:var(--text-color-primary)}.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>

步骤4:开启直播

开启您的第一次主播开播。
npm run dev

高级功能集成

支持媒体设置能力
支持横竖屏推流能力
支持连观众能力
支持布局设置能力
若您需要支持媒体设置能力,包括设置扬声器、麦克风音量大小等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<template>
<!-- MicVolumeSetting 麦克风设置 -->
<div class="device-setting">
<IconAudio :size="16" :audioVolume="audioVolume" :isMuted="microphoneStatus === DeviceStatus.Off" @click="switchMicrophoneStatus" />
<TUISlider v-if="microphoneStatus !== DeviceStatus.Off" v-model="microphoneVolume" class="device-slider" :min="0" :max="100" @change="handleMicrophoneVolumeChange" />
<TUISlider v-else class="device-slider" :min="0" :max="100" disabled />
</div>
<!-- SpeakerVolumeSetting 扬声器设置 -->
<div class="device-setting">
<TUIIcon class="device-icon" :icon="speakerIsOn ? IconSpeakerOn : IconSpeakerOff" @click="switchSpeaker(!speakerIsOn)" />
<TUISlider v-if="speakerIsOn" v-model="speakerVolume" class="device-slider" :min="0" :max="100" @change="handleSpeakerVolumeChange" />
</div>
</template>

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

const {
captureVolume,
setCaptureVolume,
microphoneStatus,
openLocalMicrophone,
closeLocalMicrophone,
audioVolume,
} = useDeviceState();
const { outputVolume, setOutputVolume } = useDeviceState();

const microphoneVolume = ref(captureVolume.value);
const speakerVolume = ref(outputVolume.value);
const speakerIsOn = ref(true);
const templateSpeakerVolume = ref(outputVolume.value);

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

const switchMicrophoneStatus = () => {
if (microphoneStatus.value === DeviceStatus.On) {
closeLocalMicrophone();
} else {
openLocalMicrophone();
}
};

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>

若您需要支持横竖屏推流能力,包括设置切换横竖屏等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<template>
<!-- LayoutSwitch 横竖屏推流设置 -->
<div class="custom-icon-container":class="{ 'disabled': localLiveStatus === LiveStatus.Live }" @click="handleOrientationSwitch">
<span v-if="currentOrientation === LiveOrientation.Landscape" class="custom-icon horizontal-icon" />
<span v-else class="custom-icon portrait-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 } from '@tencentcloud/uikit-base-component-vue3';
import { useLiveState, LiveOrientation, LiveStatus } 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, localLiveStatus, updateLiveInfo } = useLiveState();
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 (localLiveStatus.value === LiveStatus.Live) {
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>


若您需要支持观众连麦能力,包括设置连麦申请、连麦管理等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<template>
<!-- CoGuestSetting 观众连麦设置 -->
<div class="custom-icon-container" @click="handleCoGuest">
<span v-if="receivedCoGuestUserList.length > 0" class="unread-count">{{ receivedCoGuestUserList.length }}</span>
<span class="custom-icon co-guest-icon" />
<span class="custom-text co-guest-text">{{ t('CoGuest') }}</span>
</div>
<TUIDialog :title="t('CoGuest')" :visible="coGuestPanelVisible" :customClasses="['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 { ref } from 'vue';
import { useUIKit, TUIDialog } from '@tencentcloud/uikit-base-component-vue3';
import { CoGuestPanel, useCoGuestState } from 'tuikit-atomicx-vue3';

const { t } = useUIKit();
const { receivedCoGuestUserList } = useCoGuestState();

const coGuestPanelVisible = ref(false);

const handleCoGuest = () => {
coGuestPanelVisible.value = true;
};
</script>
若您需要支持视频流切换布局能力,包括设置动态宫格布局、静态宫格布局、静态小窗布局、浮动小窗布局等内容,请参考如下代码示例复制到live-pusher.vue 文件中即可。
<template>
<!-- OrientationSwitch 布局设置 -->
<div class="custom-icon-container" @click="handleSwitchLayout">
<span class="custom-icon layout-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">
<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 } from 'vue';
import { TUIErrorCode, } from '@tencentcloud/tuiroom-engine-js';
import { useUIKit, TUIDialog, TUIToast, TOAST_TYPE } from '@tencentcloud/uikit-base-component-vue3';
import { LiveStatus, useLiveState } from 'tuikit-atomicx-vue3';
import { TUISeatLayoutTemplate } from '../types/LivePusher';

const { t } = useUIKit();
const { localLiveStatus, currentLive, updateLiveInfo } = useLiveState();

watch(
localLiveStatus,
() => {
if (localLiveStatus.value === LiveStatus.IDLE) {
updateLiveInfo({ layoutTemplate: TUISeatLayoutTemplate.PortraitDynamic_Grid9 });
}
},
{ immediate: true },
);

const layoutSwitchVisible = ref(false);

const handleSwitchLayout = () => {
layoutSwitchVisible.value = true;
};

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

const horizontalLayoutOptions = computed(() => [
{
id: 'LandscapeDynamic_1v3',
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>

自由定制

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

颜色主题

参见步骤3中代码示例,您可以使用操作 theme 的值来满足您切换颜色主题。
<UIKitProvider theme="dark"> // theme 传入 dark 时,界面整体颜色主题为黑色
xxx // theme 传入 light 时,界面整体颜色主题为白色
</UIKitProvider>

按钮 Button / 图标 Icon

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


下一步

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