java实现局域网内视频投屏播放(四)投屏实现

代码链接​​​​​​​​​​​​​​​​​​​​​

设备发现

上一篇文章说过,设备的发现有两种情况,主动和被动,下面我们来用java实现这两种模式

主动发现

构建一个UDP请求发送到239.255.255.250:1900获取设备信息,UDP包的内容和http一样


等待响应,当接收到一个完整的响应包后,将数据包封装成设备对象SSDPRespBO

private void receiveSSDP(DatagramSocket udpSocket, Consumer<SSDPRespBO> consumer) throws IOException {
        long time;
        int resIndex = 0;
        byte[] res = new byte[1024];
        byte[] data = new byte[1024];
        long endTime = System.currentTimeMillis() + timeout;
        //一次从socket内核缓冲区复制到进程缓冲的最大字节数
        DatagramPacket dp = new DatagramPacket(data, data.length);
        while ((time = endTime - System.currentTimeMillis()) > 0) {
            udpSocket.setSoTimeout((int) time);
            udpSocket.receive(dp);
            //本次接收到的数据的实际长度(<=DatagramPacket第二个构造参数)从索引0开始覆盖data数组
            int length = dp.getLength();
            for (int i = 0; i < length; i++) {
                if (resIndex == res.length) {
                    //如果res数组已经满了需要进行扩容
                    res = ArrayExtraUtil.byteExpansion(res, 1024);
                }
                res[resIndex++] = data[i];
                if (NetUtil.headerEnd(res, resIndex)) {
                    String str = new String(res, 0, resIndex);
                    consumer.accept(buildSSDPResp(str));
                    //一个响应结束后重置数组以接收其他设备服务的响应
                    resIndex = 0;
                    res = new byte[1024];
                }
            }
            //设置下次读取的最大长度,否则会使用上次接收到的字节长度,receive会设置length属性
            dp.setLength(data.length);
        }
    }
private SSDPRespBO buildSSDPResp(String resp) {
        String[] respArray = resp.split("\r\n");
        if (!respArray[0].contains(" 200 OK")) {
            log.error("响应失败:{}", resp);
            return null;
        }
        SSDPRespBO ssdpRespBO = new SSDPRespBO();
        buildSSDPResp(Arrays.stream(respArray), ssdpRespBO);
        return ssdpRespBO;
    }

然后将SSDPRespBO提交给线程池去获取设备描述文档

根据设备描述文档地址去请求文档,这个地址是http地址,直接通过get请求就可以了

    private void setDeviceDesc(SSDPRespBO ssdpRespBO, List<DeviceDescBO> list) {
        if (ssdpRespBO != null) {
            String location = ssdpRespBO.getLocation();
            Result<DeviceDescBO> result = deviceService.getDeviceDesc(location);
            if (result.isSuccess()) {
                DeviceDescBO deviceDescBO = result.getData();
                deviceDescBO.setUrl(location);
                list.add(deviceDescBO);
            }
        }
    }
    @Override
    public Result<DeviceDescBO> getDeviceDesc(String desUrl) {
        HttpRespBO httpRespBO = httpGet(desUrl);
        return Optional.ofNullable(httpRespBO).map(this::buildDeviceDesc)
                .map(Result::success).orElseGet(() -> Result.fail(ResultEnum.GET_DEVICE_DESC_FAIL));
    }

然后将http返回的内容组装成设备描述对象DeviceDescBO

