AndroidTV: Hotel scan code authentication and screencasting DLNA

I have been picked on by my boss several times, and recently I just can’t stand it anymore and am ready to leave my job;

But I am worried that I will not be able to find a job for a long time after leaving my job.

I just want to contribute a set of procedures that I have figured out in my daily life. If you are able, please help me get through this cold winter.

Currently, there are three terminals written: Android TV terminal, Android mobile terminal, and Windows-Qt terminal.

This article is part of the code on the TV side. Of course, you need to understand it. I cannot paste the complete code. Please forgive me;

If you want to get the complete source code, you can send me a private message directly. Thank you very much.

Then start pasting some source code:

1.SSDP sending

DLNA is analyzed by listening to broadcasts on the network, that is, sending several messages in a loop

Some of these messages are about device information, and some are about control commands. In short, you have to decide according to your own needs.


    public String toString(int i) {

        //都是些固定格式,NT和USN是两辆相关
        StringBuilder content = new StringBuilder();
        content.append("NOTIFY * HTTP/1.1").append("\r\n");
        content.append("SERVER: Linux/3.14.29 UPnP/1.0 Cling/2.0").append("\r\n");
        content.append("CACHE-CONTROL: max-age=1800").append("\r\n");//指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
        content.append("LOCATION: "+_LOCATION).append("\r\n");
        //乐播投屏加了下面三行,加了就搜索不到了
//        content.append("OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01; ds=0").append("\r\n");
//        content.append("01-NLS: 0dcebdf8-1dd2-11b2-8b72-f07170d94291").append("\r\n");
//        content.append("X-User-Agent: redsonic").append("\r\n");
        content.append("NTS: ssdp:alive").append("\r\n");
        if(i==0){
            content.append("NT: upnp:rootdevice").append("\r\n");
            content.append("USN: uuid:"+ MainActivity._UUID+"::upnp:rootdevice").append("\r\n");
        }else if(i==1){
            content.append("NT: uuid:"+MainActivity._UUID).append("\r\n");
            content.append("USN: uuid:"+MainActivity._UUID).append("\r\n");
        } else if(i==2){
            content.append("NT: urn:schemas-upnp-org:device:MediaRenderer:1").append("\r\n");
            content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:device:MediaRenderer:1").append("\r\n");
        }else if(i==3){
            content.append("NT: urn:schemas-upnp-org:service:RenderingControl:1").append("\r\n");
            content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:RenderingControl:1").append("\r\n");
        }else if(i==4){
            content.append("NT: urn:schemas-upnp-org:service:ConnectionManager:1").append("\r\n");
            content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:ConnectionManager:1").append("\r\n");
        }else if(i==5){
            content.append("NT: urn:schemas-upnp-org:service:AVTransport:1").append("\r\n");
            content.append("USN: uuid:"+MainActivity._UUID+"::urn:schemas-upnp-org:service:AVTransport:1").append("\r\n");
        }
        content.append("HOST: 239.255.255.250:1900").append("\r\n");
        content.append("\r\n");
        //Log.e("SSDPSendMsg",MainActivity._UUID+"\r\n\r\n");
        return content.toString();

        //https://blog.csdn.net/thebestleo/article/details/50800781
    }

On the Android side, after being sent successfully, it cannot be displayed normally on devices on all major platforms.

At this time, you need to build a web service on the Android side. The web service points to a description xml and some usable information about the screen projection device, such as a certain "room number" in the hotel, "brand + room number". Dynamic settings, such as MyDLNA that I set

2. Build a web service

//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-server:8.1.21.v20160908'
//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-servlet:8.1.21.v20160908'
//noinspection GradleDependency
api 'org.eclipse.jetty:jetty-client:8.1.21.v20160908'
api 'org.nanohttpd:nanohttpd:2.3.1'

I won’t paste a lot here.

Paste the content of the authentication part. Authentication is simple to say, that is, the TV side gives a link so that the mobile phone can access it, and the TV side converts the link into a QR code;

The mobile phone must first be connected to the same network as the TV. After scanning the code on the mobile phone, it will directly request the TV.

After scanning the QR code, the TV will get the header of the request, which is the requested mobile phone IP address, cache this IP, make a judgment, and only send a broadcast to this mobile phone IP, so that a single cast can be achieved after scanning the code. screen, the TV sets in all rooms in the same hotel will not be displayed.

public class ControlHandler extends org.eclipse.jetty.server.handler.DefaultHandler {

    private Activity myactivity;
    private String PlayerURL = "";
    private String DesiredVolume = "";
    private String PlayTarget = "";
    private String PlayState = "NO_MEDIA_PRESENT";
    private String PlayerURLTemp = "xxx";

