跳转到内容

架构概览

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)
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} 重渲

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')

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.close
LotteryEntry ws.room_config.lottery → lottery.start / lottery.open / lottery.close
TaskRewardEntry ws.room_config.task → task.new / task.open / task.close
ProductListEntry ws.room_config.goods → goods.config / goods.open / goods.close
CommentaryEntry ws.room_config.commentary → commentary.push / commentary.open / commentary.close

NEEDS-SERVER-VERIFY — 5010 subfield 名称(coupon / lottery / task / goods / commentary)及 payload schema 均为推断值,无真实 LAPS 端点可验证。

观看时长跟踪服务,每 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() → 清空,删除 localStorage

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>
}
常量 方向 说明
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
disconnected → connecting → authing → ready → reconnecting → destroyed
  • 认证:TCP 握手成功后立即发 LOGIN_REQ,5s 超时,最多重试 3 次。失败 emit ws.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

方向判断(src/lib/core/orientation.ts)

Section titled “方向判断(src/lib/core/orientation.ts)”

优先级(AC-D-02):

  1. config.layout.orientation = 'portrait' | 'landscape' → 强制覆盖
  2. config.layout.orientation = 'auto'(默认)→ 读 API 字段
  3. body.extendSet.screenType === 1'landscape'(官方 schema:1=横屏,2=竖屏)
  4. 其他值(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,降级到固定估算高度)。


// 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-CNoptions.i18n 可在运行时覆盖任意 key。


使用 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),
}
}
  • 返回带 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-packagedist/
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/**


vite.umd.config.ts
build: { target: ['es2015', 'safari12', 'ios12'] }

处理:箭头函数、模板字符串、解构、class。

{
"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.tsrollupOptions.plugins@rollup/plugin-babel 实现。

ESM 库(dist/)不打包 polyfill — 消费方自行提供。


封装 tcplayer.js@^5.3.4(npm,非 CDN)。licenseUrl(TRTC 许可证)为必填字段。

协议回退顺序:WebRTC → FLV → HLS(utils/browser.tssupportsWebRTC() / 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 丢弃,避免黑屏无提示。

封装 @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 推断,待服务端验证