一.电视机向224.0.0.251发送mdns组播消息并注册
iPhone设备发现屏幕镜像设备依靠的是mdns协议,这是一个用于局域网发现设备的协议,仿照dns协议,镜像设备启动后,会注册自己到路由器的组播地址224.0.0.251,当iPhone设备发起搜索协议的时候,会发送搜索的信息到224.0.0.251,这时路由器会转发信息到所有曾经注册到224.0.0.251的镜像设备
._airplay._tcp.local airPlayPort 7000
._raop._tcp.local airTunesPort 49152
字典信息
"deviceid" -> "58:55:CA:1A:E2:88"
"features" -> "0x5A7FFEE6""
"flags" -> "0x4"
"model" -> "AppleTV3,1"
"pk" -> "b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7"
"pi" -> "2e388006-13ba-4041-9a67-25dd4a43d536"
"rhd" -> "5.6.0.0"
"pw" -> "false"
"srcvers" -> "220.68"
"vv" -> "2"
iPhone收到上面的信息之后,还会再次查询txt信息,依然把上面的字典信息返回,这时iPhone上面会看到新的镜像设备
My AirPlay Device
二.建立会话,建立连接与密钥交换
参考:https://www.jianshu.com/p/ae7eb3fba1e9
三.一些请求与认证的信息
1.GET | /info
iPhone没有发送什么信息过来,只有一个请求,这是屏幕镜像设备需要准备比较多的数据,形成一个字典,字典里面可能包含信息对,也可能包含字典,还可以包含字典,并把数据保存为plist二进制形式发送给手机,例如根字典数据如下
字典数据参考
NSDictionary r_node = new NSDictionary();
r_node["txtAirPlay"] = new NSData(AirPlayServer_mdns.bytesProperties);
r_node["features"] =new NSNumber((UInt64)0x1E << 32 | 0x5A7FFFF7);
r_node["audioFormats"] = audio_formats_node;
r_node["pi"] = new NSString("2e388006-13ba-4041-9a67-25dd4a43d536");
r_node["vv"] = new NSNumber(2);
r_node["statusFlags"] = new NSNumber(68);
r_node["keepAliveLowPower"] = new NSNumber(1);
r_node["sourceVersion"] = new NSString("220.68");
r_node["pk"] = new NSData(HexStringToBytes("b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7"));
r_node["keepAliveSendStatsAsBody"] = new NSNumber(1);
r_node["deviceID"] = new NSString("58:55:CA:1A:E2:88");
r_node["name"] = new NSString("My AirPlay Device");
r_node["model"] = new NSString("AppleTV2,1");
r_node["macAddress"] = new NSString("58:55:CA:1A:E2:88");
NSArray audio_formats_node = new NSArray();
NSDictionary audio_format_0_node = new NSDictionary();
audio_format_0_node["type"] = new NSNumber(100);
...
audio_formats_node.Add(audio_format_0_node);
NSDictionary audio_format_1_node = new NSDictionary();
audio_format_1_node["type"] = new NSNumber(101);
...
audio_formats_node.Add(audio_format_1_node);
NSArray audio_latencies_node = new NSArray();
NSDictionary audio_latencies_0_node = new NSDictionary();
audio_latencies_0_node["outputLatencyMicros"] = new NSNumber(0);
...
audio_latencies_node.Add(audio_latencies_0_node);
NSDictionary audio_latencies_1_node = new NSDictionary();
audio_latencies_1_node["outputLatencyMicros"] = new NSNumber(0);
...
audio_latencies_node.Add(audio_latencies_1_node);
r_node["audioLatencies"] = audio_latencies_1_node;
NSArray displays_node = new NSArray();
NSDictionary displays_0_node = new NSDictionary();
displays_0_node["uuid"] = new NSString("e0ff8a27-6738-3d56-8a16-cc53aacee925");
displays_0_node["widthPhysical"] = new NSNumber(0);
displays_0_node["heightPhysical"] = new NSNumber(0);
...
displays_node.Add(displays_0_node);
r_node["displays"] = displays_node;
上述数据中,r_node["txtAirPlay"]比较特别,它是文章AirPlay 镜像协议-上(发现)中的字典信息,而且格式比较特别,举例如下
假如有配对信息a->b,和cd->ef, 那么数据AirPlayServer_mdns.bytesProperties内容为{0x3, 'a', '=', 'b', 0x5, 'c', 'd', '=', 'e', 'f'}
,可以看到配对信息用等号=相连,配对信息前面有一字节的信息表明该组信息的长度,所以限制了配对信息长度不可以大于255,在返回给iPhone的RTSP信息中,HEADER部分增加信息表明返回的是二进制形式的plist文件
response.AddHeader("Content-Type", "application/x-apple-binary-plist");
GET /info RTSP/1.0
X-Apple-ProtocolVersion: 1
Content-Type: application/x-apple-binary-plist
CSeq: 0
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 70
response
RTSP/1.0 200 OK
CSeq: 0
content-length: 689
?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>audioFormats</key>
<array>
<dict>
<key>audioInputFormats</key>
<integer>67108860</integer>
<key>audioOutputFormats</key>
<integer>67108860</integer>
<key>type</key>
<integer>100</integer>
</dict>
<dict>
<key>audioInputFormats</key>
<integer>67108860</integer>
<key>audioOutputFormats</key>
<integer>67108860</integer>
<key>type</key>
<integer>101</integer>
</dict>
</array>
<key>audioLatencies</key>
<array>
<dict>
<key>audioType</key>
<string>default</string>
<key>inputLatencyMicros</key>
<false/>
<key>type</key>
<integer>100</integer>
</dict>
<dict>
<key>audioType</key>
<string>default</string>
<key>inputLatencyMicros</key>
<false/>
<key>type</key>
<integer>101</integer>
</dict>
</array>
<key>displays</key>
<array>
<dict>
<key>features</key>
<integer>14</integer>
<key>height</key>
<integer>1080</integer>
<key>heightPhysical</key>
<false/>
<key>heightPixels</key>
<integer>1080</integer>
<key>maxFPS</key>
<integer>30</integer>
<key>overscanned</key>
<false/>
<key>refreshRate</key>
<integer>60</integer>
<key>rotation</key>
<false/>
<key>uuid</key>
<string>e5f7a68d-7b0f-4305-984b-974f677a150b</string>
<key>width</key>
<integer>1920</integer>
<key>widthPhysical</key>
<false/>
<key>widthPixels</key>
<integer>1920</integer>
</dict>
</array>
<key>features</key>
<integer>130367356919</integer>
<key>keepAliveSendStatsAsBody</key>
<integer>1</integer>
<key>model</key>
<string>AppleTV2,1</string>
<key>name</key>
<string>Apple TV</string>
<key>pi</key>
<string>b08f5a79-db29-4384-b456-a4784d9e6055</string>
<key>sourceVersion</key>
<string>220.68</string>
<key>statusFlags</key>
<integer>68</integer>
<key>vv</key>
<integer>2</integer>
</dict>
</plist>
2.POST | /pair-setup
该请求,iPhone没有携带重要的信息,镜像设备发送一个ed25519的public key到iPhone,发送内容作为RTSP的Body部分,该ed25519秘钥对可以在使用的时候才生成
POST /pair-setup RTSP/1.0
Content-Type: application/octet-stream
CSeq: 1
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 32
RTSP/1.0 200 OK
CSeq: 1
content-length: 32
3.POST | /pair-verify
这两次请求是非常关键的,首先iPhone发送了自己的加密信息中的公钥部分,也包含签名需要的信息,然后镜像设备进行了签名,并把签名结果返回给iPhone,如果iPhone验证了签名成功,则把再次签名的结果发送给镜像设备来验证,如果镜像设备验证成功,说明双方都得到了对方身份已确认,稍微详细点的信息可以看散列与加密算法的几处实际应用场景中的
场景4:AirPlay协议
POST /pair-verify RTSP/1.0
X-Apple-PD: 1
X-Apple-AbsoluteTime: 721822232
Content-Type: application/octet-stream
CSeq: 2
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 68
RTSP/1.0 200 OK
CSeq: 2
content-length: 96
4.POST | /pair-verify
POST /pair-verify RTSP/1.0
X-Apple-PD: 1
X-Apple-AbsoluteTime: 721822233
Content-Type: application/octet-stream
CSeq: 3
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 68
RTSP/1.0 200 OK
CSeq: 3
content-length: 0
5.POST | /fp-setup
两次请求,body部分都带有数据,分别调用fairplay函数的setup和handshake,返回这两个函数的返回值即可
POST /fp-setup RTSP/1.0
X-Apple-ET: 32
Content-Type: application/octet-stream
CSeq: 4
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 16
RTSP/1.0 200 OK
CSeq: 5
content-length: 32
6.SETUP | rtsp://172.16.0.105/13682783630232207885
iPhone请求第二次,iPhone发给镜像设备key,该key经过步骤5和6初始化之后的fairplay解码成一个aes加密算的aeskey,未来传输的视频编码会用aeskey可以来加密,镜像设备准备好事件反馈端口和时间对齐端口发送给iPhone
SETUP rtsp://172.16.0.105/14027482186540797578 RTSP/1.0
Content-Type: application/x-apple-binary-plist
CSeq: 6
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 656
RTSP/1.0 200 OK
CSeq: 6
content-length: 0
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>timingPort</key>
<integer>6000</integer>
<key>eventPort</key>
<integer>47010</integer>
<key>streams</key>
<array>
<dict>
<key>type</key>
<integer>96</integer>
<key>controlPort</key>
<integer>6001</integer>
<key>dataPort</key>
<integer>6003</integer>
</dict>
</array>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>et</key>
<integer>32</integer>
<key>eiv</key>
<data>
aAW66U8aj8bSvSFEJYVr1w==
</data>
<key>timingProtocol</key>
<string>NTP</string>
<key>sessionUUID</key>
<string>F2F647DB-1E87-4AA6-B2AF-AA6CE0BF9D3F</string>
<key>osName</key>
<string>iPhone OS</string>
<key>osBuildVersion</key>
<string>17F80</string>
<key>sourceVersion</key>
<string>420.45</string>
<key>timingPort</key>
<integer>53648</integer>
<key>isScreenMirroringSession</key>
<true/>
<key>osVersion</key>
<string>13.5.1</string>
<key>ekey</key>
<data>
RlBMWQECAQAAAAA8AAAAACbSNQTFG57dB11iWsNtMj8AAAAQvJ5sT/YIec4lRLGGRXsi
HZ0UBENC+6P6Vco8NOvFLRsXN1Qi
</data>
<key>deviceID</key>
<string>F8:95:EA:78:14:F0</string>
<key>model</key>
<string>iPhone10,3</string>
<key>name</key>
<string>姝︽眽鐨勬祴璇曟満iPhoneX 2</string>
<key>macAddress</key>
<string>F8:95:EA:83:87:59</string>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>timingPort</key>
<integer>0</integer>
<key>eventPort</key>
<integer>46001</integer>
<key>streams</key>
<array>
<dict>
<key>type</key>
<integer>110</integer>
<key>dataPort</key>
<integer>46000</integer>
</dict>
</array>
</dict>
</plist>
7.GET_PARAMETER | rtsp://172.16.0.105/13682783630232207885
iPHone查询镜像设备的一些信息,目前在body播放只有如下内容
"volume\r\n"
, 镜像设备可以返回给iPhone的body部分为"volume:0.0\r\n"
GET_PARAMETER rtsp://172.16.0.105/14027482186540797578 RTSP/1.0
Content-Type: text/parameters
CSeq: 8
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 8
RTSP/1.0 200 OK
CSeq: 8
content-length: 18
8.RECORD | rtsp://172.16.0.105/13682783630232207885
iPhone发送Record命令,镜像设备返回的RTSP Header中增加一条记录,body为空
response.AddHeader("Audio-Latency", request.GetHeader("2205"));
RECORD rtsp://172.16.0.105/14027482186540797578 RTSP/1.0
CSeq: 9
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 0
RTSP/1.0 200 OK
CSeq: 9
Audio-Latency: 11025
Audio-Jack-Status: connected; type=analog
content-length: 0
9.SETUP | rtsp://172.16.0.105/13682783630232207885
SETUP rtsp://172.16.0.105/14027482186540797578 RTSP/1.0
Content-Type: application/x-apple-binary-plist
CSeq: 10
DACP-ID: DBA1F21D1459CFDD
Active-Remote: 1345566021
User-Agent: AirPlay/665.13.1
content-length: 204
RTSP/1.0 200 OK
CSeq: 6
content-length: 0
10.SET_PARAMETER | rtsp://172.16.0.105/13682783630232207885
iPhone发送音量或者进度条信息,可以不用处理,返回RTSP 200
四.建立会话连接后,会向手机端会询问心跳消息(POST /feedback)回复2秒一次
iPhone会不间断发送/feedback,里面包含时间信息,可以不用处理,返回RTSP 200
这时,在步骤11中准备的新tcp服务器,可以开始收到经过步骤7中的提供的秘钥进行aes加密的视频数据了。
音频数据依然会通过roap.tcp.local来传输,所以现在收到的数据不包含音频数据。
RTSP/1.0 200 OK
CSeq: 6
content-length: 0
五.电视机通过socket 7000端口接收视频流
六.通过解析裸流当中的类型区分开音频和视频流
音频数据依然会通过roap.tcp.local来传输
七.关于加密
关键代码
InputStream request 请求体输入流
OutputStream response 响应体输出流
class FairPlay {
// private static final Logger log = LoggerFactory.getLogger(FairPlay.class);
private final OmgHax omgHax = new OmgHax();
private final byte[] keyMsg = new byte[164];
void fairPlaySetup(InputStream request, OutputStream response) throws IOException {
// byte[] data = request.readAllBytes();
byte[] data = readInputStream(request);
if (data[4] != 3) {
// log.error("FairPlay version {} is not supported!", data[4]);
return;
}
if (data.length == 16) {
int mode = data[14];
byte[][] replyMessage = {
{70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 0, 15, -97, 63, -98, 10, 37, 33, -37, -33, 49, 42, -78, -65, -78, -98, -115, 35, 43, 99, 118, -88, -56, 24, 112, 29, 34, -82, -109, -40, 39, 55, -2, -81, -99, -76, -3, -12, 28, 45, -70, -99, 31, 73, -54, -86, -65, 101, -111, -84, 31, 123, -58, -9, -32, 102, 61, 33, -81, -32, 21, 101, -107, 62, -85, -127, -12, 24, -50, -19, 9, 90, -37, 124, 61, 14, 37, 73, 9, -89, -104, 49, -44, -100, 57, -126, -105, 52, 52, -6, -53, 66, -58, 58, 28, -39, 17, -90, -2, -108, 26, -118, 109, 74, 116, 59, 70, -61, -89, 100, -98, 68, -57, -119, 85, -28, -99, -127, 85, 0, -107, 73, -60, -30, -9, -93, -10, -43, -70},
{70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 1, -49, 50, -94, 87, 20, -78, 82, 79, -118, -96, -83, 122, -15, 100, -29, 123, -49, 68, 36, -30, 0, 4, 126, -4, 10, -42, 122, -4, -39, 93, -19, 28, 39, 48, -69, 89, 27, -106, 46, -42, 58, -100, 77, -19, -120, -70, -113, -57, -115, -26, 77, -111, -52, -3, 92, 123, 86, -38, -120, -29, 31, 92, -50, -81, -57, 67, 25, -107, -96, 22, 101, -91, 78, 25, 57, -46, 91, -108, -37, 100, -71, -28, 93, -115, 6, 62, 30, 106, -16, 126, -106, 86, 22, 43, 14, -6, 64, 66, 117, -22, 90, 68, -39, 89, 28, 114, 86, -71, -5, -26, 81, 56, -104, -72, 2, 39, 114, 25, -120, 87, 22, 80, -108, 42, -39, 70, 104, -118},
{70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 2, -63, 105, -93, 82, -18, -19, 53, -79, -116, -35, -100, 88, -42, 79, 22, -63, 81, -102, -119, -21, 83, 23, -67, 13, 67, 54, -51, 104, -10, 56, -1, -99, 1, 106, 91, 82, -73, -6, -110, 22, -78, -74, 84, -126, -57, -124, 68, 17, -127, 33, -94, -57, -2, -40, 61, -73, 17, -98, -111, -126, -86, -41, -47, -116, 112, 99, -30, -92, 87, 85, 89, 16, -81, -98, 14, -4, 118, 52, 125, 22, 64, 67, -128, 127, 88, 30, -28, -5, -28, 44, -87, -34, -36, 27, 94, -78, -93, -86, 61, 46, -51, 89, -25, -18, -25, 11, 54, 41, -14, 42, -3, 22, 29, -121, 115, 83, -35, -71, -102, -36, -114, 7, 0, 110, 86, -8, 80, -50},
{70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 3, -112, 1, -31, 114, 126, 15, 87, -7, -11, -120, 13, -79, 4, -90, 37, 122, 35, -11, -49, -1, 26, -69, -31, -23, 48, 69, 37, 26, -5, -105, -21, -97, -64, 1, 30, -66, 15, 58, -127, -33, 91, 105, 29, 118, -84, -78, -9, -91, -57, 8, -29, -45, 40, -11, 107, -77, -99, -67, -27, -14, -100, -118, 23, -12, -127, 72, 126, 58, -24, 99, -58, 120, 50, 84, 34, -26, -9, -114, 22, 109, 24, -86, 127, -42, 54, 37, -117, -50, 40, 114, 111, 102, 31, 115, -120, -109, -50, 68, 49, 30, 75, -26, -64, 83, 81, -109, -27, -17, 114, -24, 104, 98, 51, 114, -100, 34, 125, -126, 12, -103, -108, 69, -40, -110, 70, -56, -61, 89}};
// Log.e("waylon1117",replyMessage[mode].toString());
response.write(replyMessage[mode]);
} else if (data.length == 164) {
System.arraycopy(data, 0, keyMsg, 0, 164);
byte[] fpHeader = {70, 80, 76, 89, 3, 1, 4, 0, 0, 0, 0, 20};
response.write(fpHeader);
response.write(data, 144, 20);
}
}
public byte[] readInputStream(InputStream inputStream) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
try {
while ((length = inputStream.read(buffer)) != -1) {
outStream.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
return outStream.toByteArray();
}
byte[] decryptAesKey(byte[] key) {
byte[] aesKey = new byte[16];
omgHax.decryptAesKey(keyMsg, key, aesKey);
// log.info("FairPlay AES key decrypted: " + Utils.bytesToHex(aesKey));
return aesKey;
}
}
文章参考
:【精选】实时流协议---RTSP【详解】_贺二公子的博客-CSDN博客
:Unofficial AirPlay Protocol Specification
:nodejs:GitHub - marcklefter/node-appletv-pairing: Apple TV device authentication in Node
:nodejs库:详解 npm airplayer库 - 知乎