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


    背景

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    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();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    加密完成之后将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
    • 2
    • 3
    • 4
    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();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    修改类“DefaultDataSourceFactory
    此类只需要修改一点,那就是将DefaultDataSource的create过程引导到我们自己写的DefaultDataSource,也就是删除原来的ExoPlayer2的依赖引入,引入刚刚讲到的DefaultHttpDataSource,不需要修改代码,只需要切换依赖即可

     public DefaultDataSourceFactory(
          Context context, @Nullable String userAgent, @Nullable TransferListener listener) {
        this(context, listener, new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
      } 
    
    • 1
    • 2
    • 3
    • 4

    音视频播放

    因为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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    源码下载

    结语

    代码里给大家提供了一个小视频,如果按照流程编写应该是可以顺利播放的,如果需要还可以把m3u8文件进行加密处理,一切处理方法都可以实现,如果对您有帮助不妨点个赞

  • 相关阅读:
    STL容器
    72. 编辑距离(动态规划)
    JS 使用Reg表达式匹配后直接取值
    Spring Security(三) —— 加密系统
    压缩和归档操作
    企业安全—SDL概述篇
    LeetCode·71.简化路径·栈模拟
    备战无人机配送:互联网派To C、技术派To B
    学习完C++ 并发编程后 手写线程池 最简单的线程池
    Java8 Stream 的核心秘密
  • 原文地址:https://blog.csdn.net/weixin_43328457/article/details/132941832