Skip to content

一起听 · 接口对接文档(开发者向)

本文档面向第三方客户端 / 二次开发者。如果你只是用澜音客户端听歌,去看 一起听使用指南

一、接口总览

一起听由两部分接口组成:

类型协议路径鉴权用途
RESTHTTPS/api/listen-together/*Logto access token房间生命周期(创建 / 解析口令 / 查预览 / 查我的房间)
WebSocketSocket.IOnamespace /lt(挂在根,不带 /api)handshake auth.token实时事件(进出房间 / 播放控制 / 队列 / 角色 / 聊天)

完整 Swagger 文档:

Swagger 不原生支持 WebSocket,如果以后有 AsyncAPI 客户端需求会迁移过去。

二、鉴权

REST 和 WebSocket 用同一份 Logto access token,resource 必须是 生产环境的 API 地址(无论你连的是 dev 还是 prod 后端):

text
https://api.ceru.shiqianjiang.cn/api

REST: HTTP header Authorization: Bearer <token>(部分公开端点除外,详见后面)

WebSocket: socket.io handshake auth payload

js
const sock = io('https://api.ceru.shiqianjiang.cn/lt', {
  transports: ['websocket'],
  auth: { token }
})

鉴权失败会立刻收到 lt:error { code: 'AUTH_FAILED' } 然后 disconnect

三、REST 接口

3.1 创建房间

http
POST /api/listen-together/create
Authorization: Bearer <token>
Content-Type: application/json

{
  "mode": "group",
  "name": "一起摇起来",
  "maxMembers": 50
}

返回 RoomMeta + shareUrl + shareText注意创建后房主还没加入房间,要紧接着 socket emit room:join 才算进房。

3.2 解析口令

http
POST /api/listen-together/resolve
Authorization: Bearer <token>
Content-Type: application/json

{
  "code": "XTYASG"            // 或者整段分享文案,后端会从 #CODE# 中提取
}

返回 RoomPreview { code, mode, name, ownerId, maxMembers, createdAt },房间不存在 / 过期返回 null

3.3 公开预览(无需登录)

http
GET /api/listen-together/{code}

返回 { code, mode, name, maxMembers, createdAt }(不含 ownerId,避免分享落地页泄露隐私)。

3.4 查我的当前房间

http
GET /api/listen-together/my/current
Authorization: Bearer <token>

返回 { code, mode, name } | null。常用于客户端启动时检查"上次还没退的房间"。

四、WebSocket 事件参考

4.1 命名约定

  • 客户端 → 服务端: <domain>:<action>(行为意图)
  • 服务端 → 客户端: <domain>:<state>(状态变更)
  • 错误回包统一 lt:error { code, message },不用 socket.error
  • 所有客户端事件名都列在 src/listen-together/constants.tsClientEvents 常量;服务端事件在 ServerEvents

4.2 客户端 → 服务端

事件描述payload
room:join加入房间{ code: string }
room:leave主动离开{}
room:resume30s 墓碑期内续连{ code: string; lastSeq?: number }
room:sync-contexthost 上传共享列表 / 当前播放{ current?, queue?: SongRef[] }
ctl:play播放{ time?: number }
ctl:pause暂停{ time?: number }
ctl:seek拖进度{ time: number }
ctl:change-song立即切歌{ song: SongRef }
ctl:play-queue-item跳到队列指定项{ itemId: string }
ctl:skip下一首(带 seq 幂等){ seq?: number }
ctl:prev上一首{ seq?: number }
ctl:song-ended当前歌自然播完信号{ songmid?, source?, seq? }
queue:request普通成员点歌(进 pending){ song: SongRef }
queue:addadmin+ 直接入队{ song: SongRef }
queue:approveadmin+ 审批通过{ reqId: string }
queue:rejectadmin+ 审批拒绝{ reqId: string }
queue:remove删队列项{ itemId: string }
role:promoteowner 提升管理员{ userId: string }
role:demoteowner 撤销管理员{ userId: string }
member:kick踢人{ userId: string }
chat:send发送聊天{ type: 'text'|'emoji'|'sticker'; content: string }
ping时钟同步 ping{ clientTs: number }

4.3 服务端 → 客户端

事件描述payload 形状
room:state房间完整快照(进房 / 续连后第一时间下发){ meta, members, current, queue, pending, chat, serverTs }
room:dismissed房间已解散{}
member:join新成员加入RoomMember
member:leave成员离开{ userId, reason? }
member:kicked自己被踢(只发给被踢者){ byUser: { nickname } }
member:reconnect成员墓碑期内续连成功{ userId, nickname? }
sync播放状态同步(同步算法核心)PlaybackSnapshot { song, isPlaying, anchorPos, anchorAt, seq, action?, operatorId? }
queue:update队列变更{ queue: QueueItem[] }
pending:update待审批列表变更{ pending: PendingItem[] }
role:changed角色变更{ userId, role, reason? }
chat:msg用户聊天ChatMsg
chat:system系统消息(模板 key 在 content,参数在 meta)ChatMsg(type='system')
pongping 回包{ clientTs, serverTs }
lt:error业务错误{ code, message }

4.4 错误码

lt:error.code 取值:

code含义
AUTH_FAILED鉴权失败(token 过期 / resource 错)
ROOM_NOT_FOUND房间不存在或已过期
ROOM_FULL房间已满
ALREADY_IN_ROOM用户已在另一房间
PERMISSION_DENIED权限不足(普通成员尝试控制播放等)
INVALID_PAYLOAD入参格式错
NO_SONG当前房间无歌(尝试 play/pause/seek/skip 时)
INTERNAL_ERROR兜底错误

五、同步算法说明

sync 事件是整套机制的核心。每条 sync 都带:

  • anchorPos(锚点位置,秒) + anchorAt(锚点对应的服务端 ms epoch)
  • isPlaying(逻辑播放状态)
  • seq(单调递增版本号)

客户端推算"现在应该在哪儿":

ts
const nowServer = Date.now() + clockOffset
const elapsed = isPlaying ? Math.max((nowServer - anchorAt) / 1000, 0) : 0
const targetPos = anchorPos + elapsed

clockOffset = serverTs - clientTs(估算值),通过 ping/pong 五次取中位数得到。

漂移处理:

  • 漂移 > 0.5s 或动作 ∈ {seek, change-song, play-queue-item, skip} → 硬 seek 对齐
  • 漂移 ≤ 0.5s → 接受偏差,不打扰播放

乱序保护: 客户端只接受 payload.seq > localSeq 的 sync,丢弃旧包。

六、数据结构

ts
interface SongRef {
  songmid: string
  source: 'wy' | 'tx' | 'kg' | 'mg'
  name?: string
  singer?: string
  cover?: string
  duration?: number
  albumName?: string
  albumId?: string
  hash?: string                      // 酷狗特有
  types?: any[]
  lrc?: string | null
}

interface RoomMember {
  userId: string
  nickname: string
  avatar?: string
  role: 'owner' | 'admin' | 'member'
}

interface PlaybackSnapshot {
  song: SongRef | null
  isPlaying: boolean
  anchorPos: number   // seconds
  anchorAt: number    // ms epoch
  seq: number
  action?: 'play' | 'pause' | 'seek' | 'change-song' | 'play-queue-item' | 'skip' | 'prev' | 'song-ended' | 'room-sync-context'
  operatorId?: string
}

interface QueueItem {
  id: string
  song: SongRef
  addedBy: { userId, nickname, avatar? }
  ts: number
}

interface ChatMsg {
  id: string
  type: 'text' | 'emoji' | 'sticker' | 'system'
  content: string
  from: { userId, nickname, avatar? } | null
  meta?: Record<string, string>      // 系统消息的模板参数
  ts: number
}

七、最小可用客户端例子

ts
import { io } from 'socket.io-client'

const sock = io('https://api.ceru.shiqianjiang.cn/lt', {
  transports: ['websocket'],
  auth: { token: '<logto access token>' },
  reconnection: true
})

sock.on('connect', () => sock.emit('room:join', { code: 'XTYASG' }))

sock.on('room:state', (state) => {
  console.log('已加入', state.meta.name, '当前歌:', state.current.song?.name)
})

sock.on('sync', (snap) => {
  // 应用到本地 audio: 计算 targetPos 后对齐
  console.log('sync seq=', snap.seq, 'isPlaying=', snap.isPlaying)
})

sock.on('chat:msg', (msg) => {
  console.log(msg.from?.nickname, ':', msg.content)
})

sock.on('lt:error', (err) => console.warn('[lt]', err.code, err.message))

// host 控制播放
sock.emit('ctl:play', { time: 12.5 })
sock.emit('ctl:seek', { time: 60 })
sock.emit('chat:send', { type: 'text', content: 'hi 大家好' })

八、运维 / 限制

  • 房间 TTL: 30 分钟无活动自动清理(任何 emit 都会续期)
  • 聊天历史: Redis 环形缓冲 100 条,超出自动覆盖最早
  • 审计日志: 所有事件都进 room_audit_log 表,默认保留 90 天(AUDIT_LOG_RETENTION_DAYS 环境变量可调)
  • PM2 cluster 友好: WebSocket 通过 Redis pub/sub 跨 worker 同步,审计 flush 走 Redis Stream + leader 锁单点写库

九、相关文档

Released under the Apache License 2.0 License.