跳芭蕾的小丑 發表於 2023-2-4 16:55:00

打造自己的智能投屏体验——Android投屏开发入门

<div>
<h2>前言</h2>
<p>目前音视频领域的应用已涉及到众多领域,而其中投屏功能,成为很多家庭影院、会议观看等的高频使用场景。为了尽享大屏体验,不妨自己来实现一个投屏功能</p>
<h2>1、投屏原理简介</h2>
<p>投屏,就是将一台设备上的媒体内容,通过一定的技术方案,在另外一台设备上显示。其中这个一定的技术方案,主要分为两种:</p>
<ul>
<li>推送模式:主要用于投屏音视频。投屏之后手机可以关闭,电视(接收端)不会停止播放。核心原理就是通过指定协议,类似于蓝牙那样搜索匹配,并将<strong>音视频的播放地址</strong>传输过去,然后接收端播放这个地址的流媒体。常见的投屏协议有DLNA、Airplay。</li>
<li>镜像模式:所谓的镜像就是同屏,把手机(发送端)的<strong>屏幕内容</strong>同步传输到电视(接收端)上显示。核心原理就是一边录屏一遍发送给电视同步播放。常见协议有Miracast、Airplay。</li>
</ul>
<p>一般来说我们常用的投屏是推送模式,本文主要也是介绍这个。</p>
<h3>1.1 投屏协议——DLNA</h3>
<p>DLNA代表“数字生活网络联盟”。DLNA使用通用即插即用(UPnP)协议。DLNA并不是真正的无线显示解决方案。相反,它只是一种在一个设备上获取内容并在另一台设备上播放内容的方法。也就是说他不是真正的投屏技术。</p>
<p>我们手机上爱奇艺APP、腾讯视频APP,在打开视频后,右上角有一个【TV】的小图标,你点击这个小图标,就会弹出“正在搜寻可投屏设备”,将会显示同一个Wi-Fi网络下能够发现的投屏设备,选择投屏的电视机后,电视机就会播放对应的视频。这里有一个注意点,就是当你在手机上是VIP会员时,你要想将VIP视频通过DLNA投屏到智能电视上时,是没法投屏的,因为爱奇艺或者腾讯将限制这种操作,避免手机VIP用户通过投屏来实现电视机播放VIP视频,原因就是DLNA协议要求最终还是需要智能电视自己去视频服务器获取视频,视频服务器可能会在流媒体地址的获取/解析等做限制。</p>
<h3>1.2 投屏协议 —— Miracast</h3>
<p>Miracast是Wi-Fi联盟制定的Wi-Fi投屏行业标准,实质上是对Apple AirPlay的回应。Miracast支持内置在<strong>Android 4.2+</strong>和<strong>Windows 8.1、Windows 10</strong>。允许Android智能手机、Windows平板电脑和笔记本电脑以及其他设备以无线方式传输到兼容Miracast的接收器比如智能电视、平板电脑等。当前已经有<strong>很多电视盒子都支持Miracast协议,比如小米盒子、荣耀盒子等等,小米手机、华为的手机也都支持Miracast协议,配合小米盒子、荣耀盒子即可实现投屏。</strong></p>
<p>各品牌设备该功能名称可能不同,比如:无线显示、屏幕共享、多屏互动、Screen Mirroring等。可以看这个乐播关于设备的入口收集部分:https://www.lebo.cn/news/AboutNewsContent?id=667</p>
<p>Miracast相比AirPlay来讲,有缺点也有优点,优点在于:</p>
<ol>
<li>内置在Andorid和Windows中,不要求必须是苹果的终端设备。</li>
<li>Miracast可以在没有无线路由器的时候也能很好的工作,也就是说手机可以直接通过Wi-Fi连接到电视的Wi-Fi网卡上进行投屏(Wi-Fi Direct技术),在没有无线路由器的时候是比较方便的。</li>
</ol>
<p>缺点在于:</p>
<ol>
<li><strong>只支持屏幕镜像模式投屏</strong>,<strong>而不支持流模式的投屏</strong>。当你在投屏的时候手机整个屏幕(包括状态栏等)会复制到电视机上,并且要始终保持手机屏幕是处于播放和显示状态。苹果的AirPlay则可以允许你在手机上一�边浏览网页,一边通过电视播放手机中的视频。</li>
<li>Miracast毕竟是一种行业标准,各个厂家实现良莠不齐,不同设备之间投屏可能出现体验不佳的问题。</li>
</ol>
<p>另一个问题是该标准不要求设备必须带有“ Miracast”品牌的商标。制造商已将其Miracast实现称为其他东西。例如,LG称其Miracast支持为“ SmartShare”,三星称其为“ AllShare Cast”,索尼称其为“屏幕镜像”,而松下称其为“显示镜像”。</p>
<h3>1.3 投屏协议 ——Airplay</h3>
<p>AIrplay协议是苹果的协议,主要局限在仅适用于 Apple 设备,我们在这里不做展开。</p>
<h3>1.4 投屏协议 —— 其他第三方</h3>
<p>最后就是很多专门投屏的投屏APP,这些APP要么是实现了上面几种协议,要么是自己实现一套私有协议。手机和智能电视都要安装这些APP,否则无法投屏。而前面几个协议都是标准协议,操作系统内置,无需安装。比较著名的投屏APP有乐播投屏、APowerMirror等,使用都很方便,一般是通过扫描智能电视显示的二维码来实现投屏到特定电视机上。这些投屏APP的另外一个好处就是:<strong>不局限在同一个局域网内,可以跨三层网络、甚至广域网</strong>。</p>
<hr>
<h2>2. 投屏功能开发实践</h2>
<p>在这里我们选择用来保利威的官方Demo为例,之所以用它为例,是因为他也是一家视频提供商,并且提供了视频加密服务,也就是说,他可以做到提供主流视频厂商那样的VIP视频服务,并且其允许投屏。我们可以查看官方文档,借此探究Android投屏的开发实现。其基本都封装好了,我们可以复制过来改改就能应用到自己项目上,也可以参考实现。</p>
<h3>2.1 投屏环境搭建和体验</h3>
<p><strong>1、注册第三方投屏SDK(可选)</strong><br>
第三方SDK往往和电视厂家有一定的合作,会内置支持,或者提供对应的电视端APP,可以拥有更良好的投屏体验。如果要自己实现投屏的话,还需要对实现协议对接,甚至还要开发对应的接收端APP,工作量上就大了不少。由于保利威的demo投屏功能是基于乐播的,如果需要集成到自己项目上,就需要在乐播上注册绑定包名生成key。当然我们直接运行demo,里面就内置了对应的key,体验的话可以忽略这一步。</p>
<p><strong>2、 准备两台Android手机</strong><br>
因为开发者未必有电视,可以用另外一台Android设备充当电视接收端。接收端需要安装乐播的apk,乐播apk在安卓应用市场就能找到,如果应用市场没有,也可以去乐播官网进行下载乐播投屏电视版。</p>
<p><strong>3、下载Demo工程</strong><br>
本文是基于Github Demo项目讲解,所以可以直接下载他们的Github项目运行体验。下载地址:https://github.com/easefun/polyv-android-sdk-2.0-demo<br>
Demo中默认隐藏了投屏按钮,如果要体验的话,需要在<code>PolyvPlayerActivity</code>中取消注释</p>

