33本模块定义了一个用于跨端同步播放媒体信息的数据传输协议,在双方结构情况支持的情况下,可以以任意形式传输并同步包括歌词在内的音频媒体播放状态。
44
55## 协议概述
6- 本协议基于二进制WebSocket消息实现音乐播放状态同步,包含播放控制、元数据传输、歌词同步等功能。协议使用小端字节序进行二进制序列化。
6+
7+ 本协议基于二进制 WebSocket 消息实现音乐播放状态同步,包含播放控制、元数据传输、歌词同步等功能。协议使用小端字节序进行二进制序列化。
78
89## 数据结构定义
910
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
2123struct Artist {
2224 id : NullString , // 艺术家的唯一标识字符串
@@ -25,6 +27,7 @@ struct Artist {
2527```
2628
2729### LyricWord 歌词单词
30+
2831``` rust
2932struct LyricWord {
3033 start_time : u64 , // 单词开始时间,单位为毫秒
@@ -34,6 +37,7 @@ struct LyricWord {
3437```
3538
3639### LyricLine 歌词行
40+
3741``` rust
3842struct 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// 二进制 -> 结构体
232237pub 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
257261interface 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 -->
0 commit comments