功能简介
以代码实现媒体格式转换原理:.m3u8 转 .mp4。
参考资料
- m3u8格式详解:https://blog.csdn.net/weixin_39399492/article/details/131687865
- Git:https://github.com/qq494257084/m3u8Download
Java
环境
JDK-8
无第三方 jar 依赖
Git源码
- 码云:https://gitee.com/HTouying/M3U.git
com.m3u.local.Transf
核心代码逻辑
Core.java
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
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;
public class Core {
/**
* 解密文件
* @param f {@link File}:ts文件;
* @param k {@link byte}[]:密钥。由#EXT-X-KEY标签的URI属性获取;
* @param iv {@link String}:AES初始化向量。#EXT-X-KEY标签的IV属性;
* @return {@link InputStream}:解密流。
* @author Haining.Liu
* @date 2024-8-8
*/
public static File decryptFile(File f, String method, byte[] k, String iv) {
byte[] tbs = read(f);
if (tbs == null || tbs.length == 0) {
return null;
}
if ("AES-128".equals(method)) {
tbs = AES.decryptCBC(tbs, k, doIV(iv));
}
return outFile(f.getPath() + ".mp4", new ByteArrayInputStream(tbs));
}
/**
* 读取整文件
* @param f {@link File}:文件;
* @date 2024-8-8
*/
private static byte[] read(File f) {
try (FileInputStream in = new FileInputStream(f);) {
byte[] bs = new byte[in.available()];
in.read(bs);
return bs;
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
/**
* 输出文件
* @param fileName {@link String}:输出文件名;
* @param in {@link InputStream}:解析完的文件流;
* @return {@link File}:生成的文件。
* @date 2024-8-8
*/
private static File outFile(String fileName, InputStream in) {
File f = new File(fileName);
try (
FileOutputStream out = new FileOutputStream(f);
) {
byte[] bs = new byte[1024 << 7];
for (int rl; (rl = in.read(bs)) != -1;) {
out.write(bs, 0, rl);
}
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
return f;
}
/**
* IV转byte字节
*/
private static byte[] doIV(String iv) {
if (iv.startsWith("0x")) {
iv = iv.substring(2);
return hexToBytes(iv);
} else {
return iv.getBytes(StandardCharsets.UTF_8);
}
}
/**
* 16进制转byte
*/
public static byte[] hexToBytes(String s) {
int radix = 16;
BigInteger hex = new BigInteger(s, radix);
ByteBuffer bf = ByteBuffer.allocate(radix);
bf.put(hex.toByteArray());
return bf.array();
}
/**
* AES加解密
* @date 2024-8-8
*/
public static class AES {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
static {
// java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // 依赖第三方包注册"AES/CBC/PKCS7Padding"
}
/**
* AES-CBC模式解密
* @param bs byte[]:被解密内容;
* @param k byte[]:密钥;
* @param iv {@link String}:初始化向量(IV)(16字节);
* @return byte[]:解密后的内容。
* @date 2024-8-8
*/
public static byte[] decryptCBC(byte[] bs, byte[] k, byte[] iv) {
Optional<IvParameterSpec> opt = Optional.ofNullable(iv)
.map(AES::fillIV)
.map(IvParameterSpec::new);
try {
SecretKeySpec sks = new SecretKeySpec(fill(k), "AES");
Cipher ci = Cipher.getInstance(ALGORITHM);
if (opt.isPresent()) {
ci.init(Cipher.DECRYPT_MODE, sks, opt.get());
} else {
ci.init(Cipher.DECRYPT_MODE, sks);
}
return ci.doFinal(bs);
} catch (NoSuchAlgorithmException e) {
System.out.println("无此算法");
} catch (NoSuchPaddingException e) {
System.out.println("无此填充");
} catch (InvalidKeyException e) {
System.out.println("无此密钥");
} catch (InvalidAlgorithmParameterException e) {
System.out.println("无此算法参数");
} catch (IllegalBlockSizeException e) {
System.out.println("非法块大小");
} catch (BadPaddingException e) {
System.out.println("不良填充");
}
return bs;
}
/**
* 填充初始化向量(IV)
*/
private static byte[] fillIV(byte[] k) {
return fill(k, (byte) 16);
}
/**
* 填充
*/
private static byte[] fill(byte[] k) {
return fill(k, (byte) 0);
}
/**
* 填充
*/
private static byte[] fill(byte[] k, byte len) {
int kl = k.length;
if (len > 0) {
if (kl == len) {
return k;
}
} else if (kl == 16
|| kl == 24
|| kl == 32) {
return k;
} else if (kl < 16) {
len = 16;
} else if (kl < 24) {
len = 24;
} else {
len = 32;
}
byte[] bs = new byte[ len ];
// Arrays.fill(bs, (byte) '0'); // 右侧补'0'
System.arraycopy(k, 0, bs, 0, kl < len ? kl : len);
return bs;
}
}
}
测试示例
public static void main(String[] args) {
File f = new File("D:\\m3u8\\0.ts"); // ts媒体文件
File kf = new File("D:\\m3u8\\K.key"); // 密钥文件,由#EXT-X-KEY标签的URI属性获取
byte[] k = read(kf);
String method = /* null */ "AES-128";
File outFile = decryptFile(f, method, k, "0x0123456789abcdef0123456789abcdef");
System.out.println("解密的文件:" + outFile);
}