</div>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">投屏功能默认隐藏,如果需要请注释下面两行代码
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">      iv_screencast_search.setVisibility(View.GONE);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">      iv_screencast_search_land.setVisibility(View.GONE);</span></pre>
</div>
<div>
<div>
<p>然后,我们将两台手机(发送端和接收端),分别打开对应的APP,将其置于同一个wifi(局域网)之下,就可以开始投屏了。</p>
<h3>2.2 投屏开发浅析</h3>
<p>我们可以看下Demo结构中的投屏模块,其中widget是关于UI层的实现,主要是<code>PolyvScreencastManager</code>,封装了投屏功能。我们主要就是聚焦这个的实现。</p>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">com/easefun/polyvsdk/cast</span>
├── PolyvAllCast.java <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">乐播投屏二次封装类</span>
├── PolyvIUIUpdateListener.java <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 封装的投屏状态回调监听器</span>
├── PolyvScreencastManager.java <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">投屏封装工具类,操作投屏功能使用。等同于旧版的PolyvScreencastHelper</span>
<span style="color: rgba(0, 0, 0, 1)">└── widget
    ├── PolyvScreencastSearchLayout.java </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">搜索投屏Layout</span>
    └── PolyvScreencastStatusLayout.java <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">投屏状态管理Layout</span></pre>
