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@Overridepublic 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);}}}
//最后释放资源@Overridepublic 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_player);// Initialize PlayerViewplayerView = findViewById(R.id.player);// Create a DefaultTrackSelector to enable tracksDefaultTrackSelector trackSelector = new DefaultTrackSelector(this);// Create an instance of ExoPlayerplayer = new SimpleExoPlayer.Builder(this).setTrackSelector(trackSelector).build();// Attach the player to the PlayerViewplayerView.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 HlsMediaSourceHlsMediaSource mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)));// Prepare the player with the media sourceplayer.prepare(mediaSource);}@Overrideprotected void onDestroy() {super.onDestroy();// Release the player when the activity is destroyedplayer.release();}
}
源码下载
结语
代码里给大家提供了一个小视频,如果按照流程编写应该是可以顺利播放的,如果需要还可以把m3u8文件进行加密处理,一切处理方法都可以实现,如果对您有帮助不妨点个赞
相关文章:
Android 自定义加解密播放音视频(m3u8独立加密)
文章目录 背景加密流程音视频解密音视频播放结语 背景 当涉及App内部视频的时候,我们不希望被别人以抓包的形式来爬取我们的视频大视频文件以文件方式整个加密的话需要完全下载后才能进行解密当前m3u8格式虽然支持加密,但是ts格式的小视频可以独立播放的…...
常见的文件格式
一、C:\fakepath\新建文本文档.txt [object String] 实现方式: <input onchange"test(this.value)" type"file"></input><script>function test(e){console.log(e,Object.prototype.toString.call(e))}</script> 二、…...
浏览器输入url后回车展开过程
当你在浏览器中输入一个URL并敲下回车后,浏览器会执行一系列步骤来访问并展示网页。下面是浏览器访问网页的一般流程: DNS解析:浏览器首先会提取URL中的主机名,然后向DNS服务器发送请求,将主机名解析为对应的IP地址。这…...
Docker 容器创建命令说明
使用命令如下: docker run -itd --name=cluster -v /home/ClusterApp/cluster:/home/ClusterApp/cluster --restart=always --privileged=true -p 12000:12000 python:3.8 命令说明: docker run 创建一个容器并运行 docker run --help 可以查看所有的参数: 命令中参数说明 -…...
西瓜书读书笔记整理(六)—— 第六章 支持向量机
第六章 支持向量机 6.1 间隔与支持向量6.1.1 什么是支持向量机6.1.2 支持向量与间隔6.1.3 支持向量机的求解过程 6.2 对偶问题(dual problem)6.2.1 什么是对偶问题6.2.2 如何求解支持向量机的对偶问题 6.3 核函数(kernel function)…...
蓝桥杯每日一题2023.9.23
4961. 整数删除 - AcWing题库 题目描述 分析 注:如果要进行大量的删除操作可以使用链表 动态求最小值使用堆,每次从堆中取出最小值的下标然后在链表中删除 注意long long 代码解释: while(k --){auto t q.top();q.pop();res t.first;i…...
C语言数组和指针笔试题(三)(一定要看)
目录 字符数组四例题1例题2例题3例题4例题5例题6例题7 结果字符数组五例题1例题2例题3例题4例题5例题6例题7结果字符数组六例题1例题2例题3例题4例题5例题6例题7 结果 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 🐒🐒🐒个…...
leetcode11 盛水最多的容器
题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 示例 输入:[1,8,6,2,5,4,8,…...
进入数据结构的世界
数据结构和算法的概述 一、什么是数据结构二、什么是算法三、如何去学习数据结构和算法四、算法的时间复杂度和空间复杂度4.1 算法效率4.2 大O的渐进表示法4.3 时间复杂度4.4 空间复杂度4.5 常见复杂度对比 一、什么是数据结构 数据结构是计算机存储、组织数据的方式。&#x…...
stm32之看门狗
STM32 有两个看门狗,独立看门狗和窗口看门狗,独立看门狗又称宠物狗,窗 口看门狗又称警犬。可用来检测和解决由软件错误引起的故障。两个看门狗的原理都是当计数器达到给定的超时值时,产生系统复位,对于窗口型看门狗同…...
纤维蛋白单体(FM)介绍
纤维蛋白单体(FM)是血栓形成的早期产物,是纤维蛋白原(Fibrinogen,Fbg)在凝血酶(Thrombin)的作用下,脱掉肽A(Fibrinopeptide A,Fp A)与…...
知识图谱实战导论:从什么是KG到LLM与KG/DB的结合实战
前言 本文侧重讲解: 什么是知识图谱LLM与langchain/数据库/知识图谱的结合应用 比如,虽说基于知识图谱的问答 早在2019年之前就有很多研究了,但谁会想到今年KBQA因为LLM如此突飞猛进呢 第一部分 知识图谱入门导论 //待更.. 第二部分 LLM与…...
第5章 会话与会话技术
第5章 会话与会话技术 一. 单选题(共5题,50分)二. 判断题(共5题,50分) 一. 单选题(共5题,50分) (单选题) 阅读下面代码: Book book BookDB.getBook(id)…...
IDEA2023新UI回退老UI
idea2023年发布了新UI,如下所示 但是用起来真心不好用,各种位置也是错乱,用下面方法可以回退老UI...
ElasticSearch(三)
1.数据聚合 聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如: 什么品牌的手机最受欢迎? 这些手机的平均价格、最高价格、最低价格? 这些手机每月的销售情况如何? 实现这些…...
【LinkedHashMap】146. LRU 缓存
146. LRU 缓存 解题思路 与普通的 HashMap 不同,LinkedHashMap 会保持元素的有序性。这可以在某些情况下提供更可预测的迭代顺序直接获取元素 因为使用到该元素 将该元素重新放入队尾 表示最近使用该元素写入元素,首先如果该元素原来存在 那么需要将ke…...
Opencv-python去图标与水印方案实践
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色ÿ…...
自己写过比较蠢的代码:从失败中学习的经验
文章目录 引言1. 代码没有注释2. 长函数和复杂逻辑3. 不恰当的变量名4. 重复的代码5. 不适当的异常处理6. 硬编码的敏感信息7. 没有单元测试结论 🎉 自己写过比较蠢的代码:从失败中学习的经验 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒🍹✨博客主页&a…...
C语言 cortex-A7核 点LED灯 (附 汇编实现、使用C语言 循环实现、使用C语言 封装函数实现【重要、常用】)
1 汇编实现 text global _start start: ************** LED1点灯 ---> PE10 **************/ ************** RCC章节初始化 **************/ CC_INIT:1.使能GPIOE组控制器,通过RCC_MP_AHB4ENSETR寄存器设置GPIOE组使能0x50000A28[4] 1ldr r0,0x50000A28 准…...
LABVIEW 实战案例1--温度报警系统
图1 温度报警系统前面板 图2 温度报警系统后面板...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
