iOS Airplay--Airtunes音樂播放在Android盒子和手機(jī)上的實(shí)現(xiàn) (第二篇)

在上一篇,我們讓iOS設(shè)備通過AirTunes發(fā)現(xiàn)了Android設(shè)備鏈接
這一篇,我們將完成iOS設(shè)備通過AirTunes連接上Android設(shè)備。

三、實(shí)現(xiàn)iOS設(shè)備通過AirTunes連接上Android devices

  • 1 使用netty構(gòu)造一個(gè)server,設(shè)定基礎(chǔ)配置。
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個(gè)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連接需要的另外三個(gè)核心handler。


public class RaopRtsPipelineFactory implements ChannelPipelineFactory {
    @Override
    public ChannelPipeline getPipeline() throws Exception {

        final ChannelPipeline pipeline = Channels.pipeline();
        //因?yàn)槭枪艿?注意保持正確的順序
        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:蘋果設(shè)備向Android設(shè)備中發(fā)送的Request的Header中包含一個(gè)叫"Apple-Challenge"的字段。這個(gè)字段需要通過base64的解密獲取憑證,該憑證會(huì)在回復(fù)信息給蘋果設(shè)備時(shí)候使用。*
@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:在回復(fù)給蘋果設(shè)備時(shí),需要構(gòu)造"Apple-Response"的key,其值為challengeResponse解密后16位憑證 + 16位的ipv6 address + 6位的網(wǎng)絡(luò)硬件地址。這個(gè)里面的加密所使用的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詳解:

  • 對(duì)于Rtsp Header來說,每一個(gè)都包含一個(gè)CSeq的頭,在request和response中保持一致。每個(gè)response的 RTSP Header 還要帶上一個(gè)值為 connected; type=analog 的頭 Audio-Jack-Status。

C. RaopRtspOptionsHandler詳解:

  • 在iOS設(shè)備發(fā)起請求后,我們需要相應(yīng)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 運(yùn)行第二部代碼,代碼見github鏈接

  • 4 在iOS設(shè)備上通過AirTunes看到"RDuwan-Airtunes",點(diǎn)擊就可以成功連接上。如圖:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容