前言

目前音视频领域的应用已涉及到众多领域,而其中投屏功能,成为很多家庭影院、会议观看等的高频使用场景。为了尽享大屏体验,不妨自己来实现一个投屏功能

1、投屏原理简介

投屏,就是将一台设备上的媒体内容,通过一定的技术方案,在另外一台设备上显示。其中这个一定的技术方案,主要分为两种:

  • 推送模式:主要用于投屏音视频。投屏之后手机可以关闭,电视(接收端)不会停止播放。核心原理就是通过指定协议,类似于蓝牙那样搜索匹配,并将音视频的播放地址传输过去,然后接收端播放这个地址的流媒体。常见的投屏协议有DLNA、Airplay。
  • 镜像模式:所谓的镜像就是同屏,把手机(发送端)的屏幕内容同步传输到电视(接收端)上显示。核心原理就是一边录屏一遍发送给电视同步播放。常见协议有Miracast、Airplay。

一般来说我们常用的投屏是推送模式,本文主要也是介绍这个。

1.1 投屏协议——DLNA

DLNA代表“数字生活网络联盟”。DLNA使用通用即插即用(UPnP)协议。DLNA并不是真正的无线显示解决方案。相反,它只是一种在一个设备上获取内容并在另一台设备上播放内容的方法。也就是说他不是真正的投屏技术。

我们手机上爱奇艺APP、腾讯视频APP,在打开视频后,右上角有一个【TV】的小图标,你点击这个小图标,就会弹出“正在搜寻可投屏设备”,将会显示同一个Wi-Fi网络下能够发现的投屏设备,选择投屏的电视机后,电视机就会播放对应的视频。这里有一个注意点,就是当你在手机上是VIP会员时,你要想将VIP视频通过DLNA投屏到智能电视上时,是没法投屏的,因为爱奇艺或者腾讯将限制这种操作,避免手机VIP用户通过投屏来实现电视机播放VIP视频,原因就是DLNA协议要求最终还是需要智能电视自己去视频服务器获取视频,视频服务器可能会在流媒体地址的获取/解析等做限制。

1.2 投屏协议 —— Miracast

Miracast是Wi-Fi联盟制定的Wi-Fi投屏行业标准,实质上是对Apple AirPlay的回应。Miracast支持内置在Android 4.2+Windows 8.1、Windows 10。允许Android智能手机、Windows平板电脑和笔记本电脑以及其他设备以无线方式传输到兼容Miracast的接收器比如智能电视、平板电脑等。当前已经有很多电视盒子都支持Miracast协议,比如小米盒子、荣耀盒子等等,小米手机、华为的手机也都支持Miracast协议,配合小米盒子、荣耀盒子即可实现投屏。

各品牌设备该功能名称可能不同,比如:无线显示、屏幕共享、多屏互动、Screen Mirroring等。可以看这个乐播关于设备的入口收集部分:https://www.lebo.cn/news/AboutNewsContent?id=667

Miracast相比AirPlay来讲,有缺点也有优点,优点在于:

  1. 内置在Andorid和Windows中,不要求必须是苹果的终端设备。
  2. Miracast可以在没有无线路由器的时候也能很好的工作,也就是说手机可以直接通过Wi-Fi连接到电视的Wi-Fi网卡上进行投屏(Wi-Fi Direct技术),在没有无线路由器的时候是比较方便的。

缺点在于:

  1. 只支持屏幕镜像模式投屏而不支持流模式的投屏。当你在投屏的时候手机整个屏幕(包括状态栏等)会复制到电视机上,并且要始终保持手机屏幕是处于播放和显示状态。苹果的AirPlay则可以允许你在手机上一�边浏览网页,一边通过电视播放手机中的视频。
  2. Miracast毕竟是一种行业标准,各个厂家实现良莠不齐,不同设备之间投屏可能出现体验不佳的问题。

另一个问题是该标准不要求设备必须带有“ Miracast”品牌的商标。制造商已将其Miracast实现称为其他东西。例如,LG称其Miracast支持为“ SmartShare”,三星称其为“ AllShare Cast”,索尼称其为“屏幕镜像”,而松下称其为“显示镜像”。

1.3 投屏协议 ——Airplay

AIrplay协议是苹果的协议,主要局限在仅适用于 Apple 设备,我们在这里不做展开。

1.4 投屏协议 —— 其他第三方

最后就是很多专门投屏的投屏APP,这些APP要么是实现了上面几种协议,要么是自己实现一套私有协议。手机和智能电视都要安装这些APP,否则无法投屏。而前面几个协议都是标准协议,操作系统内置,无需安装。比较著名的投屏APP有乐播投屏、APowerMirror等,使用都很方便,一般是通过扫描智能电视显示的二维码来实现投屏到特定电视机上。这些投屏APP的另外一个好处就是:不局限在同一个局域网内,可以跨三层网络、甚至广域网


2. 投屏功能开发实践

在这里我们选择用来保利威的官方Demo为例,之所以用它为例,是因为他也是一家视频提供商,并且提供了视频加密服务,也就是说,他可以做到提供主流视频厂商那样的VIP视频服务,并且其允许投屏。我们可以查看官方文档,借此探究Android投屏的开发实现。其基本都封装好了,我们可以复制过来改改就能应用到自己项目上,也可以参考实现。

