从文件加密到到视频文件进度条播放揭秘

Posted 汪小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从文件加密到到视频文件进度条播放揭秘相关的知识,希望对你有一定的参考价值。

文件加密

使用 Cipher CipherInputStream CipherOutputStream 实现对文件的加解密
每个文件使用一个秘钥 String aesKey = UUID.randomUUID().toString().replace("-",""); 可以通过uuid or 其他的途径生成一个唯一的秘钥。

 private static final String ALGORITHM_STREAM = "AES/ECB/PKCS5Padding";

    /**
     * 加密数据
     *
     * @param input
     * @param key
     * @return
     * @throws Exception
     */
    public static InputStream encodeStream(InputStream input, String key) throws Exception 
        SecretKey secretKey = generateAesKey(key);

        Cipher c = Cipher.getInstance(ALGORITHM_STREAM);

        c.init(1, secretKey);
        return new CipherInputStream(input, c);
    


    /**
     * 解密文件流信息
     *
     * @param input
     * @param key
     * @return
     * @throws Exception
     */
    public static InputStream decodeStream(InputStream input, String key) throws Exception 
        SecretKey secretKey = generateAesKey(key);

        Cipher c = Cipher.getInstance(ALGORITHM_STREAM);

        c.init(2, secretKey);
        return new CipherInputStream(input, c);
    


    private static SecretKey generateAesKey(String key) 
        try 
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
            secureRandom.setSeed(key.getBytes());
            keyGenerator.init(128, secureRandom);
            return keyGenerator.generateKey();
         catch (GeneralSecurityException e) 
            throw new RuntimeException(e);
        
    

视频分段渐进式播放

样例eg: http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4
当前这个视频播放实现随机播放、实现分块下载等等能力,一般情况下后端下载视频
http://localhost:8080/web/file-upload/common-file-download?fileId=ab87ef175dc1419b922acb35dd3ad58e
提供类似的URL地址 后端直接写流到浏览器 IOUtils.copy(encodeInputStream, response.getOutputStream());
当点击视频中进度条的时候永远都不行,点击进度条相当于重新请求、视频流信息重0开始,直接破坏了进度条的规则。

查询了一些资料,其实视频播放的时候通过Range 进行了分段的请求、我们可以对于流量进行分段的下载处理

206 状态码 & Content-Range 分段的响应下载,读取当前流信息中指定开始到指定长度的流信息
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());

    @GetMapping(value = "/common-file-download")
    @ResponseBody
    public void commonFileDownload(@RequestParam String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception 
        File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
        //获取从那个字节开始读取文件
        try (FileInputStream inputStream = new FileInputStream(desc)) 
            InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
            int fSize = FILE_LENGTH;

            if (this.haveRanges(request)) 
                // 断点续传
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
                //开始下载位置
                int startByte = rangeInfo.getRight();
                //结束下载位置
                int endByte = rangeInfo.getLeft();
                //要下载的长度
                int contentLength = endByte - startByte + 1;
                //Content-Length 表示资源内容长度,即:文件大小
                response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
                //Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
                response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);

                String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
                String mimetype = Mimetypes.getInstance().getMimetype(fileName);
                response.setContentType(mimetype);
                IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
                return;

            
            String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
            String mimetype = Mimetypes.getInstance().getMimetype(fileName);
            response.setContentType(mimetype);
            IOUtils.copy(encodeInputStream, response.getOutputStream());
         catch (ClientAbortException e) 
            log.debug("ignore ", e.getMessage());
         catch (Exception e) 
            log.error("error", e);
         finally 
            IOUtils.close(response.getOutputStream());
        
    


    /**
     * 获取 range 的长度信息
     *
     * @param range
     * @param fileSize
     * @param defaultRangeLengthSize
     * @return
     */
    public MutablePair<Integer, Integer> getRangeInfo(String range, int fileSize, int defaultRangeLengthSize) 
        MutablePair<Integer, Integer> rangeInfo = new MutablePair<>();
        rangeInfo.setLeft(fileSize - 1);
        rangeInfo.setRight(0);
        if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) 
            range = range.substring(range.lastIndexOf("=") + 1).trim();
            String[] ranges = range.split("-");
            int startByte = 0;
            int endByte = fileSize - 1;
            try 
                //根据range解析下载分片的位置区间
                if (ranges.length == 1) 
                    //情况1,如:bytes=-1024  从开始字节到第1024个字节的数据
                    if (range.startsWith("-")) 
                        endByte = Integer.parseInt(ranges[0]);
                    
                    //情况2,如:bytes=1024-  第1024个字节到最后字节的数据
                    else if (range.endsWith("-")) 
                        startByte = Integer.parseInt(ranges[0]);
                        //增加一个默认的信息
                        endByte = startByte + defaultRangeLengthSize;
                        if (endByte >= fileSize - 1) 
                            endByte = fileSize - 1;
                        
                    
                
                //情况3,如:bytes=1024-2048  第1024个字节到2048个字节的数据
                else if (ranges.length == 2) 
                    startByte = Integer.parseInt(ranges[0]);
                    endByte = Integer.parseInt(ranges[1]);
                
             catch (NumberFormatException e) 
                startByte = 0;
                endByte = fileSize - 1;
            
            rangeInfo.setRight(startByte);
            rangeInfo.setLeft(endByte);
        
        return rangeInfo;
    

    /**
     * 是否有Range
     *
     * @param request
     * @return
     */
    public boolean haveRanges(HttpServletRequest request) 
        String range = request.getHeader(HttpHeaders.RANGE);
        if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) 
            return true;
        
        return false;
    

