diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
new file mode 100644
index 0000000..ff50cf0
--- /dev/null
+++ b/app/CMakeLists.txt
@@ -0,0 +1,44 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+add_library( # Sets the name of the library.
+ hnos
+
+ # Sets the library as a shared library.
+ SHARED
+
+ # Provides a relative path to your source file(s).
+ src/main/jni/hnos.cpp)
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log)
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+ hnos
+
+ # Links the target library to the log library
+ # included in the NDK.
+ ${log-lib})
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 93e8576..324d8d5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,8 +18,8 @@ android {
//There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature.
minSdkVersion 23
targetSdkVersion 29
- versionCode 7
- versionName "1.0.6"
+ versionCode 8
+ versionName "1.0.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -30,12 +30,17 @@ android {
/*, "x86_64", "mips", "mips64"*/
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
lintOptions {
checkReleaseBuilds false
abortOnError false
}
- viewBinding{
+ viewBinding {
enabled = true
}
@@ -44,9 +49,10 @@ android {
}
}
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ externalNativeBuild {
+ cmake {
+ path file('CMakeLists.txt')
+ }
}
//签名
@@ -144,6 +150,11 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.multidex:multidex:2.0.1"
+ implementation 'com.google.android.exoplayer:exoplayer:2.14.1'
+// implementation 'androidx.media3:media3-exoplayer:1.3.1'
+// implementation 'androidx.media3:media3-exoplayer-dash:1.3.1'
+// implementation 'androidx.media3:media3-ui:1.3.1'
+
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d1086e6..8e1d601 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,7 +67,10 @@
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/DialogCloseOnTouchOutside" />
-
+
, ArrayList>() {
@Override
public ArrayList apply(List stringList) throws Throwable {
- ArrayList localVideoInfos = stringList.stream().map(new Function() {
- @Override
- public LocalVideoInfo apply(String s) {
- File videoFile = new File(file.getAbsolutePath() + File.separator + s);
- LocalVideoInfo localVideoInfo = new LocalVideoInfo();
- localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
- localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
+ ArrayList localVideoInfos = stringList.stream()
+ .filter(new Predicate() {
+ @Override
+ public boolean test(String s) {
+ return VideoUtils.isVideoFormat(s) || s.endsWith(".hnv");
+ }
+ })
+ .map(new Function() {
+ @Override
+ public LocalVideoInfo apply(String s) {
+ File videoFile = new File(file.getAbsolutePath() + File.separator + s);
+ LocalVideoInfo localVideoInfo = new LocalVideoInfo();
+ localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
+ localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
- long time = System.currentTimeMillis();
- FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
- mmr.setDataSource(videoFile.getAbsolutePath());
- int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
- Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
- mmr.release();//释放资源
- localVideoInfo.setDuration(duration / 1000);
- return localVideoInfo;
- }
- }).collect(Collectors.toCollection(ArrayList::new));
+ FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
+ try {
+ long time = System.currentTimeMillis();
+ mmr.setDataSource(videoFile.getAbsolutePath());
+ int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
+ Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
+ localVideoInfo.setDuration(duration / 1000);
+ } catch (Exception e) {
+ Log.e(TAG, "apply: " + e.getMessage());
+ } finally {
+ mmr.release();//释放资源
+ }
+
+ return localVideoInfo;
+ }
+ }).collect(Collectors.toCollection(ArrayList::new));
return localVideoInfos;
}
})
diff --git a/app/src/main/java/com/hainaos/vc/activity/login/LoginActivity.java b/app/src/main/java/com/hainaos/vc/activity/login/LoginActivity.java
index f2c7285..0afb04c 100644
--- a/app/src/main/java/com/hainaos/vc/activity/login/LoginActivity.java
+++ b/app/src/main/java/com/hainaos/vc/activity/login/LoginActivity.java
@@ -47,7 +47,7 @@ public class LoginActivity extends BaseMvvmActivity {
+ private static final String TAG = "DecryptionPlayer";
+
+ private ExoPlayer mExoPlayer;
+ private String mUrl;
+
+ @Override
+ protected int getLayoutId() {
+ return R.layout.activity_decryption_player;
+ }
+
+ @Override
+ protected void initDataBinding() {
+ mViewModel.setCtx(this);
+ mViewModel.setVDBinding(mViewDataBinding);
+ mViewModel.setLifecycle(getLifecycleSubject());
+ mViewDataBinding.setClick(new BtnClick());
+ }
+
+ @Override
+ protected void initView() {
+ mExoPlayer = new SimpleExoPlayer.Builder(this).build();
+ mViewDataBinding.playerView.setPlayer(mExoPlayer);
+
+ }
+
+ @Override
+ protected void initData() {
+ Intent intent = getIntent();
+ mUrl = intent.getStringExtra("url");
+ if (!TextUtils.isEmpty(mUrl)) {
+ Uri videoUri = Uri.parse(mUrl);
+ MediaItem mediaItem = MediaItem.fromUri(videoUri);
+
+ byte[] key = JgyUtils.getSecretKey().getBytes();
+ byte[] iv = JgyUtils.getIvParameter().getBytes();
+
+// SecretKeySpec keySpec = new SecretKeySpec(JgyUtils.getSecretKey().getBytes(), "AES");
+// IvParameterSpec ivSpec = new IvParameterSpec(JgyUtils.getIvParameter().getBytes());
+
+// try {
+// Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+// cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ // 创建自定义 Factory
+ DataSource.Factory dataSourceFactory = () -> {
+ try {
+ return new AesDataSource(key, iv);
+// DataSource upstream = new FileDataSource();
+// return new AesDataSource2(upstream, key, iv);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ };
+
+ MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
+ .createMediaSource(mediaItem);
+ mExoPlayer.setMediaSource(mediaSource);
+ mExoPlayer.prepare();
+ mExoPlayer.play();
+// } catch (Exception e) {
+// Log.e(TAG, "initData: " + e.getMessage());
+// }
+ } else {
+ Toaster.show("文件地址为空");
+ finish();
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // 在 Android 7.0 (API 24) 及以上,在 onStart 中初始化
+ if (Util.SDK_INT >= 24) {
+ initializePlayer();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 在 Android 7.0 以下,在 onResume 中初始化
+ if (Util.SDK_INT < 24) {
+ initializePlayer();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // 在 Android 7.0 以下,在 onPause 中释放
+ if (Util.SDK_INT < 24) {
+ releasePlayer();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ // 在 Android 7.0 及以上,在 onStop 中释放
+ if (Util.SDK_INT >= 24) {
+ releasePlayer();
+ }
+ }
+
+ private void initializePlayer() {
+// if (mExoPlayer == null) {
+// // 1. 创建 ExoPlayer 实例
+// mExoPlayer = new ExoPlayer.Builder(this).build();
+// // 2. 将播放器绑定到视图
+// playerView.setPlayer(mExoPlayer);
+// // 3. 恢复之前的播放状态(如果有)
+// mExoPlayer.setPlayWhenReady(playWhenReady);
+// mExoPlayer.seekTo(currentMediaItemIndex, playbackPosition);
+// }
+//
+// // 4. 准备媒体源
+// Uri videoUri = Uri.parse("https://your-video-url.com/sample.mp4"); // 替换为你的视频URL
+// MediaItem mediaItem = MediaItem.fromUri(videoUri);
+// mExoPlayer.setMediaItem(mediaItem);
+// mExoPlayer.prepare();
+// // 如果需要自动播放,可以调用 exoPlayer.play();
+ }
+
+ private void releasePlayer() {
+ if (mExoPlayer != null) {
+ // 记录当前的播放状态
+// playbackPosition = mExoPlayer.getCurrentPosition();
+// currentMediaItemIndex = mExoPlayer.getCurrentMediaItemIndex();
+// playWhenReady = mExoPlayer.getPlayWhenReady();
+
+ // 释放播放器资源
+ mExoPlayer.release();
+ mExoPlayer = null;
+ }
+ }
+
+ public class BtnClick {
+
+ }
+}
diff --git a/app/src/main/java/com/hainaos/vc/activity/player/DecryptionPlayerViewModel.java b/app/src/main/java/com/hainaos/vc/activity/player/DecryptionPlayerViewModel.java
new file mode 100644
index 0000000..933e7a6
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/activity/player/DecryptionPlayerViewModel.java
@@ -0,0 +1,18 @@
+package com.hainaos.vc.activity.player;
+
+import com.hainaos.vc.base.mvvm.BaseViewModel;
+import com.hainaos.vc.databinding.ActivityDecryptionPlayerBinding;
+import com.trello.rxlifecycle4.android.ActivityEvent;
+
+public class DecryptionPlayerViewModel extends BaseViewModel {
+
+ @Override
+ public ActivityDecryptionPlayerBinding getVDBinding() {
+ return binding;
+ }
+
+ @Override
+ public void onDestroy() {
+
+ }
+}
diff --git a/app/src/main/java/com/hainaos/vc/adapter/CategoryVideoAdapter.java b/app/src/main/java/com/hainaos/vc/adapter/CategoryVideoAdapter.java
index bf5a0b9..70411c5 100644
--- a/app/src/main/java/com/hainaos/vc/adapter/CategoryVideoAdapter.java
+++ b/app/src/main/java/com/hainaos/vc/adapter/CategoryVideoAdapter.java
@@ -23,6 +23,7 @@ import com.arialyy.annotations.Download;
import com.arialyy.aria.core.Aria;
import com.arialyy.aria.core.task.DownloadTask;
import com.hainaos.vc.R;
+import com.hainaos.vc.activity.player.DecryptionPlayerActivity;
import com.hainaos.vc.activity.preview.VideoPreviewActivity;
import com.hainaos.vc.bean.CategoryVideoInfo;
import com.hainaos.vc.config.Permissions;
@@ -103,6 +104,7 @@ public class CategoryVideoAdapter extends RecyclerView.Adapter
holder.root.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- playVideo(position);
+ if (file.getAbsolutePath().endsWith(".hnv")) {
+ Intent intent = new Intent(mContext, DecryptionPlayerActivity.class);
+ intent.putExtra("url", file.getAbsolutePath());
+ mContext.startActivity(intent);
+ } else {
+ playVideo(position);
+ }
}
});
holder.root.setOnLongClickListener(view -> {
diff --git a/app/src/main/java/com/hainaos/vc/utils/FileUtils.java b/app/src/main/java/com/hainaos/vc/utils/FileUtils.java
index 7bcb2f1..3202357 100644
--- a/app/src/main/java/com/hainaos/vc/utils/FileUtils.java
+++ b/app/src/main/java/com/hainaos/vc/utils/FileUtils.java
@@ -89,6 +89,41 @@ public class FileUtils {
return path + File.separator;
}
+ public static void ariaDownloadCover(Context context, String dirName, String fileName, String url, String md5) {
+ String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
+ File dirFile = new File(downLoadPath);
+ if (!dirFile.exists()) {
+ Log.e(TAG, "ariaDownload: mkdirs = " + dirFile.mkdirs());
+ }
+ File file = new File(downLoadPath + fileName);
+ if (file.exists() && !file.isDirectory()) {
+ String fileMD5 = com.blankj.utilcode.util.FileUtils.getFileMD5ToString(file);
+ Log.e("ariaDownload", "fileOnlineMD5=" + md5);
+ Log.e("ariaDownload", "fileMD5=" + fileMD5);
+ if (!TextUtils.isEmpty(md5)) {
+ if (!md5.equals(fileMD5)) {
+ Aria.download(context)
+ .load(url) //读取下载地址
+ .setFilePath(file.getAbsolutePath())
+// .ignoreFilePathOccupy()
+ .setExtendField(url)
+ .create(); //启动下载}
+ } else {
+ Log.e("ariaDownload", "fileName = " + fileName + " exists");
+ }
+ } else {
+ Log.e("ariaDownload", url + " no md5 params , skip");
+ }
+ } else {
+ Aria.download(context)
+ .load(url) //读取下载地址
+ .setFilePath(file.getAbsolutePath())
+// .ignoreFilePathOccupy()
+ .setExtendField(url)
+ .create(); //启动下载}
+ }
+ }
+
public static void ariaDownload(Context context, String dirName, String url, String md5) {
String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
File dirFile = new File(downLoadPath);
diff --git a/app/src/main/java/com/hainaos/vc/utils/JgyUtils.java b/app/src/main/java/com/hainaos/vc/utils/JgyUtils.java
index 090132f..b901a13 100644
--- a/app/src/main/java/com/hainaos/vc/utils/JgyUtils.java
+++ b/app/src/main/java/com/hainaos/vc/utils/JgyUtils.java
@@ -31,6 +31,13 @@ public class JgyUtils {
private static JgyUtils sInstance;
private Context mContext;
+ static {
+ System.loadLibrary("hnos");
+ }
+
+ public static native String getSecretKey();
+
+ public static native String getIvParameter();
private JgyUtils(Context context) {
if (context == null) {
diff --git a/app/src/main/java/com/hainaos/vc/utils/VideoUtils.java b/app/src/main/java/com/hainaos/vc/utils/VideoUtils.java
index 5cec218..d6a1541 100644
--- a/app/src/main/java/com/hainaos/vc/utils/VideoUtils.java
+++ b/app/src/main/java/com/hainaos/vc/utils/VideoUtils.java
@@ -1,5 +1,7 @@
package com.hainaos.vc.utils;
+import android.text.TextUtils;
+
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
@@ -27,6 +29,9 @@ public class VideoUtils {
};
public static boolean isVideoFormat(String filePath) {
+ if (TextUtils.isEmpty(filePath)) {
+ return false;
+ }
for (String s : video_extension) {
if (filePath.endsWith(s)) {
return true;
diff --git a/app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java b/app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java
new file mode 100644
index 0000000..9240741
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java
@@ -0,0 +1,166 @@
+package com.hainaos.vc.video;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AesCtrFileUtil {
+
+ private static final String ALGORITHM = "AES";
+ private static final String TRANSFORMATION = "AES/CTR/NoPadding";
+ private static final int IV_SIZE = 16; // AES block size
+ private static final int BUFFER_SIZE = 8192; // 8KB buffer
+
+ /**
+ * 加密文件
+ * 逻辑:生成随机IV -> 写入IV到文件头 -> 读取源文件 -> 加密 -> 写入密文
+ *
+ * @param keySecret AES密钥 (16, 24, or 32 bytes)
+ * @param inputFile 源文件路径
+ * @param outputFile 输出文件路径
+ */
+ public static void encryptFile(byte[] keySecret, File inputFile, File outputFile)
+ throws GeneralSecurityException, IOException {
+
+ // 1. 准备密钥
+ SecretKeySpec keySpec = new SecretKeySpec(keySecret, ALGORITHM);
+
+ // 2. 生成随机 IV
+ // byte[] iv = new byte[IV_SIZE];
+ byte[] iv = "hainaos1hainaos1".getBytes();
+ new SecureRandom().nextBytes(iv);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ // 3. 初始化 Cipher
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+
+ // 4. 流式读写
+ try (FileInputStream inputStream = new FileInputStream(inputFile);
+ FileOutputStream outputStream = new FileOutputStream(outputFile)) {
+
+ // IMPORTANT: 先将 IV 写入输出文件的头部,解密时需要读取它
+ outputStream.write(iv);
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ // update 方法处理数据块
+ byte[] output = cipher.update(buffer, 0, bytesRead);
+ if (output != null) {
+ outputStream.write(output);
+ }
+ }
+ // doFinal 处理最后的数据(虽然 NoPadding 通常不需要,但必须调用以完成操作)
+ byte[] outputBytes = cipher.doFinal();
+ if (outputBytes != null) {
+ outputStream.write(outputBytes);
+ }
+ }
+ }
+
+ /**
+ * 解密文件
+ * 逻辑:读取文件头IV -> 初始化Cipher -> 读取密文 -> 解密 -> 写入原文件
+ */
+ public static void decryptFile(byte[] keySecret, File inputFile, File outputFile)
+ throws GeneralSecurityException, IOException {
+
+ SecretKeySpec keySpec = new SecretKeySpec(keySecret, ALGORITHM);
+
+ try (FileInputStream inputStream = new FileInputStream(inputFile);
+ FileOutputStream outputStream = new FileOutputStream(outputFile)) {
+
+ // 1. 从文件头部读取 IV
+ byte[] iv = new byte[IV_SIZE];
+ int ivRead = inputStream.read(iv);
+ if (ivRead < IV_SIZE) {
+ throw new IllegalArgumentException("文件太短,无法读取 IV/文件已损坏");
+ }
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ // 2. 初始化 Cipher (解密模式)
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ // 3. 流式处理剩余内容
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ byte[] output = cipher.update(buffer, 0, bytesRead);
+ if (output != null) {
+ outputStream.write(output);
+ }
+ }
+ byte[] outputBytes = cipher.doFinal();
+ if (outputBytes != null) {
+ outputStream.write(outputBytes);
+ }
+ }
+ }
+
+ public static void encryptFile(File inputFile, File outputFile, byte[] key, byte[] iv) throws Exception {
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+
+ try (FileInputStream fis = new FileInputStream(inputFile);
+ FileOutputStream fos = new FileOutputStream(outputFile);
+ CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
+
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = fis.read(buffer)) != -1) {
+ cos.write(buffer, 0, read);
+ }
+ }
+ }
+
+ // --- 测试 Main 方法 ---
+ public static void main(String[] args) {
+ try {
+ // 1. 生成一个测试密钥 (256位)
+ KeyGenerator keyGen = KeyGenerator.getInstance("AES");
+ keyGen.init(256);
+ SecretKey secretKey = keyGen.generateKey();
+ // byte[] keyBytes = secretKey.getEncoded();
+ byte[] keyBytes = "hainaos_key_123_".getBytes();
+
+ // 2. 准备文件
+ File sourceFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_video_1.mp4");
+ // File sourceFile = new File("test_original.txt");
+ File encryptedFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_video_1.hnv");
+ File decryptedFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_decrypted.mp4");
+
+ encryptFile(sourceFile, encryptedFile, keyBytes, "hainaos1hainaos1".getBytes());
+
+ // 创建一个简单的测试文件
+ // try (FileWriter writer = new FileWriter(sourceFile)) {
+ // writer.write("Hello, World! This is a test for AES/CTR/NoPadding
+ // encryption.");
+ // }
+
+ // System.out.println("开始加密...");
+ // encryptFile(keyBytes, sourceFile, encryptedFile);
+ // System.out.println("加密完成: " + encryptedFile.getAbsolutePath());
+
+ // System.out.println("开始解密...");
+ // decryptFile(keyBytes, encryptedFile, decryptedFile);
+ // System.out.println("解密完成: " + decryptedFile.getAbsolutePath());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hainaos/vc/video/AesDataSource.java b/app/src/main/java/com/hainaos/vc/video/AesDataSource.java
new file mode 100644
index 0000000..45e4182
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/AesDataSource.java
@@ -0,0 +1,69 @@
+package com.hainaos.vc.video;
+
+import android.net.Uri;
+
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AesDataSource implements DataSource {
+ private final Cipher cipher;
+ private InputStream inputStream;
+ private Uri uri;
+
+ public AesDataSource(byte[] key, byte[] iv) throws Exception {
+ cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ this.uri = dataSpec.uri;
+ // 注意:如果是网络流,这里需要使用 HttpURLConnection
+ // 示例代码以本地文件为例
+ FileInputStream fis = new FileInputStream(uri.getPath());
+ inputStream = new CipherInputStream(fis, cipher);
+
+ // 如果支持 Seek(拖动进度条),此处需处理 skip
+ if (dataSpec.position > 0) {
+ inputStream.skip(dataSpec.position);
+ }
+ return dataSpec.length;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) return 0;
+ int read = inputStream.read(buffer, offset, readLength);
+ if (read == -1) return -1;
+ return read;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (inputStream != null) {
+ inputStream.close();
+ inputStream = null;
+ }
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hainaos/vc/video/AesDataSource2.java b/app/src/main/java/com/hainaos/vc/video/AesDataSource2.java
new file mode 100644
index 0000000..6be0322
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/AesDataSource2.java
@@ -0,0 +1,200 @@
+package com.hainaos.vc.video;
+
+import android.net.Uri;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AesDataSource2 implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] secretKey;
+ private final byte[] iv;
+
+ private CipherInputStream cipherInputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ public AesDataSource2(DataSource upstream, byte[] secretKey, byte[] iv) {
+ this.upstream = upstream;
+ this.secretKey = secretKey;
+ this.iv = iv;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ // 1. 获取请求的绝对位置
+ long position = dataSpec.position;
+
+ // AES 块大小通常为 16 字节
+ final int AES_BLOCK_SIZE = 16;
+
+ // 2. 计算块索引 (Block Index) 和 块内偏移 (Offset inside the block)
+ // 例如:position = 100,blockIndex = 6 (96字节处),offset = 4
+ long blockIndex = position / AES_BLOCK_SIZE;
+ int offsetInBlock = (int) (position % AES_BLOCK_SIZE);
+
+ // 3. 计算对齐后的起始读取位置 (必须是 16 的倍数)
+ long startPosition = blockIndex * AES_BLOCK_SIZE;
+
+ try {
+ // 4. 初始化 Cipher
+ Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
+
+ // 【关键步骤】:根据 blockIndex 计算新的 IV
+ // CTR 模式下:NewIV = OriginalIV + blockIndex
+ byte[] newIv = getAdjustedIv(this.iv, blockIndex);
+ IvParameterSpec ivSpec = new IvParameterSpec(newIv);
+
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ // 5. 让上层数据源 (FileDataSource) 从对齐的位置 (startPosition) 开始读
+ // 注意:我们修改了 position,但保持 length 不变 (或者处理 open ended)
+ long length = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length + offsetInBlock : C.LENGTH_UNSET;
+
+ DataSpec newSpec = dataSpec.buildUpon()
+ .setPosition(startPosition)
+ .setLength(length)
+ .build();
+
+ // 打开上层流
+ InputStream inputStream = new DataSourceInputStream(upstream, newSpec);
+
+ // 创建解密流
+ cipherInputStream = new CipherInputStream(inputStream, cipher);
+
+ // 6. 【重要】跳过块内的偏移量
+ // 因为我们要给 ExoPlayer 返回的是从 dataSpec.position 开始的数据,
+ // 但我们是从 startPosition (前一个16倍数) 开始解密的,所以前面多读的 offsetInBlock 个字节是无用的。
+ if (offsetInBlock > 0) {
+ forceSkip(cipherInputStream, offsetInBlock);
+ }
+
+ opened = true;
+
+ // 计算剩余长度
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ long upstreamLength = upstream.open(newSpec); // 这一步其实已经在 DataSourceInputStream 里做过了,这里仅作逻辑参考
+ // 通常 upstream.open 返回的是从 startPosition 开始的长度
+ // 如果 upstream 支持长度解析:
+ // bytesRemaining = upstreamLength != C.LENGTH_UNSET ? upstreamLength - offsetInBlock : C.LENGTH_UNSET;
+ bytesRemaining = C.LENGTH_UNSET;
+ }
+
+ return bytesRemaining;
+
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * 根据块索引计算新的 IV。
+ * CTR 模式将 IV 视为一个大整数 (BigEndian),每过一个块,计数器 +1。
+ */
+ private byte[] getAdjustedIv(byte[] originalIv, long blockIndex) {
+ // 使用 BigInteger 处理大数加法,防止溢出
+ BigInteger ivVal = new BigInteger(1, originalIv);
+ BigInteger offset = BigInteger.valueOf(blockIndex);
+ BigInteger newIvVal = ivVal.add(offset);
+
+ byte[] newIv = newIvVal.toByteArray();
+
+ // BigInteger.toByteArray() 可能会因为符号位导致长度变为 17 (如果是正数且最高位是1)
+ // 或者因为数值较小导致长度小于 16。必须确保返回 16 字节。
+ byte[] result = new byte[16];
+ int srcOffset = newIv.length > 16 ? newIv.length - 16 : 0;
+ int dstOffset = newIv.length < 16 ? 16 - newIv.length : 0;
+ int copyLength = newIv.length > 16 ? 16 : newIv.length;
+
+ System.arraycopy(newIv, srcOffset, result, dstOffset, copyLength);
+ return result;
+ }
+
+ /**
+ * 强制跳过指定字节数。CipherInputStream 的 skip 有时不可靠,建议循环 read。
+ */
+ private void forceSkip(InputStream stream, int bytesToSkip) throws IOException {
+ long skipped = 0;
+ byte[] skipBuffer = new byte[1024];
+ while (skipped < bytesToSkip) {
+ int toRead = (int) Math.min(bytesToSkip - skipped, skipBuffer.length);
+ int read = stream.read(skipBuffer, 0, toRead);
+ if (read == -1) {
+ break;
+ }
+ skipped += read;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ int bytesToRead = readLength;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesToRead = (int) Math.min(readLength, bytesRemaining);
+ }
+ if (bytesToRead == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead = cipherInputStream.read(buffer, offset, bytesToRead);
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // 预期还要读数据但读不到了,抛错
+ throw new IOException("End of stream reached prematurely");
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (opened) {
+ opened = false;
+ if (cipherInputStream != null) {
+ cipherInputStream.close(); // 这也会关闭 upstream
+ cipherInputStream = null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hainaos/vc/video/DecryptingDataSource.java b/app/src/main/java/com/hainaos/vc/video/DecryptingDataSource.java
new file mode 100644
index 0000000..6b20d47
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/DecryptingDataSource.java
@@ -0,0 +1,60 @@
+package com.hainaos.vc.video;
+
+import android.net.Uri;
+
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+import java.io.IOException;
+
+import javax.crypto.Cipher;
+
+public class DecryptingDataSource implements DataSource {
+ private final DataSource upstream;
+ private final Cipher cipher;
+
+ public DecryptingDataSource(DataSource upstream, Cipher cipher) {
+ this.upstream = upstream;
+ this.cipher = cipher;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ // 初始化解密状态,比如根据 dataSpec.position 调整 Cipher 的偏移量
+ return upstream.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) return 0;
+
+ // 1. 从原始源(文件或网络)读取加密数据
+ int bytesRead = upstream.read(buffer, offset, readLength);
+ if (bytesRead == -1) return -1;
+
+ // 2. 原地解密 (In-place decryption)
+ // 注意:此处需要处理流式解密的块对齐问题
+ byte[] decryptedData = cipher.update(buffer, offset, bytesRead);
+ if (decryptedData != null) {
+ System.arraycopy(decryptedData, 0, buffer, offset, decryptedData.length);
+ }
+
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hainaos/vc/video/DecryptingDataSourceFactory.java b/app/src/main/java/com/hainaos/vc/video/DecryptingDataSourceFactory.java
new file mode 100644
index 0000000..15a8151
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/DecryptingDataSourceFactory.java
@@ -0,0 +1,25 @@
+package com.hainaos.vc.video;
+
+import android.content.Context;
+
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSource;
+
+import javax.crypto.Cipher;
+
+public class DecryptingDataSourceFactory implements DataSource.Factory {
+ private final Context context;
+ private final Cipher cipher;
+
+ public DecryptingDataSourceFactory(Context context, Cipher cipher) {
+ this.context = context;
+ this.cipher = cipher;
+ }
+
+ @Override
+ public DataSource createDataSource() {
+ // 通常包装一个 DefaultDataSource 用于支持本地文件和网络
+ DataSource upstream = new DefaultDataSource(context, "User-Agent", false);
+ return new DecryptingDataSource(upstream, cipher);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java b/app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java
new file mode 100644
index 0000000..973e178
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java
@@ -0,0 +1,40 @@
+package com.hainaos.vc.video;
+
+public class DecryptionPlay {
+ private static final String ALGORITHM = "AES/CTR/NoPadding";
+
+// public void testCode(Context context) {
+// SecretKeySpec keySpec = new SecretKeySpec(JgyUtils.getSecretKey().getBytes(), "AES");
+// IvParameterSpec ivSpec = new IvParameterSpec(JgyUtils.getIvParameter().getBytes());
+//
+// // 初始化 Cipher (例如 AES/CTR/NoPadding)
+// Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+// cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+// DataSource.Factory dataSourceFactory = new DecryptingDataSourceFactory(context, cipher);
+// MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
+// .createMediaSource(MediaItem.fromUri(encryptedUri));
+//
+//
+//
+// // 1. 准备密钥和 IV (需与加密时一致)
+// byte[] secretKey = "your_16_byte_key".getBytes();
+// byte[] iv = "your_16_byte_iv__".getBytes();
+//
+// // 2. 创建用于解密的 DataSource 工厂
+// DataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context) ;
+// DataSource.Factory decryptingDataSourceFactory = () ->
+// new AesCipherDataSource(secretKey, upstreamFactory.createDataSource());
+//
+// // 3. 构建播放列表项并设置 MediaSource
+// ExoPlayer player = new ExoPlayer.Builder(context).build();
+// MediaItem mediaItem = MediaItem.fromUri(Uri.fromFile(encryptedFile));
+//
+// MediaSource mediaSource = new ProgressiveMediaSource.Factory(decryptingDataSourceFactory)
+// .createMediaSource(mediaItem);
+//
+// player.setMediaSource(mediaSource);
+// player.prepare();
+// player.play();
+// }
+
+}
diff --git a/app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java b/app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java
new file mode 100644
index 0000000..2ea65a9
--- /dev/null
+++ b/app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java
@@ -0,0 +1,36 @@
+package com.hainaos.vc.video;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class VideoEncryptor {
+ private static final String ALGORITHM = "AES/CTR/NoPadding";
+
+ // 注意:实际开发中,Key 和 IV 应该安全存储,不要硬编码
+ public static void encryptVideo(File inputFile, File outputFile, byte[] key, byte[] iv) throws Exception {
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ Cipher cipher = Cipher.getInstance(ALGORITHM);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+
+ try (FileInputStream fis = new FileInputStream(inputFile);
+ FileOutputStream fos = new FileOutputStream(outputFile);
+ CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
+
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ cos.write(buffer, 0, bytesRead);
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/jni/hnos.cpp b/app/src/main/jni/hnos.cpp
new file mode 100644
index 0000000..f101156
--- /dev/null
+++ b/app/src/main/jni/hnos.cpp
@@ -0,0 +1,22 @@
+#include
+#include
+#include
+// 日志打印
+#include
+
+#define LOG_TAG "TAG_LOG"
+#define LOGI(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_hainaos_vc_utils_JgyUtils_getSecretKey(JNIEnv *env, jclass clazz) {
+ std::string key = "hainaos_key_123_";
+ return env->NewStringUTF(key.c_str());
+}
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_hainaos_vc_utils_JgyUtils_getIvParameter(JNIEnv *env, jclass clazz) {
+ std::string key = "hainaos1hainaos1";
+ return env->NewStringUTF(key.c_str());
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_decryption_player.xml b/app/src/main/res/layout/activity_decryption_player.xml
new file mode 100644
index 0000000..4304f59
--- /dev/null
+++ b/app/src/main/res/layout/activity_decryption_player.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/py/encryption_video.py b/ui/py/encryption_video.py
similarity index 70%
rename from py/encryption_video.py
rename to ui/py/encryption_video.py
index de1da7d..1493390 100644
--- a/py/encryption_video.py
+++ b/ui/py/encryption_video.py
@@ -27,11 +27,11 @@ class VideoEncryptorGUI:
# Key输入
self.label_key = tk.Label(master, text="AES密钥 (Base64编码或16/24/32字节字符串):")
self.label_key.pack(pady=(10,0))
- self.entry_key = tk.Entry(master, width=50, show="*")
+ self.entry_key = tk.Entry(master, width=50)
self.entry_key.pack(pady=5)
# IV输入
- self.label_iv = tk.Label(master, text="初始化向量IV (Base64编码或16字节字符串):")
+ self.label_iv = tk.Label(master, text="初始化向量IV (Base64编码或8字节字符串):")
self.label_iv.pack(pady=(10,0))
self.entry_iv = tk.Entry(master, width=50)
self.entry_iv.pack(pady=5)
@@ -64,21 +64,21 @@ class VideoEncryptorGUI:
self.status_label.config(text="文件已选择,请设置密钥")
def generate_key_iv(self):
- """生成随机的密钥和IV"""
- # 生成随机密钥(32字节,AES-256)和IV(16字节)
+ """生成随机的密钥和nonce"""
+ # 生成随机密钥(32字节,AES-256)和nonce(8字节用于CTR模式)
key = get_random_bytes(32)
- iv = get_random_bytes(16)
+ nonce = get_random_bytes(8) # AES CTR模式使用的nonce应该是8字节
# 转换为Base64编码字符串,显示在输入框中
key_b64 = base64.b64encode(key).decode('utf-8')
- iv_b64 = base64.b64encode(iv).decode('utf-8')
+ nonce_b64 = base64.b64encode(nonce).decode('utf-8')
self.entry_key.delete(0, tk.END)
self.entry_key.insert(0, key_b64)
self.entry_iv.delete(0, tk.END)
- self.entry_iv.insert(0, iv_b64)
+ self.entry_iv.insert(0, nonce_b64)
- self.status_label.config(text="已生成随机密钥和IV,请妥善保存!")
+ self.status_label.config(text="已生成随机密钥和nonce,请妥善保存!")
def encrypt_video(self):
"""加密视频文件"""
@@ -94,35 +94,38 @@ class VideoEncryptorGUI:
return
try:
+
# 处理密钥:尝试Base64解码,否则使用字符串编码
- try:
- key = base64.b64decode(key_str)
- except:
- key = key_str.encode('utf-8')
+
+ # try:
+ # key = base64.b64decode(key_str)
+ # except:
+ key = key_str.encode('utf-8')
# 处理IV:尝试Base64解码,否则使用字符串编码
- try:
- iv = base64.b64decode(iv_str)
- except:
- iv = iv_str.encode('utf-8')
+
+ # try:
+ # iv = base64.b64decode(iv_str)
+ # except:
+ iv = iv_str.encode('utf-8')
# 确保密钥长度为16、24或32字节
- if len(key) not in [16, 24, 32]:
- if len(key) < 16:
- key = key.ljust(16, b'\0')
- elif len(key) < 24:
- key = key.ljust(24, b'\0')
- elif len(key) < 32:
- key = key.ljust(32, b'\0')
- else:
- key = key[:32]
+ # if len(key) not in [16, 24, 32]:
+ # if len(key) < 16:
+ # key = key.ljust(16, b'\0')
+ # elif len(key) < 24:
+ # key = key.ljust(24, b'\0')
+ # elif len(key) < 32:
+ # key = key.ljust(32, b'\0')
+ # else:
+ # key = key[:32]
# 确保IV长度为16字节
- if len(iv) != 16:
- if len(iv) < 16:
- iv = iv.ljust(16, b'\0')
- else:
- iv = iv[:16]
+ # if len(iv) != 16:
+ # if len(iv) < 16:
+ # iv = iv.ljust(16, b'\0')
+ # else:
+ # iv = iv[:16]
# 生成输出文件路径
file_dir = os.path.dirname(self.input_file_path)
@@ -140,24 +143,30 @@ class VideoEncryptorGUI:
messagebox.showerror("加密错误", f"加密过程中发生错误: {str(e)}")
def do_encryption(self, input_path, output_path, key, iv):
+ print(f"iv len: {len(iv)}")
+
"""
执行加密操作
- 使用AES/CBC/PKCS5Padding模式,与Java代码保持一致
+ 使用AES/CTR/NoPadding模式
"""
- # 读取输入文件
- with open(input_path, 'rb') as f_in:
- plaintext = f_in.read()
-
- # 创建密码器
- cipher = AES.new(key, AES.MODE_CBC, iv)
-
- # 应用PKCS5填充并加密
- padded_data = pad(plaintext, AES.block_size)
- ciphertext = cipher.encrypt(padded_data)
-
- # 写入输出文件
- with open(output_path, 'wb') as f_out:
- f_out.write(ciphertext)
+ try:
+ # 创建CTR模式加密器
+ cipher = AES.new(key, AES.MODE_CTR, nonce=iv)
+ with open(input_path, 'rb') as fin:
+ with open(output_path, 'wb') as fout:
+ # 将IV写入文件开头,解密时需要
+ fout.write(iv)
+
+ # 逐块加密文件
+ while True:
+ chunk = fin.read(4096) # 每次读取4KB
+ if len(chunk) == 0:
+ break
+ encrypted_chunk = cipher.encrypt(chunk)
+ fout.write(encrypted_chunk)
+
+ except Exception as e:
+ raise Exception(f"加密失败: {str(e)}")
def main():
# 检查所需库是否已安装
diff --git a/ui/py/encryption_video.spec b/ui/py/encryption_video.spec
new file mode 100644
index 0000000..d229ae2
--- /dev/null
+++ b/ui/py/encryption_video.spec
@@ -0,0 +1,38 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+ ['encryption_video.py'],
+ pathex=[],
+ binaries=[],
+ datas=[],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+ optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ [],
+ name='encryption_video',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+)