Android投屏方案(基於cling)

一 、前言

最近做了一個瀏覽器&視頻播放的項目,是在73.0.3683.90版本的chrome源碼上修改而來,涉及到抓取網頁裏視頻的播放地址、播放視頻、視頻投屏、視頻下載、網頁內廣告屏蔽等方面,瞭解到ijkplayer、GSYVideoPlayer、ffmpeg、樂播投屏、cling、NanoHttp、adblock等相關技術,現在就準備花點時間把一些技術相關的內容整理一下,分享給大家。

爲什麼先寫的是投屏相關的技術呢?剛開始投屏用的樂播的sdk,樂播的效果肯定是很好的,支持的協議更多,更穩定,但是樂播有一個限制,個人開發者不能獲取到APPID和SDK資源,最開始是幫別人做的項目,他們提供了相關的資源,所以就沒有去研究過投屏的其他方案。但是後來又有了個新項目,新項目也有一個需求是投屏,但是他們沒法提供相關的APPID和SDK,所以我就只能找新的方案,它就是cling。

android相關的投屏方案封裝不止cling一個,只是恰巧看到了,並且有人說cling算是封裝的比較好的了,所以就直接選擇了cling開始做。截止目前,我做的這個項目基本上能正常的投屏圖片、音頻、視頻等資源了,至於控制功能暫時還未嘗試,但是相關的方法是有的,只是沒有嘗試調用。因爲需求不同,所以目前我只研究了發送端的功能,至於接收端,我給的參考鏈接的最後兩個鏈接裏是有代碼可以參考的。

本來說到投屏技術,一般都會講到DLNA、AirPlay、UPNP協議等相關基礎,但是這方面的介紹文獻實在是多如牛毛,我就不在這裏浪費時間去複製粘貼別人的勞動成果了,我給出幾個當時我找資料時參考的幾篇文章,供大家參考:

Android手機投屏

cling源碼解析

投屏Cling DLNA 播放本地/網絡資源方法梳理

我demo參考的github源碼

本着大家都是着重於“取而用之”的實際需求,這裏先附上本次項目的源碼

基於cling實現的Android投屏方案

二 、實現的過程

我這個人呢,有個特別不好的習慣,不是十分喜歡直接抄襲別人的東西,又喜歡重複造輪子,但是呢,能力又有限,所以寫出來的東西會和參考的東西有所區別,但是不一定比別人的好,請大家不要見怪。但這次重複造輪子的原因,主要是因爲那個demo裏的代碼我沒辦法直接用,以及要解決cling2.2.0版本在9.0系統上出現無法解析描述文件的問題。

整個工程的目錄結構如下圖所示

