Android 自定义加解密播放音视频(m3u8独立加密)
背景
- 当涉及App内部视频的时候,我们不希望被别人以抓包的形式来爬取我们的视频
- 大视频文件以文件方式整个加密的话需要完全下载后才能进行解密
- 当前m3u8格式虽然支持加密,但是ts格式的小视频可以独立播放的,也就是ts文件本身没有被加密,或者加密方法过于复杂
根据以上,我通过修改ExoPlayer的源代码实现以下功能,这里不讨论其他视频流加密解密的方法
- 大文件分段加密后应用分段解密(m3u8)
- 高度自定义,你可以实现任何你需要的加密方法,甚至每一个ts都有自己的解码方式
- ts加密,不允许独立播放
加密流程
PS:使用ffmpeg进行音视频分割后使用Java代码进行加密
- 音视频分割
代码就是通过java执行ffmpeg的命令即可,请确保环境变量中安装了ffmpeg,内部的代码可以自己通过需求来修改,其中音频与视频的分割方式差不多
private static String encryptVideoWithFFmpeg(String videoFilePath, String outputDirPath) {
File outputDir = new File(outputDirPath);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
String outputFileName = "output"; // 输出文件名,这里可以根据需要自定义
String tsOutputPath = outputDirPath + File.separator + outputFileName + ".ts";
String m3u8OutputPath = outputDirPath + File.separator + outputFileName + ".m3u8";
try {
ProcessBuilder processBuilder = new ProcessBuilder("ffmpeg",
"-i", videoFilePath,
"-c:v", "libx264",
"-c:a", "aac",
"-f", "hls",
"-hls_time", "5",
"-hls_list_size", "0",
"-hls_segment_filename", outputDirPath + File.separator + "output%03d.ts",
m3u8OutputPath);
// 设置工作目录,可以防止某些情况下找不到 ffmpeg 命令的问题
Process process = processBuilder.start();
// 获取 ffmpeg 命令执行的输出信息(可选,如果需要查看 ffmpeg 执行日志)
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("FFmpeg command executed successfully.");
} else {
System.err.println("Error executing FFmpeg command. Exit code: " + exitCode);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return tsOutputPath;
}
private static String splitAudioWithFFmpeg(String audioFilePath, String outputDirPath) {
File outputDir = new File(outputDirPath);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
String outputFileName = "output"; // 输出文件名,这里可以根据需要自定义
String tsOutputPath = outputDirPath + File.separator + outputFileName + ".ts";
String m3u8OutputPath = outputDirPath + File.separator + outputFileName + ".m3u8";
try {
ProcessBuilder processBuilder = new ProcessBuilder("ffmpeg",
"-i", audioFilePath,
"-c:a", "aac",
"-f", "hls",
"-hls_time", "10",
"-hls_list_size", "0",
"-hls_segment_filename", outputDirPath + File.separator + "output%03d.ts",
m3u8OutputPath);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("FFmpeg command executed successfully.");
} else {
System.err.println("Error executing FFmpeg command. Exit code: " + exitCode);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return tsOutputPath;
}
- 音视频加密
这里的视频加密使用的是AES加密,是将ts结尾的所有文件进行加密,后面的方法是解密,一般用不到
private static void encryptTSSegmentsWithAES(String outputDirPath, String aesKey) {
File outputDir = new File(outputDirPath);
File[] tsFiles = outputDir.listFiles((dir, name) -> name.endsWith(".ts"));
if (tsFiles != null) {
try {
byte[] keyBytes = aesKey.getBytes();
Key aesKeySpec = new SecretKeySpec(keyBytes, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec);
for (File tsFile : tsFiles) {
byte[] tsData = Files.readAllBytes(Paths.get(tsFile.getPath()));
byte[] encryptedData = cipher.doFinal(tsData);
Files.write(Paths.get(tsFile.getPath()), encryptedData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void decryptTSSegmentsWithAES(String outputDirPath, String aesKey) {
File outputDir = new File(outputDirPath);
File[] tsFiles = outputDir.listFiles((dir, name) -> name.endsWith(".ts"));
if (tsFiles != null) {
try {
byte[] keyBytes = aesKey.getBytes();
Key aesKeySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, aesKeySpec);
for (File tsFile : tsFiles) {
byte[] tsData = Files.readAllBytes(Paths.get(tsFile.getPath()));
byte[] encryptedData = cipher.doFinal(tsData);
Files.write(Paths.get(tsFile.getPath()), encryptedData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
加密完成之后将m3u8放在服务器上,并且分割的文件也要在同一目录,或者切片的时候手动设置,保证切片后的视频可以正常播放即可
音视频解密
这里使用的是修改ExoPlayer的源代码来实现的,因为在Android手机上面播放视频的选择有很多,大家也可以根据我的方法修改其他播放器,本次按照ExoPlayer进行演示教学
PS:因为Google把ExoPlayer整合到MediaPlayer3里了,所以如果不使用纯源代码来修改的话,也会跟我的演示一样有删除线,但是无伤大雅
- 引入依赖,直接在App层的Build.gradle引入ExoPlayer2的依赖,其中我们要使用的视频流为hls格式,所以需要引入hls模块
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.0'
- 准备修改代码,我们需要修改的类如下
- DefaultDataSource
- DefaultDataSourceFactory
- DefaultHttpDataSource
- HttpDataSource
我们只需要复制其源码然后进行修改后,使用ExoPlayer播放视频的时候,使用我们自己的类即可,如果你不想这样,那么可以直接下载ExoPlayer2的源代码进行修改,这样的话还能去除废弃的表示,没有那么多删除线,接下来我们正式开始修改
修改类“DefaultHttpDataSource”
我将以注释的方式来讲解代码,注意这里只是演示一个简单的自定义加解密的切入方式,所以按照文件名末尾为ts的文件进行暴力判断,精细化的处理方式可以有很多拓展,比如仅加密视频的中间部分作为会员视频,这样只需要单一视频流就可以解决试看的问题,而且不怕应用内部修改VIP标志位(对于修改源码等暴力破解的方法无效,毕竟源码都给你扒出来了)
//定义解密流,主要使用此流来进行解密
private CipherInputStream cipherInputStream;
//修改open方法代码,最后的try代码块中增加如下内容用来解密流
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
....
try {
inputStream = connection.getInputStream();
if (isCompressed) {
inputStream = new GZIPInputStream(inputStream);
}
//新增代码块,这里的解密方法可以按照自己的需求编写----------------------------------
if (dataSpec.uri.getPath().endsWith(".ts")) {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
Key aesKeySpec = new SecretKeySpec("1234567890abcdef".getBytes(), "AES");
try {
cipher.init(Cipher.DECRYPT_MODE, aesKeySpec);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
}
cipherInputStream = new CipherInputStream(inputStream, cipher);
}
//新增代码块结束------------------------------
} catch (IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
....
}
//修改read方法如下,如果判断是需要解密的文件则走cipherInputStream
@Override
public final int read(byte[] buffer, int offset, int length) throws IOException {
if (dataSpec.uri.getPath().endsWith(".ts")) {
Assertions.checkNotNull(cipherInputStream);
int bytesRead = cipherInputStream.read(buffer, offset, length);
if (bytesRead < 0) {
return C.RESULT_END_OF_INPUT;
}
return bytesRead;
} else {
try {
return readInternal(buffer, offset, length);
} catch (IOException e) {
throw HttpDataSourceException.createForIOException(
e, castNonNull(dataSpec), HttpDataSourceException.TYPE_READ);
}
}
}
//最后释放资源
@Override
public void close() throws HttpDataSourceException {
try {
@Nullable InputStream inputStream = this.inputStream;
if (inputStream != null) {
long bytesRemaining =
bytesToRead == C.LENGTH_UNSET ? C.LENGTH_UNSET : bytesToRead - bytesRead;
maybeTerminateInputStream(connection, bytesRemaining);
try {
inputStream.close();
} catch (IOException e) {
throw new HttpDataSourceException(
e,
castNonNull(dataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE);
}
}
if (cipherInputStream != null) {
cipherInputStream.close();
}
} catch (IOException e) {
throw new HttpDataSourceException(
e,
castNonNull(dataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE);
} finally {
inputStream = null;
cipherInputStream = null;
closeConnectionQuietly();
if (opened) {
opened = false;
transferEnded();
}
}
}
修改类“DefaultDataSourceFactory”
此类只需要修改一点,那就是将DefaultDataSource的create过程引导到我们自己写的DefaultDataSource,也就是删除原来的ExoPlayer2的依赖引入,引入刚刚讲到的DefaultHttpDataSource,不需要修改代码,只需要切换依赖即可
public DefaultDataSourceFactory(
Context context, @Nullable String userAgent, @Nullable TransferListener listener) {
this(context, listener, new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
}
音视频播放
因为ExoPlayer2同时支持音频和视频的播放,所以均可使用下列方式完成
public class PlayerActivity extends AppCompatActivity {
private PlayerView playerView;
private SimpleExoPlayer player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
// Initialize PlayerView
playerView = findViewById(R.id.player);
// Create a DefaultTrackSelector to enable tracks
DefaultTrackSelector trackSelector = new DefaultTrackSelector(this);
// Create an instance of ExoPlayer
player = new SimpleExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build();
// Attach the player to the PlayerView
playerView.setPlayer(player);
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(this, userAgent);
String videoUrl = "http://zhangzhiao.top/missit/aa/output.m3u8";
// Create an HlsMediaSource
HlsMediaSource mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)));
// Prepare the player with the media source
player.prepare(mediaSource);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Release the player when the activity is destroyed
player.release();
}
}
结语
代码里给大家提供了一个小视频,如果按照流程编写应该是可以顺利播放的,如果需要还可以把m3u8文件进行加密处理,一切处理方法都可以实现,如果对您有帮助不妨点个赞