ios开发进阶Android开发经验谈Android进阶之路

iOS Airplay--Airtunes音乐播放在Androi

2017-07-04  本文已影响550人  RDuwan

在上一篇,我们让iOS设备通过AirTunes连接上了Android设备链接
这一篇,我们将完成iOS设备通过AirTunes把音乐推给Android设播放。

四、实现Android设备播放AirTunes音乐

- 1 对RaopRtsPipelineFactory的pipeline 构造完整的handler处理,新增了一个最核心的handler--RaopAudioHandler
public class RaopRtsPipelineFactory implements ChannelPipelineFactory {
    @Override
    public ChannelPipeline getPipeline() throws Exception {

        final ChannelPipeline pipeline = Channels.pipeline();
        //因为是管道 注意保持正确的顺序

        //构造executionHanlder 和关闭executionHanlder
        final AirTunesRunnable airTunesRunnable = AirTunesRunnable.getInstance();
        pipeline.addLast("exectionHandler", airTunesRunnable.getChannelExecutionHandler());
        pipeline.addLast("closeOnShutdownHandler", new SimpleChannelUpstreamHandler(){
            @Override
            public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
                airTunesRunnable.getChannelGroup().add(e.getChannel());
                super.channelOpen(ctx, e);
            }
        });

        //add exception logger
        pipeline.addLast("exceptionLogger", new ExceptionLoggingHandler());

        //rtsp decoder & encoder
        pipeline.addLast("decoder", new RtspRequestDecoder());
        pipeline.addLast("encoder", new RtspResponseEncoder());

        //rstp logger and errer response
        pipeline.addLast("logger", new RtspLoggingHandler());
        pipeline.addLast("errorResponse", new RtspErrorResponseHandler());

        //app airtunes need
        pipeline.addLast("challengeResponse", new RaopRtspChallengeResponseHandler(NetworkUtils.getInstance().getHardwareAddress()));
        pipeline.addLast("header", new RaopRtspHeaderHandler());
        //let iOS devices know server support methods
        pipeline.addLast("options", new RaopRtspOptionsHandler());

        //!!!Core handler audioHandler
        pipeline.addLast("audio", new RaopAudioHandler(airTunesRunnable.getExecutorService()));

        //unsupport Response
        pipeline.addLast("unsupportedResponse", new RtspUnsupportedResponseHandler());


        return pipeline;
    }
}
- 2 RaopAudioHandler的处理流程:ANNOUNCE(标识链接,更新客户端session),SETUP(构造连接),RECORD(记录保存媒体数据),FLUSH(当airtunes中断时,清空里面的数据),TEARDOWN(关闭连接)。
@Override
    public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent evt) throws Exception {
        final HttpRequest req = (HttpRequest)evt.getMessage();
        final HttpMethod method = req.getMethod();

        LOG.info("messageReceived : HttpMethod: " + method);
        
        if (RaopRtspMethods.ANNOUNCE.equals(method)) {
            announceReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.SETUP.equals(method)) {
            setupReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.RECORD.equals(method)) {
            recordReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.FLUSH.equals(method)) {
            flushReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.TEARDOWN.equals(method)) {
            teardownReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.SET_PARAMETER.equals(method)) {
            setParameterReceived(ctx, req);
            return;
        }
        else if (RaopRtspMethods.GET_PARAMETER.equals(method)) {
            getParameterReceived(ctx, req);
            return;
        }

        super.messageReceived(ctx, evt);
    }

A. AUNOUNCE处理。announce在传输的时候遵循了SDP协议。SDP协议用来描述媒体信息。AirTunes协议的样式如下:

/**
         * Sample sdp content:
         * 
            v=0
            o=iTunes 3413821438 0 IN IP4 fe80::217:f2ff:fe0f:e0f6
            s=iTunes
            c=IN IP4 fe80::5a55:caff:fe1a:e187
            t=0 0
            m=audio 0 RTP/AVP 96
            a=rtpmap:96 AppleLossless
            a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
            a=fpaeskey:RlBMWQECAQAAAAA8AAAAAPFOnNe+zWb5/n4L5KZkE2AAAAAQlDx69reTdwHF9LaNmhiRURTAbcL4brYAceAkZ49YirXm62N4
            a=aesiv:5b+YZi9Ikb845BmNhaVo+Q
         */

对协议进行解析:

