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
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
RTSP/1.0 200 OK
CSeq: 0
content-length: 689
?xml version="1.0" encoding="UTF-8"?>
audioFormats
audioInputFormats
67108860
audioOutputFormats
67108860
type
100
audioInputFormats
67108860
audioOutputFormats
67108860
type
101
audioLatencies
audioType
default
inputLatencyMicros
type
100
audioType
default
inputLatencyMicros
type
101
displays
features
14
height
1080
heightPhysical
heightPixels
1080
maxFPS
30
overscanned
refreshRate
60
rotation
uuid
e5f7a68d-7b0f-4305-984b-974f677a150b
width
1920
widthPhysical
widthPixels
1920
features
130367356919
keepAliveSendStatsAsBody
1
model
AppleTV2,1
name
Apple TV
pi
b08f5a79-db29-4384-b456-a4784d9e6055
sourceVersion
220.68
statusFlags
68
vv
2
该请求,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
这两次请求是非常关键的,首先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
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
两次请求,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
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
timingPort
6000
eventPort
47010
streams
type
96
controlPort
6001
dataPort
6003
et
32
eiv
aAW66U8aj8bSvSFEJYVr1w==
timingProtocol
NTP
sessionUUID
F2F647DB-1E87-4AA6-B2AF-AA6CE0BF9D3F
osName
iPhone OS
osBuildVersion
17F80
sourceVersion
420.45
timingPort
53648
isScreenMirroringSession
osVersion
13.5.1
ekey
RlBMWQECAQAAAAA8AAAAACbSNQTFG57dB11iWsNtMj8AAAAQvJ5sT/YIec4lRLGGRXsi
HZ0UBENC+6P6Vco8NOvFLRsXN1Qi
deviceID
F8:95:EA:78:14:F0
model
iPhone10,3
name
姝︽眽鐨勬祴璇曟満iPhoneX 2
macAddress
F8:95:EA:83:87:59
timingPort
0
eventPort
46001
streams
type
110
dataPort
46000
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
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
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
iPhone发送音量或者进度条信息,可以不用处理,返回RTSP 200
iPhone会不间断发送/feedback,里面包含时间信息,可以不用处理,返回RTSP 200
这时,在步骤11中准备的新tcp服务器,可以开始收到经过步骤7中的提供的秘钥进行aes加密的视频数据了。
音频数据依然会通过roap.tcp.local来传输,所以现在收到的数据不包含音频数据。
RTSP/1.0 200 OK
CSeq: 6
content-length: 0
音频数据依然会通过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博客
:AirPlay 镜像协议-上(发现) - 简书
:airplay:实现-CSDN博客
:Unofficial AirPlay Protocol Specification
:nodejs:GitHub - marcklefter/node-appletv-pairing: Apple TV device authentication in Node
:nodejs库:详解 npm airplayer库 - 知乎