[外鏈圖片轉存失敗(img-znXFPZXt-1563761574281)(https://raw.githubusercontent.com/ykbjson/ykbjson.github.io/master/blogimage/simpledlna/simpledlna_code_structure.png)]

2.1源碼淺析前的說明

webserver這個module就是基於NanoHttp實現的本地http服務器的代碼。

simplepermission整個module是一個權限請求的庫,因爲整個工程基於androidx,沒花時間去找適配androidx的權限庫,就自己改吧改吧了一下原來用的一個權限庫來用,因爲要實現投屏,必須要一些權限,參見screening module的manifest文件。

sereening module是整個項目的核心,有三個地方要先提出來說清楚,一個是log包下的AndroidLoggingHandler,這個類是爲了解決cling包裏的logger不輸出日誌的問題,具體的請看

How to configure java.util.logging on Android?

另一個是xml包下的幾個類,主要是重寫了cling裏解析設備交互報文的SAX解析器,cling原來的代碼,在生成解析器的時候拋了異常,導致設備交互的報文無法被解析,後續流程就中斷了,以至於無法發現可以投屏的設備。說到這裏,不得不說,大神們寫的代碼,設計的真的非常強大,擴展性考慮的很好,我本以爲只能clone cling的源碼下來自己改,沒想到這個解析器可以自定義,爲作者手動點贊!

最後一個地方呢,就是DLNABrowserService,裏面只是重載了AndroidUpnpServiceImpl的一個方法,返回DLNAUDA10ServiceDescriptorBinderSAXImpl,以便於替換cling自帶的無法在android9.0上面正常工作的UDA10ServiceDescriptorBinderSAXImpl。所以,在使用這個庫的時候,在app module的manifest裏聲明的就不是AndroidUpnpServiceImpl而是DLNABrowserService,這一點要注意。

至於bean包下的兩個類,DeviceInfo是對支持投屏的設備——Device 的一個封裝;MediaInfo是爲了方便傳遞要投屏的多媒體信息做的封裝。

2.2部分源碼淺析

接下來我們從listener包開始講解整個項目的源碼,裏面有四個回調接口,其實我感覺有些是多餘的,但是呢,因爲一些操作是異步的,感覺有一個回調接口能更好的控制使用這個庫的邏輯,避免出現一些錯誤。

###初始化DLNAManager回調接口——DLNAStateCallback

public interface DLNAStateCallback {

    void onConnected();

    void onDisconnected();

}

這個其實應該叫DLNAManagerInitCallback,初始化DLNAManager的時候傳遞的,可以爲null,只要你能保證你後續代碼時在DLNAManager初始化之後調用的。

###註冊設備列表和狀態回調接口——DLNARegistryListener

public abstract class DLNARegistryListener implements RegistryListener {
    private final DeviceType DMR_DEVICE_TYPE = new UDADeviceType("MediaRenderer");

    public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {

    }

    public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {

    }

    /**
     * Calls the {@link #onDeviceChanged(List)} method.
     *
     * @param registry The Cling registry of all devices and services know to the local UPnP stack.
     * @param device   A validated and hydrated device metadata graph, with complete service metadata.
     */
    public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
        onDeviceChanged(build(registry.getDevices()));
        onDeviceAdded(registry, device);
    }

    public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {

    }

    /**
     * Calls the {@link #onDeviceChanged(List)} method.
     *
     * @param registry The Cling registry of all devices and services know to the local UPnP stack.
     * @param device   A validated and hydrated device metadata graph, with complete service metadata.
     */
    public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
        onDeviceChanged(build(registry.getDevices()));
        onDeviceRemoved(registry, device);
    }

    /**
     * Calls the {@link #onDeviceChanged(List)} method.
     *
     * @param registry The Cling registry of all devices and services know to the local UPnP stack.
     * @param device   The local device added to the {@link org.fourthline.cling.registry.Registry}.
     */
    public void localDeviceAdded(Registry registry, LocalDevice device) {
        onDeviceChanged(build(registry.getDevices()));
        onDeviceAdded(registry, device);
    }

    /**
     * Calls the {@link #onDeviceChanged(List)} method.
     *
     * @param registry The Cling registry of all devices and services know to the local UPnP stack.
     * @param device   The local device removed from the {@link org.fourthline.cling.registry.Registry}.
     */
    public void localDeviceRemoved(Registry registry, LocalDevice device) {
        onDeviceChanged(build(registry.getDevices()));
        onDeviceRemoved(registry, device);
    }

    public void beforeShutdown(Registry registry) {

    }

    public void afterShutdown() {

    }

    public void onDeviceChanged(Collection<Device> deviceInfoList) {
        onDeviceChanged(build(deviceInfoList));
    }

    public abstract void onDeviceChanged(List<DeviceInfo> deviceInfoList);

    public void onDeviceAdded(Registry registry, Device device) {

    }

    public void onDeviceRemoved(Registry registry, Device device) {

    }

    private List<DeviceInfo> build(Collection<Device> deviceList) {
        final List<DeviceInfo> deviceInfoList = new ArrayList<>();
        for (Device device : deviceList) {
            //過濾不支持投屏渲染的設備
            if (null == device.findDevices(DMR_DEVICE_TYPE)) {
                continue;
            }
            final DeviceInfo deviceInfo = new DeviceInfo(device, getDeviceName(device));
            deviceInfoList.add(deviceInfo);
        }

        return deviceInfoList;
    }

    private String getDeviceName(Device device) {
        String name = "";
        if (device.getDetails() != null && device.getDetails().getFriendlyName() != null) {
            name = device.getDetails().getFriendlyName();
        } else {
            name = device.getDisplayString();
        }

        return name;
    }
}

這個類只是對RegistryListener的封裝,因爲我當時想着這個類主要是回調當前發現的設備的列表信息,所以就簡單封裝了一下,每次設備數量改變的時候就把新的設備數量通過一個回調方法傳遞出去,忽略一些不關注的方法。

###連接設備回調接口——DLNADeviceConnectListener

public interface DLNADeviceConnectListener {

    int TYPE_DLNA = 1;
    int TYPE_IM = 2;
    int TYPE_NEW_LELINK = 3;
    int CONNECT_INFO_CONNECT_SUCCESS = 100000;
    int CONNECT_INFO_CONNECT_FAILURE = 100001;
    int CONNECT_INFO_DISCONNECT = 212000;
    int CONNECT_INFO_DISCONNECT_SUCCESS = 212001;
    int CONNECT_ERROR_FAILED = 212010;
    int CONNECT_ERROR_IO = 212011;
    int CONNECT_ERROR_IM_WAITTING = 212012;
    int CONNECT_ERROR_IM_REJECT = 212013;
    int CONNECT_ERROR_IM_TIMEOUT = 212014;
    int CONNECT_ERROR_IM_BLACKLIST = 212015;

