android投屏技术🔥🔥:发现设备源码分析

cover

前言

上篇文章是关于发现设备代码实现过程,本来这两篇文章是一起的,写着写着发现实在是太长了,我担心会看着会消化不良,所以分开了。

关于 android 投屏技术系列:
一、知识概念

这章主要讲一些基本概念, 那些 DLNA 类库都是基于这些概念来做的,了解这些概念能帮助你理清思路,同时可以提升开发效率,遇到问题也能有个解决问题的清晰思路。

二、手机与tv对接

这部分是通过Cling DLNA类库来实现发现设备的。
内容包括:

  1. 抽出发现设备所需接口
  2. 发现设备步骤的实现
  3. 原理的分析

三、手机与tv通信

这部分也是通过Cling DLNA类库来实现手机对tv的控制。
内容包括:

  1. 控制设备步骤
  2. 控制设备代码实现
  3. 手机如何控制tv
  4. tv将自己的信息如何通知手机
  5. 原理的分析

源码分析阶段

什么源码?
打开ide看源码

我们先从入口开始:

upnpService.getControlPoint().search();

可见是 控制点执行的 search 方法。而 ControlPoint 的实现类是 ControlPointImpl:
ControlPointImpl.search() 如下:

public void search() {
    search(new STAllHeader(), MXHeader.DEFAULT_VALUE);
}

STAllHeader 是什么玩意?进去看看

public class STAllHeader extends UpnpHeader<NotificationSubtype> {

    public STAllHeader() {
        setValue(NotificationSubtype.ALL);
    }

    public void setString(String s) throws InvalidHeaderException {
        if (!s.equals(NotificationSubtype.ALL.getHeaderString())) {
            throw new InvalidHeaderException("Invalid ST header value (not "+NotificationSubtype.ALL+"): " + s);
        }
    }

    public String getString() {
        return getValue().getHeaderString();
    }
}

setValue(NotificationSubtype.ALL) ???

public enum NotificationSubtype {

    ALIVE("ssdp:alive"),
    UPDATE("ssdp:update"),
    BYEBYE("ssdp:byebye"),
    ALL("ssdp:all"),
    DISCOVER("ssdp:discover"),
    PROPCHANGE("upnp:propchange");

    private String headerString;

    NotificationSubtype(String headerString) {
        this.headerString = headerString;
    }

    public String getHeaderString() {
        return headerString;
    }
}

NotificationSubtype.ALL = "ssdp:all"
是否记得 ssdp ? 这个就是发现设备的协议, ":" 后面就是一个筛选。
好了,我们继续返回到 search 中。
ControlPointImpl.search() 实际调用的是

public void search(UpnpHeader searchType, int mxSeconds) {
        log.fine("Sending asynchronous search for: " + searchType.getString());
        getConfiguration().getAsyncProtocolExecutor().execute(
                getProtocolFactory().createSendingSearch(searchType, mxSeconds)
        );
    }

下面解释一下:

getConfiguration() 返回的对象是 UpnpServiceConfiguration
getAsyncProtocolExecutor() 返回的对象是一个执行者 Executor
getProtocolFactory() 返回的对象是 ProtocolFactory
看起来最后执行的是 ProtocolFactory.createSendingSearch() 方法进行的设备发现。

这段代码仍然有很多疑惑的地方:

  1. UpnpServiceConfiguration 是什么? 有什么作用?
  2. UpnpServiceConfiguration 包含了一个 Executor 它如何工作的?
  3. ProtocolFactory 协议工厂? 它跟协议有什么关系吗?
  4. 这些乱七八糟的怎么连接起来的?

我们带着这些问题来解析源码。

先补充能量~

首先 UpnpServiceConfiguration 是不是有点面熟?其实在前面AndroidUpnpServiceImpl 中就有一个方法:

public UpnpServiceConfiguration getConfiguration();