//构建设备的描述和其服务列表信息
    private DeviceDescBO buildDeviceDesc(HttpRespBO httpRespBO) {
        try {
            if (!httpRespBO.ok()) {
                log.error("设备描述响应错误:{}", JSON.toJSONString(httpRespBO));
                return null;
            }
            String xml = httpRespBO.getUTF8Body();
            DeviceDescBO deviceDescBO = new DeviceDescBO();
            deviceDescBO.setServiceList(new ArrayList<>());
            Document doc = DocumentHelper.parseText(xml);
            Element rootElt = doc.getRootElement();
            Element recordEle = rootElt.element("device");
            Element serviceList = recordEle.element("serviceList");
            Iterator<?> iterator = serviceList.elementIterator("service");
            deviceDescBO.setDeviceType(recordEle.elementTextTrim("deviceType"));
            deviceDescBO.setFriendlyName(recordEle.elementTextTrim("friendlyName"));
            while (iterator.hasNext()) {
                ServiceBO serviceVO = new ServiceBO();
                deviceDescBO.getServiceList().add(serviceVO);
                Element serviceElement = (Element) iterator.next();
                serviceVO.setScpDUrl(serviceElement.elementTextTrim("SCPDURL"));
                serviceVO.setServiceId(serviceElement.elementTextTrim("serviceId"));
                serviceVO.setControlUrl(serviceElement.elementTextTrim("controlURL"));
                serviceVO.setServiceType(serviceElement.elementTextTrim("serviceType"));
                serviceVO.setEventSubUrl(serviceElement.elementTextTrim("eventSubURL"));
            }
            return deviceDescBO;
        } catch (DocumentException e) {
            log.error("设备描述响应解析失败:{}", JSON.toJSONString(httpRespBO), e);
            return null;
        }
    }

并将其加入设备描述对象列表中,返回给调用方

整个发现过程持续5秒,在这5秒内持续阻塞等待组播返回符合条件的设备。这个时间可以在application.yml中指定ssdp.timeout

被动发现

构建一个服务加入组播,监听服务上线和下线事件,设备上线或下线,会发送UDP到组播中,所有加入到组播的服务会收到这个UDP请求,这个请求的内容和上面主动发现的响应内容差不多,所以我们接受请求数据的方法和主动发现用的是同一个都是receiveSSDP

    private void runNotify() {
        log.info("ssdp notify监听开始");
        //构建一个服务加入组播,监听服务上线和下线事件
        try (MulticastSocket socket = new MulticastSocket(1900)) {
            socket.joinGroup(InetAddress.getByName("239.255.255.250"));
            while (!Thread.currentThread().isInterrupted()) {
                receiveSSDP(socket, this::runNotify);
            }
        } catch (Exception e) {
            log.error("ssdp notify异常", e);
        } finally {
            log.info("ssdp notify监听结束");
        }
    }
    //notifyDeviceList只有一个线程操作,没有并发问题
    private void runNotify(SSDPRespBO ssdpRespBO) {
        if (ssdpRespBO != null) {
            String nts = ssdpRespBO.getNts();
            String url = ssdpRespBO.getLocation();
            SSDPStEnum nt = SSDPStEnum.getEnumByType(ssdpRespBO.getNt());
            if (nts.equals("ssdp:alive") && notifyServiceTypes.contains(nt) &&
                    notifyDeviceList.stream().map(DeviceDescBO::getUrl).noneMatch(url::equals)) {
                setDeviceDesc(ssdpRespBO, notifyDeviceList);
            }
            if (nts.equals("ssdp:byebye")) {
                notifyDeviceList.removeIf(deviceDescBO -> deviceDescBO.getUrl().equals(url));
            }
        }
    }

接收完一个完整的包后,如果是设备上线,则和主动发现一样执行setDeviceDesc方法,加入设备描述对象列表中

如果是设备下线,将设备从设备描述对象列表中移除

设备控制

其实这个设备控制,只需要向控制地址发送soap请求即可,在homer-service/src/main/resources/upnp/action/目录下保存了xml的模版,发送soap请求的时候只需要将模版中的参数占位符替换成实际的值即可,在UPNPActionEnum中设置了模版的地址和获取模版内容的方法

@Getter
@AllArgsConstructor
public enum UPNPActionEnum {

    PLAY("upnp/action/play.xml", "播放资源"),
    SET_URI("upnp/action/set_uri.xml", "设置播放资源url"),
    URI_METADATA("upnp/action/uri_metadata.xml", "播放资源元数据");

    private String path;
    private String desc;

    public String getXmlText() {
        return fileTextCache.get(path);
    }
}

对模版内容做了一个本地缓存

@Slf4j
public class ResourceUtil {

    private ResourceUtil() {
        throw new IllegalStateException("Utility class");
    }

    public static final LoadingCache<String, String> fileTextCache = Caffeine.newBuilder()
            .maximumSize(10).expireAfterAccess(100, TimeUnit.MINUTES).build(ResourceUtil::getFileText);