    public ControlHandler(Activity activity) {
        super();
        this.myactivity = activity;
    }

    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        if (response.isCommitted() || baseRequest.isHandled()) {
            return;
        }

        Log.e("RequestShare", request.getRequestURI());
        //认证过就返回
        if (request.getRequestURI().contains("/certification")) {
            Constants.IPDEVICE = request.getRemoteAddr();
            Log.e("RequestShare", "正在认证设备");
            baseRequest.setHandled(true);
            ServletMsg.certificationMsg(response);
        }

        //这个是读取认证的设备的(可以使得投屏设备具有唯一性)
        if (request.getRequestURI().contains("/upnp/dev/" + MainActivity._UUID + "/desc")) {//验证可以通过对比IP实现指定设备投屏
            if (Constants.ALLDEVICES) {
                Log.e("RequestShare", "所有设备都可以投屏");
                baseRequest.setHandled(true);
                ServletMsg.defaultMsg(response);
            } else {
                if (request.getRemoteAddr().contains(Constants.IPDEVICE)) {//验证可以通过对比IP实现指定设备投屏
                    Log.e("RequestShare", "指定:" + request.getRemoteAddr() + "设备符合投屏条件");
                    baseRequest.setHandled(true);
                    response.addHeader("Content-type: ", "text/xml");
                    response.addHeader("SERVER: ", "Linux/3.14.29 UPnP/1.0 Cling/2.0");
                    response.addHeader("Cache-control: ", "max-age=1800");
//                    response.addHeader("X-User-Agent: ","redsonic");

                    ServletMsg.defaultMsg(response);
                }
            }
        }

        //请求分析
        if (Constants.ALLDEVICES) {
            getHeader(request, baseRequest, response); //头部执行播放暂停之类的简单操作
            getBody(request, baseRequest, response);//这个主要是主要处理有携带参数的数据,response还是加了一下,避免以后自己做控制端
            //爱奇艺和腾讯会请求里面链接,单纯回复可能不管用,加的一项
            ServletRequest(request, baseRequest, response);
        } else {
            if (request.getRemoteAddr().contains(Constants.IPDEVICE)) {//验证可以通过对比IP实现指定设备投屏
                Log.e("RequestShare1", request.getRemoteAddr());//IP地址
                Log.e("RequestShare2", request.getRequestURI());//请求的路径
                getHeader(request, baseRequest, response); //头部执行播放暂停之类的简单操作
                getBody(request, baseRequest, response);//这个主要是主要处理有携带参数的数据,response还是加了一下,避免以后自己做控制端
                //爱奇艺和腾讯会请求里面链接,单纯回复可能不管用,加的一项
                ServletRequest(request, baseRequest, response);
            }
        }
    }

3. Cast video

When it comes to projecting videos, it’s very simple. It can be understood that the mobile phone sends a request to the TV, but the request header and request body need to be in a specific format.

For example:

xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas -upnp-org:metadata-1-0/upnp/"><item id="0" parentID="-1" restricted="1"><dc:title>I spent 10,000 yuan to take a taxi in Chongqing After arriving in Shanghai, he was called a douchebag by netizens across the country? </dc:title><upnp:storageMedium>UNKNOWN</upnp:storageMedium><upnp:writeStatus>UNKNOWN</upnp:writeStatus><upnp:

 This is the video link sent. After proper analysis, it can be played normally.

4. Monitor playback and control progress

The mobile phone will continuously send real-time data to the TV, such as volume, progress, etc.; after the mobile phone monitors the system volume, it will also send a piece of data about the volume to DLNA, and the TV can process it as soon as it is received.

