美文网首页Android TV其他Android知识
android投屏技术🔥🔥🔥:控制设备源码分析

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

作者: 细卷子 | 来源:发表于2017-07-23 21:12 被阅读1086次
    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 有哪些?
    • 投屏端的回调有哪些内容?这些回调是怎么工作的?
    • 里面的路由 又是怎么回事?它在整个过程中起了什么作用?
      ...

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

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

    点击查看源码

    相关文章

      网友评论

      • 借风吹箫:你好,我在使用您的框架时发现,在使用天猫盒子投屏是LastChange属性,返回为空。无法解析当前视频播放状态,怎么回事那?跪求解。1134119088可以的话加我qq帮帮忙。谢谢
      • 昵称被谁抢了:赞一个,虽然没用这个......:joy:
        好好看好好学_392e:层主实现app投屏了吗,有资料什么的提供下吗。感谢:pray:
      • 星球上的那个人:写得多好啊,也对技术有探讨之心。虽然不知道能不能过好这一生。但我还是想问下,同学多大啦,有男朋友了吗。
      • deqiutseng:博主写的非常好,不过我很想了解 设备端(TV 如小米电视 上的投屏应用开发)实现的功能是把手机上的屏幕设放到TV 应用(自主开发)上,该如何做?
      • 95beac6ddcef:美滋滋:grin:
      • trayliu_小马过河:为啥不用乐播的?
        细卷子:@trayliu_小马过河 乐播好像不是开源的吧
        trayliu_小马过河:@细卷子 我们是做盒子的, amlogic 在内很多家投屏用的都是乐播
        细卷子:@trayliu_小马过河 乐播的很好用吗?
      • 6b336bc63c07:(看到“多好的人啊”,笑出声来)对啊!多好的姐姐啊!先喜欢+评论再看!xD
        细卷子:@AnnaClancy :kissing_heart::kissing_heart:
      • 2d9566ce406b:作者这几篇文章,逻辑清晰,给人感觉眼前一亮的清新。
        细卷子:@剑客0707 真的吗? 谢谢你哦。我也会写出更好的文章的:smile::smile:
      • 296d6ebc6cd2:(多好的人啊,肯定一生美好)。:innocent:
        细卷子:@颜路 谢谢大佬:beer:

      本文标题:android投屏技术🔥🔥🔥:控制设备源码分析

      本文链接:https://www.haomeiwen.com/subject/wapwcxtx.html