架构概览
we-livesdk 是浏览器端直播 SDK,提供完整 Svelte 5 UI(直播间)。将腾讯云直播(视频)和腾讯 IM(聊天)以及 LAPS WebSocket 推送统一封装在事件驱动 API 后面。
┌──────────────────────────────────────────────────────────────┐│ ENTRY LAYER(入口层) ││ LiveSDK — new LiveSDK(config), mount(), destroy() ││ on/off/once/emit, use(plugin), getModule(id) │├──────────────────────────────────────────────────────────────┤│ APPLICATION LAYER(应用层) src/lib/app/ ││ BaseAppModule(WS 驱动入口模块基类) ││ CouponEntry · LotteryEntry · TaskRewardEntry ││ ProductListEntry · CommentaryEntry ││ ── DurationTracker src/lib/modules/app/ │├──────────────────────────────────────────────────────────────┤│ BUSINESS LAYER(业务层) src/lib/modules/ ││ LiveRoom · Player · IM · Interact · User ││ Marketing · Security │├──────────────────────────────────────────────────────────────┤│ SERVICE LAYER(服务层) src/lib/net/ ││ TencentLiveAdapter TCPlayer 封装 ││ TencentIMAdapter @tencentcloud/lite-chat 封装 ││ LapsWSAdapter LAPS JSON WebSocket(认证+心跳+重连) ││ BrowserTransport 原生 WebSocket ITransport 实现 ││ NullWSAdapter 空实现(测试/离线) ││ PollingFallback WS 不可用时 HTTP 轮询兜底 ││ HttpClient fetch REST 客户端 │├──────────────────────────────────────────────────────────────┤│ FOUNDATION LAYER(基础层) src/lib/core/ ││ EventEmitter (mitt) · Config · Logger ││ Storage · Plugin · i18n(类型化)· orientation(归一化) │└──────────────────────────────────────────────────────────────┘用户操作 / 腾讯 SDK 事件 / LAPS WS 推送 │ ▼Service Adapter(TencentLiveAdapter / TencentIMAdapter / LapsWSAdapter) │ 将底层事件映射为 SDK_EVENTS ▼EventEmitter.emit(event, payload) │ ├──▶ Module(业务逻辑、状态更新) │ └──▶ Store factory($state mutation) │ ▼ Svelte 5 组件重渲(reactive getter)示例:聊天消息流
Section titled “示例:聊天消息流”TIM.EVENT.MESSAGE_RECEIVED → TencentIMAdapter 解析消息 → emitter.emit('chat.message', { id, userId, content, ... }) → chatStore: messages = [...messages.slice(-199), msg] // $state 更新 → Chat.svelte: {#each chatStore.messages as msg} 重渲模块生命周期
Section titled “模块生命周期”new LiveSDK(config) ├── normalizeConfig() — 验证 + 合并默认值 ├── createEventEmitter() ├── createLogger('LiveSDK') ├── new PluginRegistry() └── _initModules() — LiveRoom, Player, IM, Interact, User, Marketing, Security
sdk.mount(target) ├── resolveTarget(target) ├── createLiveStore(liveRoom, emitter) ├── createPlayerStore(player, emitter) ├── createChatStore(im, emitter) ├── createInteractStore(interact, emitter) ├── createUserStore(user, emitter) ├── mount(LiveRoom, { target, props: { sdk } }) ← Svelte 5 mount() ├── module.init() — 每个模块 └── emit('ready')
sdk.destroy() ├── unmount(this.app) ← Svelte 5 unmount() ├── module.destroy() — 每个模块 ├── plugins.clear() └── emitter.clear() + emit('destroy')应用层(Application Layer)
Section titled “应用层(Application Layer)”WS 驱动入口模块(src/lib/app/)
Section titled “WS 驱动入口模块(src/lib/app/)”Phase 11 引入,接收 ws.room_config(5010 ROOM_CONFIG_NOTIFY)推送并派发语义事件。
BaseAppModule 抽象基类 ├── _on(event, handler) 自动注册清理,destroy() 时统一移除 ├── setWSAdapter(ws) 赋予出向发消息能力(保留接口) └── _cleanup() 子类 destroy() 必须调用
CouponEntry ws.room_config.coupon → coupon.show / coupon.open / coupon.closeLotteryEntry ws.room_config.lottery → lottery.start / lottery.open / lottery.closeTaskRewardEntry ws.room_config.task → task.new / task.open / task.closeProductListEntry ws.room_config.goods → goods.config / goods.open / goods.closeCommentaryEntry ws.room_config.commentary → commentary.push / commentary.open / commentary.closeNEEDS-SERVER-VERIFY — 5010 subfield 名称(
coupon/lottery/task/goods/commentary)及 payload schema 均为推断值,无真实 LAPS 端点可验证。
DurationTracker(src/lib/modules/app/)
Section titled “DurationTracker(src/lib/modules/app/)”观看时长跟踪服务,每 5 秒 emit duration.change,每 30 秒调用 taskTimeChime API 上报并 emit duration.report。数据通过 localStorage 跨页面持久化(TTL 48h,key = livesdk_duration_{roomId}-{userId})。
DurationTracker.start() → 启动 1s 计时器DurationTracker.stop() → 停止计时,持久化DurationTracker.reset() → 清空,删除 localStorageWebSocket 子系统(src/lib/net/ws/)
Section titled “WebSocket 子系统(src/lib/net/ws/)”协议帧格式(LAPS JSON)
Section titled “协议帧格式(LAPS JSON)”interface WSFrame { ver: string // '1.0' cmd: string // LAPS_CMD 枚举值 ts: number // Unix ms mid: string // message UUID rid: string // request UUID pld: Record<string, unknown>}LAPS 命令码
Section titled “LAPS 命令码”| 常量 | 值 | 方向 | 说明 |
|---|---|---|---|
HEARTBEAT_REQ |
1000 |
双向 | 心跳 ping/pong |
LOGIN_REQ |
1010 |
→ 服务端 | 认证请求 |
LOGIN_RESP |
1011 |
← 服务端 | 认证响应(code=200 成功) |
WATCH_REPORT |
2000 |
→ 服务端 | 进入房间上报 |
WATCH_RESP |
2001 |
← 服务端 | 上报 ACK |
ROOM_STATUS_NOTIFY |
5000 |
← 服务端 | 直播状态变更推送 |
ROOM_STATUS_RESP |
5001 |
→ 服务端 | 状态变更 ACK |
ROOM_CONFIG_NOTIFY |
5010 |
← 服务端 | 房间配置推送(营销模块等) |
ROOM_CONFIG_RESP |
5011 |
→ 服务端 | 配置推送 ACK |
LapsWSAdapter 状态机
Section titled “LapsWSAdapter 状态机”disconnected → connecting → authing → ready → reconnecting → destroyed- 认证:TCP 握手成功后立即发
LOGIN_REQ,5s 超时,最多重试 3 次。失败 emitws.auth_failed。 - 心跳:ready 后每 25s 发
HEARTBEAT_REQ,10s 无 pong 则主动关闭并触发重连。 - 重连:指数退避,基础 1s,上限 30s,最多 10 次。达上限 emit
ws.error(code -2)。 - 消息队列:ready 前的
send()调用进入_pendingQueue,认证成功后批量发送。 - 状态映射:
5000状态码经LAPS_STATUS_MAP(constants.ts)映射完整 10 值(101-108 与 REST 同构,109/1001→ended)。 - 终态:ENDED(103)/INVALID(109)/DELETED(1001) 停止自动重连;仅 DELETED(1001) 房间被删除时 adapter 自毁。
| 类 | 职责 |
|---|---|
BrowserTransport |
封装原生 WebSocket,实现 ITransport |
NullWSAdapter |
无操作实现,用于测试/离线场景 |
PollingFallback |
VITE_WS_URL 未配置时,每 5s 轮询房间 API,emit room.viewerCount |
横竖屏布局策略
Section titled “横竖屏布局策略”方向判断(src/lib/core/orientation.ts)
Section titled “方向判断(src/lib/core/orientation.ts)”优先级(AC-D-02):
config.layout.orientation='portrait'|'landscape'→ 强制覆盖config.layout.orientation='auto'(默认)→ 读 API 字段body.extendSet.screenType === 1→'landscape'(官方 schema:1=横屏,2=竖屏)- 其他值(2, undefined, null, 未知)→
'portrait'
import { normalizeOrientation } from '$lib/core/orientation'
const orientation = normalizeOrientation( apiResponse.body.extendSet?.screenType, // raw API value config.layout?.orientation // config override)// emitter.emit('live.orientation', orientation)| 组件 | 文件 |
|---|---|
PortraitLayout |
components/layout/PortraitLayout.svelte |
LandscapeLayout |
components/layout/LandscapeLayout.svelte |
VirtualList |
components/VirtualList.svelte |
VirtualList 使用定高虚拟列表方案,兼容 iOS 12(无 ResizeObserver,降级到固定估算高度)。
国际化(i18n)
Section titled “国际化(i18n)”类型化设计(src/lib/i18n/)
Section titled “类型化设计(src/lib/i18n/)”// types.ts — 所有组件用到的 key 必须在此声明interface I18nMessages { 'status.not_started': string 'status.ongoing': string // ... 完整列表见 src/lib/i18n/types.ts 'interact.like': string 'coupon.label': string // ...}
// zh-CN.ts — 默认 locale 字符串// index.ts — createI18n(locale?, overrides?) → { t(key) }降级规则:未知 locale 警告 + 回退到 zh-CN。options.i18n 可在运行时覆盖任意 key。
Store 架构
Section titled “Store 架构”使用 Svelte 5 $state/$derived 工厂函数,非 writable。
export function createChatStore(im: IM, emitter: IEventEmitter) { let messages = $state<ChatMessage[]>([]) const hasMessages = $derived(messages.length > 0)
emitter.on('chat.message', msg => { messages = [...messages.slice(-199), msg] })
return { get messages() { return messages }, get hasMessages() { return hasMessages }, sendMessage: (text: string) => im.sendTextMessage(text), }}工厂函数优于 writable 的原因
Section titled “工厂函数优于 writable 的原因”- 返回带 getter 的普通对象,无需
subscribe/unsubscribe - 在
LiveSDK.mount()中创建,与 SDK 实例同生命周期 - 通过
setContext('livesdk', sdk)注入,无 prop drilling $derived自动依赖追踪,无需手动管理
interface IPlugin { readonly id: string install(sdk: LiveSDK): void uninstall?(sdk: LiveSDK): void}
sdk.use(new DanmuPlugin({ speed: 8, maxLines: 3 }))sdk.use(new WatermarkPlugin({ text: 'UserName' }))PluginRegistry 重复注册同 id 时抛出异常。
LiveRoom.svelte 根组件 — setContext, CSS Grid 布局├── StatusOverlay.svelte 直播状态覆盖(not_started/ended/loading)├── PortraitLayout.svelte 竖屏布局容器│ ├── Player.svelte <video> + TCPlayer 覆盖层│ │ ├── Danmu.svelte 弹幕(DanmuPlugin 激活)│ │ ├── Watermark.svelte 用户水印│ │ └── PlayerControls.svelte 播放/暂停/音量/清晰度│ │ └── VodProgressBar.svelte VOD 进度条(回放模式)│ ├── Chat.svelte 聊天面板│ │ ├── VirtualList.svelte 虚拟列表(iOS12兼容)│ │ ├── ChatMessage.svelte 单条消息气泡│ │ └── ChatInput.svelte 输入框 + 发送│ ├── Interact.svelte 互动层(点赞/礼物动画)│ │ └── GiftAnimation.svelte CSS burst 动画│ ├── BottomActionBar.svelte 底部操作栏(购物车/分享/更多)│ ├── ViewerCount.svelte 观看人数浮层│ ├── CouponStack.svelte 优惠券浮层│ └── TaskEntry.svelte 任务奖励入口├── LandscapeLayout.svelte 横屏布局容器│ └── TabSwitch.svelte 横屏 Tab 切换└── Marketing/ ├── GoodsCard.svelte 主播商品卡片 └── CouponBadge.svelte 优惠券通知徽标所有组件使用 $props()(不使用 export let)。均通过 getContext('livesdk') 读取 SDK。
| 模式 | 命令 | 输出 |
|---|---|---|
| Dev playground | pnpm dev |
SvelteKit dev server, port 5200 |
| Library (npm) | pnpm package |
svelte-kit sync && svelte-package → dist/ |
| CDN UMD | pnpm build:umd |
Vite lib mode → dist-umd/livesdk.umd.js |
Playground 位于 src/playground/routes/(kit.files.routes 配置)。不进入 npm 包,svelte-package 只打包 src/lib/**。
iOS 12 / Android 7 兼容性
Section titled “iOS 12 / Android 7 兼容性”第一层 — esbuild(编译时)
Section titled “第一层 — esbuild(编译时)”build: { target: ['es2015', 'safari12', 'ios12'] }处理:箭头函数、模板字符串、解构、class。
第二层 — Babel(仅 UMD)
Section titled “第二层 — Babel(仅 UMD)”{ "presets": [["@babel/preset-env", { "targets": { "ios": "12", "safari": "12" } }]], "plugins": [ "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-logical-assignment-operators" ]}处理:?. ?? ??= — Svelte 5 编译输出大量使用这些语法。
Babel 后处理在 Svelte 编译 .svelte 之后执行,通过 vite.umd.config.ts 的 rollupOptions.plugins 中 @rollup/plugin-babel 实现。
ESM 库(dist/)不打包 polyfill — 消费方自行提供。
腾讯 SDK 集成
Section titled “腾讯 SDK 集成”TencentLiveAdapter(Phase 6)
Section titled “TencentLiveAdapter(Phase 6)”封装 tcplayer.js@^5.3.4(npm,非 CDN)。licenseUrl(TRTC 许可证)为必填字段。
协议回退顺序:WebRTC → FLV → HLS(utils/browser.ts 中 supportsWebRTC() / supportsHLS() 检测)。
| TCPlayer 事件 | SDK 事件 |
|---|---|
play / playing |
player.playing |
playing / canplay |
player.recovered(清除 buffering 态) |
pause |
player.pause |
error |
player.error(Player 模块指数退避重试,上限 3 次) |
ended |
live.status = 'ended' |
ready |
player.ready |
timeupdate |
player.timeupdate(节流 250ms) |
loadedmetadata / durationchange |
player.duration |
waiting / suspend / seeking |
player.waiting(卡顿/缓冲监测) |
stalled / abort |
player.stalled(拉流中断/黑屏监测) |
能力探测:
deriveSources()后经orderByCapability()二次排序——无 MSE(FLV 不可用)但有原生 HLS 的极旧设备,HLS 源提前、FLV 丢弃,避免黑屏无提示。
TencentIMAdapter(Phase 7)
Section titled “TencentIMAdapter(Phase 7)”封装 @tencentcloud/lite-chat@^4.2.6(精简版,非完整版 @tencentcloud/chat)。
架构注意:TIM 凭证(sdkAppId / userId / userSig / groupId)和 WS 端点(
ws.url)均通过环境变量/SDKConfig 提供,auth API 不返回这些字段。
| TIM 事件 | SDK 事件 |
|---|---|
SDK_READY |
im.ready |
MESSAGE_RECEIVED(文本) |
chat.message |
| 自定义类型 LIKE | interact.like |
| 自定义类型 GIFT | interact.gift |
| 自定义类型 GOODS | goods.show |
| 决策 | 理由 |
|---|---|
| SvelteKit + svelte-package | 每组件生成 .svelte.d.ts;与 ui-svelte 同模式 |
| mitt EventEmitter | 类型泛型、bundle 小、无 DOM 依赖 |
$state 工厂函数 |
无 subscribe/unsubscribe 模板代码;已在 watchTimer.svelte.ts 验证 |
| Babel 仅 UMD | 预处理 ESM 破坏 tree-shaking;CDN 消费方需要完整转译 |
| 模块用 TS 类,组件用 Svelte | 模块负责逻辑/IO,组件是响应式视图 |
| Phase 1 使用 Adapter stub | 无腾讯凭证时也可完整构建和测试 |
| WS 与 IM 并存 | WS 负责服务端推送(直播状态、营销),IM 负责用户聊天 |
| Phase 11 入口模块 PROVISIONAL | 无真实 LAPS 端点;5010 subfield schema 推断,待服务端验证 |