iOS Airplay--Airtunes音樂播放在Android盒子和手機(jī)上的實(shí)現(xiàn) (第一篇)

一、前言

在局域網(wǎng)中實(shí)現(xiàn)流媒體的播放有2種主要方式,Airplay和DLNA。對于iOS系統(tǒng),天生帶了Airplay,但可惜是蘋果秉承一貫的作風(fēng),Airplay是一個閉源協(xié)議。萬幸有大神逆向了協(xié)議內(nèi)容,使得我們可以去搭建服務(wù)完成此項(xiàng)工作。這個協(xié)議的復(fù)雜度還是略高,使得我也不可能一口氣寫出完整的文章,咱們就一篇篇的道來。

二、讓iOS通過AirTurns發(fā)現(xiàn)Android設(shè)備

Airplay是基于局域網(wǎng)的服務(wù),在相同wifi的內(nèi)網(wǎng)下,蘋果設(shè)備會去搜尋支持Airplay服務(wù)的設(shè)備。
我們可以通過mDNS服務(wù)向局域網(wǎng)中發(fā)送一個MultiCast廣播,這樣iOS設(shè)備在內(nèi)網(wǎng)中就可以發(fā)現(xiàn)你(Android設(shè)備)了。Android可以使用JmDNS這個庫來構(gòu)建相關(guān)代碼。

- 代碼實(shí)現(xiàn)

  • 1.獲取滿足條件的本地網(wǎng)絡(luò)設(shè)備接口。去除沒有運(yùn)行的設(shè)備,過濾掉回送 、點(diǎn)對點(diǎn)和虛擬接口的,并且去掉不支持MultiCast的設(shè)備接口。
