IOS Airplay -- Implementation of Airtunes Music Play on Android Box and Mobile Phone (Part 2)

Keywords: iOS Android Netty network

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:


Posted by smordue on Sun, 16 Jun 2019 15:20:23 -0700