注意:集成提示:
注意:这篇档介绍基于 UI 组件 的观众观看方式(含完整交互 UI)。若你需通过 Core SDK 自行搭建观众端界面,请参考 观众观看 (Core SDK)。
一、功能展示
默认的观看页面效果如下所示,主要包括:直播信息展示、视频播放、礼物面板、在线观众、互动聊天、播放控制等功能。
·视频播放:高清流畅的直播体验,同时支持多分辨率切换。
·互动聊天:支持实时弹幕消息,发送文字、表情消息。
·礼物面板:支持发送礼物,打赏主播。
·播放控制:支持暂停、恢复、分辨率切换、音量调节、画中画播放、全屏播放。
二、步骤1:环境配置及开通服务
在进行快速接入之前,请参考 准备工作 集成组件并实现登录。
三、步骤2:安装依赖
你可以选择以下任一方式安装依赖:
npmpnpmyarnnpm install tuikit-atomicx-react @tencentcloud/uikit-base-component-react –savenpm install sass –save-devpnpm add tuikit-atomicx-react @tencentcloud/uikit-base-component-reactpnpm add sass –devyarn add tuikit-atomicx-react @tencentcloud/uikit-base-component-reactyarn add sass –dev
四、步骤3:观看页面接入
在你的项目下创建 LivePlayerView.tsx 和 LivePlayerView.module.scss 文件,可直接复制如下代码至你的项目中集成观看页面。
注意:你可以直接复制如下代码至你的工程中集成示例工程,也可以访问 观众观看 地址,查看更加详细的源码内容。
LivePlayerView.tsxLivePlayerView.module.scssimport React, { useEffect, useCallback } from 'react';import { useSearchParams, useNavigate } from 'react-router-dom';import TUIRoomEngine, { TUIRoomEvents } from "@tencentcloud/tuiroom-engine-js";import { Avatar, LiveView, LiveGift, LiveListEvent, BarrageList, BarrageInput, LiveAudienceList, useLiveListState, useLiveAudienceState, useLoginState, useRoomEngine } from 'tuikit-atomicx-react';import { UIKitProvider, IconChevronLeft, MessageBox, Dialog, useUIKit } from '@tencentcloud/uikit-base-component-react';import styles from './LivePlayerView.module.scss';interface LivePlayerProps { className?: string;}const LivePlayer: React.FC<LivePlayerProps> = ({ className }) => { const { t } = useUIKit(); const navigate = useNavigate(); const roomEngine = useRoomEngine(); const { currentLive, leaveLive, subscribeEvent, unsubscribeEvent } = useLiveListState(); const { audienceCount } = useLiveAudienceState(); const handleAutoPlayFailed = useCallback(() => { MessageBox.alert({ content: '内容已准备就绪,点击【播放】按钮开始播放', confirmText: '播放', showClose: false, modal: false, }); }, []); const handleKickedOutOfLive = useCallback(() => { Dialog.open({ content: '你已被踢出直播间', confirmText: '确认', className: styles.livePlayer__liveDialog, showCancel: false, showClose: false, modal: true, center: true, onConfirm: () => { Dialog.close(); // 这里可以添加你自己的业务逻辑,如跳转到首页或直播列表页 }, onClose: () => { // 这里可以添加你自己的业务逻辑,如跳转到首页或直播列表页 }, }); }, [navigate]); const handleLiveEnded = useCallback(() => { Dialog.open({ content: '直播已结束', confirmText: '确认', className: styles.livePlayer__liveDialog, showCancel: false, showClose: false, modal: true, center: true, onConfirm: () => { Dialog.close(); // 这里可以添加你自己的业务逻辑,如跳转到首页或直播列表页 }, onClose: () => { // 这里可以添加你自己的业务逻辑,如跳转到首页或直播列表页 }, }); }, [navigate]); const handleLeaveLive = useCallback(async () => { try { await leaveLive(); navigate('/live-list'); } catch (error) { console.error('Failed to leave live:', error); MessageBox.alert({ content: '离开直播间失败,请重试', confirmText: '确认', showClose: false, modal: true, }); } }, [leaveLive, navigate]); // 设置事件监听 useEffect(() => { // 监听自动播放失败事件监听,浏览器默认禁止音频播放,自动播放失败时,增加一次 UI 交互触发音频播放 if (roomEngine.instance) { roomEngine.instance.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); } else { TUIRoomEngine.once("ready", () => { roomEngine.instance?.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); }); } // 监听直播结束事件 subscribeEvent(LiveListEvent.ON_LIVE_ENDED, handleLiveEnded); // 监听被主播踢出直播间事件 subscribeEvent(LiveListEvent.ON_KICKED_OUT_OF_LIVE, handleKickedOutOfLive); return () => { roomEngine.instance?.off(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); unsubscribeEvent(LiveListEvent.ON_LIVE_ENDED, handleLiveEnded); unsubscribeEvent(LiveListEvent.ON_KICKED_OUT_OF_LIVE, handleKickedOutOfLive); }; }, [handleAutoPlayFailed, handleLiveEnded, handleKickedOutOfLive, roomEngine.instance, subscribeEvent, unsubscribeEvent]); return ( <div className={`${styles.livePlayer} ${className || ''}`}> <div className={styles.livePlayer__left}> <div className={styles.livePlayer__header}> <div className={styles.livePlayer__headerContent}> <IconChevronLeft className={styles.livePlayer__headerChevronLeft} size="32" onClick={handleLeaveLive} /> <Avatar className={styles.livePlayer__headerAvatar} src={currentLive?.liveOwner?.avatarUrl} size={32} /> <span>{currentLive?.liveOwner?.userName || currentLive?.liveOwner?.userId}</span> </div> </div> <div className={styles.livePlayer__player}> <LiveView /> </div> <div className={styles.livePlayer__giftContainer}> <LiveGift /> </div> </div> <div className={styles.livePlayer__right}> <div className={styles.livePlayer__audienceList}> <div className={styles.livePlayer__audienceListTitle}> <span>{t('观众列表')} </span> <span className={styles.livePlayer__audienceCount}>({audienceCount})</span> </div> <div className={styles.livePlayer__audienceListContent}> <LiveAudienceList height="100%" /> </div> </div> <div className={styles.livePlayer__messageList}> <div className={styles.livePlayer__messageListTitle}> <span>{t('消息列表')}</span> </div> <div className={styles.livePlayer__messageListContent}> <BarrageList /> <BarrageInput /> </div> </div> </div> </div> );};const LivePlayerView: React.FC = () => { const [searchParams] = useSearchParams(); const { loginUserInfo, login, setSelfInfo } = useLoginState(); const { joinLive } = useLiveListState(); useEffect(() => { // 方式1:从 URL 参数获取(推荐用于页面跳转场景) const liveId = searchParams.get('liveId') || ''; // 方式2:从组件 Props 获取(如果将 LivePlayerView 作为子组件使用) // const liveId = props.liveId || ''; // 方式3:硬编码用于测试(请替换为实际的直播间 ID) // const liveId = 'your_live_room_id'; if (liveId) { joinLive({ liveId }); } }, [searchParams, joinLive]); const initLogin = useCallback(async () => { try { await login({ SDKAppID: 0, // 请替换为你的 SDKAppID(服务开通时获取) userID: '', // 请替换为你的用户 ID userSig: '', // 请替换为你的用户签名(详细获取方式请参阅【步骤1:环境配置及开通服务】文档) }); await setSelfInfo({ userName: '', // 用户昵称,会显示在成员列表、聊天消息中。不设置昵称时,将显示用户 ID avatarUrl: '', // 用户头像,必须为完整的 URL 图片地址,例如:https://your.domain.com/avatar-default.png }); } catch (error) { console.error('登录失败:', error); } }, [login, setSelfInfo]); useEffect(() => { async function init() { await initLogin(); } if (!loginUserInfo?.userId) { init(); } else { console.log('[LiveList]用户已登录:', loginUserInfo.userId); } }, [initLogin, loginUserInfo?.userId]); return ( <UIKitProvider theme="dark" language='zh-CN'> <div className={styles.livePlayerView}> <div className={styles.livePlayerView__body}> <LivePlayer /> </div> </div> </UIKitProvider> );};export default LivePlayerView@mixin text-size-16 { font-size: 16px; font-weight: 600;}@mixin text-size-12 { font-size: 12px; font-weight: 400;}@mixin text-size-14 { font-size: 14px; font-weight: 400;}@mixin text-size-24 { font-size: 24px; font-weight: 500;}@mixin scrollbar { &::-webkit-scrollbar { width: 6px; background: transparent; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(–uikit-color-gray-3); border-radius: 3px; border: 2px solid transparent; background-clip: padding-box; &:hover { background: var(–uikit-color-gray-3); } }}.livePlayerView { display: flex; flex-direction: column; width: 100%; height: 100%; padding: 16px; background-color: var(–uikit-bg-color-topbar); color: var(–uikit-text-color-primary); .livePlayerView__header { width: 100%; padding-bottom: 16px; } .livePlayerView__body { flex: 1; display: flex; flex-direction: column; width: 100%; overflow: auto; align-items: center; }}.livePlayer { display: flex; width: 100%; height: 100%; border-radius: 8px; overflow: hidden; @include scrollbar; .livePlayer__left { display: flex; flex-direction: column; flex: 1; min-width: 0; margin-right: 8px; overflow: hidden; .livePlayer__header { width: 100%; height: 56px; flex-shrink: 0; padding: 0 16px; background: var(–uikit-bg-color-operate); .livePlayer__headerContent { display: flex; align-items: center; width: 100%; height: 100%; border-bottom: 1px solid var(–uikit-stroke-color-primary); span { @include text-size-16; } } .livePlayer__headerChevronLeft { cursor: pointer; } .livePlayer__headerAvatar { margin: 0 8px; border: 1px solid var(–uikit-color-white-7); } } .livePlayer__player { width: 100%; flex: 1; min-height: 0; background: var(–uikit-bg-color-topbar); } .livePlayer__giftContainer { width: 100%; height: 130px; flex-shrink: 0; border-top: 1px solid var(–uikit-stroke-color-primary); background: var(–uikit-bg-color-operate); } } .livePlayer__right { display: flex; flex-direction: column; height: 100%; width: 20%; min-width: 160px; max-width: 360px; .livePlayer__audienceList { display: flex; flex-direction: column; flex-shrink: 0; height: 30%; padding: 8px; background: var(–uikit-bg-color-operate); .livePlayer__audienceListTitle { padding: 12px 0; border-bottom: 1px solid var(–uikit-stroke-color-primary); @include text-size-16; } .livePlayer__audienceCount { font-weight: 400; color: var(–uikit-text-color-secondary); } .livePlayer__audienceListContent { flex: 1; overflow: hidden; } } .livePlayer__messageList { display: flex; flex-direction: column; flex: 1 0 auto; margin-top: 8px; padding: 8px; background: var(–uikit-bg-color-operate); .livePlayer__messageListTitle { padding: 12px 0; border-bottom: 1px solid var(–uikit-stroke-color-primary); @include text-size-16; } .livePlayer__messageListContent { display: flex; flex: 1; flex-direction: column; } } }}.livePlayer__liveDialog { text-align: center;}
五、步骤4:路由配置
如果你是通过首页或者直播列表页面,跳转到直播观看页面,你需要配置 React Router 页面路由。在项目中新建或修改 src/router/index.tsx 文件。然后,在你的主文件(例如 main.tsx 或 App.tsx)中引入并使用路由。可参见 GitHub 代码示例,如果需要直播列表,可参见文档 直播列表(Web React)。
// src/router/index.tsximport { createHashRouter } from 'react-router-dom';import { LiveListView } from '../views/LiveList';import { LivePlayerView } from '../views/LivePlayer';// 路由保护组件const ProtectedRoute = ({ children }: { children: React.ReactNode; }) => { return ( <>{children}</> );};const routes = [ { path: '/live-player', element: <LivePlayerView />, }, // // 如果需要直播列表功能,可添加如下路由,接入直播列表页面 // // 接入文档,请参阅【直播列表 -> 直播列表(Web React)】 // { // path: '/live-list', // element: <LiveListView />, //}];export const router = createHashRouter( routes.map(route => ({ …route, element: <ProtectedRoute>{route.element}</ProtectedRoute>, })));// 在 src/App.tsx 中使用路由组件 src/router/index.tsximport { RouterProvider } from 'react-router-dom'import { router } from './router'import './App.css'function App() { return ( <RouterProvider router={router} /> )}export default App
六、步骤5:启动项目
打开终端,进入项目目录,执行以下命令启动项目。
npm run dev
七、步骤1:开启一场直播
·方式一(推荐):
使用我们提供的 在线开播网站 ,开启一场直播来观看,开启后获取直播间 ID
·方式二:
跑通其它端的 Demo 工程,开启一场直播,例如:跑通 Web Vue3 推流与观看。
注意:重要提示:
注意:请使用不同的用户 ID 开播和观看,否则后登录的设备会强制先登录的设备下线(即被踢下线)。
八、步骤2:观看直播
参考上述 快速接入 步骤3 的示例代码,输入登录账号和直播间 ID 后,即可进入直播间观看直播,观看效果如下:
九、颜色主题及语言
你可以使用 UIKitProvider 组件,修改默认的主题和语言。
| UIKitProvider 参数 | 可选值 | 默认值 |
| theme | "light" | "dark" | "light" |
| language | "zh-CN" | "en-US" | – |
1. 在 App.tsx 中全局设置
在 App.tsx 中,以 UIKitProvider 作为根元素。
import { RouterProvider } from 'react-router-dom'import { UIKitProvider } from '@tencentcloud/uikit-base-component-react'import { router } from './router'import './App.css'function App() { return ( <UIKitProvider theme="dark" language='zh-CN'> <RouterProvider router={router} /> </UIKitProvider> );}export default App
2. 在单个页面或组件中设置
在 React 组件中,以 UIKitProvider 作为根节点元素。以下示例代码截取自 快速接入 步骤3 的代码片段。
const LivePlayerView: React.FC = () => { return ( <UIKitProvider theme="dark" language='zh-CN'> <div className={styles.livePlayerView}> <div className={styles.livePlayerView__body}> <LivePlayer /> </div> </div> </UIKitProvider> );};export default LivePlayerView;
十、设置昵称和头像
上文中,快速接入:步骤3 已包含昵称和头像设置代码,如下面代码所示,设置的头像和昵称,将显示在自己和其它直播间成员的成员列表、聊天消息中。如果不主动设置昵称和头像,昵称将显示为登录时的用户 ID、头像将显示为默认头像。
const initLogin = useCallback(async () => { try { await login({ SDKAppID: 0, // 请替换为你的 SDKAppID(服务开通时获取) userID: '', // 请替换为你的用户 ID userSig: '', // 请替换为你的用户签名(详细获取方式请参阅【步骤1:环境配置及开通服务】文档) }); await setSelfInfo({ userName: '', // 用户昵称,会显示在成员列表、聊天消息中。不设置昵称时,将显示用户 ID avatarUrl: '', // 用户头像,必须为完整的 URL 图片地址,例如:https://your.domain.com/avatar-default.png }); } catch (error) { console.error('登录失败:', error); } }, [login, setSelfInfo]);
十一、浏览器自动播放限制
出于用户体验考虑,现代浏览器对网页自动播放 (Autoplay) 功能实施了限制性策略:所有带声音的媒体内容必须经过用户主动交互(如点击或触摸)后才能播放。这项限制主要是为了防止网站在用户未明确同意的情况下突然播放音频,避免对用户造成干扰。大多数浏览器都不限制无声视频的自动播放,但是在低电量模式下的 iOS Safari 浏览器中以及开启了自定义自动播放限制的 iOS WKWebView 中(如 iOS 微信浏览器),无声视频的自动播放也会受到限制。
十二、自动播放失败表现
当用户对该站点的媒体互动指数 (MEI, Media Engagement Index) 未达到阈值时,企图自动播放有声视频会失败。在 SDK 默认情况下,当检测到自动播放失败时,会弹窗引导用户与页面产生交互(如点击确认按钮)。产生交互后,浏览器策略即被满足,有声视频即可正常播放。
十三、解决方案:自定义处理自动播放失败
如果你希望自定义自动播放失败时的交互方式(例如替换默认的弹窗 UI),可以通过监听 SDK 抛出的 onAutoplayFailed 回调来实现。以下是 快速接入 步骤3 中的代码片段,通过监听事件并弹出自定义对话框提示用户。
import React, from 'react';import TUIRoomEngine, { TUIRoomEvents } from "@tencentcloud/tuiroom-engine-js";import { useLiveListState, useRoomEngine } from 'tuikit-atomicx-react';import { MessageBox } from '@tencentcloud/uikit-base-component-react';import styles from './LivePlayerView.module.scss';const LivePlayer: React.FC<LivePlayerProps> = ({ className }) => { const roomEngine = useRoomEngine(); const { currentLive, leaveLive, subscribeEvent, unsubscribeEvent } = useLiveListState(); // 自动播放失败事件处理函数 const handleAutoPlayFailed = useCallback(() => { MessageBox.alert({ content: '内容已准备就绪,点击【播放】按钮开始播放', confirmText: '播放', showClose: false, modal: false, }); }, []); // 设置事件监听 useEffect(() => { // 监听自动播放失败事件监听,浏览器默认禁止音频播放,自动播放失败时,增加一次 UI 交互触发音频播放 if (roomEngine.instance) { roomEngine.instance.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); } else { TUIRoomEngine.once("ready", () => { roomEngine.instance?.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); }); } // … 省略其它代码 return () => { roomEngine.instance?.off(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed); // … 省略其它代码 }; }, [handleAutoPlayFailed, handleLiveEnded, handleKickedOutOfLive, roomEngine.instance, subscribeEvent, unsubscribeEvent]); //… 省略其它代码}
十四、下一步
恭喜你,现在你已经成功集成了观众观看功能。接下来,你可以继续接入直播列表、UI 自定义和监播等功能:
| 功能 | 描述 | 集成指引 |
| 直播列表 | 展示直播列表界面和功能,包含直播列表和房间信息展示功能。 | 直播列表(Web React) |
| UI 自定义 | 更详细的 UI 组件自定义指引。 | UI 自定义 |
| 直播管理系统 | 运营平台,支持直播间管控。 | 直播管理系统(React) |