├── .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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/highlightedFiles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
20 |
--------------------------------------------------------------------------------
/app/src/test/java/com/wangyiheng/vcamsx/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.wangyiheng.vcamsx
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.1.0" apply false
4 | id("org.jetbrains.kotlin.android") version "1.8.10" apply false
5 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeepMakerAi/VCAMSX/6fd800a41dba064d6f7e643798e46407c76c83e2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Nov 17 16:03:46 HKT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | gradlePluginPortal()
13 | mavenCentral()
14 | maven("https://api.xposed.info/")
15 | maven ("https://maven.pkg.github.com/GCX-HCI/tray" )
16 | }
17 | }
18 |
19 | rootProject.name = "VCAMSX"
20 | include(":app")
21 |
--------------------------------------------------------------------------------