</div>
<div>
<div>
<h4>2.2.1 初始化</h4>
<p>从官方文档中可以知道初始化要设置AppSecret。这是乐播提供的服务中,把投屏sdk与包名绑定了,如果更换了包名我们就要重新注册,否则包名错误就会导致校验失败。然后会因此无法搜索到设备。</p>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">appId和appSecret需与包名绑定,获取方式请到乐播官网注册获取</span>
PolyvScreencastManager.init(&lt;AppId&gt;, &lt;AppSecret&gt;<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">初始化单例</span>
PolyvScreencastManager.getInstance(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">PolyvAllCast.java</span>
    <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> initLelinkService(Context context, String appid, String appSecret) {
      LelinkSourceSDK.getInstance()
                .setBindSdkListener(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> IBindSdkListener() {
                  @Override
                  </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onBindCallback(boolean result) {
                        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">绑定,并且返回绑定的结果</span>
                        Log.e(TAG, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Polyv Cast SDK Init Result :</span><span style="color: rgba(128, 0, 0, 1)">"</span> +<span style="color: rgba(0, 0, 0, 1)"> result);
                        </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (result) {
                            LelinkSourceSDK.getInstance().setOption(IAPI.OPTION_5, </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);
                            LelinkSourceSDK.getInstance().setDebugMode(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
                            LelinkSourceSDK.getInstance().enableLogCache(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
                        }
                  }
                })
                .setSdkInitInfo(context, appid, appSecret)
                .bindSdk();
    }</span></pre>
</div>
<div>
<div>
<p>我们可以看见在<code>PolyvAllCast</code>中进行了投屏SDK的初始化与绑定服务,并且返回了结果。这种绑定服务往往依赖于网络,最好就在Application中就进行初始化,避免网络延迟导致投屏服务异常。</p>
<h4>2.2.2 三大回调监听</h4>
<p>如果有蓝牙开发经验的,其实可以很容易理解,这和蓝牙的搜索匹配以及数据传输流程,基本一致。投屏模块的核心实现在于把握投屏的最基本的几个流程:<strong>初始化 → 搜索设备 →(返回设备列表)→ 连接设备 → (返回连接监听) → 开始投屏 → (返回投屏状态回调) → 停止投屏 → 断开连接。</strong><br>
初始化在前面已经提到过了,然后就是基本的三大回调:</p>
<ul>
<li>搜索设备回调</li>
<li>连接状态回调</li>
<li>投屏状态回调</li>


</ul>
<p>在<code>PolyvScreencastManager</code>中已经实现三大回调监听,然后经过转发到<code>PolyvIUIUpdateListener</code>,把这三大回调状态分割成不同的状态码,回调到上层。</p>

</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)"> PolyvIUIUpdateListener {

    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_SEARCH_SUCCESS = <span style="color: rgba(128, 0, 128, 1)">1</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">搜索成功</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_SEARCH_ERROR = <span style="color: rgba(128, 0, 128, 1)">2</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">搜索失败</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_SEARCH_NO_RESULT = <span style="color: rgba(128, 0, 128, 1)">3</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">搜索设备无结果</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_CONNECT_SUCCESS = <span style="color: rgba(128, 0, 128, 1)">10</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">链接成功</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_DISCONNECT = <span style="color: rgba(128, 0, 128, 1)">11</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 连接断开</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_CONNECT_FAILURE = <span style="color: rgba(128, 0, 128, 1)">12</span>;<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 连接失败
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">下面是投屏状态,如播放暂停完成等</span>
    <span style="color: rgba(0, 0, 255, 1)">int</span> STATE_PLAY = <span style="color: rgba(128, 0, 128, 1)">20</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_PAUSE = <span style="color: rgba(128, 0, 128, 1)">21</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_COMPLETION = <span style="color: rgba(128, 0, 128, 1)">22</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_STOP = <span style="color: rgba(128, 0, 128, 1)">23</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_SEEK = <span style="color: rgba(128, 0, 128, 1)">24</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_POSITION_UPDATE = <span style="color: rgba(128, 0, 128, 1)">25</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_PLAY_ERROR = <span style="color: rgba(128, 0, 128, 1)">26</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_LOADING = <span style="color: rgba(128, 0, 128, 1)">27</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> STATE_INPUT_SCREENCODE = <span style="color: rgba(128, 0, 128, 1)">28</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">int</span> RELEVANCE_DATA_UNSUPPORT = <span style="color: rgba(128, 0, 128, 1)">29</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 0, 255, 1)">void</span> onUpdateState(<span style="color: rgba(0, 0, 255, 1)">int</span> state, Object <span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onUpdateText(String msg);
}</span></pre>
</div>
<div>
<div>在调用<code>screencastManager.browse/stopBrowse ()</code>时候就会开始/停止搜索。我们可以看一下搜索设备回调,返回了 resultCode,是里面解析了各个 resultCode 对应的含义,并将其通过 handler 回调到主线程。包括<code>IConnectListener</code>、<code>ILelinkPlayerListener</code> 也是如此,这两个就不作展开。</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">PolyvScreencastManager.java</span><span style="color: rgba(0, 128, 0, 1)">
/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * 投屏搜索监听
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
    <span style="color: rgba(0, 0, 255, 1)">private</span> IBrowseListener mBrowseListener = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> IBrowseListener() {

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> onBrowse(<span style="color: rgba(0, 0, 255, 1)">int</span> resultCode, List&lt;LelinkServiceInfo&gt;<span style="color: rgba(0, 0, 0, 1)"> list) {
            PolyvCommonLog.d(TAG, </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">onSuccess size:</span><span style="color: rgba(128, 0, 0, 1)">"</span> + (list == <span style="color: rgba(0, 0, 255, 1)">null</span> ? <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> : list.size()));
            mInfos </span>=<span style="color: rgba(0, 0, 0, 1)"> list;
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (resultCode ==<span style="color: rgba(0, 0, 0, 1)"> IBrowseListener.BROWSE_SUCCESS) {
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">...</span>
                  <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">null</span> !=<span style="color: rgba(0, 0, 0, 1)"> mUIHandler) {
                        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 发送文本信息</span>
<span style="color: rgba(0, 0, 0, 1)">                        mUIHandler.sendMessage(buildTextMessage(buffer.toString()));
                        </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (mInfos.isEmpty()) {
                            mUIHandler.sendMessage(buildStateMessage(PolyvIUIUpdateListener.STATE_SEARCH_NO_RESULT));
                        } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                            mUIHandler.sendMessage(buildStateMessage(PolyvIUIUpdateListener.STATE_SEARCH_SUCCESS));
                        }
                  }
                }
            } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span>(resultCode ==<span style="color: rgba(0, 0, 0, 1)"> IBrowseListener.BROWSE_STOP){
                  </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
                }
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">null</span> !=<span style="color: rgba(0, 0, 0, 1)"> mUIHandler) {
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 发送文本信息</span>
                  PolyvCommonLog.d(TAG, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">browse error:Auth error</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                  String text </span>= <span style="color: rgba(128, 0, 0, 1)">""</span><span style="color: rgba(0, 0, 0, 1)">;
                  </span><span style="color: rgba(0, 0, 255, 1)">if</span>(resultCode ==<span style="color: rgba(0, 0, 0, 1)"> IBrowseListener.BROWSE_ERROR_AUTH){
                        text </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">授权失败</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
                  } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span>(resultCode ==<span style="color: rgba(0, 0, 0, 1)"> IBrowseListener.BROWSE_ERROR_AUTH_TIME){
                        text </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">授权失败次数超限</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
                  } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                        text </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">搜索错误</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
                  }
                  mUIHandler.sendMessage(buildTextMessage(text));
                  mUIHandler.sendMessage(buildStateMessage(PolyvIUIUpdateListener.STATE_SEARCH_ERROR));
                }
            }

      }

    };

    </span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * 投屏连接状态监听
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
    <span style="color: rgba(0, 0, 255, 1)">private</span> IConnectListener mConnectListener = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> IConnectListener() {

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> onConnect(final LelinkServiceInfo serviceInfo, final <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> extra) {
      }

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> onDisconnect(LelinkServiceInfo serviceInfo, <span style="color: rgba(0, 0, 255, 1)">int</span> what, <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> extra) {
      }
    };


