IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 视频加密并上传至oss实现高并发访问 -> 正文阅读

[系统运维]视频加密并上传至oss实现高并发访问

前言

本篇文章主要是分享代码,代码实现的功能是从oss上指定bucket下的upload目录下载视频到本地,然后通过openssl生成key与iv和keyinfo文件,然后调用脚本通过ffmpeg指令切分mp4视频到当前目录下,由于是调用脚本所以程序不会等待视频切分完成,会立马往下执行,这时通过获取视频时长计算视频ts切片数量,然后统计目录下ts文件数量,一旦大于或者等于立马停止等待,遍历目录下的ts、m3u8与key文件进行上传到oss中。使用m3u8的原始是为了视频安全,使用oss是为了实现视频的高并发访问。记录一下在生成环境遇到的视频加密问题,不是很清楚加密思路的可以先看lz的这篇文章,了解加密用到的相关技术:Java实现视频加密及播放 之前我是在windows上测试视频切分功能问题不大,但是到了Linux环境发现了很多问题,例如无法通过ProcessBuild(类似于黑窗口)执行语句,主要是openssl的语句通过黑窗口执行会报错,具体原因还不是很清楚,所以通过调用脚本的方式达到目的。另外还有一个Linux环境默认安装的ffmpeg无法进行视频加密操作,需要安装一系列的依赖,安装方式可以参考下面的方式,我用过其他方式安装无法进行视频截取,生成key相关的资料可以参数这个:使用ffmpeg视频切片并加密


安装ffmpeg

yum -y install epel-release
rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-1.el7.nux.noarch.rpm
yum -y install ffmpeg ffmpeg-devel
ffmpeg -version

使用指令测试切分情况

ffmpeg -i 22.mp4 -profile:v baseline -level 3.0  -start_number 0 -hls_time 10 -hls_list_size 0 -f hls index.m3u8

22.mp4代表要切分视频名称,hls_time 代表每个ts的时长,index.m3u8代表输出的文件路径和名称


代码

本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试
本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试
本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试

相关依赖

        <!--阿里云OSS依赖-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.8.0</version>
        </dependency>
        
   		<!-- IOUtils.toString()方法用到了-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.3</version>
        </dependency>

代码实现的效果是从oss上下载视频到本地,做视频切分和加密后重传到oss中,要求oss中的bucket权限为:公开读-公开读-公开读

涉及的方法比较多,由于这是项目的代码不方便直接复制类分享,所以只能分享方法,有些方法可能在不同的类中,可以自己调整用到的类应该都分享出来了。另外lz建议先将所有的方法放到一个类中,待接口调通后放到工具类方法中,再将不同的方法防到例如FileUtil、M3u8Util、OSSUtil等工具类中

测试接口方法

    //用户存储加密切分后的视频bucket
    public static final String ENCRYPT_BUCKET_NAME = "YourBucketName";
    //生成iv文件的名称
    public static final String IV_NAME = "iv.txt";
    //keyinfo的文件名称
    public static final String ENC_KEY_INFO = "enc.keyinfo";
    //生成的加密文件名称
    public static final String ENC_KEY = "enc.key";
/**
     * 测试将oss的视频转化成m3u8并存储到本地
     * 三个参数含义:bucket名称,视频所处的路径,视频名称
     * 要求bucket的upload下必须有叫做a.mp4的视频文件
     *
     * @return
     */
    @ResponseBody
    @GetMapping("/mp4Tom3u8AndEncrypt")
    public ResponseResult mp4Tom3u8AndEncrypt() {
        mp4Tom3u8AndEncrypt("YourBacketName", "upload/", "a.mp4");
        return ResponseResult.success();
    }

oss中的文件
在这里插入图片描述
mp4Tom3u8AndEncrypt方法

  • endpoint, accessKeyId, accessKeySecret使用自己的不知道怎么获取的可以查看阿里云文档
