Android custom encryption and decryption for playing audio and video (m3u8 independent encryption)

background

  1. When it comes to videos inside the App, we don’t want others to crawl our videos in the form of packet capture.
  2. Large video files are encrypted in file formatEncryptedIt needs to be completely downloaded before decryption
  3. Although the current m3u8 format supports encryption, small videos in ts format can be played independently, that is, the ts file itself is not encrypted, or the encryption method is too complicated.

Based on the above, I implemented the following functions by modifying the source code of ExoPlayer. Other video stream encryption and decryption methods will not be discussed here.

  1. Apply segmented decryption after segmented encryption of large files (m3u8)
  2. Highly customizable, you can implement any encryption method you need, and even each ts has its own decoding method
  3. ts encryption, independent playback is not allowed

Encryption process

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

  1. Audio and video segmentation
    The code is to execute the ffmpeg command through java. Please make sure that ffmpeg is installed in the environment variable. The internal code can be modified by your own needs. The audio and Videos are divided in similar ways
 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. Audio and video encryption
    The video encryption here uses AES encryption, which encrypts all files ending in ts. The latter method is decryption, which is generally not used
 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();
            }
        }
    }

After the encryption is completed, place m3u8 on the server, and the divided files must be in the same directory, or manually set when slicing to ensure that the sliced ​​video can be played normally.

Audio and video decryption

What is implemented here is to modify the source code of ExoPlayer, because there are many choices for playing videos on Android phones. You can also modify other players according to my method. This time, I will follow the ExoPlayer demonstration< /span>
PS: Because Google has integrated ExoPlayer into MediaPlayer3, if you don’t use pure source code to modify it, there will be strikethroughs like my demonstration, but it’s harmless

  1. Introduce dependencies, directly introduce the dependencies of ExoPlayer2 in Build.gradle of the App layer. The video stream we want to use is in hls format, so we need to introduce the hls module.
	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. Prepare to modify the code. The classes we need to modify are as follows
  • DefaultDataSource
  • DefaultDataSourceFactory
  • DefaultHttpDataSource
  • HttpDataSource

We only need to copy the source code and modify it. When using ExoPlayer to play videos, we can use our own class. If you don’t want to do this, you can directly download the source code of ExoPlayer2 and modify it. In this case, it is still possible It can remove obsolete expressions and there are not so many strikethroughs. Next we officially start to modify
and modify the class "DefaultHttpDataSource"
I will explain the code in the form of comments. Note that this is just a demonstration of a simple way of customizing encryption and decryption, so brute force judgment will be made based on the file with ts at the end of the file name, and it will be refined. The processing method can be expanded in many ways, such as only encrypting the middle part of the video as a member video, so that only a single video stream is needed to solve the preview problem, and there is no fear of modifying the VIP flag inside the application (it is invalid for brute force cracking methods such as modifying the source code). After all, the source code has been dug out for you)

//定义解密流,主要使用此流来进行解密
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();
            }
        }
    }

Modify the class "DefaultDataSourceFactory"
This class only needs to modify one thing, that is, change the create of DefaultDataSource The process leads to the DefaultDataSource we wrote ourselves, that is, we delete the original ExoPlayer2 dependency introduction and introduce the DefaultHttpDataSource just mentioned. There is no need to modify the code, just switch the dependencies.

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

Audio and video playback

Because ExoPlayer2 supports both audio and video playback, it can be completed using the following methods

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();
    }
}

Source code download

Conclusion

The code provides you with a short video. If it is written according to the process, it should be played smoothly. If necessary, the m3u8 file can be encrypted. All processing methods can be implemented. If it helps you, please give it a like.

Guess you like

Origin blog.csdn.net/weixin_43328457/article/details/132941832