在上一篇,我们让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 服务结构核心部分。
- 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 负责向客户端发送具体的请求。