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);}}}
    @Overridepublic 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中的占位符
    @Overridepublic 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.mzph.cn/news/219156.shtml

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

相关文章

【价值几十万的仿抖音直播电商系统源码共享】

当下&#xff0c;传统的图文电商模式已经走向没落&#xff0c;以抖音为首的直播电商模式备受用户追捧&#xff0c;它具有实时直播和强互动的特点&#xff0c;是传统电商所不具备的优势。而且&#xff0c;当前正是直播电商的红利期&#xff0c;很多主播和品牌商都通过直播电商业…

K8S(四)—pod详解

目录 pod介绍Pod的概念&#xff1a;Pod的特性&#xff1a;Pod的配置&#xff1a;Pod的控制&#xff1a;示例 YAML 文件&#xff1a; pod启动流程问题 两种方式启动镜像的升级和回滚更新 Deployment&#xff1a;回滚检查 Deployment 历史版本回滚到之前的修订版本缩放 Deploymen…

青少年CTF-Crypto(Morse code/ASCII和凯撒)

FLAG&#xff1a;你这一生到底想干嘛 专研方向: Web安全 &#xff0c;Md5碰撞 每日emo&#xff1a;不要因为别人都交卷了&#xff0c;就乱选答案 文章目录 1.Morse code2、ASCII和凯撒的约定 1.Morse code 题目提示摩尔斯电码&#xff0c;这个是给的附件 直接用摩尔斯解密&am…

常用的测试用例大全

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

[c]输出字符金字塔

我们可以把字符金字塔类比数字金字塔 输入3 输出 类似下图 下面附上我的代码&#xff0c;可能有些繁琐 #include<stdio.h> int main() {char s;scanf("%c",&s);int lens-64;//将字符的ascii码值减去64得到循环次数&#xff0c;比如你输入A&#xff0c;l…

一篇文章,带你详细了解华为认证体系证书(二)

一篇文章&#xff0c;带你详细了解华为认证体系证书 &#xff08;一&#xff09;_华为高斯数据库证书_PICACHU的博客-CSDN博客一、总体概括一、总体概括华为认证是华为技术有限公司基于“平台生态”战略&#xff0c;围绕“云-管-端”协同的新ICT技术架构&#xff0c;打造的业界…

Spring Boot--Freemarker渲染技术+实际案例

目录 Freemarker 1.1.什么是Freemarker 1.2.Freemarker模板组成部分 1.3.优点 FreeMarker常见的方法&#xff1a; 2.2.2.数值 2.2.3.布尔值 2.2.4.日期 2.3.常见指令 2.3.1.处理不存在的值 assign 2.3.4.list 2.3.5.include SpringBoot整合Freemarker Freemarker…

STM32-TIM定时器输出比较

目录 一、输出比较简介 二、PWM简介 三、输出比较通道&#xff08;通用&#xff09; 四、输出比较通道&#xff08;高级&#xff09; 五、输出比较模式 六、PWM基本结构 七、PWM参数计算 八、外设介绍 8.1 舵机 8.2 直流电机及驱动 九、开发步骤 十、输出比较库函数…

每日一题,杨辉三角

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 示例 2: 输入: numRows 1 输出: [[1]]

限流常用算法以及基于Sentinel的微服务限流及熔断

一、服务限流的作用及实现 在没有任何保护机制的情况下&#xff0c;如果所有的流量都进入服务器&#xff0c;很可能造成服务器宕机导致整个系统不可用&#xff0c;从而造成巨大的损失。为了保证系统在这些场景中仍然能够稳定运行&#xff0c;就需要采取一定的系统保护策略&…

Unity_ET框架项目-斗地主_启动运行流程

unity_ET框架项目-斗地主_启动运行流程 项目源码地址&#xff1a; Viagi/LandlordsCore: ET斗地主Demohttps://github.com/Viagi/LandlordsCore下载项目到本地。 启动运行步骤&#xff1a; 下载目录如下&#xff1a; 1. VS&#xff08;我用是2022版VisualStudio&#xff09…

