In the last article, we let iOS devices discover Android devices via AirTunes link.
In this article, we will complete iOS devices connected to Android devices via AirTunes.
3. Implementing iOS devices connected to Android devices via AirTunes
- 1. Use netty to construct a server and set the basic configuration.
final ServerBootstrap airTunesBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(executorService, executorService)); airTunesBootstrap.setOption("reuseAddress", true); //Port Reuse airTunesBootstrap.setOption("child.tcpNoDelay", true); airTunesBootstrap.setOption("child.keepAlive", true); //Keep Connection
- 2. Customize PiplineFactory for Server Bootstrap and build 5 Handler s.
Add to server configuration
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); } }
Add rtsp decode and encode handler to PiplineFactory, and add the other three core handlers required for Airtunes connection.
public class RaopRtsPipelineFactory implements ChannelPipelineFactory { @Override public ChannelPipeline getPipeline() throws Exception { final ChannelPipeline pipeline = Channels.pipeline(); //Because it's the pipeline that keeps the right order. 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. Raop Rtsp Challenge Response Handler details:
- Message Received: The Header of Request sent by Apple devices to Android devices contains a field called "Apple-Challenge". This field needs to be decrypted by base64 to obtain the credentials, which will be used when responding to information sent to Apple devices. *
@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); }
- Write Requested: When replying to Apple devices, we need to construct the key of "Apple-Response", which is 16-bit certificate + 16-bit ipv6 address + 6-bit network hardware address after challengeResponse decryption. The rsa privtekey used in this encryption comes from the decoding of foreign gods.
@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 Details:
- For Rtsp Headers, each contains a CSeq header that is consistent in request and response. Each response's RTSP Header also carries a value of connected; type=analog's header Audio-Jack-Status.
C. RaopRtsp Options Handler details:
- After the iOS device initiates the request, we need the corresponding rtsp option request to tell us what types of requests we support.
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 Run the second code, see github link
-
4 On iOS devices, you can see "RDuwan-Airtunes" through AirTunes and click on it to connect successfully. As shown in the figure: