Skip to content

Commit 0ddca36

Browse files
committed
ws-protocol: 修正文档和定义
1 parent 346fd92 commit 0ddca36

File tree

4 files changed

+122
-38
lines changed

4 files changed

+122
-38
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ws-protocol/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ binrw = "^0.14"
1919
serde_bytes = "^0.11"
2020
anyhow = "^1.0"
2121

22+
[dev-dependencies]
23+
serde_json = "^1.0"
24+
2225
[target.'cfg(target_arch = "wasm32")'.dependencies]
2326
wasm-bindgen = { version = "^0.2" }
2427
serde-wasm-bindgen = { version = "^0.6" }

packages/ws-protocol/README.md

Lines changed: 102 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
本模块定义了一个用于跨端同步播放媒体信息的数据传输协议,在双方结构情况支持的情况下,可以以任意形式传输并同步包括歌词在内的音频媒体播放状态。
44

55
## 协议概述
6-
本协议基于二进制WebSocket消息实现音乐播放状态同步,包含播放控制、元数据传输、歌词同步等功能。协议使用小端字节序进行二进制序列化。
6+
7+
本协议基于二进制 WebSocket 消息实现音乐播放状态同步,包含播放控制、元数据传输、歌词同步等功能。协议使用小端字节序进行二进制序列化。
78

89
## 数据结构定义
910

@@ -13,10 +14,11 @@
1314

1415
为便于阅读理解,以下定义了一部分常见数据结构:
1516

16-
- `NullString`: 一个以 `\0` 结尾的 UTF-8 编码字符串
17-
- `Vec<T>`: 一个以一个 `u32` 开头作为数据结构 `T` 的数量的线性数据结构,后紧跟指定数量的 `T` 数据结构
17+
- `NullString`: 一个以 `\0` 结尾的 UTF-8 编码字符串
18+
- `Vec<T>`: 一个以一个 `u32` 开头作为数据结构 `T` 的数量的线性数据结构,后紧跟指定数量的 `T` 数据结构
1819

1920
### Artist 艺术家信息
21+
2022
```rust
2123
struct Artist {
2224
id: NullString, // 艺术家的唯一标识字符串
@@ -25,6 +27,7 @@ struct Artist {
2527
```
2628

2729
### LyricWord 歌词单词
30+
2831
```rust
2932
struct LyricWord {
3033
start_time: u64, // 单词开始时间,单位为毫秒
@@ -34,6 +37,7 @@ struct LyricWord {
3437
```
3538

3639
### LyricLine 歌词行
40+
3741
```rust
3842
struct LyricLine {
3943
start_time: u64, // 歌词行开始时间,单位为毫秒
@@ -67,12 +71,12 @@ struct LyricLine {
6771

6872
无需任何额外数据。
6973

70-
### SetMusicId (2) (接收)
74+
### SetMusicInfo (2) (接收)
7175

7276
报告当前歌曲的主要信息,提供以下数据:
7377

7478
```rust
75-
struct SetMusicId {
79+
struct SetMusicInfo {
7680
music_id: NullString, // 歌曲的唯一标识字符串
7781
music_name: NullString, // 歌曲名称
7882
album_id: NullString, // 歌曲所属的专辑ID,如果没有可以留空
@@ -227,6 +231,7 @@ struct SeekPlayProgress {
227231
## 序列化/反序列化
228232

229233
### Rust 方法
234+
230235
```rust
231236
// 二进制 -> 结构体
232237
pub fn parse_body(body: &[u8]) -> anyhow::Result<Body>
@@ -237,64 +242,123 @@ pub fn to_body(body: &Body) -> anyhow::Result<Vec<u8>>
237242

238243
### WebAssembly 绑定
239244

240-
对于 WASM 绑定库,所有的字段都将从下划线命名方式转换成小驼峰命名方式,例如 `img_url` 变为 `imgUrl`。
241-
245+
对于 WASM 绑定库,所有的字段和名称都将从下划线命名方式转换成小驼峰命名方式,例如 `img_url` 变为 `imgUrl`。
242246

243247
```typescript
244248
// JavaScript 接口
245-
window.parseBody = function(body: Uint8Array): Promise<{
249+
export function parseBody(body: Uint8Array): {
246250
type: keyof Body,
247251
value: Body<any>,
248252
}>
249-
window.toBody = function(body: object): Promise<Uint8Array>
253+
export function toBody(body: Body<any>): Uint8Array
250254
```
251255

252256
且返回的 `Body` 结构将映射成以 `type` 为枚举类别名称,`value` 为附加数据的结构。
253257

254-
以 `SetMusicId` 主体为例,映射为 TypeScript 数据类型后结构如下(接口名称不限):
258+
`Ping` `SetMusicInfo` `SetMusicAlbumCoverImageURI` 主体为例,映射为 TypeScript 数据类型后结构如下(接口名称不限):
255259

256260
```typescript
257261
interface Artist {
258-
id: string,
259-
name: string,
262+
id: string;
263+
name: string;
264+
}
265+
266+
// 以 Ping 主体为例
267+
interface PingBody {
268+
type: "ping"; // 和文档中标题注明的英文枚举名除了首字母小写外完全一致
269+
// value: undefined; // 无需额外数据
260270
}
261271

262-
interface SetMusicIdBody {
263-
type: "SetMusicId", // 和文档中标题注明的英文枚举名完全一致
272+
// 以 SetMusicInfo 主体为例
273+
interface SetMusicInfoBody {
274+
type: "setMusicInfo"; // 和文档中标题注明的英文枚举名除了首字母小写外完全一致
264275
value: {
265-
musicId: string, // 歌曲的唯一标识字符串
266-
musicName: string, // 歌曲名称
267-
albumId: string, // 歌曲所属的专辑ID,如果没有可以留空
268-
albumName: string, // 歌曲所属的专辑名称,如果没有可以留空
269-
artists: Artist[], // 歌曲的艺术家/制作者列表
270-
duration: number, // 歌曲的时长,单位为毫秒
271-
}
276+
musicId: string; // 歌曲的唯一标识字符串
277+
musicName: string; // 歌曲名称
278+
albumId: string; // 歌曲所属的专辑ID,如果没有可以留空
279+
albumName: string; // 歌曲所属的专辑名称,如果没有可以留空
280+
artists: Artist[]; // 歌曲的艺术家/制作者列表
281+
duration: number; // 歌曲的时长,单位为毫秒
282+
};
283+
}
284+
285+
// 以 SetMusicAlbumCoverImageURI 主体为例
286+
interface SetMusicAlbumCoverImageURIBody {
287+
type: "setMusicAlbumCoverImageURI"; // 和文档中标题注明的英文枚举名除了首字母小写外完全一致
288+
value: {
289+
imgUrl: string; // 歌曲专辑图片对应的资源链接,可以为 HTTP URL 或 Base64 Data URI
290+
};
272291
}
273292
```
274293

275-
## 使用示例
294+
## 使用示例 (TypeScript)
276295

277296
### 设置音乐信息
278-
```rust
279-
let body = Body::SetMusicId {
280-
id: "123".into(),
281-
name: "Sample Song".into(),
282-
duration: 240000
283-
};
284-
285-
let bin_data = to_body(&body)?;
286-
websocket.send(bin_data);
297+
298+
```typescript
299+
import { toBody } from "@applemusic-like-lyrics/ws-protocol";
300+
301+
const encoded = toBody({
302+
type: "setMusicInfo",
303+
value: {
304+
musicId: "1",
305+
musicName: "2",
306+
albumId: "3",
307+
albumName: "4",
308+
artists: [
309+
{
310+
id: "5",
311+
name: "6",
312+
},
313+
],
314+
duration: 7,
315+
},
316+
});
317+
318+
console.log(encoded); // Uint8Array
287319
```
288320

289321
### 处理播放进度更新
322+
323+
<!-- prettier-ignore-start -->
290324
```typescript
291-
// 浏览器环境
292-
websocket.onmessage = async (event) => {
293-
const data = new Uint8Array(await event.data.arrayBuffer());
294-
const message = await window.parseBody(data);
295-
296-
if (message.type === "onPlayProgress") {
297-
audioElement.currentTime = message.value.progress;
325+
import { parseBody } from "@applemusic-like-lyrics/ws-protocol";
326+
327+
const body = new Uint8Array(
328+
[
329+
0x02, 0x00, // SetMusicInfo
330+
0x31, 0x00, // musicId: "1"
331+
0x32, 0x00, // musidName: "2"
332+
0x33, 0x00, // albumId: "3"
333+
0x34, 0x00, // albumName: "4"
334+
0x01, 0x00, 0x00, 0x00, // artists: size 1
335+
0x35, 0x00, // artist.id: "5"
336+
0x36, 0x00, // artist.name: "6"
337+
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // duration: 7
338+
]
339+
);
340+
341+
const parsed = parseBody(body);
342+
343+
console.log(parsed);
344+
345+
/* 预期输出:
346+
{
347+
type: "setMusicInfo",
348+
value: {
349+
musicId: "1",
350+
musicName: "2",
351+
albumId: "3",
352+
albumName: "4",
353+
artists: [
354+
{
355+
id: "5",
356+
name: "6"
357+
}
358+
],
359+
duration: 7
298360
}
299361
}
362+
*/
300363
```
364+
<!-- prettier-ignore-end -->

packages/ws-protocol/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,23 @@ fn body_test() {
173173
}],
174174
duration: 7,
175175
};
176+
let encoded = to_body(&body).unwrap();
177+
// print hex
178+
print!("[");
179+
for byte in &encoded {
180+
print!("0x{:02x}, ", byte);
181+
}
182+
println!("]");
183+
assert_eq!(parse_body(&encoded).unwrap(), body);
184+
println!("{}", serde_json::to_string_pretty(&body).unwrap());
185+
let body = Body::SetMusicAlbumCoverImageURI {
186+
img_url: "https://example.com".into(),
187+
};
188+
assert_eq!(parse_body(&to_body(&body).unwrap()).unwrap(), body);
189+
println!("{}", serde_json::to_string_pretty(&body).unwrap());
190+
let body = Body::Ping;
176191
assert_eq!(parse_body(&to_body(&body).unwrap()).unwrap(), body);
192+
println!("{}", serde_json::to_string_pretty(&body).unwrap());
177193
}
178194

179195
#[cfg(target_arch = "wasm32")]

0 commit comments

Comments
 (0)