//go through each line and parse the sdp parameters
for(final String line: sdp.split("\n")) {
    /* Split SDP line into attribute and setting */
    final Matcher lineMatcher = s_pattern_sdp_line.matcher(line);

    if ( ! lineMatcher.matches()){
        throw new ProtocolException("Cannot parse SDP line " + line);
    }

    final char attribute = lineMatcher.group(1).charAt(0);
    final String setting = lineMatcher.group(2);

    /* Handle attributes */
    switch (attribute) {
        case 'm':
            /* Attribute m. Maps an audio format index to a stream */
            final Matcher m_matcher = s_pattern_sdp_m.matcher(setting);
            if (!m_matcher.matches())
                throw new ProtocolException("Cannot parse SDP " + attribute + "'s setting " + setting);
            audioFormatIndex = Integer.valueOf(m_matcher.group(2));
            break;

        case 'a':
            LOG.info("setting: " + setting);

            /* Attribute a. Defines various session properties */
            final Matcher a_matcher = s_pattern_sdp_a.matcher(setting);

            if ( ! a_matcher.matches() ){
                throw new ProtocolException("Cannot parse SDP " + attribute + "'s setting " + setting);
            }

            final String key = a_matcher.group(1);
            final String value = a_matcher.group(2);

            if ("rtpmap".equals(key)) {
                /* Sets the decoder for an audio format index */
                final Matcher a_rtpmap_matcher = s_pattern_sdp_a_rtpmap.matcher(value);
                if (!a_rtpmap_matcher.matches())
                    throw new ProtocolException("Cannot parse SDP " + attribute + "'s rtpmap entry " + value);

                final int formatIdx = Integer.valueOf(a_rtpmap_matcher.group(1));
                final String format = a_rtpmap_matcher.group(2);
                if ("AppleLossless".equals(format))
                    alacFormatIndex = formatIdx;
            }
            else if ("fmtp".equals(key)) {
                /* Sets the decoding parameters for a audio format index */
                final String[] parts = value.split(" ");
                if (parts.length > 0)
                    descriptionFormatIndex = Integer.valueOf(parts[0]);
                if (parts.length > 1)
                    formatOptions = Arrays.copyOfRange(parts, 1, parts.length);
            }
            else if ("rsaaeskey".equals(key)) {
                /* Sets the AES key required to decrypt the audio data. The key is
                 * encrypted wih the AirTunes private key
                 */
                byte[] aesKeyRaw;

                rsaPkCS1OaepCipher.init(Cipher.DECRYPT_MODE, AirTunesCryptography.PrivateKey);
                aesKeyRaw = rsaPkCS1OaepCipher.doFinal(Base64.decodeUnpadded(value));

                aesKey = new SecretKeySpec(aesKeyRaw, "AES");
            }
            else if ("aesiv".equals(key)) {
                /* Sets the AES initialization vector */
                aesIv = new IvParameterSpec(Base64.decodeUnpadded(value));
            }
            break;

        default:
            /* Ignore */
            break;
    }
}

*通过AES 解密的 秘钥 和 初始化矩阵IV 以及流的数据格式,从而初始化 ALAC Decoder *

B. SETUP处理。 SETUP就是iOS设备和我们信息交换:主要是三个 port 的信息,对应三个 channel。分别是 control port -> control channel , timing port -> timing channel 和 server port -> audio channel ,这是三个 UDP 连接 的端口。这也是整个 Airtunes 服务结构核心部分。

协议解析:对指定几个 key 进行 response ,其中 interleaved 和 mode 返回的是固定参数, control_port 和 timing_port 在 request 中所对应的 value 是客户端的端口,而 response 中需要带上服务端的端口。同时,这两个 UDP 连接由服务端发起去连接客户端对应的端口。最后再告知客户端 server_port 的端口。

for(final String requestOption: requestOptions) {
    /* Split option into key and value */
    final Matcher transportOption = PATTERN_TRANSPORT_OPTION.matcher(requestOption);
    if ( ! transportOption.matches() ){
        throw new ProtocolException("Cannot parse Transport option " + requestOption);
    }
    final String key = transportOption.group(1);
    final String value = transportOption.group(3);

    if ("interleaved".equals(key)) {
        /* Probably means that two channels are interleaved in the stream. Included in the response options */
        if ( ! "0-1".equals(value)){
            throw new ProtocolException("Unsupported Transport option, interleaved must be 0-1 but was " + value);
        }
        responseOptions.add("interleaved=0-1");
    }
    else if ("mode".equals(key)) {
        /* Means the we're supposed to receive audio data, not send it. Included in the response options */
        if ( ! "record".equals(value)){
            throw new ProtocolException("Unsupported Transport option, mode must be record but was " + value);
        }
        responseOptions.add("mode=record");
    }
    else if ("control_port".equals(key)) {
        /* Port number of the client's control socket. Response includes port number of *our* control port */
        final int clientControlPort = Integer.valueOf(value);

        controlChannel = createRtpChannel(
            substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 53670),
            substitutePort((InetSocketAddress)ctx.getChannel().getRemoteAddress(), clientControlPort),
            RaopRtpChannelType.Control
        );

        LOG.info("Launched RTP control service on " + controlChannel.getLocalAddress());

        responseOptions.add("control_port=" + ((InetSocketAddress)controlChannel.getLocalAddress()).getPort());
    }
    else if ("timing_port".equals(key)) {
        /* Port number of the client's timing socket. Response includes port number of *our* timing port */
        final int clientTimingPort = Integer.valueOf(value);

        timingChannel = createRtpChannel(
            substitutePort((InetSocketAddress)ctx.getChannel().getLocalAddress(), 53669),
            substitutePort((InetSocketAddress)ctx.getChannel().getRemoteAddress(), clientTimingPort),
            RaopRtpChannelType.Timing
        );

        LOG.info("Launched RTP timing service on " + timingChannel.getLocalAddress());

        responseOptions.add("timing_port=" + ((InetSocketAddress)timingChannel.getLocalAddress()).getPort());
    }
    else {
        /* Ignore unknown options */
        responseOptions.add(requestOption);
    }
}
- 3 在setup执行后,整个Airtunes的通信图示**

(1)UpStream:数据进入 pipeline 之后,按照 RTP Packet 的格式进行 decode。在 Airplay 协议中,总共有如下几种

(2)Down Stream: timing channel 和 control channel channel 负责向客户端发送具体的请求。

- 4 运行工程到Android设备上,在iOS通过AirTunes找到"RDuwan-Airtunes",连接上设备,打开iOS上的音乐软件(比如QQ音乐),即可以在Android设备上成功听到了音乐的播放。
- 5 完整工程见github链接
上一篇 下一篇

猜你喜欢

热点阅读