Android 自定义加解密播放音视频(m3u8独立加密)

2023-09-17 14:48:41

背景

  1. 当涉及App内部视频的时候,我们不希望被别人以抓包的形式来爬取我们的视频
  2. 大视频文件以文件方式整个加密的话需要完全下载后才能进行解密
  3. 当前m3u8格式虽然支持加密,但是ts格式的小视频可以独立播放的,也就是ts文件本身没有被加密,或者加密方法过于复杂

根据以上,我通过修改ExoPlayer的源代码实现以下功能,这里不讨论其他视频流加密解密的方法

  1. 大文件分段加密后应用分段解密(m3u8)
  2. 高度自定义,你可以实现任何你需要的加密方法,甚至每一个ts都有自己的解码方式
  3. ts加密,不允许独立播放

加密流程

PS:使用ffmpeg进行音视频分割后使用Java代码进行加密

  1. 音视频分割
    代码就是通过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;
    }
  1. 音视频加密
    这里的视频加密使用的是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里了,所以如果不使用纯源代码来修改的话,也会跟我的演示一样有删除线,但是无伤大雅

  1. 引入依赖,直接在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'
  1. 准备修改代码,我们需要修改的类如下
  • 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文件进行加密处理,一切处理方法都可以实现,如果对您有帮助不妨点个赞

更多推荐

[NLP] LLM---<训练中文LLama2(二)>扩充LLama2词表构建中文tokenization

使用SentencePiece的除了从0开始训练大模型的土豪和大公司外,大部分应该都是使用其为当前开源的大模型扩充词表,比如为LLama扩充通用中文词表(通用中文词表,或者垂直领域词表)。LLaMA原生tokenizer词表中仅包含少量中文字符,在对中文字进行tokenzation时,一个中文汉字往往被切分成多个tok

Selenium+python怎么搭建自动化测试框架、执行自动化测试用例、生成自动化测试报告、发送测试报告邮件

本人在网上查找了很多做自动化的教程和实例,偶然的一个机会接触到了selenium,觉得非常好用。后来就在网上查阅各种selenium的教程,但是网上的东西真的是太多了,以至于很多东西参考完后无法系统的学习和应用。以下整理的只是书中自动化项目的知识内容,介绍怎么搭建自动化测试框架、执行自动化测试用例、生成自动化测试报告、

JSP ssm 网上求职管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点JSPssm网上求职管理系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,spring+springMVC+mybatis),对理解JSPjava编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发,数据库为M

Spring学习(三):MVC

一、什么是MVCMVC(Model-View-Controller)是一种软件设计模式,用于组织和管理应用程序的代码结构。它将应用程序分为三个主要部分,即模型(Model)、视图(View)和控制器(Controller),每个部分都有特定的职责和功能。以下是MVC模式中各个组成部分的概述:模型(Model):模型代表

软件机器人助力企业产地证自动化申报,提高竞争力,降低成本

在国际贸易中,产地证是一项重要的文件,它用于证明货物的原产地,有助于企业在海外清关时获得优惠税率。然而,产地证的申报过程通常涉及繁琐的数据整理和报文生成,消耗了大量时间和精力。本文将探讨如何利用博为小帮软件机器人实现产地证的自动化申报,以提高工作效率和优惠税率的获取。软件机器人简介软件机器人是一种自动化软件机器人,可以

RFID产线自动化升级改造管理方案

应用背景在现代制造业中,产线管理是实现高效生产和优质产品的关键环节,产线管理涉及到生产过程的监控、物料管理、工艺控制、质量追溯等多个方面,有效的产线管理可以提高生产效率、降低成本、改善产品质量,并满足市场需求的变化。产线管理的难点和挑战数据采集和记录的准确性和效率低下:传统的手工记录和条码扫描方式需要大量的人工操作,非

七天学会C语言-第二天(数据结构)

1.If语句:If语句是一种条件语句,用于根据条件的真假执行不同的代码块。它的基本形式如下:if(条件){//条件为真时执行的代码}else{//条件为假时执行的代码}写一个基础的If语句#include<stdio.h>intmain(){intx=10;if(x>5){printf("x大于5\n");}else{

【深度学习】Pytorch 系列教程(十一):PyTorch数据结构:3、变量(Variable)介绍

目录一、前言二、实验环境三、PyTorch数据结构0、分类1、张量(Tensor)2、张量操作(TensorOperations)3、变量(Variable)一、前言ChatGPT:PyTorch是一个开源的机器学习框架,广泛应用于深度学习领域。它提供了丰富的工具和库,用于构建和训练各种类型的神经网络模型。下面是PyT

【C++】详解std::mutex

2023年9月11日,周一中午开始2023年9月11日,周一晚上23:25写完目录概述头文件std::mutex类的成员类型方法没有std::mutex会产生什么问题问题一:数据竞争问题二:不一致lock和unlock死锁概述std::mutex是C++标准库中提供的一种同步原语,用于保护共享资源的访问。std::mu

防火墙 (五十四)

目录前言一、防火墙作用二、防火墙分类三、防火墙性能四、硬件防火墙五、软件防火墙5.1iptables六、iptables应用前言本文就简单的介绍了防火墙的基础内容和一些简单案例的操作。提示:以下是本篇文章正文内容,下面案例可供参考一、防火墙作用在计算机领域,防火墙是用于保护信息安全的设备,其会依照用户定义的规则,允许或

Ascend-pytorch插件介绍及模型迁移

Ascend-pytorch插件介绍及模型迁移用于昇腾适配PyTorch框架,为使用PyTorch框架的开发者提供昇腾AI处理器的超强算力。links:AscendPyTorch官方仓库PyTorch官方主页PyTorch官方文档PyTorch官方仓库当前(2023.9.20)AscendPyTorch支持的pytor

热文推荐