这个 UpnpServiceConfiguration 在 AndroidUpnpServiceImpl 中实际上是 AndroidUpnpServiceConfiguration。在 AndroidUpnpServiceImpl onCreate 时构造的。
其实这个东西,它是用于配置环境的,比如 AndroidUpnpServiceConfiguration 它就用于配置 android 环境,比如一些网络、xml解析、全局使用的一些方法之类的。所以想想 从这里面获取执行者(这个执行者是:ClingExecutor)也比较正常了。那么如果我们有什么特殊的需要,也可以自己定义一个配置,然后增加一些自己需要的方法等。

上面提到了 ClingExecutor 它有什么用处?

我们看一下 ClingExecutor 源码:

public static class ClingExecutor extends ThreadPoolExecutor {

        public ClingExecutor() {
            this(new ClingThreadFactory(),
                 new ThreadPoolExecutor.DiscardPolicy() {
                     // The pool is unbounded but rejections will happen during shutdown
                     @Override
                     public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
                         // Log and discard
                         log.info("Thread pool rejected execution of " + runnable.getClass());
                         super.rejectedExecution(runnable, threadPoolExecutor);
                     }
                 }
            );
        }

        public ClingExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedHandler) {
            // This is the same as Executors.newCachedThreadPool
            super(0,
                  Integer.MAX_VALUE,
                  60L,
                  TimeUnit.SECONDS,
                  new SynchronousQueue<Runnable>(),
                  threadFactory,
                  rejectedHandler
            );
        }

        @Override
        protected void afterExecute(Runnable runnable, Throwable throwable) {
            super.afterExecute(runnable, throwable);
            if (throwable != null) {
                Throwable cause = Exceptions.unwrap(throwable);
                if (cause instanceof InterruptedException) {
                    // Ignore this, might happen when we shutdownNow() the executor. We can't
                    // log at this point as the logging system might be stopped already (e.g.
                    // if it's a CDI component).
                    return;
                }
                // Log only
                log.warning("Thread terminated " + runnable + " abruptly with exception: " + throwable);
                log.warning("Root cause: " + cause);
            }
        }
    }

通过源码我们可以知道两点:

  1. ClingExecutor 继承 ThreadPoolExecutor(线程池) 说明它就是执行线程池相关配置等工作的。
  2. 里面提到了 ClingThreadFactory,说明线程是在这里创建的。

ClingExecutor 在 AndroidUpnpServiceConfiguration 父类 DefaultUpnpServiceConfiguration 构造中 构造的:

   protected ExecutorService getDefaultExecutorService() {
        return defaultExecutorService;
    }

    protected ExecutorService createDefaultExecutorService() {
        return new ClingExecutor();
    }

在 发现设备 中,是通过获取 AndroidUpnpServiceConfiguration 的执行者,返回的就是这个 ClingExecutor

    public Executor getAsyncProtocolExecutor() {
        return getDefaultExecutorService();
    }

在 控制设备 中,也是通过获取 AndroidUpnpServiceConfiguration 中的执行者,它返回的还是这个 ClingExecutor

public ExecutorService getSyncProtocolExecutorService() {
        return getDefaultExecutorService();
    }

所以在 AndroidUpnpServiceConfiguration 中所有的返回执行者都是返回的它。
而且控制点的命令都是通过这个执行者来完成的,说明它担任了 Cling 中很重要的角色。

执行过程的话,简单来说就是发一个指令(这个指令要么是 发现设备协议 要么是 控制设备协议 的指令),Executor 执行的 就是一个 Runnable 了。在 发现设备中 这个 Runnable 的实现类是 SendingSearch;控制设备 中实现类是 ActionCallback。(这篇文章 主要分析一下 发现设备,控制设备 下篇文章来看)

下面我们看一下 SendingSearch 的精简版源码:

public abstract class SendingAsync implements Runnable {
...
}

public class SendingSearch extends SendingAsync {
...

    // 发现设备 最后执行方法, 是它 是它 就是它。。
    protected void execute() throws RouterException {

        log.fine("Executing search for target: " + searchTarget.getString() + " with MX seconds: " + getMxSeconds());

        OutgoingSearchRequest msg = new OutgoingSearchRequest(searchTarget, getMxSeconds());
        prepareOutgoingSearchRequest(msg);

        for (int i = 0; i < getBulkRepeat(); i++) {
            try {

                getUpnpService().getRouter().send(msg);

                // UDA 1.0 is silent about this but UDA 1.1 recommends "a few hundred milliseconds"
                log.finer("Sleeping " + getBulkIntervalMilliseconds() + " milliseconds");
                Thread.sleep(getBulkIntervalMilliseconds());

            } catch (InterruptedException ex) {
                // Interruption means we stop sending search messages, e.g. on shutdown of thread pool
                break;
            }
        }
    }

...
}