感觉一切都很好了… 本地文件也可以了…

块存储文件服务器加密文件分块下下载

第一个版本 【进度条点击越靠后的时候 响应的时间越来越长】

下载非常的慢
http://localhost:8080/web/file-upload/version1-file-download

为什么?主要是下面这个代码每次都需要下载跳过的流文件进行解密,随着长度的加深 速度越来越慢
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray())

/**
     * 下载比较慢
     *
     * @param fileId
     * @param request
     * @param response
     * @throws Exception
     */
    @GetMapping(value = "/version1-file-download")
    @ResponseBody
    public void version1(@RequestParam(required = false) String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception 
        File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
        fileId = "Qq87r0SidfdM9i5QDrtKLTbRpcGam0qd";
        S3Object object = s3.getObject(BUCK_NAME, fileId);
        try (InputStream inputStream = object.getObjectContent()) 
            InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
            int fSize = FILE_LENGTH;

            if (this.haveRanges(request)) 
                // 断点续传
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
                //开始下载位置
                int startByte = rangeInfo.getRight();
                //结束下载位置
                int endByte = rangeInfo.getLeft();
                //要下载的长度
                int contentLength = endByte - startByte + 1;
                //Content-Length 表示资源内容长度,即:文件大小
                response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
                //Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
                response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);

                String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
                String mimetype = Mimetypes.getInstance().getMimetype(fileName);
                response.setContentType(mimetype);
                IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
                return;

            
            String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
            String mimetype = Mimetypes.getInstance().getMimetype(fileName);
            response.setContentType(mimetype);
            IOUtils.copy(encodeInputStream, response.getOutputStream());
         catch (ClientAbortException e) 
            log.debug("ignore ", e.getMessage());
         catch (Exception e) 
            log.error("error", e);
         finally 
            IOUtils.close(response.getOutputStream());
        
    

第二个版本 偶尔出行错误 & aes 解密 没有16块 & 播放偶现突然停止

http://localhost:8080/web/file-upload/version2-file-download

为了解决下载响应非常慢的问题,需要通过s3 支持的分块下载
支持通过header 进行分块下载…

GetObjectRequest getObjectRequest = new GetObjectRequest(BUCK_NAME, fileId);
int startOffset = rangeInfo.getRight();
getObjectRequest.setRange(startOffset);
object = s3.getObject(getObjectRequest);

感觉好像好了,其实还有问题,任意拖住进度条一会就不能播放了…自然就停止了…
看到报错,不过这个问题很关键 下载的内容长度没有16的整数倍

java.io.IOException: javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
	at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:128)
	at java.base/javax.crypto.CipherInputStream.read(CipherInputStream.java:242)
	at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1384)
	at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1342)
**
     * 下载偶尔失败
     *
     * @param fileId
     * @param request
     * @param response
     * @throws Exception
     */
    @GetMapping(value = "/version2-file-download")
    @ResponseBody
    public void version2(@RequestParam(required = false) String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception 
        File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
        fileId = "Qq87r0SidfdM9i5QDrtKLTbRpcGam0qd";
        S3Object object = null;
        boolean isRange3s = false;
        if (haveRanges(request)) 
            GetObjectRequest getObjectRequest = new GetObjectRequest(BUCK_NAME, fileId);
            MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), FILE_LENGTH, 5 * 1024 * 1024);
            int startOffset = rangeInfo.getRight();
            getObjectRequest.setRange(startOffset);
            object = s3.getObject(getObjectRequest);
            isRange3s = true;
         else 
            object = s3.getObject(BUCK_NAME, fileId);
        
        try (InputStream inputStream = object.getObjectContent()) 
            InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
            int fSize = FILE_LENGTH;
            if (this.haveRanges(request)) 
                // 断点续传
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
                //开始下载位置
                int startByte = rangeInfo.getRight();
                //结束下载位置
                int endByte = rangeInfo.getLeft();
                //要下载的长度
                int contentLength = endByte - startByte + 1;
                //Content-Length 表示资源内容长度,即࿱

以上是关于从文件加密到到视频文件进度条播放揭秘的主要内容,如果未能解决你的问题,请参考以下文章

从文件加密到到视频文件进度条播放揭秘

从文件加密到到视频文件进度条播放揭秘

向 mp3 AVAudioPlayer 添加进度条播放 swift

opencv学习之路鼠标截图,滑动条播放视频

分享几个加密视频软件工具,比较好用

android 项目学习随笔十六( 广告轮播条播放)