在上一篇,我们让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. 远程调测:Chrome on Android之三 调测WebView
  3. Android之Bluetooth
  4. 初遇Android——跨进程使用Service
  5. Google Android应用开发03 Android(安卓)SDK介绍
  6. AnDroidDraw.apk的安装
  7. Android系统底层架构【译】
  8. Android(安卓)Binder -什么是binder
  9. Android蓝牙开发

随机推荐

  1. Android(安卓)studio3.6.3NDK环境配置
  2. Android中使用系统桌面背景作为应用背景,
  3. Android——Notification的基本使用
  4. Android(安卓)MediaPlayer源码分析总结
  5. Webview 和js之间安全交互
  6. 【转】Android通过Intent发送电子邮件含
  7. getGlobalVisibleRect和getLocalVisibleR
  8. Android使用百度地图的注意点
  9. Android(安卓)Serivce 高级篇AIDL讲解
  10. Android(安卓)软键盘盖住输入框或者布局