我们一起分析一下,你要听 简单版 还是 复杂版?
简单版:
重要代码:getUpnpService().getRouter().send(msg);
翻译出来就是 向路由 发消息;
这个消息 msg 是 OutgoingSearchRequest 的实例,OutgoingSearchRequest 里面就封装了 发现设备 的请求内容。

复杂版:
%……¥……%%&%@#@@%#%%$$%&&((
(太复杂了,系统无法翻译)

不调你口味了。。 还是详细看一下
这里面的疑点:

  1. 这个路由是什么鬼?
  2. SendingSearch 在哪定义的?

首先 getRouter 是不是在哪看过? 是的 在上篇文章,
AndroidUpnpServiceImpl 里面看过。在 AndroidUpnpServiceImpl 里,getRouter() 是 AndroidRouter 对象。

public class AndroidRouter extends RouterImpl {
...
}

getUpnpService().getRouter().send(msg); send 方法实际在 RouterImpl 中。
看一下:

    /**
     * Sends the UDP datagram on all bound {@link org.fourthline.cling.transport.spi.DatagramIO}s.
     *
     * @param msg The UDP datagram message to send.
     */
    public void send(OutgoingDatagramMessage msg) throws RouterException {
        lock(readLock);
        try {
            if (enabled) {
                for (DatagramIO datagramIO : datagramIOs.values()) {
                    datagramIO.send(msg);
                }
            } else {
                log.fine("Router disabled, not sending datagram: " + msg);
            }
        } finally {
            unlock(readLock);
        }
    }

DatagramIO 它的实现类是 DatagramIOImpl,send 方法其实就是发io流给路由了。
这个路由 就是封装了一些网络相关的内容,包括网络地址、发送io流的内容等等。

回想一下发现设备流程,我们首先确保 android手机 跟 tv盒子在同一个网络下(这样 tv盒子其实向路由发送了自己的信息),然后 我们的手机设备告诉路由 我们需要什么样的设备(支持投屏),路由通过我们的需求在设备列表中筛选完之后 我们就得到了这些设备。

好了,发现设备流程还剩最后一步就结束了:
向路由发完消息之后 我怎么得到设备列表的?

  1. 这些设备保存在哪里?
  2. 我们如何被通知到设备的改变?

这些设备保存在哪里?
这些设备是保存在 RegistryImpl 中:
保存的设备有两种:一个是 RemoteItems(远程设备,不是当前设备);另一个是 LocalItems(本地设备,就是当前设备)。他们就相当于列表。

我们如何被通知到设备的改变?
是否记得上篇文章提到的,监听。发现设备之后 会回调到我们定义的监听。
其实在 lan 层会截获到路由发的消息,然后会通知到我们。

总结一下:

  1. AndroidUpnpServiceConfiguration 它就用于配置 android 环境,比如一些网络、xml解析、全局使用的一些方法之类的。那么如果我们有什么特殊的需要,也可以自己定义一个配置,然后增加一些自己需要的方法等。
  2. ClingExecutor 是一个执行者,发现设备、控制设备命令都是由它来执行
  3. 发现设备实际是通过 SendingSearch 来实现的,控制设备则是 ActionCallback(下篇文章会提到)
  4. SendingSearch 其实就是向路由发消息,告诉路由 我需要什么样的设备,路由会筛选,通过我们定义的监听返回给我们。

点击查看详细代码

大功告成,我终于可以开心的玩耍了
收工

下集预告:
我们现在已经成功发现了 tv盒子,我们要投屏必须要控制它的 播放、暂停、停止、拖拽等操作。下集我们会一步步 来实现这些操作。

下集看点:

  1. 设备控制 的代码实现
  2. 设备控制 原理分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容