【机器学习】多模态机器学习

多模态机器学习是指利用多种不同的数据类型&#xff08;如图像、文本、音频等&#xff09;来训练和优化机器学习模型。相比于单一模态的机器学习&#xff0c;多模态机器学习可以更好地模拟现实世界中的复杂信息交互&#xff0c;从而提高模型的性能和泛化能力&#xff0c;同时也…

大数据机器学习与深度学习—— 生成对抗网络(GAN)

GAN概述 在讲GAN之前&#xff0c;先讲一个小趣事&#xff0c;你知道GAN是怎么被发明的吗&#xff1f;据Ian Goodfellow自己说&#xff1a; 之前他一直在研究生成模型&#xff0c;可能是一时兴起&#xff0c;有一天他在酒吧喝酒时&#xff0c;在酒吧里跟朋友讨论起生成模型。然…

Linux—深入理解进程的概念,PCB,父子进程及查看进程信息的两种方法

进程的概念 课本概念&#xff1a;程序的一个执行实例&#xff0c;正在执行的程序等。 内核观点&#xff1a;担当分配系统资源&#xff08;CPU&#xff0c;内存&#xff09;的实体。 1.描述进程-PCB 我们知道程序是代码编译好后形成的可执行文件&#xff0c;存放在磁盘上。而我…

Qt设置类似于qq登录页面(ikun)

头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QWindow> #include <QIcon> #include <QLabel> #include <QMovie> #include <QLineEdit> #include <QPushButton>QT_BEGIN_NAMESPACE namespace Ui { class…

Prompt提示优化工具

在大模型使用过程中,Prompt尤为重要。一个好的Prompt可以解决很多的问题。那么如何撰写一个完美的Prompt? 下面汇总了一些Prompt工具,可以辅助日常Prompt的编写。 欢迎关注公众号 1. prompt perfect 可以自动优化已有的提示,比较多个模型的输出,运行智能体等。 https:…

带大家做一个,易上手的家常土豆炒瘦肉(糖醋水果版)

先那一块瘦肉 用水化冰 拿一颗土豆 去皮切成小块 瘦肉洗干净 然后切成小块 与土豆混在一起 打一个鸡蛋 将 鸡蛋液 一小勺淀粉 小半勺生抽 一小勺料酒 用手抓均匀 起锅烧油 倒入瘦肉土豆 翻炒 看肉变色了 加入 一包番茄酱(可以多放一点 味道足一点) 小半勺老抽调色 小半…

文献速递:PET-影像组学专题--影像组学和肺癌免疫治疗反应:文献系统综述

文献速递&#xff1a;PET-影像组学专题–影像组学和肺癌免疫治疗反应&#xff1a;文献系统综述 01 文献速递介绍 肺癌&#xff08;LC&#xff09;是全球癌症相关死亡的主要原因&#xff0c;尽管在诊断和治疗方面取得了进展[2,3]&#xff0c;但它仍然对公共健康构成严重威胁[…

Android : Room 数据库的基本用法 —简单应用_一_入门

1.Room介绍&#xff1a; Android Room 是 Android 官方提供的一个持久性库&#xff0c;用于在 Android 应用程序中管理数据库。它提供了一个简单的 API 层&#xff0c;使得使用 SQLite 数据库变得更加容易和方便。 以下是 Android Room 的主要特点&#xff1a; 对象关系映射…

无代码,零基础!手把手教你掌握Stable Diffusion,创作出惊艳AI插画!

【前言】 Stable Diffusion是一款开源、免费的应用程序&#xff0c;因为其生态开放且发展迅速&#xff0c;所以不同时期的安装、配置方式可能都会有所变化。如果遇到什么安装上的问题可以评论提出&#xff0c;帮你解答。 关于Stable Diffusion Stable Diffusion&#xff08;简…