    void onConnect(DeviceInfo deviceInfo, int errorCode);

    void onDisconnect(DeviceInfo deviceInfo,int type,int errorCode);
}

這個類是給DLNAPlayer連接設備時用的。說到這個所謂的連接設備,其實感覺也不需要這個步驟,cling本身可能已經做好了設備之間的連接,回調回來的設備列表裏的設備都是連接過了的,直接可以通信。但是我發現樂播的sdk裏就有一個連接設備的方法,必須先調用連接設備的這個方法,在回調裏才能繼續後續操作,所以我這裏也設計了一個連接設備的步驟,我怕萬一是cling有專門連接設備的接口,只是我還沒發現而已,後面發現了就來改寫這個連接設備的方法。

###控制設備回調接口——DLNAControlCallback

public interface DLNAControlCallback {
    int ERROR_CODE_NO_ERROR = 0;

    int ERROR_CODE_RE_PLAY = 1;

    int ERROR_CODE_RE_PAUSE = 2;

    int ERROR_CODE_RE_STOP = 3;

    int ERROR_CODE_DLNA_ERROR = 4;

    int ERROR_CODE_SERVICE_ERROR = 5;

    int ERROR_CODE_NOT_READY = 6;


    void onSuccess(@Nullable ActionInvocation invocation);

    void onReceived(@Nullable ActionInvocation invocation,@Nullable Object ... extra);

    void onFailure(@Nullable ActionInvocation invocation,
                   @IntRange(from = ERROR_CODE_NO_ERROR, to = ERROR_CODE_NOT_READY) int errorCode,
                   @Nullable String errorMsg);
}

顧名思義,這個類就是發送端在控制接收端做出一系列動作時的回調接口,包括播放、暫停、結束、靜音開閉、音量調整、播放進度獲取等等。播放、暫停、結束、靜音開閉、音量調整等方法只會回調onSuccess和onFailure方法;獲取播放進度這種需要獲取結果的方法會在onReceived方法裏返回結果。

看完這幾個類之後,我們應該大致知道這個庫整個工作的流程了:初始化DLNAManager -> 註冊設備列表回調接口 -> 連接一個設備 -> 控制這個設備。只不過呢,我把連接設備和控制設備部分功能封裝到了DLNAPlayer裏面,不然DLNAManager會有點臃腫,不便於維護。這裏說到了整個庫的工作流程,那麼接下來我們就從DLNAManager開始接着分析。

###整個庫的入口——DLNAManager

public final class DLNAManager {
    private static final String TAG = "DLNAManager";
    private static final String LOCAL_HTTP_SERVER_PORT = "9090";

    private static boolean isDebugMode = false;

    private Context mContext;
    private AndroidUpnpService mUpnpService;
    private ServiceConnection mServiceConnection;
    private DLNAStateCallback mStateCallback;

    private RegistryListener mRegistryListener;
    private List<DLNARegistryListener> registryListenerList;
    private Handler mHandler;
    private BroadcastReceiver mBroadcastReceiver;