Most video manufacturers can report normally. Youku and Mango have added some mechanisms. The TV needs to send a message to the device saying that I have started playing, and then Youku will continue to play. Otherwise, after playing for two seconds, Youku will not When receiving a message from the TV, the link will be turned off immediately...

 //获取body信息
    private void getBody(HttpServletRequest request, Request baseRequest, HttpServletResponse response) throws IOException {
        Log.e("getBody", "------getBody分割线start------");
        BufferedReader br = request.getReader();
        String str, wholeStr = "";
        while ((str = br.readLine()) != null) {
            wholeStr += str;
        }
        Log.e("getBody", wholeStr);

        //拿到播放的链接
        if (wholeStr.contains("u:SetAVTransportURI")) {
            Log.e("55m5m53", wholeStr);
            String temp = wholeStr.substring(0, wholeStr.indexOf("</CurrentURI>"));
            PlayerURL = temp.substring(temp.indexOf("<CurrentURI>"))
                    .replace("<CurrentURI>", "")
                    .replace("&amp;", "&")
                    .replace("&quot;", "\"")
                    .replace("&lt;", "<")
                    .replace("&gt;", "<")
                    .replace("&nbsp;", " ")
                    .replace("&apos;", "'");
            Log.e("55m5m54", PlayerURL);
            PlayState = "PLAYING";
            //Response响应一下
            baseRequest.setHandled(true);
            ServletMsg.SetAVTransportURIResponseMsg(response);
            //发送并打开页面给播放器
//            Intent intent = new Intent(myactivity, PlayerActivity.class);
//            intent.putExtra("PlayerURL", PlayerURL);
//            myactivity.startActivity(intent);
            //芒果TV一次性发了5个
//            if (!PlayerURL.equals(PlayerURLTemp)) {
//                PlayerURLTemp = PlayerURL;
                countDownTimer.start();
//                intent.putExtra("PlayerURL", PlayerURL);
//                myactivity.startActivity(intent);
//            }
        } else if (wholeStr.contains("u:SetVolume")) {
            //摘取出要我设置的音量
            String Volume = wholeStr.substring(0, wholeStr.indexOf("</DesiredVolume>"));
            DesiredVolume = Volume.substring(Volume.indexOf("<DesiredVolume>")).replace("<DesiredVolume>", "");
            Log.e("SetVolume", DesiredVolume);
            //Response响应一下
            baseRequest.setHandled(true);
            ServletMsg.SetVolumeResponseMsg(response);
            //到这边的意思就是和播放器说要控制一下设备的音频
            SendBroadcast("Volume#" + DesiredVolume);
        } else if (wholeStr.contains("u:Seek")) {
            //摘取出要我设置的进度
            String Target = wholeStr.substring(0, wholeStr.indexOf("</Target>"));
            PlayTarget = Target.substring(Target.indexOf("<Target>")).replace("<Target>", "");
            Log.e("PlayTarget", PlayTarget);
            SendBroadcast("Seek#" + PlayTarget);//告诉播放器,我要设置进度了
        } else if (wholeStr.contains("u:GetTransportInfo")) {
            //告诉控制端,我还活着
            Log.e("u:GetTransportInfo", "电视系统的State:" + PlayState);
            baseRequest.setHandled(true);
            ServletMsg.GetTransportInfoMsg(response, PlayState, "OK");
        } else if (wholeStr.contains("u:GetVolume")) {
            //腾讯bilibili会要求获取一下音量,就给呗
            //bilibili给了后,会一直索要~好奇怪
            Log.e("u:GetVolume", "电视系统的Volume:" + PlayerActivity.playerData.get_volume());
            baseRequest.setHandled(true);
            ServletMsg.GetVolumeMsg(response, PlayerActivity.playerData.get_volume());
        } else if (wholeStr.contains("u:GetPositionInfo")) {
            //告诉控制端我当前的播放状态
            GetPositionInfo(response, baseRequest);
        } else if (wholeStr.contains("u:GetMediaInfo")) {
            //爱奇艺单独列了这个~,尝试发了,但没反应,找了几个数据,应该是正确的
            Log.e("getBody", "我拿到GetMediaInfo了");
            GetMediaInfo(response, baseRequest);
        } else if (wholeStr.contains("u:Stop")) {
            //有些东西需要响应的,就响应一下
            Log.e("getBody", "我拿到Stop了");
            baseRequest.setHandled(true);
            ServletMsg.StopResponseMsg(response);
        }
        Log.e("getBody", "------getBody分割线end------");
    }


    private CountDownTimer countDownTimer = new CountDownTimer(1000, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
        }

        @Override
        public void onFinish() {
            //PlayerURLTemp="Fowlet";

            if(isTopActivity(Constants.PLAYACTIVITY)){
                SendBroadcast("State#Stop");
            }

            Intent intent = new Intent(myactivity, PlayerActivity.class);
            //Intent intent = new Intent(myactivity, Player2Activity.class);
            intent.putExtra("PlayerURL", PlayerURL);
            myactivity.startActivity(intent);
        }
    };




    //获取的头部信息
    private void getHeader(HttpServletRequest request, Request baseRequest, HttpServletResponse response) {
        Log.e("getHeader", "------getHeader分割线start------");
        Enumeration<String> em = request.getHeaderNames();
        while (em.hasMoreElements()) {
            String name = (String) em.nextElement();
            String value = request.getHeader(name);
            //System.out.println(name + "=" + value);
            Log.e("getHeader", name + "=" + value);
            if (name.equals("SOAPAction") | name.equals("SOAPACTION")) {
                String[] split = value.split("#");
                String result = split[1];
                //获取头部的一些快捷操作,如果说有一些要参数要回调的话,还是要走body
                String Transport = result.substring(0, result.indexOf("\""));
                Log.e("yongge", Transport);
                if (Transport.equals("Play")) {
                    //播放
                    PlayState = "PLAYING";
                    Log.e("Header__", "我拿到Play了:" + PlayState);
                    //开始播放了就让播放器开始收集信息
                    //SendBroadcast("GetInfo#"+Transport);
                    //暂停播放也会用的到
                    SendBroadcast("State#" + Transport);
                } else if (Transport.equals("Stop")) {
                    //停止
                    PlayState = "STOPPED";
                    Log.e("Header", "我拿到Stop了:" + PlayState);
                    SendBroadcast("State#" + Transport);
                } else if (Transport.equals("Pause")) {
                    //暂停
                    PlayState = "PAUSED_PLAYBACK";
                    Log.e("Header__", "我拿到Pause了:" + PlayState);
                    SendBroadcast("State#" + Transport);
                } else if (Transport.equals("SetVolume")) {
                    //设置音量
                    Log.e("Header", "我拿到SetVolume了" + DesiredVolume);
                } else if (Transport.equals("GetPositionInfo")) {
                    SendBroadcast("GetInfo#" + Transport);//告诉播放器,你要开始收集播放器信息了
                    //获取进度
                    Log.e("Header", "我拿到GetPositionInfo了");
                } else if (Transport.equals("GetTransportInfo")) {
                    //获取传输状态
                    //安卓端的腾讯是拿的GetTransportInfo
                    Log.e("Header", "我拿到GetTransportInfo了");
                } else if (Transport.equals("SetMute")) {
                    //静音/取消
                    Log.e("Header", "我拿到SetMute了");
                } else if (Transport.equals("Seek")) {
                    //设置进度
                    Log.e("Header", "我拿到Seek了" + PlayerActivity.playerData.get_seek());
                } else if (Transport.equals("GetMediaInfo")) {
                    //爱奇艺单独列了这个~
                    Log.e("Header", "我拿到GetMediaInfo了");
                } else if (Transport.equals("GetVolume")) {
                    Log.e("Header", "我拿到GetVolume了");
                }
            }
            //优酷是要走订阅
            if (name.equals("CALLBACK")) {
                String callbackUrl = value.replace("<", "").replace(">", "");
                Log.e("callbackUrl", callbackUrl);
                String str[] = callbackUrl.replace("/","").split(":");
                try {
                    Socket socket = new Socket(str[1], Integer.parseInt(str[2]));
                    Log.e("callbackUrl2", str[1]+"---"+str[2]);
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                    bw.write("HTTP/1.1 200 OK\r\n");
                    bw.write("Server: Linux/3.14.29 UPnP/1.0 Cling/2.0\r\n");
                    bw.write("SID: uuid:"+MainActivity._UUID+"\r\n");
                    bw.write("Content-Type: text/html; charset=\"utf-8\""+"\r\n");
                    bw.write("TIMEOUT: Second-3600"+"\r\n\r\n");
                    bw.flush();

                    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    while(true) {
                        String readLine = null;
                        if ((readLine = br.readLine()) != null) {
                            Log.e("socket", readLine);
                        } else {
                            break;
                        }
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
//                YoukuMsg youkuMsg = new YoukuMsg();
//                //要返回一下订阅成功
//                baseRequest.setHandled(true);
//                ServletMsg.VendorMsg(response, youkuMsg.toString());

//                SubResponse subRespons = new SubResponse();
//                subRespons.getTest(callbackUrl,MainActivity._UUID);

//                onUnicastSend("172.16.0.121", 49152);//源IP和源端口
//
//                response.setStatus(SC_OK);
//                response.addHeader("Server: ","Linux/3.14.29 UPnP/1.0 Cling/2.0");
//                response.addHeader("SID: ","uuid:"+MainActivity._UUID);
//                //response.addHeader("Content-Type: ","text/html; charset=\"utf-8\"");
//                response.addHeader("TIMEOUT: ","Second-3600");
//                response.setContentType("text/html; charset=\"utf-8\"");
//
//                baseRequest.setHandled(true);
//                ServletMsg.VendorMsg(response, TencentMsg.RenderingControl_desc);
//                baseRequest.setHandled(true);
//                ServletMsg.VendorMsg(response, "");

//                response.setHeader();


            }
        }
        Log.e("getHeader", "------getHeader分割线end:" + PlayState + "------");
    }

Processing returned progress

Guess you like

Origin blog.csdn.net/title71/article/details/132663726