2.1 投屏环境搭建和体验

1、注册第三方投屏SDK(可选)
第三方SDK往往和电视厂家有一定的合作,会内置支持,或者提供对应的电视端APP,可以拥有更良好的投屏体验。如果要自己实现投屏的话,还需要对实现协议对接,甚至还要开发对应的接收端APP,工作量上就大了不少。由于保利威的demo投屏功能是基于乐播的,如果需要集成到自己项目上,就需要在乐播上注册绑定包名生成key。当然我们直接运行demo,里面就内置了对应的key,体验的话可以忽略这一步。

2、 准备两台Android手机
因为开发者未必有电视,可以用另外一台Android设备充当电视接收端。接收端需要安装乐播的apk,乐播apk在安卓应用市场就能找到,如果应用市场没有,也可以去乐播官网进行下载乐播投屏电视版。

3、下载Demo工程
本文是基于Github Demo项目讲解,所以可以直接下载他们的Github项目运行体验。下载地址:https://github.com/easefun/polyv-android-sdk-2.0-demo
Demo中默认隐藏了投屏按钮,如果要体验的话,需要在PolyvPlayerActivity中取消注释

        //投屏功能默认隐藏,如果需要请注释下面两行代码
//        iv_screencast_search.setVisibility(View.GONE);
//        iv_screencast_search_land.setVisibility(View.GONE);

然后,我们将两台手机(发送端和接收端),分别打开对应的APP,将其置于同一个wifi(局域网)之下,就可以开始投屏了。

2.2 投屏开发浅析

我们可以看下Demo结构中的投屏模块,其中widget是关于UI层的实现,主要是PolyvScreencastManager,封装了投屏功能。我们主要就是聚焦这个的实现。

//com/easefun/polyvsdk/cast
├── PolyvAllCast.java //乐播投屏二次封装类
├── PolyvIUIUpdateListener.java // 封装的投屏状态回调监听器
├── PolyvScreencastManager.java //投屏封装工具类,操作投屏功能使用。等同于旧版的PolyvScreencastHelper
└── widget
    ├── PolyvScreencastSearchLayout.java //搜索投屏Layout
    └── PolyvScreencastStatusLayout.java //投屏状态管理Layout

2.2.1 初始化

从官方文档中可以知道初始化要设置AppSecret。这是乐播提供的服务中,把投屏sdk与包名绑定了,如果更换了包名我们就要重新注册,否则包名错误就会导致校验失败。然后会因此无法搜索到设备。

//appId和appSecret需与包名绑定,获取方式请到乐播官网注册获取
PolyvScreencastManager.init(<AppId>, <AppSecret>);
//初始化单例
PolyvScreencastManager.getInstance(this);

//PolyvAllCast.java
    private void initLelinkService(Context context, String appid, String appSecret) {
        LelinkSourceSDK.getInstance()
                .setBindSdkListener(new IBindSdkListener() {
                    @Override
                    public void onBindCallback(boolean result) {
                        //绑定,并且返回绑定的结果
                        Log.e(TAG, "Polyv Cast SDK Init Result :" + result);
                        if (result) {
                            LelinkSourceSDK.getInstance().setOption(IAPI.OPTION_5, false);
                            LelinkSourceSDK.getInstance().setDebugMode(true);
                            LelinkSourceSDK.getInstance().enableLogCache(true);
                        }
                    }
                })
                .setSdkInitInfo(context, appid, appSecret)
                .bindSdk();
    }

我们可以看见在PolyvAllCast中进行了投屏SDK的初始化与绑定服务,并且返回了结果。这种绑定服务往往依赖于网络,最好就在Application中就进行初始化,避免网络延迟导致投屏服务异常。

2.2.2 三大回调监听

如果有蓝牙开发经验的,其实可以很容易理解,这和蓝牙的搜索匹配以及数据传输流程,基本一致。投屏模块的核心实现在于把握投屏的最基本的几个流程:初始化 → 搜索设备 →(返回设备列表)→ 连接设备 → (返回连接监听) → 开始投屏 → (返回投屏状态回调) → 停止投屏 → 断开连接。
初始化在前面已经提到过了,然后就是基本的三大回调:

  • 搜索设备回调
  • 连接状态回调
  • 投屏状态回调

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

public interface PolyvIUIUpdateListener {

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

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

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

        }

    };

    /**
     * 投屏连接状态监听
     */
    private IConnectListener mConnectListener = new IConnectListener() {

        @Override
        public void onConnect(final LelinkServiceInfo serviceInfo, final int extra) {
        }

        @Override
        public void onDisconnect(LelinkServiceInfo serviceInfo, int what, int extra) {
        }
    };


 /**
     * 投屏播放监听
     */
    private ILelinkPlayerListener mPlayerListener = new ILelinkPlayerListener() {

        @Override
        public void onLoading() {
        }

        @Override
        public void onStart() {
        }

        @Override
        public void onPause() {
        }

    //。。。省略
  };

