MInIO入门-04 基于minio+ckplayer视频点播 实现
1、ckplayer(前端js播放器)
1.1、官网
https://www.ckplayer.com/
2、后台实现
2.1、参考博客
https://blog.csdn.net/waleit/article/details/117693364
2.2、通过request的Range来实现
2.3、后台代码
package cn.lyf.minio.controller;
import cn.lyf.minio.core.MinioTemplate;
import cn.lyf.minio.entity.MinioObject;
import io.minio.StatObjectResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
/**
* @author lyf
* @description:
* @version: v1.0
* @since 2022-05-12 14:07
*/
@RestController
@Slf4j
@RequestMapping(value = "/video")
public class VideoController {
private static final String OBJECT_INFO_LIST = "cn:lyf:minio:media:object:info:list";
@Autowired
private MinioTemplate minioTemplate;
@Resource(name = "jsonRedisTemplate")
private RedisTemplate<String, Serializable> jsonRedisTemplate;
/**
* 支持分段读取视频流
*
* @param request 请求对象
* @param response 响应对象
*/
@GetMapping(value = "/play")
public void getVideoOutStream(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "bucketName") String bucketName,
@RequestParam(value = "objectName") String objectName) {
// 设置响应报头
// 需要查询redis
String key = bucketName + ":" + objectName;
Object obj = jsonRedisTemplate.boundHashOps(OBJECT_INFO_LIST).get(key);
MinioObject minioObject;
if (obj == null) {
StatObjectResponse objectInfo = minioTemplate.getObjectInfo(bucketName, objectName);
// 存入Redis中
if (objectInfo == null) {
throw new IllegalArgumentException(key + "不存在");
}
// 判断是否是视频,是否为mp4格式
String filenameExtension = StringUtils.getFilenameExtension(objectName);
if (!"mp4".equalsIgnoreCase(filenameExtension)) {
throw new IllegalArgumentException("不支持的媒体类型");
}
minioObject = new MinioObject();
BeanUtils.copyProperties(objectInfo, minioObject);
jsonRedisTemplate.boundHashOps(OBJECT_INFO_LIST).put(key, minioObject);
} else {
minioObject = (MinioObject) obj;
}
long fileSize = minioObject.getSize();
// Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
//pos开始读取位置; last最后读取位置
long startPos = 0;
long endPos = fileSize - 1;
String rangeHeader = request.getHeader("Range");
if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {
try {
// 情景一:RANGE: bytes=2000070- 情景二:RANGE: bytes=2000070-2000970
String numRang = request.getHeader("Range").replaceAll("bytes=", "");
if (numRang.startsWith("-")) {
endPos = fileSize - 1;
startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1,
numRang.length() - 1)) + 1;
} else if (numRang.endsWith("-")) {
endPos = fileSize - 1;
startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0,
numRang.length() - 1));
} else {
String[] strRange = numRang.split("-");
if (strRange.length == 2) {
startPos = Long.parseLong(strRange[0].trim());
endPos = Long.parseLong(strRange[1].trim());
} else {
startPos = Long.parseLong(numRang.replaceAll("-", "").trim());
}
}
if (startPos < 0 || endPos < 0 || endPos >= fileSize || startPos > endPos) {
// SC 要求的范围不满足
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
// 断点续传 状态码206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
} catch (NumberFormatException e) {
log.error(request.getHeader("Range") + " is not Number!");
startPos = 0;
}
}
// 总共需要读取的字节
long rangLength = endPos - startPos + 1;
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, fileSize));
response.addHeader("Content-Length", String.valueOf(rangLength));
response.addHeader("Content-Type", "video/mp4");
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
BufferedInputStream bis = new BufferedInputStream(
minioTemplate.getObject(bucketName, objectName, startPos, rangLength))) {
IOUtils.copy(bis, bos);
} catch (
IOException e) {
if (e instanceof ClientAbortException) {
// ignore
} else {
log.error(e.getMessage());
}
}
}
}
2.4、相关依赖代码见博客
https://blog.csdn.net/lyf_zm/article/details/124627842
3、测试使用
3.1、包结构
3.2、video.html
3.2.1、创建视频对象
// 创建视频对象
let videoObject = {
container: '.video',//视频容器的ID
volume: 0.8,//默认音量,范围0-1
video: 'http://localhost:18002/video/play?bucketName=minio-test&objectName=488b4f54-a5be-4796-883c-954e0cd83742.mp4',//视频地址
};
// 调用播放器并赋值给变量player
let player = new ckplayer(videoObject)
3.2.2、完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ckplayer</title>
<link rel="shortcut icon" href="#"/>
<link type="text/css" rel="stylesheet" href="../static/ckplayer/ckplayer/css/ckplayer.css"/>
<!--
如果需要使用其它语言,请在此处引入相应的js,比如:<script type="text/javascript" src="ckplayer/language/en.js" charset="UTF-8"></script>
-->
<script type="text/javascript" src="../static/ckplayer/ckplayer/js/ckplayer.min.js" charset="UTF-8"></script>
<div class="video" style="width: 100%; height: 500px;max-width: 800px;">播放容器</div>
<script>
//调用开始
let videoObject = {
container: '.video',//视频容器的ID
volume: 0.8,//默认音量,范围0-1
video: 'http://localhost:18002/video/play?bucketName=minio-test&objectName=488b4f54-a5be-4796-883c-954e0cd83742.mp4',//视频地址
};
let player = new ckplayer(videoObject)//调用播放器并赋值给变量player
/*
* ===============================================================================================
* 以上代码已完成调用演示,下方的代码是演示监听动作和外部控制的部分
* ===============================================================================================
* ===============================================================================================
*/
player.play(function () {
document.getElementById('state').innerHTML = '监听到播放';
});
player.pause(function () {
document.getElementById('state').innerHTML = '监听到暂停';
});
player.volume(function (vol) {
document.getElementById('state').innerHTML = '监听到音量改变:' + vol;
});
player.muted(function (b) {
document.getElementById('state2').innerHTML = '监听到静音状态:' + b;
});
player.full(function (b) {
document.getElementById('state').innerHTML = '监听到全屏状态:' + b;
});
player.ended(function () {
document.getElementById('state').innerHTML = '监听到播放结束';
});
</script>
</head>
<body>
<p>官网:<a href="https://www.ckplayer.com" target="_blank">www.ckplayer.com</a></p>
<p>手册:<a href="https://www.ckplayer.com/manual/" target="_blank">www.ckplayer.com/manual/</a></p>
<p>社区:<a href="https://bbs.ckplayer.com/" target="_blank">bbs.ckplayer.com</a></p>
<p>全功能演示:<a href="https://www.ckplayer.com/demo.html" target="_blank">www.ckplayer.com/demo.html</a></p>
<p>控制示例:</p>
<p>
<button type="button" onclick="player.play()">播放</button>
<button type="button" onclick="player.pause()">暂停</button>
<button type="button" onclick="player.seek(20)">跳转</button>
<button type="button" onclick="player.volume(0.6)">修改音量</button>
<button type="button" onclick="player.muted()">静音</button>
<button type="button" onclick="player.exitMuted()">恢复音量</button>
<button type="button" onclick="player.full()">全屏</button>
<button type="button" onclick="player.webFull()">页面全屏</button>
<button type="button" onclick="player.theatre()">剧场模式</button>
<button type="button" onclick="player.exitTheatre()">退出剧场模式</button>
</p>
<p id="state"></p>
<p id="state2"></p>
</body>
</html>