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来实现

image-20220512141841099

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、包结构

image-20220512142346657

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>

3.3、播放

image-20220512143437637

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