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

news2024/12/23 14:09:53

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

设备发现

上一篇文章说过,设备的发现有两种情况,主动和被动,下面我们来用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);
    }

效果

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录汇川:套接字TCP通信-梯形图

H5U集成一路以太网接口。使用AutoShop可以通过以太网方便、快捷对H5U进行行监控、下载、上载以及调试等操作。同时也可以通过以太网与网络中的其他设备进行数据交互。H5U集成了Modbus-TCP协议&#xff0c;包括服务器与客户端。可轻松实现与支持Modbus-TCP的设备进行通讯与数据交…

redis-学习笔记(Jedis string 简单命令)

mset & mget 批量设置和获取键值对 可以看出,参数都是可变参数 (就是说, 可以写任意个) 代码演示 getrange & setrange 获取和设置 string 类型中 某一区间的值 代码演示 append 往字符串的末尾拼接字符串 代码演示 incr & decr 如果 string 中为数字的话, 可以进行…

最新版ES8的client API操作 Elasticsearch Java API client 8.0

作者&#xff1a;ChenZhen 本人不常看网站消息&#xff0c;有问题通过下面的方式联系&#xff1a; 邮箱&#xff1a;1583296383qq.comvx: ChenZhen_7 我的个人博客地址&#xff1a;https://www.chenzhen.space/&#x1f310; 版权&#xff1a;本文为博主的原创文章&#xff…

大数据机器学习与深度学习——回归模型评估

大数据机器学习与深度学习——回归模型评估 回归模型的性能的评价指标主要有&#xff1a;MAE(平均绝对误差)、MSE(平均平方误差)、RMSE(平方根误差)、R2_score。但是当量纲不同时&#xff0c;RMSE、MAE、MSE难以衡量模型效果好坏&#xff0c;这就需要用到R2_score。 平均绝对…

怎么去评估数据资产?一个典型的政务数据资产评估案例

据中国资产评估协会《数据资产评估指导意见》&#xff0c;数据资产评估主要是三个方法&#xff1a;市场法、成本法和收益法。之前小亿和大家分享了数据资产评估方法以及价值发挥的路径&#xff0c;今天结合一个案例来具体讲解一下怎么去评估数据资产。 这个案例是一个典型的一个…

【LeetCode刷题】-- 165.比较版本号

165.比较版本号 方法&#xff1a;使用双指针 class Solution {public int compareVersion(String version1, String version2) {//使用双指针int n version1.length(),m version2.length();int i 0,j 0;while(i<n || j <m){int x 0;for(; i < n && vers…

做数据分析为何要学统计学(6)——什么问题适合使用卡方检验?

卡方检验作为一种非常著名的非参数检验方法&#xff08;不受总体分布因素的限制&#xff09;&#xff0c;在工程试验、临床试验、社会调查等领域被广泛应用。但是也正是因为使用的便捷性&#xff0c;造成时常被误用。本文参阅相关的文献&#xff0c;对卡方检验的适用性进行粗浅…

智能优化算法应用:基于生物地理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于生物地理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于生物地理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.生物地理学算法4.实验参数设定5.算法…

【C语言宝库】- 操作符|详解进制转换|计算机小白必备技能(上)

&#x1f308;个人主页: Aileen_0v0 &#x1f525;系列专栏:C语言学习 &#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 目录 进制 定义 基本原理 转换方式 常见的进制转换 二进制和进制的转换 二进制转十进制 十进制转二进制 &#xff08;1&#xf…

Pytorch的安装

Pytorch的安装 Pytorch的安装查看显卡信息CUDA兼容性安装说明开始安装常见异常安装CUDA Pytorch的安装 PyTorch的安装绝对是一个不是那么简单的过程&#xff0c;或多或少总是会出现一些奇奇怪怪的问题&#xff0c;这里分享记录一下PyTorch的安装心得。 查看显卡信息 没用显卡的…

常用的测试用例大全

登录、添加、删除、查询模块是我们经常遇到的&#xff0c;这些模块的测试点该如何考虑 1)登录 ① 用户名和密码都符合要求(格式上的要求) ② 用户名和密码都不符合要求(格式上的要求) ③ 用户名符合要求&#xff0c;密码不符合要求(格式上的要求) ④ 密码符合要求&#xf…

实用干货:推荐4个超级好用的Vue库,你可能不知道!

大家好&#xff0c;我是大澈&#xff01; 本文约1200字&#xff0c;整篇阅读大约需要3分钟。 感谢关注微信公众号&#xff1a;“程序员大澈”&#xff0c;然后免费加入问答群&#xff0c;从此让解决问题的你不再孤单&#xff01; 1. 干货速览 这两天老粉会发现&#xff0c;大…

【CCF BDCI 2023】多模态多方对话场景下的发言人识别 Baseline 0.71 CNN 部分

【CCF BDCI 2023】多模态多方对话场景下的发言人识别 Baseline 0.71 CNN 部分 概述CNN 简介数据预处理查看数据格式加载数据集 图像处理限定图像范围图像转换加载数据 CNN 模型Inception 网络ResNet 残差网络总结参数设置 训练 Train模型初始化数据加载训练超参数训练循环 验证…

idea一些报错

java: 非法字符: \ufeff 使用IDEA修改文件编码 在IDEA右下角&#xff0c;将编码改为GBK&#xff0c;再转为UTF-8&#xff0c;重新启动项目。具体步骤如下&#xff1a; 在IDEA右下角找到UTF-8字样的编码格式设计项&#xff0c;点击选择第一项GBK&#xff0c;然后Convert&#xf…

电脑出现msvcr120_1.dll丢失如何解决,怎么修复

一、msvcr120.dll_1.dll文件的作用&#xff1a; msvcr120.dll_1.dll是一个动态链接库文件&#xff0c;它是Microsoft Visual C Redistributable Package的一部分。该文件包含了许多常用的函数和类&#xff0c;这些函数和类被许多应用程序所共享和使用。因此&#xff0c;当您在…

“ABCD“[(int)qrand() % 4]作用

ABCD[(int)qrand() % 4] 作用 具体来说&#xff1a; qrand() 是一个函数&#xff0c;通常在C中用于生成一个随机整数。% 4 会取 qrand() 生成的随机数除以4的余数。因为4只有四个不同的余数&#xff08;0, 1, 2, 3&#xff09;&#xff0c;所以这实际上会生成一个0到3之间的随…

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

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

自助式可视化开发,ETLCloud的集成之路

自助式可视化开发 自助式可视化开发是指利用可视化工具和平台&#xff0c;使非技术人员能够自主创建、定制和部署数据分析和应用程序的过程。 传统上&#xff0c;数据分析和应用程序开发需要专业的编程和开发技能。但是&#xff0c;自助式可视化开发工具的出现&#xff0c;使…

Unity 通过鼠标控制模拟人物移动和旋转视角

要通过鼠标控制并模拟人物移动和转换视角&#xff0c;将会使用射线检测、鼠标点击和鼠标水平移动&#xff0c;配合物体旋转和移动方法共同实现。 首先搭建个由一个Plane地板和若干cube组成的简单场景&#xff1a; 其次创建一个Capsule作为移动物体&#xff0c;并把摄像头拉到该…

Leetcode—10.正则表达式匹配【困难】

2023每日刷题&#xff08;五十八&#xff09; Leetcode—10.正则表达式匹配 算法思想 参考题解 实现代码 class Solution { public:bool isMatch(string s, string p) {int m s.size(), n p.size();vector<vector<bool>> dp(m 1, vector<bool>(n …