    private DLNAManager() {
        AndroidLoggingHandler.injectJavaLogger();
        mHandler = new Handler(Looper.getMainLooper());
        registryListenerList = new ArrayList<>();
        mRegistryListener = new RegistryListener() {

            @Override
            public void remoteDeviceDiscoveryStarted(final Registry registry, final RemoteDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.remoteDeviceDiscoveryStarted(registry, device);
                        }
                    }
                });
            }

            @Override
            public void remoteDeviceDiscoveryFailed(final Registry registry, final RemoteDevice device, final Exception ex) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.remoteDeviceDiscoveryFailed(registry, device, ex);
                        }
                    }
                });
            }

            @Override
            public void remoteDeviceAdded(final Registry registry, final RemoteDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.remoteDeviceAdded(registry, device);
                        }
                    }
                });
            }

            @Override
            public void remoteDeviceUpdated(final Registry registry, final RemoteDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.remoteDeviceUpdated(registry, device);
                        }
                    }
                });
            }

            @Override
            public void remoteDeviceRemoved(final Registry registry, final RemoteDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.remoteDeviceRemoved(registry, device);
                        }
                    }
                });
            }

            @Override
            public void localDeviceAdded(final Registry registry, final LocalDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.localDeviceAdded(registry, device);
                        }
                    }
                });
            }

            @Override
            public void localDeviceRemoved(final Registry registry, final LocalDevice device) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.localDeviceRemoved(registry, device);
                        }
                    }
                });
            }

            @Override
            public void beforeShutdown(final Registry registry) {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.beforeShutdown(registry);
                        }
                    }
                });
            }

            @Override
            public void afterShutdown() {
                mHandler.post(() -> {
                    synchronized (DLNAManager.class) {
                        for (DLNARegistryListener listener : registryListenerList) {
                            listener.afterShutdown();
                        }
                    }
                });
            }
        };

        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (null != intent && TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {
                    final NetworkInfo networkInfo = getNetworkInfo(context);
                    if (null == networkInfo) {
                        return;
                    }
                    if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
                        initLocalMediaServer();
                    }
                }
            }
        };
    }

    private static class DLNAManagerCreator {
        private static DLNAManager manager = new DLNAManager();
    }

    public static DLNAManager getInstance() {
        return DLNAManagerCreator.manager;
    }

    public void init(@NonNull Context context) {
        init(context, null);
    }

    public void init(@NonNull Context context, @Nullable DLNAStateCallback stateCallback) {
        if (null != mContext) {
            logW("ReInit DLNAManager");
            return;
        }
        if (context instanceof ContextThemeWrapper || context instanceof android.view.ContextThemeWrapper) {
            mContext = context.getApplicationContext();
        } else {
            mContext = context;
        }
        mStateCallback = stateCallback;
        initLocalMediaServer();
        initConnection();
        registerBroadcastReceiver();
    }

    private void initConnection() {
        mServiceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mUpnpService = (AndroidUpnpService) service;
                mUpnpService.getRegistry().addListener(mRegistryListener);
                mUpnpService.getControlPoint().search();
                if (null != mStateCallback) {
                    mStateCallback.onConnected();
                }
                logD("onServiceConnected");
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                mUpnpService = null;
                if (null != mStateCallback) {
                    mStateCallback.onDisconnected();
                }
                logD("onServiceDisconnected");
            }
        };

        mContext.bindService(new Intent(mContext, DLNABrowserService.class),
                mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    /**
     * 本地視頻和圖片也可以直接投屏,根目錄爲sd卡根目錄
     */
    private void initLocalMediaServer() {
        checkConfig();
        try {
            final PipedOutputStream pipedOutputStream = new PipedOutputStream();
            System.setIn(new PipedInputStream(pipedOutputStream));
            new Thread(() -> {
                final String localIpAddress = getLocalIpStr(mContext);
                final String localMediaRootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
                String[] args = {
                        "--host",
                        localIpAddress,/*局域網ip地址*/
                        "--port",
                        LOCAL_HTTP_SERVER_PORT,/*局域網端口*/
                        "--dir",
                        localMediaRootPath/*下載視頻根目錄*/
                };
                SimpleWebServer.startServer(args);
                logD("initLocalLinkService success,localIpAddress : " + localIpAddress +
                        ",localVideoRootPath : " + localMediaRootPath);
            }).start();
        } catch (IOException e) {
            e.printStackTrace();
            logE("initLocalLinkService failure", e);
        }
    }

    private void registerBroadcastReceiver() {
        checkConfig();
        mContext.registerReceiver(mBroadcastReceiver,
                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
    }

    private void unregisterBroadcastReceiver() {
        checkConfig();
        mContext.unregisterReceiver(mBroadcastReceiver);
    }

    public void registerListener(DLNARegistryListener listener) {
        checkConfig();
        checkPrepared();
        if (null == listener) {
            return;
        }
        registryListenerList.add(listener);
        listener.onDeviceChanged(mUpnpService.getRegistry().getDevices());
    }

    public void unregisterListener(DLNARegistryListener listener) {
        checkConfig();
        checkPrepared();
        if (null == listener) {
            return;
        }
        mUpnpService.getRegistry().removeListener(listener);
        registryListenerList.remove(listener);
    }

    public void startBrowser() {
        checkConfig();
        checkPrepared();
        mUpnpService.getRegistry().addListener(mRegistryListener);
        mUpnpService.getControlPoint().search();
    }

    public void stopBrowser() {
        checkConfig();
        checkPrepared();
        mUpnpService.getRegistry().removeListener(mRegistryListener);
    }

    public void destroy() {
        checkConfig();
        registryListenerList.clear();
        unregisterBroadcastReceiver();
        SimpleWebServer.stopServer();
        stopBrowser();
        if (null != mUpnpService) {
            mUpnpService.getRegistry().removeListener(mRegistryListener);
            mUpnpService.getRegistry().shutdown();
        }
        if (null != mServiceConnection) {
            mContext.unbindService(mServiceConnection);
            mServiceConnection = null;
        }
        if (null != mHandler) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }
        registryListenerList = null;
        mRegistryListener = null;
        mBroadcastReceiver = null;
        mStateCallback = null;
        mContext = null;
    }

    private void checkConfig() {
        if (null == mContext) {
            throw new IllegalStateException("Must call init(Context context) at first");
        }
    }

    private void checkPrepared() {
        if (null == mUpnpService) {
            throw new IllegalStateException("Invalid AndroidUpnpService");
        }
    }

    //------------------------------------------------------靜態方法-----------------------------------------------

    /**
     * 獲取ip地址
     *
     * @param context
     * @return
     */
    public static String getLocalIpStr(@NonNull Context context) {
        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        if (null == wifiInfo) {
            return "";
        }
        return intToIpAddress(wifiInfo.getIpAddress());
    }

    /**
     * int類型的ip轉換成標準ip地址
     *
     * @param ip
     * @return
     */
    public static String intToIpAddress(int ip) {
        return (ip & 0xff) + "." + ((ip >> 8) & 0xff) + "." + ((ip >> 16) & 0xff) + "." + ((ip >> 24) & 0xff);
    }

    public static NetworkInfo getNetworkInfo(@NonNull Context context) {
        final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        return null == connectivityManager ? null : connectivityManager.getActiveNetworkInfo();
    }

    static String tryTransformLocalMediaAddressToLocalHttpServerAddress(@NonNull Context context,
                                                                        String sourceUrl) {
        logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,sourceUrl : " + sourceUrl);
        if (TextUtils.isEmpty(sourceUrl)) {
            return sourceUrl;
        }

        if (!isLocalMediaAddress(sourceUrl)) {
            return sourceUrl;
        }

        String newSourceUrl = getLocalHttpServerAddress(context) +
                sourceUrl.replace(Environment.getExternalStorageDirectory().getAbsolutePath(), "");
        logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,newSourceUrl : " + newSourceUrl);

        try {
            final String[] urlSplits = newSourceUrl.split("/");
            final String originFileName = urlSplits[urlSplits.length - 1];
            String fileName = originFileName;
            fileName = URLEncoder.encode(fileName, "UTF-8");
            fileName = fileName.replaceAll("\\+", "%20");
            newSourceUrl = newSourceUrl.replace(originFileName, fileName);
            logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,encodeNewSourceUrl : " + newSourceUrl);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return newSourceUrl;
    }

    private static boolean isLocalMediaAddress(String sourceUrl) {
        return !TextUtils.isEmpty(sourceUrl)
                && !sourceUrl.startsWith("http://")
                && !sourceUrl.startsWith("https://")
                && sourceUrl.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
    }

    /**
     * 獲取本地http服務器地址
     *
     * @param context
     * @return
     */
    public static String getLocalHttpServerAddress(Context context) {
        return "http://" + getLocalIpStr(context) + ":" + LOCAL_HTTP_SERVER_PORT;
    }

    public static void setIsDebugMode(boolean isDebugMode) {
        DLNAManager.isDebugMode = isDebugMode;
    }


    static void logV(String content) {
        logV(TAG, content);
    }

    public static void logV(String tag, String content) {
        if (!isDebugMode) {
            return;
        }
        Log.v(tag, content);
    }

    static void logD(String content) {
        logD(TAG, content);
    }

    public static void logD(String tag, String content) {
        if (!isDebugMode) {
            return;
        }
        Log.d(tag, content);
    }


    static void logI(String content) {
        logI(TAG, content);
    }

    public static void logI(String tag, String content) {
        if (!isDebugMode) {
            return;
        }
        Log.i(tag, content);
    }


    static void logW(String content) {
        logW(TAG, content);
    }

    public static void logW(String tag, String content) {
        if (!isDebugMode) {
            return;
        }
        Log.w(tag, content);
    }


    static void logE(String content) {
        logE(TAG, content);
    }


    public static void logE(String tag, String content) {
        logE(tag, content, null);
    }


    static void logE(String content, Throwable throwable) {
        logE(TAG, content, throwable);
    }

    public static void logE(String tag, String content, Throwable throwable) {
        if (!isDebugMode) {
            return;
        }
        if (null != throwable) {
            Log.e(tag, content, throwable);
        } else {
            Log.e(tag, content);
        }
    }
}

這個類有點長,但是要關注的方法就那麼幾個。init方法裏幹了幾件事:

1.初始化本地投屏服務——initLocalMediaServer,投屏本地視頻

2.連接AndroidUpnpService——initConnection,獲取控制點和投屏服務

3.註冊了一個網絡連接變化的廣播——registerBroadcastReceiver,網絡變化時重啓LocalMediaServer,保證本地資源投屏成功的機率

還有就是發起搜索設備的動作、停止搜索設備的動作、註冊RegistryListener、移除RegistryListener等方法。剩下一些就是可以封裝到工具類裏的方法,懶得在添加類了,索性就寫到了裏面。

這個類還有一個作用就是維護了一個RegistryListener,統一的分發局域網內設備數量、設備狀態、設備服務狀態變化的回調事件。當你初始化完DLNAManager,並向這個類註冊了DLNARegistryListener,然後調用startBrowser發起搜索,如果局域網內有可以接受投屏的設備,你就可以在DLNARegistryListener的onDeviceChanged方法裏收到當前局域網內可以投屏的設備列表了。有了可用的設備列表,接下來,我們就可以開始連接接收端設備發送投屏數據以及控制他了。

連接和控制接收端設備——DLNAPlayer

public class DLNAPlayer {

    private static final String DIDL_LITE_FOOTER = "</DIDL-Lite>";
    private static final String DIDL_LITE_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>"
            + "<DIDL-Lite "
            + "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" "
            + "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
            + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
            + "xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\">";

    /**
     * 未知狀態
     */
    public static final int UNKNOWN = -1;

    /**
     * 已連接狀態
     */
    public static final int CONNECTED = 0;

    /**
     * 播放狀態
     */
    public static final int PLAY = 1;
    /**
     * 暫停狀態
     */
    public static final int PAUSE = 2;
    /**
     * 停止狀態
     */
    public static final int STOP = 3;
    /**
     * 轉菊花狀態
     */
    public static final int BUFFER = 4;
    /**
     * 投放失敗
     */
    public static final int ERROR = 5;

    /**
     * 已斷開狀態
     */
    public static final int DISCONNECTED = 6;

    private int currentState = UNKNOWN;
    private DeviceInfo mDeviceInfo;
    private Device mDevice;
    private MediaInfo mMediaInfo;
    private Context mContext;//鑑權預留
    private ServiceConnection mServiceConnection;
    private AndroidUpnpService mUpnpService;
    private DLNADeviceConnectListener connectListener;
    /**
     * 連接、控制服務
     */
    private ServiceType AV_TRANSPORT_SERVICE;
    private ServiceType RENDERING_CONTROL_SERVICE;


    public DLNAPlayer(@NonNull Context context) {
        mContext = context;
        AV_TRANSPORT_SERVICE = new UDAServiceType("AVTransport");
        RENDERING_CONTROL_SERVICE = new UDAServiceType("RenderingControl");
        initConnection();
    }

    public void setConnectListener(DLNADeviceConnectListener connectListener) {
        this.connectListener = connectListener;
    }

    private void initConnection() {
        mServiceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mUpnpService = (AndroidUpnpService) service;
                currentState = CONNECTED;
                if (null != mDeviceInfo) {
                    mDeviceInfo.setState(CONNECTED);
                    mDeviceInfo.setConnected(true);
                }
                if (null != connectListener) {
                    connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS);
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                currentState = DISCONNECTED;
                if (null != mDeviceInfo) {
                    mDeviceInfo.setState(DISCONNECTED);
                    mDeviceInfo.setConnected(false);
                }
                if (null != connectListener) {
                    connectListener.onDisconnect(mDeviceInfo, DLNADeviceConnectListener.TYPE_DLNA,
                            DLNADeviceConnectListener.CONNECT_INFO_DISCONNECT_SUCCESS);
                }
                mUpnpService = null;
                connectListener = null;
                mDeviceInfo = null;
                mDevice = null;
                mMediaInfo = null;
                AV_TRANSPORT_SERVICE = null;
                RENDERING_CONTROL_SERVICE = null;
                mServiceConnection = null;
                mContext = null;
            }
        };
    }

    public void connect(@NonNull DeviceInfo deviceInfo) {
        checkConfig();
        mDeviceInfo = deviceInfo;
        mDevice = mDeviceInfo.getDevice();
        if (null != mUpnpService) {
            currentState = CONNECTED;
            if (null != connectListener) {
                connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS);
            }
            return;
        }
        mContext.bindService(new Intent(mContext, DLNABrowserService.class),
                mServiceConnection, Context.BIND_AUTO_CREATE);
    }


    public void disconnect() {
        checkConfig();
        try {
            mContext.unbindService(mServiceConnection);
        } catch (Exception e) {
            DLNAManager.logE("DLNAPlayer disconnect error.", e);
        }
    }

    private void checkPrepared() {
        if (null == mUpnpService) {
            throw new IllegalStateException("Invalid AndroidUpnpService");
        }
    }

    private void checkConfig() {
        if (null == mContext) {
            throw new IllegalStateException("Invalid context");
        }
    }

    private void execute(@NonNull ActionCallback actionCallback) {
        checkPrepared();
        mUpnpService.getControlPoint().execute(actionCallback);

    }

    private void execute(@NonNull SubscriptionCallback subscriptionCallback) {
        checkPrepared();
        mUpnpService.getControlPoint().execute(subscriptionCallback);
    }

    public void play(@NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(PLAY, avtService, callback)) {
            return;
        }
        execute(new Play(avtService) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                currentState = PLAY;
                callback.onSuccess(invocation);
                mDeviceInfo.setState(PLAY);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }

    public void pause(@NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(PAUSE, avtService, callback)) {
            return;
        }

        execute(new Pause(avtService) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                currentState = PAUSE;
                callback.onSuccess(invocation);
                mDeviceInfo.setState(PAUSE);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }


    public void stop(@NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(STOP, avtService, callback)) {
            return;
        }
        execute(new Stop(avtService) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                currentState = STOP;
                callback.onSuccess(invocation);
                mDeviceInfo.setState(STOP);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }

    public void seekTo(String time, @NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(avtService, callback)) {
            return;
        }
        execute(new Seek(avtService, time) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                callback.onSuccess(invocation);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }

    public void setVolume(long volume, @NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE);
        if (checkErrorBeforeExecute(avtService, callback)) {
            return;
        }

        execute(new SetVolume(avtService, volume) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                callback.onSuccess(invocation);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }

    public void mute(boolean desiredMute, @NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE);
        if (checkErrorBeforeExecute(avtService, callback)) {
            return;
        }
        execute(new SetMute(avtService, desiredMute) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                callback.onSuccess(invocation);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        });
    }


    public void getPositionInfo(@NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(avtService, callback)) {
            return;
        }

        final GetPositionInfo getPositionInfo = new GetPositionInfo(avtService) {
            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }

            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                callback.onSuccess(invocation);
            }

            @Override
            public void received(ActionInvocation invocation, PositionInfo info) {
                callback.onReceived(invocation, info);
            }
        };

        execute(getPositionInfo);
    }


    public void getVolume(@NonNull DLNAControlCallback callback) {
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (checkErrorBeforeExecute(avtService, callback)) {
            return;
        }
        final GetVolume getVolume = new GetVolume(avtService) {

            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                callback.onSuccess(invocation);
            }

            @Override
            public void received(ActionInvocation invocation, int currentVolume) {
                callback.onReceived(invocation, currentVolume);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                currentState = ERROR;
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
                mDeviceInfo.setState(ERROR);
            }
        };
        execute(getVolume);
    }


    public void setDataSource(@NonNull MediaInfo mediaInfo) {
        mMediaInfo = mediaInfo;
        //嘗試變換本地播放地址
        mMediaInfo.setUri(DLNAManager.tryTransformLocalMediaAddressToLocalHttpServerAddress(mContext,
                mMediaInfo.getUri()));
    }

    public void start(final @NonNull DLNAControlCallback callback) {
        mDeviceInfo.setMediaID(mMediaInfo.getMediaId());
        String metadata = pushMediaToRender(mMediaInfo);
        final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE);
        if (null == avtService) {
            callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null);
            return;
        }
        execute(new SetAVTransportURI(avtService, mMediaInfo.getUri(), metadata) {
            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                play(callback);
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                DLNAManager.logE("play error:" + defaultMsg);
                currentState = ERROR;
                mDeviceInfo.setState(ERROR);
                callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg);
            }
        });
    }


    private String pushMediaToRender(@NonNull MediaInfo mediaInfo) {
        return pushMediaToRender(mediaInfo.getUri(), mediaInfo.getMediaId(), mediaInfo.getMediaName(),
                mediaInfo.getMediaType());
    }

    private String pushMediaToRender(String url, String id, String name, int ItemType) {
        final long size = 0;
        final Res res = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), size, url);
        final String creator = "unknow";
        final String parentId = "0";
        final String metadata;

        switch (ItemType) {
            case MediaInfo.TYPE_IMAGE:
                ImageItem imageItem = new ImageItem(id, parentId, name, creator, res);
                metadata = createItemMetadata(imageItem);
                break;
            case MediaInfo.TYPE_VIDEO:
                VideoItem videoItem = new VideoItem(id, parentId, name, creator, res);
                metadata = createItemMetadata(videoItem);
                break;
            case MediaInfo.TYPE_AUDIO:
                AudioItem audioItem = new AudioItem(id, parentId, name, creator, res);
                metadata = createItemMetadata(audioItem);
                break;
            default:
                throw new IllegalArgumentException("UNKNOWN MEDIA TYPE");
        }

        DLNAManager.logE("metadata: " + metadata);
        return metadata;
    }

    /**
     * 創建投屏的參數
     *
     * @param item
     * @return
     */
    private String createItemMetadata(DIDLObject item) {
        StringBuilder metadata = new StringBuilder();
        metadata.append(DIDL_LITE_HEADER);

        metadata.append(String.format("<item id=\"%s\" parentID=\"%s\" restricted=\"%s\">", item.getId(), item.getParentID(), item.isRestricted() ? "1" : "0"));

        metadata.append(String.format("<dc:title>%s</dc:title>", item.getTitle()));
        String creator = item.getCreator();
        if (creator != null) {
            creator = creator.replaceAll("<", "_");
            creator = creator.replaceAll(">", "_");
        }
        metadata.append(String.format("<upnp:artist>%s</upnp:artist>", creator));
        metadata.append(String.format("<upnp:class>%s</upnp:class>", item.getClazz().getValue()));

        DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        Date now = new Date();
        String time = sdf.format(now);
        metadata.append(String.format("<dc:date>%s</dc:date>", time));

        Res res = item.getFirstResource();
        if (res != null) {
            // protocol info
            String protocolinfo = "";
            ProtocolInfo pi = res.getProtocolInfo();
            if (pi != null) {
                protocolinfo = String.format("protocolInfo=\"%s:%s:%s:%s\"", pi.getProtocol(), pi.getNetwork(), pi.getContentFormatMimeType(), pi
                        .getAdditionalInfo());
            }
            DLNAManager.logE("protocolinfo: " + protocolinfo);

            // resolution, extra info, not adding yet
            String resolution = "";
            if (res.getResolution() != null && res.getResolution().length() > 0) {
                resolution = String.format("resolution=\"%s\"", res.getResolution());
            }

            // duration
            String duration = "";
            if (res.getDuration() != null && res.getDuration().length() > 0) {
                duration = String.format("duration=\"%s\"", res.getDuration());
            }

            // res begin
            //            metadata.append(String.format("<res %s>", protocolinfo)); // no resolution & duration yet
            metadata.append(String.format("<res %s %s %s>", protocolinfo, resolution, duration));

            // url
            String url = res.getValue();
            metadata.append(url);

            // res end
            metadata.append("</res>");
        }
        metadata.append("</item>");

        metadata.append(DIDL_LITE_FOOTER);

        return metadata.toString();
    }

    private boolean checkErrorBeforeExecute(int expectState, Service avtService, @NonNull DLNAControlCallback callback) {
        if (currentState == expectState) {
            callback.onSuccess(null);
            return true;
        }

        return checkErrorBeforeExecute(avtService, callback);
    }

    private boolean checkErrorBeforeExecute(Service avtService, @NonNull DLNAControlCallback callback) {
        if (currentState == UNKNOWN) {
            callback.onFailure(null, DLNAControlCallback.ERROR_CODE_NOT_READY, null);
            return true;
        }

        if (null == avtService) {
            callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null);
            return true;
        }

        return false;
    }

}