    public static String getFileText(String path) {
        int len;
        ClassPathResource classPathResource = new ClassPathResource(path);
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             InputStream inputStream = classPathResource.getInputStream()) {
            byte[] bytes = new byte[inputStream.available()];
            while ((len = inputStream.read(bytes)) > -1) {
                bos.write(bytes, 0, len);
            }
            return new String(bos.toByteArray(), StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("获取{}文件失败", path, e);
            return null;
        }
    }
}

设置播放资源

  1. 设置控制动作(请求头中的SOAPACTION)
  2. 获取xml模版
  3. 替换xml中的占位符
    @Override
    public Result<Void> setResourceUrl(ActionBO actionBO) {
        String progress = actionBO.getProgress();
        String resourceUrl = actionBO.getResourceUrl();
        String resourceTitle = actionBO.getResourceTitle();

        String metadata = UPNPActionEnum.URI_METADATA.getXmlText();
        metadata = String.format(metadata, resourceTitle, new Date(), resourceUrl);

        String xml = UPNPActionEnum.SET_URI.getXmlText();
        xml = String.format(xml, progress, resourceUrl, StringEscapeUtils.escapeXml10(metadata));
        return executeAction(actionBO, xml);
    }
    private Result<Void> executeAction(ActionBO actionBO, String xml) {
        String actionUrl = actionBO.getActionUrl();
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("SOAPACTION", actionBO.getSoapAction());
        HttpRespBO httpRespBO = httpPostXml(actionUrl, xml, headerMap);
        return Optional.ofNullable(httpRespBO).filter(HttpRespBO::success).map(r -> Result.empty()).orElseGet(() -> {
            log.error("执行动作失败,{},{},{}", actionUrl, xml, httpRespBO);
            return Result.fail("执行动作失败");
        });
    }

播放资源

和上面的流程差不多,只不过xml和soapAction(也就是请求头中的SOAPACTION)不一样。有的投屏设备不需要这一步,只需要设置完播放资源就能播放,有的必须有这一步才能播放,为了兼容不同类型的设备,需要在设置完播放资源后再执行一次播放动作。

    public Result<Void> playResource(ActionBO actionBO) {
        String speed = actionBO.getSpeed();
        String progress = actionBO.getProgress();
        String xml = UPNPActionEnum.PLAY.getXmlText();
        xml = String.format(xml, progress, speed);
        return executeAction(actionBO, xml);
    }

完整的投屏流程

  1. 搜索设备,一般用主动搜索就行
  2. 获取视频名和视频的本地播放地址
  3. 设置播放资源
  4. 播放资源
    public Result<Void> playVideo(int deviceId, String videoId) {
        List<DeviceDescBO> deviceDescList = context.getDeviceDescList();
        Assert.isTrue(deviceDescList != null, "未搜索投屏设备");
        Assert.isTrue(deviceId < deviceDescList.size(), "设备id错误");
        DeviceDescBO deviceDescBO = deviceDescList.get(deviceId);

        List<ServiceBO> serviceList = deviceDescBO.getServiceList();
        Assert.isNotEmpty(serviceList, "设备服务不存在");

        Optional<ServiceBO> serviceOptional = serviceList.stream().filter(s ->
                SSDPStEnum.AV_TRANSPORT_V1.getType().equals(s.getServiceType())).findFirst();
        Assert.isTrue(serviceOptional.isPresent(), "投屏服务不存在");

        ServiceBO serviceBO = serviceOptional.get();
        String controlUrl = serviceBO.getControlUrl();
        controlUrl = controlUrl.startsWith("/") ? controlUrl.substring(1) : controlUrl;

        Result<byte[]> infoResult = videoService.getFileByte(videoId + "/info.txt");
        Assert.isTrue(infoResult.isSuccess(), infoResult.getMessage());

        String videoInfo = new String(infoResult.getData(), StandardCharsets.UTF_8);
        Matcher videoNameMatcher = videoNamePat.matcher(videoInfo);
        String videoName = Optional.of(videoNameMatcher).filter(Matcher::find).map(m -> m.group(1)).orElse(null);

        ActionBO urlAction = new ActionBO();
        urlAction.setProgress("0");
        urlAction.setResourceTitle(videoName);
        urlAction.setResourceUrl(context.getLocalHost() + "/video/m3u8/" + videoId);
        urlAction.setSoapAction("\"" + serviceBO.getServiceType() + "#SetAVTransportURI\"");
        urlAction.setActionUrl(NetUtil.resolveRootUrl(deviceDescBO.getUrl()) + "/" + controlUrl);
        Result<Void> result = setResourceUrl(urlAction);
        Assert.isTrue(result.isSuccess(), result.getCode(), result.getMessage());
        urlAction.setSoapAction("\"" + serviceBO.getServiceType() + "#Play\"");
        urlAction.setSpeed("1");
        urlAction.setProgress("0");
        return playResource(urlAction);
    }

