跳转到内容

架构概览

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

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。


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

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 基线增长的 progresscurrentTimetotalVideoFrames)。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 必须由同一 switchIdswitchedswitch-failedswitch-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.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
GoodsMarketingEntry 营销弹框入口(numIid 惰性拉取,sdk.openGoodsMarketing 驱动)

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


LAPS 协议本体(帧格式、命令码、编解码、认证/心跳/重连状态机)已迁移到依赖包 @zan/lapsLapsAdapter)。历史手写镜像(constants / protocol / LapsWSAdapter / NullWSAdapter / types)全部删除,livesdk 仅保留两个本地件 + 一层桥接:

文件 职责
BrowserTransport 封装原生 WebSocket,实现 @zan/lapsITransport(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 等)
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)命中即停止自动重连。

观众变更(cmd 2020)按 pld.agentId === identity.agentId 门控——单用户事件不广播给整个直播间(SPEC §6 NEEDS-SERVER-VERIFY)。


方向判断(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。共 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),
}
}
  • 返回带 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 时抛出异常。


命令式入口(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>
  • containedtrue = SDK 容器内 absolutefalse = body fixed
  • 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 pointchatStore —— 发送侧 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 纯投影

RoomMarketingEntryprime / resync / refreshInteractiveTools 共用 commitGeneration(last-request-wins);HTTP 结果经 buildPrimeOpsstore.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-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 实现。

第三层 — 运行时 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 — 消费方自行提供。


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

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 不同。