2.2.3 视频播放

播放视频投屏主要是通过playNetMedia方法。其中通过setTypesetUrl设置流媒体的类型和流媒体地址。从下面的代码中就可以看出其投屏的API,实际上是推送模式,把流媒体地址传输到接收端去解析播放的。

//PolyvScreencastManager.java
    public void playNetMedia(LelinkPlayerInfo lelinkPlayerInfo, String playPath, int type, String screenCode, int seconds) {
        currentPlayPath = playPath;
        lelinkPlayerInfo.setType(type);
        lelinkPlayerInfo.setUrl(playPath);
        String userAgent = "PolyvAndroidScreencast-lelink" + BuildConfig.VERSION_NAME;
        lelinkPlayerInfo.setHeader("{\"user-agent\":\" " + userAgent + "\"}");
        lelinkPlayerInfo.setLoopMode(LelinkPlayerInfo.LOOP_MODE_DEFAULT);
        lelinkPlayerInfo.setOption(IAPI.OPTION_6, screenCode);
        lelinkPlayerInfo.setStartPosition(seconds);
        mAllCast.playNetMediaWithHeader(lelinkPlayerInfo);
    }

在Demo中,他还设置了Header,一般可以通过这个来设置参数,如user-agent等,可以借此跟踪发送端设备参数。但是这个LelinkPlayerInfo 是乐播提供的实体,这种Header的设置,只能在乐联协议中生效!而在DLNA连接中其实是无法生效的。更好的解决方案,是通过playPath中追加参数。

乐联协议:也就是上面说的第三方投屏协议,这是乐播自己修改兼容的投屏协议。


以上基本就是PolyvScreencastManager提供的投屏封装的功能了,关于基本使用可以查看文档

3. 加密视频投屏

前面有说到,很多视频站都有VIP视频,这些视频往往投屏之后也不支持解析播放。那保利威中的加密视频是如何做到投屏播放的呢?
我们可以追查到PolyvScreencastSearchLayout#loadInfoAndPlay方法,它通过一个PolyvScreencastHelper.getInstance().transformPlayObject()方法,将LelinkPlayerInfo对象转化成为了支持解密的对象。

//PolyvScreencastSearchLayout#loadInfoAndPlay
            LelinkPlayerInfo lelinkPlayerInfo = new LelinkPlayerInfo();
            PolyvScreencastHelper.getInstance().transformPlayObject(lelinkPlayerInfo, video,
                        bitrate, playPath, new PolyvScreencastHelper.PolyvCastTransformCallback() {
                            @Override
                            public void onSucceed(Object object, String newPlayPath) {
                                PolyvCommonLog.d(TAG, "cast: " + newPlayPath);
                                int videoPosition = screencastStatusLayout.getCurrentPlayPosition();
                                play((LelinkPlayerInfo) object, newPlayPath, bitrate, videoPosition);
                                screencastStatusLayout.resetBitRateView(bitrate);
                            }

                            @Override
                            public void onFailed(Throwable e) {
                            }
                        });


            }
        });

无疑这个也是通过乐联协议去匹配的。乐播的文档中也提供了接口

lelinkPlayerInfo.setAesKey(String key)
lelinkPlayerInfo.setAesIv(String iv)

那么一个加密视频的播放,往往就需要在发送端和接收端都要去做一定的兼容,或者直接使用这种第三方SDK提供接口,通过约定的加解密协议方式,去对视频进行解密,然后再渲染播放。

4. 扩展:WifiDisplay介绍

前面说到的投屏分为Miracast和DLNA。上面一直说的都是DLNA的推送模式,那么关于镜像模式又是如何实现的呢?
镜像投屏,核心技术原理和WifiDisplay有关。它的本质和DLNA其实相似,都是建立连接然后传输数据,只是这里的数据是屏幕数据,数据量和交互都比较大,所以形成了一套协议方案。

WifiDIsplay涉及的技术和协议比较多,包括了WIFI P2P技术、RTSP及RTP技术、流媒体技术以及音视频编解码相关的技术等等。

我们可以通过 Wi-Fi 直连创建点对点连接,然后在这个基础上从传输文件到传输音视频。大致的原理就是将Source端(发送端)的音视频数据投屏到Sink端(接收端)。

以上可以简单拆分为几个过程:
1、设备(Source/Sink端)发现彼此:实际上属于P2P Device的彼此发现(涉及到WifiP2pManager.discoverPeers + (WifiP2pService) )
2、发起connect:WFD支持能力的协商,如双方设备支持的分辨率,还有会话的建立(WFD Capalibility Negotiation + WFD Session Establish)
3、Source端的Vedio/Audio 的capture 以及 encoder:(涉及到 Vedio Driver + Audio driver + Audio Capture)
4、上面的音视频数据从Soure端到Sink端。

看似做起来有点麻烦,那么能使用第三方的SDK吗?可以的,就是乐播他们要收费,所以不做探究。

———————————————————————————————————————————————————–
作者:白帽子耗子
链接:https://www.jianshu.com/p/1765480fb759
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

版权声明:本文为ming原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/Im-Victor/p/17091901.html