当前位置: 首页 > news >正文

springboot编写mp4视频播放接口

简单粗暴方式

直接读取指定文件,用文件流读取视频文件,输出到响应中

    @GetMapping("/display1/{fileName}")public void displayMp41(HttpServletRequest request, HttpServletResponse response,@PathVariable("fileName") String fileName) throws IOException {File file=new File("D:/Download/"+fileName+".mp4");if(!file.exists()){response.getOutputStream().close();return;}InputStream inStream=new FileInputStream(file);byte[] buffer = new byte[1024];int len;while ((len = inStream.read(buffer)) != -1) {response.getOutputStream().write(buffer, 0, len);}inStream.close();response.getOutputStream().flush();response.getOutputStream().close();}

这种方式很尴尬,可以播放视频,然而你会发现视频自带的进度条无法拖动。。。。。。。,只能暂停播放,没办法前进,也没办法后退。。。。。。

高端优雅方式

需要加一个断点续传的规范,实现很简单

注:如果你是用的h5原生的<video>,请求头会有一个Range: bytes=589824-,表示该请求希望返回的数据是从589824位置开始即可。

实现一共两点:

(1)响应头部添加如下格式响应头

Content-Range: bytes 589824-32153693/32153694

大概就是:Content-Range: bytes 请求头指定的开始字节数-本次返回的文件字节位置/总共多少字节,值得注意的是,本次返回的文件字节位置一定要比总字节数至少少一个字节,否则视频缓存结束的最后一次数据无法播放,可能是浏览器出了异常,视频会重新播放。本次返回的文件字节位置可以不准,因为浏览器会自动记录真实拿到的字节数量,但一定要少一个字节。

(2)响应码改为206

有了这两点就可以实现正常的视频播放接口了。

优化

考虑到视频的进度条很大概览是会被拖来拖去的,导致频繁请求接口。

假设你的文件200MB,频繁的请求每次都会把整个文件读入http流中,如果用户网速慢,或者浏览器的缓存策略会阻塞http请求,慢慢从http响应中读取这部分数据,这可能就会使数据都堆积到服务器内存里(本人毫无根据瞎想的),浪费资源。

(1)因为需要指定字节位置读取视频文件,使用随机读取RandomAccessFile类来操作。