</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
   * 投屏播放监听
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
    <span style="color: rgba(0, 0, 255, 1)">private</span> ILelinkPlayerListener mPlayerListener = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ILelinkPlayerListener() {

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onLoading() {
      }

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onStart() {
      }

      @Override
      </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onPause() {
      }

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">。。。省略</span>
};</pre>
</div>
<div>
<div>
<h4>2.2.3 视频播放</h4>
<p>播放视频投屏主要是通过<code>playNetMedia</code>方法。其中通过<code>setType</code>和<code>setUrl</code>设置流媒体的类型和流媒体地址。从下面的代码中就可以看出其投屏的API,实际上是推送模式,把流媒体地址传输到接收端去解析播放的。</p>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">PolyvScreencastManager.java</span>
    <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> playNetMedia(LelinkPlayerInfo lelinkPlayerInfo, String playPath, <span style="color: rgba(0, 0, 255, 1)">int</span> type, String screenCode, <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> seconds) {
      currentPlayPath </span>=<span style="color: rgba(0, 0, 0, 1)"> playPath;
      lelinkPlayerInfo.setType(type);
      lelinkPlayerInfo.setUrl(playPath);
      String userAgent </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">PolyvAndroidScreencast-lelink</span><span style="color: rgba(128, 0, 0, 1)">"</span> +<span style="color: rgba(0, 0, 0, 1)"> BuildConfig.VERSION_NAME;
      lelinkPlayerInfo.setHeader(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">{\"user-agent\":\" </span><span style="color: rgba(128, 0, 0, 1)">"</span> + userAgent + <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">\"}</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      lelinkPlayerInfo.setLoopMode(LelinkPlayerInfo.LOOP_MODE_DEFAULT);
      lelinkPlayerInfo.setOption(IAPI.OPTION_6, screenCode);
      lelinkPlayerInfo.setStartPosition(seconds);
      mAllCast.playNetMediaWithHeader(lelinkPlayerInfo);
    }</span></pre>
</div>
<div>
<div>
<p>在Demo中,他还设置了Header,一般可以通过这个来设置参数,如<code>user-agent</code>等,可以借此跟踪发送端设备参数。但是这个<code>LelinkPlayerInfo</code> 是乐播提供的实体,这种Header的设置,只能在乐联协议中生效!而在DLNA连接中其实是无法生效的。更好的解决方案,是通过playPath中追加参数。</p>
<blockquote>
<p>乐联协议:也就是上面说的第三方投屏协议,这是乐播自己修改兼容的投屏协议。</p>
</blockquote>
<hr>
<p>以上基本就是<code>PolyvScreencastManager</code>提供的投屏封装的功能了,关于基本使用可以查看文档。</p>
<h3>3. 加密视频投屏</h3>
<p>前面有说到,很多视频站都有VIP视频,这些视频往往投屏之后也不支持解析播放。那保利威中的加密视频是如何做到投屏播放的呢?<br>
我们可以追查到<code>PolyvScreencastSearchLayout#loadInfoAndPlay</code>方法,它通过一个<code>PolyvScreencastHelper.getInstance().transformPlayObject()</code>方法,将<code>LelinkPlayerInfo</code>对象转化成为了支持解密的对象。</p>

</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">PolyvScreencastSearchLayout#loadInfoAndPlay</span>
            LelinkPlayerInfo lelinkPlayerInfo = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> LelinkPlayerInfo();
            PolyvScreencastHelper.getInstance().transformPlayObject(lelinkPlayerInfo, video,
                        bitrate, playPath, </span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> PolyvScreencastHelper.PolyvCastTransformCallback() {
                            @Override
                            </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> onSucceed(Object <span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)">, String newPlayPath) {
                              PolyvCommonLog.d(TAG, </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">cast: </span><span style="color: rgba(128, 0, 0, 1)">"</span> +<span style="color: rgba(0, 0, 0, 1)"> newPlayPath);
                              </span><span style="color: rgba(0, 0, 255, 1)">int</span> videoPosition =<span style="color: rgba(0, 0, 0, 1)"> screencastStatusLayout.getCurrentPlayPosition();
                              play((LelinkPlayerInfo) </span><span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)">, newPlayPath, bitrate, videoPosition);
                              screencastStatusLayout.resetBitRateView(bitrate);
                            }

                            @Override
                            </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> onFailed(Throwable e) {
                            }
                        });


            }
      });</span></pre>
