在上一篇辛慰,我們讓iOS設(shè)備通過(guò)AirTunes連接上了Android設(shè)備鏈接。
這一篇,我們將完成iOS設(shè)備通過(guò)AirTunes把音樂(lè)推給Android設(shè)播放瓷翻。
四、實(shí)現(xiàn)Android設(shè)備播放AirTunes音樂(lè)
- 1 對(duì)RaopRtsPipelineFactory的pipeline 構(gòu)造完整的handler處理吐咳,新增了一個(gè)最核心的handler--RaopAudioHandler逻悠。
public class RaopRtsPipelineFactory implements ChannelPipelineFactory {
@Override
public ChannelPipeline getPipeline() throws Exception {
final ChannelPipeline pipeline = Channels.pipeline();
//因?yàn)槭枪艿?注意保持正確的順序
//構(gòu)造executionHanlder 和關(guān)閉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(標(biāo)識(shí)鏈接,更新客戶端session)韭脊,SETUP(構(gòu)造連接)童谒,RECORD(記錄保存媒體數(shù)據(jù)),F(xiàn)LUSH(當(dāng)airtunes中斷時(shí)沪羔,清空里面的數(shù)據(jù))饥伊,TEARDOWN(關(guān)閉連接)。
@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在傳輸?shù)臅r(shí)候遵循了SDP協(xié)議琅豆。SDP協(xié)議用來(lái)描述媒體信息。AirTunes協(xié)議的樣式如下:
/**
* 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
*/
對(duì)協(xié)議進(jìn)行解析:
//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;
}
}
*通過(guò)AES 解密的 秘鑰 和 初始化矩陣IV 以及流的數(shù)據(jù)格式篓吁,從而初始化 ALAC Decoder *
B. SETUP處理茫因。 SETUP就是iOS設(shè)備和我們信息交換:主要是三個(gè) port 的信息,對(duì)應(yīng)三個(gè) channel杖剪。分別是 control port -> control channel 冻押, timing port -> timing channel 和 server port -> audio channel 驰贷,這是三個(gè) UDP 連接 的端口。這也是整個(gè) Airtunes 服務(wù)結(jié)構(gòu)核心部分洛巢。
- control port 是用來(lái)發(fā)送 resendTransmitRequest 的 channel括袒,也就是當(dāng) Android 這邊發(fā)現(xiàn)我收到的音樂(lè)流數(shù)據(jù)包中有丟失幀的時(shí)候,可以通過(guò) control port 發(fā)送 resendTransmit 的 request 給 iOS 設(shè)備稿茉,設(shè)備收到后會(huì)將幀在 response 中補(bǔ)發(fā)回來(lái)锹锰。
- timing port 用來(lái)傳輸 Airplay 的時(shí)間同步包,同時(shí)也可以主動(dòng)向 iOS 設(shè)備請(qǐng)求當(dāng)前的時(shí)間戳來(lái)校準(zhǔn)流的時(shí)間戳漓库。
- server port 則是用來(lái)傳輸最主要的音樂(lè)流數(shù)據(jù)包恃慧。
- 對(duì)于這三個(gè)端口,我們同樣建立了netty server和 pipelinefactory
協(xié)議解析:對(duì)指定幾個(gè) key 進(jìn)行 response 米苹,其中 interleaved 和 mode 返回的是固定參數(shù)糕伐, control_port 和 timing_port 在 request 中所對(duì)應(yīng)的 value 是客戶端的端口,而 response 中需要帶上服務(wù)端的端口蘸嘶。同時(shí)良瞧,這兩個(gè) UDP 連接由服務(wù)端發(fā)起去連接客戶端對(duì)應(yīng)的端口。最后再告知客戶端 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執(zhí)行后褥蚯,整個(gè)Airtunes的通信圖示**
(1)UpStream:數(shù)據(jù)進(jìn)入 pipeline 之后,按照 RTP Packet 的格式進(jìn)行 decode况增。在 Airplay 協(xié)議中赞庶,總共有如下幾種
Packet Type:
TimingRequest [timing channel]
TimingResponse [timing channel]
Sync [timing channel]
RetransmitRequest [control channel]
AudioRetransmit [audio channel]
AudioTransmit [audio channel]timing channel 在 Sync 數(shù)據(jù)的同事,開(kāi)啟單獨(dú)的線程每三秒鐘執(zhí)行一次 timing request澳骤,來(lái)確認(rèn)本地時(shí)鐘和客戶端時(shí)鐘的同步歧强。control channel 每收到一個(gè) 新的 audio 數(shù)據(jù)包的時(shí)候都會(huì) 確認(rèn)一次數(shù)據(jù)包的 sequence number 是否和當(dāng)前的是連續(xù)的 ,如果不連續(xù)的为肮,則將中間缺失的 number 標(biāo)記為 missing 的數(shù)據(jù)包摊册,并且向客戶端發(fā)送一個(gè) resend 的請(qǐng)求。當(dāng)客戶端發(fā)來(lái)了 AudioRetransmit 類型的數(shù)據(jù)包后颊艳,由 audio channel 接收的茅特,control channel 只是負(fù)責(zé)將剛才標(biāo)記為 missing 的 sequence number 清除掉。
這兩個(gè) channel 在發(fā)送 request 的時(shí)候棋枕,也會(huì)發(fā)回到 audio channel 的 Handler 上來(lái)白修,通過(guò) audio channel 這邊的 encode 之后再發(fā)送出去。
而音樂(lè)數(shù)據(jù)包重斑,則需要經(jīng)過(guò) AES 解密兵睛,這個(gè)解密器我們已經(jīng)在 ANNOUNCE 的時(shí)候初始化好了,再經(jīng)過(guò) ALACDecoder,也是在 ANNOUNCE 的時(shí)候根據(jù)獲得的媒體信息初始化的音頻解碼器祖很,最后在 EnqueueHandler 中決定是否進(jìn)入音頻輸出隊(duì)列累盗。
(2)Down Stream: timing channel 和 control channel channel 負(fù)責(zé)向客戶端發(fā)送具體的請(qǐng)求。