(2)既然支持分段获取数据,不如每次返回定量的字节数即可。我这里设置成每次获取1MB,浏览器播放完了会自动接着调用。根据实际情况考虑,内网环境使用可以设大一些,如果数值设置的小,这请求频率会变的很多,得不偿失。

    @GetMapping("/display/{fileName}")public void displayMp4(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {File file = new File("D:/Download/" + fileName + ".mp4");if (!file.exists()) {response.getOutputStream().close();return;}String range = request.getHeader("Range");long lenStart = 0;if (range != null && range.length() > 7) {range = range.substring(6, range.length() - 1);lenStart = Long.parseLong(range);}int size = 1048576;response.setHeader("Content-Range", "bytes " + lenStart + "-" + ((file.length() - lenStart-2 < size)?file.length()-1:lenStart+size- 1) +"/" + file.length());response.setHeader("Content-Type", "video/mp4");response.setStatus(HttpStatus.PARTIAL_CONTENT.value());//响应码206表示响应内容为部分数据,需要多次请求RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");randomAccessFile.seek(lenStart);//设置读取的开始字节数//视频每次返回一兆数据byte[] buffer = new byte[size];int len = randomAccessFile.read(buffer);if (len != -1) {response.getOutputStream().write(buffer, 0, len);}randomAccessFile.close();response.getOutputStream().flush();response.getOutputStream().close();}

跨平台测试优化

上述的方式对于pc端是没有问题的,手机上的谷歌浏览器,火狐浏览器,edge浏览器都没问题,但小米浏览器和夸克不能正常播放,都是播放几秒然后就加载失败了。。。。于是将测试的视频放到普通的web服务器上直接访问,这俩浏览器没问题,那一定是接口兼容性的问题。

通过抓包,不断测试分析,最后大概摸清了问题所在:

(1)对于普通的web服务器是可以用cookie的,浏览器通过cookie能够信任服务器的连接,有计划的指定获取字节数范围,提供给服务器起始和结束两个字节的位置

(2)springboot可能配置的原因,不会记录cookie,导致浏览器某些原因不会有计划的分段获取,而是会给服务端一个自己当前已保存到的字节位置,希望服务器能够每次都返回剩余的全部字节数。然后浏览器自己来确定要多少。

所以,当请求头只指明了起始位置,但是没有结束的位置,就是需要服务端把所有数据都给返回。

如果指明了起始位置,也指明了结束位置,就需要按照要求返回对应的字节数。

这里需要注意的是,content-length要在返回数据之前预先加到响应头上,这个是可以计算出来的,计算要准确,不加上这个也会出兼容性问题。

有了上述这些条件,基本上在各个浏览器就可以正常的视频播放了,如果想通过该接口直接下载视频,还做不到,因为普通下载好像不会用这套协议,需要判断请求头,有Range可能就是播放视频的,没有就是下载视频的,用普通的流输出文件即可。

 /*** 读取视频文件*/@GetMapping("/display/{fileName}")public void displayMp4(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {File file = new File("/usr/local/nginx/html/video/" + fileName);if (!file.exists()||!file.getName().endsWith(".mp4")) {response.getOutputStream().close();return;}String range = request.getHeader("Range");log.info("Range:" + range);if (range != null && range.length() > 7) {log.info("该请求符合断点续传");range = range.substring(6);String[] arr = range.split("-");long lenStart = Long.parseLong(arr[0]);long end=0;if (arr.length > 1) {end = Long.parseLong(arr[1]) ;}long contentLength=end>0?(end-(lenStart-1)):(file.length()-(lenStart>0?lenStart-1:0));//如果指定范围,就返回范围数据长度,如果没有就返回剩余全部长度response.setHeader("Content-Length", String.valueOf(contentLength));
//            response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");//加上会报错,不能用中文response.setHeader("Content-Range", "bytes " + lenStart + "-" + (end>0?end:(file.length() - 1)) + "/" + file.length());response.setContentType("video/mp4");response.setHeader("Accept-Ranges", "bytes");response.setStatus(HttpStatus.PARTIAL_CONTENT.value());//响应码206表示响应内容为部分数据,需要多次请求RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");randomAccessFile.seek(lenStart);//设置读取的开始字节数if(end>0){//客户端指定了范围的数据,那就只给范围数据int size= (int) (end-lenStart+1);byte[] buffer = new byte[size];int len = randomAccessFile.read(buffer);if (len != -1) {response.getOutputStream().write(buffer, 0, len);}}else{//没有指定范围//视频每次返回一兆数据int size = 1048576;//1MBbyte[] buffer = new byte[size];int len ;while ((len = randomAccessFile.read(buffer)) != -1) {response.getOutputStream().write(buffer, 0, len);}}randomAccessFile.close();}else{log.info("该请求不符合断点续传");response.setHeader("Content-Disposition", "attachment; filename=\"" +System.currentTimeMillis()+".mp4" + "\"");//不能用中文response.setHeader("Content-Length", String.valueOf(file.length()-1));response.setHeader("Content-Range", "" + (file.length()-1));response.setHeader("Accept-Ranges", "bytes");InputStream inStream=new FileInputStream(file);byte[] buffer = new byte[1024];int len;while ((len = inStream.read(buffer)) != -1) {response.getOutputStream().write(buffer, 0, len);}inStream.close();}response.getOutputStream().flush();response.getOutputStream().close();}

 附上测试的vue代码,当然里面有丰富的video的监听事件

<template><div><video ref="video"controls@loadedmetadata="loadedmetadata"@canplay="canplay"@waiting="waiting"@timeupdate="timeupdate"@play="play"@pause="pause"@ended="ended"style="width: 400px;height: 200px;"><source :src="getMp4Url(displayName)"  type="video/mp4">您的浏览器不支持 HTML5 video 标签。</video><div>当前时长:{{formatTime(nowTime)}}</div><div>总时长:{{formatTime(totalTime)}}</div><div><button @click="playPause">{{!displayStatus?'播放':'暂停'}}</button></div></div></template><script>
import config from "@/config";
export default {name: "VideoIndex",data(){return{displayName: '最伟大的作品',displayStatus:false,nowTime: 0,//当前正在播放的时间,单位:秒,带三位小数totalTime:0,//视频的总长度,单位:秒,带三位小数videoWidth:0,//视频宽度videoHeight:0,//视频宽度}},mounted(){// this.$refs.video.onloadstart =(e)=> {//   //在浏览器开始寻找指定视频/音频(audio/video)触发//   console.log("onloadstart",e)// }// this.$refs.video.onprogress =(e)=> {//   //在浏览器下载指定的视频/音频(audio/video)时触发//   console.log("onprogress",e)// }// this.$refs.video.ondurationchange =(e)=> {//   //事件在视频/音频(audio/video)的时长发生变化时触发//   console.log("ondurationchange",e)// }// this.$refs.video.onloadeddata =(e)=> {//   //事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发//   console.log("onloadeddata",e)// }// this.$refs.video.oncanplaythrough =(e)=> {//   //可以正常播放且无需停顿和缓冲时触发//   console.log("oncanplaythrough",e)// }},methods:{getMp4Url(name){return config.BASE_URL+"/video/display/"+name},playPause(){//播放状态切换if(this.$refs.video.paused){this.$refs.video.play();}else{this.$refs.video.pause();}},waiting(){//转圈的时候才会调用,秒加载好像不会触发console.log("加载中");},loadedmetadata(){this.totalTime=this.$refs.video.duration;console.log("获取视频总时间长度:"+this.formatTime(this.totalTime));},canplay(){//表示视频已经加载好了//这可以获取视频真是高度和宽度,this.videoWidth=this.$refs.video.videoWidththis.videoHeight=this.$refs.video.videoHeightconsole.log("视频已准备好了,可以播放,宽度:"+this.videoWidth+",高度:"+this.videoHeight)},play(){this.displayStatus=true;console.log("开始播放");},pause(){console.log("暂停播放");this.displayStatus=false;},ended(){console.log("播放结束");},timeupdate(){ //播放的时间戳更新this.nowTime=this.$refs.video.currentTime},formatTime(time){let temp=time; //302.432slet s= Math.ceil(temp%60); //0.01会进位+1temp=temp/60;let m=Math.floor(temp%60);let h=Math.floor(temp/60);return `${h>9?h:("0"+h)}:${m>9?m:("0"+m)}:${s>9?s:("0"+s)}`},},
}
</script><style scoped></style>

相关文章:

springboot编写mp4视频播放接口

简单粗暴方式 直接读取指定文件&#xff0c;用文件流读取视频文件&#xff0c;输出到响应中 GetMapping("/display1/{fileName}")public void displayMp41(HttpServletRequest request, HttpServletResponse response,PathVariable("fileName") String fi…...

华为OD机试真题 JavaScript 实现【机器人活动区域】【2023Q1 200分】,附详细解题思路

目录 一、题目描述二、输入描述三、输出描述四、解题思路五、JavaScript算法源码六、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试&am…...

C++中的静态分配和动态分配

为什么不是 LaoJiaHelper mydalnew LaoJiaHelper (); 而是LaoJiaHelper mydal&#xff1f; 这个都没有new &#xff0c;对象为什么能用&#xff1f;在 C 中&#xff0c;有两种创建对象的方式&#xff1a;静态分配和动态分配。 静态分配&#xff1a; 当你使用类似 LaoJiaHelpe…...

【Android常见问题(五)】- Flutter项目性能优化

文章目录 知识回顾前言源码分析1. 渲染过程2. 分析工具3. 优化方法合理使用const关键词合理使用组件管理着色器编译垃圾 知识回顾 前言 项目迭代开发一定程度后&#xff0c;性能优化是重中之重&#xff0c;其中包括了包体积&#xff0c;UI 渲染、交互等多个方面。 通过 Flutt…...

JSON转换:实体类和JSONObject互转,List和JSONArray互转(fastjson版)

//1.java对象转化成String String sJSONObject.toJSONString(javaObject.class); //2. java对象转化成Object Object strJSONObject.toJSON(javaObject.class); //3.String类型转json对象 JSONObject jsonObject JSONObject.parseObject(str); //4. String…...

Java单例模式几种代码详解

在软件开发中&#xff0c;单例模式是一种常见的设计模式&#xff0c;它的目的是确保一个类在任何情况下都只有一个实例&#xff0c;同时提供一个全局访问点。在Java中&#xff0c;有几种常见的实现单例模式的方式&#xff0c;下面将逐一进行详细解释。 懒汉式&#xff08;非线…...

PHP代码审计--理论

提供资料&#xff1a; php 基础 : https://www.runoob.com/php/php-tutorial.html php是什么&#xff1f; PHP 是服务器端脚本语言。 首先在学习PHP前需要对HTML 和CSS有一定的认识 PHP 能做什么&#xff1f; PHP 可以生成动态页面内容PHP 可以创建、打开、读取、写入、关…...

在云服务器上,clone github时报Connection timed outexit code: 128

文章目录 问题解决方案 问题 在执行pip install安装依赖时&#xff0c;需要clone github代码&#xff0c;此时报了Connection timed out&exit code: 128错误&#xff0c;原因是访问超时了&#xff0c;此时需要使用代理 fatal: unable to access https://github.com/hugg…...

小型双轮差速底盘寻迹功能的实现

1. 功能说明 寻迹机器人是一种能够跟踪特定物体或线路的机器人。它们通常具有以下功能和特点&#xff1a; ① 传感器&#xff1a;寻迹机器人配备了用于感知环境的传感器&#xff0c;如摄像头、灰度传感器等。这些传感器可以探测地面上的标记、颜色、纹理或其他特定特征&#xf…...

第七篇:k8s集群使用helm3安装Prometheus Operator

安装Prometheus Operator 目前网上主要有两种安装方式&#xff0c;分别为&#xff1a;1. 使用kubectl基于manifest进行安装 2. 基于helm3进行安装。第一种方式比较繁琐&#xff0c;需要手动配置yaml文件&#xff0c;特别是需要配置pvc相关内容时&#xff0c;涉及到的yaml文件太…...

Chrome 75不支持保存成mhtml的解决方法

在Chrome 75之前&#xff0c;可以设置chrome://flags -> save as mhtml来保存网页为mhtml。 升级新版&#xff0c;发现无法另存为/保存网页为MHTML了。 在网上搜索无果后&#xff0c;只得从chromium项目的commits中查找&#xff0c;原来chrome搞了个"Chrome Flag Owner…...

工程监测振弦采集仪应用于岩土工程监测案例

振弦采集仪是一种用于测量地面或岩土中振动参数的仪器&#xff0c;可以对地基、土壤和岩体的性质及其变化进行监测。在岩土工程监测中&#xff0c;振弦传感器被广泛应用于测量土体或岩体的振动情况&#xff0c;以了解地震或其他自然灾害的影响。 以下是一个振弦采集仪应用岩土工…...

配置HDFS单机版,打造数据存储的强大解决方案

目录 简介&#xff1a;步骤&#xff1a;安装java下载安装hadoop配置hadoop-env.sh配置 core-site.xml配置hdfs-site.xml初始化hdfs文件系统启动hdfs服务验证hdfs 结论&#xff1a; 简介&#xff1a; Hadoop分布式文件系统&#xff08;HDFS&#xff09;是Hadoop生态系统中的一个…...

U盘删除的文件怎么找回?4个简单方法分享!

“在u盘里不小心删除的文件到底还能不能找回来呀&#xff1f;真的好着急啊&#xff01;这个u盘对我来说真的很重要&#xff0c;怎么恢复里面的数据呢&#xff1f;请各位大佬帮帮我吧&#xff01;” 作为一个便捷的存储工具&#xff0c;u盘逐渐获得大众的青睐。在互联网时代&…...

【雕爷学编程】MicroPython动手做(27)——物联网之掌控板小程序2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…...

形参动态内存开辟和柔性数组

//柔性数组 //定义&#xff1a;结构体最后一个成员允许是未知大小的数组 // 优点;在开辟空间时&#xff0c;连续开辟&#xff0c;便于释放空间&#xff0c;不会因多次开辟&#xff0c;导致释放空间出错 // 开辟空间时&#xff0c;节省动态开辟次数&#xff0c;节省空间&am…...

【LLM系列之指令微调】长话短说大模型指令微调的“Prompt”

1 指令微调数据集形式“花样”太多 大家有没有分析过 prompt对模型训练或者推理的影响&#xff1f;之前推理的时候&#xff0c;发现不加训练的时候prompt&#xff0c;直接输入模型性能会变差的&#xff0c;这个倒是可以理解。假如不加prompt直接训练&#xff0c;是不是测试的时…...

MacOS使用brew如何下载Nginx

首先&#xff0c;第一步切换源&#xff1a; 切换 brew.git 仓库地址&#xff1a; cd "$(brew --repo)" git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git 替换 homebrew-core.git 仓库地址: cd "$(brew --repo)/Library/Taps/home…...

linux ftp

使用ftp连接本机进行文件传输 1、下载vsftpd服务器程序 apt install vsftpd 2、使用tcp抓包 tcpdump -nt -i lo port 20 在FTP连接到本地主机&#xff08;127.0.0.1&#xff09;时&#xff0c;数据可能通过本地回环接口&#xff08;loopback interface&#xff09;传输&…...

你知道HTTP与HTTPS有什么区别吗?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、什么是HTTP&#xff1f; 二、什么是HTTPS&#xff1f; 三、HTTPS 的工作原理 1、客户端发起 HTTPS 请求 2、服务端的配置 3、…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统

目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索&#xff08;基于物理空间 广播范围&#xff09;2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

【JVM面试篇】高频八股汇总——类加载和类加载器

目录 1. 讲一下类加载过程&#xff1f; 2. Java创建对象的过程&#xff1f; 3. 对象的生命周期&#xff1f; 4. 类加载器有哪些&#xff1f; 5. 双亲委派模型的作用&#xff08;好处&#xff09;&#xff1f; 6. 讲一下类的加载和双亲委派原则&#xff1f; 7. 双亲委派模…...

【C++】纯虚函数类外可以写实现吗?

1. 答案 先说答案&#xff0c;可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...

MySQL的pymysql操作

本章是MySQL的最后一章&#xff0c;MySQL到此完结&#xff0c;下一站Hadoop&#xff01;&#xff01;&#xff01; 这章很简单&#xff0c;完整代码在最后&#xff0c;详细讲解之前python课程里面也有&#xff0c;感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...

xmind转换为markdown

文章目录 解锁思维导图新姿势&#xff1a;将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件&#xff08;ZIP处理&#xff09;2.解析JSON数据结构3&#xff1a;递归转换树形结构4&#xff1a;Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...

多元隐函数 偏导公式

我们来推导隐函数 z z ( x , y ) z z(x, y) zz(x,y) 的偏导公式&#xff0c;给定一个隐函数关系&#xff1a; F ( x , y , z ( x , y ) ) 0 F(x, y, z(x, y)) 0 F(x,y,z(x,y))0 &#x1f9e0; 目标&#xff1a; 求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂x∂z​、 …...

用递归算法解锁「子集」问题 —— LeetCode 78题解析

文章目录 一、题目介绍二、递归思路详解&#xff1a;从决策树开始理解三、解法一&#xff1a;二叉决策树 DFS四、解法二&#xff1a;组合式回溯写法&#xff08;推荐&#xff09;五、解法对比 递归算法是编程中一种非常强大且常见的思想&#xff0c;它能够优雅地解决很多复杂的…...

深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙

WebGL&#xff1a;在浏览器中解锁3D世界的魔法钥匙 引言&#xff1a;网页的边界正在消失 在数字化浪潮的推动下&#xff0c;网页早已不再是静态信息的展示窗口。如今&#xff0c;我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室&#xff0c;甚至沉浸式的V…...

Java并发编程实战 Day 11:并发设计模式

【Java并发编程实战 Day 11】并发设计模式 开篇 这是"Java并发编程实战"系列的第11天&#xff0c;今天我们聚焦于并发设计模式。并发设计模式是解决多线程环境下常见问题的经典解决方案&#xff0c;它们不仅提供了优雅的设计思路&#xff0c;还能显著提升系统的性能…...