iOS Airplay--Airtunes音樂(lè)播放在Android盒子和手機(jī)上的實(shí)現(xiàn) (終結(jié)篇)

在上一篇辛慰,我們讓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)求。

- 4 運(yùn)行工程到Android設(shè)備上突琳,在iOS通過(guò)AirTunes找到"RDuwan-Airtunes",連接上設(shè)備符相,打開(kāi)iOS上的音樂(lè)軟件(比如QQ音樂(lè))拆融,即可以在Android設(shè)備上成功聽(tīng)到了音樂(lè)的播放。
- 5 完整工程見(jiàn)github鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末啊终,一起剝皮案震驚了整個(gè)濱河市镜豹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蓝牲,老刑警劉巖趟脂,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異例衍,居然都是意外死亡昔期,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門佛玄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)硼一,“玉大人,你說(shuō)我怎么就攤上這事梦抢“阍簦” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵奥吩,是天一觀的道長(zhǎng)哼蛆。 經(jīng)常有香客問(wèn)我,道長(zhǎng)霞赫,這世上最難降的妖魔是什么腮介? 我笑而不...
    開(kāi)封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮绩脆,結(jié)果婚禮上萤厅,老公的妹妹穿的比我還像新娘。我一直安慰自己靴迫,他們只是感情好惕味,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著玉锌,像睡著了一般名挥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上主守,一...
    開(kāi)封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天禀倔,我揣著相機(jī)與錄音榄融,去河邊找鬼。 笑死救湖,一個(gè)胖子當(dāng)著我的面吹牛愧杯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鞋既,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼力九,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了邑闺?” 一聲冷哼從身側(cè)響起跌前,我...
    開(kāi)封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陡舅,沒(méi)想到半個(gè)月后抵乓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡靶衍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年灾炭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摊灭。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咆贬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帚呼,到底是詐尸還是另有隱情掏缎,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布煤杀,位于F島的核電站眷蜈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏沈自。R本人自食惡果不足惜酌儒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枯途。 院中可真熱鬧忌怎,春花似錦、人聲如沸酪夷。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)晚岭。三九已至鸥印,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背库说。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工狂鞋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人潜的。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓骚揍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親啰挪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疏咐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容