效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/242396.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

对比学习学习记录1

对比学习学习记录 SimCLR Framework 关键在于定义正负样本判断异同相同的就是正例不同的就是负例让模型学到其中的规律 通过encoder对图像提取特征得到一个向量这里的encoder可以是resnet还需要定义相似度的函数计算正负样本之间的距离 对于上面的图片首先对图片进行两种随机的…

zabbix——实现高效网络监控

在当今的数字化时代&#xff0c;网络和服务器的健康状况对于企业的正常运营至关重要。为了及时发现和解决潜在的问题&#xff0c;许多企业选择使用网络监控工具来追踪服务器的性能和网络参数。其中&#xff0c;Zabbix是一个功能强大且开源的网络监控工具&#xff0c;被广泛应用…

环境搭建及源码运行_java环境搭建_mysql安装

1、介绍 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle旗下产品。MySQL是最流行的关系型数据库管理系统之一 1、源码中涉及到的表&#xff1a;mysql 表&#xff1a;订单、意见反馈、用户基础信息、商品、购物车等表 2、订单属于…

虚拟现实三维电子沙盘数字沙盘开发教程第5课

虚拟现实三维电子沙盘数字沙盘无人机倾斜摄影全景建模开发教程第5课 设置system.ini 如下内容 Server122.112.229.220 userGisTest Passwordchinamtouch.com 该数据库中只提供 成都市火车南站附近的数据请注意&#xff0c;104.0648,30.61658 在鼠标指定的位置增加自己的UI对象&…

Java听潮阁(SpringCloud项目)

一、简介 本网站是不凉域网络技术工作室的后台管理网站和旗下的网站&#xff08;目前只有Java听潮阁&#xff09;&#xff0c;后台管理网站具有统计旗下所有网站的数据功能&#xff0c;并且能直接对旗下所有网站进行管理。 Java听潮阁网站是一个Java书籍网站&#xff0c;名字…

力扣40. 组合总和 II(java 回溯法)

Problem: 40. 组合总和 II 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 在使用回溯之前我们首先可以明确该题目也是一种元素存在重复但不可复用的组合类型问题。而此题目可以参考下面一题的大体处理思路&#xff1a; Problem: 90. 子集 II 具体的&#xff1a; 1.首…

2023-12-13 树的层次遍历和树的反转以及树的对称

二叉树的层次遍历、翻转二叉树和对称二叉树 102. 二叉树的层序遍历 核心&#xff1a;BFS广度优先遍历&#xff0c;就是维护一对队列去遍历&#xff01;队列先进先出&#xff0c;符合一层一层遍历的逻辑。 # Definition for a binary tree node. # class TreeNode: # def …

四、编写第一个 Shell 脚本

一、编写 Shell 脚本内容 打开文本编辑器&#xff08;可以使用 vi/vim 命令来创建文件&#xff09;&#xff0c;新建一个文件 chaoqing.sh&#xff0c;扩展名为 sh &#xff08;sh 表示 shell&#xff09;&#xff0c;扩展名不影响脚本的运行。 输入一些代码&#xff0c;如下…

配置本地端口镜像示例(1:1)

本地端口镜像简介 本地端口镜像是指观察端口与监控设备直接相连&#xff0c;观察端口直接将镜像端口复制来的报文转发到与其相连的监控设备进行故障定位和业务监测。 配置注意事项 观察端口专门用于镜像报文的转发&#xff0c;因此不要在上面配置其他业务&#xff0c;防止镜像…

