V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yuyujulin
V2EX  ›  Java

M3u8 文件解析和 TS 文件加解密,不知道对不对

  •  
  •   yuyujulin · 2020-08-27 14:51:03 +08:00 · 1692 次点击
    这是一个创建于 1548 天前的主题,其中的信息可能已经有所发展或是发生改变。
    package com.example.demo;
    
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.util.encoders.Hex;
    import org.junit.jupiter.api.Test;
    
    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    import java.io.*;
    import java.nio.charset.StandardCharsets;
    import java.security.InvalidAlgorithmParameterException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.Security;
    
    /**
     * TS 文件加解密。 包含如下两套加解密方式:
     * 1. AES/CBC/PKCS7Padding 标准 Java 加解密方式
     * 2. AES/CBC/NoPadding 加手动 PKCS7Padding 方式。当前 Stream 采用这种方式。
     * <p>
     * AES-CBC-128 加密
     */
    public class MediaFileCryptoUtils {
        // 算法名称
        private static final String KEY_ALG = "AES";
    
        /**
         * 加解密算法 /模式 /填充方式。PKCS7Padding
         */
        private static final String AES_CBC_PKCS7PADDING = "AES/CBC/PKCS7Padding";
    
        /**
         * 加解密算法 /模式 /填充方式。
         * 这里虽然是 NoPadding,但实际最后一个数据块会手动做 PKCS7Padding
         */
        private static final String AES_CBC_NOPADDING = "AES/CBC/NoPadding";
    
        /**
         * AES 加密数据块分组长度必须为 128 比特( bit 位),
         * 密钥长度可以是 128 比特、192 比特、256 比特中的任意一个(如果数据块不足密钥长度时,会补齐)。
         */
        private static final long CIPHER_BLOCK_SIZE = 16;
    
        // 每次读取的缓冲区长度,必须为 CIPHER_BLOCK_SIZE 的倍数
        private static final int BUFFER_SIZE = 1024;
    
        // 加密后的 ts 文件块大小
        private static final int TS_BLOCK_SIZE = 188;
    
        static {
            Security.addProvider(new BouncyCastleProvider());
        }
    
        private static Cipher getCipher(byte[] keyBytes, byte[] ivBytes, String transformation, int encryptMode) {
            try {
                Cipher cipher = Cipher.getInstance(transformation);
                cipher.init(encryptMode, new SecretKeySpec(keyBytes, KEY_ALG), new IvParameterSpec(ivBytes));
                return cipher;
            } catch (NoSuchAlgorithmException | NoSuchPaddingException
                    | InvalidAlgorithmParameterException | InvalidKeyException e) {
                throw new RuntimeException("Error occurred while getting cipher", e);
            }
        }
    
        /**
         * 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流
         *
         * @param keyString   秘钥字符串,例如 "362ed0938ef220d8"
         * @param ivHexString 初始向量的十六进制字符串,前面有 0x 开头,例如 "0x04401234f48591766c1a3bc51ab173f0"
         * @param sourceTS    源 TS 文件路径
         * @param os          要输出到的流
         */
        public static void encryptTS(String keyString, String ivHexString, String sourceTS, OutputStream os) {
            byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
            byte[] ivBytes = Hex.decode(ivHexString.substring(2));
            encryptTS(keyBytes, ivBytes, sourceTS, os);
        }
    
        public static void encryptTsWithManualPadding(String keyString, String ivHexString, String sourceTS, OutputStream os) {
            byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
            byte[] ivBytes = Hex.decode(ivHexString.substring(2));
            encryptTsWithManualPadding(keyBytes, ivBytes, sourceTS, os);
        }
    
    
        /**
         * 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流。
         * <p>
         * AES-CBC 对文件加密的标准 Java 写法。
         *
         * @param keyBytes 秘钥
         * @param ivBytes  初始向量
         * @param sourceTS 源 TS 文件路径
         * @param os       输出流
         */
        public static void encryptTS(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
            // 初始化 cipher, 同一个文件要用一个 Cipher
            Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_PKCS7PADDING, Cipher.ENCRYPT_MODE);
            File plainFile = new File(sourceTS);
            try (FileInputStream fis = new FileInputStream(plainFile)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int length = -1;
                int count = 0;
                while ((length = fis.read(buffer)) != -1) {
                    System.out.println("count: " + count++ + ", length: " + length);
                    byte[] encryptedData;
                    // 可读大小为 0,表示当前已读到的数据是最后一块数据
                    if (fis.available() == 0) {
                        encryptedData = cipher.doFinal(buffer, 0, length);
                    } else {
                        encryptedData = cipher.update(buffer, 0, length);
                    }
                    os.write(encryptedData);
                }
            } catch (IOException | BadPaddingException | IllegalBlockSizeException e) {
                throw new RuntimeException("Error occurred while encrypting ts", e);
            }
        }
    
        /**
         * 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流。
         * <p>
         * Stream 里面 TS 加密的 Java 实现,所有数据块采用 AES_CBC_NOPADDING,最后一个数据块需要手动加上 PKCS7Padding 。
         *
         * @param keyBytes 秘钥
         * @param ivBytes  初始向量
         * @param sourceTS 源 TS 文件路径
         * @param os       输出流
         */
        public static void encryptTsWithManualPadding(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
            // 初始化 cipher, 同一个文件要用一个 Cipher
            Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_NOPADDING, Cipher.ENCRYPT_MODE);
            File plainFile = new File(sourceTS);
            try (FileInputStream fis = new FileInputStream(plainFile)) {
                long totalLength = plainFile.length();
                int paddingLength = (int) (CIPHER_BLOCK_SIZE - totalLength % CIPHER_BLOCK_SIZE);
                byte[] buffer = new byte[BUFFER_SIZE];
                int length = -1;
                while ((length = fis.read(buffer)) != -1) {
                    byte[] plainData = buffer;
                    // 可读大小为 0,表示当前已读到的数据是最后一块数据, 且需要 padding
                    if (fis.available() == 0 && paddingLength != 0) {
                        plainData = new byte[length + paddingLength];
                        System.arraycopy(buffer, 0, plainData, 0, length);
                        // PCKS7 填充,在填充字节上都填相同的数据,比如数据缺少 4 字节,所以所有字节上都填 4
                        for (int i = length; i < plainData.length; i++) {
                            plainData[i] = (byte) paddingLength;
                        }
                    }
    
                    /**
                     *这里不要使用 cipher.doFinal 因为 CBC 是循环加密,要把上一个加密快的结果作为下一次加密的 iv 。
                     * 即使是最后一个数据块也不需要使用 cipher.doFinal,因为上面针对最后一个数据块手动进行了 PKCS7 填充
                     */
                    byte[] encryptedData = cipher.update(plainData);
                    os.write(encryptedData);
                }
            } catch (IOException e) {
                throw new RuntimeException("Error occurred while encrypting ts with manual padding", e);
            }
        }
    
    
        /**
         * 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流
         *
         * @param keyString   秘钥字符串,例如 "362ed0938ef220d8"
         * @param ivHexString 初始向量的十六进制字符串,前面有 0x 开头,例如 "0x04401234f48591766c1a3bc51ab173f0"
         * @param sourceTS    源 TS 文件路径
         * @param os          要输出到的流
         */
        public static void decryptTS(String keyString, String ivHexString, String sourceTS, OutputStream os) {
            byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
            byte[] ivBytes = Hex.decode(ivHexString.substring(2));
            decryptTS(keyBytes, ivBytes, sourceTS, os);
        }
    
        public static void decryptTsWithManualPadding(String keyString, String ivHexString, String sourceTS, OutputStream os) {
            byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
            byte[] ivBytes = Hex.decode(ivHexString.substring(2));
            decryptTsWithManualPadding(keyBytes, ivBytes, sourceTS, os);
        }
    
        /**
         * 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流。
         * <p>
         * AES-CBC 对文件解密的标准 Java 写法。
         *
         * @param keyBytes 秘钥
         * @param ivBytes  初始向量
         * @param sourceTS 源 TS 文件路径
         * @param os       输出流
         */
        public static void decryptTS(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
            // 初始化 cipher, 同一个文件要用一个 Cipher
            Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_PKCS7PADDING, Cipher.DECRYPT_MODE);
            File encryptedFile = new File(sourceTS);
            try (FileInputStream fis = new FileInputStream(encryptedFile)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int length;
                while ((length = fis.read(buffer)) != -1) {
                    byte[] plainData;
                    if (fis.available() == 0) {
                        plainData = cipher.doFinal(buffer, 0, length);
                    } else {
                        plainData = cipher.update(buffer, 0, length);
                    }
                    os.write(plainData);
                }
            } catch (IOException | BadPaddingException | IllegalBlockSizeException e) {
                throw new RuntimeException("Error occurred while decrypting ts", e);
            }
        }
    
        /**
         * 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流。
         * <p>
         * Stream 里面 TS 解密的 Java 实现,所有数据块采用 AES_CBC_NOPADDING,最后一个数据块需要手动去除 padding 。
         *
         * @param keyBytes 秘钥
         * @param ivBytes  初始向量
         * @param sourceTS 源 TS 文件路径
         * @param os       输出流
         */
        public static void decryptTsWithManualPadding(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
            // 初始化 cipher, 同一个文件要用一个 Cipher
            Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_NOPADDING, Cipher.DECRYPT_MODE);
            File encryptedFile = new File(sourceTS);
            try (FileInputStream fis = new FileInputStream(encryptedFile)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int totalLength = fis.available();
                int length;
                while ((length = fis.read(buffer)) != -1) {
                    byte[] plainData = cipher.update(buffer);
                    int plainDataLength = plainData.length; // 默认为解密后的数据长度
                    if (fis.available() == 0) {
                        // 最后一个解密出来的数据数据块,要去掉 Padding 的数据
                        // 计算 padding 长度
                        int paddingLength = totalLength % TS_BLOCK_SIZE;
                        // 去掉无用的 padding
                        plainDataLength = length - paddingLength;
                    }
                    os.write(plainData, 0, plainDataLength);
                }
            } catch (IOException e) {
                throw new RuntimeException("Error occurred while decrypting ts with manual padding", e);
            }
        }
    
        @Test
        public void testEncryptFile() {
            try (FileOutputStream fos = new FileOutputStream(new File("D:\\196.ets"))) {
                encryptTS("7db4fd4359bb25b0", "0xb70cbefa3168efd2d0984abc8181ecff",
                        "D:\\196.ts", fos);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Test
        public void testDecryptFile() {
            try (FileOutputStream fos = new FileOutputStream(new File("D:\\9.ts"))) {
                decryptTS("7db4fd4359bb25b0", "0xb70cbefa3168efd2d0984abc8181ecff",
                        "D:\\record-crypt\\06987ff1-0357-45b1-a6b8-f062e989c82d\\videoHD\\9.ts", fos);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    package com.example.demo;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.util.CollectionUtils;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileReader;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.LinkedHashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    public class M3u8Parser {
        /**
         * m3u8 文件头指令:m3u8 文件头。必须在文件第一行。
         */
        private static final String DIRECTIVE_HEADER = "#EXTM3U";
    
        /**
         * 码流信息指令:带宽、分辨率,解码器等键值对信息。后一行跟对应码流的 m3u8 文件位置。
         */
        private static final String DIRECTIVE_STREAM_INF = "#EXT-X-STREAM-INF";
    
        /**
         * 音频,视频轨道信息指令:时长(秒),标题,其他额外信息(如 logo )以键值对显示。后一行跟对应 ts 的文件位置
         */
        private static final String DIRECTIVE_TRACK_INF = "#EXTINF";
    
        /**
         * 列表终止标识指令
         */
        private static final String DIRECTIVE_ENDLIST = "#EXT-X-ENDLIST";
    
        /**
         * m3u8 文件包含的最小行数
         */
        private static final int M3U8_MIN_LINES = 2;
    
        public List<String> getAllTsPaths(String indexM3u8) {
            File indexM3u8File = new File(indexM3u8);
            if (!indexM3u8File.exists()) {
                throw new IllegalArgumentException("File not found");
            }
    
            if (!indexM3u8File.isFile()) {
                throw new IllegalArgumentException(indexM3u8File + " is not a file");
            }
            String basePath = indexM3u8File.getParentFile().getAbsolutePath();
            Set<String> tsSet = parseIndexM3u8(basePath, indexM3u8File);
            if (CollectionUtils.isEmpty(tsSet)) {
                throw new IllegalArgumentException("No TS in specified m3u8 file");
            }
            return tsSet.stream().map(tsName -> basePath + File.separator + tsName).collect(Collectors.toList());
        }
    
        private Set<String> parseIndexM3u8(String basePath, File indexM3u8File) {
            // index m3u8 文件比较小,一次性读完
            List<String> indexM3u8Lines = readAllLines(indexM3u8File);
            validateM3u8(indexM3u8Lines);
            for (int i = 1; i < indexM3u8Lines.size(); i++) {
                String line = indexM3u8Lines.get(i);
                if (line.startsWith(DIRECTIVE_STREAM_INF)) {
                    // 遇到第一个码流信息,取码流之后的一行就是子 m3u8 文件的位置,当前第一个码流信息就够了
                    String subM3u8 = basePath + File.separator + indexM3u8Lines.get(i + 1);
                    return parseSubM3u8(subM3u8);
                }
            }
            throw new IllegalArgumentException("Not a valid m3u8 file: no ts info");
        }
    
        private Set<String> parseSubM3u8(String subM3u8) {
            // sub m3u8 文件可能会比较大,每读一行就解析一行
            try (FileReader fr = new FileReader(new File(subM3u8));
                 BufferedReader bf = new BufferedReader(fr)) {
                Set<String> tracks = new LinkedHashSet<>();
                String line;
                while ((line = bf.readLine()) != null) {
                    if (line.startsWith(DIRECTIVE_TRACK_INF)) {
                        // 当前行是轨道信息,就再读一行
                        line = bf.readLine();
                        if (line != null) {
                            tracks.add(line);
                        }
                    }
                    if (line.startsWith(DIRECTIVE_ENDLIST)) {
                        break;
                    }
                }
                return tracks;
            } catch (IOException e) {
                throw new IllegalArgumentException("Error occurred while parsing sub m3u8 file", e);
            }
        }
    
        private void validateM3u8(List<String> indexM3u8Lines) {
            if (indexM3u8Lines.size() < M3U8_MIN_LINES) {
                throw new IllegalArgumentException("Invalid m3u8 file: insufficient lines");
            }
            if (!DIRECTIVE_HEADER.equals(indexM3u8Lines.get(0))) {
                throw new IllegalArgumentException("Invalid m3u8 file: invalid m3u8 header");
            }
        }
    
        public List<String> readAllLines(File file) {
            List<String> lines = new ArrayList<>();
            try (FileReader fr = new FileReader(file);
                 BufferedReader bf = new BufferedReader(fr)) {
                String line;
                while ((line = bf.readLine()) != null) {
                    lines.add(line);
                }
                return lines;
            } catch (IOException e) {
                throw new RuntimeException("Error occurred while reading file", e);
            }
        }
    
        @Test
        public void testM3u8Parser() {
            M3u8Parser m3u8Parser = new M3u8Parser();
            m3u8Parser.getAllTsPaths("D:\\record-crypt\\40ac6397-5116-4b44-8cb9-a2f70d8d68fa\\videoHD\\index.m3u8").stream().forEach(System.out::println);
        }
    }
    
    
    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1626 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 16:57 · PVG 00:57 · LAX 08:57 · JFK 11:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.