for (final NetworkInterface networkInterface : workInterfaces) {
            //如果網(wǎng)絡(luò)設(shè)備接口是 回送接口 & 點(diǎn)對點(diǎn)接口 & 沒有運(yùn)行 & 虛擬端口,則跳過執(zhí)行
            if (networkInterface.isLoopback() || networkInterface.isPointToPoint() || !networkInterface.isUp()
                    || networkInterface.isVirtual()) {
                continue;
            }
            // 不支持組播  跳過
            if (!networkInterface.supportsMulticast()) {
                continue;
            }
  • 2.對于滿足條件的設(shè)備,選取ipv4和ipv6的端口。
for (final InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
                        //端口是是ipv4 或者  ipv6 端口
                        if (address instanceof Inet4Address || address instanceof Inet6Address) {
                            try {
                                final JmDNS jmDNS = JmDNS.create(address, hostName);
                                jmDNSList.add(jmDNS);
  • 3.對滿足條件的端口號開啟 AirTunes/RAOP (遠(yuǎn)程音頻傳輸協(xié)議)服務(wù)。

協(xié)議常量定義

/**
     * The AirTunes/RAOP service type
     */
    static final String AIR_TUNES_SERVICE_TYPE = "_raop._tcp.local.";

    /**
     * The AirTunes/RAOP M-DNS service properties (TXT record)
     */
    static final Map<String, String> AIRTUNES_SERVICE_PROPERTIES = NetworkUtils.map(
            "txtvers", "1",
            "tp", "UDP",
            "ch", "2",
            "ss", "16",
            "sr", "44100",
            "pw", "false",
            "sm", "false",
            "sv", "false",
            "ek", "1",
            "et", "0,1",
            "cn", "0,1",
            "vn", "3");

    /**
     * The AirTunes/RAOP RTSP port
     */
    private int rtspPort = 5000; //default value

邏輯代碼


final ServiceInfo airTunesServiceInfo = ServiceInfo.create(
                                        AIR_TUNES_SERVICE_TYPE,
                                        hardwareAddressString + "@" + hostName,
                                        getRstpPort(),
                                        0,
                                        0,
                                        AIRTUNES_SERVICE_PROPERTIES
                                );
                                jmDNS.registerService(airTunesServiceInfo);
  • 4 完整工作線程代碼如下
package ss.serven.rduwan.airtunesandroid;

import android.util.Log;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

/**
 * Created by rduwan on 17/6/29.
 */

public class AirTunesRunnable implements Runnable {

    /**
     * The AirTunes/RAOP service type
     */
    static final String AIR_TUNES_SERVICE_TYPE = "_raop._tcp.local.";

    /**
     * The AirTunes/RAOP M-DNS service properties (TXT record)
     */
    static final Map<String, String> AIRTUNES_SERVICE_PROPERTIES = NetworkUtils.map(
            "txtvers", "1",
            "tp", "UDP",
            "ch", "2",
            "ss", "16",
            "sr", "44100",
            "pw", "false",
            "sm", "false",
            "sv", "false",
            "ek", "1",
            "et", "0,1",
            "cn", "0,1",
            "vn", "3");

    /**
     * The AirTunes/RAOP RTSP port
     */
    private int rtspPort = 5000; //default value

    protected List<JmDNS> jmDNSList;

    private static AirTunesRunnable instance = null;

    private final static String TAG = "AirTunesRunnable";

    private AirTunesRunnable() {
        jmDNSList = new java.util.LinkedList<JmDNS>();
    }

    public synchronized static AirTunesRunnable getInstance() {
        if (instance == null) {
            instance = new AirTunesRunnable();
        }
        return instance;
    }


    @Override
    public void run() {
        startAirTunesService();
    }

    private void startAirTunesService() {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                onAppShutDown();
            }
        }));
        sendMulitCastToiOS();
    }

    /**
     * 通過DNS服務(wù)給局域網(wǎng)里面的iOS發(fā)送組播,使得iOS設(shè)備能發(fā)現(xiàn)你
     * java 使用JmDNS庫
     */
    private void sendMulitCastToiOS() {
        //get Network details
        NetworkUtils networkUtils = NetworkUtils.getInstance();
        String hostName = "Rduwan-AirTunes";
        networkUtils.setHostName(hostName);
        String hardwareAddressString = networkUtils.getHardwareAddressString();
        try {
            synchronized (jmDNSList) {
                List<NetworkInterface> workInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
                for (final NetworkInterface networkInterface : workInterfaces) {
                    //如果網(wǎng)絡(luò)設(shè)備幾口是 回送接口 & 點(diǎn)對點(diǎn)接口 & 沒有運(yùn)行 & 虛擬端口,則跳過執(zhí)行
                    if (networkInterface.isLoopback() || networkInterface.isPointToPoint() || !networkInterface.isUp()
                            || networkInterface.isVirtual()) {
                        continue;
                    }
                    // 不支持組播  跳過
                    if (!networkInterface.supportsMulticast()) {
                        continue;
                    }

                    for (final InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
                        //端口是是ipv4 或者  ipv6 端口
                        if (address instanceof Inet4Address || address instanceof Inet6Address) {
                            try {
                                final JmDNS jmDNS = JmDNS.create(address, hostName);
                                jmDNSList.add(jmDNS);

                                //構(gòu)建AirTunes/RAOP (遠(yuǎn)程音頻傳輸協(xié)議)服務(wù)
                                final ServiceInfo airTunesServiceInfo = ServiceInfo.create(
                                        AIR_TUNES_SERVICE_TYPE,
                                        hardwareAddressString + "@" + hostName,
                                        getRstpPort(),
                                        0,
                                        0,
                                        AIRTUNES_SERVICE_PROPERTIES
                                );
                                jmDNS.registerService(airTunesServiceInfo);
                                Log.d(TAG, "Success to publish service on " + address + ", port: " + getRstpPort());
                            } catch (final Throwable e) {
                                Log.e(TAG, "Failed to publish service on " + address, e);

                            }

                        }
                    }
                }
            }
        } catch (Exception e) {

        }
    }

    public int getRstpPort() {
        return rtspPort;
    }

    private void onAppShutDown() {
        /* Stop all mDNS responders */
        synchronized(jmDNSList) {
            for(final JmDNS jmDNS: jmDNSList) {
                try {
                    jmDNS.unregisterAllServices();
                    Log.i(TAG, "Unregistered all services ");
                }
                catch (final Exception e) {
                    Log.i(TAG, "Failed to unregister some services");

                }
            }
        }
    }
}

  • 5 定義一個Service,啟動該線程。在Android設(shè)備上運(yùn)行此程序。在同樣wifi下的iOS設(shè)備,即可以看到名字叫"RDuwan-AirTunes"的AirTurns服務(wù)。
    如下圖

  • 6 第一部分工程代碼見Github鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,604評論 2 380

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