zxjy008- 项目集成Swagger

Swagger可以生成在线文档&#xff0c;还可以进行接口测试。 1、创建common模块(maven类型) 为了让所有的微服务子子模块都可以使用&#xff0c;可以在guli_parent父工程下面创建公共模块 1.1 在guli_parent父工程下面创建公共模块 配置&#xff1a; groupId&#xff1a;com…

HTML---表单

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一.表单概念 HTML表单是网页上用于收集用户输入信息的一种元素。它由一系列输入字段&#xff08;input&#xff09;、选择字段&#xff08;select&#xff09;、文本区域&#xff08;textarea&a…

Terrain

最近工作中遇到一个需求&#xff0c;需要在地形上随机生成对应的植被&#xff0c;需要对地形就行解析。 1.对地形的贴图的解析 需要把对应坐标在Terrain Layer成分获取到 2.计算出对应坐标中Terrain Layer占比最大的成分当作当前坐标的主要成分&#xff0c;例如草地&#xff…

C# 两个日期比较大小

文章目录 C# 两个日期比较大小直接比较大小工具类DateTime.Compare C# 两个日期比较大小 直接比较大小 string ed "2023-12-13 09:27:59.000";//过去式DateTime nowDateTime DateTime.Now;DateTime expirationDate Convert.ToDateTime(ed);//质保期 长日期DateT…

python篇FastAPI_快速使用手册

一个新兴的web框架 安装fastapi pip install fastapi asgi 服务器安装 pip install "uvicor[standard]"helloworld from fastapi import FastAPI appFastAPI() app.get("/") async def root():return {"message" : "hello world"…

工业性能CCD图像处理

硬件部分 软件部分 CCD新相机的调试处理(更换相机处理,都要点执行检测来查看图像变化) 问题:新相机拍摄出现黑屏,图像拍摄不清晰,(可以点击图像,向下转动鼠标的滚轮(Mouse Wheel)放大图像) 解决办法:进入CCD的设定,选择对应的相机,调试好参数(如下图) 选择好相…

CentOS7安装MySQL8.0

一、使用Yum安装 1. 使用wget下载MySQL的rpm包 wget https://repo.mysql.com//mysql80-community-release-el7-3.noarch.rpm2. 安装下载好的rpm包 yum localinstall mysql80-community-release-el7-3.noarch.rpm 3. 安装mysql&#xff08;该步可能出现问题&#xff09; yum…

代码分支管理+DevOps策略实践

现在团队相关的一些背景&#xff1a;1、一个人维护几个系统&#xff1b;2、需求急且大小不一&#xff0c;产品经理也不会去细拆需求&#xff1b;3、敏捷成熟度几乎为0&#xff0c;没有DevOps&#xff0c;没有自动化测试&#xff1b;4、使用SVN。 将要变成&#xff1a;1、一个人…

第二百零四回 模拟对话窗口的页面

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 经验分享5. 内容总结 我们在上一章回中介绍了"修改组件风格的另外一种方法"相关的内容&#xff0c;本章回中将介绍" 如何做一个模拟对话框窗口的页面".闲话休提&#xff0c;让我…

C语言小游戏之三子棋(可以做期末设计作业)

哈喽大家好&#xff0c;今天为大家带来一个用C语言写的小游戏--三子棋&#xff0c;就是大家小时候用树枝和石子玩的那种游戏&#xff0c;这个小项目可以用于大家的C语言期末设计作业&#xff0c;不会很难&#xff0c;都是C语言基本的操作 下面是游戏截图&#xff1a; 完全可以…

acwing-蓝桥杯C++ AB组辅导课Day1-递归

感谢梦翔老哥的蓝桥杯C AB组辅导课~ 省一刷200题 国赛拿成绩300题 比赛考察的是各种模型的熟练度&#xff0c;可以从dfs的角度比较各个模型与当前问题的匹配程度。 常见时间复杂度&#xff0c;根据时间复杂度可以判别是否可以选用这个解题思路 写递归的时候&#xff…