学习链接
vue+springboot文件分片上传与边放边播实现
同步加载、播放视频的实现 ---- range blob mediaSource
通过调试技术,我理清了 b 站视频播放很快的原理
MSE (Media Source Extensions) 上手指南
浅聊音视频的媒体扩展(Media Source Extension)
今天又学到一种播放视频的方法,先把视频分块的都获取到,然后再使用URL.createObject(chunks)创建blobUrl,再把这个blobUrl给到video标签即可 播放视频(可以拖动视频进度条)。但是这种缺点也很明显,得把所有视频文件分块获取完成后,才能播放视频。体验上就不是很好。后面有时间可以看下MediaSource相关(能否实现边下载边播放,而不是非得等到全部下载完了再播放?)
VideoPlay.vue
<template>
<div class="release_wrap">
<el-card class="release_card">
<el-table stripe :data="tableData" style="width: 100%" height="600px">
<el-table-column prop="videoName" label="视频名称" min-width="280">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="playVideo(scope.$index, scope.row)">播放</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog :modal="false" title="视频播放" :visible.sync="dialogVisible" width="40%">
<video :src="videoUrl" controls="controls" width="100%" @canplay="getVidDur()" id="myvideo"></video>
</el-dialog>
</div>
</template>
<script>
var video = () => {
var videoTime = document.getElementById("myvideo");
console.log(videoTime.duration); //获取视频时长
console.log(videoTime.currentTime); //获取视频当前播放时间
};
export default {
data() {
return {
title: "",
videolist: "",
//表格数据
tableData: [],
//弹框组件隐藏
dialogVisible: false,
//用于保存视频的id
videoId: 0,
//保存视频的名称
videoName: '',
videoUrl: '',
};
},
created() {
this.getVideoInfo();
},
methods: {
jump_home() {
this.$router.replace('/')
},
getVidDur() {
video();
},
//获取video表格数据
getVideoInfo() {
this.$axios.get("http://127.0.0.1:9098/SelectVideo/table").then((res) => {
this.tableData = res.data;
});
},
// 点击播放按钮
playVideo(i, val) {
// 显示弹框
this.dialogVisible = true;
// 保存视频名字
this.videoName = val.videoName;
// 保存视频id
this.videoId = val.id;
// 发送HEAD请求获取视频的总大小
this.$axios.get(`http://127.0.0.1:9098/SelectVideo/getVideoSizeById/${this.videoId}`).then(res => {
const totalSize = res.data;
const chunkSize = Math.ceil(totalSize / 20); // 设置分片大小为总大小的1/5
// 定义分片传输的函数
const loadVideoChunk = (startByte, endByte) => {
return new Promise((resolve, reject) => {
this.$axios.get(`http://127.0.0.1:9098/SelectVideo/policemen/${this.videoId}`, {
headers: {
Range: `bytes=${startByte}-${endByte}`
},
responseType: 'blob'
}).then(response => {
// 返回获取到的视频分片数据
resolve(response.data);
}).catch(error => {
reject(error);
});
});
};
// 创建一个数组来保存所有分片的Promise
const chunkPromises = [];
// 获取所有分片的Promise
for (let i = 0; i < 20; i++) {
const startByte = i * chunkSize;
const endByte = Math.min(startByte + chunkSize - 1, totalSize - 1);
chunkPromises.push(loadVideoChunk(startByte, endByte));
}
// 执行所有分片请求,并在全部请求完成后开始播放视频
Promise.all(chunkPromises).then(chunks => {
// 将分片数据合并成完整的视频Blob
const videoBlob = new Blob(chunks);
const videoUrl = URL.createObjectURL(videoBlob);
this.videoUrl = videoUrl;
}).catch(error => {
console.error('Failed to load video:', error);
});
}).catch(error => {
console.error('Failed to get video size:', error);
});
},
},
};
</script>
<style></style>
SelectVideoController
//查询视频流的接口
@GetMapping("/policemen/{videoId}")
public void videoPreview(HttpServletRequest request, HttpServletResponse response, @PathVariable("videoId") String videoId) throws Exception
{
System.out.println(videoId);
VideoUpload videoPathList = videoUploadMapper.SelectVideoId(Integer.parseInt(videoId));
String videoPathUrl = videoPathList.getVideoUrl();
Path filePath = Paths.get(videoPathUrl);
if (Files.exists(filePath))
{
String mimeType = Files.probeContentType(filePath);
if (StringUtils.hasText(mimeType))
{
response.setContentType(mimeType);
}
// 设置支持部分请求(范围请求)的 'Accept-Ranges' 响应头
response.setHeader("Accept-Ranges", "bytes");
// 从请求头中获取请求的视频片段的范围(如果提供)
long startByte = 0;
long endByte = Files.size(filePath) - 1;
String rangeHeader = request.getHeader("Range");
// System.out.println("rangeHeader:" + rangeHeader);
if (rangeHeader != null && rangeHeader.startsWith("bytes="))
{
String[] range = rangeHeader.substring(6).split("-");
startByte = Long.parseLong(range[0]);
if (range.length == 2)
{
endByte = Long.parseLong(range[1]);
}
}
// System.out.println("start:" + startByte + ",end:" + endByte);
log.info("start:" + startByte + ",end:" + endByte);
// 设置 'Content-Length' 响应头,指示正在发送的视频片段的大小
long contentLength = endByte - startByte + 1;
response.setHeader("Content-Length", String.valueOf(contentLength));
// 设置 'Content-Range' 响应头,指示正在发送的视频片段的范围
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + Files.size(filePath));
// 设置响应状态为 '206 Partial Content'
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// 使用 'RangeFileChannel' 进行视频片段的传输,以高效地只读取文件的请求部分
ServletOutputStream outputStream = response.getOutputStream();
try (RandomAccessFile file = new RandomAccessFile(filePath.toFile(), "r"); FileChannel fileChannel = file.getChannel())
{
fileChannel.transferTo(startByte, contentLength, Channels.newChannel(outputStream));
} finally
{
outputStream.close();
}
} else
{
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}