在上一篇,我们让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 parametersfor(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 服务结构核心部分。

  • control port 是用来发送 resendTransmitRequest 的 channel,也就是当 Android 这边发现我收到的音乐流数据包中有丢失帧的时候,可以通过 control port 发送 resendTransmit 的 request 给 iOS 设备,设备收到后会将帧在 response 中补发回来。
  • timing port 用来传输 Airplay 的时间同步包,同时也可以主动向 iOS 设备请求当前的时间戳来校准流的时间戳。
  • server port 则是用来传输最主要的音乐流数据包。
  • 对于这三个端口,我们同样建立了netty server和 pipelinefactory

协议解析:对指定几个 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 协议中,总共有如下几种

  • Packet Type:
    TimingRequest [timing channel]
    TimingResponse [timing channel]
    Sync [timing channel]
    RetransmitRequest [control channel]
    AudioRetransmit [audio channel]
    AudioTransmit [audio channel]

  • timing channel 在 Sync 数据的同事,开启单独的线程每三秒钟执行一次 timing request,来确认本地时钟和客户端时钟的同步。control channel 每收到一个 新的 audio 数据包的时候都会 确认一次数据包的 sequence number 是否和当前的是连续的 ,如果不连续的,则将中间缺失的 number 标记为 missing 的数据包,并且向客户端发送一个 resend 的请求。当客户端发来了 AudioRetransmit 类型的数据包后,由 audio channel 接收的,control channel 只是负责将刚才标记为 missing 的 sequence number 清除掉。

  • 这两个 channel 在发送 request 的时候,也会发回到 audio channel 的 Handler 上来,通过 audio channel 这边的 encode 之后再发送出去。

  • 而音乐数据包,则需要经过 AES 解密,这个解密器我们已经在 ANNOUNCE 的时候初始化好了,再经过 ALACDecoder,也是在 ANNOUNCE 的时候根据获得的媒体信息初始化的音频解码器,最后在 EnqueueHandler 中决定是否进入音频输出队列。

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

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

更多相关文章

  1. Android USB Gadget复合设备驱动(打印机)测试方法
  2. 使用User Agent分辨出Android设备类型的安全做法
  3. [置顶] android eoe客户端
  4. Ionic2 在Android设备上的部署
  5. 通过JavaScript或PHP检测Android设备的代码
  6. 一个 Android 简易的新闻客户端
  7. Android error: adb 端口被占用 (adb.exe,start-server' failed
  8. Android基于XMPP Smack Openfire开发IM【三】客户端接收服务器发
  9. android 获取设备硬件信息

随机推荐

  1. Android 控制软键盘的显示与隐藏
  2. android 从系统相册获取一张图片
  3. android actionbar tab style
  4. Android ImageView.ScaleType说明
  5. android中view组件使用详解
  6. Android Base64Encoder解决方案
  7. Android studio自定义变量
  8. Android: min3D study
  9. android 3D gallery 并 判断当前选中项
  10. Android Seek自定义样式