/**
     * 将oss中的视频下载到本地,然后进行加密切分成m3u8文件
     * 生成key文件参考资料:https://www.cnblogs.com/codeAB/p/9184266.html
     *
     * @param bucketName 文件所处在的oss的bucket名称
     * @param path       文件所在的路径,例如:upload/
     * @param name       文件名称,例如:8KAnimal.mp4
     * @return
     */
    public static String mp4Tom3u8AndEncrypt(String bucketName, String path, String name) {
        //文件在oss中的路径加名称
        String objectName = path + name;
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        //判断当前oss上文件是否存在
        boolean found = ossClient.doesObjectExist(bucketName, objectName);
        if (!found) {
            throw new BusinessException(CommonExEnum.OSS_FILE_NOT_EXIST);
        }
        //将oss文件下载到本地服务器,返回值路径+文件名称
        String storagePath = downloadToLocal(bucketName, path, name);
        //截取文件暂存路径
        String storageFilePath = storagePath.substring(0, storagePath.indexOf(name));
        log.info(storageFilePath);
        //生成16位enc.key
        String encKey = generateRandom(16);
        log.info(encKey);
        //创建enc.key并将上面生成的encKey写入到文件中
        createFileWriterContent(storageFilePath, ENC_KEY, encKey);
        //生成vi文件
        generateIVFile(storageFilePath);
        //生成enc.keyinfo文件,enc.key的ng路径地址,ng本地地址,iv文件内容
        String content = ENC_KEY + "\n" +
                storageFilePath + File.separator + ENC_KEY + "\n" +
                FileUtil.readFileContent(storageFilePath + File.separator + IV_NAME);
        log.info("***keyinfo文件的内容为:{}***", content);
        createFileWriterContent(storageFilePath, ENC_KEY_INFO,
                content);

        //使用ffmpeg指令调用服务器/data/shell目录下的脚本实现视频切分和加密
        dealMp4ToM3u8AndEnc(storageFilePath + File.separator + ENC_KEY_INFO,
                storageFilePath + File.separator + name);

        //由于ffmpeg视频切分是调用服务器脚本,代码执行与脚本执行是异步操作,
        // 所以需要等待一段时间待视频切分完成后将文件上传到oss中
        int duration = calculateVideoDuration(storageFilePath + File.separator + name);
        log.info("***视频时长为:【{}】秒***", duration);

        //计算ts的数量,向上取整。20是每个ts文件的视频长度,每个是20秒
        int fragmentNum = (int) Math.ceil((float) duration / 20);
        log.info("***ts数量为:{}***", fragmentNum);

        //等待视频切割完成,通过检测文件夹下总ts文件数量与计算的ts数量是否相同判断切割是否完成
        // (切割后端ts总大小与原视频大小不一致)
        waitVideoSegmentFinish(storageFilePath, fragmentNum);

        //获取文件路径下所有ts与m3u8、enc.key文件列表
        List<String> uploadPathList = getUploadPathList(storageFilePath);

		//获取文件名称
        String filePrefix = name.substring(0, name.indexOf("."));


        //将文件上传到oss上
        for (String uploadFile : uploadPathList) {
            File file = new File(uploadFile);
            String uploadFileName = file.getName();
            //创建oss上传的路径,加密后的文件传到security下,未加密的传到upload目录下
            String fileName = "security/" + filePrefix + "/" + uploadFileName;

            try {
                String s = uploadFileWithInputSteam(ENCRYPT_BUCKET_NAME, fileName,
                        new FileInputStream(file));
                log.info("***文件上传后的绝对路径为:【{}】***", s);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
        return "";
    }

下面的方法是mp4Tom3u8AndEncrypt中用到的方法

downloadToLocal方法
注意

  • 替换endpoint, accessKeyId, accessKeySecret参数
  • /opt/nginx/html/m3u8/ 字符串换成自己服务器上nginx的html目录并且nginx.conf文件需要做配置,默认不支持播放m3u8文件,配置防止可以在Java实现视频加密及播放
/**
     * 下载到本地临时文件夹
     *
     * @param bucketName 存储名称
     * @param path       文件夹路径,例如:abc/efg/
     * @param name       文件名称,例如:123.jpg
     * @return filename 下载后的文件全路径名
     */
    public static String downloadToLocal(String bucketName, String path, String name) {
        String objectName = path + name;
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        String tempPath;
        String osName = System.getProperty("os.name");
        String fileName = name.substring(0, name.indexOf("."));
        if (osName.startsWith("Windows")) {
            tempPath = "C:\\Users\\Administrator\\Desktop\\m3u8\\codeEncrypt\\" + fileName;
        } else {
            //用于存储oss上下载下来的临时存储文件,必须是nginx的html路径,由于生成keyinfo文件需要路径参数
            tempPath = "/opt/nginx/html/m3u8/" + fileName;
        }
        File uploadFile = new File(tempPath);
        if (!uploadFile.exists()) {
            log.info("创建的文件夹目录:{}", uploadFile.getPath());
            uploadFile.mkdirs();
        }
        // 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。
        ossClient.getObject(new GetObjectRequest(bucketName, objectName),
                new File(tempPath + File.separator + name));

        // 关闭OSSClient。
        ossClient.shutdown();
        return tempPath + name;
    }

generateRandom方法
生成一定长度的字母与数字随机数

/**
     * 生成一定长度的字母与数字随机数
     *
     * @param length 长度
     * @return 随机数
     */
    public static String generateRandom(int length) {
        StringBuilder val = new StringBuilder();
        Random random = new Random();
        //参数length,表示生成几位随机数
        for (int i = 0; i < length; i++) {
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
            //输出字母还是数字
            if ("char".equalsIgnoreCase(charOrNum)) {
                //输出是大写字母还是小写字母
                int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                val.append((char) (random.nextInt(26) + temp));
            } else if ("num".equalsIgnoreCase(charOrNum)) {
                val.append(String.valueOf(random.nextInt(10)));
            }
        }
        return val.toString();
    }

createFileWriterContent方法

/**
     * 创建文件并写入内容
     *
     * @param filePath 文件路径
     * @param fileName 文件名称
     * @param content  需要写入的文件内容
     * @throws IOException
     */
    public static void createFileWriterContent(String filePath, String fileName, String content) {
        File dir = new File(filePath);
        // 一、检查放置文件的文件夹路径是否存在,不存在则创建
        if (!dir.exists()) {
            dir.mkdirs();// mkdirs创建多级目录
        }
        File checkFile = new File(filePath + File.separator + fileName);
        FileWriter writer = null;
        try {
            // 二、检查目标文件是否存在,不存在则创建
            if (!checkFile.exists()) {
                checkFile.createNewFile();// 创建目标文件
            }
            // 三、向目标文件中写入内容
            // FileWriter(File file, boolean append),append为true时为追加模式,false或缺省则为覆盖模式
            writer = new FileWriter(checkFile);
            writer.append(content);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

generateIVFile方法
注意

  • 这个方法会运行服务器中/data/shell的脚本,目录下有两个脚本,在这个方法下直接复制出来
/**
     * 生成视频播放所需要的的iv文件
     * generate-iv.sh脚本指令:openssl rand -hex 16
     *
     * @param path 生成iv文件的工作路径,不带文件名
     */
    public static void generateIVFile(String path) {
        List<String> command = new ArrayList<>();
        command.add("sh");
        command.add("/data/shell/generate-iv.sh");
        //传入生成iv路径的参数,指定iv文件生成路径
        command.add(path + File.separator);
        System.out.println("command命令:{}" + command.toString());
        ProcessBuilder builder = new ProcessBuilder(command);
        try {
            Process start = builder.start();
            BufferedReader br2 = new BufferedReader(new InputStreamReader(start.getErrorStream()));
            StringBuilder buf = new StringBuilder(); // 保存输出结果流
            String line;
            while ((line = br2.readLine()) != null) buf.append(line);
            System.out.println("执行生成iv文件脚本返回输出结果:{}" + buf.toString());

        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info("***生成iv文件完成***");
    }

generate-ts-video.sh脚本,防至/data/shell/目录下
$1、$2、$3代表脚本参数,调用脚本时传入

ffmpeg -i $1 -profile:v baseline -level 3.0 -start_number 0 -hls_time 20 -hls_list_size 0 -f hls -hls_key_info_file $2 $3

generate-iv.sh脚本,防至/data/shell/目录下

openssl rand -hex 16 > $1iv.txt

如图
在这里插入图片描述

dealMp4ToM3u8AndEnc方法

/**
     * 将mp4文件切分成ts文件,并根据keyinfo文件加密
     * generate-ts-video.sh脚本中ffmpeg切分指令:
     * ffmpeg -i a.mp4 -profile:v baseline -level 3.0  -start_number 0
     * -hls_time 20 -hls_list_size 0 -f hls -hls_key_info_file enc.keyinfo index.m3u8
     *
     * @param sourcePath  需要加密的视频路径
     * @param keyInfoPath keyinfo文件的路径
     * @throws IOException
     */
    public static void dealMp4ToM3u8AndEnc(String keyInfoPath,
                                           String sourcePath) {
        List<String> command = new ArrayList<>();
        command.add("sh");
        command.add("/data/shell/generate-ts-video.sh");
        //需要切分的视频路径
        command.add(sourcePath);
        //keyinfo文件位置
        command.add(keyInfoPath);
        //自定义切分名称,同时包含m3u8文件的输出路径
        command.add(sourcePath.replace("mp4", "m3u8"));
        long start = System.currentTimeMillis();
        ProcessBuilder builder = new ProcessBuilder(command);
        try {
            builder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        log.info(Arrays.toString(builder.command().toArray()));
        log.info("总共耗时=" + (end - start) + "毫秒");
    }

calculateVideoDuration方法

 /**
     * 计算视频的时长
     *
     * @param storageFilePath 视频路径
     * @return 视频时长,单位为秒
     */
    public static int calculateVideoDuration(String storageFilePath) {
        //生成获取视频时长的指令
        String dos = "ffprobe " + storageFilePath;
        //开始时间
        long time = System.currentTimeMillis();
        String dosText = execDos(dos);
        System.out.println("耗时:" + (System.currentTimeMillis() - time) + "毫秒");
        System.out.println("dos文本:" + dosText);
        int startIndex = dosText.indexOf("Duration: ") + "Duration: ".length();
        System.out.println("startIndex:" + startIndex);
        int endIndex = dosText.indexOf(", start", startIndex);
        System.out.println("endIndex:" + endIndex);
        String durationText = dosText.substring(startIndex, endIndex).trim();
        System.out.println("视频时长文本为:{}" + durationText);
        String[] arr = durationText.split(":");
        //计算视频总时长(单位:秒),每20秒切一次
        int duration = 0;

        //计算小时单位的数量
        if (!"00".equals(arr[0])) {
            duration = (Integer.parseInt(arr[0]) * 3600);
        }
        if (!"00".equals(arr[1])) {
            duration += (Integer.parseInt(arr[1]) * 60);
        }
        if (!"00".equals(arr[2]) && !"00.00".equals(arr[2])) {
            String secondStr = arr[2];
            String[] split = secondStr.split("\\.");
            duration += Integer.parseInt(split[0]);
            if (!"00".equals(split[1])) {
                //带有毫秒的单位不为0,则默认将视频长度加1s
                duration++;
            }
        }
        log.info("视频时长为: 【{}】 秒", duration);
        return duration;
    }

exec方法

/**
     * 通过终端执行获取时长指令
     * 指令格式:ffprobe 视频路径
     *
     * @param dos 指令
     * @return 返回的文本
     */
    private static String execDos(String dos) {
        try {
            Runtime runtime = Runtime.getRuntime();
            Process process = runtime.exec(dos);
            process.waitFor();
            InputStream in = process.getErrorStream();
            //commons-io的v2.3依赖,org.apache.commons.io包
            return IOUtils.toString(in, StandardCharsets.UTF_8);
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
        return null;
    }

waitVideoSegmentFinish方法

/**
     * 等待视频切片完成
     *
     * @param storageFilePath 发生切片的路径
     * @param fragmentNum     计算出来的视频切片数量
     */
    public static void waitVideoSegmentFinish(String storageFilePath, int fragmentNum) {
        //为了防止死循环可以循环进入的次数,通过切片数量调整次数
        long start = System.currentTimeMillis();
        int entryTime = 0;
        do {
            if (entryTime > fragmentNum * 4) {
                //正常情况下每切一次耗时10秒左右,进入循环4次,总共数量为切片耗时:fragmentNum * 3 +10
                break;
            }
            try {
                Thread.sleep(3000);
                //增加进入的次数
                entryTime++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (getAllFileNameCountTsNumber(storageFilePath) < fragmentNum);

        //ts数量与计算的切片总数一样时再等10秒,等待切片数据写入完成
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        log.info("***本次切片耗时:【{}】秒***", (end - start) / 1000);
    }

getAllFileNameCountTsNumber方法

/**
     * 统计当前路径下的ts后缀文件数量,通过检测文件夹下总ts文件数量
     * 与计算的ts数量是否相同判断切割是否完成(切割后端ts总大小与原视频大小不一致)
     *
     * @param directoryPath 需要遍历的文件夹路径
     * @return 文件名称
     */
    private static int getAllFileNameCountTsNumber(String directoryPath) {
        //路径下的所有文件,不包括文件夹
        List<String> list = new ArrayList<>();
        File baseFile = new File(directoryPath);
        if (baseFile.isFile() || !baseFile.exists()) {
            return 0;
        }
        File[] files = baseFile.listFiles();
        for (File file : files) {
            if (!file.isDirectory()) {
                list.add(file.getName());
            }
        }

        //计算ts文件后缀数量
        int count = 0;
        for (String s : list) {
            int start = s.lastIndexOf(".") + 1;
            String suffix = s.substring(start);
            if ("ts".equals(suffix)) {
                count++;
            }
        }
        return count;
    }

getUploadPathList方法

/**
     * 获取指定目录下的ts、m3u8、enc.key文件的绝对路径列表
     *
     * @param basePath 基础路径
     * @return 路径集合
     */
    public static List<String> getUploadPathList(String basePath) {
        List<String> list = new ArrayList<>();
        File baseFile = new File(basePath);
        if (baseFile.isFile() || !baseFile.exists()) {
            return null;
        }
        File[] files = baseFile.listFiles();
        for (File file : files) {
            if (!file.isDirectory()) {
                String name = file.getName();
                int start = name.lastIndexOf(".") + 1;
                //文件后缀
                String suffix = name.substring(start);
                if ("ts".equals(suffix) || "m3u8".equals(suffix) || "key".equals(suffix)) {
                    //将需要上传的ts、m3u8、enc.key文件加入到list中
                    list.add(file.getAbsolutePath());
                }
            }
        }
        return list;
    }

uploadFileWithInputSteam方法

 /**
     * 上传文件流
     *
     * @param oranFileName 上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg
     * @param inputStream  来自本地的文件流
     */
    public static String uploadFileWithInputSteam(String bucketName, String oranFileName, InputStream inputStream) {
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        // 上传文件流
        try {
            //上传到OSS
            ossClient.putObject(bucketName, objectName, inputStream);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        // 关闭OSSClient。
        ossClient.shutdown();
        //返回文件在服务器上的全路径+名称
        return getRealName(bucketName, objectName);
    }

代码编写与整理都花了很长时间、代码中有很多注释,如果漏有什么地方没有分享到可以给我留言我一般工作日一天之内就会回复,如果对你有帮助希望点个赞哦

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 18:00:11  更:2022-03-12 18:01:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 1:32:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码