M3u8 文件解析和 TS 文件加解密,不知道对不对 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yuyujulin
V2EX    Java

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

  •  
  •   yuyujulin 2020-08-27 14:51:03 +08:00 2067 次点击
    这是一个创建于 1872 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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.BaPaddingException; 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     Solana     819 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 21:29 PVG 05:29 LAX 14:29 JFK 17:29
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86