</div>
<p>无疑这个也是通过乐联协议去匹配的。乐播的文档中也提供了接口</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">lelinkPlayerInfo.setAesKey(String key)
lelinkPlayerInfo.setAesIv(String iv)</span></pre>
</div>
<div>
<div>
<p>那么一个加密视频的播放,往往就需要在发送端和接收端都要去做一定的兼容,或者直接使用这种第三方SDK提供接口,通过约定的加解密协议方式,去对视频进行解密,然后再渲染播放。</p>
<h3>4. 扩展:WifiDisplay介绍</h3>
<p>前面说到的投屏分为Miracast和DLNA。上面一直说的都是DLNA的推送模式,那么关于镜像模式又是如何实现的呢?<br>
镜像投屏,核心技术原理和WifiDisplay有关。它的本质和DLNA其实相似,都是建立连接然后传输数据,只是这里的数据是屏幕数据,数据量和交互都比较大,所以形成了一套协议方案。</p>
<p>WifiDIsplay涉及的技术和协议比较多,包括了WIFI P2P技术、RTSP及RTP技术、流媒体技术以及音视频编解码相关的技术等等。</p>
<p>我们可以通过 Wi-Fi 直连创建点对点连接,然后在这个基础上从传输文件到传输音视频。大致的原理就是将Source端(发送端)的音视频数据投屏到Sink端(接收端)。</p>
<p>以上可以简单拆分为几个过程:<br>
1、设备(Source/Sink端)发现彼此:实际上属于P2P Device的彼此发现(涉及到WifiP2pManager.discoverPeers + (WifiP2pService))<br>
2、发起connect:WFD支持能力的协商,如双方设备支持的分辨率,还有会话的建立(WFD Capalibility Negotiation + WFD Session Establish)<br>
3、Source端的Vedio/Audio 的capture 以及 encoder:(涉及到 Vedio Driver + Audio driver + Audio Capture)<br>
4、上面的音视频数据从Soure端到Sink端。</p>
<p>看似做起来有点麻烦,那么能使用第三方的SDK吗?可以的,就是乐播他们要收费,所以不做探究。</p>

</div>

<br>-----------------------------------------------------------------------------------------------------------------------------------------------------------<br>作者:白帽子耗子<br>链接:https://www.jianshu.com/p/1765480fb759<br>来源:简书<br>著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</div>
</div>
</div>
</div>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/Im-Victor/p/17091901.html
頁: [1]
查看完整版本: 打造自己的智能投屏体验——Android投屏开发入门