├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── git_toolbox_prj.xml ├── gradle.xml ├── highlightedFiles.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── release │ ├── VCAMSX1.1.2.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wangyiheng │ │ └── vcamsx │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── xposed_init │ ├── java │ │ └── com │ │ │ └── wangyiheng │ │ │ └── vcamsx │ │ │ ├── MainActivity.kt │ │ │ ├── MainHook.kt │ │ │ ├── MyApplication.kt │ │ │ ├── camerahook │ │ │ ├── CameraOne.kt │ │ │ └── CameraTwo.kt │ │ │ ├── components │ │ │ ├── DisclaimerDialog.kt │ │ │ ├── FlowRow.kt │ │ │ ├── LivePlayerDialog.kt │ │ │ ├── SettingRow.kt │ │ │ └── VideoPlayerDialog.kt │ │ │ ├── data │ │ │ ├── di │ │ │ │ └── AppModule.kt │ │ │ ├── models │ │ │ │ ├── Collect.kt │ │ │ │ ├── VideoInfo.kt │ │ │ │ └── VideoStatues.kt │ │ │ └── services │ │ │ │ ├── ApiInterceptor.kt │ │ │ │ ├── ApiService.kt │ │ │ │ └── NetworkModule.kt │ │ │ ├── hooks │ │ │ └── CameraHookManager.kt │ │ │ ├── modules │ │ │ └── home │ │ │ │ ├── controllers │ │ │ │ └── HomeController.kt │ │ │ │ └── view │ │ │ │ └── HomeScreen.kt │ │ │ ├── services │ │ │ └── VcamsxForegroundService.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── utils │ │ │ ├── HLog.kt │ │ │ ├── InfoManage.kt │ │ │ ├── InfoProcesser.kt │ │ │ ├── Lib.kt │ │ │ ├── MediaPlayerManager.kt │ │ │ ├── MultiprocessSharedPreferences.kt │ │ │ ├── VideoPlayer.kt │ │ │ ├── VideoProvider.kt │ │ │ └── VideoToFrames.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── logo.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── logo.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── logo.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── logo.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── logo.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── logo.png │ │ ├── raw │ │ └── vcamsx.mp4 │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── wangyiheng │ └── vcamsx │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/highlightedFiles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 iiheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 安卓虚拟摄像头 2 | - 基于Xposed的虚拟摄像头 3 | # 请勿用于任何非法用途,所有后果自负!! 4 | ## 使用演示 5 | - https://fastly.jsdelivr.net/gh/iiheng/TuChuang@main/1700961311425EasyGIF-1700961287297.gif 6 | ## 开发计划 7 | - [x] 支持rtmp传输直播,提高稳定性 8 | - [ ] 支持视频提前选择,自定义播放顺序 9 | ## 开发环境 10 | - Android SDK 34 11 | - Xposed 82 12 | - xiaomi 9 MIUI 11.0.3 13 | - xiaomi 8 MIUI 11.0.3 14 | - Redim K40 MUI 14.0.7 15 | - 酷比魔方50pro MIUI 14.0.5 16 | - Lsposed lastest 17 | ## 使用方法 18 | 1. 在Lsposed中勾选自己想要的播放平台 19 | 2. 在软件中选择自己想要播放的视频 20 | 3. 打开视频开关 21 | 4. 然后选择平台播放 22 | ## 支持替换 23 | 1. 支持视频替换 24 | 2. 支持RTMP直播替换,不稳定 25 | ## 注意事项 26 | 1. 视频播放需要与平台播放的格式相同,基本支持9:16的视频,例如:3840x2160,1920x1080,1280x720,854x480,640x360,426x240,256x144 27 | 2. 画面黑屏,相机启动失败,因为视频解码有问题,请多次点击翻转摄像头 28 | 3. 画面翻转,和原视频不匹配,当前视频播放还未做调整,请手动调整视频 29 | 4. 不同软件对于硬解码和软解码的要求不同,如果多次只出声音不出画面,请切换视频解码方式 30 | 5. 硬解码流畅于软解码,请根据你的手机型号来判断是否支持硬解码,软解码的适配性较高,视频基本都支持播放 31 | 32 | ## 反馈问题 33 | - Telegram : [点击加入](https://t.me/+WbEK_suGxG9mZGM1) 34 | - 在issues中反馈,如果为BUG反馈,请附带Xposed模块日志信息 35 | 36 | ## 致谢 37 | - 提供hook代码:https://github.com/Xposed-Modules-Repo/com.example.vcam 38 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.wangyiheng.vcamsx" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "com.wangyiheng.vcamsx" 12 | minSdk = 24 13 | //noinspection EditedTargetSdkVersion 14 | targetSdk = 34 15 | versionCode = 13 16 | versionName = "1.1.2" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary = true 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 28 | signingConfig = signingConfigs.getByName("debug") 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = "1.8" 37 | } 38 | buildFeatures { 39 | compose = true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion = "1.4.3" 43 | } 44 | packaging { 45 | resources { 46 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | 53 | // Core library for Kotlin extensions and utilities 54 | implementation("androidx.core:core-ktx:1.9.0") 55 | // Lifecycle components for using ViewModel and LiveData in a Kotlin-friendly way 56 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") 57 | 58 | // Compose support for Activities 59 | implementation("androidx.activity:activity-compose:1.8.0") 60 | 61 | // Bill of Materials (BOM) for all Compose libraries, ensures compatible versions 62 | implementation(platform("androidx.compose:compose-bom:2023.03.00")) 63 | implementation ("com.contrarywind:Android-PickerView:4.1.9") 64 | // Compose UI framework 65 | implementation("androidx.compose.ui:ui") 66 | 67 | // Compose library for graphics 68 | implementation("androidx.compose.ui:ui-graphics") 69 | 70 | // Tooling for UI preview in Compose 71 | implementation("androidx.compose.ui:ui-tooling-preview") 72 | 73 | // Material3 design components for Compose 74 | implementation("androidx.compose.material3:material3") 75 | 76 | // Media3 ExoPlayer for handling media playback 77 | implementation("androidx.media3:media3-exoplayer:1.2.0") 78 | implementation("androidx.media3:media3-ui:1.2.0") 79 | implementation("androidx.compose.ui:ui-text-android:1.5.4") 80 | 81 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 82 | implementation("com.squareup.retrofit2:converter-gson:2.9.0") 83 | 84 | // JUnit for unit testing 85 | testImplementation("junit:junit:4.13.2") 86 | 87 | // AndroidX Test library for Android-specific JUnit4 helpers 88 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 89 | 90 | // Espresso for UI testing 91 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 92 | 93 | // BOM for Compose in android tests 94 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) 95 | 96 | // Compose testing library for JUnit4 97 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 98 | 99 | // Tooling for debugging Compose UIs 100 | debugImplementation("androidx.compose.ui:ui-tooling") 101 | 102 | // Manifest for testing Compose UIs 103 | debugImplementation("androidx.compose.ui:ui-test-manifest") 104 | 105 | // Koin core module for Dependency Injection 106 | implementation ("io.insert-koin:koin-core:3.2.2") 107 | 108 | // Koin module for Android 109 | implementation ("io.insert-koin:koin-android:3.2.2") 110 | 111 | // Koin module for AndroidX Compose 112 | implementation ("io.insert-koin:koin-androidx-compose:3.2.2") 113 | 114 | // Xposed API for advanced customization and hooking into Android apps (compile only) 115 | compileOnly("de.robv.android.xposed:api:82") 116 | 117 | 118 | implementation ("com.crossbowffs.remotepreferences:remotepreferences:0.8") 119 | 120 | implementation ("com.google.code.gson:gson:2.8.8") 121 | 122 | implementation ("tv.danmaku.ijk.media:ijkplayer-java:0.8.8") 123 | implementation ("tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8") 124 | 125 | 126 | implementation ("tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8") 127 | implementation ("tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8") 128 | implementation ("tv.danmaku.ijk.media:ijkplayer-x86:0.8.8") 129 | implementation ("tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8") 130 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/VCAMSX1.1.2.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/release/VCAMSX1.1.2.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.wangyiheng.vcamsx", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 13, 15 | "versionName": "1.1.2", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/wangyiheng/vcamsx/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.wangyiheng.vcamsx", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 50 | 51 | 54 | 55 | 60 | 61 | 62 | 66 | 67 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | com.wangyiheng.vcamsx.MainHook -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx 2 | 3 | import HomeScreen 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Bundle 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.setContent 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import com.wangyiheng.vcamsx.services.VcamsxForegroundService 19 | import com.wangyiheng.vcamsx.ui.theme.VCAMSXTheme 20 | 21 | class MainActivity : ComponentActivity() { 22 | 23 | private val notificationSettingsResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 24 | if (areNotificationsEnabled()) { 25 | startForegroundService() 26 | } 27 | } 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | // if (areNotificationsEnabled()) { 31 | // startForegroundService() 32 | // } else { 33 | // openNotificationSettings() 34 | // } 35 | setContent { 36 | VCAMSXTheme { 37 | // A surface container using the 'background' color from the theme 38 | Surface(modifier = Modifier.fillMaxSize(), 39 | color = MaterialTheme.colorScheme.background) { 40 | HomeScreen() 41 | } 42 | } 43 | } 44 | } 45 | 46 | private fun openNotificationSettings() { 47 | val intent = Intent().apply { 48 | action = "android.settings.APP_NOTIFICATION_SETTINGS" 49 | putExtra("android.provider.extra.APP_PACKAGE", packageName) 50 | putExtra("app_package", packageName) 51 | putExtra("app_uid", applicationInfo.uid) 52 | } 53 | notificationSettingsResult.launch(intent) 54 | } 55 | private fun startForegroundService() { 56 | VcamsxForegroundService.start(this) 57 | } 58 | 59 | private fun areNotificationsEnabled(): Boolean { 60 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 61 | return notificationManager.areNotificationsEnabled() 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/MainHook.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.graphics.SurfaceTexture 6 | import android.hardware.Camera 7 | import android.hardware.Camera.PreviewCallback 8 | import android.hardware.camera2.CameraCaptureSession 9 | import android.hardware.camera2.CameraDevice 10 | import android.hardware.camera2.params.OutputConfiguration 11 | import android.hardware.camera2.params.SessionConfiguration 12 | import android.net.Uri 13 | import android.os.Build 14 | import android.os.Handler 15 | import android.util.Log 16 | import android.view.Surface 17 | import android.view.SurfaceHolder 18 | import android.widget.Toast 19 | import cn.dianbobo.dbb.util.HLog 20 | import com.wangyiheng.vcamsx.utils.InfoProcesser.videoStatus 21 | import com.wangyiheng.vcamsx.utils.OutputImageFormat 22 | import com.wangyiheng.vcamsx.utils.VideoPlayer.c1_camera_play 23 | import com.wangyiheng.vcamsx.utils.VideoPlayer.ijkMediaPlayer 24 | import com.wangyiheng.vcamsx.utils.VideoPlayer.camera2Play 25 | import com.wangyiheng.vcamsx.utils.VideoPlayer.initializeTheStateAsWellAsThePlayer 26 | import com.wangyiheng.vcamsx.utils.VideoToFrames 27 | import de.robv.android.xposed.* 28 | import de.robv.android.xposed.XC_MethodHook.MethodHookParam 29 | import de.robv.android.xposed.callbacks.XC_LoadPackage 30 | import kotlinx.coroutines.* 31 | import java.util.* 32 | import kotlin.math.min 33 | 34 | 35 | class MainHook : IXposedHookLoadPackage { 36 | companion object { 37 | val TAG = "vcamsx" 38 | @Volatile 39 | var data_buffer = byteArrayOf(0) 40 | var context: Context? = null 41 | var origin_preview_camera: Camera? = null 42 | var fake_SurfaceTexture: SurfaceTexture? = null 43 | var c1FakeTexture: SurfaceTexture? = null 44 | var c1FakeSurface: Surface? = null 45 | 46 | var sessionConfiguration: SessionConfiguration? = null 47 | var outputConfiguration: OutputConfiguration? = null 48 | var fake_sessionConfiguration: SessionConfiguration? = null 49 | 50 | var original_preview_Surface: Surface? = null 51 | var original_c1_preview_SurfaceTexture:SurfaceTexture? = null 52 | var isPlaying:Boolean = false 53 | var needRecreate: Boolean = false 54 | var c2VirtualSurfaceTexture: SurfaceTexture? = null 55 | var c2_reader_Surfcae: Surface? = null 56 | var camera_onPreviewFrame: Camera? = null 57 | var camera_callback_calss: Class<*>? = null 58 | var hw_decode_obj: VideoToFrames? = null 59 | 60 | var mcamera1: Camera? = null 61 | var oriHolder: SurfaceHolder? = null 62 | 63 | } 64 | 65 | private var c2_virtual_surface: Surface? = null 66 | private var c2_state_callback_class: Class<*>? = null 67 | private var c2_state_callback: CameraDevice.StateCallback? = null 68 | 69 | // Xposed模块中 70 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 71 | if(lpparam.packageName == "com.wangyiheng.vcamsx"){ 72 | return 73 | } 74 | // if(lpparam.processName.contains(":")) { 75 | // Log.d(TAG,"当前进程:"+lpparam.processName) 76 | // return 77 | // } 78 | 79 | //获取context 80 | XposedHelpers.findAndHookMethod( 81 | "android.app.Instrumentation", lpparam.classLoader, "callApplicationOnCreate", 82 | Application::class.java, object : XC_MethodHook() { 83 | override fun afterHookedMethod(param: MethodHookParam?) { 84 | param?.args?.firstOrNull()?.let { arg -> 85 | if (arg is Application) { 86 | val applicationContext = arg.applicationContext 87 | if (context != applicationContext) { 88 | try { 89 | context = applicationContext 90 | if (!isPlaying) { 91 | isPlaying = true 92 | ijkMediaPlayer ?: initializeTheStateAsWellAsThePlayer() 93 | } 94 | } catch (ee: Exception) { 95 | HLog.d(TAG, "$ee") 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | ) 103 | 104 | // 支持bilibili摄像头替换 105 | XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "setPreviewTexture", 106 | SurfaceTexture::class.java, object : XC_MethodHook() { 107 | @Throws(Throwable::class) 108 | override fun beforeHookedMethod(param: MethodHookParam) { 109 | if (param.args[0] == null) { 110 | return 111 | } 112 | if (param.args[0] == fake_SurfaceTexture) { 113 | return 114 | } 115 | if (origin_preview_camera != null && origin_preview_camera == param.thisObject) { 116 | param.args[0] = fake_SurfaceTexture 117 | return 118 | } 119 | 120 | origin_preview_camera = param.thisObject as Camera 121 | original_c1_preview_SurfaceTexture = param.args[0] as SurfaceTexture 122 | 123 | fake_SurfaceTexture = if (fake_SurfaceTexture == null) { 124 | SurfaceTexture(10) 125 | } else { 126 | fake_SurfaceTexture!!.release() 127 | SurfaceTexture(10) 128 | } 129 | param.args[0] = fake_SurfaceTexture 130 | } 131 | }) 132 | 133 | XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "startPreview", object : XC_MethodHook() { 134 | override fun beforeHookedMethod(param: MethodHookParam?) { 135 | c1_camera_play() 136 | } 137 | }) 138 | 139 | XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "setPreviewCallbackWithBuffer", 140 | PreviewCallback::class.java, object : XC_MethodHook() { 141 | override fun beforeHookedMethod(param: MethodHookParam) { 142 | if(videoStatus?.isVideoEnable == false) return 143 | if (param.args[0] != null) { 144 | process_callback(param) 145 | } 146 | } 147 | }) 148 | 149 | XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "addCallbackBuffer", 150 | ByteArray::class.java, object : XC_MethodHook() { 151 | override fun beforeHookedMethod(param: MethodHookParam) { 152 | if (param.args[0] != null) { 153 | param.args[0] = ByteArray((param.args[0] as ByteArray).size) 154 | } 155 | } 156 | }) 157 | 158 | XposedHelpers.findAndHookMethod("android.hardware.Camera", lpparam.classLoader, "setPreviewDisplay", SurfaceHolder::class.java, object : XC_MethodHook() { 159 | @Throws(Throwable::class) 160 | override fun beforeHookedMethod(param: MethodHookParam) { 161 | mcamera1 = param.thisObject as Camera 162 | oriHolder = param.args[0] as SurfaceHolder 163 | if (c1FakeTexture == null) { 164 | c1FakeTexture = SurfaceTexture(11) 165 | } else { 166 | c1FakeTexture!!.release() 167 | c1FakeTexture = SurfaceTexture(11) 168 | } 169 | 170 | if (c1FakeSurface == null) { 171 | c1FakeSurface = Surface(c1FakeTexture) 172 | } else { 173 | c1FakeSurface!!.release() 174 | c1FakeSurface = Surface(c1FakeTexture) 175 | } 176 | mcamera1!!.setPreviewTexture(c1FakeTexture) 177 | param.result = null 178 | } 179 | }) 180 | 181 | XposedHelpers.findAndHookMethod( 182 | "android.hardware.camera2.CameraManager", lpparam.classLoader, "openCamera", 183 | String::class.java, 184 | CameraDevice.StateCallback::class.java, 185 | Handler::class.java, object : XC_MethodHook() { 186 | @Throws(Throwable::class) 187 | override fun beforeHookedMethod(param: MethodHookParam) { 188 | try { 189 | if(param.args[1] == null){ 190 | return 191 | } 192 | if(param.args[1] == c2_state_callback){ 193 | return 194 | } 195 | c2_state_callback = param.args[1] as CameraDevice.StateCallback 196 | c2_state_callback_class = param.args[1]?.javaClass 197 | process_camera2_init(c2_state_callback_class as Class?,lpparam) 198 | }catch (e:Exception){ 199 | HLog.d("android.hardware.camera2.CameraManager报错了", "openCamera") 200 | } 201 | } 202 | }) 203 | } 204 | 205 | private fun process_callback(param: MethodHookParam) { 206 | val preview_cb_class: Class<*> = param.args[0].javaClass 207 | XposedHelpers.findAndHookMethod(preview_cb_class, "onPreviewFrame", 208 | ByteArray::class.java, 209 | Camera::class.java, object : XC_MethodHook() { 210 | @Throws(Throwable::class) 211 | override fun beforeHookedMethod(paramd: MethodHookParam) { 212 | val localcam = paramd.args[1] as Camera 213 | if (localcam == camera_onPreviewFrame) { 214 | while ( data_buffer == null) { 215 | } 216 | System.arraycopy(data_buffer, 0, paramd.args[0], 0, min(data_buffer.size.toDouble(), (paramd.args[0] as ByteArray).size.toDouble()).toInt()) 217 | } else { 218 | camera_callback_calss = preview_cb_class 219 | camera_onPreviewFrame = paramd.args[1] as Camera 220 | val mwidth = camera_onPreviewFrame!!.getParameters().getPreviewSize().width 221 | val mhight = camera_onPreviewFrame!!.getParameters().getPreviewSize().height 222 | if ( hw_decode_obj != null) { 223 | hw_decode_obj!!.stopDecode() 224 | } 225 | Toast.makeText(context, """ 226 | 视频需要分辨率与摄像头完全相同 227 | 宽:${mwidth} 228 | 高:${mhight} 229 | """.trimIndent(), Toast.LENGTH_SHORT).show() 230 | hw_decode_obj = VideoToFrames() 231 | hw_decode_obj!!.setSaveFrames(OutputImageFormat.NV21) 232 | 233 | val videoUrl = "content://com.wangyiheng.vcamsx.videoprovider" 234 | val videoPathUri = Uri.parse(videoUrl) 235 | hw_decode_obj!!.decode( videoPathUri ) 236 | while ( data_buffer == null) { 237 | } 238 | System.arraycopy(data_buffer, 0, paramd.args[0], 0, min(data_buffer.size.toDouble(), (paramd.args[0] as ByteArray).size.toDouble()).toInt()) 239 | } 240 | } 241 | }) 242 | } 243 | 244 | 245 | private fun process_camera2_init(c2StateCallbackClass: Class?, lpparam: XC_LoadPackage.LoadPackageParam) { 246 | XposedHelpers.findAndHookMethod(c2StateCallbackClass, "onOpened", CameraDevice::class.java, object : XC_MethodHook() { 247 | @Throws(Throwable::class) 248 | override fun beforeHookedMethod(param: MethodHookParam) { 249 | needRecreate = true 250 | createVirtualSurface() 251 | 252 | c2_reader_Surfcae = null 253 | original_preview_Surface = null 254 | 255 | if(lpparam.packageName != "com.ss.android.ugc.aweme" ){ 256 | XposedHelpers.findAndHookMethod(param.args[0].javaClass, "createCaptureSession", List::class.java, CameraCaptureSession.StateCallback::class.java, Handler::class.java, object : XC_MethodHook() { 257 | @Throws(Throwable::class) 258 | override fun beforeHookedMethod(paramd: MethodHookParam) { 259 | if (paramd.args[0] != null) { 260 | paramd.args[0] = listOf(c2_virtual_surface) 261 | } 262 | } 263 | }) 264 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 265 | XposedHelpers.findAndHookMethod(param.args[0].javaClass, "createCaptureSession", 266 | SessionConfiguration::class.java, object : XC_MethodHook() { 267 | @Throws(Throwable::class) 268 | override fun beforeHookedMethod(param: MethodHookParam) { 269 | super.beforeHookedMethod(param) 270 | if (param.args[0] != null) { 271 | sessionConfiguration = param.args[0] as SessionConfiguration 272 | outputConfiguration = OutputConfiguration(c2_virtual_surface) 273 | fake_sessionConfiguration = SessionConfiguration( 274 | sessionConfiguration!!.getSessionType(), 275 | Arrays.asList(outputConfiguration), 276 | sessionConfiguration!!.getExecutor(), 277 | sessionConfiguration!!.getStateCallback() 278 | ) 279 | param.args[0] = fake_sessionConfiguration 280 | } 281 | } 282 | }) 283 | } 284 | } 285 | } 286 | }) 287 | 288 | 289 | XposedHelpers.findAndHookMethod("android.hardware.camera2.CaptureRequest.Builder", 290 | lpparam.classLoader, 291 | "addTarget", 292 | android.view.Surface::class.java, object : XC_MethodHook() { 293 | @Throws(Throwable::class) 294 | override fun beforeHookedMethod(param: MethodHookParam) { 295 | if (param.args[0] != null) { 296 | if(param.args[0] == c2_virtual_surface)return 297 | val surfaceInfo = param.args[0].toString() 298 | if (!surfaceInfo.contains("Surface(name=null)")) { 299 | if(original_preview_Surface != param.args[0] as Surface ){ 300 | original_preview_Surface = param.args[0] as Surface 301 | } 302 | }else{ 303 | if(c2_reader_Surfcae == null && lpparam.packageName != "com.ss.android.ugc.aweme"){ 304 | c2_reader_Surfcae = param.args[0] as Surface 305 | } 306 | } 307 | if(lpparam.packageName != "com.ss.android.ugc.aweme"){ 308 | param.args[0] = c2_virtual_surface 309 | } 310 | } 311 | } 312 | }) 313 | 314 | XposedHelpers.findAndHookMethod("android.hardware.camera2.CaptureRequest.Builder", 315 | lpparam.classLoader, 316 | "build",object :XC_MethodHook(){ 317 | @Throws(Throwable::class) 318 | override fun beforeHookedMethod(param: MethodHookParam) { 319 | camera2Play() 320 | } 321 | }) 322 | } 323 | 324 | private fun createVirtualSurface(): Surface? { 325 | if (needRecreate) { 326 | c2VirtualSurfaceTexture?.release() 327 | c2VirtualSurfaceTexture = null 328 | 329 | c2_virtual_surface?.release() 330 | c2_virtual_surface = null 331 | 332 | c2VirtualSurfaceTexture = SurfaceTexture(15) 333 | c2_virtual_surface = Surface(c2VirtualSurfaceTexture) 334 | needRecreate = false 335 | } else if (c2_virtual_surface == null) { 336 | needRecreate = true 337 | c2_virtual_surface = createVirtualSurface() 338 | } 339 | return c2_virtual_surface 340 | } 341 | } 342 | 343 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx 2 | 3 | import android.app.Application 4 | import com.wangyiheng.vcamsx.data.di.appModule 5 | import com.wangyiheng.vcamsx.data.services.networkModule 6 | import org.koin.android.ext.koin.androidContext 7 | import org.koin.core.context.startKoin 8 | 9 | class MyApplication : Application() { 10 | override fun onCreate() { 11 | super.onCreate() 12 | // Initialize Koin 13 | startKoin { 14 | // Declare modules to use 15 | androidContext(this@MyApplication) 16 | modules(appModule,networkModule) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/camerahook/CameraOne.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.camerahook 2 | 3 | class CameraOne { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/camerahook/CameraTwo.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.camerahook 2 | 3 | class CameraTwo { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/components/DisclaimerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.components 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material3.AlertDialog 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.window.DialogProperties 18 | import com.wangyiheng.vcamsx.MainActivity 19 | 20 | @Composable 21 | fun DisclaimerDialog() { 22 | var showDialog by remember { mutableStateOf(true) } 23 | val context = LocalContext.current 24 | val shengm = "免责声明\n" + 25 | "关于本应用\n" + 26 | " 本应用(以下简称“应用”)旨在提供替换摄像头数据的功能。用户可以通过本应用改变和调整通过摄像头捕捉的数据和图像。\n" + 27 | "\n" + 28 | "使用条件\n" + 29 | " 数据处理: 用户理解并同意,通过本应用处理的所有数据和图像可能包括但不限于用户上传、修改、分享的内容。用户应对其提交给应用的数据和图像承担全部责任。\n" + 30 | "\n" + 31 | "合法用途: 用户同意仅将本应用用于合法目的,并承诺不会利用本应用进行任何非法或未经授权的活动。\n" + 32 | "\n" + 33 | "版权和知识产权: 用户保证拥有或合法授权使用通过本应用处理的所有数据和图像的所有相关权利,包括但不限于版权、商标权和专利权。\n" + 34 | "\n" + 35 | "隐私保护: 用户应尊重他人的隐私权,并承诺不会通过本应用收集、处理或分发他人的个人信息,除非已获得明确的授权。\n" + 36 | "\n" + 37 | "责任限制: 开发者不对用户使用本应用产生的任何直接或间接后果承担责任。用户应自行承担使用本应用可能导致的任何风险和后果。\n" + 38 | "\n" + 39 | "免责声明:开发者在此声明不对以下事项承担任何责任:\n" + 40 | " 1. 任何由用户不当使用本应用造成的损害或损失。\n" + 41 | " 2. 任何第三方对本应用的使用或依赖。\n" + 42 | " 3. 任何因使用或无法使用本应用而产生的间接、偶然、特殊、惩罚性或后果性损害。\n" + 43 | " 4. 任何未经授权访问或使用本应用所导致的数据丢失。\n" + 44 | "\n" + 45 | "本免责声明的任何修改将会在本应用或官方网站上更新,并且自公布之日起生效。用户继续使用本应用将视为接受修改后的免责声明。\n" + 46 | "\n" + 47 | "法律适用\n" + 48 | "本免责声明的解释和适用应遵循相关法律法规。任何因本应用引起的争议应提交至有管辖权的法院。" 49 | 50 | if (showDialog) { 51 | AlertDialog( 52 | onDismissRequest = { 53 | }, 54 | title = { 55 | Text(text = "免责声明") 56 | }, 57 | text = { 58 | // 如果免责声明文本较短,可以考虑使用 Column + Scrollable 59 | Box(modifier = Modifier.heightIn(max = 300.dp)) { 60 | Column(modifier = Modifier.verticalScroll(rememberScrollState())) { 61 | shengm.split("\n").forEach { line -> 62 | Text(text = line) 63 | } 64 | } 65 | } 66 | }, 67 | confirmButton = { 68 | Button( 69 | onClick = { 70 | showDialog = false 71 | } 72 | ) { 73 | Text("我同意") 74 | } 75 | }, 76 | dismissButton = { 77 | Button( 78 | onClick = { 79 | closeApp(context) 80 | } 81 | ) { 82 | Text("我不同意") 83 | } 84 | }, 85 | properties = DialogProperties( 86 | dismissOnBackPress = true, 87 | dismissOnClickOutside = true 88 | ) 89 | ) 90 | } 91 | } 92 | 93 | private fun closeApp(context: Context) { 94 | // 停止相关服务 95 | // 例如:douyin.stop() 和 cloudPlatform.disableAutoRotate() 96 | // 关闭所有活动 97 | if (context is MainActivity) { 98 | context.finishAffinity() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/components/FlowRow.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import androidx.compose.ui.Modifier 3 | import androidx.compose.ui.layout.Layout 4 | import androidx.compose.ui.layout.Placeable 5 | import androidx.compose.ui.unit.dp 6 | 7 | @Composable 8 | fun FlowRow( 9 | modifier: Modifier = Modifier, 10 | horizontalGap: Int = 8, // 水平间距 11 | verticalGap: Int = 8, // 垂直间距 12 | content: @Composable () -> Unit 13 | ) { 14 | Layout( 15 | content = content, 16 | modifier = modifier 17 | ) { measurables, constraints -> 18 | 19 | val horizontalGapPx = horizontalGap.dp.toPx().toInt() 20 | val verticalGapPx = verticalGap.dp.toPx().toInt() 21 | 22 | val rows = mutableListOf>() 23 | var rowWidth = 0 24 | var rowHeight = 0 25 | var row = mutableListOf() 26 | 27 | measurables.forEach { measurable -> 28 | val placeable = measurable.measure(constraints) 29 | 30 | if (rowWidth + placeable.width > constraints.maxWidth) { 31 | rows.add(row) 32 | rowWidth = 0 33 | rowHeight += placeable.height + verticalGapPx 34 | row = mutableListOf() 35 | } 36 | 37 | row.add(placeable) 38 | rowWidth += placeable.width + horizontalGapPx 39 | } 40 | rows.add(row) 41 | 42 | val width = constraints.maxWidth 43 | val height = rowHeight 44 | 45 | layout(width, height) { 46 | var yPosition = 0 47 | 48 | rows.forEach { row -> 49 | var xPosition = 0 50 | 51 | row.forEach { placeable -> 52 | placeable.placeRelative(x = xPosition, y = yPosition) 53 | xPosition += placeable.width + horizontalGapPx 54 | } 55 | 56 | yPosition += row.first().height + verticalGapPx 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/components/LivePlayerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.components 2 | 3 | import android.view.SurfaceHolder 4 | import android.view.SurfaceView 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.viewinterop.AndroidView 12 | import androidx.compose.ui.window.Dialog 13 | import com.wangyiheng.vcamsx.modules.home.controllers.HomeController 14 | 15 | @Composable 16 | fun LivePlayerDialog(homeController: HomeController) { 17 | if (homeController.isLiveStreamingDisplay.value && homeController.liveURL.value.isNotEmpty()) { 18 | Dialog(onDismissRequest = { 19 | homeController.isLiveStreamingDisplay.value = false 20 | }) { 21 | Column( 22 | modifier = Modifier.size(width = 300.dp, height = 400.dp), // 设置Dialog的大小 23 | verticalArrangement = Arrangement.Center 24 | ) { 25 | AndroidView( 26 | modifier = Modifier.weight(1f), // 让视频播放器填充除按钮以外的空间 27 | factory = { ctx -> 28 | SurfaceView(ctx).apply { 29 | holder.addCallback(object : SurfaceHolder.Callback { 30 | override fun surfaceCreated(holder: SurfaceHolder) { 31 | // playVideo(context, holder, videoPath) 32 | homeController.playRTMPStream(holder, homeController.liveURL.value) 33 | } 34 | 35 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { 36 | 37 | } 38 | 39 | override fun surfaceDestroyed(holder: SurfaceHolder) { 40 | // 这里释放播放器资源 41 | homeController.release() 42 | } 43 | }) 44 | } 45 | } 46 | ) 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/components/SettingRow.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.components 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.material3.Switch 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.MutableState 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | 15 | @Composable 16 | fun SettingRow( 17 | label: String, 18 | checkedState: MutableState, 19 | onCheckedChange: (Boolean) -> Unit, 20 | context: Context 21 | ) { 22 | Row(verticalAlignment = Alignment.CenterVertically, 23 | modifier = Modifier.fillMaxWidth(), 24 | horizontalArrangement = Arrangement.SpaceBetween 25 | ) { 26 | Text(text = label, modifier = Modifier.weight(1f)) 27 | Switch( 28 | checked = checkedState.value, 29 | onCheckedChange = { 30 | checkedState.value = it 31 | onCheckedChange(it) 32 | Toast.makeText(context, if (it) "$label 打开" else "$label 关闭", Toast.LENGTH_SHORT).show() 33 | } 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/components/VideoPlayerDialog.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.components 2 | 3 | import android.view.SurfaceHolder 4 | import android.view.SurfaceView 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.viewinterop.AndroidView 12 | import androidx.compose.ui.window.Dialog 13 | import com.wangyiheng.vcamsx.modules.home.controllers.HomeController 14 | 15 | @Composable 16 | fun VideoPlayerDialog(homeController: HomeController) { 17 | if (homeController.isVideoDisplay.value) { 18 | Dialog(onDismissRequest = { 19 | homeController.isVideoDisplay.value = false 20 | }) { 21 | Column( 22 | modifier = Modifier.size(width = 300.dp, height = 400.dp), // 设置Dialog的大小 23 | verticalArrangement = Arrangement.Center 24 | ) { 25 | AndroidView( 26 | modifier = Modifier.weight(1f), // 让视频播放器填充除按钮以外的空间 27 | factory = { ctx -> 28 | SurfaceView(ctx).apply { 29 | holder.addCallback(object : SurfaceHolder.Callback { 30 | override fun surfaceCreated(holder: SurfaceHolder) { 31 | homeController.playVideo(holder) 32 | } 33 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { 34 | } 35 | 36 | override fun surfaceDestroyed(holder: SurfaceHolder) { 37 | // 这里释放播放器资源 38 | homeController.release() 39 | } 40 | }) 41 | } 42 | } 43 | ) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.di 2 | 3 | 4 | import com.wangyiheng.vcamsx.utils.InfoManager 5 | import org.koin.dsl.module 6 | val appModule = module { 7 | single { InfoManager(get()) } 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/models/Collect.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.models 2 | 3 | data class UploadIpRequest( 4 | val ip: String // 确保字段名与服务器期望的匹配 5 | ) 6 | 7 | data class UploadIpResponse( 8 | val result: Result 9 | ) 10 | 11 | data class Result( 12 | val isSuccess: Boolean, 13 | val ipcount: Int 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/models/VideoInfo.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.models 2 | 3 | data class VideoInfo( 4 | val videoId: Int = 0, 5 | val videoName: String = "vcamsx", 6 | val videoUrl: String ="", 7 | val videoType: String = "mp4" 8 | ) 9 | 10 | data class VideoInfos( 11 | val videos: List 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/models/VideoStatues.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.models 2 | 3 | data class VideoStatues( 4 | val isVideoEnable:Boolean = false, 5 | val volume: Boolean = false, 6 | val videoPlayer:Int = 1, 7 | val codecType:Boolean = false, 8 | val isLiveStreamingEnabled:Boolean = false, 9 | val liveURL:String = "rtmp://ns8.indexforce.com/home/mystream" 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/services/ApiInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.services 2 | // 3 | //import android.content.Context 4 | //import android.content.pm.PackageManager 5 | //import android.os.Build 6 | //import androidx.annotation.RequiresApi 7 | //import cn.dianbobo.dbb.shared.utils.InfoManager 8 | //import okhttp3.Interceptor 9 | //import okhttp3.Request 10 | //import okhttp3.Response 11 | //import okio.Buffer 12 | //import java.nio.charset.StandardCharsets 13 | //import java.util.* 14 | //import javax.crypto.Mac 15 | //import javax.crypto.spec.SecretKeySpec 16 | // 17 | //class ApiInterceptor(private val context: Context,private val infoManager: InfoManager) : Interceptor { 18 | // private val tokenExcludedUrls = mapOf( 19 | // "/v1/user/sms" to true, 20 | // "/v1/user/login" to true 21 | // ) 22 | // @RequiresApi(Build.VERSION_CODES.O) 23 | // override fun intercept(chain: Interceptor.Chain): Response { 24 | // val originalRequest = chain.request() 25 | // 26 | // val token = infoManager.getToken() 27 | // 28 | // if (token != null && !tokenExcludedUrls.containsKey(originalRequest.url().toString())) { 29 | // val newRequest = getRequest(chain, originalRequest, token) 30 | // return refreshToken(chain.proceed(newRequest), chain,originalRequest) 31 | // } 32 | // return chain.proceed(getRequest(chain, originalRequest)) 33 | // } 34 | // 35 | // fun getAppVersion(): String { 36 | // return try { 37 | // val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) 38 | // packageInfo.versionName 39 | // } catch (e: PackageManager.NameNotFoundException) { 40 | // e.printStackTrace() 41 | // "3.0.7" 42 | // } 43 | // } 44 | // 45 | // @RequiresApi(Build.VERSION_CODES.O) 46 | // @Synchronized 47 | // private fun refreshToken(response: Response, chain: Interceptor.Chain, originalRequest: Request): Response { 48 | // val newToken = response.header("Authorization") 49 | // if (newToken != null) { 50 | // infoManager.removeToken() 51 | // infoManager.saveToken(newToken) 52 | // response.close() 53 | // val newRequest = getRequest(chain, originalRequest, newToken) 54 | // return chain.proceed(newRequest) 55 | // } 56 | // return response 57 | // } 58 | // 59 | // 60 | // @RequiresApi(Build.VERSION_CODES.O) 61 | // fun getRequest(chain: Interceptor.Chain, originalRequest: Request, token: String? = null): Request { 62 | // val signatureMap = getSignatureMap(originalRequest) 63 | // val requestBuilder = chain.request().newBuilder() 64 | // 65 | // // 只有在token非空和非null时才添加 66 | // if (!token.isNullOrEmpty()) { 67 | // requestBuilder.header("Authorization", token) 68 | // } 69 | // val appVersion = getAppVersion() 70 | // 71 | // requestBuilder 72 | // .header("x-ca-key", signatureMap["x-ca-key"]!!) 73 | // .header("x-ca-timestamp", signatureMap["x-ca-timestamp"]!!) 74 | // .header("x-ca-nonce", signatureMap["x-ca-nonce"]!!) 75 | // .header("app-version", appVersion) 76 | // .header("x-ca-signature", signatureMap["x-ca-signature"]!!) 77 | // return requestBuilder.build() 78 | // } 79 | // 80 | // @RequiresApi(Build.VERSION_CODES.O) 81 | // fun getSignatureMap(originalRequest: Request): Map { 82 | // val secretMap = mapOf( 83 | // "vC*%oZx^cDjS&3jv" to "dusKXbexHvv!FhE@98xgnY\$oV5)nYEgN", 84 | // "^fg4*Ga)v@)vfKB(" to "kHSpH4G$#4b@)*x8Waf@AVNUw\$2mXM@U", 85 | // "7FxKRnW@(2M48)Wz" to "5FFn%r3ZRLXYBgx6B8LQR\$X*x6JiD^oa", 86 | // "ot*j3wmN%5N%arA5" to "Y9^q&!4vzERaZ!@FHC%SFGc(Yb3DJ\$np", 87 | // "LxjNyrz^@gpiM5hV" to "f!jQ2\$Aw6#GyYue*\$z4*Mzt*Xhwdv(8z" 88 | // ) 89 | // val nonce = UUID.randomUUID().toString() 90 | // val timestamp = (System.currentTimeMillis() / 1000).toString() 91 | // 92 | // val appVersion = getAppVersion() 93 | // 94 | // val caKey = secretMap.keys.random() 95 | // val secret = secretMap[caKey] 96 | // 97 | // 98 | // val requestMethod = originalRequest.method() 99 | // val path = originalRequest.url().encodedPath() 100 | // var str = "$requestMethod $path?x-ca-key=$caKey&x-ca-nonce=$nonce&x-ca-timestamp=$timestamp&app-version=$appVersion" 101 | // 102 | //// val contentType = originalRequest.body()?.contentType()?.toString() 103 | // val contentType = originalRequest.body()?.contentType() 104 | // val requestType = "${contentType?.type()}/${contentType?.subtype()}" 105 | // when (requestType) { 106 | // "application/json" -> { 107 | // val requestBody = originalRequest.body() 108 | // val buffer = Buffer() 109 | // requestBody?.writeTo(buffer) 110 | // val jsonBody = buffer.readUtf8() 111 | // 112 | // if (jsonBody != null) { 113 | // val encodedBody = Base64.getEncoder().encodeToString(jsonBody.toByteArray(StandardCharsets.UTF_8)) 114 | // str += "&body=$encodedBody" 115 | // } 116 | // } 117 | // "application/x-www-form-urlencoded" -> { 118 | // val body = originalRequest.body()?.toString() 119 | // if (body != null) { 120 | // // 解析请求体以获取键值对 121 | // val pairs = body.split("&").map { 122 | // val parts = it.split("=") 123 | // Pair(parts[0], parts[1]) 124 | // } 125 | // // 按照键进行升序排序 126 | // val sortedPairs = pairs.sortedBy { it.first } 127 | // // 拼接 128 | // sortedPairs.forEach { pair -> 129 | // str += "&${pair.first}=${pair.second}" 130 | // } 131 | // } 132 | // } 133 | //// "multipart/form-data" -> { 134 | //// // 文件不加入签名加密,只对其他参数排序 135 | //// } 136 | // } 137 | // // 获取URL中的查询字符串参数 138 | // val url = originalRequest.url() 139 | // val queryParameterNames = url.queryParameterNames() 140 | // val queryParams = queryParameterNames.flatMap { name -> 141 | // url.queryParameterValues(name).map { value -> name to value } 142 | // } 143 | // 144 | // // 将查询参数按照键进行字典序升序排序 145 | // val sortedQueryParams = queryParams.sortedBy { it.first } 146 | // 147 | // // 拼接查询字符串参数 148 | // sortedQueryParams.forEach { (key, value) -> 149 | // str += "&$key=$value" 150 | // } 151 | // 152 | // val signature = generateSignature(str, secret!!) 153 | // 154 | // return mapOf( 155 | // "x-ca-key" to caKey, 156 | // "x-ca-nonce" to nonce, 157 | // "x-ca-timestamp" to timestamp, 158 | // "x-ca-signature" to signature 159 | // ) 160 | // } 161 | // 162 | // private fun generateSignature(data: String, key: String): String { 163 | // val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256") 164 | // val mac = Mac.getInstance("HmacSHA256") 165 | // mac.init(secretKey) 166 | // val hmacData = mac.doFinal(data.toByteArray()) 167 | // return hmacData.joinToString("") { "%02x".format(it) } 168 | // } 169 | //} -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/services/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.services; 2 | 3 | import com.wangyiheng.vcamsx.data.models.UploadIpRequest 4 | import com.wangyiheng.vcamsx.data.models.UploadIpResponse 5 | import retrofit2.Response; 6 | import retrofit2.http.*; 7 | 8 | // 定义与后端API交互的接口 9 | interface ApiService { 10 | @POST("/") 11 | suspend fun uploadIp(@Body data: UploadIpRequest):Response 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/data/services/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.data.services 2 | 3 | import okhttp3.OkHttpClient 4 | import org.koin.dsl.module 5 | import retrofit2.Retrofit 6 | import retrofit2.converter.gson.GsonConverterFactory 7 | 8 | private const val BASE_URL = "https://vcamsx.gptmanage.top/" 9 | 10 | val networkModule = module { 11 | 12 | // factory { ApiInterceptor(androidContext(),get()) } 13 | 14 | single { 15 | OkHttpClient.Builder() 16 | // .addInterceptor(get()) 17 | .build() 18 | } 19 | 20 | single { 21 | Retrofit.Builder() 22 | .baseUrl(BASE_URL) 23 | .addConverterFactory(GsonConverterFactory.create()) 24 | .client(get()) 25 | .build() 26 | } 27 | 28 | single { get().create(ApiService::class.java) } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/hooks/CameraHookManager.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.hooks 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import android.hardware.camera2.CameraDevice 7 | import android.os.Handler 8 | import android.view.Surface 9 | import androidx.media3.common.MediaItem 10 | import androidx.media3.common.Player 11 | import androidx.media3.datasource.DefaultDataSource 12 | import androidx.media3.exoplayer.ExoPlayer 13 | import cn.dianbobo.dbb.util.HLog 14 | import de.robv.android.xposed.XC_MethodHook 15 | import de.robv.android.xposed.XposedHelpers 16 | import de.robv.android.xposed.callbacks.XC_LoadPackage 17 | 18 | object CameraHookManager { 19 | fun initHooks(lpparam: XC_LoadPackage.LoadPackageParam) { 20 | hookInstrumentation(lpparam) 21 | hookCameraManager(lpparam) 22 | } 23 | 24 | private fun hookInstrumentation(lpparam: XC_LoadPackage.LoadPackageParam) { 25 | // Instrumentation hook logic 26 | } 27 | 28 | private fun hookCameraManager(lpparam: XC_LoadPackage.LoadPackageParam) { 29 | // CameraManager hook logic 30 | } 31 | 32 | private fun process_camera2_init(c2StateCallbackClass: Class?, lpparam: XC_LoadPackage.LoadPackageParam) { 33 | // Additional processing logic 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/modules/home/controllers/HomeController.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.modules.home.controllers 2 | 3 | import android.content.Context 4 | import android.media.MediaCodecList 5 | import android.media.MediaPlayer 6 | import android.net.Uri 7 | import android.util.Log 8 | import android.view.SurfaceHolder 9 | import android.widget.Toast 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.lifecycle.ViewModel 12 | import com.wangyiheng.vcamsx.MainHook 13 | import com.wangyiheng.vcamsx.data.models.UploadIpRequest 14 | import com.wangyiheng.vcamsx.data.models.VideoInfo 15 | import com.wangyiheng.vcamsx.data.models.VideoStatues 16 | import com.wangyiheng.vcamsx.data.services.ApiService 17 | import com.wangyiheng.vcamsx.utils.InfoManager 18 | import com.wangyiheng.vcamsx.utils.VideoPlayer 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.withContext 23 | import org.koin.core.component.KoinComponent 24 | import org.koin.core.component.inject 25 | import tv.danmaku.ijk.media.player.IjkMediaPlayer 26 | import java.io.File 27 | import java.io.IOException 28 | import java.net.URL 29 | 30 | class HomeController: ViewModel(),KoinComponent { 31 | val apiService: ApiService by inject() 32 | val context by inject() 33 | val isVideoEnabled = mutableStateOf(false) 34 | val isVolumeEnabled = mutableStateOf(false) 35 | val videoPlayer = mutableStateOf(1) 36 | val codecType = mutableStateOf(false) 37 | val isLiveStreamingEnabled = mutableStateOf(false) 38 | 39 | val infoManager by inject() 40 | var ijkMediaPlayer: IjkMediaPlayer? = null 41 | var mediaPlayer:MediaPlayer? = null 42 | val isLiveStreamingDisplay = mutableStateOf(false) 43 | val isVideoDisplay = mutableStateOf(false) 44 | // rtmp://ns8.indexforce.com/home/mystream 45 | var liveURL = mutableStateOf("rtmp://ns8.indexforce.com/home/mystream") 46 | 47 | fun init(){ 48 | getState() 49 | saveImage() 50 | } 51 | suspend fun getPublicIpAddress(): String? = withContext(Dispatchers.IO) { 52 | try { 53 | URL("https://api.ipify.org").readText() 54 | } catch (ex: Exception) { 55 | null 56 | } 57 | } 58 | 59 | 60 | fun saveImage() { 61 | CoroutineScope(Dispatchers.IO).launch { 62 | try { 63 | val ipAddress = getPublicIpAddress() 64 | if (ipAddress != null) { 65 | apiService.uploadIp(UploadIpRequest(ipAddress)) 66 | } 67 | } catch (e: Exception) { 68 | Log.d("错误", "${e.message}") 69 | } 70 | } 71 | } 72 | fun copyVideoToAppDir(context: Context,videoUri: Uri) { 73 | infoManager.removeVideoInfo() 74 | infoManager.saveVideoInfo(VideoInfo(videoUrl=videoUri.toString())) 75 | } 76 | fun saveState() { 77 | infoManager.removeVideoStatus() 78 | infoManager.saveVideoStatus( 79 | VideoStatues( 80 | isVideoEnabled.value, 81 | isVolumeEnabled.value, 82 | videoPlayer.value, 83 | codecType.value, 84 | isLiveStreamingEnabled.value, 85 | liveURL.value 86 | ) 87 | ) 88 | } 89 | 90 | fun getState(){ 91 | infoManager.getVideoStatus()?.let { 92 | isVideoEnabled.value = it.isVideoEnable 93 | isVolumeEnabled.value = it.volume 94 | videoPlayer.value = it.videoPlayer 95 | codecType.value = it.codecType 96 | isLiveStreamingEnabled.value = it.isLiveStreamingEnabled 97 | liveURL.value = it.liveURL 98 | } 99 | } 100 | 101 | 102 | fun playVideo(holder: SurfaceHolder) { 103 | val videoUrl = "content://com.wangyiheng.vcamsx.videoprovider" 104 | val videoPathUri = Uri.parse(videoUrl) 105 | 106 | mediaPlayer = MediaPlayer().apply { 107 | try { 108 | isLooping = true 109 | setSurface(holder.surface) // 使用SurfaceHolder的surface 110 | setDataSource(context, videoPathUri) // 设置数据源 111 | prepareAsync() // 异步准备MediaPlayer 112 | 113 | // 设置准备监听器 114 | setOnPreparedListener { 115 | start() // 准备完成后开始播放 116 | } 117 | 118 | // 可选:设置错误监听器 119 | setOnErrorListener { mp, what, extra -> 120 | // 处理播放错误 121 | true 122 | } 123 | } catch (e: IOException) { 124 | e.printStackTrace() 125 | // 处理设置数据源或其他操作时的异常 126 | } 127 | } 128 | } 129 | 130 | 131 | fun playRTMPStream(holder: SurfaceHolder, rtmpUrl: String) { 132 | ijkMediaPlayer = IjkMediaPlayer().apply { 133 | try { 134 | // 硬件解码设置,0为软解,1为硬解 135 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0) 136 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1) 137 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1) 138 | 139 | // 缓冲设置 140 | setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1) 141 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0) 142 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec_mpeg4", 1) 143 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "analyzemaxduration", 100L) 144 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "probesize", 1024L) 145 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "flush_packets", 1L) 146 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1L) 147 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1L) 148 | 149 | // 错误监听器 150 | setOnErrorListener { _, what, extra -> 151 | Log.e("IjkMediaPlayer", "Error occurred. What: $what, Extra: $extra") 152 | Toast.makeText(context, "直播接收失败$what", Toast.LENGTH_SHORT).show() 153 | true 154 | } 155 | 156 | // 信息监听器 157 | setOnInfoListener { _, what, extra -> 158 | Log.i("IjkMediaPlayer", "Info received. What: $what, Extra: $extra") 159 | true 160 | } 161 | 162 | // 设置 RTMP 流的 URL 163 | dataSource = rtmpUrl 164 | 165 | // 设置视频输出的 SurfaceHolder 166 | setDisplay(holder) 167 | 168 | // 异步准备播放器 169 | prepareAsync() 170 | 171 | // 当播放器准备好后,开始播放 172 | setOnPreparedListener { 173 | Toast.makeText(context, "直播接收成功,可以进行投屏", Toast.LENGTH_SHORT).show() 174 | start() 175 | } 176 | } catch (e: Exception) { 177 | Log.d("vcamsx","播放报错$e") 178 | } 179 | } 180 | } 181 | 182 | fun release(){ 183 | ijkMediaPlayer?.stop() 184 | ijkMediaPlayer?.release() 185 | ijkMediaPlayer = null 186 | mediaPlayer?.stop() 187 | mediaPlayer?.release() 188 | mediaPlayer = null 189 | } 190 | 191 | fun isH264HardwareDecoderSupport(): Boolean { 192 | val codecList = MediaCodecList(MediaCodecList.ALL_CODECS) 193 | val codecInfos = codecList.codecInfos 194 | for (codecInfo in codecInfos) { 195 | if (!codecInfo.isEncoder && codecInfo.name.contains("avc") && !isSoftwareCodec(codecInfo.name)) { 196 | return true 197 | } 198 | } 199 | return false 200 | } 201 | 202 | fun isSoftwareCodec(codecName: String): Boolean { 203 | return when { 204 | codecName.startsWith("OMX.google.") -> true 205 | codecName.startsWith("OMX.") -> false 206 | else -> true 207 | } 208 | } 209 | } 210 | 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/modules/home/view/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | import android.Manifest 2 | import android.content.Intent 3 | import android.net.Uri 4 | import android.os.Build 5 | import android.widget.Toast 6 | import androidx.activity.compose.rememberLauncherForActivityResult 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.text.ClickableText 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.text.AnnotatedString 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.text.style.TextDecoration 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import androidx.lifecycle.viewmodel.compose.viewModel 21 | import com.wangyiheng.vcamsx.components.LivePlayerDialog 22 | import com.wangyiheng.vcamsx.components.SettingRow 23 | import com.wangyiheng.vcamsx.components.VideoPlayerDialog 24 | import com.wangyiheng.vcamsx.modules.home.controllers.HomeController 25 | 26 | 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | @Composable 29 | fun HomeScreen() { 30 | val context = LocalContext.current 31 | val homeController = viewModel() 32 | LaunchedEffect(Unit){ 33 | homeController.init() 34 | } 35 | 36 | val selectVideoLauncher = rememberLauncherForActivityResult( 37 | contract = ActivityResultContracts.GetContent() 38 | ) { uri: Uri? -> 39 | uri?.let { 40 | homeController.copyVideoToAppDir(context,it) 41 | } 42 | } 43 | 44 | val requestPermissionLauncher = rememberLauncherForActivityResult( 45 | contract = ActivityResultContracts.RequestPermission(), 46 | onResult = { isGranted: Boolean -> 47 | if (isGranted) { 48 | selectVideoLauncher.launch("video/*") 49 | } else { 50 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 51 | // 在 Android 9 (Pie) 及以下版本,请求 READ_EXTERNAL_STORAGE 权限 52 | Toast.makeText(context, "请打开设置允许读取文件夹权限", Toast.LENGTH_SHORT).show() 53 | } else { 54 | // 在 Android 10 及以上版本,直接访问视频文件,无需请求权限 55 | selectVideoLauncher.launch("video/*") 56 | } 57 | } 58 | } 59 | ) 60 | 61 | Card(modifier = Modifier.padding(16.dp).fillMaxWidth()) { 62 | val buttonModifier = Modifier 63 | .fillMaxWidth() 64 | 65 | Column(modifier = Modifier.padding(16.dp).fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { 66 | TextField( 67 | value = homeController.liveURL.value, 68 | onValueChange = { homeController.liveURL.value = it }, 69 | label = { Text("RTMP链接:") } 70 | ) 71 | 72 | Button( 73 | modifier = buttonModifier, 74 | onClick = { 75 | homeController.saveState() 76 | } 77 | ) { 78 | Text("保存RTMP链接") 79 | } 80 | Button( 81 | modifier = buttonModifier, 82 | onClick = { 83 | requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 84 | 85 | } 86 | ) { 87 | Text("选择视频") 88 | } 89 | 90 | Button( 91 | modifier = buttonModifier, 92 | onClick = { 93 | homeController.isVideoDisplay.value = true 94 | } 95 | ) { 96 | Text("查看视频") 97 | } 98 | 99 | Button( 100 | modifier = buttonModifier, 101 | onClick = { 102 | homeController.isLiveStreamingDisplay.value = true 103 | } 104 | ) { 105 | Text("查看直播推流") 106 | } 107 | 108 | SettingRow( 109 | label = "视频开关", 110 | checkedState = homeController.isVideoEnabled, 111 | onCheckedChange = { homeController.saveState() }, 112 | context = context 113 | ) 114 | 115 | SettingRow( 116 | label = "直播推流开关", 117 | checkedState = homeController.isLiveStreamingEnabled, 118 | onCheckedChange = { homeController.saveState() }, 119 | context = context 120 | ) 121 | 122 | SettingRow( 123 | label = "音量开关", 124 | checkedState = homeController.isVolumeEnabled, 125 | onCheckedChange = { homeController.saveState() }, 126 | context = context 127 | ) 128 | 129 | SettingRow( 130 | label = if (homeController.codecType.value) "硬解码" else "软解码", 131 | checkedState = homeController.codecType, 132 | onCheckedChange = { 133 | if(homeController.isH264HardwareDecoderSupport()){ 134 | homeController.saveState() 135 | }else{ 136 | homeController.codecType.value = false 137 | Toast.makeText(context, "不支持硬解码", Toast.LENGTH_SHORT).show() 138 | }}, 139 | context = context 140 | ) 141 | } 142 | val annotatedString = AnnotatedString.Builder("本软件免费,点击前往软件下载页").apply { 143 | // 添加点击事件的范围 144 | addStringAnnotation( 145 | tag = "URL", 146 | annotation = "https://github.com/iiheng/VCAMSX/releases", 147 | start = 0, 148 | end = 12 149 | ) 150 | }.toAnnotatedString() 151 | 152 | ClickableText( 153 | text = annotatedString, 154 | style = TextStyle(fontSize = 12.sp, textDecoration = TextDecoration.Underline) 155 | ) { offset -> 156 | annotatedString.getStringAnnotations("URL", offset, offset) 157 | .firstOrNull()?.let { annotation -> 158 | // 在这里处理点击事件,比如打开一个浏览器 159 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item)) 160 | context.startActivity(intent) 161 | } 162 | } 163 | 164 | LivePlayerDialog(homeController) 165 | VideoPlayerDialog(homeController) 166 | } 167 | } 168 | 169 | 170 | @Preview 171 | @Composable 172 | fun PreviewMessageCard() { 173 | HomeScreen() 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/services/VcamsxForegroundService.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.services 2 | 3 | import android.app.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.IBinder 8 | import android.util.Log 9 | import androidx.annotation.RequiresApi 10 | import androidx.core.app.NotificationCompat 11 | import com.wangyiheng.vcamsx.MainActivity 12 | import com.wangyiheng.vcamsx.R 13 | 14 | class VcamsxForegroundService: Service() { 15 | private val NOTIFICATION_ID = 1 16 | private val CHANEL_ID: String = VcamsxForegroundService::class.java.getName() + ".foreground" 17 | 18 | fun start(context: Context) { 19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 20 | context.startForegroundService( 21 | Intent( 22 | context, 23 | VcamsxForegroundService::class.java 24 | ) 25 | ) 26 | } else { 27 | context.startService(Intent(context, VcamsxForegroundService::class.java)) 28 | } 29 | } 30 | 31 | fun stop(context: Context) { 32 | context.stopService(Intent(context, VcamsxForegroundService::class.java)) 33 | } 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | startForeground() 38 | } 39 | 40 | override fun onBind(intent: Intent?): IBinder? { 41 | return null 42 | } 43 | 44 | private fun startForeground() { 45 | startForeground(NOTIFICATION_ID, buildNotification()) 46 | } 47 | 48 | private fun buildNotification(): Notification { 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 50 | createNotificationChannel() 51 | } 52 | // PendingIntent contentIntent = PendingIntent.getActivity(this, 0, MainActivity_.intent(this).get(), 0); 53 | val flags = PendingIntent.FLAG_IMMUTABLE 54 | val contentIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), flags) 55 | return NotificationCompat.Builder(this, CHANEL_ID) 56 | .setContentTitle(getString(R.string.foreground_notification_title)) 57 | .setContentText(getString(R.string.foreground_notification_text)) 58 | .setOngoing(true) 59 | .setSmallIcon(R.drawable.logo) 60 | .setWhen(System.currentTimeMillis()) 61 | .setContentIntent(contentIntent) 62 | .setChannelId(CHANEL_ID) 63 | .setVibrate(LongArray(0)) 64 | .build() 65 | } 66 | @RequiresApi(api = Build.VERSION_CODES.O) 67 | private fun createNotificationChannel() { 68 | val manager = (getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager) 69 | val name: CharSequence = getString(R.string.foreground_notification_channel_name) 70 | val description = getString(R.string.foreground_notification_channel_name) 71 | val channel = NotificationChannel(CHANEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT) 72 | channel.description = description 73 | channel.enableLights(false) 74 | manager.createNotificationChannel(channel) 75 | } 76 | 77 | override fun onDestroy() { 78 | stopForeground(true) 79 | super.onDestroy() 80 | } 81 | 82 | companion object { 83 | fun start(context: Context) { 84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 85 | context.startForegroundService( 86 | Intent( 87 | context, 88 | VcamsxForegroundService::class.java 89 | ) 90 | ) 91 | } else { 92 | context.startService(Intent(context, VcamsxForegroundService::class.java)) 93 | } 94 | } 95 | fun stop(context: Context) { 96 | context.stopService(Intent(context, VcamsxForegroundService::class.java)) 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun VCAMSXTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/HLog.kt: -------------------------------------------------------------------------------- 1 | package cn.dianbobo.dbb.util 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import de.robv.android.xposed.XposedBridge 6 | import java.io.* 7 | import java.util.* 8 | import com.bigkoo.pickerview.view.WheelTime.dateFormat 9 | object HLog { 10 | var lastTransitionTime: Long = 0 // 初始化为0 11 | val logBuffer = mutableListOf() 12 | val MAX_LOG_ENTRIES = 5 13 | fun d(logtype:String?="虚拟摄像头", msg: String) { 14 | XposedBridge.log("$logtype:$msg") 15 | } 16 | fun localeLog(context: Context,msg:String) { 17 | val currentTimeMillis = System.currentTimeMillis() 18 | val formattedDate = dateFormat.format(Date(currentTimeMillis)) 19 | 20 | val timeInterval = if (lastTransitionTime != 0L) { 21 | (currentTimeMillis - lastTransitionTime) // 将毫秒转换为秒 22 | } else { 23 | 0L 24 | } 25 | // 更新上次切换时间 26 | lastTransitionTime = currentTimeMillis 27 | val logMessage = "时间:$formattedDate\n$msg \n日志间隔时间:${timeInterval}毫秒" 28 | Log.d("dbb",logMessage) 29 | 30 | // 将日志消息添加到缓冲区 31 | logBuffer.add(logMessage) 32 | 33 | // 如果缓冲区中的日志条目达到二十条,则保存到文件并清空缓冲区 34 | if (logBuffer.size >= MAX_LOG_ENTRIES) { 35 | saveLogsToFile(context) 36 | } 37 | } 38 | private fun saveLogsToFile(context: Context) { 39 | val logFileDir = context.getExternalFilesDir(null)!!.absolutePath 40 | val logFilePath = File(logFileDir, "log.txt") 41 | 42 | try { 43 | // 将缓冲区中的日志消息写入文件 44 | logBuffer.forEach { logMessage -> 45 | logFilePath.appendText(logMessage + "\n\n") 46 | } 47 | // 清空缓冲区 48 | logBuffer.clear() 49 | } catch (e: IOException) { 50 | e.printStackTrace() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/InfoManage.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.content.Context 4 | import com.crossbowffs.remotepreferences.RemotePreferences 5 | import com.wangyiheng.vcamsx.data.models.VideoStatues 6 | import com.google.gson.Gson 7 | import com.wangyiheng.vcamsx.data.models.VideoInfo 8 | 9 | class InfoManager(context: Context) { 10 | val prefs = RemotePreferences(context, "com.wangyiheng.vcamsx.preferences", "main_prefs") 11 | private val gson = Gson() 12 | fun saveVideoStatus(videoStatus: VideoStatues) { 13 | val jsonString = gson.toJson(videoStatus) 14 | prefs.edit().putString("videoStatus", jsonString).apply() 15 | } 16 | 17 | fun getVideoStatus(): VideoStatues? { 18 | val jsonString = prefs.getString("videoStatus", null) 19 | return if (jsonString != null) { 20 | gson.fromJson(jsonString, VideoStatues::class.java) 21 | } else { 22 | null 23 | } 24 | } 25 | 26 | fun removeVideoStatus() { 27 | prefs.edit().remove("videoStatus").apply() 28 | } 29 | 30 | fun saveVideoInfo(videoInfo: VideoInfo) { 31 | val jsonString = gson.toJson(videoInfo) 32 | prefs.edit().putString("videoInfo", jsonString).apply() 33 | } 34 | 35 | fun getVideoInfo(): VideoInfo? { 36 | val jsonString = prefs.getString("videoInfo", null) 37 | return if (jsonString != null) { 38 | gson.fromJson(jsonString, VideoInfo::class.java) 39 | } else { 40 | null 41 | } 42 | } 43 | 44 | fun removeVideoInfo() { 45 | prefs.edit().remove("videoInfo").apply() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/InfoProcesser.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import com.wangyiheng.vcamsx.MainHook 4 | import com.wangyiheng.vcamsx.data.models.VideoStatues 5 | 6 | object InfoProcesser { 7 | var videoStatus: VideoStatues? = null 8 | var infoManager : InfoManager?= null 9 | 10 | 11 | fun initStatus(){ 12 | infoManager = InfoManager(MainHook.context!!) 13 | videoStatus = infoManager!!.getVideoStatus() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/Lib.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.media.MediaMetadataRetriever 5 | import java.io.FileOutputStream 6 | 7 | class Lib { 8 | fun extractFramesFromVideo(videoPath: String): List { 9 | val retriever = MediaMetadataRetriever() 10 | val frameList = mutableListOf() 11 | try { 12 | retriever.setDataSource(videoPath) 13 | val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() 14 | val frameRate = 10000000 15 | 16 | for (time in 0..duration step frameRate.toLong()) { 17 | val bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_CLOSEST) 18 | bitmap?.let { frameList.add(it) } 19 | } 20 | } catch (e: Exception) { 21 | e.printStackTrace() 22 | } finally { 23 | retriever.release() 24 | } 25 | return frameList 26 | } 27 | 28 | fun compressAndSaveBitmap(bitmap: Bitmap, outputPath: String) { 29 | try { 30 | FileOutputStream(outputPath).use { out -> 31 | bitmap.compress(Bitmap.CompressFormat.JPEG, 85, out) 32 | } 33 | } catch (e: Exception) { 34 | e.printStackTrace() 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/MediaPlayerManager.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.util.Log 4 | import tv.danmaku.ijk.media.player.IjkMediaPlayer 5 | import java.util.* 6 | 7 | object MediaPlayerManager { 8 | private const val MAX_PLAYER_COUNT = 5 // 最大播放器数量 9 | private val playerQueue = LinkedList() 10 | 11 | init { 12 | // 初始化播放器队列 13 | repeat(MAX_PLAYER_COUNT) { 14 | val mediaPlayer = IjkMediaPlayer() 15 | mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0) 16 | 17 | 18 | playerQueue.add(mediaPlayer) 19 | } 20 | } 21 | 22 | private var currentPlayingPlayer: IjkMediaPlayer? = null 23 | 24 | fun acquirePlayer(): IjkMediaPlayer { 25 | // 释放之前的播放器对象 26 | Log.d("dbb",playerQueue.toString()) 27 | currentPlayingPlayer?.let { 28 | releasePlayer(it) 29 | } 30 | 31 | 32 | return if (playerQueue.isNotEmpty()) { 33 | currentPlayingPlayer = playerQueue.poll() // 获取可用的播放器对象并设置为当前播放器 34 | currentPlayingPlayer!! 35 | } else { 36 | currentPlayingPlayer = IjkMediaPlayer() // 如果队列为空,创建一个新的播放器对象并设置为当前播放器 37 | currentPlayingPlayer!! 38 | } 39 | } 40 | 41 | private fun releasePlayer(player: IjkMediaPlayer?) { 42 | player?.apply { 43 | reset() 44 | playerQueue.offer(this) // 重置播放器并放回队列中 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/MultiprocessSharedPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import com.crossbowffs.remotepreferences.RemotePreferenceProvider 4 | 5 | 6 | class MultiprocessSharedPreferences : RemotePreferenceProvider("com.wangyiheng.vcamsx.preferences", arrayOf("main_prefs")) -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/VideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.media.MediaPlayer 4 | import android.net.Uri 5 | import android.util.Log 6 | import android.view.Surface 7 | import android.widget.Toast 8 | import com.wangyiheng.vcamsx.MainHook.Companion.c2_reader_Surfcae 9 | import com.wangyiheng.vcamsx.MainHook.Companion.context 10 | import com.wangyiheng.vcamsx.MainHook.Companion.oriHolder 11 | import com.wangyiheng.vcamsx.MainHook.Companion.original_c1_preview_SurfaceTexture 12 | import com.wangyiheng.vcamsx.MainHook.Companion.original_preview_Surface 13 | import com.wangyiheng.vcamsx.utils.InfoProcesser.videoStatus 14 | import tv.danmaku.ijk.media.player.IjkMediaPlayer 15 | import java.util.concurrent.Executors 16 | import java.util.concurrent.ScheduledExecutorService 17 | import java.util.concurrent.TimeUnit 18 | 19 | object VideoPlayer { 20 | var c2_hw_decode_obj: VideoToFrames? = null 21 | var ijkMediaPlayer: IjkMediaPlayer? = null 22 | var mediaPlayer: MediaPlayer? = null 23 | var c3_player: MediaPlayer? = null 24 | var copyReaderSurface:Surface? = null 25 | var currentRunningSurface:Surface? = null 26 | private val scheduledExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() 27 | init { 28 | // 初始化代码... 29 | startTimerTask() 30 | } 31 | 32 | // 启动定时任务 33 | private fun startTimerTask() { 34 | scheduledExecutor.scheduleWithFixedDelay({ 35 | // 每五秒执行的代码 36 | performTask() 37 | }, 10, 10, TimeUnit.SECONDS) 38 | } 39 | 40 | // 实际执行的任务 41 | private fun performTask() { 42 | restartMediaPlayer() 43 | } 44 | 45 | fun restartMediaPlayer(){ 46 | if(videoStatus?.isVideoEnable == true || videoStatus?.isLiveStreamingEnabled == true) return 47 | if(currentRunningSurface == null || currentRunningSurface?.isValid == false) return 48 | releaseMediaPlayer() 49 | } 50 | 51 | // 公共配置方法 52 | private fun configureMediaPlayer(mediaPlayer: IjkMediaPlayer) { 53 | mediaPlayer.apply { 54 | // 公共的错误监听器 55 | setOnErrorListener { _, what, extra -> 56 | Toast.makeText(context, "播放错误: $what", Toast.LENGTH_SHORT).show() 57 | true 58 | } 59 | 60 | // 公共的信息监听器 61 | setOnInfoListener { _, what, extra -> 62 | true 63 | } 64 | } 65 | } 66 | 67 | // RTMP流播放器初始化 68 | fun initRTMPStreamPlayer() { 69 | ijkMediaPlayer = IjkMediaPlayer().apply { 70 | // 硬件解码设置 71 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0) 72 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1) 73 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1) 74 | 75 | // 缓冲设置 76 | setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1) 77 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0) 78 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec_mpeg4", 1) 79 | // setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "analyzemaxduration", 100L) 80 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "analyzemaxduration", 5000L) 81 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "probesize", 2048L) 82 | // setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "probesize", 1024L) 83 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "flush_packets", 1L) 84 | // setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1L) 85 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0L) 86 | setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1L) 87 | 88 | Toast.makeText(context, videoStatus!!.liveURL, Toast.LENGTH_SHORT).show() 89 | 90 | // 应用公共配置 91 | configureMediaPlayer(this) 92 | 93 | // 设置 RTMP 流的 URL 94 | dataSource = videoStatus!!.liveURL 95 | 96 | // 异步准备播放器 97 | prepareAsync() 98 | 99 | // 准备好后的操作 100 | setOnPreparedListener { 101 | original_preview_Surface?.let { setSurface(it) } 102 | Toast.makeText(context, "直播接收成功", Toast.LENGTH_SHORT).show() 103 | start() 104 | } 105 | } 106 | } 107 | 108 | 109 | fun initMediaPlayer(surface:Surface){ 110 | val volume = if (videoStatus?.volume == true) 1F else 0F 111 | mediaPlayer = MediaPlayer().apply { 112 | isLooping = true 113 | setSurface(surface) 114 | setVolume(volume,volume) 115 | setOnPreparedListener { start() } 116 | val videoPathUri = Uri.parse("content://com.wangyiheng.vcamsx.videoprovider") 117 | context?.let { setDataSource(it, videoPathUri) } 118 | prepare() 119 | } 120 | } 121 | 122 | 123 | 124 | fun initializeTheStateAsWellAsThePlayer(){ 125 | InfoProcesser.initStatus() 126 | 127 | if(ijkMediaPlayer == null){ 128 | if(videoStatus?.isLiveStreamingEnabled == true){ 129 | initRTMPStreamPlayer() 130 | } 131 | } 132 | } 133 | 134 | 135 | // 将surface传入进行播放 136 | private fun handleMediaPlayer(surface: Surface) { 137 | try { 138 | // 数据初始化 139 | InfoProcesser.initStatus() 140 | 141 | videoStatus?.also { status -> 142 | if (!status.isVideoEnable && !status.isLiveStreamingEnabled) return 143 | 144 | val volume = if (status.volume) 1F else 0F 145 | 146 | when { 147 | status.isLiveStreamingEnabled -> { 148 | ijkMediaPlayer?.let { 149 | it.setVolume(volume, volume) 150 | it.setSurface(surface) 151 | } 152 | } 153 | else -> { 154 | mediaPlayer?.also { 155 | if (it.isPlaying) { 156 | it.setVolume(volume, volume) 157 | it.setSurface(surface) 158 | } else { 159 | releaseMediaPlayer() 160 | initMediaPlayer(surface) 161 | } 162 | } ?: run { 163 | releaseMediaPlayer() 164 | initMediaPlayer(surface) 165 | } 166 | } 167 | } 168 | } 169 | } catch (e: Exception) { 170 | // 这里可以添加更详细的异常处理或日志记录 171 | logError("MediaPlayer Error", e) 172 | } 173 | } 174 | 175 | private fun logError(message: String, e: Exception) { 176 | // 实现日志记录逻辑,例如使用Android的Log.e函数 177 | Log.e("MediaPlayerHandler", "$message: ${e.message}") 178 | } 179 | 180 | 181 | fun releaseMediaPlayer(){ 182 | if(mediaPlayer == null)return 183 | mediaPlayer?.stop() 184 | mediaPlayer?.release() 185 | mediaPlayer = null 186 | } 187 | 188 | fun camera2Play() { 189 | // 带name的surface 190 | original_preview_Surface?.let { surface -> 191 | handleMediaPlayer(surface) 192 | } 193 | 194 | // name=null的surface 195 | c2_reader_Surfcae?.let { surface -> 196 | c2_reader_play(surface) 197 | } 198 | } 199 | 200 | fun c1_camera_play() { 201 | if (original_c1_preview_SurfaceTexture != null) { 202 | original_preview_Surface = Surface(original_c1_preview_SurfaceTexture) 203 | if(original_preview_Surface!!.isValid == true){ 204 | handleMediaPlayer(original_preview_Surface!!) 205 | } 206 | } 207 | 208 | if(oriHolder?.surface != null){ 209 | original_preview_Surface = oriHolder?.surface 210 | if(original_preview_Surface!!.isValid == true){ 211 | handleMediaPlayer(original_preview_Surface!!) 212 | } 213 | } 214 | 215 | c2_reader_Surfcae?.let { surface -> 216 | c2_reader_play(surface) 217 | } 218 | } 219 | 220 | fun c2_reader_play(c2_reader_Surfcae:Surface){ 221 | if(c2_reader_Surfcae == copyReaderSurface){ 222 | return 223 | } 224 | 225 | copyReaderSurface = c2_reader_Surfcae 226 | 227 | if(c2_hw_decode_obj != null){ 228 | c2_hw_decode_obj!!.stopDecode() 229 | c2_hw_decode_obj = null 230 | } 231 | 232 | c2_hw_decode_obj = VideoToFrames() 233 | try { 234 | val videoUrl = "content://com.wangyiheng.vcamsx.videoprovider" 235 | val videoPathUri = Uri.parse(videoUrl) 236 | c2_hw_decode_obj!!.setSaveFrames(OutputImageFormat.NV21) 237 | c2_hw_decode_obj!!.set_surface(c2_reader_Surfcae) 238 | c2_hw_decode_obj!!.decode(videoPathUri) 239 | }catch (e:Exception){ 240 | Log.d("dbb",e.toString()) 241 | } 242 | } 243 | 244 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/VideoProvider.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.database.Cursor 6 | import android.database.MatrixCursor 7 | import android.net.Uri 8 | import android.os.ParcelFileDescriptor 9 | import android.util.Log 10 | import com.wangyiheng.vcamsx.R 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | import java.io.File 14 | import java.io.FileNotFoundException 15 | import java.io.FileOutputStream 16 | import java.io.IOException 17 | 18 | class VideoProvider : ContentProvider(), KoinComponent { 19 | val infoManager by inject() 20 | 21 | // override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { 22 | // val readerContent = extractContent(uri.toString()) 23 | // Log.d("vcamsx", "内容"+readerContent) 24 | // // 获取外部文件目录 25 | // val externalFilesDir = context?.getExternalFilesDir(null)?.absolutePath ?: return null 26 | // 27 | // // 创建一个指向 "copied_video.mp4" 的文件对象 28 | // val vcamsxFile = File(externalFilesDir, "copied_video.mp4") 29 | // 30 | // // 检查文件是否存在,如果不存在,则从资源中复制 31 | // if (!vcamsxFile.exists()) { 32 | // try { 33 | // // 使用 try-with-resources 语句确保资源被正确关闭 34 | // context?.resources?.openRawResource(R.raw.vcamsx)?.use { inputStream -> 35 | // FileOutputStream(vcamsxFile).use { fileOutputStream -> 36 | // inputStream.copyTo(fileOutputStream) 37 | // } 38 | // } 39 | // } catch (e: IOException) { 40 | // e.printStackTrace() 41 | // return null 42 | // } 43 | // } 44 | // // 返回文件的 ParcelFileDescriptor,设置为只读模式 45 | // return ParcelFileDescriptor.open(vcamsxFile, ParcelFileDescriptor.MODE_READ_ONLY) 46 | // } 47 | 48 | override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { 49 | val videoInfo = infoManager.getVideoInfo() 50 | val url = videoInfo!!.videoUrl 51 | val fixedUri = Uri.parse(url) 52 | 53 | return try { 54 | // 直接使用ContentResolver打开固定URI指向的文件的ParcelFileDescriptor 55 | context.contentResolver.openFileDescriptor(fixedUri, mode) 56 | } catch (e: Exception) { // 捕获所有异常,包括FileNotFoundException 57 | Log.e("Error", "打开文件失败: ${e.message}") 58 | null 59 | } 60 | } 61 | 62 | 63 | override fun onCreate(): Boolean { 64 | // 初始化内容提供器 65 | return true 66 | } 67 | 68 | fun extractContent(url: String): String { 69 | val prefix = "com.wangyiheng.vcamsx.videoprovider/" 70 | val index = url.indexOf(prefix) 71 | 72 | return if (index != -1) { 73 | url.substring(index + prefix.length) 74 | } else { 75 | "" 76 | } 77 | } 78 | 79 | override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor { 80 | // 创建MatrixCursor 81 | val cursor = MatrixCursor(arrayOf("_id", "display_name", "size", "date_modified","file")) 82 | val path = context?.getExternalFilesDir(null)!!.absolutePath 83 | val file = File(path, "advancedModeMovies/654e1835b70883406c4640c3/caibi_60.mp4") 84 | // 获取视频文件夹路径 85 | cursor.addRow(arrayOf(0, file.name, file.length(), file.lastModified(),file)) 86 | 87 | return cursor 88 | } 89 | 90 | // 其他方法根据需要实现,这里为了简单起见,我们留空 91 | override fun getType(uri: Uri): String? = null 92 | override fun insert(uri: Uri, values: ContentValues?): Uri? = null 93 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 94 | override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/wangyiheng/vcamsx/utils/VideoToFrames.kt: -------------------------------------------------------------------------------- 1 | package com.wangyiheng.vcamsx.utils 2 | 3 | import android.content.ContentValues 4 | import android.content.ContentValues.TAG 5 | import android.graphics.* 6 | import android.media.* 7 | import android.net.Uri 8 | import android.util.Log 9 | import android.view.Surface 10 | import com.wangyiheng.vcamsx.MainHook 11 | import com.wangyiheng.vcamsx.MainHook.Companion.context 12 | import de.robv.android.xposed.XposedBridge 13 | import java.io.ByteArrayOutputStream 14 | import java.io.IOException 15 | import java.nio.ByteBuffer 16 | import java.util.concurrent.LinkedBlockingQueue 17 | 18 | class VideoToFrames : Runnable { 19 | 20 | private var stopDecode = false 21 | 22 | private var outputImageFormat: OutputImageFormat? = null 23 | private var videoFilePath: Any? = null 24 | private var childThread: Thread? = null 25 | private var throwable: Throwable? = null // 定义 throwable 变量 26 | private val decodeColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible 27 | private var play_surf: Surface? = null 28 | private val DEFAULT_TIMEOUT_US: Long = 10000 29 | private val callback: Callback? = null 30 | private val mQueue: LinkedBlockingQueue? = null 31 | private val COLOR_FormatI420 = 1 32 | private val COLOR_FormatNV21 = 2 33 | private val VERBOSE = false 34 | fun stopDecode() { 35 | stopDecode = true 36 | } 37 | 38 | interface Callback { 39 | fun onFinishDecode() 40 | fun onDecodeFrame(index: Int) 41 | } 42 | 43 | @Throws(IOException::class) 44 | fun setSaveFrames(imageFormat: OutputImageFormat) { 45 | outputImageFormat = imageFormat 46 | } 47 | 48 | fun set_surface(player_surface:Surface){ 49 | if(player_surface != null){ 50 | play_surf = player_surface 51 | } 52 | } 53 | 54 | fun decode(videoFilePath: Any) { 55 | this.videoFilePath = videoFilePath 56 | if (childThread == null) { 57 | childThread = Thread(this, "decode").apply { 58 | start() 59 | } 60 | throwable?.let { throw it } 61 | } 62 | } 63 | 64 | override fun run() { 65 | try { 66 | Log.d("vcamsxtoast","------开始解码------") 67 | videoFilePath?.let { videoDecode(it) } 68 | } catch (t: Throwable) { 69 | throwable = t 70 | } 71 | } 72 | 73 | private fun videoDecode(videoPath: Any) { 74 | var extractor: MediaExtractor? = null 75 | var decoder: MediaCodec? = null 76 | 77 | try { 78 | extractor = MediaExtractor().apply { 79 | when (videoPath) { 80 | is String -> setDataSource(videoPath) // 当参数是 String 时 81 | is Uri -> context?.let { setDataSource(it, videoPath, null) } // 当参数是 Uri 时 82 | else -> throw IllegalArgumentException("Unsupported video path type") 83 | } 84 | } 85 | val trackIndex = selectTrack(extractor) 86 | if (trackIndex < 0) { 87 | XposedBridge.log("​``【oaicite:5】``​​``【oaicite:4】``​No video track found in $videoFilePath") 88 | } 89 | extractor.selectTrack(trackIndex) 90 | val mediaFormat = extractor.getTrackFormat(trackIndex) 91 | val mime = mediaFormat.getString(MediaFormat.KEY_MIME) 92 | decoder = MediaCodec.createDecoderByType(mime!!) 93 | showSupportedColorFormat(decoder.codecInfo.getCapabilitiesForType(mime)) 94 | if (isColorFormatSupported(decodeColorFormat, decoder.codecInfo.getCapabilitiesForType(mime))) { 95 | mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat) 96 | XposedBridge.log("​``【oaicite:3】``​​``【oaicite:2】``​set decode color format to type $decodeColorFormat") 97 | } else { 98 | Log.i(ContentValues.TAG, "unable to set decode color format, color format type $decodeColorFormat not supported") 99 | XposedBridge.log("​``【oaicite:1】``​​``【oaicite:0】``​unable to set decode color format, color format type $decodeColorFormat not supported") 100 | } 101 | decodeFramesToImage(decoder, extractor, mediaFormat) 102 | decoder.stop() 103 | while (!stopDecode) { 104 | extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC) 105 | decodeFramesToImage(decoder, extractor, mediaFormat) 106 | decoder.stop() 107 | } 108 | } catch (e: Exception) { 109 | // Handle exceptions 110 | } finally { 111 | if(decoder != null) { 112 | decoder.stop() 113 | decoder.release() 114 | decoder = null 115 | } 116 | if(extractor != null) { 117 | extractor.release() 118 | extractor = null 119 | } 120 | } 121 | } 122 | private fun selectTrack(extractor: MediaExtractor): Int { 123 | val numTracks = extractor.trackCount 124 | for (i in 0 until numTracks) { 125 | val format = extractor.getTrackFormat(i) 126 | val mime = format.getString(MediaFormat.KEY_MIME) 127 | if (mime!!.startsWith("video/")) { 128 | return i 129 | } 130 | } 131 | return -1 132 | } 133 | 134 | private fun showSupportedColorFormat(caps: MediaCodecInfo.CodecCapabilities) { 135 | for (c in caps.colorFormats) { 136 | print("$c\t") 137 | } 138 | println() 139 | } 140 | 141 | fun isColorFormatSupported(colorFormat: Int, caps: MediaCodecInfo.CodecCapabilities): Boolean { 142 | return caps.colorFormats.any { it == colorFormat } 143 | } 144 | 145 | private fun decodeFramesToImage(decoder: MediaCodec, extractor: MediaExtractor, mediaFormat: MediaFormat) { 146 | var isFirst = false 147 | var startWhen: Long = 0 148 | val info = MediaCodec.BufferInfo() 149 | decoder.configure(mediaFormat, play_surf, null, 0) 150 | var sawInputEOS = false 151 | var sawOutputEOS = false 152 | decoder.start() 153 | var outputFrameCount = 0 154 | 155 | while (!sawOutputEOS && !stopDecode) { 156 | if (!sawInputEOS) { 157 | val inputBufferId = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US) 158 | if (inputBufferId >= 0) { 159 | val inputBuffer = decoder.getInputBuffer(inputBufferId) 160 | val sampleSize = extractor.readSampleData(inputBuffer!!, 0) 161 | if (sampleSize < 0) { 162 | decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM) 163 | sawInputEOS = true 164 | } else { 165 | val presentationTimeUs = extractor.sampleTime 166 | decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0) 167 | extractor.advance() 168 | } 169 | } 170 | } 171 | 172 | val outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US) 173 | if (outputBufferId >= 0) { 174 | if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { 175 | sawOutputEOS = true 176 | } 177 | val doRender = info.size != 0 178 | if (doRender) { 179 | outputFrameCount++ 180 | callback?.onDecodeFrame(outputFrameCount) 181 | 182 | if (!isFirst) { 183 | startWhen = System.currentTimeMillis() 184 | isFirst = true 185 | } 186 | if (play_surf == null) { 187 | val image = decoder.getOutputImage(outputBufferId) 188 | val buffer = image!!.planes[0].buffer 189 | val arr = ByteArray(buffer.remaining()) 190 | buffer.get(arr) 191 | mQueue?.put(arr) 192 | 193 | if (outputImageFormat != null) { 194 | // MainHook.data_buffer =bitmapToYUV( imageToBitmap(image)) 195 | MainHook.data_buffer = getDataFromImage(image) 196 | } 197 | image.close() 198 | } 199 | 200 | val sleepTime = info.presentationTimeUs / 1000 - (System.currentTimeMillis() - startWhen) 201 | if (sleepTime > 0) { 202 | try { 203 | Thread.sleep(sleepTime) 204 | } catch (e: InterruptedException) { 205 | XposedBridge.log("​``【oaicite:1】``​" + e.toString()) 206 | XposedBridge.log("​``【oaicite:0】``​线程延迟出错") 207 | } 208 | } 209 | decoder.releaseOutputBuffer(outputBufferId, true) 210 | } 211 | } 212 | } 213 | callback?.onFinishDecode() 214 | } 215 | 216 | fun logImageFormat(image: Image) { 217 | val format = image.format 218 | val formatString = when (format) { 219 | ImageFormat.YUV_420_888 -> "YUV_420_888" 220 | ImageFormat.JPEG -> "JPEG" 221 | ImageFormat.RAW_SENSOR -> "RAW_SENSOR" 222 | ImageFormat.NV21 -> "NV21" 223 | ImageFormat.YV12 -> "YV12" 224 | ImageFormat.RAW_PRIVATE -> "RAW_PRIVATE" 225 | ImageFormat.RAW10 -> "RAW10" 226 | ImageFormat.RAW12 -> "RAW12" 227 | ImageFormat.DEPTH_JPEG -> "DEPTH_JPEG" 228 | ImageFormat.DEPTH16 -> "DEPTH16" 229 | ImageFormat.DEPTH_POINT_CLOUD -> "DEPTH_POINT_CLOUD" 230 | // 添加更多格式根据需要 231 | else -> "Unknown format: $format" 232 | } 233 | Log.d("vcamsx", "Image format is $formatString") 234 | } 235 | 236 | fun imageToBitmap(image: Image): Bitmap { 237 | Log.d("vcamsx",image.format.toString()) 238 | val yBuffer = image.planes[0].buffer // Y 239 | val uBuffer = image.planes[1].buffer // U 240 | val vBuffer = image.planes[2].buffer // V 241 | 242 | val ySize = yBuffer.remaining() 243 | val uSize = uBuffer.remaining() 244 | val vSize = vBuffer.remaining() 245 | 246 | val nv21 = ByteArray(ySize + uSize + vSize) 247 | 248 | // YUV_420_888数据转NV21 249 | yBuffer.get(nv21, 0, ySize) 250 | vBuffer.get(nv21, ySize, vSize) 251 | uBuffer.get(nv21, ySize + vSize, uSize) 252 | 253 | val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null) 254 | val out = ByteArrayOutputStream() 255 | yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 75, out) 256 | 257 | val imageBytes = out.toByteArray() 258 | return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) 259 | } 260 | 261 | // fun bitmapToByteArray(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int): ByteArray { 262 | // val stream = ByteArrayOutputStream() 263 | // bitmap.compress(format, quality, stream) 264 | // return stream.toByteArray() 265 | // } 266 | 267 | fun bitmapToYUV(bitmap: Bitmap): ByteArray { 268 | val width = bitmap.width 269 | val height = bitmap.height 270 | val intArray = IntArray(width * height) 271 | bitmap.getPixels(intArray, 0, width, 0, 0, width, height) 272 | 273 | val yuvArray = ByteArray(width * height * 3) 274 | 275 | var index = 0 276 | intArray.forEach { color -> 277 | val r = (color shr 16) and 0xFF 278 | val g = (color shr 8) and 0xFF 279 | val b = color and 0xFF 280 | 281 | // Apply the RGB to YUV formula 282 | val y = (0.257 * r) + (0.504 * g) + (0.098 * b) + 16 283 | val u = -(0.148 * r) - (0.291 * g) + (0.439 * b) + 128 284 | val v = (0.439 * r) - (0.368 * g) - (0.071 * b) + 128 285 | 286 | // Assuming the YUV format is YUV444, store each Y, U, and V value sequentially 287 | yuvArray[index++] = y.toInt().toByte() 288 | yuvArray[index++] = u.toInt().toByte() 289 | yuvArray[index++] = v.toInt().toByte() 290 | } 291 | 292 | return yuvArray 293 | } 294 | // private fun getDataFromImage(image: Image, colorFormat: Int): ByteArray { 295 | // if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) { 296 | // throw IllegalArgumentException("only support COLOR_FormatI420 and COLOR_FormatNV21") 297 | // } 298 | // 299 | // logImageFormat(image) 300 | // if (!isImageFormatSupported(image)) { 301 | // throw RuntimeException("can't convert Image to byte array, format ${image.format}") 302 | // } 303 | // 304 | // 305 | // val crop = image.cropRect 306 | // val format = image.format 307 | // val width = crop.width() 308 | // val height = crop.height() 309 | // val planes = image.planes 310 | // val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8) 311 | // val rowData = ByteArray(planes[0].rowStride) 312 | // 313 | // var channelOffset = 0 314 | // var outputStride = 1 315 | // for (i in planes.indices) { 316 | // when (i) { 317 | // 0 -> { 318 | // channelOffset = 0 319 | // outputStride = 1 320 | // } 321 | // 1 -> { 322 | // channelOffset = if (colorFormat == COLOR_FormatI420) width * height else width * height + 1 323 | // outputStride = 2 324 | // } 325 | // 2 -> { 326 | // channelOffset = if (colorFormat == COLOR_FormatI420) (width * height * 1.25).toInt() else width * height 327 | // outputStride = 2 328 | // } 329 | // } 330 | // val buffer = planes[i].buffer 331 | // val rowStride = planes[i].rowStride 332 | // val pixelStride = planes[i].pixelStride 333 | // 334 | // val shift = if (i == 0) 0 else 1 335 | // val w = width shr shift 336 | // val h = height shr shift 337 | // buffer.position(rowStride * (crop.top shr shift) + pixelStride * (crop.left shr shift)) 338 | // for (row in 0 until h) { 339 | // val length: Int 340 | // if (pixelStride == 1 && outputStride == 1) { 341 | // length = w 342 | // buffer.get(data, channelOffset, length) 343 | // channelOffset += length 344 | // } else { 345 | // length = (w - 1) * pixelStride + 1 346 | // buffer.get(rowData, 0, length) 347 | // for (col in 0 until w) { 348 | // data[channelOffset] = rowData[col * pixelStride] 349 | // channelOffset += outputStride 350 | // } 351 | // } 352 | // if (row < h - 1) { 353 | // buffer.position(buffer.position() + rowStride - length) 354 | // } 355 | // } 356 | // } 357 | // return data 358 | // } 359 | 360 | private fun getDataFromImage(image: Image): ByteArray { 361 | 362 | logImageFormat(image) 363 | if (!isImageFormatSupported(image)) { 364 | throw RuntimeException("can't convert Image to byte array, format ${image.format}") 365 | } 366 | 367 | val crop = image.cropRect 368 | val width = crop.width() 369 | val height = crop.height() 370 | val planes = image.planes 371 | val pixelFormatBits = ImageFormat.getBitsPerPixel(image.format) 372 | val data = ByteArray(width * height * pixelFormatBits / 8) 373 | val rowData = ByteArray(planes[0].rowStride) 374 | 375 | fun copyPlaneData(planeIndex: Int, buffer: ByteBuffer, rowStride: Int, pixelStride: Int, width: Int, height: Int, channelOffset: Int, outputStride: Int) { 376 | var outputOffset = channelOffset 377 | buffer.position(rowStride * (crop.top / 2) + pixelStride * (crop.left / 2)) 378 | for (row in 0 until height) { 379 | val length = if (pixelStride == 1 && outputStride == 1) { 380 | width 381 | } else { 382 | (width - 1) * pixelStride + 1 383 | } 384 | if (length == rowStride && outputStride == 1) { 385 | buffer.get(data, outputOffset, length) 386 | outputOffset += length 387 | } else { 388 | buffer.get(rowData, 0, length) 389 | for (col in 0 until width) { 390 | data[outputOffset] = rowData[col * pixelStride] 391 | outputOffset += outputStride 392 | } 393 | } 394 | if (row < height - 1) { 395 | buffer.position(buffer.position() + rowStride - length) 396 | } 397 | } 398 | } 399 | 400 | var channelOffset = 0 401 | val uvHeight = height / 2 402 | val uvWidth = width / 2 403 | 404 | // Y Plane 405 | copyPlaneData(0, planes[0].buffer, planes[0].rowStride, planes[0].pixelStride, width, height, channelOffset, 1) 406 | channelOffset += width * height 407 | 408 | 409 | copyPlaneData(1, planes[2].buffer, planes[2].rowStride, planes[2].pixelStride, uvWidth, uvHeight, channelOffset, 2) 410 | copyPlaneData(2, planes[1].buffer, planes[1].rowStride, planes[1].pixelStride, uvWidth, uvHeight, channelOffset + 1, 2) 411 | 412 | 413 | return data 414 | } 415 | 416 | 417 | 418 | private fun isImageFormatSupported(image: Image): Boolean { 419 | val format = image.format 420 | Log.d("vcamsx", "format$format") 421 | return when (format) { 422 | ImageFormat.YUV_420_888, ImageFormat.NV21, ImageFormat.YV12 -> true 423 | else -> false 424 | } 425 | } 426 | } 427 | 428 | 429 | enum class OutputImageFormat(val friendlyName: String) { 430 | I420("I420"), 431 | NV21("NV21"), 432 | JPEG("JPEG"); 433 | 434 | override fun toString() = friendlyName 435 | } 436 | 437 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-hdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-mdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/mipmap-xxxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/raw/vcamsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/app/src/main/res/raw/vcamsx.mp4 -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VCAMSX 3 | VCAMSX保持运行中 4 | 点击进入app 5 | 前台服务通知 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |