1. JdPlaySS协议
1.1. 使用场景
使用网络协议控制背景音主机,支持局域网发现和控制背景音主机,开放本地和场景音乐资源及控制,主要用于Linux/RTOS的网关。
1.2. 概述
JdPlaySS是JdPlay Server Socket的缩写,参考了mqtt、DLNA协议的思想,是基于TCP协议的简易Server Socket,可用于非Android/iOS系统(如Linux网关)下的局域网背景音系统控制,并可支持多个客户端同时连接。
1.3. 交互图
1.4. 条件&约束
C/S传输数据格式采用json, 且以换行符作为消息分割符。因此不允许消息中存在实际的换行符,但可以存在转义换行符。
1.5. 发现协议
设备发现采用标准的mDNS协议。使用如下命令发现JdPlaySS设备, 服务类型为_jdplayss._tcp
, JdPlaySS服务的ip地址为 192.168.1.67, 端口号为8000
$ avahi-browse -a
+ eno1 IPv4 SmartSpeaker H7[1115] _jdplayss._tcp local
$ avahi-browse -r _jdplayss._tcp
+ eno1 IPv4 SmartSpeaker H7[1115] _jdplayss._tcp local
= eno1 IPv4 SmartSpeaker H7[1115] _jdplayss._tcp local
hostname = [\040none\041.local]
address = [192.168.1.67]
port = [8000]
txt = ["name=BGM-3588" "id=fe0064a3f0f2b5d73588"]
1.6. 控制协议
1.6.1. 消息格式
C/S直接数据传递采用如下字段,注意seq=0保留给服务器端消息的主动反馈。
{
"type": [int][必选]控制包类型,
"seq": [int][可选] 控制包序号,
"s0": [string][可选] 字符串参数0,
"s1": [string][可选] 字符串参数1,
"i0": [int][可选] 整形参数0,
"i1": [int)[可选] 整形参数1,
}
1.6.2. 消息类型
消息包类型有如下几种
type类型值 | 传输方向 | 描述 |
---|---|---|
1 | C->S | CONNECT: 连接请求 |
2 | S->C | CONNACK: 连接应答 |
3 | C->S S->C |
PUBLISH: 发布消息 |
4 | C->S S->C |
PUBACK: 发布消息确认 |
12 | C->S | PINGREQ: 心跳请求 |
13 | S->C | PINGRESP: 心跳反馈 |
14 | C->S | DISCONNECT: 断开请求 |
1.6.3. 消息描述
CONNECT
i0: version 客户端协议版本,本协议为1
i1: keepalive 心跳包时间,单位秒,范围(10~600秒),建议值300, 客户端每隔240秒,发一次心跳包或者传输一次数据。
CONNACK
i0: version 服务器端协议版本,本协议为1
i1: 0:成功 -1:失败
s0: 反馈的消息字符串,用于调试
PUBLISH
用于C/S之间消息传递,目前支持2种消息,控制消息(C->S)和状态主动(S->C)反馈的消息。Client发送消息给Server时,Server会回复PUBACK消息;Server主动反馈消息给Client时,Client端无需回复PUBACK消息。
seq: [必选] 消息序号,从1开始递增(0:保留给Server消息主动发送)
i0: [必选] cmd命令,支持的命令及参数设置如下。
命令值 | 命令名 | 方向 | 功能 | 请求参数 | PubAck回复参数 |
---|---|---|---|---|---|
100 | MEDIA_GET_METADATA | C->S | 获取元数据 | 无 | s0:参考元数据 |
101 | MEDIA_PLAY | C->S | 播放 | 无 | 无参数 |
102 | MEDIA_PAUSE | C->S | 暂停 | 无 | 无参数 |
103 | MEDIA_NEXT | C->S | 下一首 | 无 | 无参数 |
104 | MEDIA_PREV | C->S | 上一首 | 无 | 无参数 |
105 | MEDIA_SEEK | C->S | 跳播 | i1: 跳播位置,单位秒 | 无参数 |
106 | MEDIA_GET_POSITION | C->S | 获取播放位置 | 无 | s0: 当前时间(秒):总时间(秒) |
107 | MEDIA_SET_VOLUME | C->S | 设置音量 | i1: 音量值(0~100) | 无参数 |
108 | MEDIA_GET_VOLUME | C->S | 获取音量 | 无 | i1: 音量值(0~100) |
109 | MEDIA_GET_ALL_LOCAL_MEDIA | C->S | 获取所有本地歌曲信息 | 无 | s0:简单音乐元数据数组 |
111 | MEDIA_SWITCH_PLAY_MODE | C->S | 切换播放模式,如果是电台类,会提示不支持电台类模式切换 | 无 | 无参数 |
115 | MEDIA_GET_PLAY_MODE | C->S | 获取当前播放模式 | 无 | i1: 播放模式 0:重复所有 1:单曲循环 2:随机播放 |
116 | MEDIA_PLAY_TTS | C->S | 用语音播放TTS文本 | s0:TTS文本 | 无参数 |
118 | MEDIA_PLAY_HINT_PATH | C->S | 播放提示音 | s0:完整的提示音路径 | 无参数 |
119 | MEDIA_GET_AUDIO_SOURCE | C->S | 获取当前的音源 | 无 | s0: 当前的音源 |
120 | MEDIA_SET_AUDIO_SOURCE | C->S | 切换音源 | s0:目标音源 | 无参数 |
150 | MEDIA_REPORT_METADATA | S->C | 反馈元数据 | s0:参考元数据 | Client无需回复 |
151 | MEDIA_REPORT_PlAY_STATE | S->C | 反馈播放状态 | i1: 播放状态 0:暂停 1:正在播放 2:缓冲结束 |
Client无需回复 |
152 | MEDIA_REPORT_VOLUME | S->C | 反馈音量 | i1:音量值(0~100) | Client无需回复 |
153 | MEDIA_REPORT_PLAY_MODE | S->C | 反馈当前的播放模式 | i1: 播放模式 0:重复所有 1:单曲循环 2:随机播放 3:顺序播放 |
Client无需回复 |
154 | MEDIA_REPORT_AUDIO_SOURCE | S->C | 反馈当前的音源 | 无 | s0:当前音源 sdcard:本地 bt:蓝牙 online:在线 auxin:外部音频 |
160 | MEDIA_GET_SONGLIST | S->C | 获取歌单 | s0.type: 请求歌曲json参数 0:最近播放列表 1:我的最爱 2:我的歌单 100:当前播放歌单 |
s0:音乐或歌单元数据数组 |
161 | MEDIA_PLAY_SONGLIST | C->S | 播放歌单 | s0:音乐元数据 i1:播放模式 |
无参数 |
200 | DEVICE_POWER_ON | C->S | 开屏 | 无 | 无参数 |
201 | DEVICE_POWER_OFF | C->S | 关屏 | 无 | 无参数 |
202 | DEVICE_POWER_REBOOT | C->S | 重启 | 无 | 无参数 |
203 | DEVICE_GET_POWER_STATUS | C->S | 获取开机状态 | 无 | i1: 0表示关机,1表示开机 |
204 | DEVICE_GET_INFO | C->S | 获取设备信息 | 无 | s0:小可主机信息 uuid、name、version等 |
元数据
{
"playState": 1,
"singer": "刘德华",
"songId": "548408",
"songTitle": "爱你一万年",
"songUrl": "http://www.xxx.com/1.mp3",
"volume": 80
}
音乐元数据
{
"songId": "548408",
"songTitle": "爱你一万年",
"singer": "刘德华",
"source": "migu",
"type":2 //歌单或歌曲类型
}
PUBACK
i0: 跟PUBLISH cmd命令值一致
i1,s0: [可选],如果需要反馈消息
回复参数如上表格
PINGREQ
无参数
PINGRESP
无参数
DISCONNECT
无参数
1.7. 集成&开发
1.7.1. Client端流程
- 通过DLNA设备发现发现背景音主机设备
- 建立socket链接,TCP连接到设备IP地址的8000端口, 开始socket收发数据,并以换行符作为包分隔符。
- 发送CONNECT请求,参考发送数据
{"type":1,"i0":1,"i1":240}
等待Server端反馈连接成功。 参考接收到的数据{"i0":1,"i1":0,"s0":"OK","seq":0,"type":2}
- 如果连接成功,发送 PUBLISH/MEDIA_GET_METADATA 请求,获取元数据。 参考发送数据
{"type":3,"i0":100,"seq":1}
- 根据播放状态决定是否周期性读取播放位置,如果 s0.playState=1, 发送获取播放位置请求,参考发送数据
{"type":3,"i0":106,"seq":1}
- 客户端需要在KeepAlive时间周期内发送数据给服务端以维持长连接,否则服务器端超时会主动断开连接。客户端也需要检测PINGREQ/PINGRESP消息或ACK消息是否收到,如果客户端检测到通信链路异常,则需要跟服务端重连。
- 接收线程可能监听到元数据、播放状态以及音量改变的消息。
1.8. 测试&调试方法
- 进入背景音系统 “设置>网络设置”, 查看ip地址,通过telnet连接背景音系统
telnet ip地址 8000
- 根据如上"Client端流程",在telnet终端通过命令测试交互
发送”连接请求“命令=> telnet 192.168.1.154 8000 Trying 192.168.1.154... Connected to 192.168.1.154. Escape character is '^]'. {"type":1,"i0":1,"i1":240} {"i0":1,"i1":0,"s0":"OK","seq":0,"type":2} {"type":3,"i0":101,"seq":1} {"i0":101,"i1":0,"seq":1,"type":4} {"i0":150,"i1":0,"s0":"{\"playState\":0,\"singer\":\"Brooke White\",\"songId\":\"1552954\",\"songTitle\":\"Let It Be\",\"songUrl\":\"http://mr3.doubanio.com/87b89194e8858151bf1375eb17c96878/0/fm/song/p1552954_128k.mp3\",\"volume\":40}","seq":0,"type":3} {"i0":151,"i1":2,"seq":0,"type":3} {"type":3,"i0":102,"seq":1} {"i0":151,"i1":0,"seq":0,"type":3} {"i0":102,"i1":0,"seq":1,"type":4}
背景音系统返回{"type":1,"i0":1,"i1":240}
发送”播放“命令{"i0":1,"i1":0,"s0":"OK","seq":0,"type":2}
背景音系统返回{"type":3,"i0":101,"seq":1}
发送”暂停“命令{"i0":101,"i1":0,"seq":1,"type":4} {"i0":150,"i1":0,"s0":"{\"playState\":0,\"singer\":\"Brooke White\",\"songId\":\"1552954\",\"songTitle\":\"Let It Be\",\"songUrl\":\"http://mr3.doubanio.com/87b89194e8858151bf1375eb17c96878/0/fm/song/p1552954_128k.mp3\",\"volume\":40}","seq":0,"type":3} {"i0":151,"i1":2,"seq":0,"type":3}
背景音系统返回{"type":3,"i0":102,"seq":1}
{"i0":151,"i1":0,"seq":0,"type":3} {"i0":102,"i1":0,"seq":1,"type":4}
1.9. 参考
1.9.1. 常量定义
为简化开发者工作量,常量定义如下:
//JSSS协议命令
#define CONNECT 1
#define CONNACK 2
#define PUBLISH 3
#define PUBACK 4
#define PINGREQ 12
#define PINGRESP 13
#define DISCONNECT 14
//JSSS协议媒体控制命令
#define MEDIA_GET_METADATA 100
#define MEDIA_PLAY 101
#define MEDIA_PAUSE 102
#define MEDIA_NEXT 103
#define MEDIA_PREV 104
#define MEDIA_SEEK 105
#define MEDIA_GET_POSITION 106
#define MEDIA_SET_VOLUME 107
#define MEDIA_GET_VOLUME 108
#define MEDIA_GET_ALL_LOCAL_MEDIA 109
#define MEDIA_PLAY_LOCAL_SONG 110
#define MEDIA_SWITCH_PLAY_MODE 111
#define MEDIA_GET_SCENE_MUSICS 112
#define MEDIA_PLAY_SCENE_MUSIC 113
#define MEDIA_PLAY_ONCE_LOCAL_SONG 114
#define MEDIA_GET_PLAY_MODE 115
#define MEDIA_PLAY_TTS 116
#define MEDIA_PLAY_HINT 117
#define MEDIA_PLAY_HINT_PATH 118
#define MEDIA_GET_AUDIO_SOURCE 119
#define MEDIA_SET_AUDIO_SOURCE 120
//JSSS协议媒体状态反馈命令
#define MEDIA_REPORT_METADATA 150
#define MEDIA_REPORT_PLAY_STATE 151
#define MEDIA_REPORT_VOLUME 152
#define MEDIA_REPORT_PLAY_MODE 153
#define MEDIA_REPORT_AUDIO_SOURCE 154
#define MEDIA_REPORT_PROGRESS 155
#define MEDIA_GET_SONGLIST 160
#define MEDIA_PLAY_SONGLIST 161
//JSSS协议设备控制命令
#define DEVICE_POWER_ON 200
#define DEVICE_POWER_OFF 201
#define DEVICE_POWER_REBOOT 202
#define DEVICE_GET_POWER_STATUS 203
#define DEVICE_GET_INFO 204
1.9.2. 常用指令示例
连接命令
telnet 10.0.0.26 8000
telnet 192.168.1.176 8000
{"type":1,"i0":109,"i1":240}
获取所有本地歌曲
{"type":3,"i0":109,"seq":1}
播放本地歌曲
注意:在播放本地歌曲前必须先发获取所有本地歌曲的指令。s0:简单音乐元数据数组 ,i1:开始播放索引
{"i0":110,"i1":0,"s0":"[{\"songId\":\"40\",\"songTitle\":\"DreamVillage_GuZheng-pre\"},{\"songId\":\"101\",\"songTitle\":\"A.I.N.Y.-[爱你]\"}]","seq":1,"type":3}
播放本地单首歌曲,单曲不循环
注意:s0此时为单个音乐元数据json字符串
{"i0":114,"i1":0,"s0":"{\"songId\":\"40\",\"songTitle\":\"DreamVillage_GuZheng-pre\"}","seq":1,"type":3}
获取当前的音源
注意:s0参数有sdcard 本地、bt 蓝牙、online 在线、auxin 外部音频四种
{"i0":119,"seq":1,"type":3}
{"i0":119,"i1":0,"s0":"sdcard","seq":1,"type":4}
获取我的歌单列表
{"i0":160,"i1":1,"seq":1,"type":3, "s0":"{\"type\":2}"}
播放歌单
{"i0":161,"i1":1,"seq":1,"type":3,"s0":"{\"songTitle\":\"测试\",\"songId\":\"261aba01-760f-47\",\"singer\":\"\",\"source\":\"\",\"type\":2}"}
获取当前播放列表
{"i0":160,"seq":1,"type":3, "s0":"{\"type\":100}"}
播放当前播放列表某首歌曲
注意:i1为播放第几首歌曲
{"i0":161,"i1":1,"seq":1,"type":3,"s0":"{\"type\":100}"}
切换音源
注意:切换完本地或在线音源后要发送播放指令才能播放歌曲
想播放本地歌曲使用上面109(获取本地歌曲)和110(播放本地歌曲)指令结合实现
想随机播放在线歌曲可以使用120(切到在线音源)和 101(播放歌曲)指令结合实现
C -> S
{"i0":120,"s0":"online","seq":1,"type":3}
{"i0":120,"s0":"bt","seq":1,"type":3}
{"i0":120,"s0":"sdcard","seq":1,"type":3}
{"i0":120,"s0":"auxin","seq":1,"type":3}
S -> C
{"i0":120,"i1":0,"seq":1,"type":4}
i1值为0表切换音源成功,-1表切换音源失败
播放
{"i0":101,"seq":1,"type":3}
获取当前音乐元数据
C -> S
{"type":3,"i0":100,"seq":1}
S -> C
{"i0":100,"i1":0,"s0":"{\"playState\":1,\"singer\":\"Harry Styles\",\"songId\":\"003luGpS16qZgi\",\"songTitle\":\"Sweet Creature\",\"songUrl\":\"http://dl.stream.qqmusic.qq.com/C400003luGpS16qZgi.m4a?vkey=E3EDE3145DFD5364AA8DE7315B218A9A963C25BA76AD47D7760486319CB9A3980125E46BD1A19BF0FBC94D2099B32DD27B0B104FF236EBE5&guid=5358354936&fromtag=30\",\"volume\":7}","seq":1,"type":4}
切换播放模式
{"type":3,"i0":111,"seq":4}
1.10. FAQ
E:开关屏操作仅针对于有屏设备
Q:为什么Server端Socket连接会主动断开
A:请检查是否在KeepAlive时间内发送过PING或者PUBLISH命令
Q:为什么Client发布消息不成功
A:请检查seq字段是否设置为非0值,并主动递增
Q:Window 使用 PuTTY 进行测试
A: 
发送连接请求{"type":1,"i0":1,"i1":240}
Q:获取当前播放的歌曲信息策略
A:Socket连接上之后,只需要发送一次命令 MEDIA_GET_METADATA 获取当前播放的歌曲信息,只要Socket连接一直不断,以后都通过MEDIA_REPORT_PlAY_STATE主动反馈歌曲信息即可。
Q:换行符用哪个
A:换行符统一使用 \n
Q:怎么获取小可主机设备ID
A:发现设备的时候,notify包里面包含设备ID
Q:播放模式有哪些
A:
0 列表循环 REPEAT_ALL
1 单曲循环 REPEAT_ONE
2 随机播放 SHUFFLE
3 顺序播放 ORDER
Q:哪些信息发生改变服务端会主动上报
A:小可主机端的歌曲、音量、播放状态、播放模式发生改变后,会主动上报新状态
Q:怎么获取播放进度
A:当前播放进度,要C主动向S查询,指令:MEDIA_GET_POSITION
Q:客户端发M-SEARCH包,server并不会立即应答
A:SSDP包是组播包,存在被丢包的可能。每次发送search包的时候,可以发多个
Q:如果网络不好的情况下,小可在什么情况下会主动断开连接,有没有重连机制
A:JSSS 小可作为服务器端,不会根据网络状况来决定断开。会重连,客户端需要在KeepAlive时间周期内发送数据给服务端以维持长连接,否则服务器端超时会主动断开连接。客户端也需要检测PINGREQ/PINGRESP消息或ACK消息是否收到,如果客户端检测到通信链路异常,则需要跟服务端重连。