From 85ccdcce72d59ad51d9a94d59f8e66915844c9d8 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Wed, 25 Feb 2026 10:39:28 +0800 Subject: [PATCH] =?UTF-8?q?version:1.0.7=20bugfixes:=20update:=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E9=BB=98=E8=AE=A4=E7=99=BB=E5=BD=95=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=92=AD=E6=94=BE=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/CMakeLists.txt | 44 ++++ app/build.gradle | 23 +- app/src/main/AndroidManifest.xml | 5 +- .../local/LocalCategoryViewModel.java | 50 +++-- .../vc/activity/login/LoginActivity.java | 4 +- .../player/DecryptionPlayerActivity.java | 161 ++++++++++++++ .../player/DecryptionPlayerViewModel.java | 18 ++ .../vc/adapter/CategoryVideoAdapter.java | 16 +- .../com/hainaos/vc/adapter/VideoAdapter.java | 9 +- .../java/com/hainaos/vc/utils/FileUtils.java | 35 +++ .../java/com/hainaos/vc/utils/JgyUtils.java | 7 + .../java/com/hainaos/vc/utils/VideoUtils.java | 5 + .../com/hainaos/vc/video/AesCtrFileUtil.java | 166 +++++++++++++++ .../com/hainaos/vc/video/AesDataSource.java | 69 ++++++ .../com/hainaos/vc/video/AesDataSource2.java | 200 ++++++++++++++++++ .../vc/video/DecryptingDataSource.java | 60 ++++++ .../vc/video/DecryptingDataSourceFactory.java | 25 +++ .../com/hainaos/vc/video/DecryptionPlay.java | 40 ++++ .../com/hainaos/vc/video/VideoEncryptor.java | 36 ++++ app/src/main/jni/hnos.cpp | 22 ++ .../res/layout/activity_decryption_player.xml | 23 ++ {py => ui/py}/encryption_video.py | 99 +++++---- ui/py/encryption_video.spec | 38 ++++ 23 files changed, 1079 insertions(+), 76 deletions(-) create mode 100644 app/CMakeLists.txt create mode 100644 app/src/main/java/com/hainaos/vc/activity/player/DecryptionPlayerActivity.java create mode 100644 app/src/main/java/com/hainaos/vc/activity/player/DecryptionPlayerViewModel.java create mode 100644 app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java create mode 100644 app/src/main/java/com/hainaos/vc/video/AesDataSource.java create mode 100644 app/src/main/java/com/hainaos/vc/video/AesDataSource2.java create mode 100644 app/src/main/java/com/hainaos/vc/video/DecryptingDataSource.java create mode 100644 app/src/main/java/com/hainaos/vc/video/DecryptingDataSourceFactory.java create mode 100644 app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java create mode 100644 app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java create mode 100644 app/src/main/jni/hnos.cpp create mode 100644 app/src/main/res/layout/activity_decryption_player.xml rename {py => ui/py}/encryption_video.py (70%) create mode 100644 ui/py/encryption_video.spec 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, +)