简单粗暴方式
直接读取指定文件,用文件流读取视频文件,输出到响应中
@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();
}
附上测试的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.videoWidth
this.videoHeight=this.$refs.video.videoHeight
console.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.432s
let s= Math.ceil(temp%60); //0.01会进位+1
temp=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>