架构概览
we-livesdk 是浏览器端直播 SDK,提供完整 Svelte 5 UI(直播间)。将腾讯云直播(视频)和腾讯 IM(聊天)以及 LAPS WebSocket 推送统一封装在事件驱动 API 后面。
┌──────────────────────────────────────────────────────────────┐│ ENTRY LAYER(入口层) src/lib/index.ts ││ LiveSDK — new LiveSDK(config), mount(), destroy() ││ on/off/once/emit · use(plugin) · getModule(id) · t(key) ││ modal()/dialog() · toast · showLoading()/hideLoading() │├──────────────────────────────────────────────────────────────┤│ APPLICATION LAYER(应用层) ││ 互动应用模块 src/lib/modules/app/(appModules 配置) ││ LikeModule · GiftModule · GoodsModule · ViewerCountModule ││ SecurityAppModule ││ 运营入口模块 src/lib/app/(entryModules 配置,WS 驱动) ││ LotteryEntry · TaskRewardEntry + TaskStore ││ ProductListEntry · CommentaryEntry · GoodsMarketingEntry │├──────────────────────────────────────────────────────────────┤│ BUSINESS LAYER(业务层) src/lib/modules/ ││ LiveRoom · Player · IM · Interact · User ││ Marketing · Security(均继承 BaseModule) │├──────────────────────────────────────────────────────────────┤│ SERVICE LAYER(服务层) src/lib/net/ ││ TencentLiveAdapter TCPlayer 封装 ││ TencentIMAdapter @tencentcloud/lite-chat 封装 ││ AuthClient 认证 + 多接口编排(详情/拉流/凭证/配置)││ BrowserTransport 原生 WebSocket → @zan/laps ITransport ││ lapsBridge @zan/laps 事件 → SDK_EVENTS 桥接 ││ PollingFallback WS 不可用时 HTTP 轮询兜底 ││ HttpClient fetch REST 客户端 ││ GoodsListFetcher / goodsMapper / marketingApi 商品营销取数 │├──────────────────────────────────────────────────────────────┤│ FOUNDATION LAYER(基础层) src/lib/core/ ││ EventEmitter (mitt) · Config · Logger · Storage · Plugin ││ LifecycleController(前后台/online 事件总线,per-instance) ││ i18n(类型化)· theme(运行时)· orientation · fullscreen ││ modal/dialog/drawer/toast(浮层命令式入口) ││ Reporter + telemetry/(采样上报)· sensitive-word(敏感词) ││ WatchClock · WatchReportingService · EndpointController │└──────────────────────────────────────────────────────────────┘用户操作 / 腾讯 SDK 事件 / LAPS WS 推送 │ ▼Service Adapter(TencentLiveAdapter / TencentIMAdapter / LapsAdapter+lapsBridge) │ 将底层事件映射为 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 “服务端时间校准”config/detail.serverTime ─┐LAPS serverTime/5010 ─────┼─▶ LiveSDK.ServerClock ─▶ getServerTime()营销 HTTP timestamp ─────┘ │ ├─▶ 预告倒计时 └─▶ 签到/福袋/券倒计时与过期裁剪ServerClock 为每个 LiveSDK 实例独立创建的时间单一事实来源。协议边界把秒/毫秒统一成 epoch 毫秒;锚定后以 performance.now() 单调增量推进,不读取设备墙钟。重新校时时只允许向前校正,旧 HTTP 响应或重复秒级帧不会造成时间回拨。live.setRoomConfig({ serverTime }) 与内部推送共用同一写入口。首个有效锚点前 getServerTime() 返回 undefined,业务 UI 保持隐藏/00:00,不以 Date.now() 猜测;签到活动若先到,锚定状态更新会立即驱动倒计时 snapshot 重算。TaskStore 同样以该时钟判定普通/Range 的 start/end,并在最近边界自动重拉配置、重算 channel。
观看时长与任务上报
Section titled “观看时长与任务上报”Player 归一媒体信号(intent/blocker/sourceEpoch/progress) → WatchClock(本机媒体事实、48h scope 持久化) → watch.progress / getWatchProgress() → TaskStore(普通/Range 配置、阈值、完成 diff、奖励队列) → 两个 WatchReportChannel(各自 anchor/outbox/retry/final ACK) → Ordinary / Range Adapter → EndpointController 安全端点WatchClock 不读取任务响应;服务端秒数只进入各 channel 的 ServerProgressAnchor。认证后的稳定用户 scope 必须在 _initEntryModules() 前建立,避免任务工厂取得空时钟。无稳定用户、可信 watchSession、有效任务配置或 watchReportBaseUrl 时,网络链路保持关闭。destroy/auth.kicked/pagehide 按“结算→最终入队→持久化→keepalive”执行;同步 destroy 后 final channel 可脱离 UI 生命周期继续校准/退避,最长 60 秒。普通与 Range 不共享 anchor/outbox。
模块生命周期
Section titled “模块生命周期”new LiveSDK(config) ├── normalizeConfig() — 验证 + 合并默认值 ├── createEventEmitter() ├── createLogger('LiveSDK') ├── new PluginRegistry() ├── createToast(_ownToasts) + acquireToaster() 全局唯一 Toaster 引用计数 +1 ├── createReporter(emitter, telemetry).start() 遥测上报 └── _initModules() — LiveRoom, Player, IM, Interact, User, Marketing, Security
sdk.mount(target) ├── resolveTarget(target) ├── createLiveStore / PlayerStore / ChatStore / InteractStore / UserStore ├── createDrawerStore / LoadingStore / GoodsListStore / MarketingStore ├── auth:fetchRoomDetail 优先 → 并行拉流/凭证/配置(AuthClient 编排) │ ├── liveType!==6 → AuthError 硬拦截;config 缺失 → auth.warning 不阻断 │ ├── reporter.setIdentity(userId)(脱敏后置注入) │ └── 首帧播种:muteStatus / liveWatch / serverTime / buyingFrenzy / replayList ├── _initWS():new LapsAdapter(BrowserTransport) → bridgeLapsEvents(或 PollingFallback 降级) ├── mount(LiveRoom, { target, props: { sdk } }) ← Svelte 5 mount() ├── module.init() — 每个模块 └── emit('ready', { detail: roomDetail })
sdk.destroy() ├── WatchClock settle → TaskStore final enqueue/persist/keepalive ├── emit('destroy') ├── unmount(this.app) ← Svelte 5 unmount() ├── module.destroy() — 每个模块 ├── plugins.clear() ├── 关本实例弹框(_ownModals)+ dismiss 本实例 toast(_ownToasts)+ releaseToaster() └── emitter.clear() + emit('destroy')应用层(Application Layer)
Section titled “应用层(Application Layer)”Player ↔ Adapter generation 内部完成通道
Section titled “Player ↔ Adapter generation 内部完成通道”直播首次起播在 auth.success 前同步 prepare、事件发出后仅启动最终 URL;运行时回放转直播先 teardown VOD,再用同一原子入口恢复真实手动档或执行默认策略。Player 为首次起播与运行时切流分配单调递增的 generation。首次起播、VOD 与故障自愈仍使用单播放器;运行时清晰度切换由 Player.svelte 提供 active/standby 两个稳定槽位:active 保持当前源持续播放,TencentLiveAdapter 在隐藏、静音的 standby 中预加载目标源,并经内部 StreamObserver 回报 loadstart/playing/progress/error。
事务完成证据必须是目标 generation 的 loadstart(或 src() 已同步应用的等价回调)开门后依次取得 playing 与相对 source-applied 基线增长的 progress(currentTime 或 totalVideoFrames)。Adapter 在 ready 延迟期间仍把旧流事件标为旧 generation,旧 ready 回调不得覆盖 latest-wins 请求。公开的 player.playing 供 UI/store 兼容消费;play/ready/canplay/recovered/firstframe 或公开 player.playing 均不能解除事务互斥或单独提交切流。
切流期间通用错误重试与卡顿看门狗让位给当前事务;同一 switchId 先在 standby 对目标 URL 做唯一一次内部重试,并复用 Adapter 按设备能力生成的协议候选。目标取得真实进度后,同一渲染周期先显示 standby、再隐藏并释放旧 active,同时按 Player 缓存的音量/静音真值迁移音频状态;目标失败、超时、取消或被替代时只释放 standby,active 不换源、不执行回切。非 superseded 的取消会把档位、URL 与 state sink 恢复到 confirmed 真值,保证 detach→reattach 不会拿乐观目标档重播。公开 pause() 会同步暂停 active/standby 并挂起事务完成门与超时,play() 后才恢复。
双槽位存在时,切流资源路径与声音状态完全解耦:iOS/WebKit、已开声、预加载中开声及微信桥接开声都必须继续使用原 standby,禁止销毁 standby 后对 active 调 switchStream()。微信桥接事件只同步 Player 的 muted 缓存;交换时不主动修改旧 active 的声音状态,先停止旧 active,再把缓存音量/静音应用到新 active。媒体槽统一固定在 z-index: 0,控制条和水印覆盖层固定在 z-index: 10,避免目标首帧压住 UI;聊天输入只由布局层 ChatInput 渲染,Player 不再重复挂载 Danmu 输入框。standby 的公开 player.* 事件关闭,事务 generation 的中间 error 只进入内部 observer,不污染 error store/遮罩。无双槽位的旧 attachElement 宿主保留单实例 src() 兼容路径。每个 player.quality.switching 必须由同一 switchId 的 switched、switch-failed 或 switch-cancelled 唯一收口。
应用层有两个独立家族(注意两处各有一份 BaseAppModule,职责不同):
A. 互动应用模块(src/lib/modules/app/,appModules 配置)
Section titled “A. 互动应用模块(src/lib/modules/app/,appModules 配置)”emitter 驱动的行为模块,基类 modules/app/BaseAppModule(_on 自动注册清理,isEnabled 开关,destroy() 统一移除监听)。
| 模块 | 职责 |
|---|---|
WatchClock |
Player 内部媒体事实时钟:意图/进度双门控、source epoch、blocker、watchdog、live/replay 分项与 48h 稳定用户 scope 持久化;不持有 HTTP/任务状态 |
LikeModule |
点赞单一真源:本地计数 + live.thumb 权威总数;上行经 LAPS reportBehavior(cmd 2010)2s 防抖缓冲,已去 IM 化 |
GiftModule |
礼物动画聚合,emit interact.gift / 动画队列 |
GoodsModule |
商品状态(浮窗显隐 / 热卖 / 讲解 badge / 已抢光 / 隐藏价格),双源(WS 7000 + 推送)优先级收敛 |
ViewerCountModule |
在线人数(WS / 轮询兜底) |
SecurityAppModule |
禁言 / 踢出运行时处置 |
B. 运营入口模块(src/lib/app/,entryModules 配置,WS 驱动)
Section titled “B. 运营入口模块(src/lib/app/,entryModules 配置,WS 驱动)”Phase 11 引入,接收 ws.room_config(5010 ROOM_CONFIG_NOTIFY)推送并派发语义事件。基类 app/BaseAppModule(另一份,持 LapsAdapter 备未来出向消息,不继承 BaseModule)。
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.closeGoodsMarketingEntry 营销弹框入口(numIid 惰性拉取,sdk.openGoodsMarketing 驱动)NEEDS-SERVER-VERIFY — 5010 subfield 名称(
coupon/lottery/task/goods/commentary)及 payload schema 均为推断值,无真实 LAPS 端点可验证。
WebSocket 子系统(src/lib/net/ws/)
Section titled “WebSocket 子系统(src/lib/net/ws/)”LAPS 协议本体(帧格式、命令码、编解码、认证/心跳/重连状态机)已迁移到依赖包 @zan/laps(LapsAdapter)。历史手写镜像(constants / protocol / LapsWSAdapter / NullWSAdapter / types)全部删除,livesdk 仅保留两个本地件 + 一层桥接:
| 文件 | 职责 |
|---|---|
BrowserTransport |
封装原生 WebSocket,实现 @zan/laps 的 ITransport(onOpen/onClose/onError/send/close) |
lapsBridge |
唯一翻译点:把 @zan/laps 事件名(roomStatusChange / enterMessage / thumb 等)翻成 SDK 约定事件(ws.* / live.*);下游业务/应用层对 @zan 事件名无感知 |
index.ts |
barrel —— 本地导出 + 从 @zan/laps re-export(CMD RESP_CODE ROOM_STATUS WATCH_EVENT TERMINAL_STATUSES encode/decode LapsAdapter MockTransport pairResponse 等) |
接线(src/lib/index.ts _initWS)
Section titled “接线(src/lib/index.ts _initWS)”new LapsAdapter({ transport: new BrowserTransport(wsUrl), ... }) → bridgeLapsEvents(adapter, emitter, { agentId: viewerUserId })wsUrl 缺失时降级 PollingFallback(每 5s 轮询房间 API,emit room.viewerCount)。页面回前台且 adapter 处 reconnecting 时主动催连。
房间状态映射(lapsBridge.LIVE_STATUS)
Section titled “房间状态映射(lapsBridge.LIVE_STATUS)”LAPS 房间状态码 → SDK LiveStatus:
101 ongoing · 102 not_started · 103 ended · 104 banned · 105 paused · 106 error · 107 expired · 108 playback · 109/1001 → ended(109 失效 / 1001 删除为 LAPS 终态码)。TERMINAL_STATUSES(@zan/laps)命中即停止自动重连。
单用户事件门控
Section titled “单用户事件门控”观众变更(cmd 2020)按 pld.agentId === identity.agentId 门控——单用户事件不广播给整个直播间(SPEC §6 NEEDS-SERVER-VERIFY)。
横竖屏布局策略
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。共 10 个 store(src/lib/stores/):
| Store | 内容 |
|---|---|
liveStore |
直播状态/方向/屏型/serverTime(实例 ServerClock 当前值)/buyingFrenzy/liveWatch |
playerStore |
播放态/时间轴/进度/控制可见性/回放 |
chatStore |
消息列表(截留 200)/锚定/未读计数/禁言态;敏感词过滤 choke point |
interactStore |
点赞/礼物/飘心动画 |
userStore |
当前观众身份/等级 |
drawerStore |
浮层抽屉栈(base z 1090 + depth×10 自动管理) |
loadingStore |
全局 loading(内外可调,首帧消失,跟随主题色) |
goodsListStore |
商品列表弹框数据(SDK 内拉全量) |
marketingStore |
营销弹框 section 数据 |
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 时抛出异常。
浮层与弹框子系统
Section titled “浮层与弹框子系统”命令式入口(core/)与声明式组件(components/overlay/)双轨,Props 真源 types/overlay.ts:
| 件 | 角色 |
|---|---|
core/modal.ts |
openModal/closeAll 内核 —— 只吃 content/html 字符串、返 Promise<boolean>;per-instance _ownModals 注册,destroy() 仅关本实例 |
core/dialog.ts |
openDialog = openModal 别名(确认/提示二元场景;表单/snippet 标 defer) |
core/drawer.ts |
命令式 defer(声明式 <Drawer> 已覆盖;命令式消费方出现再补) |
core/toast.ts |
svelte-sonner 薄封装:全局唯一 <Toaster> + 引用计数惰性挂载,实例级 ownIds 追踪,destroy()/直播结束仅 dismiss 本实例 |
Modal.svelte / overlay/{Dialog,Drawer,Toaster}.svelte |
UI 内核与声明式组件 |
- z 层级:嵌套抽屉由
drawerStore(base 1090 + depth×10)自动管理;商品橱窗/营销面板已收敛为<Drawer contained>。 contained:true= SDK 容器内absolute;false= bodyfixed。- Dialog
preventScroll={false}(不锁宿主 body);Drawer 用--vh约束高度(禁裸100vh)。
遥测 / Reporter 子系统(src/lib/core/telemetry/ + Reporter.ts)
Section titled “遥测 / Reporter 子系统(src/lib/core/telemetry/ + Reporter.ts)”createReporter(emitter, config).start() 经 onAny 通配订阅真实 SDK 事件,链路:采样 → 60 秒窗口/漏斗/APM → 紧凑编码 + 字段裁剪 → 有界队列 → 单一 Transport。稳定窗口每 5 个合并;故障受 3 次/5 分钟即时预算约束。killSwitch + 故障隔离保证采集异常不影响业务;setIdentity(userId, deviceId) 在 auth 后注入明文 userKey 与 SHA-256 截断 deviceKey。
| 子模块 | 职责 |
|---|---|
codes / types |
枚举码表(reason 归一 / capBits) |
sampler |
会话稳定分桶 + 分层采样;生产提采受 diagnostic userKey/30 分钟闸门约束 |
funnel |
漏斗阶段时延 / 掉队定位 |
stall |
卡顿状态机(开窗守卫 / 60s 聚合) |
encoder |
deviceKey 哈希 / 元组编码;userKey 由 Reporter 标准化后明文写入上下文 |
queue |
单飞 + 全抖动退避 + 有界 + L0 去重 |
transport |
sendBeacon 字节预算 / 拆分 / fetch 批 |
敏感词过滤(src/lib/sensitive-word/)
Section titled “敏感词过滤(src/lib/sensitive-word/)”normalize(全/半角折叠 + 零宽剥离 + 大小写,手写码点表,无 NFKC/Segmenter/\p{},iOS12 安全)→ matcher.findAll(O(n) 逐字符扫描、无回溯,exact/fuzzy 共用 normalize)→ MatchResult(hit/spans/masked)。
- 单一 choke point:
chatStore—— 发送侧 shadow echo(命中短路不真发,本地 echo 发送者可见原文)+ 接收/历史一致过滤。 contentMask.ts:手机号/链接打码,SDKConfig.messageMask参数化,rules 可扩展。- fail-open 总则:matcher 空 / 匹配异常 /
system|welcome|本实例local-前缀均放行;服务端为权威兜底。 - R2 词库通道 / R3 LAPS 变更事件:🚧 P-0 gate 待解(端点 / @zan cmd 未实测)。
LiveRoom.svelte 根组件 — setContext, CSS Grid 布局├── Loading.svelte 全局 loading(首帧消失,替换 TCPlayer spinner,跟随主题色)├── StatusOverlay.svelte 直播状态覆盖(not_started/ended/playback)├── TopBar.svelte 顶部容器(对齐小程序顶部元素)│ ├── AccountBar.svelte 主播/店铺账号条│ ├── StoreInfoBar.svelte 店铺信息│ ├── RightButtons.svelte 右侧按钮组(OperateButton 复用)│ ├── NoticeBar / AdBanner 公告 / 广告│ ├── DescriptionMarquee.svelte 描述跑马灯│ └── CountdownBanner.svelte 开播倒计时├── PortraitLayout.svelte 竖屏布局容器│ ├── Player.svelte active/standby 双槽位 + TCPlayer 覆盖层(运行时切档预加载)│ │ ├── Danmu.svelte 弹幕(DanmuPlugin 激活)│ │ ├── Watermark.svelte 用户水印│ │ ├── FullscreenButton.svelte 全屏切换│ │ └── PlayerControls.svelte 播放/暂停/音量/清晰度│ │ └── VodProgressBar.svelte VOD 进度条(回放模式)│ ├── Chat.svelte 聊天面板│ │ ├── VirtualList.svelte 虚拟列表(iOS12兼容)│ │ ├── ChatMessage.svelte 单条消息气泡(含 MemberLevelBadge 等级徽标)│ │ └── ChatInput.svelte 输入框 + 发送│ │ ├── EmojiPicker.svelte 表情选择│ │ └── ShortcutComments.svelte 快捷评论胶囊│ ├── Interact.svelte 互动层(点赞/礼物动画)│ │ ├── LikeButton.svelte 点赞按钮(心跳动画)│ │ ├── GiftAnimation.svelte CSS burst 动画│ │ └── MilestoneToast.svelte 里程碑提示│ ├── BottomActionBar.svelte 底部操作栏(点赞/购物车/分享/更多)│ ├── HotSellCard.svelte 主播热卖商品浮层卡片│ ├── BuyingFeed.svelte 抢购飘条│ ├── ViewerCount.svelte 观看人数浮层│ ├── CouponStack.svelte ⚠️ 已停止内部挂载(券浮标由 FloatBar 取代,保留导出)│ └── TaskEntry.svelte 任务奖励入口├── LandscapeLayout.svelte 横屏布局容器│ └── TabSwitch.svelte 横屏 Tab 切换├── Marketing/ 商品营销(按 numIid 拉券/兑换)│ ├── GoodsCard.svelte 主播商品卡片│ ├── CouponBadge.svelte 优惠券通知徽标│ └── CouponSection / ExchangeCouponSection / GenericSection 营销面板 section├── room-marketing/ 房间营销(签到/福袋/券,读 roomMarketingStore)│ ├── CouponLayer.svelte 券弹窗│ ├── CheckInLayer.svelte 签到弹窗│ ├── LuckyLayer.svelte 福袋弹窗│ ├── FloatBar.svelte 营销浮标(横竖屏统一顶部横向靠左、溢出横滚;最多 3 项,有可见项即显示更多)│ ├── FloatMoreDrawer.svelte 互动工具聚合抽屉(时间轴列表、60% 高度、静默刷新)│ ├── InteractiveActivityList.svelte 时间轴列表(纯展示)│ ├── InteractiveActivityCard.svelte 单活动卡片│ ├── FloatIconButton.svelte 单图标单元(PNG+类型/福袋状态文案+定时倒计时+×关闭;本地隐藏标记不删除数据)│ ├── floatIcons.ts kind→图标/文案映射│ ├── activity-time.ts 活动时间归一(HTTP/实时)│ ├── interactive-items.ts cards→interactiveItems 纯投影RoomMarketingEntry:prime / resync / refreshInteractiveTools 共用 commitGeneration(last-request-wins);HTTP 结果经 buildPrimeOps → store.reconcilePrime(静默路径不 primeShow)。LiveSDK._refreshInteractiveTools() 仅供内部 UI 委托。
├── GoodsListPanel.svelte 商品列表弹框(<Drawer contained>)├── GoodsMarketingPanel.svelte 营销弹框(<Drawer contained>)└── overlay/ + Modal 浮层(容器级,脱离布局树) ├── Modal.svelte 命令式 sdk.modal/dialog 内核 ├── overlay/Dialog.svelte 声明式 bits-ui 居中卡 ├── overlay/Drawer.svelte 原生底弹(弃 vaul,iOS12 触摸) └── overlay/Toaster.svelte svelte-sonner(全局唯一,构造期挂载)所有组件使用 $props()(不使用 export let)。均通过 getContext('livesdk') 读取 SDK。营销 section(CouponSection 等)经 registerSection(kind, component) 注册,集成方可覆盖。
| 模式 | 命令 | 输出 |
|---|---|---|
| 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 实现。
第三层 — 运行时 polyfill banner(仅 UMD)
Section titled “第三层 — 运行时 polyfill banner(仅 UMD)”src/lib/polyfills.ts 注入 UMD bundle 首部:globalThis + Array.prototype.at + Object.fromEntries。保证 bits-ui Dialog 等依赖在 iOS 12.0/12.1 模块求值期不崩(这些 API 编译期无法降级,只能运行时垫片)。
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 推断,待服务端验证 |
遥测 v3 架构
Section titled “遥测 v3 架构”Reporter 被动消费 EventEmitter 的已归一内部信号,按“稀有故障、漏斗终态、60 秒聚合”三种模型写入有界队列;contract manifest 是事件 payload 顺序/类型/枚举、ctx、stats、sidecar 的单一真源,生成本地码表、JSON Schema、14/14 完整参考表和服务端导出日志严格解析器,运行时不依赖 contract 包。数据依次经过 schema 归一、隐私 default-deny、稳定采样、UTF-8 裁剪、单飞 Transport。HTTP/Beacon 或第三方 Transport 为单一 Sink,不 fan-out;Transport 自身统计只搭载下一批,禁止递归上报。Web 与 zan-mini 共享事件/原因/stage,仅 capability 与 adapter 不同。