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原生的,请求头会有一个Range: bytes=- , 表示该请求希望返回的数据是从位置开始即可 。
实现一共两点:
(1)响应头部添加如下格式响应头
-Range: bytes -/
大概就是:-Range: bytes 请求头指定的开始字节数-本次返回的文件字节位置/总共多少字节,值得注意的是 , 本次返回的文件字节位置一定要比总字节数至少少一个字节,否则视频缓存结束的最后一次数据无法播放,可能是浏览器出了异常,视频会重新播放 。本次返回的文件字节位置可以不准 , 因为浏览器会自动记录真实拿到的字节数量,但一定要少一个字节 。

springboot编写mp4视频播放接口

文章插图
(2)响应码改为206
有了这两点就可以实现正常的视频播放接口了 。
优化
考虑到视频的进度条很大概览是会被拖来拖去的,导致频繁请求接口 。
假设你的文件200MB,频繁的请求每次都会把整个文件读入http流中 , 如果用户网速慢 , 或者浏览器的缓存策略会阻塞http请求,慢慢从http响应中读取这部分数据 , 这可能就会使数据都堆积到服务器内存里(本人毫无根据瞎想的),浪费资源 。
(1)因为需要指定字节位置读取视频文件 , 使用随机读取类来操作 。
【springboot编写mp4视频播放接口】(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服务器上直接访问,这俩浏览器没问题,那一定是接口兼容性的问题 。
springboot编写mp4视频播放接口

文章插图
通过抓包,不断测试分析,最后大概摸清了问题所在:
(1)对于普通的web服务器是可以用的,浏览器通过能够信任服务器的连接,有计划的指定获取字节数范围 , 提供给服务器起始和结束两个字节的位置
(2)可能配置的原因,不会记录,导致浏览器某些原因不会有计划的分段获?。腔岣穸艘桓鲎约旱鼻耙驯4娴降淖纸谖恢?nbsp;, 希望服务器能够每次都返回剩余的全部字节数 。然后浏览器自己来确定要多少 。
所以,当请求头只指明了起始位置,但是没有结束的位置 , 就是需要服务端把所有数据都给返回 。
如果指明了起始位置,也指明了结束位置,就需要按照要求返回对应的字节数 。
这里需要注意的是,-要在返回数据之前预先加到响应头上,这个是可以计算出来的 , 计算要准确,不加上这个也会出兼容性问题 。
有了上述这些条件,基本上在各个浏览器就可以正常的视频播放了,如果想通过该接口直接下载视频,还做不到,因为普通下载好像不会用这套协议,需要判断请求头,有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的监听事件
当前时长:{{formatTime(nowTime)}}总时长:{{formatTime(totalTime)}}