airplay:实现

一.电视机向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"?>


    
        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
    

 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




	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
		
	

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博客

:AirPlay 镜像协议-上(发现) - 简书

:airplay:实现-CSDN博客

:Unofficial AirPlay Protocol Specification

:nodejs:GitHub - marcklefter/node-appletv-pairing: Apple TV device authentication in Node

:nodejs库:详解 npm airplayer库 - 知乎 

你可能感兴趣的:(网络,linux,运维)