在上一篇,我們讓iOS設備通過AirTunes發現了Android設備鏈接。
這一篇,我們將完成iOS設備通過AirTunes連接上Android設備。
三、實現iOS設備通過AirTunes連接上Android devices
- 1 使用netty構造一個server,設定基礎配置。
final ServerBootstrap airTunesBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(executorService, executorService));
airTunesBootstrap.setOption("reuseAddress", true); //端口重用
airTunesBootstrap.setOption("child.tcpNoDelay", true);
airTunesBootstrap.setOption("child.keepAlive", true); //保持連接
- 2 給ServerBootstrap自定義PiplineFactory,并且建立5個Handler。
添加到server配置
airTunesBootstrap.setPipelineFactory(new RaopRtsPipelineFactory());
try {channelGroup.add(airTunesBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), getRtspPort())));
}
catch (Exception e) {
LOG.log(Level.SEVERE, "error",e);
try {channelGroup.add(airTunesBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), getAnotherRtspPort())));
}
catch (Exception e1) {
LOG.log(Level.SEVERE, "error",e1);
}
}
給PiplineFactory添加rtsp decode和encode的handler,并添加Airtunes連接需要的另外三個核心handler。
public class RaopRtsPipelineFactory implements ChannelPipelineFactory {
@Override
public ChannelPipeline getPipeline() throws Exception {
final ChannelPipeline pipeline = Channels.pipeline();
//因為是管道 注意保持正確的順序
pipeline.addLast("decoder", new RtspRequestDecoder());//RtspRequestDecoder
pipeline.addLast("encoder", new RtspResponseEncoder());//RtspResponseEncoder
pipeline.addLast("challengeResponse", new RaopRtspChallengeResponseHandler(NetworkUtils.getInstance().getHardwareAddress()));
pipeline.addLast("header", new RaopRtspHeaderHandler());
pipeline.addLast("options", new RaopRtspOptionsHandler());
return pipeline;
}
}
A. RaopRtspChallengeResponseHandler詳解:
- messageReceived:蘋果設備向Android設備中發送的Request的Header中包含一個叫"Apple-Challenge"的字段。這個字段需要通過base64的解密獲取憑證,該憑證會在回復信息給蘋果設備時候使用。*
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt)
throws Exception
{
final HttpRequest req = (HttpRequest)evt.getMessage();
synchronized(this) {
if (req.containsHeader(HeaderChallenge)) {
/* The challenge is sent without padding! */
final byte[] challenge = Base64.decodeUnpadded(req.getHeader(HeaderChallenge));
/* Verify that we got 16 bytes */
if (challenge.length != 16)
throw new ProtocolException("Invalid Apple-Challenge header, " + challenge.length + " instead of 16 bytes");
/* Remember challenge and local address.
* Both are required to compute the response
*/
m_challenge = challenge;
m_localAddress = ((InetSocketAddress)ctx.getChannel().getLocalAddress()).getAddress();
}
else {
/* Forget last challenge */
m_challenge = null;
m_localAddress = null;
}
}
super.messageReceived(ctx, evt);
}
- writeRequested:在回復給蘋果設備時,需要構造"Apple-Response"的key,其值為challengeResponse解密后16位憑證 + 16位的ipv6 address + 6位的網絡硬件地址。這個里面的加密所使用的rsa的privtekey,來自于國外大神的破譯。
@Override
public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent evt)
throws Exception
{
final HttpResponse resp = (HttpResponse)evt.getMessage();
synchronized(this) {
if (m_challenge != null) {
try {
/* Get appropriate response to challenge and
* add to the response base-64 encoded. XXX
*/
final String sig = Base64.encodePadded(getSignature());
resp.setHeader(HeaderSignature, sig);
}
finally {
/* Forget last challenge */
m_challenge = null;
m_localAddress = null;
}
}
}
super.writeRequested(ctx, evt);
}
B. RaopRtspHeaderHandler詳解:
- 對于Rtsp Header來說,每一個都包含一個CSeq的頭,在request和response中保持一致。每個response的 RTSP Header 還要帶上一個值為 connected; type=analog 的頭 Audio-Jack-Status。
C. RaopRtspOptionsHandler詳解:
- 在iOS設備發起請求后,我們需要相應rtsp的option請求,以告知我們支持哪些類型的請求。
public class RaopRtspOptionsHandler extends SimpleChannelUpstreamHandler {
private static final String Options =
RaopRtspMethods.ANNOUNCE.getName() + ", " +
RaopRtspMethods.SETUP.getName() + ", " +
RaopRtspMethods.RECORD.getName() + ", " +
RaopRtspMethods.PAUSE.getName() + ", " +
RaopRtspMethods.FLUSH.getName() + ", " +
RtspMethods.TEARDOWN.getName() + ", " +
RaopRtspMethods.OPTIONS.getName() + ", " +
RaopRtspMethods.GET_PARAMETER.getName() + ", " +
RaopRtspMethods.SET_PARAMETER.getName();
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt) throws Exception {
final HttpRequest req = (HttpRequest)evt.getMessage();
if (RtspMethods.OPTIONS.equals(req.getMethod())) {
final HttpResponse response = new DefaultHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
response.setHeader(RtspHeaders.Names.PUBLIC, Options);
ctx.getChannel().write(response);
}
else {
super.messageReceived(ctx, evt);
}
}
}
3 運行第二部代碼,代碼見github鏈接
-
4 在iOS設備上通過AirTunes看到"RDuwan-Airtunes",點擊就可以成功連接上。如圖: