android投屏技术🔥🔥🔥:控制设备源码分析

cover

前言

又来到了源码分析,说实话在写文章之前 我并没有很仔细的阅读过 Cling 的源码,所以说 我也只是个菜比。但我会竭尽所能的把我所了解的东西分享出来,我希望对那些做 DLNA 的童鞋有所帮助。阅读源码的好处,首先就是能够更了解它的原理,这能帮助我们更好的使用它。同时,阅读源码可以能提升我们逻辑思维能力,以及设计能力,让我们能够设计出更简洁的代码。
其实我并不喜欢看别人写的源码分析,我更喜欢自己去看源码。
所以,我一直在想,
为什么要看别人写的源码分析?

  • 这样做是不是能更快速的了解源码?
  • 是不是源码难懂?

所以。。。
这篇文章会把 Cling 库控制设备流程大致讲一下,同时我会告诉你 我阅读 Cling 源码的过程,这样 你以后看源码的思路会更清晰一点 (多好的人啊,我都被自己感动了)。
先从控制设备流程开始说起,然后根据这个流程一一展开,介绍整体结构。这个过程也保持之前的模式:带着问题 一步一步的寻找答案。

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

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

二、手机与tv对接

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

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

三、手机与tv通信

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

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

关于控制设备

上篇文章解释了 什么是控制设备;控制设备过程是怎样的;以及代码的实现。
那么,Cling 源码 是怎么实现这个过程的呢?

为了简单明了,我们先忽视那些底层的东西,从最简单的开始看。
为什么要这样?
因为如果不忽视这些底层的东西,你会很容易陷入迷茫的状态。而先把外层简单的逻辑梳理清楚以后,再深入了解底层的就更容易看懂一点。

我们理一下控制设备相关的类和方法有哪些?
是否记得 控制设备 使用的三步曲?(不记得的可以翻上面的链接看,不看也不要紧)

  1. 获取tv设备控制服务:通过选中的设备执行 device.findService(serviceType);
  2. 获取控制点:通过执行 UpnpService.getControlPoint()
  3. 执行指定控制命令:通过执行 ControlPoint.execute(命令)

于是,我们可以根据这些入手,看它们之间是如何连接起来的,然后它们分别做了什么事情。

Service service = device.findService(serviceType);

这段代码 返回的是 服务 Service
serviceType 就是服务类型,比如 AVTransport ...
我们知道,控制设备 是通过 控制点 执行控制命令。
下面就是控制设备播放的操作:

Service avtService = device.findService(new UDAServiceType("AVTransport"));
ControlPoint controlPoint = UpnpService.getControlPoint();
controlPoint.execute(new Play(avtService) {

            @Override
            public void success(ActionInvocation invocation) {
                super.success(invocation);
                // to do success
            }

            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                // to do failure
                }
            }
        });

可见,controlPoint.execute(new Play(avtService) 这一句很关键。
它告诉我们

  1. ControlPoint 有一个 execute 方法
  2. 执行命令时 传入了一个 Play ,Play(服务)

在分析 发现设备 源码的时候,我们得出 controlPoint.execute(..) 是通过 ExecutorService.submit(...) 执行的。 最后的执行者是 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);
            }
        }
    }

可见 ClingExecutor extends ThreadPoolExecutor。
而 ThreadPoolExecutor 也是继承于 ExecutorService。
那么 最后执行的就是 ClingExecutor.submit(Runnable)。
submit 里面的参数类型是 Runnable。那么我们看看这个 Play 是不是一个 Runnable.

我们看一下这个 Play:

public abstract class Play extends ActionCallback {

    private static Logger log = Logger.getLogger(Play.class.getName());

    public Play(Service service) {
        this(new UnsignedIntegerFourBytes(0), service, "1");
    }

    public Play(Service service, String speed) {
        this(new UnsignedIntegerFourBytes(0), service, speed);
    }

    public Play(UnsignedIntegerFourBytes instanceId, Service service) {
        this(instanceId, service, "1");
    }

    public Play(UnsignedIntegerFourBytes instanceId, Service service, String speed) {
        super(new ActionInvocation(service.getAction("Play")));
        getActionInvocation().setInput("InstanceID", instanceId);
        getActionInvocation().setInput("Speed", speed);
    }

    @Override
    public void success(ActionInvocation invocation) {
        log.fine("Execution successful");
    }
}

Play 继承 ActionCallback 这个 ActionCallback 是什么?

service.getAction("Play")? 那么这个 Play 就是一个action了。

而 Play 继承 ActionCallback。 我们可以猜测,其它 action 也是继承 ActionCallback 的,那还有哪些操作呢?


ActionCallback的儿女们

这些类 跟 Play 的设计是一样的。

好了,我们看一下 ActionCallback:

public abstract class ActionCallback implements Runnable {
...
    protected final ActionInvocation actionInvocation;
    protected ControlPoint controlPoint;

    public void run() {
        Service service = actionInvocation.getAction().getService();

        // Local execution
        if (service instanceof LocalService) {
            LocalService localService = (LocalService)service;

            // Executor validates input inside the execute() call immediately
            localService.getExecutor(actionInvocation.getAction()).execute(actionInvocation);

            if (actionInvocation.getFailure() != null) {
                failure(actionInvocation, null);
            } else {
                success(actionInvocation);
            }

        // Remote execution
        } else if (service instanceof RemoteService){

            if (getControlPoint()  == null) {
                throw new IllegalStateException("Callback must be executed through ControlPoint");
            }

            RemoteService remoteService = (RemoteService)service;

            // Figure out the remote URL where we'd like to send the action request to
            URL controLURL;
            try {
                controLURL = remoteService.getDevice().normalizeURI(remoteService.getControlURI());
            } catch(IllegalArgumentException e) {
                failure(actionInvocation, null, "bad control URL: " + remoteService.getControlURI());
                return ;
            }

            // Do it
            SendingAction prot = getControlPoint().getProtocolFactory().createSendingAction(actionInvocation, controLURL);
            prot.run();

            IncomingActionResponseMessage response = prot.getOutputMessage();

            if (response == null) {
                failure(actionInvocation, null);
            } else if (response.getOperation().isFailed()) {
                failure(actionInvocation, response.getOperation());
            } else {
                success(actionInvocation);
            }
        }
    }
...
}

就如你所想到的, ActionCallback implements Runnable
在 run 方法里可看到,它是如何回调回来的。我们以 Remote execution 为例
首先获取远程设备的 url,就是远程设备地址,然后执行了这么一段代码:

SendingAction prot = getControlPoint().getProtocolFactory().createSendingAction(actionInvocation, controLURL);
            prot.run();

我怀疑这个就是发送指令的过程。

IncomingActionResponseMessage response = prot.getOutputMessage();

这段应该就是获取请求结果了,之后就回调成功失败。

我们看看那段是不是发送指令过程,
如果是,它是如何做到的?
getControlPoint() 无疑就是获取到控制点。
getProtocolFactory() 返回的是一个协议工厂。
听这个名字,是不是创建协议的东西?
我们继续...
...

getProtocolFactory().createSendingAction(actionInvocation, controLURL);

像这句,最后执行了 createSendingAction 的方法,我猜想应该是发送指令的终极方法。
ProtocolFactory 的实现类是 ProtocolFactoryImpl。
我们看看 createSendingAction 这个终极方法到底干了什么?

public SendingAction createSendingAction(ActionInvocation actionInvocation, URL controlURL) {
    return new SendingAction(getUpnpService(), actionInvocation, controlURL);
}

它创建了一个 SendingAction 这个名字就很直白.
继续进去 发现只是创建了它,赋了一些值给它,然后就没有然后呢
肿么回事?
噢? 我们再回去看看:

SendingAction prot = getControlPoint().getProtocolFactory().createSendingAction(actionInvocation, controLURL);
            prot.run();

这下明白了, run() 之后 开始执行了
SendingAction 里 没发现 run 方法

public class SendingAction extends SendingSync {
...
}

噢? 那我们看看 SendingSync
其实也木有 run 方法

public abstract class SendingSync extends SendingAsync {
...
}

哥们,闹够了没? 终于 run 方法出现了

public abstract class SendingAsync implements Runnable {
    public void run() {
        try {
            execute();
        } catch (Exception ex) {
            Throwable cause = Exceptions.unwrap(ex);
            if (cause instanceof InterruptedException) {
                log.log(Level.INFO, "Interrupted protocol '" + getClass().getSimpleName() + "': " + ex, cause);
            } else {
                throw new RuntimeException(
                    "Fatal error while executing protocol '" + getClass().getSimpleName() + "': " + ex, ex
                );
            }
        }
    }

    protected abstract void execute() throws RouterException;
}

这个 run 方法其实只是执行了 execute
哎,我们又得回到它的子类看实现了

execute 最后执行的是 SendingAction 的 executeSync()

protected IncomingActionResponseMessage executeSync() throws RouterException {
        return invokeRemote(getInputMessage());
    }

我们看看 invokeRemote (注意看代码中的注释)

protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessage requestMessage) throws RouterException {
        Device device = actionInvocation.getAction().getService().getDevice();

        log.fine("Sending outgoing action call '" + actionInvocation.getAction().getName() + "' to remote service of: " + device);
        IncomingActionResponseMessage responseMessage = null;
        try {
             // hello, 看这里 。。 这里就是重点
            StreamResponseMessage streamResponse = sendRemoteRequest(requestMessage);

            if (streamResponse == null) {
                log.fine("No connection or no no response received, returning null");
                actionInvocation.setFailure(new ActionException(ErrorCode.ACTION_FAILED, "Connection error or no response received"));
                return null;
            }
       
            responseMessage = new IncomingActionResponseMessage(streamResponse);

            if (responseMessage.isFailedNonRecoverable()) {
                log.fine("Response was a non-recoverable failure: " + responseMessage);
                throw new ActionException(
                        ErrorCode.ACTION_FAILED, "Non-recoverable remote execution failure: " + responseMessage.getOperation().getResponseDetails()
                );
            } else if (responseMessage.isFailedRecoverable()) {
                handleResponseFailure(responseMessage);
            } else {
                handleResponse(responseMessage);
            }

            return responseMessage;


        } catch (ActionException ex) {
            log.fine("Remote action invocation failed, returning Internal Server Error message: " + ex.getMessage());
            actionInvocation.setFailure(ex);
            if (responseMessage == null || !responseMessage.getOperation().isFailed()) {
                return new IncomingActionResponseMessage(new UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR));
            } else {
                return responseMessage;
            }
        }
    }

手机发送控制命令给投屏端的方法就是 sendRemoteRequest 它。。

protected StreamResponseMessage sendRemoteRequest(OutgoingActionRequestMessage requestMessage)
        throws ActionException, RouterException {

        try {
            log.fine("Writing SOAP request body of: " + requestMessage);
            getUpnpService().getConfiguration().getSoapActionProcessor().writeBody(requestMessage, actionInvocation);

            log.fine("Sending SOAP body of message as stream to remote device");
            return getUpnpService().getRouter().send(requestMessage);
        } catch (RouterException ex) {
            Throwable cause = Exceptions.unwrap(ex);
            if (cause instanceof InterruptedException) {
                if (log.isLoggable(Level.FINE)) {
                    log.fine("Sending action request message was interrupted: " + cause);
                }
                throw new ActionCancelledException((InterruptedException)cause);
            }
            throw ex;
        } catch (UnsupportedDataException ex) {
            if (log.isLoggable(Level.FINE)) {
                log.fine("Error writing SOAP body: " + ex);
                log.log(Level.FINE, "Exception root cause: ", Exceptions.unwrap(ex));
            }
            throw new ActionException(ErrorCode.ACTION_FAILED, "Error writing request message. " + ex.getMessage());
        }
    }

writeBody(requestMessage, actionInvocation)
看到没。 就在这里将控制命令写入进来了。
然后就是
getUpnpService().getRouter().send(requestMessage);
发送出去了。

流程就是这样的,有没有疑惑的地方呢?
知道这些又有何用?知道这些就能让我过好这一生吗?

哎,过好这一生 这么容易就好了 我还写什么文章啊。

来,说说你的故事

虽然无法确保能否过好这一生,但是有机会提升自己能力也是一件好事啊
duibudui
对不对..

我们先总结上面的内容,然后我分享阅读源码的方法以及 Cling 中的要点。

总结 控制设备

ControlPointImpl 是控制点的实现类,它有一个 execute 的方法,来执行控制命令。
执行 Play 其实就是执行控制点的 execute(ActionCallback callback) 这个方法
ActionCallback 它继承 Runnable,所有类似 Play 的指令都是继承它的。
这些指令最后的执行是 ActionCallback 的 run 方法;在 run 方法中执行了 SendingAction 的方法来发送指令
SendingAction 它也是继承 Runnable,它最后通过 sendRemoteRequest 这个方法完成指令的发送。

下面,我分享一下我阅读源码的方法。
我阅读源码都是从简单的入口开始入手,先跟随入口 一步一步往下走,不要在中途被其它东西打断,从入口开始就像游戏中的一条主线任务,了解完这个主线任务对你了解整个故事情节 都是有很大帮助的。
走完这段流程之后,你就有两个选择:要么继续其它的主线任务(其它执行入口),要么看支线任务(那些重要方法和重要类)。
其实这两种选择都是可以的,看你的权衡。

栗子?

以 Cling 为例:
我们刚刚走完 控制设备 的主流程,我们可以走 发现设备 的主流程(上上篇文章走过了)
我们还可以看一下这些主要的方法和类
比如:

  • 在 Cling 中协议是怎么创建的? 以及它是怎么跟其它部分连接起来的?
  • 那些 Service 有哪些?
  • 投屏端的回调有哪些内容?这些回调是怎么工作的?
  • 里面的路由 又是怎么回事?它在整个过程中起了什么作用?
    ...

有很多很多可以分析的地方,我们还可以在发现源码的优缺点,学习它设计得好的地方,有空还可以想想 是不是也存在有待提高的地方?,反正学无止境... 对吧...

下面是源码地址,啊啊啊啊 啊 明天又是周一了

点击查看源码

————2020年3月11日 更新
最近建了微信公众号和微博,由我(卷子)和我的好朋友(樱桃)两只小程序媛经营的。
我们都喜欢程序员这个呆萌的群体,我们希望能给你带来技术上的帮助以及生活上的快乐。嘿嘿~唯一的私心就是 希望你能喜欢我们咯。
大哥,我先敬你一瓶,先干为尽


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

推荐阅读更多精彩内容