這個類也很長,因爲幹事情的就是他,所以他的方法比較多,設定播放數據、播放、暫停、停止、拖動進度、靜音控制、音量控制等等都在這個DLNAPlayer裏實現的。cling對設定投屏數據、播放、暫停、停止、拖動進度、靜音控制、音量控制等功能都做了封裝,我這裏只是統一了一個回調接口,這些個方法裏,只有設定投屏數據的時候才需要發送upnp協議規定的xml數據,其他方法都不需要。構建xml數據的方法也是在上面給出的鏈接裏複製的,反正就是upnp協議規定好的,需要這中格式的數據,如果你想接收端能比較完整的顯示投屏的數據信息,傳遞的MediaInfo可以詳細些,我這裏都值傳遞了多媒體地址信息。

三、結語

唉,終於貼完代碼了,貼的時候感覺好無奈,自己也很反感這中方式,但是這只是對cling的一個簡單實用實用示例,技術細節都是別人處理好了的,我只是做了點簡單的分層,希望大家看了demo能直接使用cling實現投屏功能,也沒什麼技術分析,所以就只是貼個代碼了。

至於使用的方法,我就更懶得貼了,沒有任何意義,大家直接看源碼的demo就可以了,我只給大家提幾個需要注意的地方:

1.app module的build.gradle文件必須要加上一句

	//去重複的引用
packagingOptions {
    exclude 'META-INF/beans.xml'
}

這是由於引入jetty引起的文件重複。

2.build.gradle文件裏類似如下代碼

  minSdkVersion rootProject.ext.minSdkVersion
 targetSdkVersion rootProject.ext.targetSdkVersion
 versionCode rootProject.ext.versionCode
 versionName rootProject.ext.versionName

裏面的ext.minSdkVersion等等,請參見根目錄的build.gradle。

3.所有工程的依賴庫都基於androidx,所以,如果有需要的童鞋在集成到自己的工程裏的時候要慎重,因爲androidx庫和support庫不兼容。

最後,祝大家工作愉快。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章